diff --git a/2nd-gen/packages/swc/.storybook/DocumentTemplate.mdx b/2nd-gen/packages/swc/.storybook/DocumentTemplate.mdx index 274cda1adb..6c19228e63 100644 --- a/2nd-gen/packages/swc/.storybook/DocumentTemplate.mdx +++ b/2nd-gen/packages/swc/.storybook/DocumentTemplate.mdx @@ -1,22 +1,11 @@ import { - Meta, - Title, - Primary, - Controls, - Stories, - ArgTypes, Description, - Subtitle, HeaderMdx, + Meta, + Stories, useOf, } from '@storybook/addon-docs/blocks'; -import { - ApiTable, - GettingStarted, - OverviewStory, - SpectrumStories, - StatusBadge, -} from './blocks'; +import { DocsFooter, DocsHeader, SpectrumStories } from './blocks'; export const checkIsSingleStory = () => { const resolvedOf = useOf('meta', ['meta']); @@ -64,56 +53,12 @@ export const ConditionalSection = ({ tag, title, hideTitle = false }) => { ); -}; -export const ConditionalAPISection = () => { -const resolvedOf = useOf('meta', ['meta']); -const tags = resolvedOf?.csfFile?.meta?.tags ?? []; -const hasCustomAPIDocs = tags.includes('api') || Object.values(resolvedOf.csfFile.stories).some( -(story) => story.tags?.includes('api') -); - - if (!hasCustomAPIDocs) { - return ( - <> - API - - - - - ); - } - - return ( - <> - API - - - -
- - - ); - -}; - -export const ConditionalGettingStarted = () => { - const resolvedOf = useOf('meta', ['meta']); - - const tags = resolvedOf?.csfFile?.meta?.tags ?? []; - - return ; - }; - -<StatusBadge /> -<Subtitle /> -<Description /> +<DocsHeader /> <SingleStoryDescription /> -<OverviewStory /> -<ConditionalGettingStarted /> <ConditionalSection tag="anatomy" title="Anatomy" hideTitle={true} /> <ConditionalSection tag="upcoming" title="Upcoming features" hideTitle={true} /> @@ -124,12 +69,7 @@ export const ConditionalGettingStarted = () => { <ConditionalSection tag="a11y" title="Accessibility" hideTitle={true} /> <ConditionalSection tag="full-pattern" title="Full pattern" /> -## API - -<ApiTable /> <AdvancedExamplesStories /> <ConditionalSection tag="appendix" title="Appendix" hideTitle={true} /> -## Feedback - -Have feedback or questions? [Open an issue](https://github.com/adobe/spectrum-web-components/issues/new/choose). +<DocsFooter /> diff --git a/2nd-gen/packages/swc/.storybook/blocks/DocsFooter.tsx b/2nd-gen/packages/swc/.storybook/blocks/DocsFooter.tsx new file mode 100644 index 0000000000..8017e76925 --- /dev/null +++ b/2nd-gen/packages/swc/.storybook/blocks/DocsFooter.tsx @@ -0,0 +1,74 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + Controls, + HeaderMdx, + Primary, + useOf, +} from '@storybook/addon-docs/blocks'; +import React from 'react'; + +import { ApiTable } from './ApiTable'; +import { SpectrumStories } from './SpectrumStories'; + +/** + * The standard bottom of a Storybook docs page: API table, primary story, + * controls, and the feedback link. Genre-aware via meta tags; controllers + * (tagged `controller`) skip the API table because they expose a TypeScript + * class rather than a custom element manifest. + * + * Authors compose a docs page as: + * + * <Meta of={Stories} /> + * <DocsHeader /> + * ...component-specific sections... + * <DocsFooter /> + */ +export const DocsFooter = () => { + const resolvedOf = useOf('meta', ['meta']); + const tags: string[] = resolvedOf?.preparedMeta?.tags ?? []; + const isController = tags.includes('controller'); + + // Per-unit pages may flag custom API content by tagging stories with `api`. + const hasCustomApiStories = Object.values( + resolvedOf?.csfFile?.stories ?? {} + ).some((story: any) => story.tags?.includes('api')); + + return ( + <> + <HeaderMdx as="h2" id="api"> + API + </HeaderMdx> + {!isController && <ApiTable />} + <Primary /> + <Controls /> + {hasCustomApiStories && ( + <> + <hr /> + <SpectrumStories tag="api" hideTitle={true} /> + </> + )} + + <HeaderMdx as="h2" id="feedback"> + Feedback + </HeaderMdx> + <p> + Have feedback or questions?{' '} + <a href="https://github.com/adobe/spectrum-web-components/issues/new/choose"> + Open an issue + </a> + . + </p> + </> + ); +}; diff --git a/2nd-gen/packages/swc/.storybook/blocks/DocsHeader.tsx b/2nd-gen/packages/swc/.storybook/blocks/DocsHeader.tsx new file mode 100644 index 0000000000..70f46130dd --- /dev/null +++ b/2nd-gen/packages/swc/.storybook/blocks/DocsHeader.tsx @@ -0,0 +1,52 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + Description, + Subtitle, + Title, + useOf, +} from '@storybook/addon-docs/blocks'; +import React from 'react'; + +import { GettingStarted } from './GettingStarted'; +import { OverviewStory } from './OverviewStory'; +import { StatusBadge } from './StatusBadge'; + +/** + * The standard top of a Storybook docs page: title, status badge, subtitle, + * description, overview story, and getting-started instructions. Genre-aware + * via meta tags (`migrated`, `controller`, `utility`); component, pattern, + * and controller pages all use this single block. + * + * Authors compose a docs page as: + * + * <Meta of={Stories} /> + * <DocsHeader /> + * ...component-specific sections... + * <DocsFooter /> + */ +export const DocsHeader = () => { + const resolvedOf = useOf('meta', ['meta']); + const tags: string[] = resolvedOf?.preparedMeta?.tags ?? []; + + return ( + <> + <Title /> + <StatusBadge /> + <Subtitle /> + <Description /> + <OverviewStory /> + <GettingStarted tags={tags} /> + </> + ); +}; diff --git a/2nd-gen/packages/swc/.storybook/blocks/index.ts b/2nd-gen/packages/swc/.storybook/blocks/index.ts index 17346333f6..385ef108b6 100644 --- a/2nd-gen/packages/swc/.storybook/blocks/index.ts +++ b/2nd-gen/packages/swc/.storybook/blocks/index.ts @@ -10,6 +10,8 @@ * governing permissions and limitations under the License. */ export * from './ApiTable'; +export * from './DocsFooter'; +export * from './DocsHeader'; export * from './GettingStarted'; export * from './OverviewStory'; export * from './SpectrumDocs'; diff --git a/2nd-gen/packages/swc/.storybook/preview.ts b/2nd-gen/packages/swc/.storybook/preview.ts index 73e4673e0a..a52fb42a93 100644 --- a/2nd-gen/packages/swc/.storybook/preview.ts +++ b/2nd-gen/packages/swc/.storybook/preview.ts @@ -248,6 +248,7 @@ const preview = { 'Tools vs packages', 'Writing migration guides', 'Focus management', + 'Changelog strategy', ], 'Style guide', [ @@ -330,7 +331,10 @@ const preview = { 'Rendering and styling migration analysis', ], 'Action button', - ['Rendering and styling migration analysis'], + [ + 'Accessibility migration analysis', + 'Rendering and styling migration analysis', + ], 'Action group', ['Rendering and styling migration analysis'], 'Action menu', @@ -360,9 +364,14 @@ const preview = { 'Rendering and styling migration analysis', ], 'Button group', - ['Rendering and styling migration analysis'], + [ + 'Accessibility migration analysis', + 'Rendering and styling migration analysis', + ], 'Checkbox', ['Rendering and styling migration analysis'], + 'Close button', + ['Accessibility migration analysis'], 'Color field', ['Rendering and styling migration analysis'], 'Color loupe', @@ -382,6 +391,11 @@ const preview = { ['Rendering and styling migration analysis'], 'Field label', ['Rendering and styling migration analysis'], + 'Grid', + [ + 'Accessibility migration analysis', + 'Rendering and styling migration analysis', + ], 'Help text', ['Rendering and styling migration analysis'], 'Illustrated message', @@ -391,12 +405,16 @@ const preview = { 'Rendering and styling migration analysis', ], 'Infield button', - ['Rendering and styling migration analysis'], + [ + 'Accessibility migration analysis', + 'Rendering and styling migration analysis', + ], 'Infield progress circle', ['Rendering and styling migration analysis'], 'Link', [ 'Accessibility migration analysis', + 'Migration plan', 'Rendering and styling migration analysis', ], 'Menu', @@ -424,6 +442,7 @@ const preview = { 'Popover', [ 'Accessibility migration analysis', + 'Migration plan', 'Rendering and styling migration analysis', ], 'Progress bar', diff --git a/2nd-gen/packages/swc/components/asset/asset.internal.mdx b/2nd-gen/packages/swc/components/asset/asset.internal.mdx new file mode 100644 index 0000000000..a9601b9f49 --- /dev/null +++ b/2nd-gen/packages/swc/components/asset/asset.internal.mdx @@ -0,0 +1,66 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as AssetStories from './stories/asset.internal.stories'; + +<Meta of={AssetStories} /> + +<DocsHeader /> + +## Anatomy + +An asset consists of: + +1. **Icon or image content**: either a file/folder icon or custom slotted content +2. **Accessible label**: provides context for assistive technologies + +The asset automatically centers its content both horizontally and vertically within the available space. + +### Content + +- **Default slot**: custom content to display (typically an image) when variant is not set +- **Label**: accessible label for screen readers (used as `aria-label` on the icon SVGs) + +<Canvas of={AssetStories.Anatomy} /> + +## Options + +### Variants + +Assets support two built-in icon variants for representing files and folders: + +- **`file`**: displays a file icon, useful for representing documents, files, or file types +- **`folder`**: displays a folder icon, useful for representing directories or collections + +When no variant is specified, the asset displays custom content provided via the default slot (typically an image). + +<Canvas of={AssetStories.Variants} /> + +## Accessibility + +### Features + +The `<swc-asset>` element implements several accessibility features: + +#### ARIA implementation + +- **Icon labeling**: file and folder SVG icons automatically use the `label` property as `aria-label` +- **Non-interactive**: assets have no interactive behavior and are not focusable + +#### Visual accessibility + +- Icons use sufficient color contrast in both light and dark modes +- High contrast mode is supported with appropriate color overrides +- Content automatically centers for consistent layout and visual balance + +### Best practices + +- Always provide a descriptive `label` attribute for file and folder variants +- Use specific, meaningful labels or alt text (e.g., "Project proposal PDF", "projects/2025/proposal.pdf", or not just "File") +- The `label` on the asset itself should describe the asset's purpose or context +- For decorative images, use an empty `alt=""` attribute on the img tag +- Test with screen readers to verify assets are announced appropriately in context + +<Canvas of={AssetStories.Accessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/asset/stories/asset.internal.stories.ts b/2nd-gen/packages/swc/components/asset/stories/asset.internal.stories.ts index f4a72e2f1c..3919950b08 100644 --- a/2nd-gen/packages/swc/components/asset/stories/asset.internal.stories.ts +++ b/2nd-gen/packages/swc/components/asset/stories/asset.internal.stories.ts @@ -51,7 +51,7 @@ const meta: Meta = { export default meta; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── export const Playground: Story = { @@ -59,7 +59,7 @@ export const Playground: Story = { label: 'Background', 'default-slot': `<img src="https://picsum.photos/id/56/80/80/?blur=2" alt="preview of background" />`, }, - tags: ['autodocs', 'dev'], + tags: ['dev'], }; // ──────────────────── @@ -78,19 +78,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * An asset consists of: - * - * 1. **Icon or image content** - Either a file/folder icon or custom slotted content - * 2. **Accessible label** - Provides context for assistive technologies - * - * The asset automatically centers its content both horizontally and vertically within the available space. - * - * ### Content - * - * - **Default slot**: Custom content to display (typically an image) when variant is not set - * - **Label**: Accessible label for screen readers (used as `aria-label` on the icon SVGs) - */ export const Anatomy: Story = { render: (args) => html` ${template({ ...args, variant: 'file', label: 'README.md' })} @@ -108,14 +95,6 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * Assets support two built-in icon variants for representing files and folders: - * - * - **`file`**: Displays a file icon, useful for representing documents, files, or file types - * - **`folder`**: Displays a folder icon, useful for representing directories or collections - * - * When no variant is specified, the asset displays custom content provided via the default slot (typically an image). - */ export const Variants: Story = { render: (args) => html` ${template({ @@ -134,9 +113,6 @@ export const Variants: Story = { 'default-slot': `<img src="https://picsum.photos/id/64/80/80" alt="sunset over a sandy beach" />`, })} `, - parameters: { - 'section-order': 1, - }, tags: ['options'], }; @@ -144,30 +120,6 @@ export const Variants: Story = { // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-asset>` element implements several accessibility features: - * - * #### ARIA implementation - * - * - **Icon labeling**: File and folder SVG icons automatically use the `label` property as `aria-label` - * - **Non-interactive**: Assets have no interactive behavior and are not focusable - * - * #### Visual accessibility - * - * - Icons use sufficient color contrast in both light and dark modes - * - High contrast mode is supported with appropriate color overrides - * - Content automatically centers for consistent layout and visual balance - * - * ### Best practices - * - * - Always provide a descriptive `label` attribute for file and folder variants - * - Use specific, meaningful labels or alt text (e.g., "Project proposal PDF", "projects/2025/proposal.pdf", or not just "File") - * - The `label` on the asset itself should describe the asset's purpose or context - * - For decorative images, use an empty `alt=""` attribute on the img tag - * - Test with screen readers to verify assets are announced appropriately in context - */ export const Accessibility: Story = { render: (args) => html` ${template({ diff --git a/2nd-gen/packages/swc/components/avatar/avatar.mdx b/2nd-gen/packages/swc/components/avatar/avatar.mdx new file mode 100644 index 0000000000..e64ae96803 --- /dev/null +++ b/2nd-gen/packages/swc/components/avatar/avatar.mdx @@ -0,0 +1,101 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as AvatarStories from './stories/avatar.stories'; + +<Meta of={AvatarStories} /> + +<DocsHeader /> + +## Anatomy + +An avatar consists of: + +1. **Image** — a circular clipped profile photo + +### Content + +- `src`: URL of the profile image +- `alt`: text description for assistive technology. Pass `alt=""` to mark as decorative. +- `size`: numeric size token (50–1500). Defaults to `500` (40 px). + +<Canvas of={AvatarStories.Anatomy} /> + +## Upcoming features + +### Additional avatar types + +- **Gradient image**: shows a generated colorful gradient when no photo is available +- **Initials**: shows the user's initials inside the avatar circle as a photo fallback +- **Guest**: shows a default guest icon when no user identity is known + +### Avatar Group + +- Display a collection of avatars in a stacked layout with configurable overlap and overflow + +## Options + +### Sizes + +Avatars come in 17 sizes from 50 to 1500, ranging from 16 px to 104 px. +Sizes 50–700 match 1st-gen; sizes 800–1500 are new in Spectrum 2. + +The default size is `500` (40 px). + +<Canvas of={AvatarStories.Sizes} /> + +### Decorative + +Use the `decorative` attribute and `alt=""` to treat the avatar as decorative: the image is hidden from assistive technology. + +Use this **only when the surrounding context already identifies the person** (e.g., their name appears next to the avatar). + +<Canvas of={AvatarStories.Decorative} /> + +### Outline + +Use the `outline` attribute to render a solid outline around the avatar image. This is useful when the avatar's image border color matches the surrounding background. The outline uses `--swc-avatar-outline-width` (currently 1 px) for sizes 50–900 and a hardcoded 2 px for sizes 1000–1500, matching the Spectrum 2 specification. Within an Avatar Group, `outline` defaults to `true` to visually separate stacked avatars. + +<Canvas of={AvatarStories.Outline} /> + +### Disabled + +A disabled avatar indicates that the entity is not currently active or available. The avatar remains visible in the layout at reduced opacity, communicating that it may become active later. It remains in the accessibility tree: `disabled` is purely visual. + +<Canvas of={AvatarStories.Disabled} /> + +## Behaviors + +### In Action Button + +An avatar can be placed inside an action button to create a user-triggered action tied to a specific person or entity. + +<Canvas of={AvatarStories.InActionButton} /> + +## Accessibility + +### Features + +The `<swc-avatar>` element implements several accessibility features: + +#### Alt text + +- Provide a descriptive `alt` value identifying the person or entity depicted +- Pass `alt=""` and set the `decorative` attribute when the name already appears in context + +#### Non-interactive element + +- Avatars have no interactive behavior and are not focusable +- Screen readers announce the image using the `alt` attribute value +- No keyboard interaction is required or expected +- To make an avatar a link, wrap it in a standard `<a>` element and set `aria-label` on the anchor when the destination is not clear from surrounding context + +### Best practices + +- Always set `alt`: omitting it causes some screen readers to announce the image URL +- Use `alt=""` and the `decorative` attribute only when the person is identified by adjacent text +- Keep alt text short and descriptive: prefer `"Jane Doe"` over `"Profile photo of Jane Doe"` + +<Canvas of={AvatarStories.Accessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts b/2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts index 208691227d..eb75db4f34 100644 --- a/2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts +++ b/2nd-gen/packages/swc/components/avatar/stories/avatar.stories.ts @@ -77,7 +77,7 @@ export default meta; const PLACEHOLDER_SRC = 'https://picsum.photos/id/64/500/500'; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── // alt ?? '' guards against undefined produced by Storybook controls when @@ -98,7 +98,7 @@ export const Playground: Story = { ></swc-avatar> </div> `, - tags: ['autodocs', 'dev'], + tags: ['dev'], args: { src: PLACEHOLDER_SRC, alt: 'Jane Doe', @@ -126,17 +126,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * An avatar consists of: - * - * 1. **Image** — A circular clipped profile photo - * - * ### Content - * - * - `src`: URL of the profile image - * - `alt`: Text description for assistive technology. Pass `alt=""` to mark as decorative. - * - `size`: Numeric size token (50–1500). Defaults to `500` (40 px). - */ export const Anatomy: Story = { render: (args) => html` <swc-avatar src=${args.src} alt=${args.alt} size=${args.size}></swc-avatar> @@ -153,12 +142,6 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * Avatars come in 17 sizes from 50 to 1500, ranging from 16 px to 104 px. - * Sizes 50–700 match 1st-gen; sizes 800–1500 are new in Spectrum 2. - * - * The default size is `500` (40 px). - */ export const Sizes: Story = { render: (args) => html` ${AVATAR_VALID_SIZES.map( @@ -176,18 +159,10 @@ export const Sizes: Story = { }, parameters: { flexLayout: 'row-wrap', - 'section-order': 1, }, tags: ['options'], }; -/** - * Use the `decorative` attribute and `alt=""` to treat the avatar as decorative — - * the image is hidden from assistive technology. - * - * Use this **only when the surrounding context already identifies the person** - * (e.g., their name appears next to the avatar). - */ export const Decorative: Story = { render: (args) => html` <swc-avatar @@ -202,49 +177,9 @@ export const Decorative: Story = { src: PLACEHOLDER_SRC, size: '500', }, - parameters: { 'section-order': 2 }, tags: ['options'], }; -// ────────────────────────────── -// BEHAVIORS STORIES -// ────────────────────────────── - -/** - * An avatar can be placed inside an action button to create a user-triggered - * action tied to a specific person or entity. - */ -export const InActionButton: Story = { - // TODO: Replace <button> with <swc-action-button> once that component is migrated to 2nd-gen. - render: (args) => html` - <button - type="button" - style="display:inline-flex;align-items:center;gap:8px;padding:4px 12px;cursor:pointer;" - > - <swc-avatar - src=${args.src} - alt=${args.alt} - size=${args.size} - ></swc-avatar> - Jane Doe - </button> - `, - args: { - src: PLACEHOLDER_SRC, - alt: 'Jane Doe', - size: '100', - }, - tags: ['behaviors'], -}; - -/** - * Use the `outline` attribute to render a solid outline around the avatar - * image. This is useful when the avatar's image border color matches the - * surrounding background. The outline uses `--swc-avatar-outline-width` - * (currently 1 px) for sizes 50–900 and a hardcoded 2 px for sizes 1000–1500, - * matching the Spectrum 2 specification. Within an Avatar Group, `outline` - * defaults to `true` to visually separate stacked avatars. - */ export const Outline: Story = { render: (args) => html` <div @@ -268,16 +203,9 @@ export const Outline: Story = { src: PLACEHOLDER_SRC, alt: 'Jane Doe', }, - parameters: { 'section-order': 3 }, tags: ['options'], }; -/** - * A disabled avatar indicates that the entity is not currently active or - * available. The avatar remains visible in the layout at reduced opacity, - * communicating that it may become active later. It remains in the - * accessibility tree — `disabled` is purely visual. - */ export const Disabled: Story = { render: (args) => html` <swc-avatar @@ -292,57 +220,40 @@ export const Disabled: Story = { alt: 'Jane Doe', size: '500', }, - parameters: { 'section-order': 4 }, tags: ['options'], }; -// ────────────────────────────────── -// UPCOMING FEATURES STORIES -// ────────────────────────────────── +// ────────────────────────────── +// BEHAVIORS STORIES +// ────────────────────────────── -/** - * ### Additional avatar types - * - * - **Gradient image**: Shows a generated colorful gradient when no photo is available - * - **Initials**: Shows the user's initials inside the avatar circle as a photo fallback - * - **Guest**: Shows a default guest icon when no user identity is known - * - * ### Avatar Group - * - * - Display a collection of avatars in a stacked layout with configurable overlap and overflow - */ -export const UpcomingFeatures: Story = { - tags: ['upcoming', 'description-only'], +export const InActionButton: Story = { + // TODO: Replace <button> with <swc-action-button> once that component is migrated to 2nd-gen. + render: (args) => html` + <button + type="button" + style="display:inline-flex;align-items:center;gap:8px;padding:4px 12px;cursor:pointer;" + > + <swc-avatar + src=${args.src} + alt=${args.alt} + size=${args.size} + ></swc-avatar> + Jane Doe + </button> + `, + args: { + src: PLACEHOLDER_SRC, + alt: 'Jane Doe', + size: '100', + }, + tags: ['behaviors'], }; -UpcomingFeatures.storyName = 'Upcoming features'; // ──────────────────────────────── // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-avatar>` element implements several accessibility features: - * - * #### Alt text - * - * - Provide a descriptive `alt` value identifying the person or entity depicted - * - Pass `alt=""` and set the `decorative` attribute when the name already appears in context - * - * #### Non-interactive element - * - * - Avatars have no interactive behavior and are not focusable - * - Screen readers announce the image using the `alt` attribute value - * - No keyboard interaction is required or expected - * - To make an avatar a link, wrap it in a standard `<a>` element and set `aria-label` on the anchor when the destination is not clear from surrounding context - * - * ### Best practices - * - * - Always set `alt` — omitting it causes some screen readers to announce the image URL - * - Use `alt=""` and `decorative` attribute only when the person is identified by adjacent text - * - Keep alt text short and descriptive: prefer `"Jane Doe"` over `"Profile photo of Jane Doe"` - */ export const Accessibility: Story = { render: (args) => html` <swc-avatar src=${args.src} alt="Jane Doe" size=${args.size}></swc-avatar> diff --git a/2nd-gen/packages/swc/components/badge/badge.mdx b/2nd-gen/packages/swc/components/badge/badge.mdx new file mode 100644 index 0000000000..ce59964ad4 --- /dev/null +++ b/2nd-gen/packages/swc/components/badge/badge.mdx @@ -0,0 +1,185 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as BadgeStories from './stories/badge.stories'; + +<Meta of={BadgeStories} /> + +<DocsHeader /> + +## Anatomy + +A badge consists of: + +1. **Container** — colored background with rounded corners +2. **Label** — text content describing the status or category (required) +3. **Icon** (optional) — visual indicator positioned before the label + +### Content + +- **Default slot**: text content describing the status or category (required for accessibility) +- **icon slot**: optional visual indicator positioned before the label + +<Canvas of={BadgeStories.Anatomy} /> + +## Upcoming features + +### Notification and indicator badge types + +- **Notification**: displays a numeric count to signal unread or pending items, such as a message counter on an icon +- **Indicator**: a dot-only badge that signals activity or updated content without showing a count + +## Options + +### Sizes + +Badges come in four sizes to fit various contexts: + +- **Small (`s`)**: default size; compact spaces, inline with text, or in tables +- **Medium (`m`)**: common usage when slightly more emphasis is needed +- **Large (`l`)**: increased emphasis in cards or content areas +- **Extra-large (`xl`)**: maximum visibility for primary status indicators + +The `s` size is the default. Use larger sizes sparingly to create a hierarchy of importance on a page. + +<Canvas of={BadgeStories.Sizes} /> + +### Semantic variants + +Semantic variants provide meaning through color and should be used when status has specific significance. +These variants align consistently with other design system components that use the same semantic meanings. + +Use these variants for the following statuses: + +- **accent**: new, beta, prototype, draft +- **informative**: active, in use, live, published +- **neutral**: archived, deleted, paused, not started, ended +- **positive**: approved, complete, success, purchased, licensed +- **notice**: pending, expiring soon, limited, deprecated +- **negative**: rejected, error, alert, failed + +<Canvas of={BadgeStories.SemanticVariants} /> + +### Non-semantic variants + +Non-semantic variants use distinctive colors for visual categorization without inherent meaning. +These are ideal for color-coding categories, teams, or projects, especially when there are 8 categories or fewer. + +Use non-semantic variants when: + +- Categories do not have universal status meanings +- Visual distinction matters more than semantic meaning +- Creating department, team, or project color schemes + +> **Note**: 2nd-gen adds `pink`, `turquoise`, `brown`, `cinnamon`, and `silver` variants. + +<Canvas of={BadgeStories.NonSemanticVariants} /> + +### Outline + +The `outline` style provides a bordered appearance with a transparent background. +This style reduces visual weight while maintaining semantic meaning. + +> **Important**: The outline style is only valid for semantic variants (`accent`, `informative`, `neutral`, `positive`, `notice`, `negative`). +> Attempting to use `outline` with non-semantic color variants will not apply the style. + +<Canvas of={BadgeStories.Outline} /> + +### Subtle + +The `subtle` style reduces visual prominence with a softer background fill. +Unlike outline, subtle is available for **all** variants (semantic and non-semantic). + +Use the subtle style when: + +- Multiple badges appear together and need less visual competition +- Status is secondary to main content +- Maintaining design system color palette while reducing emphasis + +<Canvas of={BadgeStories.Subtle} /> + +### Fixed + +The `fixed` attribute adjusts border radius based on edge positioning, creating the appearance that the badge is "fixed" to a UI edge. + +Fixed positioning options: + +- **block-start**: top edge (removes top-left and top-right border radius) +- **block-end**: bottom edge (removes bottom-left and bottom-right border radius) +- **inline-start**: left edge (removes top-left and bottom-left border radius) +- **inline-end**: right edge (removes top-right and bottom-right border radius) + +This is purely visual styling: actual positioning must be handled separately with CSS. + +<Canvas of={BadgeStories.Fixed} /> + +## Behaviors + +### Text Wrapping + +When a badge's label is too long for the available horizontal space, it wraps to form multiple lines. +Text wrapping can be controlled by applying a `max-inline-size` constraint to the badge. + +This ensures badges remain readable even with longer status messages or category names. + +<Canvas of={BadgeStories.TextWrapping} /> + +### Inline + +Badges flow naturally within prose text to annotate inline content such as headings, labels, list items, or table cells. + +Because `<swc-badge>` renders as `inline-flex`, it participates in the normal text flow without any extra wrapper styling required. Use small (`s`) badges in most inline contexts to avoid disrupting line height. + +<Canvas of={BadgeStories.Inline} /> + +## Accessibility + +### Features + +The `<swc-badge>` element implements several accessibility features: + +#### Color meaning + +- Colors are used in combination with text labels and/or icons to ensure that status information is not conveyed through color alone +- Users with color vision deficiencies can understand badge meaning through text content +- High contrast mode is supported with appropriate color overrides + +#### Non-interactive element + +- Badges have no interactive behavior and are not focusable +- Screen readers will announce the badge content as static text +- No keyboard interaction is required or expected + +> **Important**: In focus mode, only interactive elements and their associated labels/descriptions are announced. If content is not a label or description for a focusable element, it will not be read. For non-interactive content, screen reader users must [switch to Browse mode](https://swcpreviews.z13.web.core.windows.net/pr-6122/docs/second-gen-storybook/?path=/docs/guides-accessibility-guides-screen-reader-testing--readme#screen-reader-modes). This is expected behavior, not a bug: ensure you test both modes when evaluating component accessibility. + +### Text label + +Badges with visible text are announced directly by screen readers. The text in the default slot is the accessible name. + +### Icon + text + +When an icon accompanies a text label, the icon is decorative and should be hidden from assistive technology. +Apply `aria-hidden="true"` to the `<swc-icon>` so screen readers only announce the label text. + +### Icon only + +When space is limited and no visible label is shown, the badge **must** have an accessible name. +Set `role="img"` and `aria-label` directly on the `<swc-badge>` element to describe the badge's meaning. +`role="img"` is required because custom elements have no implicit ARIA role: without it, `aria-label` is not permitted by the ARIA specification and will fail automated accessibility checks. +Without both attributes, the badge has no accessible name and fails WCAG 1.1.1. + +### Best practices + +- Use semantic variants (`positive`, `negative`, `notice`, `informative`, `neutral`, `accent`) when the status has specific meaning +- Include clear, descriptive labels that explain the status without relying on color alone +- For icon-only badges, always set `role="img"` and `aria-label` on `swc-badge` +- Ensure sufficient color contrast between the badge and its background +- Badges are not interactive elements: for interactive status indicators, consider using buttons, tags, or links instead +- When using multiple badges together, ensure they are clearly associated with their related content +- Use consistent badge variants across your application for the same statuses +- Test with screen readers to verify badge content is announced in context +- Consider placement carefully: badges should be close to the content they describe + +<Canvas of={BadgeStories.Accessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts b/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts index c8432edebb..f42884622d 100644 --- a/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts +++ b/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts @@ -179,7 +179,7 @@ const fixedLabels = { } as const satisfies Record<FixedValues, string>; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── export const Playground: Story = { @@ -212,7 +212,7 @@ export const Playground: Story = { 'default-slot': 'Active', 'icon-slot': undefined, }, - tags: ['autodocs', 'dev'], + tags: ['dev'], }; // ────────────────────────────── @@ -233,18 +233,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * A badge consists of: - * - * 1. **Container** - Colored background with rounded corners - * 2. **Label** - Text content describing the status or category (required) - * 3. **Icon** (optional) - Visual indicator positioned before the label - * - * ### Content - * - * - **Default slot**: Text content describing the status or category (required for accessibility) - * - **icon slot**: (optional) - Visual indicator positioned before the label - */ export const Anatomy: Story = { render: (args) => { const size = args.size as BadgeSize; @@ -278,16 +266,6 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * Badges come in four sizes to fit various contexts: - * - * - **Small (`s`)**: Default size; compact spaces, inline with text, or in tables - * - **Medium (`m`)**: Common usage when slightly more emphasis is needed - * - **Large (`l`)**: Increased emphasis in cards or content areas - * - **Extra-large (`xl`)**: Maximum visibility for primary status indicators - * - * The `s` size is the default. Use larger sizes sparingly to create a hierarchy of importance on a page. - */ export const Sizes: Story = { render: (args) => html` <div @@ -334,26 +312,13 @@ export const Sizes: Story = { )} </div> `, - parameters: { 'section-order': 1, flexLayout: 'column-stretch' }, + parameters: { flexLayout: 'column-stretch' }, tags: ['options'], args: { variant: 'neutral', }, }; -/** - * Semantic variants provide meaning through color and should be used when status has specific significance. - * These variants align consistently with other design system components that use the same semantic meanings. - * - * Use these variants for the following statuses: - * - * - **accent**: New, beta, prototype, draft - * - **informative**: Active, in use, live, published - * - **neutral**: Archived, deleted, paused, not started, ended - * - **positive**: Approved, complete, success, purchased, licensed - * - **notice**: Pending, expiring soon, limited, deprecated - * - **negative**: Rejected, error, alert, failed - */ export const SemanticVariants: Story = { render: (args) => html` ${BADGE_VARIANTS_SEMANTIC.map((variant) => @@ -364,22 +329,10 @@ export const SemanticVariants: Story = { }) )} `, - parameters: { 'section-order': 2 }, tags: ['options'], }; SemanticVariants.storyName = 'Semantic variants'; -/** - * Non-semantic variants use distinctive colors for visual categorization without inherent meaning. - * These are ideal for color-coding categories, teams, or projects - especially when there are 8 categories or fewer. - * - * Use non-semantic variants when: - * - Categories don't have universal status meanings - * - Visual distinction matters more than semantic meaning - * - Creating department, team, or project color schemes - * - * > **Note**: 2nd-gen adds `pink`, `turquoise`, `brown`, `cinnamon`, and `silver` variants. - */ export const NonSemanticVariants: Story = { render: (args) => html` ${BADGE_VARIANTS_COLOR.map((variant) => @@ -390,18 +343,10 @@ export const NonSemanticVariants: Story = { }) )} `, - parameters: { 'section-order': 3 }, tags: ['options'], }; NonSemanticVariants.storyName = 'Non-semantic variants'; -/** - * The `outline` style provides a bordered appearance with a transparent background. - * This style reduces visual weight while maintaining semantic meaning. - * - * **Important**: The outline style is only valid for semantic variants (`accent`, `informative`, `neutral`, `positive`, `notice`, `negative`). - * Attempting to use `outline` with non-semantic color variants will not apply the style. - */ export const Outline: Story = { render: (args) => html` ${BADGE_VARIANTS_SEMANTIC.map((variant) => @@ -413,19 +358,9 @@ export const Outline: Story = { }) )} `, - parameters: { 'section-order': 4 }, tags: ['options'], }; -/** - * The `subtle` style reduces visual prominence with a softer background fill. - * Unlike outline, subtle is available for **all** variants (semantic and non-semantic). - * - * Use subtle style when: - * - Multiple badges appear together and need less visual competition - * - Status is secondary to main content - * - Maintaining design system color palette while reducing emphasis - */ export const Subtle: Story = { render: (args) => html` ${BADGE_VARIANTS.map((variant) => @@ -437,24 +372,9 @@ export const Subtle: Story = { }) )} `, - parameters: { 'section-order': 5 }, tags: ['options'], }; -/** - * The `fixed` attribute adjusts border radius based on edge positioning, creating the appearance that the badge is "fixed" to a UI edge. - * - * Fixed positioning options: - * - * - **block-start**: Top edge (removes top-left and top-right border radius) - * - **block-end**: Bottom edge (removes bottom-left and bottom-right border radius) - * - **inline-start**: Left edge (removes top-left and bottom-left border radius) - * - **inline-end**: Right edge (removes top-right and bottom-right border radius) - * - * This is purely visual styling - actual positioning must be handled separately with CSS. - * - * All fixed positions shown below for comparison. - */ export const Fixed: Story = { render: (args) => html` ${FIXED_VALUES.map((fixed) => @@ -466,7 +386,6 @@ export const Fixed: Story = { }) )} `, - parameters: { 'section-order': 6 }, tags: ['options'], }; @@ -474,12 +393,6 @@ export const Fixed: Story = { // BEHAVIORS STORIES // ────────────────────────────── -/** - * When a badge's label is too long for the available horizontal space, it wraps to form multiple lines. - * Text wrapping can be controlled by applying a `max-inline-size` constraint to the badge. - * - * This ensures badges remain readable even with longer status messages or category names. - */ export const TextWrapping: Story = { render: (args) => html` ${template({ @@ -492,14 +405,6 @@ export const TextWrapping: Story = { tags: ['behaviors'], }; -/** - * Badges flow naturally within prose text to annotate inline content such as headings, - * labels, list items, or table cells. - * - * Because `<swc-badge>` renders as `inline-flex`, it participates in the normal - * text flow without any extra wrapper styling required. Use small (`s`) badges in - * most inline contexts to avoid disrupting line height. - */ export const Inline: Story = { render: (args) => html` <p> @@ -536,72 +441,10 @@ export const Inline: Story = { tags: ['behaviors'], }; -// ────────────────────────────────── -// UPCOMING FEATURES STORIES -// ────────────────────────────────── - -/** - * ### Notification and indicator badge types - * - * - **Notification**: Displays a numeric count to signal unread or pending items, such as a message counter on an icon - * - **Indicator**: A dot-only badge that signals activity or updated content without showing a count - */ -export const UpcomingFeatures: Story = { - tags: ['upcoming', 'description-only'], -}; -UpcomingFeatures.storyName = 'Upcoming features'; - // ──────────────────────────────── // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-badge>` element implements several accessibility features: - * - * #### Color meaning - * - * - Colors are used in combination with text labels and/or icons to ensure that status information is not conveyed through color alone - * - Users with color vision deficiencies can understand badge meaning through text content - * - High contrast mode is supported with appropriate color overrides - * - * #### Non-interactive element - * - * - Badges have no interactive behavior and are not focusable - * - Screen readers will announce the badge content as static text - * - No keyboard interaction is required or expected - * > Important: In focus mode, only interactive elements and their associated labels/descriptions are announced. If content is not a label or description for a focusable element, it will not be read. For non-interactive content, screen reader users must [switch to Browse mode](https://swcpreviews.z13.web.core.windows.net/pr-6122/docs/second-gen-storybook/?path=/docs/guides-accessibility-guides-screen-reader-testing--readme#screen-reader-modes). This is expected behavior, not a bug — ensure you test both modes when evaluating component accessibility. - - * ### Text label - * - * Badges with visible text are announced directly by screen readers. The text in the default slot is the accessible name. - * - * ### Icon + text - * - * When an icon accompanies a text label, the icon is decorative and should be hidden from assistive technology. - * Apply `aria-hidden="true"` to the `<swc-icon>` so screen readers only announce the label text. - * - * ### Icon only - * - * When space is limited and no visible label is shown, the badge **must** have an accessible name. - * Set `role="img"` and `aria-label` directly on the `<swc-badge>` element to describe the badge's meaning. - * `role="img"` is required because custom elements have no implicit ARIA role — without it, `aria-label` is not - * permitted by the ARIA specification and will fail automated accessibility checks. - * Without both attributes, the badge has no accessible name and fails WCAG 1.1.1. - * - * ### Best practices - * - * - Use semantic variants (`positive`, `negative`, `notice`, `informative`, `neutral`, `accent`) when the status has specific meaning - * - Include clear, descriptive labels that explain the status without relying on color alone - * - For icon-only badges, always set `role="img"` and `aria-label` on `swc-badge` - * - Ensure sufficient color contrast between the badge and its background - * - Badges are not interactive elements - for interactive status indicators, consider using buttons, tags, or links instead - * - When using multiple badges together, ensure they're clearly associated with their related content - * - Use consistent badge variants across your application for the same statuses - * - Test with screen readers to verify badge content is announced in context - * - Consider placement carefully - badges should be close to the content they describe - */ export const Accessibility: Story = { render: (args) => html` ${template({ diff --git a/2nd-gen/packages/swc/components/button/button.mdx b/2nd-gen/packages/swc/components/button/button.mdx new file mode 100644 index 0000000000..4fbba47f79 --- /dev/null +++ b/2nd-gen/packages/swc/components/button/button.mdx @@ -0,0 +1,146 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as ButtonStories from './stories/button.stories'; + +<Meta of={ButtonStories} /> + +<DocsHeader /> + +## Anatomy + +A button consists of: + +- **Default slot**: visible text label +- **icon slot**: optional leading icon + +When only an icon is provided (no label), the button renders as a circular icon-only button. Icon-only buttons must include an `accessible-label` attribute so the action is announced to screen reader users. + +<Canvas of={ButtonStories.Anatomy} /> + +## Upcoming features + +The following features are planned for future releases: + +- **Form submission** (`type="submit"` / `type="reset"`): native form participation. Until then, use a native `<button type="submit">` with [global button styles](/docs/guides-customization-global-element-styling--readme). +- **`genai` variant**: for generative AI actions. +- **`premium` variant**: for premium or upgrade flows. + +## Options + +### Sizes + +Buttons come in four sizes: small (`s`), medium (`m`), large (`l`), and extra-large (`xl`). Medium is the default. + +<Canvas of={ButtonStories.Sizes} /> + +### Variants + +Four variants are available: `primary` (default), `secondary`, `accent`, and `negative`. + +`accent` and `negative` are fill-only; `fill-style="outline"` is not supported for these variants. + +<Canvas of={ButtonStories.Variants} /> + +### Outline + +The `outline` fill style renders a transparent background with a visible border. `fill-style="outline"` is only supported with `primary` and `secondary` variants. Combining it with `accent` or `negative` emits a development warning and falls back to the fill appearance. + +<Canvas of={ButtonStories.Outline} /> + +### Static colors + +The `static-color` attribute pins the button's color to either `"white"` or `"black"` regardless of the active theme. `static-color` is only supported with `primary` and `secondary` variants. Both fill styles are supported. + +When combining a `static-color` with `fill-style="outline"`, verify that the background color maintains sufficient contrast on hover. + +<Canvas of={ButtonStories.StaticColors} /> + +## States + +### States + +Buttons support three interaction states: + +- **Default**: the button is active and can be interacted with. +- **Disabled**: the button is removed from the tab order and cannot be activated. +- **Pending**: the button remains focusable but activation is suppressed while an asynchronous operation is in progress. See [Pending in Behaviors](#pending) for the full behavioral details. + +Do not set both `disabled` and `pending` simultaneously. + +<Canvas of={ButtonStories.States} /> + +## Behaviors + +### Pending + +When `pending` is set, the button remains focusable but click events are suppressed. After a 1-second delay, the button enters its active pending appearance: disabled colors and an animated inline spinner. The delay prevents the pending appearance from flashing for operations that resolve quickly. + +The accessible name during pending defaults to the visible label plus `", busy"`. For example, a button labeled "Saving" announces as `"Saving, busy"`. Provide `pending-label` to override this with a more specific description of the in-progress operation. When pending clears, the accessible name reverts to the original label. + +Use the **Pending** checkbox above the buttons to trigger and clear the state interactively. This lets you observe the 1-second activation delay and verify the accessible name switch in browser DevTools. + +<Canvas of={ButtonStories.Pending} /> + +### Text wrapping + +When a button's label is too long to fit on one line, text wraps to multiple lines by default. When a leading icon is present, the label aligns to the start edge so wrapped lines stay visually anchored to the icon rather than centering under it. + +Text wrapping is the default behavior; no attribute is needed. To suppress it, use the `truncate` attribute instead. + +<Canvas of={ButtonStories.TextWrapping} /> + +### Truncate + +When `truncate` is set, overflowing label text is clipped to a single line and an ellipsis (`...`) is shown rather than wrapping. The focus ring uses `outline` rather than `box-shadow` so it remains fully visible even though `overflow: hidden` is required for truncation. + +Because the full text is not visible, consider pairing a truncated button with a tooltip or accessible description so users can discover the complete label when needed. This is not built into the component; it is a consumer responsibility. + +<Canvas of={ButtonStories.Truncate} /> + +### Justified + +The `justified` attribute makes the button stretch to fill the available inline space of its container. This is useful for full-width actions in constrained layouts such as a form footer or a narrow sidebar. + +The container must permit the button to grow. If the container uses `justify-content: center` (on a flex or grid layout), that may override the stretch and force the button to its intrinsic size. In that case, remove the centering constraint or wrap the button in a full-width block. + +<Canvas of={ButtonStories.Justified} /> + +## Accessibility + +### Features + +The `<swc-button>` element implements several accessibility features: + +1. **Native button semantics**: a real `<button>` inside shadow DOM provides the role, so assistive technologies see a genuine button, not a host with `role="button"`. +2. **Focus delegation**: `delegatesFocus: true` routes Tab focus and programmatic focus to the internal `<button>`, keeping the host out of the tab order. +3. **Keyboard activation**: Enter and Space both activate the button via native browser behavior; no custom keyboard handling is needed or added. +4. **Pending state**: when `pending` is true, `aria-disabled="true"` is set on the internal `<button>` and the accessible name becomes `"[label], busy"` (e.g., `"Save, busy"`). The button remains focusable so users can discover it is unavailable rather than losing track of it entirely. A custom `pending-label` overrides the derived busy name. +5. **Icon-only labeling**: when there is no visible text, the `accessible-label` attribute is forwarded as `aria-label` on the internal `<button>`. A debug warning is emitted in development when an icon-only button is missing `accessible-label`. + +### Best practices + +- Always provide an `accessible-label` for icon-only buttons so screen readers can announce the button's purpose. +- Prefer `accessible-label` over placing `aria-label` directly on the `<swc-button>` host, as it is intentionally forwarded to the internal native control. +- Do not set both `pending` and `disabled` at the same time. Use `pending` to keep the button focusable while unavailable, or `disabled` to remove it from the tab order entirely. +- For navigation, use a native `<a>` element and leverage [global element styles](/docs/guides-customization-global-element-styling--readme), not `<swc-button>`. The button element activates on both Enter and Space; links activate on Enter only. + +### Host event contract + +The `<swc-button>` host dispatches or forwards the following events: + +- **`click`**: bubbles from the internal `<button>`. Suppressed while `pending`. +- **`focusin` / `focusout`**: bubble naturally from the internal `<button>` through the shadow boundary. Attach listeners to the host to observe focus changes. + +Host-level `focus` and `blur` compatibility events are not part of the initial 2nd-gen Button scope. + +### Deferred support + +The following features are outside the initial 2nd-gen Button scope: + +- **Cross-root ARIA** (`aria-labelledby` / `aria-describedby` from outside the shadow root). +- **Form-associated `submit` / `reset` types**: the button currently behaves as `type="button"` only. Use native `<button type="submit">` or [global button styles](/docs/guides-customization-global-element-styling--readme) for form submission until this lands. + +<Canvas of={ButtonStories.Accessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/button/stories/button.stories.ts b/2nd-gen/packages/swc/components/button/stories/button.stories.ts index b4bb14eacb..e992b7f9f0 100644 --- a/2nd-gen/packages/swc/components/button/stories/button.stories.ts +++ b/2nd-gen/packages/swc/components/button/stories/button.stories.ts @@ -118,11 +118,11 @@ const staticColorLabels = { const addIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" aria-hidden="true" focusable="false"><path d="M31.5 17H19V4.5a1 1 0 0 0-2 0V17H4.5a1 1 0 0 0 0 2H17v12.5a1 1 0 0 0 2 0V19h12.5a1 1 0 0 0 0-2z"/></svg>`; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── export const Playground: Story = { - tags: ['autodocs', 'dev'], + tags: ['dev'], args: { variant: 'primary', 'fill-style': 'fill', @@ -149,16 +149,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * A button consists of: - * - * - **Default slot**: Visible text label - * - **icon slot**: Optional leading icon - * - * When only an icon is provided (no label), the button renders as a circular - * icon-only button. Icon-only buttons must include an `accessible-label` attribute - * so the action is announced to screen reader users. - */ export const Anatomy: Story = { render: (args) => html` ${template({ ...args, 'default-slot': 'Label only' })} @@ -199,43 +189,26 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * Buttons come in four sizes: small (`s`), medium (`m`), large (`l`), and - * extra-large (`xl`). Medium is the default. - */ export const Sizes: Story = { render: (args) => html` ${BUTTON_VALID_SIZES.map((size) => template({ ...args, size, 'default-slot': sizeLabels[size] }) )} `, - parameters: { flexLayout: 'row-wrap', 'section-order': 1 }, + parameters: { flexLayout: 'row-wrap' }, tags: ['options'], }; -/** - * Four variants are available: `primary` (default), `secondary`, `accent`, - * and `negative`. - * - * `accent` and `negative` are fill-only; `fill-style="outline"` is not - * supported for these variants. - */ export const Variants: Story = { render: (args) => html` ${BUTTON_VARIANTS.map((variant: ButtonVariant) => template({ ...args, variant, 'default-slot': variantLabels[variant] }) )} `, - parameters: { flexLayout: 'row-wrap', 'section-order': 2 }, + parameters: { flexLayout: 'row-wrap' }, tags: ['options'], }; -/** - * The `outline` fill style renders a transparent background with a visible - * border. `fill-style="outline"` is only supported with `primary` and - * `secondary` variants. Combining it with `accent` or `negative` emits a - * development warning and falls back to the fill appearance. - */ export const Outline: Story = { render: (args) => html` ${['primary', 'secondary'].map((variant) => @@ -247,18 +220,10 @@ export const Outline: Story = { }) )} `, - parameters: { flexLayout: 'row-wrap', 'section-order': 3 }, + parameters: { flexLayout: 'row-wrap' }, tags: ['options'], }; -/** - * The `static-color` attribute pins the button's color to either `"white"` or - * `"black"` regardless of the active theme. `static-color` is only supported - * with `primary` and `secondary` variants. Both fill styles are supported. - * - * When combining a `static-color` with `fill-style="outline"`, verify that the - * background color maintains sufficient contrast on hover. - */ export const StaticColors: Story = { render: (args) => html` <div @@ -288,7 +253,6 @@ export const StaticColors: Story = { `, parameters: { staticColorsDemo: true, - 'section-order': 4, }, tags: ['options', '!test'], }; @@ -298,18 +262,6 @@ StaticColors.storyName = 'Static colors'; // STATES STORIES // ────────────────────────── -/** - * Buttons support three interaction states: - * - * - **Default**: The button is active and can be interacted with. - * - **Disabled**: The button is removed from the tab order and cannot be - * activated. - * - **Pending**: The button remains focusable but activation is suppressed - * while an asynchronous operation is in progress. See the [Pending story - * in Behaviors](#pending) for the full behavioral details. - * - * Do not set both `disabled` and `pending` simultaneously. - */ export const States: Story = { render: (args) => html` ${template({ ...args, 'default-slot': 'Default' })} @@ -324,23 +276,6 @@ export const States: Story = { // BEHAVIORS STORIES // ────────────────────────────── -/** - * When `pending` is set, the button remains focusable but click events are - * suppressed. After a 1-second delay, the button enters its active pending - * appearance: disabled colors and an animated inline spinner. The delay - * prevents the pending appearance from flashing for operations that resolve - * quickly. - * - * The accessible name during pending defaults to the visible label plus - * `", busy"`. For example, a button labeled "Saving" announces as - * `"Saving, busy"`. Provide `pending-label` to override this with a more - * specific description of the in-progress operation. When pending clears, the - * accessible name reverts to the original label. - * - * Use the **Pending** checkbox above the buttons to trigger and clear the - * state interactively. This lets you observe the 1-second activation delay - * and verify the accessible name switch in browser DevTools. - */ export const Pending: Story = { render: (args) => { let pending = false; @@ -378,18 +313,8 @@ export const Pending: Story = { `; }, tags: ['behaviors', '!test'], - parameters: { 'section-order': 1 }, }; -/** - * When a button's label is too long to fit on one line, text wraps to - * multiple lines by default. When a leading icon is present, the label - * aligns to the start edge so wrapped lines stay visually anchored to the - * icon rather than centering under it. - * - * Text wrapping is the default behavior; no attribute is needed. To - * suppress it, use the `truncate` attribute instead. - */ export const TextWrapping: Story = { render: (args) => html` ${template({ @@ -405,21 +330,10 @@ export const TextWrapping: Story = { })} `, tags: ['behaviors'], - parameters: { flexLayout: 'row-wrap', 'section-order': 2 }, + parameters: { flexLayout: 'row-wrap' }, }; TextWrapping.storyName = 'Text wrapping'; -/** - * When `truncate` is set, overflowing label text is clipped to a single line - * and an ellipsis (`...`) is shown rather than wrapping. The focus ring uses - * `outline` rather than `box-shadow` so it remains fully visible even though - * `overflow: hidden` is required for truncation. - * - * Because the full text is not visible, consider pairing a truncated button - * with a tooltip or accessible description so users can discover the complete - * label when needed. This is not built into the component; it is a - * consumer responsibility. - */ export const Truncate: Story = { render: (args) => template({ @@ -429,19 +343,8 @@ export const Truncate: Story = { style: 'max-inline-size: 200px', }), tags: ['behaviors'], - parameters: { 'section-order': 3 }, }; -/** - * The `justified` attribute makes the button stretch to fill the available - * inline space of its container. This is useful for full-width actions in - * constrained layouts such as a form footer or a narrow sidebar. - * - * The container must permit the button to grow. If the container uses - * `justify-content: center` (on a flex or grid layout), that may override - * the stretch and force the button to its intrinsic size. In that case, - * remove the centering constraint or wrap the button in a full-width block. - */ export const Justified: Story = { render: (args) => html` <div style="inline-size: min(40ch, 100%); margin-inline: auto;"> @@ -452,7 +355,7 @@ export const Justified: Story = { })} </div> `, - parameters: { layout: 'padded', 'section-order': 4 }, + parameters: { layout: 'padded' }, tags: ['behaviors'], }; @@ -460,57 +363,6 @@ export const Justified: Story = { // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-button>` element implements several accessibility features: - * - * 1. **Native button semantics**: A real `<button>` inside shadow DOM provides the role, so - * assistive technologies see a genuine button, not a host with `role="button"`. - * 2. **Focus delegation**: `delegatesFocus: true` routes Tab focus and programmatic focus to - * the internal `<button>`, keeping the host out of the tab order. - * 3. **Keyboard activation**: Enter and Space both activate the button via native browser - * behavior; no custom keyboard handling is needed or added. - * 4. **Pending state**: When `pending` is true, `aria-disabled="true"` is set on the internal - * `<button>` and the accessible name becomes `"[label], busy"` (e.g., `"Save, busy"`). - * The button remains focusable so users can discover it is unavailable rather than losing - * track of it entirely. A custom `pending-label` overrides the derived busy name. - * 5. **Icon-only labeling**: When there is no visible text, the `accessible-label` attribute - * is forwarded as `aria-label` on the internal `<button>`. A debug warning is emitted in - * development when an icon-only button is missing `accessible-label`. - * - * ### Best practices - * - * - Always provide an `accessible-label` for icon-only buttons so screen readers can - * announce the button's purpose. - * - Prefer `accessible-label` over placing `aria-label` directly on the `<swc-button>` host, - * as it is intentionally forwarded to the internal native control. - * - Do not set both `pending` and `disabled` at the same time. Use `pending` to keep the - * button focusable while unavailable, or `disabled` to remove it from the tab order entirely. - * - For navigation, use a native `<a>` element and leverage [global element styles](/docs/guides-customization-global-element-styling--readme), not `<swc-button>`. The - * button element activates on both Enter and Space; links activate on Enter only. - * - * ### Host event contract - * - * The `<swc-button>` host dispatches or forwards the following events: - * - * - **`click`**: Bubbles from the internal `<button>`. Suppressed while `pending`. - * - **`focusin` / `focusout`**: Bubble naturally from the internal `<button>` through - * the shadow boundary. Attach listeners to the host to observe focus changes. - * - * Host-level `focus` and `blur` compatibility events are not part of the initial - * 2nd-gen Button scope. - * - * ### Deferred support - * - * The following features are outside the initial 2nd-gen Button scope: - * - * - **Cross-root ARIA** (`aria-labelledby` / `aria-describedby` from outside the - * shadow root). - * - **Form-associated `submit` / `reset` types**: the button currently behaves as - * `type="button"` only. Use native `<button type="submit">` or [global button styles](/docs/guides-customization-global-element-styling--readme) - * for form submission until this lands. - */ export const Accessibility: Story = { render: (args) => html` ${template({ ...args, 'default-slot': 'Save document' })} @@ -542,21 +394,3 @@ export const Accessibility: Story = { tags: ['a11y'], parameters: { flexLayout: 'row-wrap' }, }; - -// ──────────────────────────────────── -// UPCOMING FEATURES STORIES -// ──────────────────────────────────── - -/** - * The following features are planned for future releases: - * - * - **Form submission** (`type="submit"` / `type="reset"`): Native form participation. - * Until then, use a native `<button type="submit">` with - * [global button styles](/docs/guides-customization-global-element-styling--readme). - * - **`genai` variant**: For generative AI actions. - * - **`premium` variant**: For premium or upgrade flows. - */ -export const UpcomingFeatures: Story = { - tags: ['upcoming', 'description-only'], -}; -UpcomingFeatures.storyName = 'Upcoming features'; diff --git a/2nd-gen/packages/swc/components/color-loupe/color-loupe.mdx b/2nd-gen/packages/swc/components/color-loupe/color-loupe.mdx new file mode 100644 index 0000000000..b7c90f55c9 --- /dev/null +++ b/2nd-gen/packages/swc/components/color-loupe/color-loupe.mdx @@ -0,0 +1,85 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as ColorLoupeStories from './stories/color-loupe.stories'; + +<Meta of={ColorLoupeStories} /> + +<DocsHeader /> + +## Anatomy + +A color loupe consists of: + +1. **Floating loupe element**: a teardrop-shaped container positioned above the interaction point, with an inner and outer border +2. **Color preview**: displays the currently picked color over an opacity checkerboard so transparency is visible + +<Canvas of={ColorLoupeStories.Anatomy} /> + +## Options + +### Colors + +The `color` property accepts any valid CSS color string: + +- **Named colors**: `yellow`, `red`, `blue`, etc. +- **Hex**: `#ff0000` +- **RGB/RGBA**: `rgba(44, 62, 224, 0.81)`: alpha transparency reveals the checkerboard +- **HSL**: `hsl(111, 82%, 56%)` + +When using transparent colors, the opacity checkerboard pattern shows through, giving a clear visual indication of the transparency level. + +<Canvas of={ColorLoupeStories.Colors} /> + +## States + +### Open and closed states + +The color loupe has two visibility states: + +- **Open**: fully visible with `opacity: 1` and no vertical offset +- **Closed** (default): hidden via `opacity: 0` and a downward transform + +The transition between states is animated with CSS transitions on `opacity` (125 ms) and `transform` (100 ms). + +<Canvas of={ColorLoupeStories.OpenAndClosedStates} /> + +## Behaviors + +### Parent-driven visibility + +The color loupe's `open` state is entirely managed by its parent color component; the loupe does not manage its own visibility. A parent such as `<swc-color-handle>` controls when it appears: + +- **Touch input**: the loupe automatically appears during touch interactions with any color component (`<swc-color-area>`, `<swc-color-slider>`, `<swc-color-wheel>`) to prevent the finger from obscuring the selected color +- **Mouse/stylus input**: the loupe remains hidden by default for precision pointing devices +- **Parent control**: the parent sets `open` to `true` when the user is actively selecting a color and back to `false` when the interaction ends + +The loupe animates its visibility with CSS transitions: `opacity` over 125 ms and `transform` (vertical offset) over 100 ms. The button below simulates that trigger relationship. + +<Canvas of={ColorLoupeStories.ParentDrivenVisibility} /> + +## Accessibility + +### Features + +The `<swc-color-loupe>` element is a **visual-only** component: + +#### ARIA implementation + +- **SVG is `aria-hidden="true"`**: the loupe graphic is decorative and hidden from the accessibility tree +- **Not focusable**: the component has no tab stop and no keyboard interaction + +#### Accessibility model + +The loupe does not represent a standalone accessible control. Accessibility semantics (name, value, role) are provided by the **parent** color selection component, for example `<swc-color-area>`, `<swc-color-slider>`, or `<swc-color-wheel>`. The loupe simply reflects the currently picked color as a visual aid during touch interactions. + +### Best practices + +- Never use the color loupe as the sole means of communicating a color value: always pair it with labeled controls that expose the value to assistive technology +- Ensure the parent color component provides appropriate labeling via visible text or ARIA (for example, `aria-label` on `<swc-color-area>`) +- Do not add `role`, `aria-label`, or focus management to the loupe itself: it is intentionally inert +- Avoid conveying meaning through the loupe color alone; pair color selection with text labels or other indicators as appropriate + +<Canvas of={ColorLoupeStories.Accessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/color-loupe/stories/color-loupe.stories.ts b/2nd-gen/packages/swc/components/color-loupe/stories/color-loupe.stories.ts index 19036d0aa6..d34373cd61 100644 --- a/2nd-gen/packages/swc/components/color-loupe/stories/color-loupe.stories.ts +++ b/2nd-gen/packages/swc/components/color-loupe/stories/color-loupe.stories.ts @@ -91,7 +91,7 @@ const labeledLoupe = ( `; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── export const Playground: Story = { @@ -99,7 +99,7 @@ export const Playground: Story = { open: true, color: 'rgba(0, 128, 255, 0.7)', }, - tags: ['autodocs', 'dev'], + tags: ['dev'], }; // ──────────────────── @@ -118,12 +118,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * A color loupe consists of: - * - * 1. **Floating loupe element** - A teardrop-shaped container positioned above the interaction point, with an inner and outer border - * 2. **Color preview** - Displays the currently picked color over an opacity checkerboard so transparency is visible - */ export const Anatomy: Story = { render: (args) => html` ${template({ ...args, open: true, color: 'rgba(255, 0, 0, 0.5)' })} @@ -135,19 +129,6 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * The `color` property accepts any valid CSS color string: - * - * - **Named colors**: `yellow`, `red`, `blue`, etc. - * - **Hex**: `#ff0000` - * - **RGB/RGBA**: `rgba(44, 62, 224, 0.81)` — alpha transparency reveals the checkerboard - * - **HSL**: `hsl(111, 82%, 56%)` - * - * When using transparent colors, the opacity checkerboard pattern shows through, - * giving a clear visual indication of the transparency level. - * - * All color formats shown below for comparison. - */ export const Colors: Story = { render: (args) => html` ${COLOR_FORMATS.map(({ label, color }) => @@ -157,7 +138,6 @@ export const Colors: Story = { tags: ['options'], parameters: { flexLayout: 'row-wrap', - 'section-order': 1, }, }; @@ -165,15 +145,6 @@ export const Colors: Story = { // STATES STORIES // ────────────────────────── -/** - * The color loupe has two visibility states: - * - * - **Open**: Fully visible with `opacity: 1` and no vertical offset - * - **Closed** (default): Hidden via `opacity: 0` and a downward transform - * - * The transition between states is animated with CSS transitions on - * `opacity` (125 ms) and `transform` (100 ms). - */ export const OpenAndClosedStates: Story = { render: (args) => html` ${labeledLoupe('Open', { @@ -198,23 +169,6 @@ OpenAndClosedStates.storyName = 'Open and closed states'; // BEHAVIORS STORIES // ────────────────────────────── -/** - * The color loupe's `open` state is entirely managed by its parent color - * component; the loupe does not manage its own visibility. A parent such as - * `<swc-color-handle>` controls when it appears: - * - * - **Touch input**: The loupe automatically appears during touch interactions - * with any color component (`<swc-color-area>`, `<swc-color-slider>`, - * `<swc-color-wheel>`) to prevent the finger from obscuring the selected color - * - **Mouse/stylus input**: The loupe remains hidden by default for precision - * pointing devices - * - **Parent control**: The parent sets `open` to `true` when the user is actively - * selecting a color and back to `false` when the interaction ends - * - * The loupe animates its visibility with CSS transitions: `opacity` over - * 125 ms and `transform` (vertical offset) over 100 ms. The button below - * simulates that trigger relationship. - */ export const ParentDrivenVisibility: Story = { loaders: [() => import('@adobe/spectrum-wc/components/button/swc-button.js')], render: (args) => { @@ -267,37 +221,6 @@ ParentDrivenVisibility.storyName = 'Parent-driven visibility'; // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-color-loupe>` element is a **visual-only** component: - * - * #### ARIA implementation - * - * - **SVG is `aria-hidden="true"`**: The loupe graphic is decorative and - * hidden from the accessibility tree - * - **Not focusable**: The component has no tab stop and no keyboard interaction - * - * #### Accessibility model - * - * The loupe does not represent a standalone accessible control. - * Accessibility semantics (name, value, role) are provided by the - * **parent** color selection component — for example, `<swc-color-area>`, - * `<swc-color-slider>`, or `<swc-color-wheel>`. The loupe simply reflects - * the currently picked color as a visual aid during touch interactions. - * - * ### Best practices - * - * - Never use the color loupe as the sole means of communicating a color - * value — always pair it with labeled controls that expose the value - * to assistive technology - * - Ensure the parent color component provides appropriate labeling via - * visible text or ARIA (for example, `aria-label` on `<swc-color-area>`) - * - Do not add `role`, `aria-label`, or focus management to the loupe - * itself — it is intentionally inert - * - Avoid conveying meaning through the loupe color alone; pair color - * selection with text labels or other indicators as appropriate - */ export const Accessibility: Story = { args: { open: true, diff --git a/2nd-gen/packages/swc/components/divider/divider.mdx b/2nd-gen/packages/swc/components/divider/divider.mdx new file mode 100644 index 0000000000..07a1300dc9 --- /dev/null +++ b/2nd-gen/packages/swc/components/divider/divider.mdx @@ -0,0 +1,88 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as DividerStories from './stories/divider.stories'; + +<Meta of={DividerStories} /> + +<DocsHeader /> + +## Anatomy + +A divider consists of: + +1. **Line**: the visual separator element that creates visual separation between content + +<Canvas of={DividerStories.Anatomy} /> + +## Options + +### Sizes + +Dividers come in three sizes to fit various contexts: + +- **Small (`s`)**: used to divide similar components such as table rows, action button groups, and components within a panel +- **Medium (`m`)**: used for dividing subsections on a page, or to separate different groupings of components such as panels, rails, etc. +- **Large (`l`)**: should only be used for page titles or section titles + +<Canvas of={DividerStories.Sizes} /> + +### Vertical + +The default horizontal divider is used to separate content stacked vertically. To separate horizontal content, use the `vertical` attribute. + +<Canvas of={DividerStories.Vertical} /> + +### Static colors + +Use the `static-color` attribute when displaying over images or colored backgrounds: + +- **white**: use on dark or colored backgrounds for better contrast +- **black**: use on light backgrounds for better contrast + +Each static color panel shows **horizontal** dividers at sizes `s`, `m`, and `l`, and **vertical** dividers at the same sizes (vertical rows use an explicit `block-size` so the line is visible). + +<Canvas of={DividerStories.StaticColors} /> + +## Behaviors + +### Layout orientation + +Dividers can be oriented **horizontally** (default) or **vertically** to match the layout they serve: + +- **Horizontal dividers** separate stacked content, such as sections beneath headings +- **Vertical dividers** separate side-by-side items in a flex row (e.g., navigation breadcrumbs, toolbars) + +Vertical dividers require the parent to have an **explicit height** (`block-size`): without it, `block-size: 100%` resolves to zero and the divider is invisible. + +<Canvas of={DividerStories.LayoutOrientation} /> + +## Accessibility + +### Features + +The `<swc-divider>` element implements several accessibility features: + +#### ARIA implementation + +1. **ARIA role**: automatically sets `role="separator"` to ensure proper semantic meaning for assistive technologies +2. **Orientation support**: when `vertical` is true, automatically sets `aria-orientation="vertical"` to indicate the divider's orientation +3. **Not focusable**: a `separator` is not focusable and receives no keyboard interaction. Only separators that double as interactive resize handles are focusable (and require `aria-valuenow`, `aria-valuemin`, and `aria-valuemax`). `<swc-divider>` is a static separator and should never be made focusable. + +#### Visual accessibility + +- Dividers use sufficient thickness and color contrast to be perceivable +- Static color variants ensure contrast on different backgrounds +- High contrast mode is supported with appropriate color overrides + +### Best practices + +- Place dividers between complete content sections, not between a heading and its associated body text +- Use dividers purposefully: they should reinforce existing content groupings, not substitute for clear heading structure +- Use dividers sparingly; excessive use can diminish their visual impact +- Ensure sufficient color contrast when using `static-color` variants on colored backgrounds +- Do not rely on dividers alone to communicate section boundaries to screen reader users; heading structure and landmark regions carry that responsibility + +<Canvas of={DividerStories.Accessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/divider/stories/divider.stories.ts b/2nd-gen/packages/swc/components/divider/stories/divider.stories.ts index 1b03e76c18..edbaaa44ac 100644 --- a/2nd-gen/packages/swc/components/divider/stories/divider.stories.ts +++ b/2nd-gen/packages/swc/components/divider/stories/divider.stories.ts @@ -86,11 +86,11 @@ const meta: Meta = { export default meta; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── export const Playground: Story = { - tags: ['autodocs', 'dev'], + tags: ['dev'], args: { size: 'm', vertical: false, @@ -127,11 +127,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * A divider consists of: - * - * 1. **Line** - The visual separator element that creates visual separation between content - */ export const Anatomy: Story = { render: (args) => html` <h4>Account settings</h4> @@ -150,13 +145,6 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * Dividers come in three sizes to fit various contexts: - * - * - **Small (`s`)**: Used to divide similar components such as table rows, action button groups, and components within a panel - * - **Medium (`m`)**: Used for dividing subsections on a page, or to separate different groupings of components such as panels, rails, etc. - * - **Large (`l`)**: Should only be used for page titles or section titles - */ export const Sizes: Story = { render: (args) => html` <div> @@ -181,16 +169,9 @@ export const Sizes: Story = { <p>Monitor activity and analytics.</p> </div> `, - parameters: { - 'section-order': 1, - }, tags: ['options'], }; -/** - * The default horizontal divider is used to separate content stacked vertically. To separate - * horizontal content, use the `vertical` attribute. - */ export const Vertical: Story = { render: (args) => html` <div @@ -223,7 +204,6 @@ export const Vertical: Story = { `, parameters: { flexLayout: 'column-center', - 'section-order': 2, }, tags: ['options'], args: { @@ -248,15 +228,6 @@ const STATIC_COLORS_VERTICAL_SAMPLES = [ { size: 'l' as const, blockSize: 32 }, ]; -/** - * Use the `static-color` attribute when displaying over images or colored backgrounds: - * - * - **white**: Use on dark or colored backgrounds for better contrast - * - **black**: Use on light backgrounds for better contrast - * - * Each static color panel shows **horizontal** dividers at sizes `s`, `m`, and `l`, and **vertical** - * dividers at the same sizes (vertical rows use an explicit `block-size` so the line is visible). - */ export const StaticColors: Story = { render: (args) => html` ${['white', 'black'].map( @@ -312,7 +283,6 @@ export const StaticColors: Story = { `, parameters: { staticColorsDemo: true, - 'section-order': 3, styles: { 'align-items': 'flex-start', }, @@ -325,16 +295,6 @@ StaticColors.storyName = 'Static colors'; // BEHAVIORS STORIES // ────────────────────────────── -/** - * Dividers can be oriented **horizontally** (default) or **vertically** to match - * the layout they serve: - * - * - **Horizontal dividers** separate stacked content, such as sections beneath headings - * - **Vertical dividers** separate side-by-side items in a flex row (e.g., navigation breadcrumbs, toolbars) - * - * Vertical dividers require the parent to have an **explicit height** (`block-size`) — - * without it, `block-size: 100%` resolves to zero and the divider is invisible. - */ export const LayoutOrientation: Story = { render: (args) => html` <nav @@ -367,31 +327,6 @@ LayoutOrientation.storyName = 'Layout orientation'; // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-divider>` element implements several accessibility features: - * - * #### ARIA implementation - * - * 1. **ARIA role**: Automatically sets `role="separator"` to ensure proper semantic meaning for assistive technologies - * 2. **Orientation support**: When `vertical` is true, automatically sets `aria-orientation="vertical"` to indicate the divider's orientation - * 3. **Not focusable**: A `separator` is not focusable and receives no keyboard interaction. Only separators that double as interactive resize handles are focusable (and require `aria-valuenow`, `aria-valuemin`, and `aria-valuemax`). `<swc-divider>` is a static separator and should never be made focusable. - * - * #### Visual accessibility - * - * - Dividers use sufficient thickness and color contrast to be perceivable - * - Static color variants ensure contrast on different backgrounds - * - High contrast mode is supported with appropriate color overrides - * - * ### Best practices - * - * - Place dividers between complete content sections, not between a heading and its associated body text - * - Use dividers purposefully — they should reinforce existing content groupings, not substitute for clear heading structure - * - Use dividers sparingly; excessive use can diminish their visual impact - * - Ensure sufficient color contrast when using `static-color` variants on colored backgrounds - * - Do not rely on dividers alone to communicate section boundaries to screen reader users; heading structure and landmark regions carry that responsibility - */ export const Accessibility: Story = { render: (args) => html` <h4>Project overview</h4> diff --git a/2nd-gen/packages/swc/components/icon/icon.internal.mdx b/2nd-gen/packages/swc/components/icon/icon.internal.mdx new file mode 100644 index 0000000000..023a645861 --- /dev/null +++ b/2nd-gen/packages/swc/components/icon/icon.internal.mdx @@ -0,0 +1,81 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as IconStories from './stories/icon.internal.stories'; + +<Meta of={IconStories} /> + +<DocsHeader /> + +## Anatomy + +An icon consists of: + +1. **Rendered graphic**: a shared slotted SVG template + +### Content + +- Default slot: provide SVG markup to render. + +<Canvas of={IconStories.Anatomy} /> + +## Options + +### Sizes + +Icons come in five sizes to fit different layout contexts: + +- **Extra-small (`xs`)**: dense UIs and compact metadata rows +- **Small (`s`)**: supporting iconography in constrained spaces +- **Medium (`m`)**: default size for general component usage +- **Large (`l`)**: prominent icon usage in larger controls +- **Extra-large (`xl`)**: high-emphasis icon presentation + +<Canvas of={IconStories.Sizes} /> + +### Sources + +#### Shared templates + +Import reusable templates from `../elements/index.js` and slot them into `<swc-icon>`. This keeps icon usage centralized and avoids per-component SVG duplication. + +<Canvas of={IconStories.Sources} /> + +### Shared Templates + +Use the shared icon catalog to keep icon usage consistent across components. + +Example import: + +```ts +import { Chevron100Icon } from '../elements/index.js'; +``` + +<Canvas of={IconStories.SharedTemplates} /> + +### Available Icons + +Available shared icons in the current internal catalog. Use this story as a quick reference for what can be imported from `../elements/index.js`. + +<Canvas of={IconStories.AvailableIcons} /> + +## Accessibility + +### Features + +The `<swc-icon>` element implements several accessibility features: + +#### SVG labeling + +- Slotted SVGs receive `role="img"` and use `aria-label` when `label` is provided +- When no label is provided, slotted SVGs are marked `aria-hidden="true"` + +### Best practices + +- Always provide a descriptive `label` for informative icons +- Use empty labels only for purely decorative icons +- Keep labels short and specific (e.g., "Search" instead of "Icon") + +<Canvas of={IconStories.Accessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/icon/stories/icon.internal.stories.ts b/2nd-gen/packages/swc/components/icon/stories/icon.internal.stories.ts index a533607d8b..d6eda1e736 100644 --- a/2nd-gen/packages/swc/components/icon/stories/icon.internal.stories.ts +++ b/2nd-gen/packages/swc/components/icon/stories/icon.internal.stories.ts @@ -82,11 +82,11 @@ const iconCardStyles = { } as const; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── export const Playground: Story = { - tags: ['autodocs', 'dev'], + tags: ['dev'], render: (args) => template(args, iconSvg), args: { label: 'Search', @@ -111,15 +111,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * An icon consists of: - * - * 1. **Rendered graphic** - A shared slotted SVG template - * - * ### Content - * - * - Default slot: Provide SVG markup to render. - */ export const Anatomy: Story = { render: (args) => template({ ...args, label: args.label || 'Chevron icon' }, iconSvg), @@ -130,17 +121,6 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * Icons come in five sizes to fit different layout contexts: - * - * - **Extra-small (`xs`)**: Dense UIs and compact metadata rows - * - **Small (`s`)**: Supporting iconography in constrained spaces - * - **Medium (`m`)**: Default size for general component usage - * - **Large (`l`)**: Prominent icon usage in larger controls - * - **Extra-large (`xl`)**: High-emphasis icon presentation - * - * All sizes are shown below for comparison. - */ export const Sizes: Story = { render: (args) => html` ${ICON_VALID_SIZES.map((size) => @@ -153,47 +133,21 @@ export const Sizes: Story = { tags: ['options'], parameters: { flexLayout: 'row-wrap', - 'section-order': 1, }, }; -/** - * ### Shared templates - * - * Import reusable templates from `../elements/index.js` and slot them into `<swc-icon>`. - * This keeps icon usage centralized and avoids per-component SVG duplication. - */ export const Sources: Story = { render: (args) => template({ ...args, label: args.label || 'Chevron icon' }, iconSvg), tags: ['options'], - parameters: { - 'section-order': 2, - }, }; -/** - * Use the shared icon catalog to keep icon usage consistent across components. - * - * Example import: - * - * ```ts - * import { Chevron100Icon } from '../elements/index.js'; - * ``` - */ export const SharedTemplates: Story = { render: (args) => template({ ...args, label: args.label || 'Chevron' }, Chevron100Icon()), tags: ['options'], - parameters: { - 'section-order': 3, - }, }; -/** - * Available shared icons in the current internal catalog. - * Use this story as a quick reference for what can be imported from `../elements/index.js`. - */ export const AvailableIcons: Story = { render: (args) => { const catalog = Object.entries(iconElements) @@ -228,7 +182,6 @@ export const AvailableIcons: Story = { }, }, flexLayout: 'row-wrap', - 'section-order': 4, }, }; @@ -236,22 +189,6 @@ export const AvailableIcons: Story = { // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-icon>` element implements several accessibility features: - * - * #### SVG labeling - * - * - Slotted SVGs receive `role="img"` and use `aria-label` when `label` is provided - * - When no label is provided, slotted SVGs are marked `aria-hidden="true"` - * - * ### Best practices - * - * - Always provide a descriptive `label` for informative icons - * - Use empty labels only for purely decorative icons - * - Keep labels short and specific (e.g., "Search" instead of "Icon") - */ export const Accessibility: Story = { render: (args) => template(args, iconSvg), tags: ['a11y'], diff --git a/2nd-gen/packages/swc/components/illustrated-message/illustrated-message.mdx b/2nd-gen/packages/swc/components/illustrated-message/illustrated-message.mdx new file mode 100644 index 0000000000..25465a5e4c --- /dev/null +++ b/2nd-gen/packages/swc/components/illustrated-message/illustrated-message.mdx @@ -0,0 +1,85 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as IllustratedMessageStories from './stories/illustrated-message.stories'; + +<Meta of={IllustratedMessageStories} /> + +<DocsHeader /> + +## Anatomy + +An illustrated message consists of three slots: + +1. **Default slot**: decorative or informative SVG illustration displayed above (vertical) or beside (horizontal) the heading and description. Decorative SVGs must have `aria-hidden="true"`. Informative SVGs must have `role="img"` and `aria-label`. +2. **`heading` slot**: consumer-provided `<h2>`–`<h6>` element. The component does not own the heading tag; the consumer chooses the level to match the document outline. The component warns in development if a non-heading element is used. +3. **`description` slot** (optional): supporting text that elaborates on the heading. May contain phrasing content, links, or action elements. + +<Canvas of={IllustratedMessageStories.Anatomy} /> + +## Upcoming features + +### Button group slot + +- Add a group of action buttons alongside the heading and description using the `button-group` slot + +## Options + +### Sizes + +Illustrated messages come in three sizes to fit various contexts: + +- **Small (`s`)**: 96px illustration, compact spacing, for space-constrained contexts +- **Medium (`m`)**: 96px illustration, standard spacing, the default +- **Large (`l`)**: 160px illustration, reduced spacing, for prominent empty states + +<Canvas of={IllustratedMessageStories.Sizes} /> + +### Orientation + +Illustrated messages support two layout orientations: + +- **Vertical** (default): illustration stacked above the heading and description, centered. Use for full-page or centered empty states. +- **Horizontal**: illustration beside the heading and description in a row, left-aligned. Use for inline or sidebar empty states. + +<Canvas of={IllustratedMessageStories.Orientation} /> + +### Heading levels + +The `heading` slot accepts any `<h2>`–`<h6>` element. Choose the level that fits the surrounding document outline. The component validates but does not restrict the heading level. + +Common patterns: + +- `<h2>` for page-level empty states with no other headings in scope +- `<h3>` for empty states nested inside a page section with its own `<h2>` +- `<h4>` or deeper for empty states inside nested panels or cards + +<Canvas of={IllustratedMessageStories.HeadingLevels} /> + +## Behaviors + +### Description with link + +Place an `<a>` element inside the `description` slot to provide a call-to-action link alongside the supporting text. + +<Canvas of={IllustratedMessageStories.DescriptionWithLink} /> + +## Accessibility + +### Features + +The `<swc-illustrated-message>` element implements the following accessibility features: + +1. **Heading ownership**: the consumer provides the `<h2>`–`<h6>` element directly in the `heading` slot, preserving full control over heading level and ensuring it participates in the document outline. +2. **Illustration intent**: SVGs in the default slot must declare their accessibility intent explicitly via `aria-hidden` or `role="img"`. +3. **No redundant ARIA**: the component adds no `role` or `aria-*` attributes of its own; semantics come entirely from slotted content. + +### Best practices + +- Decorative illustrations (most common): add `aria-hidden="true"` so screen readers skip the graphic and move directly to the heading. +- Informative illustrations: add `role="img"` and `aria-label` (or an inline `<title>`) so screen readers announce the illustration's meaning before reading the heading and description. +- Choose the heading level (`h2`–`h6`) that fits the surrounding document outline, not based on visual size. + +<Canvas of={IllustratedMessageStories.IllustrationAccessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/illustrated-message/stories/illustrated-message.stories.ts b/2nd-gen/packages/swc/components/illustrated-message/stories/illustrated-message.stories.ts index 2f6867fe74..efda24c17f 100644 --- a/2nd-gen/packages/swc/components/illustrated-message/stories/illustrated-message.stories.ts +++ b/2nd-gen/packages/swc/components/illustrated-message/stories/illustrated-message.stories.ts @@ -98,7 +98,7 @@ const defaultSlots = html` `; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── export const Playground: Story = { @@ -106,7 +106,7 @@ export const Playground: Story = { orientation: 'vertical', }, render: (args) => template(args, defaultSlots), - tags: ['autodocs', 'dev'], + tags: ['dev'], }; // ────────────────────────── @@ -122,20 +122,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * An illustrated message consists of three slots: - * - * 1. **Default slot**: Decorative or informative SVG illustration displayed - * above (vertical) or beside (horizontal) the heading and description. - * Decorative SVGs must have `aria-hidden="true"`. Informative SVGs must have - * `role="img"` and `aria-label`. - * 2. **`heading` slot**: Consumer-provided `<h2>`–`<h6>` element. The component - * does not own the heading tag; the consumer chooses the level to match the - * document outline. The component warns in development if a non-heading element - * is used. - * 3. **`description` slot** (optional): Supporting text that elaborates on the - * heading. May contain phrasing content, links, or action elements. - */ export const Anatomy: Story = { render: (args) => html` ${template( @@ -164,13 +150,6 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * Illustrated messages come in three sizes to fit various contexts: - * - * - **Small (`s`)**: 96px illustration, compact spacing, for space-constrained contexts - * - **Medium (`m`)**: 96px illustration, standard spacing, the default - * - **Large (`l`)**: 160px illustration, reduced spacing, for prominent empty states - */ export const Sizes: Story = { render: (args) => html` ${template( @@ -201,18 +180,9 @@ export const Sizes: Story = { tags: ['options'], parameters: { flexLayout: true, - 'section-order': 1, }, }; -/** - * Illustrated messages support two layout orientations: - * - * - **Vertical** (default): illustration stacked above the heading and description, - * centered. Use for full-page or centered empty states. - * - **Horizontal**: illustration beside the heading and description in a row, - * left-aligned. Use for inline or sidebar empty states. - */ export const Orientation: Story = { render: (args) => html` ${template( @@ -235,21 +205,9 @@ export const Orientation: Story = { tags: ['options'], parameters: { styles: { display: 'flex', 'flex-direction': 'column', gap: '2rem' }, - 'section-order': 2, }, }; -/** - * The `heading` slot accepts any `<h2>`–`<h6>` element. Choose the level that - * fits the surrounding document outline. The component validates but does not - * restrict the heading level. - * - * Common patterns: - * - * - `<h2>` for page-level empty states with no other headings in scope - * - `<h3>` for empty states nested inside a page section with its own `<h2>` - * - `<h4>` or deeper for empty states inside nested panels or cards - */ export const HeadingLevels: Story = { render: (args) => html` ${template( @@ -280,7 +238,6 @@ export const HeadingLevels: Story = { tags: ['options'], parameters: { styles: { display: 'flex', 'flex-direction': 'column', gap: '3rem' }, - 'section-order': 3, }, }; HeadingLevels.storyName = 'Heading levels'; @@ -289,10 +246,6 @@ HeadingLevels.storyName = 'Heading levels'; // BEHAVIORS STORIES // ────────────────────────────── -/** - * Place an `<a>` element inside the `description` slot to provide a - * call-to-action link alongside the supporting text. - */ export const DescriptionWithLink: Story = { render: (args) => html` ${template( @@ -311,48 +264,10 @@ export const DescriptionWithLink: Story = { }; DescriptionWithLink.storyName = 'Description with link'; -// ──────────────────────────────── -// UPCOMING FEATURES STORIES -// ──────────────────────────────── - -/** - * ### Button group slot - * - * - Add a group of action buttons alongside the heading and description using the `button-group` slot - */ -export const UpcomingFeatures: Story = { - tags: ['upcoming', 'description-only'], -}; -UpcomingFeatures.storyName = 'Upcoming features'; - // ──────────────────────────────── // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-illustrated-message>` element implements the following - * accessibility features: - * - * 1. **Heading ownership**: The consumer provides the `<h2>`–`<h6>` element - * directly in the `heading` slot, preserving full control over heading - * level and ensuring it participates in the document outline. - * 2. **Illustration intent**: SVGs in the default slot must declare their - * accessibility intent explicitly via `aria-hidden` or `role="img"`. - * 3. **No redundant ARIA**: The component adds no `role` or `aria-*` - * attributes of its own; semantics come entirely from slotted content. - * - * ### Best practices - * - * - Decorative illustrations (most common): add `aria-hidden="true"` so - * screen readers skip the graphic and move directly to the heading. - * - Informative illustrations: add `role="img"` and `aria-label` (or an - * inline `<title>`) so screen readers announce the illustration's meaning - * before reading the heading and description. - * - Choose the heading level (`h2`–`h6`) that fits the surrounding document - * outline, not based on visual size. - */ export const IllustrationAccessibility: Story = { render: (args) => html` ${template( diff --git a/2nd-gen/packages/swc/components/progress-circle/progress-circle.mdx b/2nd-gen/packages/swc/components/progress-circle/progress-circle.mdx new file mode 100644 index 0000000000..1d2c163886 --- /dev/null +++ b/2nd-gen/packages/swc/components/progress-circle/progress-circle.mdx @@ -0,0 +1,113 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as ProgressCircleStories from './stories/progress-circle.stories'; + +<Meta of={ProgressCircleStories} /> + +<DocsHeader /> + +## Anatomy + +A progress circle consists of: + +1. **Track**: background ring showing the full progress range +2. **Fill**: colored ring segment showing current progress +3. **Label**: accessible text describing the operation (not visually rendered). + +> **A11y Note**: Light DOM children are not projected into the shadow tree, so content between the opening and closing tags does not supply an accessible name. Use the `label` attribute or property, or `aria-label` / `aria-labelledby` on the host. + +<Canvas of={ProgressCircleStories.Anatomy} /> + +## Options + +### Sizes + +Progress circles come in three sizes to fit various contexts: + +- **Small (`s`)**: used for inline indicators or space-constrained areas, such as in tables or alongside small text +- **Medium (`m`)**: default size, used for typical loading states in cards, forms, or content areas +- **Large (`l`)**: used for prominent loading states, primary content areas, or full-page loading indicators + +<Canvas of={ProgressCircleStories.Sizes} /> + +### Static Colors + +Use the `static-color` attribute when displaying over images or colored backgrounds: + +- **white**: use on dark or colored backgrounds for better contrast +- **black**: use on light backgrounds for better contrast + +Each panel lists **small**, **medium**, and **large** in order so Chromatic can snapshot every static-color and size pairing with the same label text (stroke weight is what changes). + +<Canvas of={ProgressCircleStories.StaticColors} /> + +## States + +### Progress values + +Progress circles can show specific progress values from 0% to 100%. Set the `progress` attribute to a value between 0 and 100 to represent determinate progress. This automatically sets `aria-valuenow` to the provided value for screen readers. + +<Canvas of={ProgressCircleStories.ProgressValues} /> + +### Indeterminate + +The default state: when `progress` is not set, the component displays an animated loading indicator. The `aria-valuenow` attribute is omitted, signaling to assistive technologies that the operation duration is unknown. + +Use the default (no `progress`) when: + +- The operation duration is unknown +- Progress cannot be accurately measured +- Multiple sub-operations are running in parallel + +<Canvas of={ProgressCircleStories.Indeterminate} /> + +## Accessibility + +### Features + +The `<swc-progress-circle>` element implements several accessibility features: + +#### ARIA implementation + +1. **ARIA role**: automatically sets `role="progressbar"` for proper semantic meaning +2. **Labeling**: uses the `label` attribute value as `aria-label`, or rely on `aria-label` / `aria-labelledby` you set on the host +3. **Progress state** (determinate): + - Sets `aria-valuenow` with the current `progress` value +4. **Loading state** (indeterminate, default when `progress` is unset): + - When no `progress` value is set, `aria-valuenow` is omitted + - Screen readers understand this as an ongoing operation with unknown duration +5. **Status communication**: screen readers announce progress updates as values change + +#### Visual accessibility + +- Progress is shown visually through the fill amount, not relying solely on color +- High contrast mode is supported with appropriate color overrides +- Static color variants ensure sufficient contrast on different backgrounds +- At `progress="0"`, a small amount of fill is still rendered intentionally. A fully empty circle would fail WCAG 1.4.11 (non-text contrast) because the track alone may not meet the required 3:1 contrast ratio against the background. The `aria-valuenow` attribute still reflects 0 + +#### Non-interactive element + +- Progress circles have no interactive behavior and are not focusable +- Screen readers will announce the progress circle content as static text +- No keyboard interaction is required or expected + +> **Important**: In focus mode, only interactive elements and their associated labels/descriptions are announced. If content is not a label or description for a focusable element, it will not be read. For non-interactive content, screen reader users must [switch to Browse mode](https://swcpreviews.z13.web.core.windows.net/pr-6122/docs/second-gen-storybook/?path=/docs/guides-accessibility-guides-screen-reader-testing--readme#screen-reader-modes). This is expected behavior, not a bug: ensure you test both modes when evaluating component accessibility. + +### Best practices + +- Always provide a descriptive `label` that explains what the progress represents +- Use specific, meaningful labels (e.g., "Uploading profile photo" instead of "Loading") +- Use determinate progress (`progress="50"`) when possible to give users a clear sense of completion +- For determinate progress, ensure the `progress` value accurately reflects the actual progress +- Use indeterminate progress only when duration is truly unknown or when the wait is less than 3 seconds. +- Consider using `size="l"` for primary loading states to improve visibility +- Ensure sufficient color contrast between the progress circle and its background +- Use `static-color="white"` on dark backgrounds or `static-color="black"` on light backgrounds +- Test with screen readers to verify progress announcements are clear and timely +- Avoid updating progress values more frequently than every 1-2 seconds to prevent announcement overload +- Do not force live region announcements for progress durations that are 3 seconds or less. Instead, consider status messages when progress is complete or there is an error + +<Canvas of={ProgressCircleStories.Accessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/progress-circle/stories/progress-circle.stories.ts b/2nd-gen/packages/swc/components/progress-circle/stories/progress-circle.stories.ts index 13b81922cc..c215ef3d5d 100644 --- a/2nd-gen/packages/swc/components/progress-circle/stories/progress-circle.stories.ts +++ b/2nd-gen/packages/swc/components/progress-circle/stories/progress-circle.stories.ts @@ -85,11 +85,11 @@ const sizeLabels = { } as const satisfies Record<ProgressCircleSize, string>; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── export const Playground: Story = { - tags: ['autodocs', 'dev'], + tags: ['dev'], args: { size: 'm', label: 'Processing request', @@ -111,15 +111,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * A progress circle consists of: - * - * 1. **Track** - Background ring showing the full progress range - * 2. **Fill** - Colored ring segment showing current progress - * 3. **Label** - Accessible text describing the operation (not visually rendered). - * - * > **A11y Note:** Light DOM children are not projected into the shadow tree, so content between the opening and closing tags does not supply an accessible name. Use the `label` attribute or property, or `aria-label` / `aria-labelledby` on the host. - */ export const Anatomy: Story = { render: () => html` <swc-progress-circle @@ -145,13 +136,6 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * Progress circles come in three sizes to fit various contexts: - * - * - **Small (`s`)**: Used for inline indicators or space-constrained areas, such as in tables or alongside small text - * - **Medium (`m`)**: Default size, used for typical loading states in cards, forms, or content areas - * - **Large (`l`)**: Used for prominent loading states, primary content areas, or full-page loading indicators - */ export const Sizes: Story = { render: (args) => html` ${PROGRESS_CIRCLE_VALID_SIZES.map( @@ -167,15 +151,6 @@ export const Sizes: Story = { tags: ['options'], }; -/** - * Use the `static-color` attribute when displaying over images or colored backgrounds: - * - * - **white**: Use on dark or colored backgrounds for better contrast - * - **black**: Use on light backgrounds for better contrast - * - * Each panel lists **small**, **medium**, and **large** in order so Chromatic can snapshot every - * static-color and size pairing with the same label text (stroke weight is what changes). - */ export const StaticColors: Story = { render: (args) => html` ${PROGRESS_CIRCLE_STATIC_COLORS.map( @@ -198,7 +173,6 @@ export const StaticColors: Story = { tags: ['options'], parameters: { staticColorsDemo: true, - 'section-order': 2, styles: { 'align-items': 'flex-start', }, @@ -209,11 +183,6 @@ export const StaticColors: Story = { // STATES STORIES // ────────────────────────── -/** - * Progress circles can show specific progress values from 0% to 100%. - * Set the `progress` attribute to a value between 0 and 100 to represent determinate progress. - * This automatically sets `aria-valuenow` to the provided value for screen readers. - */ export const ProgressValues: Story = { render: (args) => html` ${template({ ...args, progress: 0, label: 'Starting download' })} @@ -226,22 +195,9 @@ export const ProgressValues: Story = { args: { size: 'm', }, - parameters: { - 'section-order': 2, - }, }; ProgressValues.storyName = 'Progress values'; -/** - * The default state — when `progress` is not set, the component displays an animated - * loading indicator. The `aria-valuenow` attribute is omitted, signaling to assistive - * technologies that the operation duration is unknown. - * - * Use the default (no `progress`) when: - * - The operation duration is unknown - * - Progress cannot be accurately measured - * - Multiple sub-operations are running in parallel - */ export const Indeterminate: Story = { tags: ['states'], args: { @@ -253,52 +209,6 @@ export const Indeterminate: Story = { // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-progress-circle>` element implements several accessibility features: - * - * #### ARIA implementation - * - * 1. **ARIA role**: Automatically sets `role="progressbar"` for proper semantic meaning - * 2. **Labeling**: Uses the `label` attribute value as `aria-label`, or rely on `aria-label` / `aria-labelledby` you set on the host - * 3. **Progress state** (determinate): - * - Sets `aria-valuenow` with the current `progress` value - * 4. **Loading state** (indeterminate — default when `progress` is unset): - * - When no `progress` value is set, `aria-valuenow` is omitted - * - Screen readers understand this as an ongoing operation with unknown duration - * 5. **Status communication**: Screen readers announce progress updates as values change - * - * #### Visual accessibility - * - * - Progress is shown visually through the fill amount, not relying solely on color - * - High contrast mode is supported with appropriate color overrides - * - Static color variants ensure sufficient contrast on different backgrounds - * - At `progress="0"`, a small amount of fill is still rendered intentionally. A fully empty circle - * would fail WCAG 1.4.11 (non-text contrast) because the track alone may not meet the required - * 3:1 contrast ratio against the background. The `aria-valuenow` attribute still reflects 0 - * - * #### Non-interactive element - * - * - Progress circles have no interactive behavior and are not focusable - * - Screen readers will announce the progress circle content as static text - * - No keyboard interaction is required or expected - * - * > Important: In focus mode, only interactive elements and their associated labels/descriptions are announced. If content is not a label or description for a focusable element, it will not be read. For non-interactive content, screen reader users must [switch to Browse mode](https://swcpreviews.z13.web.core.windows.net/pr-6122/docs/second-gen-storybook/?path=/docs/guides-accessibility-guides-screen-reader-testing--readme#screen-reader-modes). This is expected behavior, not a bug — ensure you test both modes when evaluating component accessibility. - * ### Best practices - * - * - Always provide a descriptive `label` that explains what the progress represents - * - Use specific, meaningful labels (e.g., "Uploading profile photo" instead of "Loading") - * - Use determinate progress (`progress="50"`) when possible to give users a clear sense of completion - * - For determinate progress, ensure the `progress` value accurately reflects the actual progress - * - Use indeterminate progress only when duration is truly unknown or when the wait is less than 3 seconds. - * - Consider using `size="l"` for primary loading states to improve visibility - * - Ensure sufficient color contrast between the progress circle and its background - * - Use `static-color="white"` on dark backgrounds or `static-color="black"` on light backgrounds - * - Test with screen readers to verify progress announcements are clear and timely - * - Avoid updating progress values more frequently than every 1-2 seconds to prevent announcement overload - * - Do not force live region announcements for progress durations that are 3 seconds or less. Instead, consider status messages when progress is complete or there is an error - */ export const Accessibility: Story = { tags: ['a11y'], args: { diff --git a/2nd-gen/packages/swc/components/status-light/status-light.mdx b/2nd-gen/packages/swc/components/status-light/status-light.mdx new file mode 100644 index 0000000000..55bd65377e --- /dev/null +++ b/2nd-gen/packages/swc/components/status-light/status-light.mdx @@ -0,0 +1,103 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as StatusLightStories from './stories/status-light.stories'; + +<Meta of={StatusLightStories} /> + +<DocsHeader /> + +## Anatomy + +A status light consists of: + +1. **Colored dot indicator**: visual representation of status or category +2. **Text label**: descriptive text providing context + +### Content + +- **Default slot**: text content describing the status or category (required for accessibility) + +<Canvas of={StatusLightStories.Anatomy} /> + +## Options + +### Sizes + +Status lights come in four sizes to fit various contexts: + +- **Small (`s`)**: used for inline indicators or space-constrained areas +- **Medium (`m`)**: default size, used for typical use cases +- **Large (`l`)**: used for prominent displays or primary content areas +- **Extra-large (`xl`)**: maximum visibility for high-priority statuses + +<Canvas of={StatusLightStories.Sizes} /> + +### Semantic Variants + +Semantic variants provide meaning through color: + +- **`neutral`**: default variant — archived, deleted, paused, draft, not started, ended +- **`info`**: active, in use, live, published +- **`positive`**: approved, complete, success, new, purchased, licensed +- **`notice`**: needs approval, pending, scheduled, syncing, indexing, processing +- **`negative`**: error, alert, rejected, failed + +Semantic status lights should never be used for color coding categories or labels, and vice versa. + +<Canvas of={StatusLightStories.SemanticVariants} /> + +### Non-semantic variants + +Non-semantic variants use color-coded categories, ideal for data visualization and labeling. Best used when there are **8 or fewer** categories being color coded. + +> **Note**: The `pink`, `turquoise`, `brown`, `cinnamon`, and `silver` variants are new in 2nd-gen and not available in 1st-gen. + +<Canvas of={StatusLightStories.NonSemanticVariants} /> + +## Behaviors + +### Text Wrapping + +When the text is too long for the horizontal space available, it wraps to form another line. You can control the wrapping behavior by setting a `max-inline-size` style on the component. + +<Canvas of={StatusLightStories.TextWrapping} /> + +## Accessibility + +### Features + +The `<swc-status-light>` element implements several accessibility features: + +#### Visual accessibility + +- Status information is conveyed through both color and text labels, not relying on color alone +- High contrast mode is supported with appropriate color overrides +- Sufficient color contrast is maintained between the status dot and background + +#### Semantic meaning + +- Semantic variants provide consistent color associations for common statuses +- Text labels provide clear context for all users +- Disabled status lights are deprecated for Spectrum 2. Content like "Unavailable" may be used to communicate that concept instead. + +#### Non-interactive element + +- Status lights have no interactive behavior and are not focusable +- Screen readers will announce the status light content as static text +- No keyboard interaction is required or expected + +> **Important**: In focus mode, only interactive elements and their associated labels/descriptions are announced. If content is not a label or description for a focusable element, it will not be read. For non-interactive content, screen reader users must [switch to Browse mode](https://swcpreviews.z13.web.core.windows.net/pr-6122/docs/second-gen-storybook/?path=/docs/guides-accessibility-guides-screen-reader-testing--readme#screen-reader-modes). This is expected behavior, not a bug: ensure you test both modes when evaluating component accessibility. + +### Best practices + +- Always provide a descriptive text label that explains the status +- Use semantic variants (`neutral` (default), `info`, `positive`, `negative`, `notice`) when the status has specific meaning +- Status lights are not interactive elements: for interactive status indicators, consider using buttons, tags, or links instead +- Use meaningful, specific labels (e.g., "Approved" instead of "Green") +- Ensure sufficient color contrast between the status light and its background +- For non-semantic variants, ensure the text label provides complete context + +<Canvas of={StatusLightStories.Accessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/status-light/stories/status-light.stories.ts b/2nd-gen/packages/swc/components/status-light/stories/status-light.stories.ts index 5e6c6cb281..69254bc15f 100644 --- a/2nd-gen/packages/swc/components/status-light/stories/status-light.stories.ts +++ b/2nd-gen/packages/swc/components/status-light/stories/status-light.stories.ts @@ -124,11 +124,11 @@ const sizeLabels = { } as const satisfies Record<StatusLightSize, string>; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── export const Playground: Story = { - tags: ['autodocs', 'dev'], + tags: ['dev'], args: { 'default-slot': semanticLabels.neutral, }, @@ -149,16 +149,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * A status light consists of: - * - * 1. **Colored dot indicator** - Visual representation of status or category - * 2. **Text label** - Descriptive text providing context - * - * ### Content - * - * - **Default slot**: Text content describing the status or category (required for accessibility) - */ export const Anatomy: Story = { render: (args) => html` ${template({ @@ -174,16 +164,6 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * Status lights come in four sizes to fit various contexts: - * - * - **Small (`s`)**: Used for inline indicators or space-constrained areas - * - **Medium (`m`)**: Default size, used for typical use cases - * - **Large (`l`)**: Used for prominent displays or primary content areas - * - **Extra-large (`xl`)**: Maximum visibility for high-priority statuses - * - * All sizes shown below for comparison. - */ export const Sizes: Story = { render: (args) => html` ${STATUS_LIGHT_VALID_SIZES.map((size) => @@ -194,21 +174,9 @@ export const Sizes: Story = { }) )} `, - parameters: { 'section-order': 1 }, tags: ['options'], }; -/** - * Semantic variants provide meaning through color: - * - * - **`neutral`**: Default variant — archived, deleted, paused, draft, not started, ended - * - **`info`**: Active, in use, live, published - * - **`positive`**: Approved, complete, success, new, purchased, licensed - * - **`notice`**: Needs approval, pending, scheduled, syncing, indexing, processing - * - **`negative`**: Error, alert, rejected, failed - * - * Semantic status lights should never be used for color coding categories or labels, and vice versa. - */ export const SemanticVariants: Story = { render: (args) => html` ${STATUS_LIGHT_VARIANTS_SEMANTIC.map( @@ -221,7 +189,6 @@ export const SemanticVariants: Story = { )} `, parameters: { - 'section-order': 2, a11y: { // @todo Known issue: neutral variant has color contrast of 4.39:1 vs required 4.5:1 // Exclude only the neutral variant from color-contrast checks @@ -237,12 +204,6 @@ export const SemanticVariants: Story = { tags: ['options'], }; -/** - * Non-semantic variants use color-coded categories, ideal for data visualization and labeling. - * Best used when there are **8 or fewer** categories being color coded. - * - * **Note**: The `pink`, `turquoise`, `brown`, `cinnamon`, and `silver` variants are new in 2nd-gen and not available in 1st-gen. - */ export const NonSemanticVariants: Story = { render: (args) => html` ${STATUS_LIGHT_VARIANTS_COLOR.map((variant: StatusLightColorVariant) => @@ -253,7 +214,6 @@ export const NonSemanticVariants: Story = { }) )} `, - parameters: { 'section-order': 3 }, tags: ['options'], }; NonSemanticVariants.storyName = 'Non-semantic variants'; @@ -262,10 +222,6 @@ NonSemanticVariants.storyName = 'Non-semantic variants'; // BEHAVIORS STORIES // ────────────────────────────── -/** - * When the text is too long for the horizontal space available, it wraps to form another line. - * You can control the wrapping behavior by setting a `max-inline-size` style on the component. - */ export const TextWrapping: Story = { render: (args) => html` ${template({ @@ -283,39 +239,6 @@ export const TextWrapping: Story = { // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-status-light>` element implements several accessibility features: - * - * #### Visual accessibility - * - * - Status information is conveyed through both color and text labels, not relying on color alone - * - High contrast mode is supported with appropriate color overrides - * - Sufficient color contrast is maintained between the status dot and background - * - * #### Semantic meaning - * - * - Semantic variants provide consistent color associations for common statuses - * - Text labels provide clear context for all users - * - Disabled status lights are deprecated for Spectrum 2. Content like "Unavailable" may be used to communicate that concept instead. - * - * #### Non-interactive element - * - * - Status lights have no interactive behavior and are not focusable - * - Screen readers will announce the status light content as static text - * - No keyboard interaction is required or expected - * - * > Important: In focus mode, only interactive elements and their associated labels/descriptions are announced. If content is not a label or description for a focusable element, it will not be read. For non-interactive content, screen reader users must [switch to Browse mode](https://swcpreviews.z13.web.core.windows.net/pr-6122/docs/second-gen-storybook/?path=/docs/guides-accessibility-guides-screen-reader-testing--readme#screen-reader-modes). This is expected behavior, not a bug — ensure you test both modes when evaluating component accessibility. - * ### Best practices - * - * - Always provide a descriptive text label that explains the status - * - Use semantic variants (`neutral` (default), `info`, `positive`, `negative`, `notice`) when the status has specific meaning - * - Status lights are not interactive elements - for interactive status indicators, consider using buttons, tags, or links instead - * - Use meaningful, specific labels (e.g., "Approved" instead of "Green") - * - Ensure sufficient color contrast between the status light and its background - * - For non-semantic variants, ensure the text label provides complete context - */ export const Accessibility: Story = { render: (args) => html` ${template({ diff --git a/2nd-gen/packages/swc/components/tabs/stories/tabs.stories.ts b/2nd-gen/packages/swc/components/tabs/stories/tabs.stories.ts index 9a899747c5..8b03e7de84 100644 --- a/2nd-gen/packages/swc/components/tabs/stories/tabs.stories.ts +++ b/2nd-gen/packages/swc/components/tabs/stories/tabs.stories.ts @@ -164,11 +164,11 @@ const renderTabGroup = ({ `; // ──────────────────── -// AUTODOCS STORY +// PLAYGROUND STORY // ──────────────────── export const Playground: Story = { - tags: ['autodocs', 'dev'], + tags: ['dev'], }; // ────────────────────────── @@ -184,40 +184,6 @@ export const Overview: Story = { // ANATOMY STORIES // ────────────────────────── -/** - * ### Visual structure - * - * A tabs component consists of: - * - * 1. **Tab list** — A horizontal or vertical row of tab items - * 2. **Tab items** — Clickable labels representing each section - * 3. **Selection indicator** — A visual line highlighting the active tab - * 4. **Tab panels** — Content areas associated with each tab - * - * ### Technical structure - * - * #### Slots - * - * - **Default slot** (on `swc-tabs`): Accepts `swc-tab` elements - * - **tab-panel slot** (on `swc-tabs`): Accepts `swc-tab-panel` elements - * - **Default slot** (on `swc-tab`): Text label content - * - **icon slot** (on `swc-tab`): Optional icon displayed before the label - * - **Default slot** (on `swc-tab-panel`): Panel content (any HTML) - * - * #### Properties - * - * Properties that render visual content: - * - * - **selected** (on `swc-tabs`): Value of the currently active tab - * - **tab-id** (on `swc-tab` and `swc-tab-panel`): Unique identifier linking tab to panel - * - **accessible-label** (on `swc-tabs`): Accessible name for the tablist - * - **aria-label** (on `swc-tab`): Accessible name for icon-only tabs - * - **direction**: Layout direction (`horizontal` or `vertical`) - * - **keyboard-activation**: `manual` (default) or `automatic` (selection follows focus) - * - **density**: `regular` (default) or `compact` spacing - * - * Examples below: text-only, icon + text, and icon-only tabs. - */ export const Anatomy: Story = { render: () => html` <p><strong>Text-only tabs</strong></p> @@ -277,9 +243,6 @@ export const Anatomy: Story = { // OPTIONS STORIES // ────────────────────────── -/** - * `density="compact"` reduces spacing between tabs. Default is `regular`. - */ export const DensityVariants: Story = { render: () => html` <p><strong>Regular (default)</strong></p> @@ -292,25 +255,8 @@ export const DensityVariants: Story = { })} `, tags: ['options'], - parameters: { - 'section-order': 1, - }, }; -/** - * Tabs support horizontal (default) and vertical layout directions. - * - * - **Horizontal**: Tabs are laid out in a row across the top. Arrow Left - * and Arrow Right navigate between tabs. - * - **Vertical**: Tabs are stacked vertically along the side. Arrow Up - * and Arrow Down navigate between tabs. Sets `aria-orientation="vertical"` - * on the tablist element. - * - * `direction="vertical-right"` from 1st-gen is not supported; use - * `direction="vertical"`. See the [migration guide](../migration.md). - * - * Both directions shown below for comparison. - */ export const Directions: Story = { render: () => html` ${TABS_DIRECTIONS.map( @@ -325,30 +271,12 @@ export const Directions: Story = { )} `, tags: ['options'], - parameters: { - 'section-order': 2, - }, }; // ────────────────────────── // STATES STORIES // ────────────────────────── -/** - * Tabs can exist in various states: - * - * - **Default**: Normal, interactive state - * - **Selected**: The currently active tab (shown with selection indicator) - * - **Disabled (individual)**: Tab exists but cannot be activated. In 2nd-gen, - * disabled tabs remain focusable via arrow keys per the - * [ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) - * and use `aria-disabled="true"` instead of the native `disabled` attribute. - * - **Disabled (container)**: The entire tab list is disabled. All interaction - * is suppressed and `aria-disabled="true"` is set on the tablist element. - * - * Disabled tabs remain focusable via arrow keys but cannot be activated; see - * the [migration guide](../migration.md). - */ export const States: Story = { render: () => html` <p><strong>Individual disabled tab</strong></p> @@ -386,24 +314,6 @@ export const States: Story = { // BEHAVIORS STORIES // ────────────────────────────── -/** - * - * `keyboard-activation="manual"` (default): Arrow keys move focus between - * tabs without changing the selected tab until Enter or Space. - * - * `keyboard-activation="automatic"`: Selection follows focus when arrowing. - * - * ### Events - * - * - **change**: Fired when the selected tab changes. Cancelable via - * `preventDefault()` to revert selection. - * - * ```javascript - * tabs.addEventListener('change', (event) => { - * console.log('Selected:', event.target.selected); - * }); - * ``` - */ export const ActivationModes: Story = { render: () => html` <p><strong>Manual activation (default)</strong></p> @@ -434,88 +344,10 @@ export const ActivationModes: Story = { tags: ['behaviors'], }; -// ────────────────────────────────── -// UPCOMING FEATURES STORIES -// ────────────────────────────────── - -/** - * ### Overflow - * - * - When tabs exceed the container width, overflowing tabs will collapse into a `<swc-picker>` dropdown - */ -export const UpcomingFeatures: Story = { - tags: ['upcoming', 'description-only'], -}; -UpcomingFeatures.storyName = 'Upcoming features'; - // ──────────────────────────────── // ACCESSIBILITY STORIES // ──────────────────────────────── -/** - * ### Features - * - * The `<swc-tabs>` component implements several accessibility features: - * - * #### Keyboard navigation - * - * - <kbd>Tab</kbd>: Moves focus into the tab list (to the selected or - * last-focused tab) and then out to the active panel - * - <kbd>Shift+Tab</kbd>: Returns focus from panel to tab list - * - <kbd>Arrow Left</kbd> / <kbd>Arrow Right</kbd>: Navigates between - * tabs in horizontal mode (swaps in RTL) - * - <kbd>Arrow Up</kbd> / <kbd>Arrow Down</kbd>: Navigates between - * tabs in vertical mode - * - <kbd>Home</kbd>: Moves focus to the first tab - * - <kbd>End</kbd>: Moves focus to the last tab - * - <kbd>Space</kbd> / <kbd>Enter</kbd>: Activates the focused tab - * (manual mode). In automatic activation, tabs activate when focused via arrows. - * - * #### ARIA implementation - * - * 1. **Roles**: `tablist` on the inner container, `tab` on each tab - * item, `tabpanel` on each panel - * 2. **Labeling**: `aria-label` on the tablist from the `accessible-label` property - * 3. **States**: `aria-selected` on tabs, `aria-disabled` on disabled - * tabs and on the tablist when the container is disabled - * 4. **Orientation**: `aria-orientation="vertical"` on the same node as - * `role="tablist"` when `direction="vertical"`. - * 5. **Relationships**: `aria-controls` on tabs and `aria-labelledby` - * on panels link each tab to its associated panel - * - * #### Roving tabindex - * - * The tablist uses a roving tabindex strategy: exactly one tab has - * `tabindex="0"` at all times (the selected or last-focused tab), - * while all other tabs have `tabindex="-1"`. This ensures a single - * Tab stop for the tab list, with arrow keys for internal navigation. - * - * #### Disabled tabs - * - * Disabled tabs use `aria-disabled="true"` (not the native `disabled` - * attribute) so they remain discoverable by assistive technology. They - * are focusable via arrow keys but cannot be activated (Enter, Space, - * and click are guarded). - * - * #### Tab panel focus management - * - * Active panels have `tabindex="0"` so they receive focus when the user - * presses Tab from the tablist. When focus enters panel content, the - * panel removes its own `tabindex` to avoid trapping Tab presses inside - * the panel. On `focusout`, `tabindex` is restored. - * - * ### Best practices - * - * - Always provide an `accessible-label` attribute on `swc-tabs` for the - * tablist accessible name - * - Use meaningful, concise text labels for each tab - * - For icon-only tabs, provide an `aria-label` attribute on `swc-tab` as - * the accessible name (since there is no visible text content) - * - Use the `tab-id` attribute to link tabs to their panels - * - Avoid disabling all tabs — at least one should be interactive - * - Test with screen readers (VoiceOver, NVDA, JAWS) to verify tab - * names, selection announcements, and panel content are accessible - */ export const Accessibility: Story = { render: () => html` ${renderTabGroup({ diff --git a/2nd-gen/packages/swc/components/tabs/tabs.mdx b/2nd-gen/packages/swc/components/tabs/tabs.mdx new file mode 100644 index 0000000000..78f9e0f5d0 --- /dev/null +++ b/2nd-gen/packages/swc/components/tabs/tabs.mdx @@ -0,0 +1,156 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as TabsStories from './stories/tabs.stories'; + +<Meta of={TabsStories} /> + +<DocsHeader /> + +## Anatomy + +### Visual structure + +A tabs component consists of: + +1. **Tab list**: a horizontal or vertical row of tab items +2. **Tab items**: clickable labels representing each section +3. **Selection indicator**: a visual line highlighting the active tab +4. **Tab panels**: content areas associated with each tab + +### Technical structure + +#### Slots + +- **Default slot** (on `swc-tabs`): accepts `swc-tab` elements +- **tab-panel slot** (on `swc-tabs`): accepts `swc-tab-panel` elements +- **Default slot** (on `swc-tab`): text label content +- **icon slot** (on `swc-tab`): optional icon displayed before the label +- **Default slot** (on `swc-tab-panel`): panel content (any HTML) + +#### Properties + +Properties that render visual content: + +- **selected** (on `swc-tabs`): value of the currently active tab +- **tab-id** (on `swc-tab` and `swc-tab-panel`): unique identifier linking tab to panel +- **accessible-label** (on `swc-tabs`): accessible name for the tablist +- **aria-label** (on `swc-tab`): accessible name for icon-only tabs +- **direction**: layout direction (`horizontal` or `vertical`) +- **keyboard-activation**: `manual` (default) or `automatic` (selection follows focus) +- **density**: `regular` (default) or `compact` spacing + +<Canvas of={TabsStories.Anatomy} /> + +## Upcoming features + +### Overflow + +- When tabs exceed the container width, overflowing tabs will collapse into a `<swc-picker>` dropdown + +## Options + +### Density variants + +`density="compact"` reduces spacing between tabs. Default is `regular`. + +<Canvas of={TabsStories.DensityVariants} /> + +### Directions + +Tabs support horizontal (default) and vertical layout directions. + +- **Horizontal**: tabs are laid out in a row across the top. Arrow Left and Arrow Right navigate between tabs. +- **Vertical**: tabs are stacked vertically along the side. Arrow Up and Arrow Down navigate between tabs. Sets `aria-orientation="vertical"` on the tablist element. + +`direction="vertical-right"` from 1st-gen is not supported; use `direction="vertical"`. See the [migration guide](../migration.md). + +<Canvas of={TabsStories.Directions} /> + +## States + +### States + +Tabs can exist in various states: + +- **Default**: normal, interactive state +- **Selected**: the currently active tab (shown with selection indicator) +- **Disabled (individual)**: tab exists but cannot be activated. In 2nd-gen, disabled tabs remain focusable via arrow keys per the [ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) and use `aria-disabled="true"` instead of the native `disabled` attribute. +- **Disabled (container)**: the entire tab list is disabled. All interaction is suppressed and `aria-disabled="true"` is set on the tablist element. + +Disabled tabs remain focusable via arrow keys but cannot be activated; see the [migration guide](../migration.md). + +<Canvas of={TabsStories.States} /> + +## Behaviors + +### Activation Modes + +`keyboard-activation="manual"` (default): arrow keys move focus between tabs without changing the selected tab until Enter or Space. + +`keyboard-activation="automatic"`: selection follows focus when arrowing. + +#### Events + +- **change**: fired when the selected tab changes. Cancelable via `preventDefault()` to revert selection. + +```javascript +tabs.addEventListener('change', (event) => { + console.log('Selected:', event.target.selected); +}); +``` + +<Canvas of={TabsStories.ActivationModes} /> + +## Accessibility + +### Features + +The `<swc-tabs>` component implements several accessibility features: + +#### Keyboard navigation + +- <kbd>Tab</kbd>: moves focus into the tab list (to the selected or last-focused + tab) and then out to the active panel +- <kbd>Shift+Tab</kbd>: returns focus from panel to tab list +- <kbd>Arrow Left</kbd> / <kbd>Arrow Right</kbd>: navigates between tabs in + horizontal mode (swaps in RTL) +- <kbd>Arrow Up</kbd> / <kbd>Arrow Down</kbd>: navigates between tabs in + vertical mode +- <kbd>Home</kbd>: moves focus to the first tab +- <kbd>End</kbd>: moves focus to the last tab +- <kbd>Space</kbd> / <kbd>Enter</kbd>: activates the focused tab (manual mode). + In automatic activation, tabs activate when focused via arrows. + +#### ARIA implementation + +1. **Roles**: `tablist` on the inner container, `tab` on each tab item, `tabpanel` on each panel +2. **Labeling**: `aria-label` on the tablist from the `accessible-label` property +3. **States**: `aria-selected` on tabs, `aria-disabled` on disabled tabs and on the tablist when the container is disabled +4. **Orientation**: `aria-orientation="vertical"` on the same node as `role="tablist"` when `direction="vertical"`. +5. **Relationships**: `aria-controls` on tabs and `aria-labelledby` on panels link each tab to its associated panel + +#### Roving tabindex + +The tablist uses a roving tabindex strategy: exactly one tab has `tabindex="0"` at all times (the selected or last-focused tab), while all other tabs have `tabindex="-1"`. This ensures a single Tab stop for the tab list, with arrow keys for internal navigation. + +#### Disabled tabs + +Disabled tabs use `aria-disabled="true"` (not the native `disabled` attribute) so they remain discoverable by assistive technology. They are focusable via arrow keys but cannot be activated (Enter, Space, and click are guarded). + +#### Tab panel focus management + +Active panels have `tabindex="0"` so they receive focus when the user presses Tab from the tablist. When focus enters panel content, the panel removes its own `tabindex` to avoid trapping Tab presses inside the panel. On `focusout`, `tabindex` is restored. + +### Best practices + +- Always provide an `accessible-label` attribute on `swc-tabs` for the tablist accessible name +- Use meaningful, concise text labels for each tab +- For icon-only tabs, provide an `aria-label` attribute on `swc-tab` as the accessible name (since there is no visible text content) +- Use the `tab-id` attribute to link tabs to their panels +- Avoid disabling all tabs: at least one should be interactive +- Test with screen readers (VoiceOver, NVDA, JAWS) to verify tab names, selection announcements, and panel content are accessible + +<Canvas of={TabsStories.Accessibility} /> + +<DocsFooter /> diff --git a/2nd-gen/packages/swc/components/typography/stories/typography.stories.ts b/2nd-gen/packages/swc/components/typography/stories/typography.stories.ts index 28d9c622e9..211b8bc187 100644 --- a/2nd-gen/packages/swc/components/typography/stories/typography.stories.ts +++ b/2nd-gen/packages/swc/components/typography/stories/typography.stories.ts @@ -80,65 +80,40 @@ export const Playground: Story = { }, }, }, - tags: ['autodocs', 'dev'], + tags: ['dev'], }; -/** - * Type variants are applied using a base class in the format `.swc-[Variant]`, such as `.swc-Heading`. - * - * Each type variant defaults to sans-serif and size medium. - */ export const Defaults: Story = { args: { showAllVariants: true, }, - parameters: { 'section-order': 1 }, tags: ['options'], }; -/** - * The following variants include a serif sub-variant, which can be used by adding the `--serif` modifier class alongside the base class. - */ export const SerifModifier: Story = { args: { showAllVariants: true, serif: true, }, - parameters: { 'section-order': 2 }, tags: ['options'], }; -/** - * The following variants may use the emphasized modifier by adding `.swc-Typography--emphasized` alongside the base class. - * It may also be applied to the serif sub-variants. - */ export const EmphasizedModifier: Story = { args: { showAllVariants: true, emphasized: true, }, - parameters: { 'section-order': 3 }, tags: ['options'], }; -/** - * Heading text represents the biggest and boldest text on a page, and it draws the most attention. Only the broadest idea, such as the main page title, should use this style. - */ export const HeadingVariant: Story = { args: { variant: 'heading', includeMultipleSizes: true, }, - parameters: { - 'section-order': 4, - }, tags: ['options'], }; -/** - * Heading is also available in a `--heavy` style that sets the font weight to the heaviest of `black`. - * Black weight text should only be used in heading type styles, and never below `18px` font-size, to ensure the text remains legible. - */ export const HeadingHeavy: Story = { args: { variant: 'heading', @@ -146,50 +121,30 @@ export const HeadingHeavy: Story = { size: 'L', sampleText: 'Adobe Express Uses Heavy Headings', }, - parameters: { - 'section-order': 5, - }, tags: ['options'], }; -/** - * Title is used for essential text items on the page, such as wayfinding or context-setting. - * While the Heading style is for the loudest, most broad message, there are still going to be other important items in an information hierarchy. - */ export const TitleVariant: Story = { args: { variant: 'title', includeMultipleSizes: true, }, - parameters: { - 'section-order': 6, - }, tags: ['options'], }; -/** - * Body is the type style that’s primarily used for longer-form text that may extend to multiple lines. - * It's used for the text that creates the main content on a page. - */ export const BodyVariant: Story = { args: { variant: 'body', includeMultipleSizes: true, }, - parameters: { 'section-order': 7 }, tags: ['options'], }; -/** - * "Detail text" is a broad term for any kind of text that communicates ideas that are even more specific than body text. - * Text using the Detail style acts as supporting context to any other information presented, such as metadata, helper copy, or captions. - */ export const DetailVariant: Story = { args: { variant: 'detail', includeMultipleSizes: true, }, - parameters: { 'section-order': 8 }, tags: ['options'], }; @@ -198,13 +153,9 @@ export const CodeVariant: Story = { variant: 'code', includeMultipleSizes: true, }, - parameters: { 'section-order': 9 }, tags: ['options'], }; -/** - * Applies block-direction margins via the `--margins` modifier. - */ export const MarginsModifier: Story = { args: { variant: 'body', @@ -215,16 +166,6 @@ export const MarginsModifier: Story = { tags: ['options'], }; -/** - * Applies block-direction margins to all type variants within a container with the `.swc-Typography--prose` class applied. - * - * This also applies foundational type properties based on heading, title and body styles to common semantic typography elements including: - * - `h1` - Heading, size M - * - `h2` - Title, size XL - * - `h3` - Title, size L - * - `h4` - Title, size M - * - `p, li` - Body, size M - */ export const ProseContainer: Story = { args: { variant: 'body', diff --git a/2nd-gen/packages/swc/components/typography/typography.mdx b/2nd-gen/packages/swc/components/typography/typography.mdx new file mode 100644 index 0000000000..d7e2b48146 --- /dev/null +++ b/2nd-gen/packages/swc/components/typography/typography.mdx @@ -0,0 +1,86 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import { DocsFooter, DocsHeader } from '../../.storybook/blocks'; + +import * as TypographyStories from './stories/typography.stories'; + +<Meta of={TypographyStories} /> + +<DocsHeader /> + +## Options + +### Defaults + +Type variants are applied using a base class in the format `.swc-[Variant]`, such as `.swc-Heading`. + +Each type variant defaults to sans-serif and size medium. + +<Canvas of={TypographyStories.Defaults} /> + +### Serif Modifier + +The following variants include a serif sub-variant, which can be used by adding the `--serif` modifier class alongside the base class. + +<Canvas of={TypographyStories.SerifModifier} /> + +### Emphasized Modifier + +The following variants may use the emphasized modifier by adding `.swc-Typography--emphasized` alongside the base class. It may also be applied to the serif sub-variants. + +<Canvas of={TypographyStories.EmphasizedModifier} /> + +### Heading Variant + +Heading text represents the biggest and boldest text on a page, and it draws the most attention. Only the broadest idea, such as the main page title, should use this style. + +<Canvas of={TypographyStories.HeadingVariant} /> + +### Heading Heavy + +Heading is also available in a `--heavy` style that sets the font weight to the heaviest of `black`. Black weight text should only be used in heading type styles, and never below `18px` font-size, to ensure the text remains legible. + +<Canvas of={TypographyStories.HeadingHeavy} /> + +### Title Variant + +Title is used for essential text items on the page, such as wayfinding or context-setting. While the Heading style is for the loudest, most broad message, there are still going to be other important items in an information hierarchy. + +<Canvas of={TypographyStories.TitleVariant} /> + +### Body Variant + +Body is the type style that's primarily used for longer-form text that may extend to multiple lines. It's used for the text that creates the main content on a page. + +<Canvas of={TypographyStories.BodyVariant} /> + +### Detail Variant + +"Detail text" is a broad term for any kind of text that communicates ideas that are even more specific than body text. Text using the Detail style acts as supporting context to any other information presented, such as metadata, helper copy, or captions. + +<Canvas of={TypographyStories.DetailVariant} /> + +### Code Variant + +<Canvas of={TypographyStories.CodeVariant} /> + +### Margins Modifier + +Applies block-direction margins via the `--margins` modifier. + +<Canvas of={TypographyStories.MarginsModifier} /> + +### Prose Container + +Applies block-direction margins to all type variants within a container with the `.swc-Typography--prose` class applied. + +This also applies foundational type properties based on heading, title and body styles to common semantic typography elements including: + +- `h1`: Heading, size M +- `h2`: Title, size XL +- `h3`: Title, size L +- `h4`: Title, size M +- `p, li`: Body, size M + +<Canvas of={TypographyStories.ProseContainer} /> + +<DocsFooter /> diff --git a/research.md b/research.md new file mode 100644 index 0000000000..673fdd791b --- /dev/null +++ b/research.md @@ -0,0 +1,471 @@ +# Research: Convert 2nd-gen story JSDoc to per-component MDX + +## Goal + +Move the long-form documentation that currently lives as JSDoc comments above +story exports into per-unit MDX files at the unit's root. The conversion +covers three genres: + +- **Components** at `2nd-gen/packages/swc/components/<name>/<name>.mdx` + (13 units: accordion, asset, avatar, badge, button, color-loupe, divider, + icon, illustrated-message, progress-circle, status-light, tabs, typography) +- **Patterns** at + `2nd-gen/packages/swc/patterns/<group>/<name>/<name>.mdx` + (11 units under `conversational-ai/`: system-message, response-status, + message-sources, message-feedback, prompt-field, conversation-thread, + conversation-turn, suggestion-item, suggestion-group, upload-artifact, + user-message). A pattern-group overview MDX (e.g. + `conversational-ai/pattern-overview.mdx`) already exists and stays. +- **Controllers** at + `2nd-gen/packages/core/controllers/<name>/<name>.mdx` + (currently 1 unit with a stories file: `focusgroup-navigation-controller`) + +Each MDX file uses Storybook doc blocks to render the stories explicitly, and +becomes the authoritative Docs page for that unit, replacing the +template-driven page generated by `DocumentTemplate.mdx` for that unit only. + +## Decisions confirmed with the user + +1. **JSDoc removal**: Stories file becomes a thin "story definitions only" file. + All long-form prose moves to MDX. The meta-level JSDoc (component + description above `const meta: Meta = { ... }`) and the copyright header + stay. Story-level JSDoc comments are removed. +2. **Override model**: Each per-component MDX uses `<Meta of={meta} />` so it + becomes the component's Docs page, overriding `docs.page = DocumentTemplate` + set in `preview.ts` for that component only. Components without their own + MDX continue to render through `DocumentTemplate.mdx`. +3. **Location & naming**: `2nd-gen/packages/swc/components/<component>/<component>.mdx`, + sibling to the existing `migration-guide.mdx`. +4. **Rollout**: Pilot with `badge` first, validate visually in Storybook, then + roll out to the other 12 components. + +## Current state (verified) + +- Storybook config (`2nd-gen/packages/swc/.storybook/main.ts`) already includes + `**/*.mdx` under `components/`, so new MDX files will be picked up without + config changes. +- `preview.ts` sets `parameters.docs.page = DocumentTemplate` globally + (line 171). A per-component MDX with `<Meta of={meta} />` overrides this for + that component. +- `DocumentTemplate.mdx` is the current source of truth for page composition. + It renders, in order: Title, StatusBadge, Subtitle, Description, Overview + story, GettingStarted, then `ConditionalSection` blocks for tags `anatomy`, + `upcoming`, `usage`, `options`, `states`, `behaviors`, `a11y`, `full-pattern`, + then API table + advanced examples + appendix + feedback. +- `<ConditionalSection>` uses the custom `<SpectrumStories>` block, which: + - Filters stories by tag + - Sorts by `parameters['section-order']` + - Renders each as: `### <story.name>` (if `hideTitle=false`) + `<Description of={story} />` (which surfaces the story's JSDoc) + `<Canvas of={story} />` (unless the story is tagged `description-only`). +- `tags: ['migrated']` on meta is what `StatusBadge` and other meta-level + decoration depend on. All 13 in-scope components carry this tag. +- 13 components in scope: accordion, asset, avatar, badge, button, color-loupe, + divider, icon, illustrated-message, progress-circle, status-light, tabs, + typography. All currently have one `*.stories.ts` file at + `components/<name>/stories/<name>.stories.ts`. +- 11 patterns in scope under `swc/patterns/conversational-ai/`, each with the + same `<unit>/stories/<unit>.stories.ts` shape. A pattern-group landing page + (`pattern-overview.mdx`) already exists at the group root and remains. +- 1 controller in scope: `core/controllers/focusgroup-navigation-controller`, + same `<unit>/stories/<unit>.stories.ts` shape. Other controllers + (`language-resolution.ts`) have no stories file and are out of scope until + one is added. +- 2 internal stories files in scope: + `swc/components/icon/stories/icon.internal.stories.ts` and + `swc/components/asset/stories/asset.internal.stories.ts`. Each gets a sibling + `<component>.internal.mdx` at the component root (matching the existing + `icon/migration-guide.internal.mdx` convention). The `.internal.mdx` suffix + is already excluded from production builds by `main.ts` (line 86, + `**/!(*.internal).mdx`). +- Total: 27 units across the genres (13 components + 2 internal + 11 patterns + - 1 controller). + +## Proposed MDX template (badge as worked example) + +The new `badge.mdx` mirrors the section order of `DocumentTemplate.mdx` but +inlines the prose currently in JSDoc, and uses explicit `<Canvas of={Story} />` +references instead of tag-based iteration. The structure: + +```mdx +import { + Meta, + Title, + Subtitle, + Description, + Primary, + Controls, + Canvas, + Markdown, +} from '@storybook/addon-docs/blocks'; +import { + ApiTable, + GettingStarted, + OverviewStory, + StatusBadge, +} from '../../../.storybook/blocks'; + +import * as BadgeStories from './stories/badge.stories'; + +<Meta of={BadgeStories} /> + +<Title /> +<StatusBadge /> +<Subtitle /> +<Description /> +<OverviewStory /> +<GettingStarted tags={['migrated']} /> + +## Anatomy + +<!-- prose previously in the Anatomy story JSDoc --> + +A badge consists of: + +1. **Container** — colored background with rounded corners +2. **Label** — text content describing the status or category (required) +3. **Icon** (optional) — visual indicator positioned before the label + +### Content + +- **Default slot**: text content describing the status or category (required for accessibility) +- **icon slot**: optional visual indicator positioned before the label + +<Canvas of={BadgeStories.Anatomy} /> + +## Options + +### Sizes + +<!-- prose previously in the Sizes story JSDoc --> + +Badges come in four sizes to fit various contexts: + +- **Small (`s`)** — default size; compact spaces, inline with text, or in tables +- **Medium (`m`)** — common usage when slightly more emphasis is needed +- **Large (`l`)** — increased emphasis in cards or content areas +- **Extra-large (`xl`)** — maximum visibility for primary status indicators + +<Canvas of={BadgeStories.Sizes} /> + +### Semantic variants + +<!-- prose previously in SemanticVariants JSDoc --> + +… + +<Canvas of={BadgeStories.SemanticVariants} /> + +<!-- continue for NonSemanticVariants, Outline, Subtle, Fixed --> + +## States + +<!-- badge has no States stories currently — section omitted --> + +## Behaviors + +### Text wrapping + +… + +<Canvas of={BadgeStories.TextWrapping} /> + +### Inline usage + +… + +<Canvas of={BadgeStories.Inline} /> + +## Upcoming features + +<!-- description-only story; render prose but no canvas --> + +… + +## Accessibility + +<!-- prose from Accessibility story JSDoc --> + +… + +<Canvas of={BadgeStories.Accessibility} /> + +## API + +<ApiTable /> +<Primary /> +<Controls /> + +## Feedback + +Have feedback or questions? [Open an issue](https://github.com/adobe/spectrum-web-components/issues/new/choose). +``` + +Notes on the template: + +- `<Meta of={BadgeStories} />` plus `<Title />` and `<Subtitle />` reuse the + meta-level description and `parameters.docs.subtitle` set in the stories + file. The meta-level JSDoc stays — it's the source for `<Description />`. +- Section headings are explicit Markdown (`##`/`###`). The `ConditionalSection` + - `slugify` machinery is no longer needed per component because the MDX is + hand-authored; anchor IDs come from Storybook's default heading id behavior, + which already matches `slugify` output. +- For `description-only` stories (e.g. `UpcomingFeatures`), inline the prose + but omit `<Canvas>`. +- `<ApiTable />`, `<Primary />`, `<Controls />` continue to work because they + resolve `of` from the nearest `<Meta />`. +- Section-order parameters in stories become unused for these components + (ordering is now explicit in MDX). They can be left in place for now; a + follow-up cleanup pass can remove them. + +## Per-story JSDoc removal plan + +For each component: + +1. Move every JSDoc block above an exported `Story` into the corresponding + MDX section as prose. +2. Delete the JSDoc block from the story export. +3. Leave the meta-level JSDoc above `const meta: Meta = { ... }` untouched — + it powers `<Description />` and IDE intellisense on the meta object. +4. Leave the copyright header untouched. +5. Leave `tags`, `args`, `parameters`, `storyName`, and `render` as-is. + +## Risks and open considerations + +1. **Section heading anchors**: The current template uses `slugify` to generate + `#anatomy`, `#options`, etc. MDX-native heading IDs use the same algorithm + (`remark-slug`-style), so existing in-page links like `[outline](#outline)` + should continue to work. We will verify in the pilot. +2. **Story ordering in sidebar**: Removing JSDoc does not change story order in + the left sidebar (driven by export order). No change expected. +3. **Drift between meta JSDoc and `parameters.docs.subtitle`**: Both surface in + the MDX (`<Description />` + `<Subtitle />`). Already true today; no new + risk. +4. **`description-only` and `upcoming` stories**: These currently rely on + `SpectrumStories` skipping `<Canvas>` for `description-only`. In the new + MDX we handle this manually by inlining prose without a `<Canvas>`. +5. **A11y test stories**: Stories tagged `'!test'` (e.g. static-color variants) + are unaffected by this change. +6. **Lint/format**: The new `.mdx` files need to pass any prettier/mdx + formatting rules. Will run `yarn prettier --check` on each in the pilot. +7. **Eventually obsoleting `DocumentTemplate.mdx`**: Once all components have + per-component MDX, the template only serves non-migrated/future components. + Out of scope for this task. + +## Pilot plan (badge) + +1. Create `2nd-gen/packages/swc/components/badge/badge.mdx` using the template + above, filling all sections with prose currently in story JSDoc. +2. Edit `2nd-gen/packages/swc/components/badge/stories/badge.stories.ts` to + remove the JSDoc blocks above each exported story. Keep meta JSDoc. +3. Run `yarn prettier --check 2nd-gen/packages/swc/components/badge/**`. +4. Start Storybook locally and verify the Badge Docs page: + - Title, subtitle, description all render + - Overview story renders + - Anatomy section: prose + canvas + - Options sub-sections: prose + canvas in the right order + - Behaviors section: TextWrapping and Inline render + - Upcoming features section: prose, no canvas + - Accessibility section: prose + canvas + - API table + controls render + - In-page anchors work +5. Visual diff against the current Docs page and confirm with the user. + +## After pilot approval + +Apply the same conversion to the remaining 12 components, one per commit (or +one PR with one commit per component, whichever the user prefers). + +## Consistency enforcement (confirmed: option A + D, B deferred) + +To keep 13+ component MDX files from drifting, the conversion ships with two +mechanisms: + +### A. Shared shell blocks + +Two new React blocks alongside the existing ones in +`2nd-gen/packages/swc/.storybook/blocks/`: + +- `DocsHeader.tsx` — renders the standard top of every doc page: `<Title />`, + `<StatusBadge />`, `<Subtitle />`, `<Description />`, `<OverviewStory />`, + `<GettingStarted />`. +- `DocsFooter.tsx` — renders the standard bottom: API table heading, + `<ApiTable />`, `<Primary />`, `<Controls />`, the Feedback heading and + link. + +Both blocks are kept generic enough to work for other doc genres (controllers, +patterns). Component-specific assumptions (e.g. `StatusBadge` reading the +`migrated` tag, `ApiTable` reading the CEM) already key off `<Meta of={…} />` +context, so a controller or pattern MDX that does not include a status badge +or API table simply omits those props or uses a slimmer variant. + +Because the conversion covers components, patterns, and controllers from +day one, the blocks must accept genre context. Two practical approaches: + +1. **Auto-detect from `<Meta>` title prefix.** The blocks call `useOf('meta')` + and branch on whether the title starts with `Components/`, `Patterns/`, + or `Core/Controllers/`. The block API stays a bare `<DocsHeader />` / + `<DocsFooter />` for every genre. +2. **Explicit prop.** `<DocsHeader genre="component" />` / + `genre="pattern"` / `genre="controller"`. Slightly more typing per file + but unambiguous. + +Recommendation: start with **auto-detection** (less ceremony, harder to get +wrong) and fall back to an explicit prop only if a unit needs to override. + +Genre-specific surface that varies today: + +| Surface | Component | Pattern | Controller | +| ------------------------------ | --------- | ------------------------------- | --------------------------------------------- | +| `<StatusBadge />` | yes | yes | yes | +| `<OverviewStory />` | yes | yes | yes (if a stories file exists) | +| `<GettingStarted />` | yes | yes | yes (import path differs) | +| `<ApiTable />` | yes (CEM) | yes (CEM, if a tag is exported) | no — controllers expose a TS class, not a CEM | +| `<Primary />` + `<Controls />` | yes | yes | yes (if a story exists) | + +So the controller variant of `DocsFooter` omits `ApiTable` and may render a +hand-authored API section instead. That argues for **either** (a) the auto- +detect approach with conditionals inside the block, **or** (b) shipping +`DocsFooter` and `ControllerDocsFooter` as separate exports. Final call: +**ship one `DocsFooter` that auto-detects and conditionally omits +`ApiTable` for controllers.** One block, predictable behavior. + +`DocumentTemplate.mdx` is rewritten in the same pass to use `DocsHeader` + +the existing `ConditionalSection` loop + `DocsFooter`. That guarantees the +template-driven Docs page and the per-component MDX share the exact same +chrome — there is no second source of truth. + +A per-component MDX therefore looks like: + +```mdx +import { Meta, Canvas } from '@storybook/addon-docs/blocks'; +import { DocsHeader, DocsFooter } from '../../../.storybook/blocks'; +import * as Stories from './stories/badge.stories'; + +<Meta of={Stories} /> +<DocsHeader /> + +## Anatomy + +…prose… + +<Canvas of={Stories.Anatomy} /> + +## Options + +### Sizes + +…prose… + +<Canvas of={Stories.Sizes} /> + +… + +## Accessibility + +…prose… + +<Canvas of={Stories.Accessibility} /> + +<DocsFooter /> +``` + +### D. Structural lint (`yarn lint:component-docs`) + +Small Node/remark script run in CI. Checks: + +1. Every `components/*/` directory contains a `<name>.mdx` (once rollout is + complete — until then, list known-pending components in an allowlist). +2. Each MDX imports stories and has exactly one `<Meta of={…} />`. +3. The set of top-level `##` headings is a subset of the canonical order: + `Anatomy`, `Options`, `States`, `Behaviors`, `Accessibility`, plus any + genre-specific additions. Order is enforced; missing sections are allowed + only if the component has no stories tagged for that section. +4. Every story tagged `anatomy|options|states|behaviors|a11y` is referenced + by at least one `<Canvas of={…} />` in the MDX (prevents authors from + forgetting to surface a story). +5. No story is referenced that does not exist in the stories module. + +The lint is path-aware from day one with three rule sets (internal MDX is +treated as a relaxed variant of the component ruleset): + +- `swc/components/*/<name>.mdx` — required sections subset of + `Anatomy, Options, States, Behaviors, Accessibility`. `ApiTable` and + `Controls` expected. +- `swc/components/*/<name>.internal.mdx` — same component ruleset but no + section is required (internal pages are dev-only and free-form). Still + must have `<Meta of={…} />` and reference all internal stories via + `<Canvas>`. +- `swc/patterns/*/*/<name>.mdx` — same component-style ruleset. Pattern + groups also require a `<group>/pattern-overview.mdx` (already true). +- `core/controllers/*/<name>.mdx` — required sections subset of + `Usage, Options, Behaviors, Accessibility` (no `Anatomy` — controllers + have no DOM). `ApiTable` not expected; an `## API` heading with + hand-authored prose is acceptable. + +The lint command is `yarn lint:docs-pages` (renamed from +`lint:component-docs` since it covers more than components). + +### Why not Option B (DocsSection block) + +A `<DocsSection title="..." of={Story}>…</DocsSection>` block would enforce +heading/anchor/canvas shape but adds an abstraction layer between authors +and the MDX primitives. We keep authors close to standard Storybook blocks +for now; if drift shows up in practice, we revisit. + +## Implications for the conversion + +A few things follow from confirming MDX as the doc surface: + +1. **`description-only` tag becomes obsolete.** It exists to let + `SpectrumStories` skip rendering a `<Canvas>` for a story that is really + just prose. In MDX, prose sections do not require a `<Canvas>` at all — + author the section without one. The stories that exist purely to hang + JSDoc on (e.g. badge's `UpcomingFeatures`, accessibility "features" + stories with no meaningful render) can be deleted entirely once their + prose is in MDX. The `description-only` tag should be retired from the + project after rollout. +2. **`upcoming` tag**: same treatment — content moves to an `## Upcoming +features` section in MDX. The tag can stay on any remaining demo stories + that genuinely have a canvas, but pure-text usages go away. +3. **`section-order` parameter**: ordering is now explicit in MDX. Existing + `parameters['section-order']` entries become dead weight; a follow-up + cleanup pass removes them once all components are converted. +4. **Story-as-prose anti-pattern is removed.** Going forward, every exported + story has a meaningful canvas. Documentation-only sections live in MDX. + +## Revised pilot plan (badge) + +1. Add `DocsHeader.tsx` and `DocsFooter.tsx` to + `2nd-gen/packages/swc/.storybook/blocks/`. Export from `blocks/index.ts`. +2. Rewrite `DocumentTemplate.mdx` to use the new blocks (no behavior change; + verify visually against an unconverted component, e.g. `accordion`, that + nothing regresses). +3. Create `components/badge/badge.mdx` using the template above. +4. Strip story-level JSDoc from `badge.stories.ts`. Delete `UpcomingFeatures` + story (its prose lives in MDX now); confirm nothing else references it. +5. Run `yarn prettier --check` on changed files. +6. Visual check in Storybook: Badge Docs page renders correctly; an + unconverted component (accordion) still renders correctly via the + template. +7. Author the lint script and wire `yarn lint:component-docs`. +8. Pause for user review of the pilot. + +## After pilot approval + +Roll out across all 25 units in a deliberate order so each genre is +validated before the next begins: + +1. **Components** (12 remaining after badge): accordion, asset, avatar, + button, color-loupe, divider, icon, illustrated-message, progress-circle, + status-light, tabs, typography. As icon and asset are converted, their + internal `.internal.stories.ts` files are converted to `.internal.mdx` + in the same commit. +2. **First pattern** as a genre-pilot + (`patterns/conversational-ai/system-message`), to confirm + `DocsHeader`/`DocsFooter` auto-detection works for patterns. Then the + other 10 patterns. +3. **Controller** (`focusgroup-navigation-controller`) to validate the + no-`ApiTable` branch. + +Open question for the user: one PR per unit (25 PRs), one PR per genre +(3 PRs), or one large PR? My default is **one PR per genre** — small enough +to review, large enough that the lint script lands with real coverage.