From cd28625d06a6755bcc814e32d210b67e2b941511 Mon Sep 17 00:00:00 2001
From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com>
Date: Mon, 28 Sep 2026 16:03:00 +0200
Subject: [PATCH 1/2] feat: CTORNDSD-679
Update Storybook and documentation links for React components, integrate Web Components support, and enhance build workflows.
---
.dockerignore | 10 +
.github/workflows/pr-head.yaml | 6 +
.gitignore | 1 +
CLAUDE.md | 5 +-
Dockerfile.storybook | 11 +-
README.md | 32 +-
bin/storybook/check-links.mjs | 115 ++++
bin/storybook/legacy-react-ids.json | 584 ++++++++++++++++++
bin/storybook/link-validation.mjs | 42 ++
bin/storybook/link-validation.test.mjs | 46 ++
bin/storybook/package-build.mjs | 10 +
bin/storybook/routes.d.mts | 4 +
bin/storybook/routes.mjs | 40 ++
bin/storybook/routes.test.mjs | 57 ++
bin/storybook/smoke-test.mjs | 104 ++++
libs/ui/.storybook/main.ts | 16 +-
libs/ui/.storybook/preview.ts | 53 +-
libs/ui/.storybook/public/defaultTheme.json | 6 +-
libs/ui/README.md | 4 +-
libs/ui/llms.txt | 36 +-
libs/ui/project.json | 3 +
libs/ui/src/ai/README.md | 4 +-
libs/ui/src/ai/a2ui/system-prompt.ts | 2 +-
.../organisms/Chart/Chart.stories.tsx | 2 +-
.../stories/Introduction/Getting-started.mdx | 4 +-
.../stories/Introduction/How-to-use-theme.mdx | 8 +-
.../Introduction/Theme-tokens-usage.mdx | 6 +-
.../Introduction/Theming-and-tokens.mdx | 42 +-
libs/ui/src/stories/Introduction/Welcome.mdx | 4 +
.../AI-Experience.mdx | 4 +-
.../Prompt-Engineering-AI-Integration.mdx | 4 +-
libs/ui/src/stories/chart/01-Introduction.mdx | 2 +-
.../form-configurator/01-GettingStarted.mdx | 10 +-
.../form-configurator/02-DataSchema.mdx | 6 +-
.../stories/form-configurator/03-Controls.mdx | 8 +-
.../stories/form-configurator/04-UISchema.mdx | 6 +-
.../stories/form-configurator/05-Logger.mdx | 4 +-
.../form-configurator/06-CustomControls.mdx | 6 +-
libs/web-components/.storybook/main.ts | 15 +
libs/web-components/.storybook/preview.ts | 15 +
libs/web-components/.storybook/vite.config.ts | 3 +
libs/web-components/README.md | 6 +
libs/web-components/project.json | 3 +
.../stories/Overview.stories.ts | 26 +
libs/web-components/tsconfig.storybook.json | 11 +
nginx.conf | 8 +
package-lock.json | 40 ++
package.json | 8 +-
48 files changed, 1325 insertions(+), 117 deletions(-)
create mode 100644 .dockerignore
create mode 100644 bin/storybook/check-links.mjs
create mode 100644 bin/storybook/legacy-react-ids.json
create mode 100644 bin/storybook/link-validation.mjs
create mode 100644 bin/storybook/link-validation.test.mjs
create mode 100644 bin/storybook/package-build.mjs
create mode 100644 bin/storybook/routes.d.mts
create mode 100644 bin/storybook/routes.mjs
create mode 100644 bin/storybook/routes.test.mjs
create mode 100644 bin/storybook/smoke-test.mjs
create mode 100644 libs/web-components/.storybook/main.ts
create mode 100644 libs/web-components/.storybook/preview.ts
create mode 100644 libs/web-components/.storybook/vite.config.ts
create mode 100644 libs/web-components/stories/Overview.stories.ts
create mode 100644 libs/web-components/tsconfig.storybook.json
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..839254a
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,10 @@
+.git
+.nx
+node_modules
+**/node_modules
+dist
+tmp
+**/storybook-static
+**/coverage
+**/.vitest-artifacts
+**/.vitest-attachments
diff --git a/.github/workflows/pr-head.yaml b/.github/workflows/pr-head.yaml
index 745494c..b485d47 100644
--- a/.github/workflows/pr-head.yaml
+++ b/.github/workflows/pr-head.yaml
@@ -37,6 +37,11 @@ jobs:
- name: Build Storybook
run: npm run build-storybook
+ - name: Verify combined Storybook in Chromium
+ run: |
+ npx playwright install --with-deps chromium
+ node bin/storybook/smoke-test.mjs
+
- name: Build Form Configurator
run: npm run build:form-configurator
@@ -62,6 +67,7 @@ jobs:
npx nx build ui
cp libs/ui/package.json dist/libs/ui/package.json
node libs/ui/scripts/postbuild-types.mjs
+ node bin/storybook/check-links.mjs --package
- name: Upload dist/libs/ui artifact
uses: actions/upload-artifact@v7
diff --git a/.gitignore b/.gitignore
index c35a2fe..10dbba2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,6 +53,7 @@ libs/ui/coverage/*
# storybook
storybook-static/*
libs/ui/storybook-static/*
+libs/web-components/storybook-static/*
*storybook.log
vite.config.*.timestamp*
diff --git a/CLAUDE.md b/CLAUDE.md
index 74c0398..5e1b053 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -18,7 +18,7 @@ Node >= 22.17.0, npm workspaces. Run `npm install` after cloning.
```bash
# Development
-npm run storybook # Storybook dev server at http://localhost:6006
+npm run storybook # Shared React + Web Components Storybook at http://localhost:6006
npm test # Unit tests for gd-design-library (vitest, jsdom)
npm run test:ai # AI integration tests in src/ai/__tests__/
npm run test:form-configurator # Unit tests for both form-configurator packages (jest)
@@ -39,6 +39,7 @@ npm run storybook:visual # Run visual regression tests
npm run build:ui # Full gd-design-library build: readme-stats + ai-docs + type-check + nx build + postbuild-types
npm run build:form-configurator
npm run build-storybook
+npm run storybook:check-links # Built story indexes + source/shipped AI link validation
# Verification (run on built dist — requires npm run build:ui first)
npm run verify:ui:full # All 10 verification phases + Verdaccio smoke test
@@ -67,6 +68,8 @@ Path aliases defined in `libs/ui/vitest.alias.ts` and `libs/ui/tsconfig.json`: `
**Build output** — Vite produces both ESM (`.js`, `preserveModules`) and CJS (`.cjs`) to `dist/libs/ui`. Post-build step (`scripts/postbuild-types.mjs`) patches `.d.ts` exports. React and `@emotion/*` are externalized. `bin/export-theme.js` serializes `defaultTheme` to JSON from the built dist; it runs automatically as part of `npm run storybook` and `npm run build-storybook`.
+**Shared Storybook** — `npm run storybook` manages the React host (6006) and native Web Components preview (6007) together. Open only the host. The React catalog receives a central `React/` title prefix; do not duplicate that prefix in stories or MDX. The combined static artifact is `libs/ui/storybook-static`, with the child under `web-components/`. Use generated story IDs for links and run `storybook:check-links` after the combined build. Package-only validation uses `node bin/storybook/check-links.mjs --package` and indexes source stories without building previews. Do not regenerate the frozen legacy route inventory to conceal missing entries. Phase 1 contains only the native overview; component coverage and harness cleanup are later phases.
+
**Package subpaths** — the published package exposes four explicit subpaths (wildcards are intentionally removed to prevent internal path coupling):
| Subpath | Contents |
diff --git a/Dockerfile.storybook b/Dockerfile.storybook
index 7b99bc0..eb26d28 100644
--- a/Dockerfile.storybook
+++ b/Dockerfile.storybook
@@ -3,13 +3,16 @@ WORKDIR /app
COPY package.json package-lock.json nx.json tsconfig.base.json .npmrc eslint.config.mjs ./
COPY libs/ui/ libs/ui/
-COPY bin/export-theme.js bin/export-theme.js
+COPY libs/design-core/ libs/design-core/
+COPY libs/web-components/ libs/web-components/
+COPY libs/form-configurator/ libs/form-configurator/
+COPY libs/react-native/package.json libs/react-native/package.json
+COPY bin/ bin/
+COPY README.md CLAUDE.md ./
RUN npm ci
-RUN npx nx build ui \
- && node ./bin/export-theme.js \
- && npx nx run ui:build-storybook
+RUN npm run build-storybook
FROM nginx:alpine AS runner
WORKDIR /usr/share/nginx/html
diff --git a/README.md b/README.md
index f3f08f3..d8ad8dd 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ export default function App() {
}
```
-See the [Storybook](https://github.com/griddynamics/gd-design-system#storybook) for interactive component examples and full API documentation.
+See the [React catalog in Storybook](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/react-introduction-welcome--docs) for interactive component examples and full API documentation.
## Setup
@@ -52,7 +52,7 @@ npm install
## Development
```bash
-npm run storybook # Storybook at http://localhost:6006
+npm run storybook # Shared React + Web Components Storybook at http://localhost:6006
npm test # gd-design-library unit tests
npm run test:form-configurator # form-configurator tests
npm run type-check # TypeScript check
@@ -64,9 +64,35 @@ npm run lint # ESLint
```bash
npm run build:ui # Build gd-design-library (ESM + CJS + types)
npm run build:form-configurator # Build both form-configurator packages
-npm run build-storybook # Build static Storybook
+npm run build-storybook # Build and validate the complete static Storybook
```
+## Storybook
+
+One entry point contains two peer catalogs: **React** (the existing stories and documentation)
+and **Web Components** (native custom-element stories). The Web Components catalog currently
+contains the Phase 1 overview; Phase 2 adds stories for the six existing atoms and two molecules.
+
+`npm run storybook` manages both preview servers automatically; open `http://localhost:6006`.
+Ports 6006 and 6007 must be free. There is no separate Web Components startup command.
+`npm run build-storybook` produces one deployable directory at `libs/ui/storybook-static`,
+including the composed preview under `web-components/`.
+
+```bash
+npm run storybook:check-links # Validate built Storybooks and shipped AI documentation links
+npm run storybook:test # Existing React interaction tests (native coverage follows in Phase 2)
+npm run storybook:visual # Existing React visual tests
+```
+
+React titles receive their `React/` prefix in `.storybook/main.ts`; do not add it again in
+individual story/MDX titles. Use the IDs in the generated `index.json` for links. Old published
+React manager/iframe links redirect to their new IDs while retaining args, globals, and anchors.
+`bin/storybook/legacy-react-ids.json` is the frozen pre-migration inventory, not a generated
+list to refresh when a story disappears. Link checks cover source documentation and the
+`llms.txt`, AI Markdown/JSON, and AI JavaScript copied into the UI package.
+
+The Angular/Vue and other Web Components harnesses are unchanged in Phase 1.
+
## Verification (gd-design-library)
```bash
diff --git a/bin/storybook/check-links.mjs b/bin/storybook/check-links.mjs
new file mode 100644
index 0000000..fd1549e
--- /dev/null
+++ b/bin/storybook/check-links.mjs
@@ -0,0 +1,115 @@
+import { execFileSync } from 'node:child_process';
+import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { extname, join, relative, sep } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { legacyReactIds } from './routes.mjs';
+import { validateLinks } from './link-validation.mjs';
+
+const root = fileURLToPath(new URL('../../', import.meta.url));
+const built = process.argv.includes('--built');
+const checkPackage = built || process.argv.includes('--package');
+const readJson = (path) => JSON.parse(readFileSync(path, 'utf8'));
+let indexes;
+if (built) {
+ indexes = {
+ react: readJson(join(root, 'libs/ui/storybook-static/index.json')),
+ webComponents: readJson(join(root, 'libs/ui/storybook-static/web-components/index.json')),
+ };
+} else {
+ // Indexing does not build the UI or either preview. Package builds can check
+ // links without creating a UI -> Storybook -> UI build dependency cycle.
+ const temporary = mkdtempSync(join(tmpdir(), 'gridkit-storybook-index-'));
+ try {
+ indexes = {};
+ for (const [catalog, project] of [
+ ['react', 'ui'],
+ ['webComponents', 'web-components'],
+ ]) {
+ const output = join(temporary, `${catalog}.json`);
+ execFileSync(
+ process.execPath,
+ [
+ join(root, 'node_modules/storybook/dist/bin/dispatcher.js'),
+ 'index',
+ '-c',
+ '.storybook',
+ '-o',
+ output,
+ '--disable-telemetry',
+ ],
+ { cwd: join(root, 'libs', project), stdio: 'inherit' }
+ );
+ indexes[catalog] = readJson(output);
+ }
+ } finally {
+ rmSync(temporary, { recursive: true, force: true });
+ }
+}
+
+const errors = [];
+for (const id of legacyReactIds) {
+ if (!indexes.react.entries[`react-${id}`]) errors.push(`Missing migrated React entry: ${id}`);
+}
+for (const entry of Object.values(indexes.react.entries)) {
+ if (!entry.title.startsWith('React/')) errors.push(`Entry is outside the React root: ${entry.id}`);
+}
+if (!indexes.webComponents.entries['introduction-overview--overview']) errors.push('Missing Web Components overview');
+
+const excluded = new Set(['node_modules', '.git', 'dist', 'storybook-static', 'coverage', 'output', '.nx']);
+const extensions = new Set([
+ '.md',
+ '.mdx',
+ '.txt',
+ '.json',
+ '.ts',
+ '.tsx',
+ '.js',
+ '.jsx',
+ '.mjs',
+ '.cjs',
+ '.html',
+ '.yml',
+ '.yaml',
+]);
+function filesIn(path) {
+ if (!existsSync(path)) return [];
+ return readdirSync(path, { withFileTypes: true }).flatMap((entry) => {
+ if (entry.isSymbolicLink() || excluded.has(entry.name)) return [];
+ const file = join(path, entry.name);
+ return entry.isDirectory() ? filesIn(file) : extensions.has(extname(file)) ? [file] : [];
+ });
+}
+
+// The frozen migration registry and its tests intentionally contain old IDs.
+const sourceFiles = ['README.md', 'CLAUDE.md']
+ .map((file) => join(root, file))
+ .filter(existsSync)
+ .concat(
+ ...['libs', '.github'].map((dir) => filesIn(join(root, dir))),
+ filesIn(join(root, 'bin')).filter((file) => !file.startsWith(join(root, 'bin', 'storybook') + sep))
+ );
+const packageFiles = [];
+if (checkPackage) {
+ for (const file of ['llms.txt', 'ai/README.md']) {
+ if (!existsSync(join(root, 'dist/libs/ui', file))) errors.push(`Missing package artifact: ${file}`);
+ }
+ packageFiles.push(...filesIn(join(root, 'dist/libs/ui/ai')));
+ for (const file of readdirSync(join(root, 'dist/libs/ui'))) {
+ if (/\.(md|txt)$/.test(file)) packageFiles.push(join(root, 'dist/libs/ui', file));
+ }
+}
+let checked = 0;
+for (const file of [...sourceFiles, ...packageFiles]) {
+ const result = validateLinks(readFileSync(file, 'utf8'), indexes);
+ checked += result.checked;
+ errors.push(...result.errors.map(({ line, message }) => `${relative(root, file)}:${line}: ${message}`));
+}
+if (errors.length) {
+ console.error(errors.join('\n'));
+ process.exitCode = 1;
+} else {
+ console.log(
+ `Verified ${legacyReactIds.length} migrated entries and ${checked} Storybook links in source${checkPackage ? ' and built package artifacts' : ''}.`
+ );
+}
diff --git a/bin/storybook/legacy-react-ids.json b/bin/storybook/legacy-react-ids.json
new file mode 100644
index 0000000..1c52f15
--- /dev/null
+++ b/bin/storybook/legacy-react-ids.json
@@ -0,0 +1,584 @@
+[
+ "atoms-avatar--default",
+ "atoms-avatar--default-tokens",
+ "atoms-avatar--docs",
+ "atoms-avatar--with-accessibility",
+ "atoms-avatar--with-badge",
+ "atoms-avatar--with-custom-background-color",
+ "atoms-avatar--with-custom-badge-color",
+ "atoms-avatar--with-different-size",
+ "atoms-avatar--with-icon",
+ "atoms-avatar--with-image-and-fallback",
+ "atoms-avatar--with-initials",
+ "atoms-badge--default",
+ "atoms-badge--default-tokens",
+ "atoms-badge--disabled",
+ "atoms-badge--docs",
+ "atoms-badge--sizes",
+ "atoms-badge--variants",
+ "atoms-badge--with-accessibility",
+ "atoms-badge--with-box-styles",
+ "atoms-badge--with-icons",
+ "atoms-box--bordered",
+ "atoms-box--centered-content",
+ "atoms-box--default",
+ "atoms-box--default-tokens",
+ "atoms-box--docs",
+ "atoms-box--examples",
+ "atoms-box--highlighted",
+ "atoms-box--horizontal-layout",
+ "atoms-box--vertical-layout",
+ "atoms-box--with-accessibility",
+ "atoms-box--with-shadow-hover",
+ "atoms-button--all-variants",
+ "atoms-button--button-states-using-class",
+ "atoms-button--custom-styled-button",
+ "atoms-button--default",
+ "atoms-button--default-tokens",
+ "atoms-button--disabled-button",
+ "atoms-button--docs",
+ "atoms-button--full-width-button",
+ "atoms-button--icon-only",
+ "atoms-button--is-loading",
+ "atoms-button--real-world-examples",
+ "atoms-button--rounded-button",
+ "atoms-button--with-accessibility",
+ "atoms-button--with-icons",
+ "atoms-checkbox--controlled",
+ "atoms-checkbox--default",
+ "atoms-checkbox--default-tokens",
+ "atoms-checkbox--disabled",
+ "atoms-checkbox--docs",
+ "atoms-checkbox--indeterminate",
+ "atoms-checkbox--sizes",
+ "atoms-icon--all-icons",
+ "atoms-icon--default",
+ "atoms-icon--default-tokens",
+ "atoms-icon--docs",
+ "atoms-icon--registering-custom-icons",
+ "atoms-icon--with-accessibility",
+ "atoms-icon--with-defined-size",
+ "atoms-image--clickable",
+ "atoms-image--default",
+ "atoms-image--default-tokens",
+ "atoms-image--docs",
+ "atoms-image--fallback-image",
+ "atoms-image--with-accessibility",
+ "atoms-image--with-as-prop",
+ "atoms-image--with-caption",
+ "atoms-image--with-caption-as-prop",
+ "atoms-image--with-placeholder",
+ "atoms-input--checkbox-with-label",
+ "atoms-input--custom-styles",
+ "atoms-input--default-tokens",
+ "atoms-input--default-with-tailwind",
+ "atoms-input--disabled",
+ "atoms-input--docs",
+ "atoms-input--error-with-label-and-helper-text",
+ "atoms-input--primary-default-with-label-and-helper-text",
+ "atoms-input--radio-controlled-with-label",
+ "atoms-input--radio-group-with-label",
+ "atoms-input--read-only",
+ "atoms-input--success-with-label-and-helper-text",
+ "atoms-input--warning-with-label-and-helper-text",
+ "atoms-input--with-accessibility",
+ "atoms-input--with-adornments",
+ "atoms-input--with-end-adornment-as-icon",
+ "atoms-input--with-start-adornment",
+ "atoms-input--wrapper-as-span",
+ "atoms-inputfile--custom-label",
+ "atoms-inputfile--default",
+ "atoms-inputfile--default-tokens",
+ "atoms-inputfile--disabled",
+ "atoms-inputfile--docs",
+ "atoms-inputfile--icon-label",
+ "atoms-inputfile--multiple",
+ "atoms-inputfile--with-accept",
+ "atoms-inputfile--with-accessibility",
+ "atoms-inputfile--with-custom-styles",
+ "atoms-label--custom-styles",
+ "atoms-label--default-tokens",
+ "atoms-label--default-with-tailwind",
+ "atoms-label--docs",
+ "atoms-label--label-default",
+ "atoms-label--label-with-child-icon",
+ "atoms-label--with-accessibility",
+ "atoms-link--custom-styles",
+ "atoms-link--default-tokens",
+ "atoms-link--disabled",
+ "atoms-link--docs",
+ "atoms-link--inherit-with-button-as-child",
+ "atoms-link--inherit-with-tailwind",
+ "atoms-link--inherit-with-typography-as-child",
+ "atoms-link--inverted",
+ "atoms-link--target-blank-visited",
+ "atoms-link--variants",
+ "atoms-link--with-accessibility",
+ "atoms-link--with-sizes",
+ "atoms-link--with-underline",
+ "atoms-loader--custom-loader-tailwind-class-bounce-animation",
+ "atoms-loader--default",
+ "atoms-loader--default-tokens",
+ "atoms-loader--docs",
+ "atoms-loader--inline-loader-button-variant",
+ "atoms-loader--loader-names",
+ "atoms-loader--loader-rounded",
+ "atoms-loader--loader-section-variant",
+ "atoms-loader--loader-sizes",
+ "atoms-loader--loader-variants-with-wrapper-view-as-header-tag",
+ "atoms-loader--section-loader-button-variant",
+ "atoms-loader--with-accessibility",
+ "atoms-select--color-error",
+ "atoms-select--color-primary",
+ "atoms-select--color-success",
+ "atoms-select--color-warning",
+ "atoms-select--custom-item-identifier-component",
+ "atoms-select--default",
+ "atoms-select--default-tokens",
+ "atoms-select--disabled",
+ "atoms-select--docs",
+ "atoms-select--empty-items",
+ "atoms-select--multiple-select",
+ "atoms-select--searchable",
+ "atoms-select--using-ref-component-with-custom-placeholder",
+ "atoms-select--with-accessibility",
+ "atoms-select--with-adornments-and-reset-option-story",
+ "atoms-select--with-custom-initiator-and-selected-output-value",
+ "atoms-select--with-file-input-and-custom-dropdown",
+ "atoms-separator--default",
+ "atoms-separator--default-tokens",
+ "atoms-separator--docs",
+ "atoms-separator--vertical",
+ "atoms-separator--vertical-with-label",
+ "atoms-separator--with-accessibility",
+ "atoms-separator--with-custom-color",
+ "atoms-separator--with-different-variants",
+ "atoms-separator--with-label",
+ "atoms-skeleton--circular",
+ "atoms-skeleton--composed-layout",
+ "atoms-skeleton--default",
+ "atoms-skeleton--default-tokens",
+ "atoms-skeleton--docs",
+ "atoms-skeleton--rectangular",
+ "atoms-skeleton--with-accessibility",
+ "atoms-skeleton--with-children",
+ "atoms-skeleton--with-custom-animation",
+ "atoms-skeleton--with-theme-color",
+ "atoms-slider--controlled",
+ "atoms-slider--custom-styles",
+ "atoms-slider--default",
+ "atoms-slider--default-tokens",
+ "atoms-slider--disabled",
+ "atoms-slider--docs",
+ "atoms-slider--with-accessibility",
+ "atoms-slider--with-initial-value",
+ "atoms-sliderdots--default",
+ "atoms-sliderdots--default-tokens",
+ "atoms-sliderdots--docs",
+ "atoms-sliderdots--interactive",
+ "atoms-sliderdots--many-dots",
+ "atoms-switch--checked",
+ "atoms-switch--controlled",
+ "atoms-switch--default",
+ "atoms-switch--default-tokens",
+ "atoms-switch--disabled",
+ "atoms-switch--docs",
+ "atoms-switch--label-left",
+ "atoms-switch--uncontrolled",
+ "atoms-switch--with-accessibility",
+ "atoms-switch--with-loading",
+ "atoms-textarea--auto-focus",
+ "atoms-textarea--color-error",
+ "atoms-textarea--color-primary",
+ "atoms-textarea--color-success",
+ "atoms-textarea--color-warning",
+ "atoms-textarea--default",
+ "atoms-textarea--default-tokens",
+ "atoms-textarea--disabled",
+ "atoms-textarea--docs",
+ "atoms-textarea--dynamic-height",
+ "atoms-textarea--max-characters",
+ "atoms-textarea--read-only",
+ "atoms-textarea--resizable",
+ "atoms-textarea--variant-inline",
+ "atoms-textarea--with-accessibility",
+ "atoms-textarea--with-char-limit",
+ "atoms-textarea--with-controlled-state-erase-on-enter-click",
+ "atoms-toggle--default",
+ "atoms-toggle--default-tokens",
+ "atoms-toggle--disabled",
+ "atoms-toggle--docs",
+ "atoms-toggle--with-accessibility",
+ "atoms-toggle--with-custom-render",
+ "atoms-truncate--default",
+ "atoms-truncate--docs",
+ "atoms-truncate--line-truncation",
+ "atoms-truncate--ref-api-with-tooltip-on-overflow",
+ "atoms-truncate--with-accessibility",
+ "atoms-truncate--with-custom-styling",
+ "atoms-typography--as-custom-component",
+ "atoms-typography--as-custom-html-tag",
+ "atoms-typography--body",
+ "atoms-typography--combined-style-variant",
+ "atoms-typography--default",
+ "atoms-typography--default-tokens",
+ "atoms-typography--disclaimers",
+ "atoms-typography--display",
+ "atoms-typography--docs",
+ "atoms-typography--heading",
+ "atoms-typography--with-accessibility",
+ "atoms-wrapper--custom-tag-wrapper",
+ "atoms-wrapper--default-tokens",
+ "atoms-wrapper--docs",
+ "atoms-wrapper--full-page-wrapper",
+ "atoms-wrapper--inline-wrapper",
+ "atoms-wrapper--interactive-example",
+ "atoms-wrapper--section-wrapper",
+ "atoms-wrapper--with-accessibility",
+ "domain-solutions-dynamic-builder-actions--docs",
+ "domain-solutions-dynamic-builder-architecture--docs",
+ "domain-solutions-dynamic-builder-form-builder--docs",
+ "domain-solutions-dynamic-builder-how-to-use--docs",
+ "domain-solutions-dynamic-builder-overview--docs",
+ "domain-solutions-dynamic-builder-providers--docs",
+ "domain-solutions-dynamic-builder-validation--docs",
+ "domain-solutions-e-commerce--docs",
+ "form-configurator-controls--docs",
+ "form-configurator-custom-controls--docs",
+ "form-configurator-data-schema--docs",
+ "form-configurator-getting-started--docs",
+ "form-configurator-logger--docs",
+ "form-configurator-ui-schema-layouts--docs",
+ "introduction-ai-integration-a2ui-mode--docs",
+ "introduction-ai-integration-actions--docs",
+ "introduction-ai-integration-code-mode--docs",
+ "introduction-ai-integration-custom-components--docs",
+ "introduction-ai-integration-how-to-use--docs",
+ "introduction-ai-integration-overview--docs",
+ "introduction-ai-integration-renderer--docs",
+ "introduction-ai-integration-schema-and-validation--docs",
+ "introduction-atomic-design--docs",
+ "introduction-charts--docs",
+ "introduction-feedback-contribution--docs",
+ "introduction-getting-started--docs",
+ "introduction-logging-and-debugging--docs",
+ "introduction-theme-tokens-usage--docs",
+ "introduction-welcome--docs",
+ "layout-structure-chatcontainer--default",
+ "layout-structure-chatcontainer--default-tokens",
+ "layout-structure-chatcontainer--docs",
+ "layout-structure-chatcontainer--interactive-demo",
+ "layout-structure-chatcontainer--minimal-layout-with-ref-control",
+ "layout-structure-column--default",
+ "layout-structure-column--default-tokens",
+ "layout-structure-column--docs",
+ "layout-structure-flexcontainer--default",
+ "layout-structure-flexcontainer--default-tokens",
+ "layout-structure-flexcontainer--docs",
+ "layout-structure-flexcontainer--justify-start",
+ "layout-structure-portal--default",
+ "layout-structure-portal--docs",
+ "layout-structure-portal--rendering-in-target",
+ "layout-structure-portal--with-wrapper",
+ "layout-structure-portal--with-wrapper-as-tag-main",
+ "layout-structure-row--default",
+ "layout-structure-row--default-tokens",
+ "layout-structure-row--docs",
+ "layout-structure-scroll--auto-hide-scrollbars",
+ "layout-structure-scroll--auto-hide-vertical-only",
+ "layout-structure-scroll--default-tokens",
+ "layout-structure-scroll--docs",
+ "layout-structure-scroll--horizontal-scroll-only",
+ "layout-structure-scroll--interactive-example",
+ "layout-structure-scroll--vertical-scroll-only",
+ "molecules-accordion--allow-multiple-expand-with-icon",
+ "molecules-accordion--as-inline",
+ "molecules-accordion--controlled",
+ "molecules-accordion--default",
+ "molecules-accordion--default-expanded",
+ "molecules-accordion--default-tokens",
+ "molecules-accordion--docs",
+ "molecules-accordion--with-box-styles",
+ "molecules-accordion--with-on-change",
+ "molecules-accordion--without-separator",
+ "molecules-attachment-file--default",
+ "molecules-attachment-file--default-tokens",
+ "molecules-attachment-file--disabled",
+ "molecules-attachment-file--docs",
+ "molecules-attachment-file--error",
+ "molecules-attachment-file--loading",
+ "molecules-attachment-file--long-file-name",
+ "molecules-attachment-file--long-file-type",
+ "molecules-attachment-file--removable",
+ "molecules-attachment-file--without-meta",
+ "molecules-attachment-file--without-remove",
+ "molecules-breadcrumbs--default",
+ "molecules-breadcrumbs--default-tokens",
+ "molecules-breadcrumbs--docs",
+ "molecules-breadcrumbs--with-border",
+ "molecules-breadcrumbs--with-end-item-link-icon-and-after-last-item",
+ "molecules-breadcrumbs--with-icon-separator",
+ "molecules-breadcrumbs--with-start-item-link-icon",
+ "molecules-breadcrumbs--with-string-separator-and-after-last-item",
+ "molecules-counter--adjusted-max-value-5",
+ "molecules-counter--adjusted-min-2-max-value-10",
+ "molecules-counter--adjusted-min-value-3",
+ "molecules-counter--default",
+ "molecules-counter--default-tokens",
+ "molecules-counter--disabled",
+ "molecules-counter--docs",
+ "molecules-counter--with-external-counter-change-handler",
+ "molecules-dropdown--default",
+ "molecules-dropdown--default-tokens",
+ "molecules-dropdown--docs",
+ "molecules-dropdown--with-accessibility",
+ "molecules-dropdown--with-checkbox-items",
+ "molecules-dropdownitem--active",
+ "molecules-dropdownitem--default",
+ "molecules-dropdownitem--default-tokens",
+ "molecules-dropdownitem--docs",
+ "molecules-dropdownitem--interactive-with-defined-on-select",
+ "molecules-form--default-tokens",
+ "molecules-form--docs",
+ "molecules-form--with-accessibility",
+ "molecules-form--with-custom-error-handling-on-change-submit",
+ "molecules-form--with-custom-styles",
+ "molecules-form--with-on-change",
+ "molecules-form--with-submit",
+ "molecules-inline-notification--default-inline-notification",
+ "molecules-inline-notification--default-tokens",
+ "molecules-inline-notification--docs",
+ "molecules-inline-notification--error-inline-notification",
+ "molecules-inline-notification--info-inline-notification",
+ "molecules-inline-notification--success-inline-notification",
+ "molecules-inline-notification--warning-inline-notification",
+ "molecules-inline-notification--with-custom-content",
+ "molecules-list--default-tokens",
+ "molecules-list--docs",
+ "molecules-list--list-variants",
+ "molecules-list--size-variants",
+ "molecules-menu--close-on-select-false",
+ "molecules-menu--default",
+ "molecules-menu--default-tokens",
+ "molecules-menu--docs",
+ "molecules-menu--placement",
+ "molecules-menu--with-edit-and-delete-modals",
+ "molecules-menu--with-height-constraints",
+ "molecules-menu--with-offset",
+ "molecules-menu--with-positioning-options",
+ "molecules-menu--with-ref-control",
+ "molecules-price--custom-class",
+ "molecules-price--default",
+ "molecules-price--default-tokens",
+ "molecules-price--docs",
+ "molecules-price--european-convention",
+ "molecules-price--european-large-price",
+ "molecules-price--free-price",
+ "molecules-price--large-price",
+ "molecules-price--size-variants",
+ "molecules-price--with-decimals",
+ "molecules-price--with-old-price",
+ "molecules-progressbar--custom-styling",
+ "molecules-progressbar--default",
+ "molecules-progressbar--default-tokens",
+ "molecules-progressbar--docs",
+ "molecules-progressbar--full-width",
+ "molecules-progressbar--indeterminate",
+ "molecules-progressbar--interactive",
+ "molecules-progressbar--with-percentage",
+ "molecules-radiogroup--default",
+ "molecules-radiogroup--default-tokens",
+ "molecules-radiogroup--docs",
+ "molecules-radiogroup--with-colors",
+ "molecules-radiogroup--with-custom-items",
+ "molecules-radiogroup--with-grid-layout",
+ "molecules-radiogroup--with-horizontal-layout",
+ "molecules-radiogroup--with-images-and-tooltip",
+ "molecules-radiogroup--with-size-variants",
+ "molecules-radiogroup--with-vertical-layout",
+ "molecules-rating--default-tokens",
+ "molecules-rating--docs",
+ "molecules-rating--max-value-custom-10",
+ "molecules-rating--pre-selected-value",
+ "molecules-rating--read-only",
+ "molecules-rating--with-custom-on-change-control",
+ "molecules-snackbar--default",
+ "molecules-snackbar--default-tokens",
+ "molecules-snackbar--docs",
+ "molecules-snackbar--error",
+ "molecules-snackbar--example",
+ "molecules-snackbar--no-auto-dismiss",
+ "molecules-snackbar--success",
+ "molecules-snackbar--warning",
+ "molecules-snackbar--with-action-and-colored",
+ "molecules-stepper--default-tokens",
+ "molecules-stepper--docs",
+ "molecules-stepper--stepper-simple-scenario",
+ "molecules-stepper--with-active-error-step",
+ "molecules-stepper--with-custom-icons-view-step",
+ "molecules-stepper--with-icons-view-active-error-step",
+ "molecules-stepper--with-icons-view-completed-error-step",
+ "molecules-table--default",
+ "molecules-table--default-tokens",
+ "molecules-table--docs",
+ "molecules-table--empty-state",
+ "molecules-table--with-column-widths",
+ "molecules-table--with-custom-cell-renderers",
+ "molecules-table--with-custom-header",
+ "molecules-table--with-expandable-rows",
+ "molecules-table--with-loading-state",
+ "molecules-table--with-pagination",
+ "molecules-table--with-scroll-methods",
+ "molecules-table--with-sticky-footer",
+ "molecules-table--with-sticky-header",
+ "molecules-table--with-sticky-header-and-footer",
+ "molecules-table--with-sticky-header-and-pagination",
+ "molecules-table--with-virtualization",
+ "molecules-tabs--default",
+ "molecules-tabs--default-tokens",
+ "molecules-tabs--docs",
+ "molecules-tabs--with-accessibility",
+ "molecules-tabs--with-disabled-first-tab",
+ "molecules-tabs--with-notices-tab",
+ "molecules-tooltip--custom-delay",
+ "molecules-tooltip--custom-gap",
+ "molecules-tooltip--custom-position",
+ "molecules-tooltip--default",
+ "molecules-tooltip--default-tokens",
+ "molecules-tooltip--docs",
+ "molecules-tooltip--multiple-tooltips",
+ "molecules-tooltip--with-accessibility",
+ "organisms-card--default-tokens",
+ "organisms-card--docs",
+ "organisms-card--my-card-with-fallback-image",
+ "organisms-card--with-build-in-components",
+ "organisms-card--with-horizontal-card",
+ "organisms-card--with-vertical-card",
+ "organisms-card--with-wishlist-and-badge",
+ "organisms-carousel--default",
+ "organisms-carousel--default-tokens",
+ "organisms-carousel--docs",
+ "organisms-carousel--layout-horizontal",
+ "organisms-carousel--layout-vertical",
+ "organisms-carousel--responsive-behavior",
+ "organisms-carousel--with-custom-content",
+ "organisms-carousel--with-navigation-dots",
+ "organisms-carousel--without-navigation-arrows",
+ "organisms-chart--area-chart",
+ "organisms-chart--axis-config",
+ "organisms-chart--bar-chart",
+ "organisms-chart--curve-types",
+ "organisms-chart--custom-colors",
+ "organisms-chart--dashboard",
+ "organisms-chart--default",
+ "organisms-chart--default-tokens",
+ "organisms-chart--docs",
+ "organisms-chart--donut-chart",
+ "organisms-chart--empty-state",
+ "organisms-chart--error-state",
+ "organisms-chart--loading-state",
+ "organisms-chart--multi-series-with-legend",
+ "organisms-chart--per-series-colors",
+ "organisms-chart--pie-chart",
+ "organisms-chart--stacked-bar-chart",
+ "organisms-chatbubble--answer-with-actions",
+ "organisms-chatbubble--answer-with-status",
+ "organisms-chatbubble--default",
+ "organisms-chatbubble--default-tokens",
+ "organisms-chatbubble--docs",
+ "organisms-chatbubble--with-image-gallery",
+ "organisms-chatbubble--with-link-preview",
+ "organisms-chatbubble--with-sizes",
+ "organisms-contentcarousel--centered-alignment",
+ "organisms-contentcarousel--custom-scroll-step",
+ "organisms-contentcarousel--default",
+ "organisms-contentcarousel--default-tokens",
+ "organisms-contentcarousel--docs",
+ "organisms-contentcarousel--multiple-visible-items",
+ "organisms-draganddropfiles--default",
+ "organisms-draganddropfiles--default-tokens",
+ "organisms-draganddropfiles--docs",
+ "organisms-draganddropfiles--drag-and-drop-lifecycle",
+ "organisms-draganddropfiles--with-button-styles",
+ "organisms-draganddropfiles--with-custom-styles",
+ "organisms-header--complete-header",
+ "organisms-header--dark-header",
+ "organisms-header--default-tokens",
+ "organisms-header--docs",
+ "organisms-header--interactive-mobile-menu",
+ "organisms-header--minimal",
+ "organisms-header--responsive-header",
+ "organisms-header--with-actions",
+ "organisms-header--with-custom-children",
+ "organisms-header--with-menu",
+ "organisms-header--with-search",
+ "organisms-imagepreview--default",
+ "organisms-imagepreview--default-tokens",
+ "organisms-imagepreview--docs",
+ "organisms-imagepreview--no-arrows",
+ "organisms-imagepreview--single-image",
+ "organisms-imagepreview--thumbnails-left",
+ "organisms-imagepreview--with-counter",
+ "organisms-inputarea--attachment-interactive",
+ "organisms-inputarea--custom-rows",
+ "organisms-inputarea--custom-send-icon",
+ "organisms-inputarea--default",
+ "organisms-inputarea--default-tokens",
+ "organisms-inputarea--disabled",
+ "organisms-inputarea--docs",
+ "organisms-inputarea--recording-states",
+ "organisms-inputarea--voice-recording-interactive",
+ "organisms-inputarea--with-character-count",
+ "organisms-modal--custom-view",
+ "organisms-modal--default",
+ "organisms-modal--default-tokens",
+ "organisms-modal--docs",
+ "organisms-modal--no-click-outside-close",
+ "organisms-modal--no-escape-close-content-with-scroll",
+ "organisms-modal--with-accessibility",
+ "organisms-search--default",
+ "organisms-search--default-tokens",
+ "organisms-search--docs",
+ "organisms-search--with-items",
+ "organisms-search--with-loading",
+ "organisms-search--with-no-results",
+ "organisms-searchmodal--default",
+ "organisms-searchmodal--default-tokens",
+ "organisms-searchmodal--docs",
+ "organisms-searchmodal--with-search-modal",
+ "organisms-searchmodal--with-sections",
+ "organisms-sidebar--collapsed",
+ "organisms-sidebar--default",
+ "organisms-sidebar--default-tokens",
+ "organisms-sidebar--docs",
+ "organisms-sidebar--with-header-and-footer",
+ "organisms-sidebar--with-sub-items",
+ "patterns-best-practices-ai-experience--docs",
+ "patterns-best-practices-cross-device-ux-notes--docs",
+ "patterns-best-practices-form-validation-patterns--docs",
+ "patterns-best-practices-layout-composition-techniques--docs",
+ "patterns-best-practices-prompt-engineering-ai-integration--docs",
+ "patterns-best-practices-responsive-design-guide--docs",
+ "patterns-best-practices-state-handling-strategies--docs",
+ "patterns-best-practices-ux-ui-design-patterns-and-best-practices--docs",
+ "support-questions-faq--docs",
+ "templates-draganddrop--default",
+ "templates-draganddrop--default-tokens",
+ "templates-draganddrop--disabled",
+ "templates-draganddrop--docs",
+ "templates-draganddrop--error",
+ "templates-draganddrop--loading",
+ "templates-draganddrop--usage-as-wrapper",
+ "theme-tokens-animations--docs",
+ "theme-tokens-breakpoints--docs",
+ "theme-tokens-colors--docs",
+ "theme-tokens-design-tokens-overview--docs",
+ "theme-tokens-dynamic-theme-switching--docs",
+ "theme-tokens-fonts--docs",
+ "theme-tokens-radius--docs",
+ "theme-tokens-shadows--docs",
+ "theme-tokens-spacing--docs",
+ "theme-tokens-theme--docs",
+ "theme-tokens-values--docs",
+ "theme-tokens-zindex--docs"
+]
diff --git a/bin/storybook/link-validation.mjs b/bin/storybook/link-validation.mjs
new file mode 100644
index 0000000..a06ac4d
--- /dev/null
+++ b/bin/storybook/link-validation.mjs
@@ -0,0 +1,42 @@
+import { storybookOrigin } from './routes.mjs';
+
+// Only Storybook routes, not example application URLs such as /docs/api.
+const links =
+ /https?:\/\/[^\s<>"'`)\]}]+|(?:\.\/)?\?path=\/(?:docs|story)\/[^\s<>"'`)\]}]+|\/(?:docs|story)\/[a-z0-9-]+--[a-z0-9-]+|(?:\.\/|\/)?iframe\.html\?[^\s<>"'`)\]}]+/g;
+
+export function validateLinks(text, indexes) {
+ const errors = [];
+ let checked = 0;
+ for (const match of text.matchAll(links)) {
+ const href = match[0].replaceAll('&', '&');
+ let url;
+ try {
+ url = new URL(href, storybookOrigin);
+ } catch {
+ // Other tools' URL templates (e.g. localhost:${PORT}) are not links.
+ if (href.startsWith(storybookOrigin))
+ errors.push({ line: text.slice(0, match.index).split('\n').length, href, message: 'Malformed Storybook URL' });
+ continue;
+ }
+ if (url.origin !== storybookOrigin && !/^http:\/\/localhost:600[67]$/.test(url.origin)) continue;
+ const path = url.searchParams.get('path') || url.pathname;
+ const route = path.match(/^\/(docs|story)\/([^#?]+)/);
+ let id = route?.[2] || url.searchParams.get('id');
+ if (!id) continue;
+ checked++;
+ const line = text.slice(0, match.index).split('\n').length;
+ let catalog = url.pathname.startsWith('/web-components/') || url.port === '6007' ? 'webComponents' : 'react';
+ if (id.startsWith('web-components_')) {
+ catalog = 'webComponents';
+ id = id.slice('web-components_'.length);
+ }
+ const entry = indexes[catalog].entries[id];
+ if (!entry) errors.push({ line, href, message: `Unknown or stale ${catalog} story ID: ${id}` });
+ else if (route && entry.type !== route[1])
+ errors.push({ line, href, message: `Expected ${entry.type} route for ${id}` });
+ if (route && !url.searchParams.has('path')) {
+ errors.push({ line, href, message: 'Use a manager ?path= URL, not a bare /docs/ or /story/ URL' });
+ }
+ }
+ return { checked, errors };
+}
diff --git a/bin/storybook/link-validation.test.mjs b/bin/storybook/link-validation.test.mjs
new file mode 100644
index 0000000..ee73297
--- /dev/null
+++ b/bin/storybook/link-validation.test.mjs
@@ -0,0 +1,46 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { validateLinks } from './link-validation.mjs';
+
+const indexes = {
+ react: { entries: { 'react-atoms-button--docs': { type: 'docs' } } },
+ webComponents: { entries: { 'introduction-overview--overview': { type: 'story' } } },
+};
+
+test('validates canonical, composed and child deep links', () => {
+ const result = validateLinks(
+ `
+ [Button](?path=/docs/react-atoms-button--docs)
+ https://storybook.cto-rnd-system-design.griddynamics.net/web-components/?path=/story/introduction-overview--overview
+ ?path=/story/web-components_introduction-overview--overview
+ iframe.html?id=react-atoms-button--docs&viewMode=docs
+ `,
+ indexes
+ );
+ assert.equal(result.checked, 4);
+ assert.deepEqual(result.errors, []);
+});
+
+test('catches stale, nonexistent, incorrectly typed and bare routes', () => {
+ for (const href of [
+ '?path=/docs/atoms-button--docs',
+ '?path=/docs/react-nonexistent--docs',
+ '?path=/story/react-atoms-button--docs',
+ '/docs/react-atoms-button--docs',
+ ])
+ assert.ok(validateLinks(href, indexes).errors.length > 0, href);
+});
+
+test('ignores example application links, external Storybooks, and portal roots', () => {
+ assert.deepEqual(
+ validateLinks(
+ `
+ API
+ https://example.com/?path=/docs/external--docs
+ https://storybook.cto-rnd-system-design.griddynamics.net/
+ `,
+ indexes
+ ),
+ { checked: 0, errors: [] }
+ );
+});
diff --git a/bin/storybook/package-build.mjs b/bin/storybook/package-build.mjs
new file mode 100644
index 0000000..b5589de
--- /dev/null
+++ b/bin/storybook/package-build.mjs
@@ -0,0 +1,10 @@
+import { cp, access } from 'node:fs/promises';
+import { resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const root = fileURLToPath(new URL('../../', import.meta.url));
+const host = resolve(root, 'libs/ui/storybook-static');
+const child = resolve(root, 'libs/web-components/storybook-static');
+await Promise.all([access(`${host}/index.json`), access(`${child}/index.json`)]);
+await cp(child, `${host}/web-components`, { recursive: true });
+console.log(`Combined Storybook artifact: ${host}`);
diff --git a/bin/storybook/routes.d.mts b/bin/storybook/routes.d.mts
new file mode 100644
index 0000000..fde6e2b
--- /dev/null
+++ b/bin/storybook/routes.d.mts
@@ -0,0 +1,4 @@
+export const legacyReactIds: string[];
+export const storybookOrigin: string;
+export function legacyRedirect(href: string, ids: string[]): string | null;
+export function legacyRedirectHead(): string;
diff --git a/bin/storybook/routes.mjs b/bin/storybook/routes.mjs
new file mode 100644
index 0000000..b5c0176
--- /dev/null
+++ b/bin/storybook/routes.mjs
@@ -0,0 +1,40 @@
+import { readFileSync } from 'node:fs';
+
+// Frozen from the pre-migration Storybook index. Do not regenerate from the new
+// index: that would hide accidentally removed stories and break old bookmarks.
+export const legacyReactIds = JSON.parse(readFileSync(new URL('./legacy-react-ids.json', import.meta.url), 'utf8'));
+export const storybookOrigin = 'https://storybook.cto-rnd-system-design.griddynamics.net';
+
+/** Resolve legacy manager, iframe and bare /docs links without dropping URL state. */
+export function legacyRedirect(href, ids) {
+ const url = new URL(href);
+ const known = new Set(ids);
+ const upgrade = (id) => (known.has(id) ? `react-${id}` : id);
+ const original = url.href;
+ const path = url.searchParams.get('path');
+ if (path) {
+ url.searchParams.set(
+ 'path',
+ path.replace(/^(\/(?:docs|story)\/)([^#?]+)/, (_, prefix, id) => prefix + upgrade(id))
+ );
+ } else {
+ const bare = url.pathname.match(/^\/(docs|story)\/([^/]+)$/);
+ if (bare && (known.has(bare[2]) || bare[2].startsWith('react-'))) {
+ url.pathname = '/';
+ url.searchParams.set('path', `/${bare[1]}/${upgrade(bare[2])}`);
+ }
+ }
+ const id = url.searchParams.get('id');
+ if (id && known.has(id)) url.searchParams.set('id', upgrade(id));
+ // URLSearchParams may normalize encoding even when no route changed.
+ const before = new URL(original);
+ return url.pathname !== before.pathname ||
+ url.searchParams.get('path') !== before.searchParams.get('path') ||
+ url.searchParams.get('id') !== before.searchParams.get('id')
+ ? url.href
+ : null;
+}
+
+export function legacyRedirectHead() {
+ return ``;
+}
diff --git a/bin/storybook/routes.test.mjs b/bin/storybook/routes.test.mjs
new file mode 100644
index 0000000..d7d8feb
--- /dev/null
+++ b/bin/storybook/routes.test.mjs
@@ -0,0 +1,57 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { legacyReactIds, legacyRedirect, legacyRedirectHead } from './routes.mjs';
+
+const origin = 'https://storybook.example';
+
+test('every legacy story and docs route redirects to its React ID', () => {
+ for (const id of legacyReactIds) {
+ const kind = id.endsWith('--docs') ? 'docs' : 'story';
+ const result = new URL(
+ legacyRedirect(`${origin}/?path=/${kind}/${id}&args=size:sm&globals=theme:dark#example`, legacyReactIds)
+ );
+ assert.equal(result.searchParams.get('path'), `/${kind}/react-${id}`);
+ assert.equal(result.searchParams.get('args'), 'size:sm');
+ assert.equal(result.searchParams.get('globals'), 'theme:dark');
+ assert.equal(result.hash, '#example');
+ }
+});
+
+test('iframe links retain their view mode and args', () => {
+ const result = new URL(
+ legacyRedirect(`${origin}/iframe.html?id=atoms-button--docs&viewMode=docs&args=size:sm`, legacyReactIds)
+ );
+ assert.equal(result.pathname, '/iframe.html');
+ assert.equal(result.searchParams.get('id'), 'react-atoms-button--docs');
+ assert.equal(result.searchParams.get('viewMode'), 'docs');
+ assert.equal(result.searchParams.get('args'), 'size:sm');
+});
+
+test('bare docs links become manager routes', () => {
+ const result = new URL(legacyRedirect(`${origin}/docs/introduction-welcome--docs#intro`, legacyReactIds));
+ assert.equal(result.pathname, '/');
+ assert.equal(result.searchParams.get('path'), '/docs/react-introduction-welcome--docs');
+ assert.equal(result.hash, '#intro');
+});
+
+test('canonical, composed, unrelated and unknown routes are not redirected', () => {
+ for (const path of [
+ '/?path=/docs/react-introduction-welcome--docs',
+ '/?path=/story/web-components_introduction-overview--overview',
+ '/web-components/?path=/story/introduction-overview--overview',
+ '/?path=/docs/unknown--docs',
+ '/docs/api',
+ '/',
+ ])
+ assert.equal(legacyRedirect(origin + path, legacyReactIds), null);
+});
+
+test('early head script is self-contained', () => {
+ const calls = [];
+ const script = legacyRedirectHead().replace(/^`;
+ return ``;
}
diff --git a/bin/storybook/routes.test.mjs b/bin/storybook/routes.test.mjs
index d7d8feb..8213365 100644
--- a/bin/storybook/routes.test.mjs
+++ b/bin/storybook/routes.test.mjs
@@ -1,16 +1,15 @@
import assert from 'node:assert/strict';
import test from 'node:test';
-import { legacyReactIds, legacyRedirect, legacyRedirectHead } from './routes.mjs';
+import { legacyReactIds, sharedDocIds, legacyRedirect, legacyRedirectHead } from './routes.mjs';
const origin = 'https://storybook.example';
-test('every legacy story and docs route redirects to its React ID', () => {
+test('every legacy story and docs route resolves to its canonical root', () => {
for (const id of legacyReactIds) {
const kind = id.endsWith('--docs') ? 'docs' : 'story';
- const result = new URL(
- legacyRedirect(`${origin}/?path=/${kind}/${id}&args=size:sm&globals=theme:dark#example`, legacyReactIds)
- );
- assert.equal(result.searchParams.get('path'), `/${kind}/react-${id}`);
+ const href = `${origin}/?path=/${kind}/${id}&args=size:sm&globals=theme:dark#example`;
+ const result = new URL(legacyRedirect(href, legacyReactIds, sharedDocIds) || href);
+ assert.equal(result.searchParams.get('path'), `/${kind}/${sharedDocIds.includes(id) ? id : `react-${id}`}`);
assert.equal(result.searchParams.get('args'), 'size:sm');
assert.equal(result.searchParams.get('globals'), 'theme:dark');
assert.equal(result.hash, '#example');
@@ -28,22 +27,53 @@ test('iframe links retain their view mode and args', () => {
});
test('bare docs links become manager routes', () => {
- const result = new URL(legacyRedirect(`${origin}/docs/introduction-welcome--docs#intro`, legacyReactIds));
+ const result = new URL(
+ legacyRedirect(`${origin}/docs/introduction-welcome--docs#intro`, legacyReactIds, sharedDocIds)
+ );
assert.equal(result.pathname, '/');
- assert.equal(result.searchParams.get('path'), '/docs/react-introduction-welcome--docs');
+ assert.equal(result.searchParams.get('path'), '/docs/introduction-welcome--docs');
assert.equal(result.hash, '#intro');
});
test('canonical, composed, unrelated and unknown routes are not redirected', () => {
for (const path of [
- '/?path=/docs/react-introduction-welcome--docs',
+ '/?path=/docs/introduction-welcome--docs',
+ '/?path=/docs/react-introduction-getting-started--docs',
'/?path=/story/web-components_introduction-overview--overview',
'/web-components/?path=/story/introduction-overview--overview',
'/?path=/docs/unknown--docs',
'/docs/api',
'/',
])
- assert.equal(legacyRedirect(origin + path, legacyReactIds), null);
+ assert.equal(legacyRedirect(origin + path, legacyReactIds, sharedDocIds), null);
+});
+
+test('all interim React shared-doc links redirect once, including iframe and bare routes', () => {
+ for (const id of sharedDocIds) {
+ for (const path of [`/?path=/docs/react-${id}`, `/iframe.html?id=react-${id}`, `/docs/react-${id}`]) {
+ const href = `${origin}${path}&args=size:sm#section`;
+ // Bare routes have no query marker yet.
+ const input = path.startsWith('/docs/') ? href.replace('&args=', '?args=') : href;
+ const result = new URL(legacyRedirect(input, legacyReactIds, sharedDocIds));
+ assert.equal(
+ result.searchParams.get('id') || result.searchParams.get('path'),
+ path.startsWith('/iframe') ? id : `/docs/${id}`
+ );
+ assert.equal(result.searchParams.get('args'), 'size:sm');
+ assert.equal(result.hash, '#section');
+ assert.equal(legacyRedirect(result.href, legacyReactIds, sharedDocIds), null);
+ }
+ }
+});
+
+test('early head script also redirects shared pages without external dependencies', () => {
+ const calls = [];
+ const script = legacyRedirectHead().replace(/^