diff --git a/docs/site/scripts/generate-tools-page.js b/docs/site/scripts/generate-tools-page.js index 7f840d27..8e43f333 100644 --- a/docs/site/scripts/generate-tools-page.js +++ b/docs/site/scripts/generate-tools-page.js @@ -35,6 +35,11 @@ const WEB_TOOLS_CONFIG = [ path: "/release-timeline/", label: "Release Timeline", }, + { + dir: "token-name-builder", + path: "/token-name-builder/", + label: "Token Name Builder", + }, ]; const WEB_TOOLS_FALLBACK_DESCRIPTIONS = { @@ -44,6 +49,8 @@ const WEB_TOOLS_FALLBACK_DESCRIPTIONS = { visualizer: "Interactive dependency graph for Spectrum 1 (legacy) tokens", "release-timeline": "Interactive visualization of Spectrum Tokens release history across legacy, stable, beta, and snapshot formats", + "token-name-builder": + "Interactive tool for building Spectrum design token names using the design data spec taxonomy", }; function readJson(path) { diff --git a/docs/site/src/tools.md b/docs/site/src/tools.md index 7f36261f..9e764c8a 100644 --- a/docs/site/src/tools.md +++ b/docs/site/src/tools.md @@ -16,6 +16,7 @@ Interactive tools deployed with this site: * **[S2 Visualizer](/s2-visualizer/)** — Interactive dependency graph for Spectrum 2 tokens showing ancestor/descendant relationships, value filters, and search * **[S1 Visualizer](/visualizer/)** — Interactive dependency graph for Spectrum 1 (legacy) tokens * **[Release Timeline](/release-timeline/)** — Interactive timeline visualization of Spectrum Tokens release history +* **[Token Name Builder](/token-name-builder/)** — Interactive tool for building Spectrum design token names using the design data spec taxonomy ## Developer Tools @@ -28,6 +29,7 @@ Packages under `tools/` in the repo: * **[@adobe/spectrum-design-data-mcp](https://www.npmjs.com/package/%40adobe%2Fspectrum-design-data-mcp)** — Model Context Protocol server for Spectrum design data including tokens, schemas, and component anatomy * **[@adobe/spectrum-diff-core](https://www.npmjs.com/package/%40adobe%2Fspectrum-diff-core)** — Shared core library for Spectrum diff generation tools (tokens, component schemas, etc.) * **[@adobe/token-diff-generator](https://www.npmjs.com/package/%40adobe%2Ftoken-diff-generator)** — Generate comprehensive diffs between design token sets with support for multiple output formats including CLI, JSON, and Markdown. Detects added, deleted, renamed, deprecated, and updated tokens across different schema versions. +* **[@adobe/token-name-builder](https://github.com/adobe/spectrum-design-data/tree/main/tools/token-name-builder)** — Interactive web tool for building Spectrum design token names * **[component-options-editor](https://github.com/adobe/spectrum-design-data/tree/main/tools/component-options-editor)** — Figma plugin for authoring Spectrum component option schemas * **[markdown-generator](https://github.com/adobe/spectrum-design-data/tree/main/tools/markdown-generator)** — Generate markdown files from tokens, component-schemas, and design-system-registry for docs and chatbot indexing * **[release-analyzer](https://github.com/adobe/spectrum-design-data/tree/main/tools/release-analyzer)** — Analyzes Spectrum Tokens release history and generates data for change frequency visualization diff --git a/packages/design-system-registry/index.js b/packages/design-system-registry/index.js index 370b5560..e962ce7c 100644 --- a/packages/design-system-registry/index.js +++ b/packages/design-system-registry/index.js @@ -90,6 +90,10 @@ export const shapes = JSON.parse( readFileSync(join(__dirname, "registry", "shapes.json"), "utf-8"), ); +export const componentAnatomy = JSON.parse( + readFileSync(join(__dirname, "registry", "component-anatomy.json"), "utf-8"), +); + /** * Get all values from a registry by ID * @param {object} registry - The registry object diff --git a/packages/design-system-registry/registry/component-anatomy-curation.json b/packages/design-system-registry/registry/component-anatomy-curation.json new file mode 100644 index 00000000..38e84783 --- /dev/null +++ b/packages/design-system-registry/registry/component-anatomy-curation.json @@ -0,0 +1,105 @@ +{ + "$comment": "Curation rules applied after extracting raw anatomy data from S2 docs. Documents why the registry diverges from the source markdown. Re-run the extraction script to apply.", + + "removals": { + "background": "This is a token object / styling surface (belongs in the 'object' field of the name object, not 'anatomy'). See token-objects.json." + }, + + "renames": { + "action-button-1": { + "to": "action-button", + "reason": "Numbering artifact from action-group diagram; normalize to the component name." + }, + "action-button-2": { + "to": "action-button", + "reason": "Numbering artifact from action-group diagram; normalize to the component name." + }, + "body-area": { + "to": "body", + "reason": "Redundant '-area' suffix; aligns with existing anatomy-terms registry term 'body'." + }, + "header-area": { + "to": "header", + "reason": "Redundant '-area' suffix; aligns with existing anatomy-terms registry term 'header'." + }, + "footer-area": { + "to": "footer", + "reason": "Redundant '-area' suffix; aligns with existing anatomy-terms registry term 'footer'." + }, + "small-divider": { + "to": "divider", + "reason": "Size qualifier is not an anatomy distinction; aligns with existing anatomy-terms registry term 'divider'." + } + }, + + "tags": { + "accordion": { + "tier": "composite", + "note": "Component used as a named part in standard-panel (spec anatomy tier 2)." + }, + "action-button": { + "tier": "composite", + "note": "Component used as a named part in action-group and contextual-help." + }, + "button": { + "tier": "composite", + "note": "Component used as a named part in alert-banner, coach-mark, drop-zone, illustrated-message." + }, + "button-group": { + "tier": "composite", + "note": "Component used as a named part in alert-dialog, coach-mark, standard-dialog, takeover-dialog." + }, + "calendar": { + "tier": "composite", + "note": "Component used as a named part in date-picker." + }, + "checkbox": { + "tier": "composite", + "note": "Component used as a named part in cards, list-view, menu, select-box, table, tree-view." + }, + "close-button": { + "tier": "composite", + "note": "Component used as a named part in alert-banner, coach-mark, standard-dialog, takeover-dialog, toast, in-line-alert." + }, + "field-label": { + "tier": "composite", + "note": "Component used as a named part in combo-box, date-picker, radio-group, slider." + }, + "help-text": { + "tier": "composite", + "note": "Component used as a named part in combo-box, number-field, picker, search-field, text-area, text-field, checkbox-group, radio-group." + }, + "link": { + "tier": "composite", + "note": "Component used as a named part in contextual-help." + }, + "popover": { + "tier": "composite", + "note": "Component used as a named part in contextual-help, menu." + }, + "swatch": { + "tier": "composite", + "note": "Component used as a named part in swatch-group." + }, + "switch": { + "tier": "composite", + "note": "Component used as a named part in menu." + }, + "tag": { + "tier": "composite", + "note": "Component used as a named part in tag-field, tag-group." + }, + "text-area": { + "tier": "composite", + "note": "Component used as a named part in tag-field." + }, + "avatar": { + "tier": "composite", + "note": "Component used as a named part in avatar-group, list-view, tag." + }, + "thumbnail": { + "tier": "composite", + "note": "Component used as a named part in list-view, menu." + } + } +} diff --git a/packages/design-system-registry/registry/component-anatomy.json b/packages/design-system-registry/registry/component-anatomy.json new file mode 100644 index 00000000..0482f78e --- /dev/null +++ b/packages/design-system-registry/registry/component-anatomy.json @@ -0,0 +1,1861 @@ +{ + "type": "component-anatomy", + "description": "Mapping of Spectrum components to their visible anatomy parts, extracted from S2 documentation and curated via component-anatomy-curation.json.", + "components": { + "accordion": { + "label": "accordion", + "source": "docs/s2-docs/components/navigation/accordion.md", + "parts": [ + { + "id": "accordion-items", + "label": "Accordion Items", + "optional": false + }, + { + "id": "divider", + "label": "Divider", + "optional": false + } + ] + }, + "action-button": { + "label": "action button", + "source": "docs/s2-docs/components/actions/action-button.md", + "parts": [ + { + "id": "icon", + "label": "Icon", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "hold-icon", + "label": "Hold Icon", + "optional": true + } + ] + }, + "action-group": { + "label": "action group", + "source": "docs/s2-docs/components/actions/action-group.md", + "parts": [ + { + "id": "action-button", + "label": "Action Button", + "optional": false, + "tier": "composite" + }, + { + "id": "action-menu", + "label": "Action Menu", + "optional": true + } + ] + }, + "alert-banner": { + "label": "alert banner", + "source": "docs/s2-docs/components/feedback/alert-banner.md", + "parts": [ + { + "id": "icon", + "label": "Icon", + "optional": false + }, + { + "id": "text", + "label": "Text", + "optional": false + }, + { + "id": "button", + "label": "Button", + "optional": false, + "tier": "composite" + }, + { + "id": "close-button", + "label": "Close Button", + "optional": false, + "tier": "composite" + } + ] + }, + "alert-dialog": { + "label": "alert dialog", + "source": "docs/s2-docs/components/feedback/alert-dialog.md", + "parts": [ + { + "id": "alert-dialog-container", + "label": "Alert Dialog Container", + "optional": false + }, + { + "id": "title", + "label": "Title", + "optional": false + }, + { + "id": "description", + "label": "Description", + "optional": false + }, + { + "id": "primary-action", + "label": "Primary Action", + "optional": false + }, + { + "id": "secondary-action", + "label": "Secondary Action", + "optional": true + }, + { + "id": "cancel-action", + "label": "Cancel Action", + "optional": true + }, + { + "id": "overlay", + "label": "Overlay", + "optional": false + } + ] + }, + "avatar": { + "label": "avatar", + "source": "docs/s2-docs/components/status/avatar.md", + "parts": [ + { + "id": "container", + "label": "Container", + "optional": false + }, + { + "id": "user-image", + "label": "User Image", + "optional": false + }, + { + "id": "gradient-image", + "label": "Gradient Image", + "optional": false + }, + { + "id": "initials", + "label": "Initials", + "optional": false + }, + { + "id": "guest-icon", + "label": "Guest Icon", + "optional": false + } + ] + }, + "avatar-group": { + "label": "avatar group", + "source": "docs/s2-docs/components/status/avatar-group.md", + "parts": [ + { + "id": "avatar", + "label": "Avatar", + "optional": false, + "tier": "composite" + }, + { + "id": "label", + "label": "Label", + "optional": false + } + ] + }, + "badge": { + "label": "badge", + "source": "docs/s2-docs/components/status/badge.md", + "parts": [ + { + "id": "icon", + "label": "Icon", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": false + } + ] + }, + "breadcrumbs": { + "label": "breadcrumbs", + "source": "docs/s2-docs/components/navigation/breadcrumbs.md", + "parts": [ + { + "id": "truncated-menu", + "label": "Truncated Menu", + "optional": false + }, + { + "id": "breadcrumbs-item", + "label": "Breadcrumbs Item", + "optional": false + }, + { + "id": "separator", + "label": "Separator", + "optional": false + }, + { + "id": "breadcrumbs-title", + "label": "Breadcrumbs Title", + "optional": false + } + ] + }, + "button": { + "label": "button", + "source": "docs/s2-docs/components/actions/button.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + } + ] + }, + "button-group": { + "label": "button group", + "source": "docs/s2-docs/components/actions/button-group.md", + "parts": [ + { + "id": "button", + "label": "Button", + "optional": false, + "tier": "composite" + }, + { + "id": "label", + "label": "Label", + "optional": false + } + ] + }, + "calendar": { + "label": "calendar", + "source": "docs/s2-docs/components/inputs/calendar.md", + "parts": [ + { + "id": "chevron", + "label": "Chevron", + "optional": false + }, + { + "id": "month", + "label": "Month", + "optional": false + }, + { + "id": "year", + "label": "Year", + "optional": false + }, + { + "id": "week", + "label": "Week", + "optional": false + }, + { + "id": "days", + "label": "Days", + "optional": false + } + ] + }, + "cards": { + "label": "card", + "source": "docs/s2-docs/components/containers/cards.md", + "parts": [ + { + "id": "container", + "label": "Container", + "optional": false + }, + { + "id": "checkbox", + "label": "Checkbox", + "optional": true, + "tier": "composite" + }, + { + "id": "preview-well", + "label": "Preview Well", + "optional": false + }, + { + "id": "preview", + "label": "Preview", + "optional": false + }, + { + "id": "metadata", + "label": "Metadata", + "optional": false + }, + { + "id": "footer", + "label": "Footer", + "optional": false + }, + { + "id": "title", + "label": "Title", + "optional": false + } + ] + }, + "checkbox": { + "label": "checkbox", + "source": "docs/s2-docs/components/inputs/checkbox.md", + "parts": [ + { + "id": "control", + "label": "Control", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": false + } + ] + }, + "checkbox-group": { + "label": "checkbox group", + "source": "docs/s2-docs/components/inputs/checkbox-group.md", + "parts": [ + { + "id": "field-label", + "label": "Field Label", + "optional": false, + "tier": "composite" + }, + { + "id": "checkbox", + "label": "Checkbox", + "optional": false, + "tier": "composite" + }, + { + "id": "help-text", + "label": "Help Text", + "optional": false, + "tier": "composite" + } + ] + }, + "close-button": { + "label": "close button", + "source": "docs/s2-docs/components/actions/close-button.md", + "parts": [ + { + "id": "cross-ui-icon", + "label": "Cross Ui Icon", + "optional": false + } + ] + }, + "coach-indicator": { + "label": "coach indicator", + "source": "docs/s2-docs/components/feedback/coach-indicator.md", + "parts": [ + { + "id": "inner-stroke", + "label": "Inner Stroke", + "optional": false + }, + { + "id": "outer-stroke", + "label": "Outer Stroke", + "optional": false + } + ] + }, + "coach-mark": { + "label": "coach mark", + "source": "docs/s2-docs/components/feedback/coach-mark.md", + "parts": [ + { + "id": "container", + "label": "Container", + "optional": false + }, + { + "id": "image", + "label": "Image", + "optional": true + }, + { + "id": "title", + "label": "Title", + "optional": false + }, + { + "id": "more-actions-menu", + "label": "More Actions Menu", + "optional": false + }, + { + "id": "keyboard-shortcuts", + "label": "Keyboard Shortcuts", + "optional": false + }, + { + "id": "description", + "label": "Description", + "optional": false + }, + { + "id": "tour-step-counter", + "label": "Tour Step Counter", + "optional": false + }, + { + "id": "button-group", + "label": "Button Group", + "optional": false, + "tier": "composite" + } + ] + }, + "color-area": { + "label": "color area", + "source": "docs/s2-docs/components/inputs/color-area.md", + "parts": [ + { + "id": "area", + "label": "Area", + "optional": false + }, + { + "id": "handle", + "label": "Handle", + "optional": false + }, + { + "id": "loupe", + "label": "Loupe", + "optional": false + } + ] + }, + "color-handle-and-loupe": { + "label": "color handle and loupe", + "source": "docs/s2-docs/components/inputs/color-handle-and-loupe.md", + "parts": [ + { + "id": "color-handle", + "label": "Color Handle", + "optional": false + }, + { + "id": "opacity-checkerboard", + "label": "Opacity Checkerboard", + "optional": false + }, + { + "id": "color-loupe", + "label": "Color Loupe", + "optional": true + } + ] + }, + "color-slider": { + "label": "color slider", + "source": "docs/s2-docs/components/inputs/color-slider.md", + "parts": [ + { + "id": "track", + "label": "Track", + "optional": false + }, + { + "id": "handle", + "label": "Handle", + "optional": false + }, + { + "id": "loupe", + "label": "Loupe", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "value", + "label": "Value", + "optional": false + } + ] + }, + "color-wheel": { + "label": "color wheel", + "source": "docs/s2-docs/components/inputs/color-wheel.md", + "parts": [ + { + "id": "track", + "label": "Track", + "optional": false + }, + { + "id": "handle", + "label": "Handle", + "optional": false + }, + { + "id": "loupe", + "label": "Loupe", + "optional": false + } + ] + }, + "combo-box": { + "label": "combo box", + "source": "docs/s2-docs/components/inputs/combo-box.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "necessity-indicator", + "label": "Necessity Indicator", + "optional": false + }, + { + "id": "field", + "label": "Field", + "optional": false + }, + { + "id": "help-text", + "label": "Help Text", + "optional": false, + "tier": "composite" + }, + { + "id": "menu-container", + "label": "Menu Container", + "optional": false + } + ] + }, + "contextual-help": { + "label": "contextual help", + "source": "docs/s2-docs/components/feedback/contextual-help.md", + "parts": [ + { + "id": "action-button", + "label": "Action Button", + "optional": false, + "tier": "composite" + }, + { + "id": "popover", + "label": "Popover", + "optional": false, + "tier": "composite" + }, + { + "id": "title", + "label": "Title", + "optional": false + }, + { + "id": "description", + "label": "Description", + "optional": false + }, + { + "id": "link", + "label": "Link", + "optional": false, + "tier": "composite" + } + ] + }, + "date-picker": { + "label": "date picker", + "source": "docs/s2-docs/components/inputs/date-picker.md", + "parts": [ + { + "id": "date-field", + "label": "Date Field", + "optional": false + }, + { + "id": "calendar", + "label": "Calendar", + "optional": false, + "tier": "composite" + }, + { + "id": "time-field", + "label": "Time Field", + "optional": true + } + ] + }, + "drop-zone": { + "label": "drop zone", + "source": "docs/s2-docs/components/inputs/drop-zone.md", + "parts": [ + { + "id": "illustration", + "label": "Illustration", + "optional": false + }, + { + "id": "title", + "label": "Title", + "optional": false + }, + { + "id": "body", + "label": "Body", + "optional": false + }, + { + "id": "button", + "label": "Button", + "optional": true, + "tier": "composite" + } + ] + }, + "field-label": { + "label": "field label", + "source": "docs/s2-docs/components/inputs/field-label.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "necessity-indicator", + "label": "Necessity Indicator", + "optional": false + }, + { + "id": "input", + "label": "Input", + "optional": false + } + ] + }, + "help-text": { + "label": "help text", + "source": "docs/s2-docs/components/inputs/help-text.md", + "parts": [ + { + "id": "icon", + "label": "Icon", + "optional": true + }, + { + "id": "text", + "label": "Text", + "optional": false + } + ] + }, + "illustrated-message": { + "label": "illustrated message", + "source": "docs/s2-docs/components/feedback/illustrated-message.md", + "parts": [ + { + "id": "illustration", + "label": "Illustration", + "optional": false + }, + { + "id": "title", + "label": "Title", + "optional": false + }, + { + "id": "description", + "label": "Description", + "optional": false + }, + { + "id": "button-group", + "label": "Button Group", + "optional": true, + "tier": "composite" + } + ] + }, + "in-line-alert": { + "label": "in-line alert", + "source": "docs/s2-docs/components/feedback/in-line-alert.md", + "parts": [ + { + "id": "title", + "label": "Title", + "optional": true + }, + { + "id": "icon", + "label": "Icon", + "optional": false + }, + { + "id": "body", + "label": "Body", + "optional": false + } + ] + }, + "list-view": { + "label": "list view", + "source": "docs/s2-docs/components/actions/list-view.md", + "parts": [ + { + "id": "list-view-section-header", + "label": "List View Section Header", + "optional": false + }, + { + "id": "list-item", + "label": "List Item", + "optional": false + }, + { + "id": "checkbox", + "label": "Checkbox", + "optional": false, + "tier": "composite" + }, + { + "id": "icon", + "label": "Icon", + "optional": false + }, + { + "id": "thumbnail", + "label": "Thumbnail", + "optional": false, + "tier": "composite" + }, + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "description", + "label": "Description", + "optional": true + }, + { + "id": "actions", + "label": "Actions", + "optional": false + }, + { + "id": "trailing-icons", + "label": "Trailing Icons", + "optional": false + } + ] + }, + "menu": { + "label": "menu", + "source": "docs/s2-docs/components/actions/menu.md", + "parts": [ + { + "id": "popover", + "label": "Popover", + "optional": false, + "tier": "composite" + }, + { + "id": "menu-section-header", + "label": "Menu Section Header", + "optional": false + }, + { + "id": "menu-section-description", + "label": "Menu Section Description", + "optional": false + }, + { + "id": "menu-items", + "label": "Menu Items", + "optional": false + }, + { + "id": "icon", + "label": "Icon", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "description", + "label": "Description", + "optional": false + }, + { + "id": "value", + "label": "Value", + "optional": false + }, + { + "id": "switch", + "label": "Switch", + "optional": false, + "tier": "composite" + }, + { + "id": "checkbox", + "label": "Checkbox", + "optional": false, + "tier": "composite" + }, + { + "id": "thumbnail", + "label": "Thumbnail", + "optional": false, + "tier": "composite" + }, + { + "id": "drill-in-chevron", + "label": "Drill In Chevron", + "optional": false + }, + { + "id": "link-out-icon", + "label": "Link Out Icon", + "optional": false + }, + { + "id": "menu-section-divider", + "label": "Menu Section Divider", + "optional": false + } + ] + }, + "meter": { + "label": "meter", + "source": "docs/s2-docs/components/status/meter.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "value", + "label": "Value", + "optional": false + }, + { + "id": "track", + "label": "Track", + "optional": false + }, + { + "id": "fill", + "label": "Fill", + "optional": false + } + ] + }, + "number-field": { + "label": "number field", + "source": "docs/s2-docs/components/inputs/number-field.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "required-asterisk", + "label": "Required Asterisk", + "optional": false + }, + { + "id": "required-text", + "label": "Required Text", + "optional": false + }, + { + "id": "optional-text", + "label": "Optional Text", + "optional": false + }, + { + "id": "field", + "label": "Field", + "optional": false + }, + { + "id": "value", + "label": "Value", + "optional": false + }, + { + "id": "validation-marker", + "label": "Validation Marker", + "optional": false + }, + { + "id": "error-icon", + "label": "Error Icon", + "optional": false + }, + { + "id": "stepper", + "label": "Stepper", + "optional": false + }, + { + "id": "help-text", + "label": "Help Text", + "optional": false, + "tier": "composite" + } + ] + }, + "picker": { + "label": "picker", + "source": "docs/s2-docs/components/inputs/picker.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "required-asterisk", + "label": "Required Asterisk", + "optional": false + }, + { + "id": "required-text", + "label": "Required Text", + "optional": false + }, + { + "id": "optional-text", + "label": "Optional Text", + "optional": false + }, + { + "id": "field", + "label": "Field", + "optional": false + }, + { + "id": "placeholder", + "label": "Placeholder", + "optional": false + }, + { + "id": "value", + "label": "Value", + "optional": false + }, + { + "id": "error-icon", + "label": "Error Icon", + "optional": false + }, + { + "id": "chevron", + "label": "Chevron", + "optional": false + }, + { + "id": "help-text", + "label": "Help Text", + "optional": false, + "tier": "composite" + }, + { + "id": "menu-container", + "label": "Menu Container", + "optional": false + } + ] + }, + "popover": { + "label": "popover", + "source": "docs/s2-docs/components/containers/popover.md", + "parts": [ + { + "id": "tip", + "label": "Tip", + "optional": true + } + ] + }, + "progress-bar": { + "label": "progress bar", + "source": "docs/s2-docs/components/status/progress-bar.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "value", + "label": "Value", + "optional": false + }, + { + "id": "track", + "label": "Track", + "optional": false + }, + { + "id": "fill", + "label": "Fill", + "optional": false + } + ] + }, + "progress-circle": { + "label": "progress circle", + "source": "docs/s2-docs/components/status/progress-circle.md", + "parts": [ + { + "id": "track", + "label": "Track", + "optional": false + }, + { + "id": "fill", + "label": "Fill", + "optional": false + } + ] + }, + "radio-button": { + "label": "radio button", + "source": "docs/s2-docs/components/inputs/radio-button.md", + "parts": [ + { + "id": "control", + "label": "Control", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": false + } + ] + }, + "radio-group": { + "label": "radio group", + "source": "docs/s2-docs/components/inputs/radio-group.md", + "parts": [ + { + "id": "field-label", + "label": "Field Label", + "optional": false, + "tier": "composite" + }, + { + "id": "radio-button-area", + "label": "Radio Button Area", + "optional": false + }, + { + "id": "help-text", + "label": "Help Text", + "optional": true, + "tier": "composite" + } + ] + }, + "rating": { + "label": "rating", + "source": "docs/s2-docs/components/inputs/rating.md", + "parts": [ + { + "id": "star-workflow-icon", + "label": "Star Workflow Icon", + "optional": false + } + ] + }, + "search-field": { + "label": "search field", + "source": "docs/s2-docs/components/inputs/search-field.md", + "parts": [ + { + "id": "field", + "label": "Field", + "optional": false + }, + { + "id": "leading-icon", + "label": "Leading Icon", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "search-term", + "label": "Search Term", + "optional": false + }, + { + "id": "in-field-button", + "label": "In Field Button", + "optional": false + }, + { + "id": "help-text", + "label": "Help Text", + "optional": false, + "tier": "composite" + } + ] + }, + "segmented-control": { + "label": "segmented control", + "source": "docs/s2-docs/components/inputs/segmented-control.md", + "parts": [ + { + "id": "track", + "label": "Track", + "optional": false + }, + { + "id": "segmented-control-item", + "label": "Segmented Control Item", + "optional": false + }, + { + "id": "icon", + "label": "Icon", + "optional": true + }, + { + "id": "label", + "label": "Label", + "optional": false + } + ] + }, + "select-box": { + "label": "select box", + "source": "docs/s2-docs/components/inputs/select-box.md", + "parts": [ + { + "id": "illustration", + "label": "Illustration", + "optional": true + }, + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "body", + "label": "Body", + "optional": true + }, + { + "id": "checkbox", + "label": "Checkbox", + "optional": false, + "tier": "composite" + } + ] + }, + "side-navigation": { + "label": "side navigation", + "source": "docs/s2-docs/components/navigation/side-navigation.md", + "parts": [ + { + "id": "header", + "label": "Header", + "optional": false + }, + { + "id": "item", + "label": "Item", + "optional": false + }, + { + "id": "icon", + "label": "Icon", + "optional": true + }, + { + "id": "label", + "label": "Label", + "optional": false + } + ] + }, + "slider": { + "label": "slider", + "source": "docs/s2-docs/components/inputs/slider.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": true + }, + { + "id": "value", + "label": "Value", + "optional": true + }, + { + "id": "track", + "label": "Track", + "optional": false + }, + { + "id": "fill", + "label": "Fill", + "optional": false + }, + { + "id": "handle", + "label": "Handle", + "optional": false + } + ] + }, + "standard-dialog": { + "label": "standard dialog", + "source": "docs/s2-docs/components/feedback/standard-dialog.md", + "parts": [ + { + "id": "standard-dialog-container", + "label": "Standard Dialog Container", + "optional": false + }, + { + "id": "cover-image", + "label": "Cover Image", + "optional": true + }, + { + "id": "close-button", + "label": "Close Button", + "optional": true, + "tier": "composite" + }, + { + "id": "header", + "label": "Header", + "optional": false + }, + { + "id": "title", + "label": "Title", + "optional": false + }, + { + "id": "body", + "label": "Body", + "optional": false + }, + { + "id": "description", + "label": "Description", + "optional": true + }, + { + "id": "footer", + "label": "Footer", + "optional": false + }, + { + "id": "footer-content", + "label": "Footer Content", + "optional": true + }, + { + "id": "button-group", + "label": "Button Group", + "optional": false, + "tier": "composite" + }, + { + "id": "overlay", + "label": "Overlay", + "optional": false + } + ] + }, + "standard-panel": { + "label": "standard panel", + "source": "docs/s2-docs/components/containers/standard-panel.md", + "parts": [ + { + "id": "header-content-area", + "label": "Header Content Area", + "optional": false + }, + { + "id": "accordion", + "label": "Accordion", + "optional": false, + "tier": "composite" + }, + { + "id": "back-button", + "label": "Back Button", + "optional": false + }, + { + "id": "close-button", + "label": "Close Button", + "optional": true, + "tier": "composite" + }, + { + "id": "gripper", + "label": "Gripper", + "optional": true + } + ] + }, + "status-light": { + "label": "status light", + "source": "docs/s2-docs/components/status/status-light.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "dot", + "label": "Dot", + "optional": false + } + ] + }, + "steplist": { + "label": "steplist", + "source": "docs/s2-docs/components/status/steplist.md", + "parts": [ + { + "id": "step-item", + "label": "Step Item", + "optional": false + }, + { + "id": "track", + "label": "Track", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": true + } + ] + }, + "swatch": { + "label": "swatch", + "source": "docs/s2-docs/components/inputs/swatch.md", + "parts": [ + { + "id": "container", + "label": "Container", + "optional": false + }, + { + "id": "color", + "label": "Color", + "optional": false + } + ] + }, + "swatch-group": { + "label": "swatch group", + "source": "docs/s2-docs/components/inputs/swatch-group.md", + "parts": [ + { + "id": "swatch", + "label": "Swatch", + "optional": false, + "tier": "composite" + } + ] + }, + "switch": { + "label": "switch", + "source": "docs/s2-docs/components/inputs/switch.md", + "parts": [ + { + "id": "track", + "label": "Track", + "optional": false + }, + { + "id": "handle", + "label": "Handle", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": false + } + ] + }, + "table": { + "label": "table", + "source": "docs/s2-docs/components/containers/table.md", + "parts": [ + { + "id": "column-header", + "label": "Column Header", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "sort-icon", + "label": "Sort Icon", + "optional": false + }, + { + "id": "column-divider", + "label": "Column Divider", + "optional": false + }, + { + "id": "row", + "label": "Row", + "optional": false + }, + { + "id": "cell", + "label": "Cell", + "optional": false + }, + { + "id": "row-divider", + "label": "Row Divider", + "optional": false + } + ] + }, + "tabs": { + "label": "tabs", + "source": "docs/s2-docs/components/navigation/tabs.md", + "parts": [ + { + "id": "tab-item", + "label": "Tab Item", + "optional": false + }, + { + "id": "selection-indicator", + "label": "Selection Indicator", + "optional": false + } + ] + }, + "tag": { + "label": "tag", + "source": "docs/s2-docs/components/inputs/tag.md", + "parts": [ + { + "id": "avatar", + "label": "Avatar", + "optional": true, + "tier": "composite" + }, + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "close-button", + "label": "Close Button", + "optional": true, + "tier": "composite" + } + ] + }, + "tag-field": { + "label": "tag field", + "source": "docs/s2-docs/components/inputs/tag-field.md", + "parts": [ + { + "id": "field-label", + "label": "Field Label", + "optional": false, + "tier": "composite" + }, + { + "id": "text-area", + "label": "Text Area", + "optional": false, + "tier": "composite" + }, + { + "id": "tag", + "label": "Tag", + "optional": false, + "tier": "composite" + }, + { + "id": "avatar", + "label": "Avatar", + "optional": true, + "tier": "composite" + }, + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "close-button", + "label": "Close Button", + "optional": true, + "tier": "composite" + } + ] + }, + "tag-group": { + "label": "tag group", + "source": "docs/s2-docs/components/inputs/tag-group.md", + "parts": [ + { + "id": "field-label", + "label": "Field Label", + "optional": false, + "tier": "composite" + }, + { + "id": "tag", + "label": "Tag", + "optional": false, + "tier": "composite" + }, + { + "id": "action-button", + "label": "Action Button", + "optional": false, + "tier": "composite" + } + ] + }, + "takeover-dialog": { + "label": "takeover dialog", + "source": "docs/s2-docs/components/feedback/takeover-dialog.md", + "parts": [ + { + "id": "takeover-dialog-container", + "label": "Takeover Dialog Container", + "optional": false + }, + { + "id": "header", + "label": "Header", + "optional": false + }, + { + "id": "title", + "label": "Title", + "optional": false + }, + { + "id": "header-content", + "label": "Header Content", + "optional": true + }, + { + "id": "button-group", + "label": "Button Group", + "optional": false, + "tier": "composite" + }, + { + "id": "body", + "label": "Body", + "optional": false + }, + { + "id": "body-content", + "label": "Body Content", + "optional": true + }, + { + "id": "overlay", + "label": "Overlay", + "optional": false + } + ] + }, + "text-area": { + "label": "text area", + "source": "docs/s2-docs/components/inputs/text-area.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "required-asterisk", + "label": "Required Asterisk", + "optional": false + }, + { + "id": "required-text", + "label": "Required Text", + "optional": false + }, + { + "id": "optional-text", + "label": "Optional Text", + "optional": false + }, + { + "id": "character-count", + "label": "Character Count", + "optional": false + }, + { + "id": "field", + "label": "Field", + "optional": false + }, + { + "id": "value", + "label": "Value", + "optional": false + }, + { + "id": "validation-marker", + "label": "Validation Marker", + "optional": false + }, + { + "id": "error-icon", + "label": "Error Icon", + "optional": false + }, + { + "id": "help-text", + "label": "Help Text", + "optional": false, + "tier": "composite" + } + ] + }, + "text-field": { + "label": "text field", + "source": "docs/s2-docs/components/inputs/text-field.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "required-asterisk", + "label": "Required Asterisk", + "optional": false + }, + { + "id": "required-text", + "label": "Required Text", + "optional": false + }, + { + "id": "optional-text", + "label": "Optional Text", + "optional": false + }, + { + "id": "character-count", + "label": "Character Count", + "optional": false + }, + { + "id": "field", + "label": "Field", + "optional": false + }, + { + "id": "value", + "label": "Value", + "optional": false + }, + { + "id": "validation-marker", + "label": "Validation Marker", + "optional": false + }, + { + "id": "error-icon", + "label": "Error Icon", + "optional": false + }, + { + "id": "help-text", + "label": "Help Text", + "optional": false, + "tier": "composite" + } + ] + }, + "thumbnail": { + "label": "thumbnail", + "source": "docs/s2-docs/components/inputs/thumbnail.md", + "parts": [ + { + "id": "container", + "label": "Container", + "optional": false + }, + { + "id": "image", + "label": "Image", + "optional": false + } + ] + }, + "toast": { + "label": "toast", + "source": "docs/s2-docs/components/feedback/toast.md", + "parts": [ + { + "id": "icon", + "label": "Icon", + "optional": false + }, + { + "id": "text", + "label": "Text", + "optional": false + }, + { + "id": "button", + "label": "Button", + "optional": false, + "tier": "composite" + }, + { + "id": "close-button", + "label": "Close Button", + "optional": false, + "tier": "composite" + } + ] + }, + "tooltip": { + "label": "tooltip", + "source": "docs/s2-docs/components/feedback/tooltip.md", + "parts": [ + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "tip", + "label": "Tip", + "optional": false + } + ] + }, + "tree-view": { + "label": "tree view", + "source": "docs/s2-docs/components/navigation/tree-view.md", + "parts": [ + { + "id": "header", + "label": "Header", + "optional": true + }, + { + "id": "tree-view-item", + "label": "Tree View Item", + "optional": false + }, + { + "id": "label", + "label": "Label", + "optional": false + }, + { + "id": "collapse", + "label": "Collapse", + "optional": false + }, + { + "id": "expand-button", + "label": "Expand Button", + "optional": false + }, + { + "id": "checkbox", + "label": "Checkbox", + "optional": true, + "tier": "composite" + }, + { + "id": "drag-icon", + "label": "Drag Icon", + "optional": true + }, + { + "id": "context-area", + "label": "Context Area", + "optional": true + }, + { + "id": "actions-area", + "label": "Actions Area", + "optional": true + }, + { + "id": "in-field-progress-circle", + "label": "In Field Progress Circle", + "optional": false + } + ] + } + } +} diff --git a/packages/design-system-registry/scripts/extract-component-anatomy.js b/packages/design-system-registry/scripts/extract-component-anatomy.js new file mode 100644 index 00000000..9bc3464c --- /dev/null +++ b/packages/design-system-registry/scripts/extract-component-anatomy.js @@ -0,0 +1,319 @@ +/* +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. +*/ + +/** + * Extract component anatomy data from S2 documentation markdown files + * and write a structured component-anatomy.json registry. + * + * Usage: node packages/design-system-registry/scripts/extract-component-anatomy.js + */ + +import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs"; +import { join, basename, dirname, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const REPO_ROOT = join(__dirname, "../../.."); +const S2_DOCS_DIR = join(REPO_ROOT, "docs/s2-docs/components"); +const OUTPUT_PATH = join(__dirname, "../registry/component-anatomy.json"); +const CURATION_PATH = join( + __dirname, + "../registry/component-anatomy-curation.json", +); + +/** + * Convert a display name to a kebab-case ID. + * "Action Button" → "action-button" + * "hold icon" → "hold-icon" + */ +export function toKebabCase(name) { + return name + .replace(/([a-z])([A-Z])/g, "$1-$2") // camelCase → kebab + .toLowerCase() + .replace(/\s+/g, "-"); +} + +/** + * Convert a kebab-case ID to a title-case label. + * "hold-icon" → "Hold Icon" + */ +function toTitleCase(kebab) { + return kebab + .split("-") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); +} + +/** + * Split a text by ", ", " or ", and " and " separators, + * but only when they appear outside parentheses. + * e.g. "icon (optional) and text (description or error message)" + * → ["icon (optional)", "text (description or error message)"] + * e.g. "required asterisk, required text, or optional text" + * → ["required asterisk", "required text", "optional text"] + */ +export function splitOutsideParens(text) { + // Replace content inside parens with placeholders to avoid splitting inside them + const placeholders = []; + const masked = text.replace(/\([^)]*\)/g, (match) => { + placeholders.push(match); + return `\x00${placeholders.length - 1}\x00`; + }); + + // Split on ", or " / ", " / " or " / " and " / " / " (try longest matches first) + const segments = masked.split(/,\s+or\s+|,\s+|\s+or\s+|\s+and\s+|\s+\/\s+/); + + // Restore placeholders + return segments + .map((s) => + s.replace(/\x00(\d+)\x00/g, (_, idx) => placeholders[Number(idx)]).trim(), + ) + .filter((s) => s.length > 0); +} + +/** + * Parse a single part line like "cover image (optional)" or "preview (asset)". + * Returns { name, annotation } where annotation may be null. + */ +export function parsePartLine(line) { + const trimmed = line.replace(/^\s*-\s*/, "").trim(); + if (!trimmed) return null; + + const match = trimmed.match(/^(.+?)\s*\(([^)]+)\)\s*$/); + if (match) { + return { name: match[1].trim(), annotation: match[2].trim() }; + } + return { name: trimmed, annotation: null }; +} + +/** + * Parse the anatomy code block content into structured data. + * + * @param {string} blockContent - The text inside the fenced code block + * @returns {{ componentName: string, parts: Array<{ id: string, label: string, optional: boolean }> }} + */ +export function parseAnatomyBlock(blockContent) { + const lines = blockContent.split("\n").filter((l) => l.trim().length > 0); + if (lines.length === 0) return null; + + // First line is the component name (strip any annotation) + const rootParsed = parsePartLine("- " + lines[0]); + const componentName = rootParsed ? rootParsed.name : lines[0].trim(); + + const partsMap = new Map(); + + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + // Skip lines that don't start with - (after stripping whitespace) + if (!line.trim().startsWith("-")) continue; + + // Handle compound/alternative lines: + // " and " = co-existing parts: "icon (optional) and text (description)" + // ", " or " or " = alternatives: "required asterisk, required text, or optional text" + // Split on these separators only when they appear outside parentheses. + const rawText = line.replace(/^\s*-\s*/, ""); + const compoundParts = splitOutsideParens(rawText).map( + (p) => "- " + p.trim(), + ); + + for (const part of compoundParts) { + const parsed = parsePartLine(part); + if (!parsed) continue; + + const id = toKebabCase(parsed.name); + const optional = + parsed.annotation !== null && + parsed.annotation.toLowerCase().includes("optional"); + + // Deduplicate by ID (keep the first occurrence, but mark optional + // only if ALL occurrences are optional) + if (!partsMap.has(id)) { + partsMap.set(id, { + id, + label: toTitleCase(id), + optional, + }); + } + } + } + + return { + componentName, + parts: [...partsMap.values()], + }; +} + +/** + * Extract the anatomy code block from a markdown file's content. + * Returns the block content or null if not found. + */ +export function extractAnatomyBlock(markdown) { + // Match ## Anatomy followed by a fenced code block + const match = markdown.match(/## Anatomy\s*\n+```[^\n]*\n([\s\S]*?)```/); + return match ? match[1] : null; +} + +/** + * Recursively find all .md files in a directory. + */ +function findMarkdownFiles(dir) { + const results = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...findMarkdownFiles(fullPath)); + } else if (entry.name.endsWith(".md")) { + results.push(fullPath); + } + } + return results; +} + +/** + * Load the curation config and apply removals, renames, and tags + * to the extracted component data. + */ +export function applyCuration(components, curationPath) { + let curation; + try { + curation = JSON.parse(readFileSync(curationPath, "utf-8")); + } catch { + // No curation file — return as-is + return { components, stats: { removed: 0, renamed: 0, tagged: 0 } }; + } + + const removals = curation.removals ?? {}; + const renames = curation.renames ?? {}; + const tags = curation.tags ?? {}; + let removedCount = 0; + let renamedCount = 0; + let taggedCount = 0; + + for (const [componentId, component] of Object.entries(components)) { + const curated = []; + const seenIds = new Set(); + + for (const part of component.parts) { + let { id } = part; + + // Apply removals + if (id in removals) { + removedCount++; + continue; + } + + // Apply renames + if (id in renames) { + const newId = renames[id].to; + id = newId; + part.id = newId; + part.label = toTitleCase(newId); + renamedCount++; + } + + // Apply tags + if (id in tags) { + part.tier = tags[id].tier; + taggedCount++; + } + + // Deduplicate after rename (e.g., footer-area→footer may collide) + if (!seenIds.has(id)) { + seenIds.add(id); + curated.push(part); + } + } + + component.parts = curated; + } + + return { + components, + stats: { + removed: removedCount, + renamed: renamedCount, + tagged: taggedCount, + }, + }; +} + +function main() { + const files = findMarkdownFiles(S2_DOCS_DIR).sort(); + const components = {}; + const skipped = []; + + for (const filePath of files) { + const markdown = readFileSync(filePath, "utf-8"); + const block = extractAnatomyBlock(markdown); + const componentId = basename(filePath, ".md"); + const relPath = relative(REPO_ROOT, filePath); + + if (!block) { + skipped.push(componentId); + continue; + } + + const parsed = parseAnatomyBlock(block); + if (!parsed || parsed.parts.length === 0) { + skipped.push(componentId); + continue; + } + + components[componentId] = { + label: parsed.componentName, + source: relPath, + parts: parsed.parts, + }; + } + + // Apply curation rules (removals, renames, tags) + const { components: curated, stats } = applyCuration( + components, + CURATION_PATH, + ); + + // Sort by component ID + const sorted = {}; + for (const key of Object.keys(curated).sort()) { + sorted[key] = curated[key]; + } + + const output = { + type: "component-anatomy", + description: + "Mapping of Spectrum components to their visible anatomy parts, extracted from S2 documentation and curated via component-anatomy-curation.json.", + components: sorted, + }; + + writeFileSync(OUTPUT_PATH, JSON.stringify(output, null, 2) + "\n", "utf-8"); + + const componentCount = Object.keys(sorted).length; + const partCount = Object.values(sorted).reduce( + (sum, c) => sum + c.parts.length, + 0, + ); + console.log( + `Generated component-anatomy.json: ${componentCount} components, ${partCount} total parts`, + ); + if (stats.removed || stats.renamed || stats.tagged) { + console.log( + `Curation applied: ${stats.removed} removed, ${stats.renamed} renamed, ${stats.tagged} tagged`, + ); + } + if (skipped.length > 0) { + console.log(`Skipped (no anatomy section): ${skipped.join(", ")}`); + } +} + +main(); diff --git a/packages/design-system-registry/test/component-anatomy.test.js b/packages/design-system-registry/test/component-anatomy.test.js new file mode 100644 index 00000000..9d11b1fc --- /dev/null +++ b/packages/design-system-registry/test/component-anatomy.test.js @@ -0,0 +1,256 @@ +/* +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 test from "ava"; +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + parseAnatomyBlock, + extractAnatomyBlock, + parsePartLine, + toKebabCase, +} from "../scripts/extract-component-anatomy.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const REGISTRY_PATH = join(__dirname, "../registry/component-anatomy.json"); + +// --- Unit tests for parsing helpers --- + +test("toKebabCase converts display names", (t) => { + t.is(toKebabCase("Action Button"), "action-button"); + t.is(toKebabCase("hold icon"), "hold-icon"); + t.is(toKebabCase("label"), "label"); + t.is(toKebabCase("cover image"), "cover-image"); +}); + +test("parsePartLine extracts name and annotation", (t) => { + t.deepEqual(parsePartLine("- label"), { name: "label", annotation: null }); + t.deepEqual(parsePartLine(" - cover image (optional)"), { + name: "cover image", + annotation: "optional", + }); + t.deepEqual(parsePartLine("- preview (asset)"), { + name: "preview", + annotation: "asset", + }); + t.is(parsePartLine("- "), null); +}); + +test("parseAnatomyBlock: button anatomy", (t) => { + const result = parseAnatomyBlock("button\n- label"); + t.is(result.componentName, "button"); + t.is(result.parts.length, 1); + t.is(result.parts[0].id, "label"); + t.is(result.parts[0].optional, false); +}); + +test("parseAnatomyBlock: slider with optional parts", (t) => { + const block = `slider +- label (optional) +- value (optional) +- track +- fill +- handle`; + const result = parseAnatomyBlock(block); + t.is(result.componentName, "slider"); + t.is(result.parts.length, 5); + t.is(result.parts[0].id, "label"); + t.is(result.parts[0].optional, true); + t.is(result.parts[2].id, "track"); + t.is(result.parts[2].optional, false); +}); + +test("parseAnatomyBlock: deduplicates by ID", (t) => { + const block = `tabs +- tab item (selected) +- tab item +- selection indicator`; + const result = parseAnatomyBlock(block); + const ids = result.parts.map((p) => p.id); + t.deepEqual(ids, ["tab-item", "selection-indicator"]); +}); + +test("parseAnatomyBlock: compound line with and", (t) => { + const block = `help text (placed under the input) +- icon (optional) and text (description or error message)`; + const result = parseAnatomyBlock(block); + t.is(result.componentName, "help text"); + t.is(result.parts.length, 2); + t.is(result.parts[0].id, "icon"); + t.is(result.parts[0].optional, true); + t.is(result.parts[1].id, "text"); + t.is(result.parts[1].optional, false); +}); + +test("parseAnatomyBlock: strips root annotation", (t) => { + const block = `help text (placed under the input) +- icon (optional)`; + const result = parseAnatomyBlock(block); + t.is(result.componentName, "help text"); +}); + +test("extractAnatomyBlock: extracts from markdown", (t) => { + const md = `# Button + +## Anatomy + +\`\`\` +button +- label +\`\`\` + +## Component options +`; + const block = extractAnatomyBlock(md); + t.truthy(block); + t.true(block.includes("button")); + t.true(block.includes("- label")); +}); + +test("extractAnatomyBlock: returns null when no anatomy section", (t) => { + const md = `# Link\n\n## Overview\n\nSome text.`; + t.is(extractAnatomyBlock(md), null); +}); + +// --- Tests against the generated registry file --- + +const registry = JSON.parse(readFileSync(REGISTRY_PATH, "utf-8")); + +test("generated registry has type and description", (t) => { + t.is(registry.type, "component-anatomy"); + t.truthy(registry.description); +}); + +test("generated registry has 60+ components", (t) => { + const count = Object.keys(registry.components).length; + t.true(count >= 60, `Expected 60+ components, got ${count}`); +}); + +test("button has label", (t) => { + const parts = registry.components.button.parts.map((p) => p.id); + t.deepEqual(parts, ["label"]); +}); + +test("slider has track, fill, handle", (t) => { + const parts = registry.components.slider.parts.map((p) => p.id); + t.true(parts.includes("track")); + t.true(parts.includes("fill")); + t.true(parts.includes("handle")); +}); + +test("slider label is optional, track is not", (t) => { + const slider = registry.components.slider; + const label = slider.parts.find((p) => p.id === "label"); + const track = slider.parts.find((p) => p.id === "track"); + t.is(label.optional, true); + t.is(track.optional, false); +}); + +test("checkbox has control and label", (t) => { + const parts = registry.components.checkbox.parts.map((p) => p.id); + t.deepEqual(parts, ["control", "label"]); +}); + +test("standard-dialog has 10+ parts", (t) => { + const count = registry.components["standard-dialog"].parts.length; + t.true(count >= 10, `Expected 10+ parts, got ${count}`); +}); + +test("action-button hold-icon is optional", (t) => { + const ab = registry.components["action-button"]; + const holdIcon = ab.parts.find((p) => p.id === "hold-icon"); + t.truthy(holdIcon); + t.is(holdIcon.optional, true); +}); + +test("tabs deduplicated tab-item", (t) => { + const parts = registry.components.tabs.parts.map((p) => p.id); + const tabItemCount = parts.filter((p) => p === "tab-item").length; + t.is(tabItemCount, 1); +}); + +// --- Curation tests --- + +test("numbering artifacts are normalized to base term", (t) => { + const ag = registry.components["action-group"]; + const ids = ag.parts.map((p) => p.id); + t.false(ids.includes("action-button-1")); + t.false(ids.includes("action-button-2")); + t.true(ids.includes("action-button")); +}); + +test("background (token object) is removed from anatomy", (t) => { + for (const [, component] of Object.entries(registry.components)) { + const ids = component.parts.map((p) => p.id); + t.false( + ids.includes("background"), + "background should be removed (it is a token object, not anatomy)", + ); + } +}); + +test("-area suffixes are renamed to base terms", (t) => { + const dialog = registry.components["standard-dialog"]; + const ids = dialog.parts.map((p) => p.id); + t.true(ids.includes("header")); + t.true(ids.includes("body")); + t.true(ids.includes("footer")); + t.false(ids.includes("header-area")); + t.false(ids.includes("body-area")); + t.false(ids.includes("footer-area")); +}); + +test("small-divider renamed to divider", (t) => { + const accordion = registry.components["accordion"]; + const ids = accordion.parts.map((p) => p.id); + t.true(ids.includes("divider")); + t.false(ids.includes("small-divider")); +}); + +test("composite parts are tagged with tier", (t) => { + const cards = registry.components["cards"]; + const checkbox = cards.parts.find((p) => p.id === "checkbox"); + t.truthy(checkbox); + t.is(checkbox.tier, "composite"); +}); + +test("all part IDs are kebab-case", (t) => { + for (const [componentId, component] of Object.entries(registry.components)) { + for (const part of component.parts) { + t.regex( + part.id, + /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, + `${componentId} part "${part.id}" is not kebab-case`, + ); + } + } +}); + +test("all components have at least one part", (t) => { + for (const [componentId, component] of Object.entries(registry.components)) { + t.true(component.parts.length > 0, `${componentId} has no anatomy parts`); + } +}); + +test("no duplicate part IDs within a component", (t) => { + for (const [componentId, component] of Object.entries(registry.components)) { + const ids = component.parts.map((p) => p.id); + const unique = new Set(ids); + t.is( + ids.length, + unique.size, + `${componentId} has duplicate part IDs: ${ids}`, + ); + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2acbc4f6..19b970c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -672,6 +672,28 @@ importers: tools/token-manifest-builder: {} + tools/token-name-builder: + dependencies: + "@spectrum-css/tokens": + specifier: ^16.0.2 + version: 16.0.2 + lit: + specifier: ^3.1.0 + version: 3.3.0 + devDependencies: + "@ava/typescript": + specifier: ^6.0.0 + version: 6.0.0 + ava: + specifier: ^6.0.1 + version: 6.4.0(@ava/typescript@6.0.0)(rollup@4.44.1) + typescript: + specifier: ^5.3.3 + version: 5.8.3 + vite: + specifier: ^5.4.0 + version: 5.4.19(@types/node@22.15.33)(terser@5.44.1) + tools/transform-tokens-json: dependencies: jsonpath-plus: diff --git a/tools/token-name-builder/ava.config.js b/tools/token-name-builder/ava.config.js new file mode 100644 index 00000000..82b978b1 --- /dev/null +++ b/tools/token-name-builder/ava.config.js @@ -0,0 +1,19 @@ +/* +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. +*/ + +export default { + files: ["test/**/*.test.js"], + verbose: true, + environmentVariables: { + NODE_ENV: "test", + }, +}; diff --git a/tools/token-name-builder/index.html b/tools/token-name-builder/index.html new file mode 100644 index 00000000..6fa75326 --- /dev/null +++ b/tools/token-name-builder/index.html @@ -0,0 +1,24 @@ + + + +
+ + +