From 2b44242c6e9717b5ccaf40bd80d847692cceb7cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliz=C3=A9=20Debray?= <33580481+alizedebray@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:07:53 +0200 Subject: [PATCH] feat(docs): add toolbar to switch theme, channel, and mode (#3528) --- .changeset/empty-islands-kneel.md | 5 + .../.storybook/addons/addons.scss | 39 ++++ .../addons/styles-switcher/StylesSwitcher.tsx | 172 ++++++++++++++++++ .../addons/styles-switcher/register.tsx | 16 ++ .../version-switcher/VersionSwitcher.tsx | 4 +- .../addons/version-switcher/register.tsx | 2 +- .../version-switcher/version-switcher.scss | 28 --- .../helpers/open-full-screen-demo.ts | 34 +++- packages/documentation/.storybook/main.ts | 7 + packages/documentation/.storybook/manager.ts | 12 +- packages/documentation/.storybook/preview.ts | 8 +- .../.storybook/styles/manager.scss | 1 + .../.storybook/styles/preview.scss | 1 - 13 files changed, 291 insertions(+), 38 deletions(-) create mode 100644 .changeset/empty-islands-kneel.md create mode 100644 packages/documentation/.storybook/addons/addons.scss create mode 100644 packages/documentation/.storybook/addons/styles-switcher/StylesSwitcher.tsx create mode 100644 packages/documentation/.storybook/addons/styles-switcher/register.tsx diff --git a/.changeset/empty-islands-kneel.md b/.changeset/empty-islands-kneel.md new file mode 100644 index 0000000000..f6c70cfcaf --- /dev/null +++ b/.changeset/empty-islands-kneel.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-documentation': minor +--- + +Added a toolbar for switching the theme, channel, and mode of all stories. diff --git a/packages/documentation/.storybook/addons/addons.scss b/packages/documentation/.storybook/addons/addons.scss new file mode 100644 index 0000000000..b34b9b2c90 --- /dev/null +++ b/packages/documentation/.storybook/addons/addons.scss @@ -0,0 +1,39 @@ +@use '@swisspost/design-system-styles/core' as post; + +.addon-dropdown { + min-width: 12rem; + display: flex; + flex-flow: column nowrap; + gap: post.$size-line; + position: absolute; + top: -5px; + right: 0; + padding: post.$size-mini; + background-color: var(--post-light); + border: post.$border-width solid post.$border-color; + border-radius: post.$border-radius; + font-size: post.$font-size-sm; + + .addon-dropdown__item { + display: block; + padding: post.$size-mini post.$size-small-regular; + border-radius: post.$border-radius-sm; + text-decoration: none; + color: post.$body-color !important; + + &:hover { + background-color: post.$gray-10; + } + + &.active { + background-color: post.$yellow; + } + } +} + +.addon-button { + post-icon { + font-size: post.$font-size-20; + margin-inline-end: post.$size-line; + } +} diff --git a/packages/documentation/.storybook/addons/styles-switcher/StylesSwitcher.tsx b/packages/documentation/.storybook/addons/styles-switcher/StylesSwitcher.tsx new file mode 100644 index 0000000000..666550feac --- /dev/null +++ b/packages/documentation/.storybook/addons/styles-switcher/StylesSwitcher.tsx @@ -0,0 +1,172 @@ +import React, { useEffect, useState } from 'react'; +import { IconButton, WithTooltip } from '@storybook/components'; + +const STYLESHEET_ID = 'preview-stylesheet'; +const STORAGE_KEY_PREFIX = 'swisspost-documentation'; +const THEMES = ['Post']; +const CHANNELS = ['External', 'Internal']; +const MODES = ['Light', 'Dark']; + +function StylesSwitcher() { + const [currentTheme, setCurrentTheme] = useState( + localStorage.getItem(`${STORAGE_KEY_PREFIX}-theme`) || THEMES[0], + ); + const [currentChannel, setCurrentChannel] = useState( + localStorage.getItem(`${STORAGE_KEY_PREFIX}-channel`) || CHANNELS[0], + ); + const [currentMode, setCurrentMode] = useState( + localStorage.getItem(`${STORAGE_KEY_PREFIX}-mode`) || MODES[0], + ); + + /** + * Sets the 'data-color-mode' attribute and preview stylesheet when the addon initializes + */ + useEffect(() => { + setPreviewStylesheet(); + setDataColorModeAttribute(); + }); + + /** + * Sets the stylesheet matching the selected theme and channel in the preview document head + */ + const setPreviewStylesheet = () => { + const preview = getPreviewDocument(); + const previewHead = preview && preview.querySelector('head'); + + if (!previewHead) return; + + let stylesheetLink = previewHead.querySelector(`#${STYLESHEET_ID}`); + + if (!stylesheetLink) { + stylesheetLink = document.createElement('link'); + stylesheetLink.setAttribute('rel', 'stylesheet'); + stylesheetLink.setAttribute('id', STYLESHEET_ID); + previewHead.appendChild(stylesheetLink); + } + + stylesheetLink.setAttribute( + 'href', + `/styles/${currentTheme.toLowerCase()}-${currentChannel.toLowerCase()}.css`, + ); + }; + + /** + * Sets the 'data-color-mode' attribute of the preview body to match the selected mode + */ + const setDataColorModeAttribute = () => { + const preview = getPreviewDocument(); + if (!preview) return; + + const mode = currentMode.toLowerCase(); + const storyContainers = preview.querySelectorAll('.sbdocs-preview, .sb-main-padded'); + storyContainers.forEach(storyContainer => { + storyContainer.classList.remove('bg-light', 'bg-dark'); + storyContainer.classList.add(`bg-${mode}`); + storyContainer.setAttribute('data-color-mode', mode); + }); + }; + + /** + * Returns the Document contained in the preview iframe + */ + const getPreviewDocument = (): Document | undefined => { + const preview = document.querySelector('#storybook-preview-iframe'); + return preview && (preview as HTMLIFrameElement).contentWindow.document; + }; + + /** + * Applies selected theme and registers it to the local storage + */ + const applyTheme = (theme: string) => { + setCurrentTheme(theme); + localStorage.setItem(`${STORAGE_KEY_PREFIX}-theme`, theme); + }; + + /** + * Applies selected channel and registers it to the local storage + */ + const applyChannel = (channel: string) => { + setCurrentChannel(channel); + localStorage.setItem(`${STORAGE_KEY_PREFIX}-channel`, channel); + }; + + /** + * Applies selected mode and registers it to the local storage + */ + const applyMode = (mode: string) => { + setCurrentMode(mode); + localStorage.setItem(`${STORAGE_KEY_PREFIX}-mode`, mode); + }; + + return ( + <> + {/* Theme dropdown */} + + {THEMES.map(theme => ( + applyTheme(theme)} + > + {theme} + + ))} + + } + > + Theme: {currentTheme} + + + {/* Channel dropdown */} + + {CHANNELS.map(channel => ( + applyChannel(channel)} + > + {channel} + + ))} + + } + > + Chanel: {currentChannel} + + + {/* Mode dropdown */} + + {MODES.map(mode => ( + applyMode(mode)} + > + {mode} + + ))} + + } + > + Mode: {currentMode} + + + ); +} + +export default StylesSwitcher; diff --git a/packages/documentation/.storybook/addons/styles-switcher/register.tsx b/packages/documentation/.storybook/addons/styles-switcher/register.tsx new file mode 100644 index 0000000000..2134d9336e --- /dev/null +++ b/packages/documentation/.storybook/addons/styles-switcher/register.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { addons, types } from '@storybook/manager-api'; +import StylesSwitcher from './StylesSwitcher'; + +const ADDON_ID = 'postStylesSwitcher'; + +addons.register(ADDON_ID, () => { + addons.add(ADDON_ID, { + title: 'Switch the documentation styles', + type: types.TOOL, + render: () => { + return ; + }, + }); +}); diff --git a/packages/documentation/.storybook/addons/version-switcher/VersionSwitcher.tsx b/packages/documentation/.storybook/addons/version-switcher/VersionSwitcher.tsx index 53ca7fc967..ad07d1df0b 100644 --- a/packages/documentation/.storybook/addons/version-switcher/VersionSwitcher.tsx +++ b/packages/documentation/.storybook/addons/version-switcher/VersionSwitcher.tsx @@ -50,7 +50,7 @@ function VersionSwitcher() { closeOnOutsideClick tooltip={() => ( <> -
+
{versions.map(version => { const isActive = getVersion(version.version ?? '', 'major') === CURRENT_MAJOR_VERSION @@ -66,7 +66,7 @@ function VersionSwitcher() { return ( c).join(' ')} + className={['addon-dropdown__item', isActive].filter(c => c).join(' ')} key={version.title} href={version.url} > diff --git a/packages/documentation/.storybook/addons/version-switcher/register.tsx b/packages/documentation/.storybook/addons/version-switcher/register.tsx index c84ba118fc..e31d6db261 100644 --- a/packages/documentation/.storybook/addons/version-switcher/register.tsx +++ b/packages/documentation/.storybook/addons/version-switcher/register.tsx @@ -9,7 +9,7 @@ addons.register(ADDON_ID, () => { addons.add(ADDON_ID, { title: 'Switch to another version', type: types.TOOLEXTRA, - match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), + match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(docs)$/)), render: () => { return ; }, diff --git a/packages/documentation/.storybook/addons/version-switcher/version-switcher.scss b/packages/documentation/.storybook/addons/version-switcher/version-switcher.scss index 366e947df5..68ad4c73d5 100644 --- a/packages/documentation/.storybook/addons/version-switcher/version-switcher.scss +++ b/packages/documentation/.storybook/addons/version-switcher/version-switcher.scss @@ -11,34 +11,6 @@ } .version-switcher__dropdown { - display: flex; - flex-flow: column nowrap; - gap: post.$size-line; - position: absolute; - top: -5px; - right: 0; - padding: post.$size-mini; - background-color: var(--post-light); - border: post.$border-width solid post.$border-color; - border-radius: post.$border-radius; - font-size: post.$font-size-sm; - - .dropdown__item { - display: block; - padding: post.$size-mini post.$size-small-regular; - border-radius: post.$border-radius-sm; - text-decoration: none; - color: inherit; - - &:hover { - background-color: post.$gray-10; - } - - &.active { - background-color: post.$yellow; - } - } - .item__title { display: block; } diff --git a/packages/documentation/.storybook/helpers/open-full-screen-demo.ts b/packages/documentation/.storybook/helpers/open-full-screen-demo.ts index 890aa1a08b..2e6689079d 100644 --- a/packages/documentation/.storybook/helpers/open-full-screen-demo.ts +++ b/packages/documentation/.storybook/helpers/open-full-screen-demo.ts @@ -1,11 +1,37 @@ +import { StoryContext, StoryFn } from '@storybook/web-components'; +import { html } from 'lit'; + +export const fullScreenUrlDecorator = (story: StoryFn, context: StoryContext) => { + const { allArgs, initialArgs, id } = context; + + const args = Object.entries(allArgs) + .map(([argKey, argValue]) => { + const initialValue = initialArgs[argKey]; + if (argValue === initialValue) return null; + + const separator = typeof argValue === 'string' || typeof argValue === 'number' ? ':' : ':!'; + const value = typeof argValue === 'string' ? encodeURI(argValue) : argValue; + return `${argKey}${separator}${value}`; + }) + .filter(arg => !!arg) + .join(';'); + + let storyURL = `/?path=/story/${id}&full=true`; + if (args.length) storyURL += `&args=${args}`; + + return html` + + ${story(context.args, context)} + `; +}; + export const openFullScreenDemo = (e: Event) => { const target = e.target as HTMLButtonElement; const canvas = target.closest('.docs-story'); - const story = canvas?.querySelector('.sb-story'); - const iframeId = story?.id?.replace('story--', ''); + const storyURL = canvas && canvas.querySelector('.storyURL'); - if (iframeId) { - window.open(`/iframe.html?id=${iframeId}`, '_blank'); + if (storyURL) { + window.open(storyURL.textContent, '_blank'); } else { alert('The full screen demo is not available.'); } diff --git a/packages/documentation/.storybook/main.ts b/packages/documentation/.storybook/main.ts index 7eab581511..842e9f7380 100644 --- a/packages/documentation/.storybook/main.ts +++ b/packages/documentation/.storybook/main.ts @@ -23,6 +23,8 @@ const config: StorybookConfig = { highlight: false, outline: false, docs: false, + measure: false, + viewport: false, }, }, { @@ -52,12 +54,17 @@ const config: StorybookConfig = { '@storybook/addon-links', '@kurbar/storybook-addon-docs-stencil', './addons/version-switcher/register', + './addons/styles-switcher/register', ], staticDirs: [ { from: '../public/assets', to: '/assets', }, + { + from: '../node_modules/@swisspost/design-system-styles', + to: '/styles', + }, '../public', '../node_modules/@swisspost/design-system-icons/public', ], diff --git a/packages/documentation/.storybook/manager.ts b/packages/documentation/.storybook/manager.ts index 7953d746ef..69559b19e2 100644 --- a/packages/documentation/.storybook/manager.ts +++ b/packages/documentation/.storybook/manager.ts @@ -1,6 +1,6 @@ import { addons } from '@storybook/manager-api'; -import themes from './styles/themes'; import { defineCustomElement as definePostIcon } from '@swisspost/design-system-components/dist/components/post-icon.js'; +import themes from './styles/themes'; definePostIcon(); @@ -12,4 +12,14 @@ addons.setConfig({ sidebar: { collapsedRoots: [], }, + + // the toolbar is only visible in the fill screen view after clicking "View full screen" on a story + toolbar: { + remount: { hidden: true }, // controls the visibility of the "Remount component" button + zoom: { hidden: true }, // controls the visibility of the "Zoom in", "Zoom out", and "Reset zoom" buttons + addons: { hidden: true }, // controls the visibility of the "Show addons" button + fullscreen: { hidden: true }, // controls the visibility of the "Go full screen" button + eject: { hidden: true }, // controls the visibility of the "Open canvas in new tab" button + copy: { hidden: true }, // controls the visibility of the "Copy canvas link" button + }, }); diff --git a/packages/documentation/.storybook/preview.ts b/packages/documentation/.storybook/preview.ts index 521f730499..abef0ed326 100644 --- a/packages/documentation/.storybook/preview.ts +++ b/packages/documentation/.storybook/preview.ts @@ -2,7 +2,12 @@ import type { Preview } from '@storybook/web-components'; import { extractArgTypes, extractComponentDescription } from '@kurbar/storybook-addon-docs-stencil'; import { format } from 'prettier'; import DocsLayout from './blocks/layout/layout'; -import { openFullScreenDemo, prettierOptions, resetComponents } from './helpers'; +import { + fullScreenUrlDecorator, + openFullScreenDemo, + prettierOptions, + resetComponents, +} from './helpers'; import './helpers/register-web-components'; import './addons/cypress-storybook/client'; @@ -16,6 +21,7 @@ SyntaxHighlighter.registerLanguage('scss', scss); export const SourceDarkMode = true; const preview: Preview = { + decorators: [fullScreenUrlDecorator], parameters: { options: { storySort: { diff --git a/packages/documentation/.storybook/styles/manager.scss b/packages/documentation/.storybook/styles/manager.scss index e13bdbb8e2..7da09d5c1d 100644 --- a/packages/documentation/.storybook/styles/manager.scss +++ b/packages/documentation/.storybook/styles/manager.scss @@ -1,6 +1,7 @@ @use '@swisspost/design-system-styles/components/root'; @use '@swisspost/design-system-styles/mixins/utilities'; @use './components'; +@use '../addons/addons'; @use '../addons/version-switcher/version-switcher'; .sidebar-header { diff --git a/packages/documentation/.storybook/styles/preview.scss b/packages/documentation/.storybook/styles/preview.scss index 8858bc1f13..e8e8a9784f 100644 --- a/packages/documentation/.storybook/styles/preview.scss +++ b/packages/documentation/.storybook/styles/preview.scss @@ -1,5 +1,4 @@ // importing the complete styles package scss -@use '@swisspost/design-system-styles/post-external.scss'; @use '@swisspost/design-system-styles/core.scss' as post; @use '@swisspost/design-system-styles/mixins/utilities'; @use '@swisspost/internet-header/dist/swisspost-internet-header/swisspost-internet-header.css';