diff --git a/.changeset/honest-teachers-fail.md b/.changeset/honest-teachers-fail.md new file mode 100644 index 00000000000..627f933637a --- /dev/null +++ b/.changeset/honest-teachers-fail.md @@ -0,0 +1,5 @@ +--- +"@spectrum-css/preview": minor +--- + +Feature to migrate Storybook to use Vite's builder instead of Webpack. This change reduces the configuration complexity with more built-in features that align with our needs. diff --git a/.changeset/twenty-starfishes-sip.md b/.changeset/twenty-starfishes-sip.md new file mode 100644 index 00000000000..a9127525c32 --- /dev/null +++ b/.changeset/twenty-starfishes-sip.md @@ -0,0 +1,5 @@ +--- +"@spectrum-css/tokens": minor +--- + +This feature adds the custom variables for each context (spectrum and express) to the root-named asset (dist/css/express/global-vars.css) diff --git a/.storybook/assets/typekit.js b/.storybook/assets/typekit.js index db29031a5fc..d71816b7c3f 100644 --- a/.storybook/assets/typekit.js +++ b/.storybook/assets/typekit.js @@ -1,42 +1,52 @@ /* global Typekit */ -// This wrapper prevents loading the font more than once -if (!window.Typekit) { - const kitId = - document.querySelector("[lang]:not([lang=\"en-US\"])") === null - ? "mge7bvf" - : "rok6rmo"; - +window.addEventListener("DOMContentLoaded", () => { const html = document.documentElement; - html.classList.add("wf-loading"); + const root = document.head ?? document.body ?? html; + const isNotEnglish = root.querySelector("[lang]:not([lang=\"en-US\"])"); + + const toggleClass = (state, force = true) => { + if (html) html.classList.toggle(`wf-${state}`, force); + else if (root) root.classList.toggle(`wf-${state}`, force); + }; - const t = setTimeout(function () { - html.classList.remove("wf-loading"); - html.classList.add("wf-inactive"); + const timeout = setTimeout(function () { + toggleClass("loading", false); + toggleClass("inactive"); }, 3000); - const tk = document.createElement("script"); - let d = false; - - // Always load over https - tk.src = "https://use.typekit.net/" + kitId + ".js"; - tk.type = "text/javascript"; - tk.async = "true"; - tk.onload = tk.onreadystatechange = () => { - const a = this.readyState; - if (d || (a && a !== "complete" && a !== "loaded")) return; - - d = true; - clearTimeout(t); - - try { - window.Typekit = Typekit.load({ - kitId, - scriptTimeout: 3000, - }); - } - catch (b) {/* empty */} + const config = { + kitId: isNotEnglish ? "mge7bvf" : "rok6rmo", + scriptTimeout: 3000, + active: () => { + toggleClass("loading"); + }, }; - document.body.appendChild(tk); -} + // This wrapper prevents loading the font more than once + if (window && !window.Typekit) { + let d = false; + let tk = document.querySelector("#typekit"); + + if (!tk) { + tk = document.createElement("script"); + tk.id = "typekit"; + tk.src = `https://use.typekit.net/${config.kitId}.js`; + tk.type = "text/javascript"; + tk.async = "true"; + tk.onload = tk.onreadystatechange = function () { + const readyState = this.readyState; + if (d || (readyState && readyState !== "complete" && readyState !== "loaded")) return; + + d = true; + clearTimeout(timeout); + + try { + window.Typekit = Typekit.load(config); + } + catch (b) {/* empty */} + }; + root.appendChild(tk); + } + } +}); diff --git a/.storybook/decorators/context.js b/.storybook/decorators/context.js index d32cebb0e45..62b502ee8bc 100644 --- a/.storybook/decorators/context.js +++ b/.storybook/decorators/context.js @@ -1,4 +1,5 @@ import { makeDecorator, useEffect } from "@storybook/preview-api"; +import { html } from "lit"; /** * @type import('@storybook/csf').DecoratorFunction @@ -8,7 +9,8 @@ export const withContextWrapper = makeDecorator({ name: "withContextWrapper", parameterName: "context", wrapper: (StoryFn, context) => { - const { args, argTypes, viewMode, id } = context; + const { args = {}, argTypes = {}, viewMode, id, loaded = {} } = context; + const { tokens = {} } = loaded; const getDefaultValue = (type) => { if (!type) return null; @@ -27,10 +29,30 @@ export const withContextWrapper = makeDecorator({ /** @type string */ const scale = args.scale ? args.scale : getDefaultValue(argTypes.scale) ?? "medium"; - const colors = ["light", "dark", "darkest"]; - const scales = ["medium", "large"]; - useEffect(() => { + const toggleStyles = (container, id, styleObj, add = true) => { + if (!container && !id) return; + + let style = container.querySelector(`#${id}`); + const styles = styleObj ? Object.values(styleObj)[0] : undefined; + + if (!add) { + if (style) style.remove(); + return; + } + + if (!style) { + style = document.createElement("style"); + style.id = id; + container.appendChild(style); + } + + if (!style) return; + + if (add && styles) style.innerHTML = styles; + else style.remove(); + }; + let containers = [document.body]; const roots = [ @@ -42,17 +64,34 @@ export const withContextWrapper = makeDecorator({ } for (const container of containers) { + const styleContainer = container.querySelector("#styles-container"); + const globalContainer = styleContainer.querySelector("#global"); + const colorsContainer = styleContainer.querySelector("#colors"); + const scalesContainer = styleContainer.querySelector("#scale"); + const contextContainer = styleContainer.querySelector("#context"); + container.classList.toggle("spectrum", true); container.classList.toggle("spectrum--express", isExpress); - for (const c of colors) { + toggleStyles(globalContainer, "vars-base", tokens?.global?.base, true); + toggleStyles(contextContainer, "vars-base-spectrum", tokens?.spectrum?.base, true); + toggleStyles(contextContainer, "vars-base-express", tokens?.express?.base, isExpress); + + for (const c of ["light", "dark", "darkest"]) { container.classList.toggle(`spectrum--${c}`, c === color); + + toggleStyles(colorsContainer, `vars-${c}`, tokens?.global?.[c], c === color); + toggleStyles(colorsContainer, `vars-${c}-spectrum`, tokens?.spectrum?.[c], c === color); + toggleStyles(colorsContainer, `vars-${c}-express`, tokens?.express?.[c], isExpress && c === color); } - for (const s of scales) { + for (const s of ["medium", "large"]) { container.classList.toggle(`spectrum--${s}`, s === scale); - } + toggleStyles(scalesContainer, `vars-${s}`, tokens?.global?.[s], s === scale); + toggleStyles(scalesContainer, `vars-${s}-spectrum`, tokens?.spectrum?.[s], s === scale); + toggleStyles(scalesContainer, `vars-${s}-express`, tokens?.express?.[s], isExpress && s === scale); + } container.style.removeProperty("background"); const hasStaticElement = container.querySelector(`.${args.rootClass}--staticWhite, .${args.rootClass}--staticBlack, .${args.rootClass}--overBackground`); @@ -65,8 +104,16 @@ export const withContextWrapper = makeDecorator({ } } } - }, [color, scale, isExpress, args.staticColor]); + }, [color, scale, isExpress, tokens, args.staticColor]); - return StoryFn(context); + return html` +
+
+
+
+
+
+ ${StoryFn(context)} + `; }, }); diff --git a/.storybook/guides/deprecation.mdx b/.storybook/guides/deprecation.mdx index 19398303b05..641c429875b 100644 --- a/.storybook/guides/deprecation.mdx +++ b/.storybook/guides/deprecation.mdx @@ -36,12 +36,12 @@ Before removing the component from the codebase, we need to flag the component a a. Edit the title of any exported stories to be prefixed with the `Deprecated` category, i.e., `title: "Quick actions"`. b. Update any local references to point to the package name instead, i.e.,
_Original_:
`import { Template } from "./template";`

_Updated to_:
`import { Template } from "@spectrum-css/quickaction/stories/template.js";`. c. In the parameters section, there are 2 important updates to make: - Add `chromatic: { disableSnapshot: true },` to ensure it no longer runs regression tests. - Update the `status` type to `deprecated`: - `json - parameters: { - chromatic: { disableSnapshot: true }, - status: { type: "deprecated" } - }, - ` + ```json + parameters: { + chromatic: { disableSnapshot: true }, + status: { type: "deprecated" } + }, + ``` 3. Update the status of the component to `Deprecated` in the `*.yml` file. Add any additional migration notes to the `deprecationNotice` keyword. i.e., ```yaml name: Quick actions diff --git a/.storybook/guides/develop.mdx b/.storybook/guides/develop.mdx index 8477a32c3ca..2283e31e56f 100644 --- a/.storybook/guides/develop.mdx +++ b/.storybook/guides/develop.mdx @@ -14,11 +14,11 @@ Welcome to the development and exploration environment for Spectrum CSS, driven This guide is intended to get you up to speed on how we work within Storybook in the Spectrum CSS project. It will cover the following topics: - - [Architecture](#architecture) - - [Writing stories](#writing-stories) - - [Templates](#templates) - - [Testing stories](#testing-stories) - - [Changelog](#changelog) +- [Architecture](#architecture) +- [Writing stories](#writing-stories) +- [Templates](#templates) +- [Testing stories](#testing-stories) +- [Changelog](#changelog) For more general information about how to contribute to the Spectrum CSS project, take a look at our [contribution guidelines on GitHub](https://github.com/adobe/spectrum-css/blob/main/.github/CONTRIBUTING.md). @@ -53,7 +53,7 @@ or in a template.js file: import { Template as Icon } from "@spectrum-css/icon/stories/template.js"; ``` -CSS for other components can be loaded in a story using the package name (rather than the directory path), i.e. `@spectrum-css/toast` vs. `../toast/index.css`. The local version of the package is used regardless but the webpack settings will resolve the pathing for you. +CSS for other components can be loaded in a story using the package name (rather than the directory path), i.e. `@spectrum-css/toast` vs. `../toast/index.css`. The local version of the package is used regardless but the bundler settings will resolve the pathing for you. For template development, we leverage lit and it's utilities to create dynamic HTML that responds to user configurations provided by the Storybook controls. This allows us to create a single source of truth for the component's mark-up and to ensure that the component is being used as intended. @@ -77,7 +77,7 @@ CSS assets will be run through their respective postcss configurations. This mea import "../index.css"; ``` -We are leaning on Storybook's `@storybook/web-components-webpack5` framework configuration as our stories rely on lit for dynamic attribute assignment. +We are leaning on Storybook's `@storybook/web-components-vite` framework configuration as our stories rely on lit for dynamic attribute assignment. ### Add-ons @@ -137,28 +137,28 @@ To import shared types into your story, use the following syntax: ```js import { - isActive, - isDisabled, - isFocused, - isHovered, - isSelected, + isActive, + isDisabled, + isFocused, + isHovered, + isSelected, } from "@spectrum-css/preview/types"; export default { - argTypes: { - isDisabled, - isSelected, - isHovered, - isFocused, - isActive, - }, - args: { - isDisabled: false, - isSelected: false, - isHovered: false, - isFocused: false, - isActive: false, - }, + argTypes: { + isDisabled, + isSelected, + isHovered, + isFocused, + isActive, + }, + args: { + isDisabled: false, + isSelected: false, + isHovered: false, + isFocused: false, + isActive: false, + }, }; ``` @@ -300,7 +300,9 @@ All return values for Template functions should be outputting TemplateResults. S ```js import { html } from "lit"; import { classMap } from "lit/directives/class-map.js"; +import { styleMap } from "lit/directives/style-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; +import { when } from "lit/directives/when.js"; import { Template as Icon } from "@spectrum-css/icon/stories/template.js"; import { Template as Avatar } from "@spectrum-css/avatar/stories/template.js"; import { Template as ClearButton } from "@spectrum-css/clearbutton/stories/template.js"; @@ -308,65 +310,67 @@ import { Template as ClearButton } from "@spectrum-css/clearbutton/stories/templ import "../index.css"; export const Template = ({ - rootClass = "spectrum-Tag", - size = "m", - iconName, - avatarUrl, - label, - isSelected = false, - isEmphasized = false, - isDisabled = false, - isInvalid = false, - hasClearButton = false, - id, - customClasses = [], - ...globals + rootClass = "spectrum-Tag", + size = "m", + iconName, + avatarUrl, + label, + isSelected = false, + isEmphasized = false, + isDisabled = false, + isInvalid = false, + hasClearButton = false, + id, + customClasses = [], + customStyles = {}, }) => { - return html` -
({ ...a, [c]: true }), {}), - })} - id=${ifDefined(id)} - tabindex=${isDisabled ? "-1" : "0"} - style=${ifDefined(styleMap(customStyles))} - > - ${avatarUrl && !iconName - ? Avatar({ - ...globals, - image: avatarUrl, - size: "50", - }) - : ""} ${iconName - ? Icon({ - ...globals, - iconName, - customClasses: [`${rootClass}s-itemIcon`], - }) - : ""} - ${label} - ${hasClearButton - ? ClearButton({ - ...globals, - customClasses: [`${rootClass}-clearButton`], - onclick: (evt) => { - const el = evt.target; - if (!el) return; - - const wrapper = el.closest(rootClass); - wrapper.parentNode.removeChild(wrapper); - }, - }) - : ""} -
- `; + return html` +
({ ...a, [c]: true }), {}), + })} + style=${styleMap(customStyles)} + id=${ifDefined(id)} + tabindex="0" + > + ${when(avatarUrl && !iconName, () => + Avatar({ + image: avatarUrl, + size: "50", + }), + )} ${when(iconName, () => + Icon({ + iconName, + customClasses: [`${rootClass}s-itemIcon`], + }), + )} + ${label} + ${when(hasClearButton, () => + ClearButton({ + customClasses: [`${rootClass}-clearButton`], + onclick: (evt) => { + const el = evt.target; + if (!el) return; + + const wrapper = el.closest(rootClass); + wrapper.parentNode.removeChild(wrapper); + }, + }), + )} +
+ `; }; ``` @@ -376,23 +380,39 @@ Now that your stories are written, we need to add them to our visual regression To optimize snapshot usage, we organize our stories into groups when rendered inside the Chromatic environment. This is done by adding an `window.isChromatic()` check and creating groups of stories (note: do not import the `isChromatic` function directly, there is added functionality we've included when sourcing it from`window`). See example below: +In the `template.js` file: + ```js -const AccordionGroup = ({ customStyles = {}, ...args }) => { - return html` - ${Template(args)} ${when(window.isChromatic(), () => - Template({ - ...args, - customStyles: { "max-inline-size": "300px" }, - }), - )} ${when(window.isChromatic(), () => - Template({ - ...args, - disableAll: true, - }), - )} - `; +export const AccordionGroup = ({ customStyles = {}, ...args }, context) => { + return html` + ${Template(args, context)} ${Template( + { + ...args, + customStyles: { + ...customStyles, + "max-inline-size": "300px", + display: window.isChromatic() ? undefined : "none", + }, + }, + context, + )} ${Template( + { + ...args, + disableAll: true, + customStyles: { + ...customStyles, + display: window.isChromatic() ? undefined : "none", + }, + }, + context, + )} + `; }; +``` + +In the `*.stories.js` file: +```js export const Default = AccordionGroup.bind({}); Default.args = {}; ``` @@ -401,17 +421,22 @@ Ideally you should have a single story file for each component with multiple var In the event that you don't want a story to be tested in Chromatic, you can use the `disabledSnapshot` paramter: -``` +```js Default.parameters = { - chromatic: { disableSnapshot: true }, -} + chromatic: { disableSnapshot: true }, +}; ``` -Similarly, if a story is only for documentation purposes and shouldn't be shown in the sidebar, you can use the custom `docs-only` tag: +If a story should not be shown in the sidebar because it is only for: -``` -Default.tags = ["docs-only"]; -``` +- Documentation purposes, you can use the custom tag: + ```js + Default.tags = ["docs-only"]; + ``` +- Visual regression testing, you can use the custom tag: + ```js + Default.tags = ["vrt-only"]; + ``` ### Getting started diff --git a/.storybook/loaders/index.js b/.storybook/loaders/index.js new file mode 100644 index 00000000000..c6100b4072f --- /dev/null +++ b/.storybook/loaders/index.js @@ -0,0 +1,201 @@ + +// Use the document.fonts API to check if fonts have loaded +export const FontLoader = async () => ({ + fonts: document.fonts ? await document.fonts.ready : true, +}); + +export const IconLoader = async () => ({ + icons: { + workflow: { + medium: await import.meta.glob( + "/node_modules/@adobe/spectrum-css-workflow-icons/dist/18/*.svg", + { + eager: true, + query: "?raw", + import: "default", + } + ), + large: await import.meta.glob( + "/node_modules/@adobe/spectrum-css-workflow-icons/dist/24/*.svg", + { + eager: true, + query: "?raw", + import: "default", + } + ), + }, + ui: { + medium: await import.meta.glob( + "/node_modules/@spectrum-css/ui-icons/dist/medium/*.svg", + { + eager: true, + query: "?raw", + import: "default", + } + ), + large: await import.meta.glob( + "/node_modules/@spectrum-css/ui-icons/dist/large/*.svg", + { + eager: true, + query: "?raw", + import: "default", + } + ), + }, + }, +}); + +export const TokenLoader = async () => ({ + tokens: { + global: { + medium: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/medium-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + large: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/large-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + base: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/global-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + light: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/light-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + dark: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/dark-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + darkest: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/darkest-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + }, + spectrum: { + medium: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/spectrum/medium-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + large: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/spectrum/large-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + base: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/spectrum/global-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + light: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/spectrum/light-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + dark: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/spectrum/dark-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + darkest: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/spectrum/darkest-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + }, + express: { + medium: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/express/medium-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + large: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/express/large-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + base: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/express/global-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + light: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/express/light-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + dark: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/express/dark-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + darkest: await import.meta.glob( + "/node_modules/@spectrum-css/tokens/dist/css/express/darkest-vars.css", + { + eager: true, + query: "?inline", + import: "default", + } + ), + }, + } +}); diff --git a/.storybook/main.js b/.storybook/main.js index 03a788934cd..f995c260b98 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,27 +1,21 @@ -const { resolve, join } = require("path"); -const { readdirSync, existsSync } = require("fs"); - -const componentsPath = resolve(__dirname, "../components"); -const componentPkgs = readdirSync(componentsPath, { - withFileTypes: true, -}) - .filter((dirent) => dirent.isDirectory() && existsSync(join(dirent.path, dirent.name, "package.json"))) - .map((dirent) => dirent.name); - -module.exports = { - stories: [{ - directory: "../components", - files: "*/stories/*.@(stories.js|mdx)", - titlePrefix: "Components", - }, { - directory: "./guides", - files: "*.mdx", - titlePrefix: "Guides", - }, { - directory: "./deprecated", - files: "**/*.@(stories.js|mdx)", - titlePrefix: "Deprecated", - }], +export default { + stories: [ + { + directory: "../components", + files: "*/stories/*.@(stories.js|mdx)", + titlePrefix: "Components", + }, + { + directory: "./guides", + files: "*.@(stories.js|mdx)", + titlePrefix: "Guides", + }, + { + directory: "./deprecated", + files: "**/*.@(stories.js|mdx)", + titlePrefix: "Deprecated", + }, + ], rootDir: "../", staticDirs: ["../assets"], addons: [ @@ -55,121 +49,32 @@ module.exports = { core: { disableTelemetry: true, disableWhatsNewNotifications: true, + builder: "@storybook/builder-vite", }, - webpackFinal: function (config) { - /* eslint-disable no-unused-vars -- removing the global alias as it conflicts with the global npm pkg */ - const { global, ...alias } = config.resolve.alias; - config.resolve.alias = alias; + async viteFinal(config, { configType }) { + const { mergeConfig } = await import("vite"); - // Parse out any storybook rules for CSS so we can replace them with our own - const storybookRules = - config && config.module && config.module.rules - ? config.module.rules.filter( - (rule) => !(rule.test && rule.test.toString().includes("css")) - ) - : []; - return { - ...config, - /* Suppress autoprefixer warnings from storybook build */ - ignoreWarnings: [ - ...(config.ignoreWarnings ?? []), - /autoprefixer/, - /postcss/, - /.*stylelint.*/, - ], - /* Add support for root node_modules imports */ - resolve: { - ...(config.resolve ? config.resolve : {}), - modules: [ - ...(config.resolve ? config.resolve.modules : []), - resolve(__dirname, "../node_modules"), - ], - alias: { - ...(config.resolve ? config.resolve.alias : {}), - ...componentPkgs.reduce((pkgs, dir) => { - const pkg = require(resolve(componentsPath, dir, "package.json")); - pkgs[pkg.name] = resolve(componentsPath, dir); - return pkgs; - }, {}), - }, + return mergeConfig(config, { + publicDir: "../assets", + port: 8080, + build: { + sourcemap: configType === "DEVELOPMENT", + manifest: true, + minify: configType === "PRODUCTION", }, - module: { - ...(config.module ?? []), - rules: [ - ...storybookRules, - { - test: /^\w+\.{ico,jpg,jpeg,png,gif,webp}$/i, - use: [ - { - loader: "file-loader", - options: { - outputPath: (url) => `assets/images/${url.replace(/_\//g, "")}`, - }, - }, - ], - }, - { - test: /\.css$/i, - sideEffects: true, - use: [ - { - loader: "style-loader", - options: { - injectType: "linkTag", - attributes: { - "data-source": "processed", - }, - }, - }, - { - loader: "file-loader", - options: { - name: "[path][name].[ext][query]", - outputPath: (url) => { - const cleanURL = url.replace(/_\//g, ""); - if (/node_modules\/@spectrum-css/.test(url)) { - return `assets/css/${cleanURL.replace(/node_modules\/@spectrum-css\//g, "")}`; - } - - return `assets/css/${cleanURL}`; - }, - esModule: false, - }, - }, - { - loader: "postcss-loader", - options: { - implementation: require("postcss"), - postcssOptions: { - config: resolve(__dirname, "../postcss.config.js"), - additionalPlugins: { - "postcss-pseudo-classes": { - restrictTo: ["focus-visible", "focus-within", "hover", "active", "disabled"], - allCombinations: true, - preserveBeforeAfter: false, - prefix: "is-" - }, - } - }, - }, - }, - ], - }, - { - test: /\.js$/, - enforce: "pre", - use: ["source-map-loader"], - } /* Raw loader */, - { - resourceQuery: /raw/, - type: "asset/source", - }, - ], + css: { + devSourcemap: configType === "DEVELOPMENT", }, - }; + }); }, framework: { - name: "@storybook/web-components-webpack5", + name: "@storybook/web-components-vite", + }, + features: { + /* Code splitting flag; load stories on-demand */ + storyStoreV7: true, + /* Builds stories.json to help with on-demand loading */ + buildStoriesJson: true, }, docs: { autodocs: true, // see below for alternatives diff --git a/.storybook/manager.js b/.storybook/manager.js index b13ae8b364b..4be4bc39b8f 100644 --- a/.storybook/manager.js +++ b/.storybook/manager.js @@ -1,4 +1,4 @@ -import "@spectrum-css/tokens"; +import "@spectrum-css/tokens/dist/index.css"; import { addons } from "@storybook/manager-api"; import { create } from "@storybook/theming"; import "./assets/index.css"; diff --git a/.storybook/package.json b/.storybook/package.json index 8a291a137a1..27e25843a08 100644 --- a/.storybook/package.json +++ b/.storybook/package.json @@ -5,7 +5,8 @@ "license": "Apache-2.0", "author": "Adobe", "homepage": "https://opensource.adobe.com/spectrum-css/preview", - "main": "main.js", + "type": "module", + "module": "main.js", "scripts": { "build": "storybook build --config-dir . --output-dir ./storybook-static" }, @@ -17,7 +18,7 @@ "devDependencies": { "@babel/core": "^7.24.5", "@chromaui/addon-visual-tests": "^1.0.0", - "@etchteam/storybook-addon-status": "^4.2.4", + "@etchteam/storybook-addon-status": "^5.0.0", "@storybook/addon-a11y": "^8.1.6", "@storybook/addon-actions": "^8.1.6", "@storybook/addon-console": "^3.0.0", @@ -26,29 +27,26 @@ "@storybook/addon-essentials": "^8.1.6", "@storybook/addon-interactions": "^8.1.6", "@storybook/blocks": "^8.1.6", + "@storybook/builder-vite": "^8.1.6", "@storybook/components": "^8.1.6", "@storybook/core-events": "^8.1.6", - "@storybook/jest": "^0.2.3", "@storybook/manager-api": "^8.1.6", "@storybook/preview-api": "^8.1.6", "@storybook/testing-library": "^0.2.2", "@storybook/theming": "^8.1.6", - "@storybook/web-components-webpack5": "^8.1.6", + "@storybook/web-components-vite": "^8.1.6", "@whitespace/storybook-addon-html": "^6.1.1", - "chromatic": "^11.4.0", - "file-loader": "^6.2.0", + "chromatic": "^11.4.1", "lit": "^3.1.3", "lodash-es": "^4.17.21", "postcss": "^8.4.38", - "postcss-loader": "^8.1.1", "postcss-pseudo-classes": "^0.4.0", "prettier": "^3.2.5", "react": "^18.3.1", "react-dom": "^18.3.1", "react-syntax-highlighter": "^15.5.0", - "source-map-loader": "^5.0.0", + "rollup-plugin-postcss-lit": "^2.1.0", "storybook": "^8.1.6", - "style-loader": "4.0.0", - "webpack": "^5.91.0" + "vite": "^5.2.12" } } diff --git a/.storybook/preview.js b/.storybook/preview.js index f8ac915289e..0efe0d0eb40 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,5 +1,5 @@ - -import "@spectrum-css/tokens/dist/index.css"; +import workflowSprite from "@adobe/spectrum-css-workflow-icons/dist/spectrum-icons.svg?raw"; +import uiSprite from "@spectrum-css/ui-icons/dist/spectrum-css-icons.svg?raw"; import { setConsoleOptions } from "@storybook/addon-console"; import "./assets/base.css"; import "./assets/typekit.js"; @@ -12,13 +12,34 @@ import { withTextDirectionWrapper, } from "./decorators/index.js"; import DocumentationTemplate from "./DocumentationTemplate.mdx"; +import { FontLoader, IconLoader, TokenLoader } from "./loaders/index.js"; import { argTypes, globalTypes } from "./types"; +window.global = window; + const panelExclude = setConsoleOptions({}).panelExclude || []; setConsoleOptions({ - panelExclude: [...panelExclude, /deprecated/, /TypeError/, /postcss/, /stylelint/], + panelExclude: [ + ...panelExclude, + /deprecated/, + /TypeError/, + /postcss/, + /stylelint/, + ], }); +// Inject the sprite sheets into the document +let sprite = document.getElementById("spritesheets"); +if (!sprite) { + sprite = document.createElement("div"); + sprite.id = "spritesheets"; + sprite.innerHTML = workflowSprite + uiSprite; + document.body.appendChild(sprite); +} +else { + sprite.innerHTML = workflowSprite + uiSprite; +} + export const args = { color: "light", scale: "medium", @@ -40,16 +61,24 @@ export const parameters = { options: { storySort: { method: "alphabetical-by-kind", - order: ["Guides", ["Contributing", "*", "Adobe Code of Conduct", "Changelog"], "Components", ["*", ["Docs", "Default", "*"]], "Deprecated", ["*", ["Docs", "Default", "*"]], "*"], + order: [ + "Guides", + ["Contributing", "*", "Adobe Code of Conduct", "Changelog"], + "Components", + ["*", ["Docs", "Default", "*"]], + "Deprecated", + ["*", ["Docs", "Default", "*"]], + "*", + ], includeNames: true, }, }, chromatic: { - // This delay ensures tokens are loaded before the story is rendered - // @todo: explore a loader for this to ensure tokens load before stories without an arbitrary delay + // @todo: use a loader to ensure tokens load before stories without arbitrary delay delay: 500, forcedColors: "none", prefersReducedMotion: "no-preference", + pauseAnimationAtEnd: true, }, controls: { expanded: true, @@ -74,7 +103,7 @@ export const parameters = { page: DocumentationTemplate, story: { inline: true, - iframeHeight: "200px", + height: "200px", }, source: { type: "dynamic", @@ -97,6 +126,12 @@ export const parameters = { }, }; +export const loaders = [ + FontLoader, + IconLoader, + TokenLoader, +]; + export const decorators = [ withTextDirectionWrapper, withLanguageWrapper, @@ -104,12 +139,13 @@ export const decorators = [ withContextWrapper, withTestingPreviewWrapper, withActions, + // Attach the icons to the window object for use in the stories + (StoryFn, context) => { + if (context?.loaded?.icons) window.icons = context.loaded.icons; + return StoryFn(context); + }, ]; -// Use the document.fonts API to check if fonts have loaded -// https://developer.mozilla.org/en-US/docs/Web/API/Document/fonts API to -export const loaders = document.fonts ? [async () => ({ fonts: await document.fonts.ready })] : []; - export default { globalTypes, argTypes, diff --git a/.storybook/project.json b/.storybook/project.json index c1f052d2bc5..37e00bfea75 100644 --- a/.storybook/project.json +++ b/.storybook/project.json @@ -75,7 +75,8 @@ "stylelint --cache --allow-empty-input --report-descriptionless-disables --report-invalid-scope-disables --report-needless-disables {projectRoot}/assets/*.css --ignore-pattern {projectRoot}/dist", "eslint --cache --no-error-on-unmatched-pattern --report-unused-disable-directives {projectRoot}/*.{js,json} {projectRoot}/**/*.js --ignore-pattern \"!.storybook/\" || exit 0" ] - }}, + } + }, "start": { "cache": true, "dependsOn": ["^build"], diff --git a/components/accordion/stories/accordion.stories.js b/components/accordion/stories/accordion.stories.js index a6942ce5dd9..a3acfa942ab 100644 --- a/components/accordion/stories/accordion.stories.js +++ b/components/accordion/stories/accordion.stories.js @@ -1,9 +1,7 @@ -import { html } from "lit"; -import { styleMap } from "lit/directives/style-map.js"; import { Template as Link } from "@spectrum-css/link/stories/template.js"; import { Template as Typography } from "@spectrum-css/typography/stories/template.js"; -import { Template } from "./template.js"; +import { AccordionGroup, Template } from "./template.js"; /** * The accordion element contains a list of items that can be expanded or collapsed to reveal additional content or information associated with each item. There can be zero expanded items, exactly one expanded item, or more than one item expanded at a time, depending on the configuration. This list of items is defined by child accordion item elements. @@ -141,61 +139,30 @@ export default { actions: { handles: ["click .spectrum-Accordion-item"], }, + chromatic: { disableSnapshot: true }, }, }; -const AccordionGroup = (args) => html` - ${window.isChromatic() ? html` -
- ${Template(args)} - ${Template({ - ...args, - customStyles: { - maxInlineSize: "300px", - }, - })} - ${Template({ - ...args, - disableAll: true, - })} -
- ` : Template(args)} -`; - export const Default = AccordionGroup.bind({}); Default.args = {}; +Default.parameters = { + chromatic: { disableSnapshot: false }, +}; -/** - * Stories for the MDX "Docs" only. - * Based off of the base `Template` which does not have conditional Chromatic-only markup. - */ export const Regular = Template.bind({}); Regular.tags = ["docs-only"]; Regular.args = { density: "regular", }; -Regular.parameters = { - chromatic: { disableSnapshot: true }, -}; export const Compact = Template.bind({}); Compact.tags = ["docs-only"]; Compact.args = { density: "compact", }; -Compact.parameters = { - chromatic: { disableSnapshot: true }, -}; export const Spacious = Template.bind({}); Spacious.tags = ["docs-only"]; Spacious.args = { density: "spacious", }; -Spacious.parameters = { - chromatic: { disableSnapshot: true }, -}; diff --git a/components/accordion/stories/template.js b/components/accordion/stories/template.js index 5df480e7cbc..ea36d42d554 100644 --- a/components/accordion/stories/template.js +++ b/components/accordion/stories/template.js @@ -1,13 +1,10 @@ +import { Template as Icon } from "@spectrum-css/icon/stories/template.js"; +import { useArgs } from "@storybook/preview-api"; import { html } from "lit"; import { classMap } from "lit/directives/class-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { repeat } from "lit/directives/repeat.js"; import { styleMap } from "lit/directives/style-map.js"; - -import { useArgs } from "@storybook/preview-api"; - -import { Template as Icon } from "@spectrum-css/icon/stories/template.js"; - import "../index.css"; export const AccordionItem = ({ @@ -23,7 +20,7 @@ export const AccordionItem = ({ customClasses = [], onclick, ...globals -}) => html` +}, context) => html`
@@ -81,7 +78,7 @@ export const Template = ({ customClasses = [], customStyles = {}, ...globals -}) => { +}, context) => { const [, updateArgs] = useArgs(); if (!items || !items.size) return html``; @@ -121,8 +118,33 @@ export const Template = ({ }); updateArgs({ items: newItems }); }, - }); + }, context); })}
`; }; + +export const AccordionGroup = (args, context) => html` +
+ ${Template(args, context)} +
+
+ ${Template(args, context)} + ${Template({ + ...args, + customStyles: { + maxInlineSize: "300px", + }, + }, context)} + ${Template({ + ...args, + disableAll: true, + }, context)} +
+`; diff --git a/components/actionbar/stories/actionbar.mdx b/components/actionbar/stories/actionbar.mdx index de4c9c0a688..a4f7c17a3bf 100644 --- a/components/actionbar/stories/actionbar.mdx +++ b/components/actionbar/stories/actionbar.mdx @@ -1,6 +1,6 @@ -import { Canvas, ArgTypes, Meta, Description, Title } from '@storybook/blocks'; +import { Canvas, ArgTypes, Meta, Description, Title } from "@storybook/blocks"; -import * as ActionBarStories from './actionbar.stories'; +import * as ActionBarStories from "./actionbar.stories"; @@ -22,8 +22,11 @@ import * as ActionBarStories from './actionbar.stories'; - Sticky - Action bars will sit on top of content until dismissed. ## Popover Dependency + Action bar requires Popover, which is nested within Action bar. Action bar background, border, and corner radius are applied to the nested Popover component and can be overriden by Action bar using `--mod-*` prefixed custom properties. A [list of the properties](https://github.com/adobe/spectrum-css/blob/main/components/actionbar/metadata/mods.md) can be found in the repo. ## Properties +The component accepts the following inputs (properties): + diff --git a/components/actionbar/stories/actionbar.stories.js b/components/actionbar/stories/actionbar.stories.js index b45f2733e2c..d1c916d228f 100644 --- a/components/actionbar/stories/actionbar.stories.js +++ b/components/actionbar/stories/actionbar.stories.js @@ -1,7 +1,7 @@ import { default as ActionButton } from "@spectrum-css/actionbutton/stories/actionbutton.stories.js"; import { default as CloseButton } from "@spectrum-css/closebutton/stories/closebutton.stories.js"; import { default as Popover } from "@spectrum-css/popover/stories/popover.stories.js"; -import { Template } from "./template"; +import { ActionBarGroup, Template } from "./template"; /** * The action bar component is a floating full width bar that appears upon selection. @@ -80,7 +80,7 @@ export default { } }; -export const Default = Template.bind({}); +export const Default = ActionBarGroup.bind({}); Default.args = {}; export const Emphasized = Template.bind({}); diff --git a/components/actionbar/stories/template.js b/components/actionbar/stories/template.js index 8c5cf720252..d07f16c247c 100644 --- a/components/actionbar/stories/template.js +++ b/components/actionbar/stories/template.js @@ -1,11 +1,10 @@ -import { html } from "lit"; -import { classMap } from "lit/directives/class-map.js"; - import { Template as ActionGroup } from "@spectrum-css/actiongroup/stories/template.js"; import { Template as CloseButton } from "@spectrum-css/closebutton/stories/template.js"; import { Template as FieldLabel } from "@spectrum-css/fieldlabel/stories/template.js"; import { Template as Popover } from "@spectrum-css/popover/stories/template.js"; - +import { html } from "lit"; +import { classMap } from "lit/directives/class-map.js"; +import { styleMap } from "lit/directives/style-map.js"; import "../index.css"; export const Template = ({ @@ -71,3 +70,23 @@ export const Template = ({ })} `; + +export const ActionBarGroup = (args) => html` +
+ ${Template(args)} +
+
+ ${Template(args)} + ${Template({ + ...args, + isEmphasized: true, + })} +
+`; diff --git a/components/actionbutton/stories/actionbutton.stories.js b/components/actionbutton/stories/actionbutton.stories.js index e075b3dbe28..f9564dd124f 100644 --- a/components/actionbutton/stories/actionbutton.stories.js +++ b/components/actionbutton/stories/actionbutton.stories.js @@ -1,11 +1,5 @@ -import { html } from "lit"; -import { styleMap } from "lit/directives/style-map.js"; -import { when } from "lit/directives/when.js"; - -import { Template } from "./template"; - import { default as IconStories } from "@spectrum-css/icon/stories/icon.stories.js"; -import { Template as Typography } from "@spectrum-css/typography/stories/template.js"; +import { Variants } from "./template"; /** * The action button component represents an action a user can take. @@ -147,266 +141,9 @@ export default { actions: { handles: ["click .spectrum-ActionButton:not([disabled])"], }, - html: { - root: "#render-root", - }, }, - decorators: [ - (Story, context) => html` - -
- ${Story(context)} -
- `, - ], }; -const ActionButtons = (args) => html`
- ${Template({ - ...args, - label: "More", - iconName: undefined, - })} - ${Template({ - ...args, - label: "More", - })} - ${Template(args)} - ${Template({ - ...args, - hasPopup: true, - })} - - ${when(window.isChromatic(), () => - Template({ - ...args, - label: "Truncate this long content", - iconName: undefined, - customStyles: { maxInlineSize: "100px" }, - }) - )} - ${when(window.isChromatic(), () => - Template({ - ...args, - label: "Truncate this long content", - customStyles: { maxInlineSize: "100px" }, - }) - )} -
`; - -const States = (args) => - html`
- ${Typography({ - semantics: "detail", - size: "s", - content: ["Default"], - customClasses: ["chromatic-ignore"], - })} - ${ActionButtons(args)} -
-
- ${Typography({ - semantics: "detail", - size: "s", - content: ["Selected"], - customClasses: ["chromatic-ignore"], - })} - ${ActionButtons({ - ...args, - isSelected: true, - })} -
-
- ${Typography({ - semantics: "detail", - size: "s", - content: ["Focused"], - customClasses: ["chromatic-ignore"], - })} - ${ActionButtons({ - ...args, - isFocused: true, - })} -
-
- ${Typography({ - semantics: "detail", - size: "s", - content: ["Hovered"], - customClasses: ["chromatic-ignore"], - })} - ${ActionButtons({ - ...args, - isHovered: true, - })} -
-
- ${Typography({ - semantics: "detail", - size: "s", - content: ["Active"], - customClasses: ["chromatic-ignore"], - })} - ${ActionButtons({ - ...args, - isActive: true, - })} -
-
- ${Typography({ - semantics: "detail", - size: "s", - content: ["Disabled"], - customClasses: ["chromatic-ignore"], - })} - ${ActionButtons({ - ...args, - isDisabled: true, - })} -
-
- ${Typography({ - semantics: "detail", - size: "s", - content: ["Disabled + selected"], - customClasses: ["chromatic-ignore"], - })} - ${ActionButtons({ - ...args, - isSelected: true, - isDisabled: true, - })} -
`; - -const Sizes = (args) => - html` ${["s", "m", "l", "xl"].map((size) => { - return html`
- ${Typography({ - semantics: "detail", - size: "s", - content: [ - { - xxs: "Extra-extra-small", - xs: "Extra-small", - s: "Small", - m: "Medium", - l: "Large", - xl: "Extra-large", - xxl: "Extra-extra-large", - }[size], - ], - customClasses: ["chromatic-ignore"], - })} - ${ActionButtons({ - ...args, - size, - })} -
`; - })}`; - -const Variants = (args) => - html` ${window.isChromatic() - ? html`
- ${Typography({ - semantics: "detail", - size: "l", - content: ["Standard"], - customClasses: ["chromatic-ignore"], - })} -
- ${States(args)} -
-
-
- ${Typography({ - semantics: "detail", - size: "l", - content: ["Emphasized"], - customClasses: ["chromatic-ignore"], - })} -
- ${States({ - ...args, - isEmphasized: true, - })} -
-
-
- ${Typography({ - semantics: "detail", - size: "l", - content: ["Quiet"], - customClasses: ["chromatic-ignore"], - })} -
- ${States({ - ...args, - isQuiet: true, - })} -
-
-
- ${Typography({ - semantics: "detail", - size: "l", - content: ["Sizing"], - customClasses: ["chromatic-ignore"], - })} -
- ${Sizes(args)} -
-
` - : ActionButtons(args)}`; - export const Default = Variants.bind({}); Default.args = {}; diff --git a/components/actionbutton/stories/template.js b/components/actionbutton/stories/template.js index 0a9765fc2e8..e6250c29581 100644 --- a/components/actionbutton/stories/template.js +++ b/components/actionbutton/stories/template.js @@ -1,12 +1,11 @@ +import { Template as Typography } from "@spectrum-css/typography/stories/template.js"; import { html } from "lit"; import { classMap } from "lit/directives/class-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { styleMap } from "lit/directives/style-map.js"; import { when } from "lit/directives/when.js"; - import { capitalize, lowerCase } from "lodash-es"; - -import "@spectrum-css/actionbutton/index.css"; +import "../index.css"; /** * @todo load order should not influence the icon size but it is; fix this @@ -38,7 +37,6 @@ export const Template = ({ role, ...globals }) => { - return html`