From d0c9c94c8870499a09260c45624d3f7cf0869e73 Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Fri, 2 Oct 2026 08:28:57 +0200 Subject: [PATCH 01/17] fix: restore composed Storybook controls --- bin/storybook/smoke-test.mjs | 14 +- package-lock.json | 682 +++++++++++++++++++++++++++++------ package.json | 18 +- 3 files changed, 584 insertions(+), 130 deletions(-) diff --git a/bin/storybook/smoke-test.mjs b/bin/storybook/smoke-test.mjs index eac2bde..6cc6aca 100644 --- a/bin/storybook/smoke-test.mjs +++ b/bin/storybook/smoke-test.mjs @@ -84,7 +84,7 @@ try { await page.waitForURL((url) => url.searchParams.get('path') === '/docs/react-introduction-theme-tokens-usage--docs'); await page.goto(`${base}/?path=/docs/react-introduction-ai-integration-overview--docs`); - await docs().getByRole('heading', { name: 'AI Integration', exact: true }).waitFor(); + await docs().getByText('AI Integration', { exact: true }).first().waitFor(); await page.goto(`${base}/?path=/docs/react-theme-tokens-colors--docs`); await page.waitForURL((url) => url.searchParams.get('path') === '/docs/theme-tokens-colors--docs'); @@ -93,15 +93,23 @@ try { .waitFor(); await page.goto(`${base}/?path=/docs/patterns-best-practices-cross-device-ux-notes--docs`); - await docs().getByRole('heading', { name: 'Cross-Device UX Notes', exact: true }).waitFor(); + await docs().getByText('Cross-Device UX Notes', { exact: true }).first().waitFor(); // Composition must load the child's own native renderer, not a React wrapper. await page.goto(`${base}/?path=/story/web-components_introduction-overview--overview`); const nativeFrame = page.frameLocator('#storybook-ref-web-components'); await nativeFrame.getByRole('heading', { name: 'Using Web Components', exact: true }).waitFor(); - await page.goto(`${base}/?path=/story/web-components_atoms-button--default`); + await page.goto(`${base}/?path=/story/web-components_atoms-button--default&panel=addon-controls`); await nativeFrame.getByRole('button', { name: 'Button', exact: true }).waitFor(); + await page.getByText('variant', { exact: true }).first().waitFor(); + await page.getByRole('combobox', { name: 'variant', exact: true }).selectOption('secondary'); + await page.waitForFunction( + () => + document.querySelector('#storybook-ref-web-components')?.contentDocument?.querySelector('gd-button')?.variant === + 'secondary' + ); + assert.equal(await nativeFrame.locator('gd-button').evaluate((element) => element.variant), 'secondary'); await page.goto(`${base}/?path=/docs/web-components_atoms-button--docs`); await nativeFrame.getByRole('heading', { name: 'Button', exact: true }).first().waitFor(); diff --git a/package-lock.json b/package-lock.json index bf6da97..61e6911 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,14 +54,14 @@ "@rolldown/plugin-babel": "^0.2.0", "@rollup/plugin-url": "^8.0.2", "@size-limit/preset-small-lib": "^12.1.0", - "@storybook/addon-a11y": "^10.3.5", - "@storybook/addon-docs": "^10.3.5", - "@storybook/addon-onboarding": "^10.3.5", - "@storybook/addon-vitest": "^10.3.5", - "@storybook/builder-vite": "^10.3.5", + "@storybook/addon-a11y": "^10.6.1", + "@storybook/addon-docs": "^10.6.1", + "@storybook/addon-onboarding": "^10.6.1", + "@storybook/addon-vitest": "^10.6.1", + "@storybook/builder-vite": "^10.6.1", "@storybook/jest": "^0.2.3", - "@storybook/react-vite": "^10.3.5", - "@storybook/web-components-vite": "10.4.6", + "@storybook/react-vite": "^10.6.1", + "@storybook/web-components-vite": "^10.6.1", "@svgr/rollup": "^8.1.0", "@swc-node/register": "^1.11.1", "@swc/cli": "^0.8.1", @@ -97,7 +97,7 @@ "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "5.0.0", "eslint-plugin-react-refresh": "^0.4.16", - "eslint-plugin-storybook": "^10.3.5", + "eslint-plugin-storybook": "^10.6.1", "globals": "^15.14.0", "husky": "^9.1.7", "inquirer": "^8.2.6", @@ -120,7 +120,7 @@ "rollup-preserve-directives": "^1.1.3", "sass": "^1.55.0", "size-limit": "^12.1.0", - "storybook": "^10.3.5", + "storybook": "^10.6.1", "ts-jest": "^29.1.0", "ts-node": "^10.9.2", "tslib": "^2.3.0", @@ -9325,7 +9325,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -17476,7 +17478,9 @@ "license": "MIT" }, "node_modules/@storybook/addon-a11y": { - "version": "10.4.6", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.6.1.tgz", + "integrity": "sha512-73wx4xyOMBbFIF4DnlfHy5o/c5sWjawn/tb+zq/GAKXkYCuBziE5aa3WDzRDhdxN259p5kTDyJcRi1SlUEUexg==", "dev": true, "license": "MIT", "dependencies": { @@ -17488,21 +17492,23 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.4.6" + "storybook": "^10.6.1" } }, "node_modules/@storybook/addon-docs": { - "version": "10.4.6", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.6.1.tgz", + "integrity": "sha512-31PXR+MCWfI5YHKKah6tWnbV1FJWA9qbTHxtvpszPLd8FBekXtPOnmyYOpFF9CfciBF9bkeJFdFanJvUCjWbjA==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.4.6", "@storybook/icons": "^2.0.2", - "@storybook/react-dom-shim": "10.4.6", + "@storybook/react-dom-shim": "10.6.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "ts-dedent": "^2.0.0" + "ts-dedent": "^2.0.0", + "unplugin": "^2.3.5" }, "funding": { "type": "opencollective", @@ -17510,7 +17516,7 @@ }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.4.6" + "storybook": "^10.6.1" }, "peerDependenciesMeta": { "@types/react": { @@ -17534,32 +17540,10 @@ "react": ">=16" } }, - "node_modules/@storybook/addon-docs/node_modules/react": { - "version": "19.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@storybook/addon-docs/node_modules/react-dom": { - "version": "19.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.7" - } - }, - "node_modules/@storybook/addon-docs/node_modules/scheduler": { - "version": "0.27.0", - "dev": true, - "license": "MIT" - }, "node_modules/@storybook/addon-onboarding": { - "version": "10.4.6", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-10.6.1.tgz", + "integrity": "sha512-DLozSQWVELz39FSlBvwM420+UrveK7Q0n2ah1TWgdIUGMVhWt+ZHw2oKcjPO3xSn2N8V08G9F6JnhBrvms2Zvw==", "dev": true, "license": "MIT", "funding": { @@ -17567,11 +17551,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.4.6" + "storybook": "^10.6.1" } }, "node_modules/@storybook/addon-vitest": { - "version": "10.4.6", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.6.1.tgz", + "integrity": "sha512-ZGB7kDd9o0UiXIIXLZMKK9ruNyqo2jEBT+2YjlpKGDtJR6iAEhfWyiPY8JkobCV5Fge0oBqyg1x9am4wv/vp5w==", "dev": true, "license": "MIT", "dependencies": { @@ -17583,11 +17569,11 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "@vitest/browser": "^3.0.0 || ^4.0.0", - "@vitest/browser-playwright": "^4.0.0", - "@vitest/runner": "^3.0.0 || ^4.0.0", - "storybook": "^10.4.6", - "vitest": "^3.0.0 || ^4.0.0" + "@vitest/browser": "^3.0.0 || ^4.0.0 || ^5.0.0", + "@vitest/browser-playwright": "^4.0.0 || ^5.0.0", + "@vitest/runner": "^3.0.0 || ^4.0.0 || ^5.0.0", + "storybook": "^10.6.1", + "vitest": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -17605,11 +17591,12 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "10.4.6", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.6.1.tgz", + "integrity": "sha512-kUMRRRS4MpJdxOLhrlQX3cpMYi2vVB3tIIRXyqWmAjVM6xT7OZ1SCQEMFPP+S3ToRqaYWZQb8aBqrJucbbYufA==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "10.4.6", "ts-dedent": "^2.0.0" }, "funding": { @@ -17617,43 +17604,10 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.4.6", + "storybook": "^10.6.1", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@storybook/csf-plugin": { - "version": "10.4.6", - "dev": true, - "license": "MIT", - "dependencies": { - "unplugin": "^2.3.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "esbuild": "*", - "rollup": "*", - "storybook": "^10.4.6", - "vite": "*", - "webpack": "*" - }, - "peerDependenciesMeta": { - "esbuild": { - "optional": true - }, - "rollup": { - "optional": true - }, - "vite": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/@storybook/expect": { "version": "28.1.3-5", "resolved": "https://registry.npmjs.org/@storybook/expect/-/expect-28.1.3-5.tgz", @@ -17937,12 +17891,14 @@ "license": "MIT" }, "node_modules/@storybook/react": { - "version": "10.4.6", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.6.1.tgz", + "integrity": "sha512-t0QIzO0MVcpD52UEMZKSv35nO/+8vUnLGoFC1E3BIVjHEIdr6IFFM7ty2E7FWsaZGsah3XrFINKT3dPd2Atz+Q==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.4.6", + "@storybook/react-dom-shim": "10.6.1", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, @@ -17955,7 +17911,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.4.6", + "storybook": "^10.6.1", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -17971,7 +17927,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "10.4.6", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.6.1.tgz", + "integrity": "sha512-OkPFycR6kVE+9ZsgQWRqnUfZm/sLY6dxsNAn3lsvV7DSAQWH9QnHLkqP9E/N/Hg6cKPDtGzDZ8vPNDz2FuaDuQ==", "dev": true, "license": "MIT", "funding": { @@ -17983,7 +17941,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.4.6" + "storybook": "^10.6.1" }, "peerDependenciesMeta": { "@types/react": { @@ -17995,17 +17953,19 @@ } }, "node_modules/@storybook/react-vite": { - "version": "10.4.6", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.6.1.tgz", + "integrity": "sha512-oyUZxuW2lIJN8BRbYyMyZbfjeYqtF6HwmNfqa1SAF525zRnMewYp66wm/5d8FAgwqejOf6N2+wR3hX866lSwMA==", "dev": true, "license": "MIT", "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "10.4.6", - "@storybook/react": "10.4.6", + "@storybook/builder-vite": "10.6.1", + "@storybook/react": "10.6.1", "empathic": "^2.0.0", - "magic-string": "^0.30.0", - "react-docgen": "^8.0.0", + "magic-string": "^1.1.0", + "react-docgen": "^8.0.2", "resolve": "^1.22.8", "tsconfig-paths": "^4.2.0" }, @@ -18016,14 +17976,30 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.4.6", + "storybook": "^10.6.1", + "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-vite/node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/@storybook/web-components": { - "version": "10.4.6", - "resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.4.6.tgz", - "integrity": "sha512-/of77LBQU7KAvj62Ff6Po1tVEi17kubqGZeezNf4YApSvfabfZDcZpnFOqKinC3v1zfF2mnCumTBu8MIzomx+A==", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-10.6.1.tgz", + "integrity": "sha512-gb/WFRfdGpQ/vzkdkivBcC6GghWBM58QqVFnF6Ky3t0T0Nx3ydYdJSbDJ1F52pV8Ik9PI/6kNgchUsC5yhy2Kw==", "dev": true, "license": "MIT", "dependencies": { @@ -18037,25 +18013,25 @@ }, "peerDependencies": { "lit": "^2.0.0 || ^3.0.0", - "storybook": "^10.4.6" + "storybook": "^10.6.1" } }, "node_modules/@storybook/web-components-vite": { - "version": "10.4.6", - "resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.4.6.tgz", - "integrity": "sha512-rho4tYXVIUDtoiM7+F89dCgsSHNxbl2si/e5DujGxC+EMzQHzOYfe0yKjrRPSWNAw2IKGUW9u06cEFPnpKYcTw==", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/@storybook/web-components-vite/-/web-components-vite-10.6.1.tgz", + "integrity": "sha512-JMU5ecEgbMBYwH2GJOpH22Mv+J6gNtnk4nOgAWvZAV1g5Il6KATQBZkToGKbi80Xug1gvmV37P29mD8VNT9KWQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-vite": "10.4.6", - "@storybook/web-components": "10.4.6" + "@storybook/builder-vite": "10.6.1", + "@storybook/web-components": "10.6.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.4.6", + "storybook": "^10.6.1", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, @@ -18855,7 +18831,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.1", + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", "dev": true, "license": "MIT", "engines": { @@ -19151,6 +19129,8 @@ }, "node_modules/@types/doctrine": { "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", "dev": true, "license": "MIT" }, @@ -19354,6 +19334,8 @@ }, "node_modules/@types/resolve": { "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", "dev": true, "license": "MIT" }, @@ -27492,15 +27474,17 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "10.4.6", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.6.1.tgz", + "integrity": "sha512-XBmXuQcI0wzxMYc0rWKIiydvUxgS+GzLesO1YK1adnj97LhMqqsnlalbM+nXfQo1JoH2F2CI12sAH9/dAC99Dw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.48.0" + "@typescript-eslint/types": "^8.60.0", + "@typescript-eslint/utils": "^8.60.0" }, "peerDependencies": { - "eslint": ">=8", - "storybook": "^10.4.6" + "eslint": ">=8" } }, "node_modules/eslint-scope": { @@ -38960,7 +38944,9 @@ } }, "node_modules/react-docgen": { - "version": "8.0.3", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.4.tgz", + "integrity": "sha512-gIYjtVTU4NCWZnb/LXVSWfClZXnjzX3+xxi69Tkfu4YsmYuqiOk8giLkKt/WwxWqh+U66CTh8Kqbha1uPv8/qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -38989,6 +38975,8 @@ }, "node_modules/react-docgen/node_modules/doctrine": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -39000,6 +38988,8 @@ }, "node_modules/react-docgen/node_modules/strip-indent": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", "dev": true, "license": "MIT", "engines": { @@ -41158,25 +41148,29 @@ } }, "node_modules/storybook": { - "version": "10.4.6", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.6.1.tgz", + "integrity": "sha512-NeEmukMWPA5OqHizUkQtxCIhGt7MUZLPY4FFgpC0LNiipjrac/oKpH0TxI+9Zc+vI5+IoN+NiskOmBP7gtYfmA==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/user-event": "^14.6.1", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/user-event": "^14.6.3", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", + "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", - "oxc-resolver": "^11.19.1", + "oxc-resolver": "11.21.2", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", - "ws": "^8.18.0" + "ws": "^8.21.1" }, "bin": { "storybook": "dist/bin/dispatcher.js" @@ -41188,7 +41182,7 @@ "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", - "vite-plus": "^0.1.15" + "vite-plus": "^0.1.15 || ^0.2.0" }, "peerDependenciesMeta": { "@types/react": { @@ -41202,6 +41196,353 @@ } } }, + "node_modules/storybook/node_modules/@emnapi/core": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/storybook/node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/storybook/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/storybook/node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", + "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.2.tgz", + "integrity": "sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.2.tgz", + "integrity": "sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.2.tgz", + "integrity": "sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.2.tgz", + "integrity": "sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.2.tgz", + "integrity": "sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.2.tgz", + "integrity": "sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.2.tgz", + "integrity": "sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.2.tgz", + "integrity": "sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.2.tgz", + "integrity": "sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.2.tgz", + "integrity": "sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.2.tgz", + "integrity": "sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.2.tgz", + "integrity": "sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.2.tgz", + "integrity": "sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.2.tgz", + "integrity": "sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.2.tgz", + "integrity": "sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.2.tgz", + "integrity": "sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.2.tgz", + "integrity": "sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.2.tgz", + "integrity": "sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/storybook/node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz", + "integrity": "sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/storybook/node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/storybook/node_modules/@vitest/spy": { "version": "3.2.4", "dev": true, @@ -41213,6 +41554,29 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/storybook/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/storybook/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/storybook/node_modules/define-lazy-prop": { "version": "3.0.0", "dev": true, @@ -41224,6 +41588,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/storybook/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/storybook/node_modules/open": { "version": "10.2.0", "dev": true, @@ -41241,6 +41612,81 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/storybook/node_modules/oxc-resolver": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.2.tgz", + "integrity": "sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.21.2", + "@oxc-resolver/binding-android-arm64": "11.21.2", + "@oxc-resolver/binding-darwin-arm64": "11.21.2", + "@oxc-resolver/binding-darwin-x64": "11.21.2", + "@oxc-resolver/binding-freebsd-x64": "11.21.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.21.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.21.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.21.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-x64-musl": "11.21.2", + "@oxc-resolver/binding-openharmony-arm64": "11.21.2", + "@oxc-resolver/binding-wasm32-wasi": "11.21.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.21.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.21.2" + } + }, + "node_modules/storybook/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/storybook/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/storybook/node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/stream-buffers": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", diff --git a/package.json b/package.json index 3375f75..97f37d0 100644 --- a/package.json +++ b/package.json @@ -113,14 +113,14 @@ "@rolldown/plugin-babel": "^0.2.0", "@rollup/plugin-url": "^8.0.2", "@size-limit/preset-small-lib": "^12.1.0", - "@storybook/addon-a11y": "^10.3.5", - "@storybook/addon-docs": "^10.3.5", - "@storybook/addon-onboarding": "^10.3.5", - "@storybook/addon-vitest": "^10.3.5", - "@storybook/builder-vite": "^10.3.5", + "@storybook/addon-a11y": "^10.6.1", + "@storybook/addon-docs": "^10.6.1", + "@storybook/addon-onboarding": "^10.6.1", + "@storybook/addon-vitest": "^10.6.1", + "@storybook/builder-vite": "^10.6.1", "@storybook/jest": "^0.2.3", - "@storybook/react-vite": "^10.3.5", - "@storybook/web-components-vite": "10.4.6", + "@storybook/react-vite": "^10.6.1", + "@storybook/web-components-vite": "^10.6.1", "@svgr/rollup": "^8.1.0", "@swc-node/register": "^1.11.1", "@swc/cli": "^0.8.1", @@ -156,7 +156,7 @@ "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "5.0.0", "eslint-plugin-react-refresh": "^0.4.16", - "eslint-plugin-storybook": "^10.3.5", + "eslint-plugin-storybook": "^10.6.1", "globals": "^15.14.0", "husky": "^9.1.7", "inquirer": "^8.2.6", @@ -179,7 +179,7 @@ "rollup-preserve-directives": "^1.1.3", "sass": "^1.55.0", "size-limit": "^12.1.0", - "storybook": "^10.3.5", + "storybook": "^10.6.1", "ts-jest": "^29.1.0", "ts-node": "^10.9.2", "tslib": "^2.3.0", From 0b9663c19ccbd4aecaa7b8bab493d6a94f9cbf5b Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:07:32 +0200 Subject: [PATCH 02/17] fix(web-components): preserve Storybook control sections --- .../scripts/check-ported-elements.mjs | 104 +++++++++++++++++- libs/web-components/stories/Avatar.stories.ts | 6 +- libs/web-components/stories/Badge.stories.ts | 6 +- libs/web-components/stories/Box.stories.ts | 6 +- libs/web-components/stories/Button.stories.ts | 6 +- .../stories/Checkbox.stories.ts | 4 +- libs/web-components/stories/Icon.stories.ts | 6 +- libs/web-components/stories/Image.stories.ts | 6 +- libs/web-components/stories/Input.stories.ts | 7 +- .../stories/InputFile.stories.ts | 6 +- libs/web-components/stories/Label.stories.ts | 6 +- libs/web-components/stories/Link.stories.ts | 6 +- libs/web-components/stories/Loader.stories.ts | 6 +- libs/web-components/stories/Menu.stories.ts | 4 +- .../stories/SliderDots.stories.ts | 6 +- libs/web-components/stories/Switch.stories.ts | 6 +- .../stories/Textarea.stories.ts | 6 +- .../stories/Truncate.stories.ts | 6 +- .../stories/Typography.stories.ts | 6 +- libs/web-components/stories/helpers.ts | 100 +++++++++++++++++ 20 files changed, 257 insertions(+), 52 deletions(-) diff --git a/libs/web-components/scripts/check-ported-elements.mjs b/libs/web-components/scripts/check-ported-elements.mjs index 62df637..3f80e0f 100644 --- a/libs/web-components/scripts/check-ported-elements.mjs +++ b/libs/web-components/scripts/check-ported-elements.mjs @@ -1,10 +1,93 @@ #!/usr/bin/env node import { readdir, readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; +import ts from 'typescript'; const packageRoot = resolve(import.meta.dirname, '..'); const componentsRoot = resolve(packageRoot, 'src/components'); const repoRoot = resolve(packageRoot, '../..'); + +const propertyName = (node) => + ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) ? node.text : undefined; +const unwrapExpression = (node) => (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) ? node.expression : node); + +function objectProperty(object, name) { + return object?.properties.find( + (property) => ts.isPropertyAssignment(property) && propertyName(property.name) === name + )?.initializer; +} + +function findObject(sourceFile, name) { + let result; + const visit = (node) => { + if ( + !result && + ts.isVariableDeclaration(node) && + propertyName(node.name) === name && + ts.isObjectLiteralExpression(unwrapExpression(node.initializer)) + ) + result = unwrapExpression(node.initializer); + ts.forEachChild(node, visit); + }; + visit(sourceFile); + return result; +} + +function storyArgTypes(source, fileName) { + const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true); + let result; + const visit = (node) => { + if (!result && ts.isPropertyAssignment(node) && propertyName(node.name) === 'argTypes') { + if (ts.isObjectLiteralExpression(node.initializer)) result = node.initializer; + if ( + ts.isCallExpression(node.initializer) && + node.initializer.arguments[1] && + ts.isObjectLiteralExpression(node.initializer.arguments[1]) + ) + result = node.initializer.arguments[1]; + } + ts.forEachChild(node, visit); + }; + visit(sourceFile); + return new Map( + (result?.properties ?? []).filter(ts.isPropertyAssignment).map((property) => { + const table = ts.isObjectLiteralExpression(property.initializer) + ? objectProperty(property.initializer, 'table') + : undefined; + const category = table && ts.isObjectLiteralExpression(table) ? objectProperty(table, 'category') : undefined; + const subcategory = + table && ts.isObjectLiteralExpression(table) ? objectProperty(table, 'subcategory') : undefined; + return [ + propertyName(property.name), + { + category: category && ts.isStringLiteral(category) ? category.text : undefined, + subcategory: subcategory && ts.isStringLiteral(subcategory) ? subcategory.text : undefined, + }, + ]; + }) + ); +} + +function sectionMap(source) { + const sourceFile = ts.createSourceFile('helpers.ts', source, ts.ScriptTarget.Latest, true); + const sections = findObject(sourceFile, 'reactArgTypeSections'); + return new Map( + (sections?.properties ?? []).filter(ts.isPropertyAssignment).map((component) => { + const controls = ts.isObjectLiteralExpression(component.initializer) ? component.initializer.properties : []; + return [ + propertyName(component.name), + new Map( + controls + .filter(ts.isPropertyAssignment) + .map((control) => [ + propertyName(control.name), + ts.isStringLiteral(control.initializer) ? control.initializer.text : undefined, + ]) + ), + ]; + }) + ); +} const fullStoryParity = new Set([ 'gd-avatar', 'gd-box', @@ -84,14 +167,16 @@ if (missingFromManifest.length || missingFromSource.length) { ); } -const [index, ssr, parityText, publishedAuditText] = await Promise.all([ +const [index, ssr, parityText, publishedAuditText, storyHelpers] = await Promise.all([ readFile(resolve(packageRoot, 'src/index.ts'), 'utf8'), readFile(resolve(packageRoot, 'scripts/ssr-dsd-render.ts'), 'utf8'), readFile(resolve(packageRoot, 'react-parity.json'), 'utf8'), readFile(resolve(packageRoot, 'published-atoms-audit.json'), 'utf8'), + readFile(resolve(packageRoot, 'stories/helpers.ts'), 'utf8'), ]); const parity = JSON.parse(parityText); const publishedAudit = JSON.parse(publishedAuditText); +const sections = sectionMap(storyHelpers); const atomTags = ports.filter(({ category }) => category === 'atoms').map(({ tag }) => tag); const publishedAtomTags = Object.keys(publishedAudit.components); const missingPublishedAtoms = publishedAtomTags.filter((tag) => !atomTags.includes(tag)); @@ -128,6 +213,23 @@ for (const { tag, category } of ports) { resolve(repoRoot, 'libs/ui/src/components', category, contract.react, `${contract.react}.stories.tsx`), 'utf8' ); + const reactControls = storyArgTypes(reactStories, `${contract.react}.stories.tsx`); + const targetControls = storyArgTypes(stories, `${storyName}.stories.ts`); + const targetSections = sections.get(storyName) ?? new Map(); + for (const control of targetControls.keys()) { + const sourceSection = reactControls.get(control)?.category; + const targetSection = targetSections.get(control); + if (sourceSection !== targetSection) { + throw new Error( + `${tag} Storybook section mismatch for ${control}: React=${sourceSection ?? 'unsectioned'}, Web Component=${ + targetSection ?? 'unsectioned' + }.` + ); + } + } + for (const control of targetSections.keys()) { + if (!targetControls.has(control)) throw new Error(`${tag} has a stale Storybook section for ${control}.`); + } const exported = [...reactStories.matchAll(/^export const (\w+)/gm)].map((match) => match[1]); const missing = exported.filter((story) => !contract.stories.includes(story)); const stale = contract.stories.filter((story) => !exported.includes(story)); diff --git a/libs/web-components/stories/Avatar.stories.ts b/libs/web-components/stories/Avatar.stories.ts index ab7fee1..4ad2403 100644 --- a/libs/web-components/stories/Avatar.stories.ts +++ b/libs/web-components/stories/Avatar.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdAvatar } from '../src'; -import { defaultTokenViewer, element, portrait } from './helpers'; +import { defaultTokenViewer, element, portrait, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -23,11 +23,11 @@ const meta = { badgeColor: 'bg.fill.success.primary.default', backgroundColor: undefined, }, - argTypes: { + argTypes: sectionedArgTypes('Avatar', { size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] }, badgeColor: { control: 'color' }, backgroundColor: { control: 'color' }, - }, + }), render: (args: Args) => element('gd-avatar', args), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Badge.stories.ts b/libs/web-components/stories/Badge.stories.ts index a97549d..1c553e3 100644 --- a/libs/web-components/stories/Badge.stories.ts +++ b/libs/web-components/stories/Badge.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdBadge } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -15,7 +15,7 @@ const meta = { }, }, args: { variant: 'primary', appearance: 'filled', size: 'md', disabled: false }, - argTypes: { + argTypes: sectionedArgTypes('Badge', { variant: { description: 'Visual style variant of the badge', control: 'select', @@ -28,7 +28,7 @@ const meta = { }, size: { description: 'Badge size', control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, disabled: { description: 'Whether the badge uses disabled styling', control: 'boolean' }, - }, + }), render: (args: Args) => element('gd-badge', args, 'Badge'), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Box.stories.ts b/libs/web-components/stories/Box.stories.ts index fa1d80c..5d81659 100644 --- a/libs/web-components/stories/Box.stories.ts +++ b/libs/web-components/stories/Box.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdBox } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -15,12 +15,12 @@ const meta = { }, }, args: { variant: 'vertical', isBordered: false, isHighlighted: false, withShadowHover: false }, - argTypes: { + argTypes: sectionedArgTypes('Box', { variant: { description: 'Box orientation variant', control: 'select', options: ['vertical', 'horizontal'] }, isBordered: { description: 'Adds a border to the box', control: 'boolean' }, isHighlighted: { description: 'Adds the React hover outline treatment', control: 'boolean' }, withShadowHover: { description: 'Adds the React elevation shadow on hover', control: 'boolean' }, - }, + }), render: (args: Args) => element('gd-box', args, 'Box content'), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Button.stories.ts b/libs/web-components/stories/Button.stories.ts index 9f94f2b..4f57de5 100644 --- a/libs/web-components/stories/Button.stories.ts +++ b/libs/web-components/stories/Button.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdButton } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick< GdButton, @@ -43,10 +43,10 @@ const meta = { ariaPressed: null, label: 'Button', }, - argTypes: { + argTypes: sectionedArgTypes('Button', { variant: { control: 'select', options: variants }, rounded: { control: 'select', options: roundedOptions }, - }, + }), render: ({ label, ...props }: Args) => element('gd-button', props, label), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Checkbox.stories.ts b/libs/web-components/stories/Checkbox.stories.ts index dfd56cb..a0c1a07 100644 --- a/libs/web-components/stories/Checkbox.stories.ts +++ b/libs/web-components/stories/Checkbox.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdCheckbox } from '../src'; -import { defaultTokenViewer, element, observed } from './helpers'; +import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -15,7 +15,7 @@ const meta = { }, }, args: { checked: false, disabled: false, indeterminate: false, size: 'md', name: '', value: 'on', required: false }, - argTypes: { size: { control: 'select', options: ['sm', 'md'] } }, + argTypes: sectionedArgTypes('Checkbox', { size: { control: 'select', options: ['sm', 'md'] } }), render: (args: Args) => { const checkbox = element('gd-checkbox', args, 'Accept terms'); checkbox.addEventListener('gd-change', (event) => { diff --git a/libs/web-components/stories/Icon.stories.ts b/libs/web-components/stories/Icon.stories.ts index 5a68ea6..985af98 100644 --- a/libs/web-components/stories/Icon.stories.ts +++ b/libs/web-components/stories/Icon.stories.ts @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { iconCatalog, type GridKitIconName } from 'gd-design-core'; import type { GdIcon } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick; const names = Object.keys(iconCatalog) as GridKitIconName[]; @@ -17,7 +17,7 @@ const meta = { }, }, args: { name: 'star', size: 'md', fill: 'currentColor' }, - argTypes: { + argTypes: sectionedArgTypes('Icon', { name: { description: 'Icon name from the shared React/Web Components catalog', control: 'select', options: names }, size: { description: 'Theme size applied to width and height', @@ -25,7 +25,7 @@ const meta = { options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'], }, fill: { description: 'Path fill or theme color token', control: 'color' }, - }, + }), render: (args: Args) => element('gd-icon', args), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Image.stories.ts b/libs/web-components/stories/Image.stories.ts index 08fef8f..ed8704a 100644 --- a/libs/web-components/stories/Image.stories.ts +++ b/libs/web-components/stories/Image.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdImage } from '../src'; -import { defaultTokenViewer, element, portrait } from './helpers'; +import { defaultTokenViewer, element, portrait, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -15,7 +15,7 @@ const meta = { }, }, args: { src: portrait, alt: 'Portrait', width: 160, height: 160, caption: '', objectFit: 'cover' }, - argTypes: { + argTypes: sectionedArgTypes('Image', { src: { description: 'Image source URL', control: 'text' }, alt: { description: 'Accessible image alternative text', control: 'text' }, width: { description: 'Rendered image width in pixels', control: 'number' }, @@ -26,7 +26,7 @@ const meta = { control: 'select', options: ['cover', 'contain', 'fill', 'none', 'scale-down'], }, - }, + }), render: (args: Args) => element('gd-image', args), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Input.stories.ts b/libs/web-components/stories/Input.stories.ts index 600694b..c41241c 100644 --- a/libs/web-components/stories/Input.stories.ts +++ b/libs/web-components/stories/Input.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdInput } from '../src'; -import { defaultTokenViewer, element, observed } from './helpers'; +import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; type Args = Pick< GdInput, @@ -64,7 +64,10 @@ const meta = { tabIndex: 0, debounceCallbackTime: undefined, }, - argTypes: { variant: { control: 'select', options: inputVariants }, color: { control: 'select', options: colors } }, + argTypes: sectionedArgTypes('Input', { + variant: { control: 'select', options: inputVariants }, + color: { control: 'select', options: colors }, + }), render: (args: Args) => { const input = element('gd-input', args); input.addEventListener('gd-input', (event) => { diff --git a/libs/web-components/stories/InputFile.stories.ts b/libs/web-components/stories/InputFile.stories.ts index 12bd498..f1a8273 100644 --- a/libs/web-components/stories/InputFile.stories.ts +++ b/libs/web-components/stories/InputFile.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdInputFile } from '../src'; -import { defaultTokenViewer, element, observed } from './helpers'; +import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -15,7 +15,7 @@ const meta = { }, }, args: { accept: '', capture: undefined, multiple: false, disabled: false, isIcon: false, buttonVariant: 'outlined' }, - argTypes: { + argTypes: sectionedArgTypes('InputFile', { accept: { description: 'Accepted file MIME types or extensions', control: 'text' }, capture: { description: 'Preferred camera capture source', @@ -30,7 +30,7 @@ const meta = { control: 'select', options: ['primary', 'secondary', 'outlined'], }, - }, + }), render: (args: Args) => observed(element('gd-input-file', args), 'gd-change', { files: [] }), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Label.stories.ts b/libs/web-components/stories/Label.stories.ts index bef2f96..cd4320b 100644 --- a/libs/web-components/stories/Label.stories.ts +++ b/libs/web-components/stories/Label.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdLabel } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -15,10 +15,10 @@ const meta = { }, }, args: { htmlFor: 'example' }, - argTypes: { + argTypes: sectionedArgTypes('Label', { htmlFor: { description: 'ID of the associated form control', control: 'text' }, styles: { description: 'Custom inline style overrides', control: 'object' }, - }, + }), render: (args: Args) => element('gd-label', args, 'Field label'), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Link.stories.ts b/libs/web-components/stories/Link.stories.ts index 127bde0..1f300c3 100644 --- a/libs/web-components/stories/Link.stories.ts +++ b/libs/web-components/stories/Link.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdLink } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick< GdLink, @@ -26,7 +26,7 @@ const meta = { target: undefined, rel: '', }, - argTypes: { + argTypes: sectionedArgTypes('Link', { variant: { description: 'Design variant', control: 'select', @@ -41,7 +41,7 @@ const meta = { target: { description: 'Browsing context target', control: 'text' }, rel: { description: 'Anchor relationship attribute', control: 'text' }, styles: { description: 'Custom inline style overrides', control: 'object' }, - }, + }), render: (args: Args) => element('gd-link', args, 'GridKit link'), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Loader.stories.ts b/libs/web-components/stories/Loader.stories.ts index 683b4b2..eb70dd8 100644 --- a/libs/web-components/stories/Loader.stories.ts +++ b/libs/web-components/stories/Loader.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdLoader } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -15,7 +15,7 @@ const meta = { }, }, args: { name: 'circle', variant: 'inline', size: 'md', rounded: 'none', withWrapper: true }, - argTypes: { + argTypes: sectionedArgTypes('Loader', { name: { control: 'select', options: ['circle', 'dots'] }, variant: { control: 'select', options: ['inline', 'section', 'fullPage'] }, size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, @@ -27,7 +27,7 @@ const meta = { withWrapper: { description: 'Whether the positioning wrapper is rendered', control: 'boolean' }, animationProps: { description: 'CSS animation timing, easing, and iteration values', control: 'text' }, styles: { description: 'Custom loader style overrides', control: 'object' }, - }, + }), render: (args: Args) => element('gd-loader', args), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Menu.stories.ts b/libs/web-components/stories/Menu.stories.ts index 7d6b6cc..a69778c 100644 --- a/libs/web-components/stories/Menu.stories.ts +++ b/libs/web-components/stories/Menu.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdMenu } from '../src'; -import { defaultTokenViewer, element, observed } from './helpers'; +import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; type Args = Pick; const placements = ['top-left', 'top-right', 'bottom-left', 'bottom-right'] as const; @@ -24,7 +24,7 @@ const meta = { minHeight: 80, maxHeight: 400, }, - argTypes: { placement: { control: 'select', options: placements } }, + argTypes: sectionedArgTypes('Menu', { placement: { control: 'select', options: placements } }), render: (args: Args) => { const menu = element('gd-menu', args); const trigger = document.createElement('span'); diff --git a/libs/web-components/stories/SliderDots.stories.ts b/libs/web-components/stories/SliderDots.stories.ts index 9fbcdb4..9b226a0 100644 --- a/libs/web-components/stories/SliderDots.stories.ts +++ b/libs/web-components/stories/SliderDots.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdSliderDots } from '../src'; -import { defaultTokenViewer, element, observed } from './helpers'; +import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -16,11 +16,11 @@ const meta = { }, }, args: { count: 5, activeIndex: 0 }, - argTypes: { + argTypes: sectionedArgTypes('SliderDots', { count: { description: 'Total number of dots to render', control: 'number' }, activeIndex: { description: 'Zero-based index of the active dot', control: 'number' }, styles: { description: 'Custom container styles', control: 'object' }, - }, + }), render: (args: Args) => observed(element('gd-slider-dots', args), 'gd-change', { index: args.activeIndex }), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Switch.stories.ts b/libs/web-components/stories/Switch.stories.ts index 100d545..5d5cef2 100644 --- a/libs/web-components/stories/Switch.stories.ts +++ b/libs/web-components/stories/Switch.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdSwitch } from '../src'; -import { defaultTokenViewer, element, observed } from './helpers'; +import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -16,14 +16,14 @@ const meta = { }, }, args: { name: 'switch', checked: false, disabled: false, isLoading: false, label: 'right' }, - argTypes: { + argTypes: sectionedArgTypes('Switch', { checked: { description: 'Checked state', control: 'boolean' }, disabled: { description: 'Disables interaction', control: 'boolean' }, isLoading: { description: 'Shows a loader and disables interaction', control: 'boolean' }, label: { description: 'Position of the slotted label', control: 'select', options: ['left', 'right'] }, name: { description: 'Native checkbox name', control: 'text' }, styles: { description: 'Custom wrapper styles', control: 'object' }, - }, + }), render: (args: Args) => observed(element('gd-switch', args, 'Label'), 'gd-change', { checked: args.checked }), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Textarea.stories.ts b/libs/web-components/stories/Textarea.stories.ts index e5e41d8..9108cf7 100644 --- a/libs/web-components/stories/Textarea.stories.ts +++ b/libs/web-components/stories/Textarea.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdTextarea } from '../src'; -import { defaultTokenViewer, element, observed } from './helpers'; +import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; type Args = Pick< GdTextarea, @@ -43,7 +43,7 @@ const meta = { readOnly: false, dynamicHeightAdjustment: false, }, - argTypes: { + argTypes: sectionedArgTypes('Textarea', { name: { description: 'Native field name', control: 'text' }, placeholder: { description: 'Placeholder text', control: 'text' }, value: { description: 'Controlled value', control: 'text' }, @@ -65,7 +65,7 @@ const meta = { maxLength: { description: 'Native hard character limit', control: 'number' }, maxCharacters: { description: 'Visual character counter threshold', control: 'number' }, styles: { description: 'Custom textarea styles', control: 'object' }, - }, + }), render: (args: Args) => observed(element('gd-textarea', args), 'gd-input', { value: args.value ?? args.defaultValue ?? '' }), } satisfies Meta; diff --git a/libs/web-components/stories/Truncate.stories.ts b/libs/web-components/stories/Truncate.stories.ts index 70c14d7..5ad7237 100644 --- a/libs/web-components/stories/Truncate.stories.ts +++ b/libs/web-components/stories/Truncate.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdTruncate } from '../src'; -import { element } from './helpers'; +import { element, sectionedArgTypes } from './helpers'; type Args = Pick & { text: string }; const long = @@ -18,11 +18,11 @@ const meta = { }, }, args: { lines: 1, text: long }, - argTypes: { + argTypes: sectionedArgTypes('Truncate', { text: { description: 'Text or slotted content to display', control: 'text' }, lines: { description: 'Maximum visible line count', control: 'number' }, styles: { description: 'Custom content styles', control: 'object' }, - }, + }), render: ({ text, ...args }: Args) => { const box = document.createElement('div'); box.style.width = '240px'; diff --git a/libs/web-components/stories/Typography.stories.ts b/libs/web-components/stories/Typography.stories.ts index 087c20f..451dbe1 100644 --- a/libs/web-components/stories/Typography.stories.ts +++ b/libs/web-components/stories/Typography.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdTypography } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick & { text: string }; const variants = [ @@ -55,14 +55,14 @@ const meta = { styleVariant: [], text: 'Body paragraph text for comparison.', }, - argTypes: { + argTypes: sectionedArgTypes('Typography', { variant: { control: 'select', options: variants }, as: { control: 'select', options: variants }, size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] }, align: { control: 'select', options: ['start', 'end', 'center', 'left', 'right', 'justify'] }, color: { control: 'color' }, styleVariant: { control: 'multi-select', options: styleVariants }, - }, + }), render: ({ text, ...props }: Args) => element('gd-typography', props, text), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/helpers.ts b/libs/web-components/stories/helpers.ts index 2b42951..453d81a 100644 --- a/libs/web-components/stories/helpers.ts +++ b/libs/web-components/stories/helpers.ts @@ -4,6 +4,106 @@ import type {} from '../src/index'; type Tag = Extract; +type ArgType = { + table?: Record; + [key: string]: unknown; +}; + +const reactArgTypeSections = { + Avatar: { badgeColor: 'Badge', backgroundColor: 'Appearance' }, + Badge: { variant: 'Visual Style', appearance: 'Visual Style', size: 'Visual Style', disabled: 'Visual Style' }, + Box: { + variant: 'Core Props', + isBordered: 'Visual Style', + isHighlighted: 'Visual Style', + withShadowHover: 'Visual Style', + }, + Button: { variant: 'Visual Style', rounded: 'Visual Style' }, + Checkbox: { size: 'Layout' }, + Icon: { name: 'Icon Selection', size: 'Size & Dimensions', fill: 'Styling & Colors' }, + Image: { + src: 'Core Properties', + alt: 'Core Properties', + width: 'Dimensions', + height: 'Dimensions', + caption: 'Content & Display', + objectFit: 'Styling', + }, + Input: { variant: 'Core Properties', color: 'Appearance & Styling' }, + InputFile: { + accept: 'File Handling', + capture: 'Mobile Features', + multiple: 'File Handling', + disabled: 'State', + isIcon: 'Button Customization', + }, + Label: { htmlFor: 'Core Properties', styles: 'Styling' }, + Link: { + variant: 'Core Properties', + size: 'Appearance', + underline: 'Appearance', + cursor: 'Appearance', + disabled: 'State & Behavior', + href: 'Core Properties', + target: 'Navigation & Behavior', + rel: 'Navigation & Behavior', + styles: 'Styling', + }, + Loader: { + name: 'Appearance', + variant: 'Layout & Positioning', + size: 'Appearance', + rounded: 'Appearance', + withWrapper: 'Layout & Positioning', + animationProps: 'Content & Customization', + styles: 'Custom Styling', + }, + Menu: { placement: 'Menu Options (Positioning & Dimensions)' }, + SliderDots: { count: 'Core Properties', activeIndex: 'Core Properties' }, + Switch: { checked: 'State', disabled: 'State', isLoading: 'State', label: 'Layout', name: 'Identification' }, + Textarea: { + name: 'Basics', + placeholder: 'Basics', + value: 'Basics', + defaultValue: 'Basics', + minHeight: 'Sizing & Layout', + maxHeight: 'Sizing & Layout', + disabled: 'Accessibility', + readOnly: 'Accessibility', + autoFocus: 'Accessibility', + resize: 'Behavior', + rows: 'Sizing & Layout', + dynamicHeightAdjustment: 'Behavior', + variant: 'Styling', + color: 'Styling', + maxLength: 'Behavior', + styles: 'Styling', + }, + Truncate: { lines: 'Truncation', styles: 'Appearance' }, + Typography: { + variant: 'Appearance', + as: 'Behavior', + size: 'Appearance', + align: 'Layout', + color: 'Appearance', + styleVariant: 'Appearance', + }, +} as const; + +/** Preserve the semantic control sections defined by the corresponding React story. */ +export function sectionedArgTypes< + Component extends keyof typeof reactArgTypeSections, + ArgTypes extends Record, +>(component: Component, argTypes: ArgTypes): ArgTypes { + const sections = reactArgTypeSections[component] as Partial>; + return Object.fromEntries( + Object.entries(argTypes).map(([name, argType]) => { + const category = sections[name]; + return [name, category ? { ...argType, table: { ...argType.table, category } } : argType]; + }) + ) as ArgTypes; +} + const sourceKeyProperties: Partial> = { 'gd-avatar': ['sizeVariant'], 'gd-badge': ['variant', 'appearance', 'size'], From cbed65eca1490d8b1612f8ed9856bd1e2e7b0aa9 Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:33:13 +0200 Subject: [PATCH 03/17] fix(web-components): match React Storybook metadata --- bin/storybook/native-checks.mjs | 34 + libs/web-components/README.md | 7 + .../capture-react-storybook-metadata.mjs | 47 + .../scripts/check-ported-elements.mjs | 74 +- .../generate-react-storybook-metadata.mjs | 157 + libs/web-components/stories/Avatar.stories.ts | 30 +- libs/web-components/stories/Button.stories.ts | 24 +- .../stories/Checkbox.stories.ts | 4 +- .../web-components/stories/Counter.stories.ts | 3 +- libs/web-components/stories/Input.stories.ts | 18 +- .../stories/InputFile.stories.ts | 22 +- libs/web-components/stories/Menu.stories.ts | 7 +- libs/web-components/stories/Select.stories.ts | 8 +- .../stories/Separator.stories.ts | 6 +- .../stories/Skeleton.stories.ts | 6 +- libs/web-components/stories/Slider.stories.ts | 6 +- libs/web-components/stories/Toggle.stories.ts | 6 +- .../stories/Truncate.stories.ts | 14 +- .../stories/Typography.stories.ts | 38 +- .../web-components/stories/Wrapper.stories.ts | 22 +- libs/web-components/stories/helpers.ts | 103 +- .../react-storybook-arg-types.generated.ts | 7878 +++++++++++++++++ .../react-storybook-arg-types.snapshot.json | 7833 ++++++++++++++++ 23 files changed, 16106 insertions(+), 241 deletions(-) create mode 100644 libs/web-components/scripts/capture-react-storybook-metadata.mjs create mode 100644 libs/web-components/scripts/generate-react-storybook-metadata.mjs create mode 100644 libs/web-components/stories/react-storybook-arg-types.generated.ts create mode 100644 libs/web-components/stories/react-storybook-arg-types.snapshot.json diff --git a/bin/storybook/native-checks.mjs b/bin/storybook/native-checks.mjs index 6a58332..eeecd3a 100644 --- a/bin/storybook/native-checks.mjs +++ b/bin/storybook/native-checks.mjs @@ -1,8 +1,42 @@ import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const reactArgTypes = JSON.parse( + await readFile( + new URL('../../libs/web-components/stories/react-storybook-arg-types.snapshot.json', import.meta.url), + 'utf8' + ) +); /** Trusted browser input is required for native popover light-dismiss. */ export async function checkNativeStories(page, base) { const index = await (await page.request.get(`${base}/web-components/index.json`)).json(); + for (const [component, sourceArgTypes] of Object.entries(reactArgTypes)) { + const title = `${component === 'Counter' || component === 'Menu' ? 'Molecules' : 'Atoms'}/${component}`; + const entry = Object.values(index.entries).find( + (candidate) => candidate.type === 'story' && candidate.title === title + ); + assert.ok(entry, `${title} must have a Web Component story`); + await page.goto(`${base}/web-components/iframe.html?id=${entry.id}&viewMode=story`); + const targetArgTypes = await page.evaluate(async (storyId) => { + const preview = window.__STORYBOOK_PREVIEW__; + await preview.storeInitializationPromise; + const story = await preview.storyStoreValue.loadStory({ storyId }); + return JSON.parse(JSON.stringify(story.argTypes ?? {})); + }, entry.id); + assert.deepEqual( + Object.keys(targetArgTypes).sort(), + Object.keys(sourceArgTypes).sort(), + `${title} control inventory must match React` + ); + for (const [name, source] of Object.entries(sourceArgTypes)) { + const target = targetArgTypes[name]; + assert.equal(target.description, source.description, `${title}.${name} description must match React`); + assert.deepEqual(target.options, source.options, `${title}.${name} options must match React`); + assert.deepEqual(target.table, source.table, `${title}.${name} table metadata must match React`); + assert.deepEqual(target.if, source.if, `${title}.${name} conditional visibility must match React`); + } + } const stories = Object.values(index.entries).filter( (entry) => entry.type === 'story' && /^(Atoms|Molecules)\//.test(entry.title) && !entry.id.endsWith('--default-tokens') diff --git a/libs/web-components/README.md b/libs/web-components/README.md index 45a15ea..98be89a 100644 --- a/libs/web-components/README.md +++ b/libs/web-components/README.md @@ -144,6 +144,13 @@ component's `styles` property rather than reproduced as React-only prop names. missing. Unit tests cover the programmatic property/event contract; the Storybook smoke test also checks representative interactions, semantic output, and visual state for the interactive ports in Chromium. +The Web Component controls tables use a snapshot of the resolved React Storybook metadata, including +props inferred by React docgen. Refresh it with +`node libs/web-components/scripts/capture-react-storybook-metadata.mjs`, then run +`node libs/web-components/scripts/generate-react-storybook-metadata.mjs`. The port check and browser +smoke test reject missing or extra controls and changes to descriptions, options, defaults, conditions, +types, or semantic sections. + There is no separate Web Components Storybook command. Run `npm run build-storybook` then `node bin/storybook/smoke-test.mjs` to verify every native story and representative interactions in Chromium. The Angular and Vue harnesses cover the complete supported catalog. diff --git a/libs/web-components/scripts/capture-react-storybook-metadata.mjs b/libs/web-components/scripts/capture-react-storybook-metadata.mjs new file mode 100644 index 0000000..f91565e --- /dev/null +++ b/libs/web-components/scripts/capture-react-storybook-metadata.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node +import { writeFile, readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { chromium } from 'playwright'; + +const packageRoot = resolve(import.meta.dirname, '..'); +const parity = JSON.parse(await readFile(resolve(packageRoot, 'react-parity.json'), 'utf8')); +const sourceUrl = + process.argv.find((argument) => argument.startsWith('--url='))?.slice('--url='.length) ?? + 'https://storybook.cto-rnd-system-design.griddynamics.net'; +const index = await (await fetch(`${sourceUrl}/index.json`)).json(); +const entries = Object.values(index.entries); +const browser = await chromium.launch({ headless: true }); +const page = await browser.newPage(); +const snapshot = {}; + +try { + for (const [tag, contract] of Object.entries(parity)) { + const group = tag === 'gd-counter' || tag === 'gd-menu' ? 'Molecules' : 'Atoms'; + const title = `${group}/${contract.react}`; + const entry = entries.find((candidate) => candidate.type === 'story' && candidate.title === title); + if (!entry) throw new Error(`The source Storybook does not contain ${title}.`); + const storyUrl = new URL('/iframe.html', sourceUrl); + storyUrl.searchParams.set('id', entry.id); + storyUrl.searchParams.set('viewMode', 'story'); + await page.goto(storyUrl.toString(), { + waitUntil: 'networkidle', + timeout: 60_000, + }); + snapshot[contract.react] = await page.evaluate(async (storyId) => { + const preview = window.__STORYBOOK_PREVIEW__; + await preview.storeInitializationPromise; + const story = await preview.storyStoreValue.loadStory({ storyId }); + return JSON.parse(JSON.stringify(story.argTypes ?? {})); + }, entry.id); + } +} finally { + await browser.close(); +} + +await writeFile( + resolve(packageRoot, 'stories/react-storybook-arg-types.snapshot.json'), + `${JSON.stringify(snapshot, null, 2)}\n` +); +console.log( + `Captured resolved React argTypes for ${Object.keys(snapshot).length} migrated components from ${sourceUrl}.` +); diff --git a/libs/web-components/scripts/check-ported-elements.mjs b/libs/web-components/scripts/check-ported-elements.mjs index 3f80e0f..4511efe 100644 --- a/libs/web-components/scripts/check-ported-elements.mjs +++ b/libs/web-components/scripts/check-ported-elements.mjs @@ -9,7 +9,6 @@ const repoRoot = resolve(packageRoot, '../..'); const propertyName = (node) => ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) ? node.text : undefined; -const unwrapExpression = (node) => (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) ? node.expression : node); function objectProperty(object, name) { return object?.properties.find( @@ -17,22 +16,6 @@ function objectProperty(object, name) { )?.initializer; } -function findObject(sourceFile, name) { - let result; - const visit = (node) => { - if ( - !result && - ts.isVariableDeclaration(node) && - propertyName(node.name) === name && - ts.isObjectLiteralExpression(unwrapExpression(node.initializer)) - ) - result = unwrapExpression(node.initializer); - ts.forEachChild(node, visit); - }; - visit(sourceFile); - return result; -} - function storyArgTypes(source, fileName) { const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true); let result; @@ -68,26 +51,6 @@ function storyArgTypes(source, fileName) { ); } -function sectionMap(source) { - const sourceFile = ts.createSourceFile('helpers.ts', source, ts.ScriptTarget.Latest, true); - const sections = findObject(sourceFile, 'reactArgTypeSections'); - return new Map( - (sections?.properties ?? []).filter(ts.isPropertyAssignment).map((component) => { - const controls = ts.isObjectLiteralExpression(component.initializer) ? component.initializer.properties : []; - return [ - propertyName(component.name), - new Map( - controls - .filter(ts.isPropertyAssignment) - .map((control) => [ - propertyName(control.name), - ts.isStringLiteral(control.initializer) ? control.initializer.text : undefined, - ]) - ), - ]; - }) - ); -} const fullStoryParity = new Set([ 'gd-avatar', 'gd-box', @@ -167,16 +130,22 @@ if (missingFromManifest.length || missingFromSource.length) { ); } -const [index, ssr, parityText, publishedAuditText, storyHelpers] = await Promise.all([ +const [index, ssr, parityText, publishedAuditText, generatedArgTypesText, sourceArgTypesText] = await Promise.all([ readFile(resolve(packageRoot, 'src/index.ts'), 'utf8'), readFile(resolve(packageRoot, 'scripts/ssr-dsd-render.ts'), 'utf8'), readFile(resolve(packageRoot, 'react-parity.json'), 'utf8'), readFile(resolve(packageRoot, 'published-atoms-audit.json'), 'utf8'), - readFile(resolve(packageRoot, 'stories/helpers.ts'), 'utf8'), + readFile(resolve(packageRoot, 'stories/react-storybook-arg-types.generated.ts'), 'utf8'), + readFile(resolve(packageRoot, 'stories/react-storybook-arg-types.snapshot.json'), 'utf8'), ]); const parity = JSON.parse(parityText); const publishedAudit = JSON.parse(publishedAuditText); -const sections = sectionMap(storyHelpers); +const generatedArgTypesSource = generatedArgTypesText.match(/reactStorybookArgTypes = ([\s\S]+) as const;/)?.[1]; +const generatedArgTypes = generatedArgTypesSource + ? Function(`"use strict"; return (${generatedArgTypesSource});`)() + : {}; +if (JSON.stringify(generatedArgTypes) !== JSON.stringify(JSON.parse(sourceArgTypesText))) + throw new Error('Generated React Storybook metadata does not match the published source snapshot.'); const atomTags = ports.filter(({ category }) => category === 'atoms').map(({ tag }) => tag); const publishedAtomTags = Object.keys(publishedAudit.components); const missingPublishedAtoms = publishedAtomTags.filter((tag) => !atomTags.includes(tag)); @@ -214,22 +183,23 @@ for (const { tag, category } of ports) { 'utf8' ); const reactControls = storyArgTypes(reactStories, `${contract.react}.stories.tsx`); - const targetControls = storyArgTypes(stories, `${storyName}.stories.ts`); - const targetSections = sections.get(storyName) ?? new Map(); - for (const control of targetControls.keys()) { - const sourceSection = reactControls.get(control)?.category; - const targetSection = targetSections.get(control); - if (sourceSection !== targetSection) { + const targetControls = generatedArgTypes[storyName] ?? {}; + if (!stories.includes(`sectionedArgTypes('${storyName}'`)) + throw new Error(`${tag} must merge the complete React argTypes metadata.`); + const missingControls = [...reactControls.keys()].filter((control) => !(control in targetControls)); + if (missingControls.length) + throw new Error(`${tag} Storybook controls are missing: ${missingControls.join(', ')}.`); + for (const [control, sourceMetadata] of reactControls) { + const targetMetadata = targetControls[control]; + for (const section of ['category', 'subcategory']) { + const sourceSection = sourceMetadata[section]; + const targetSection = targetMetadata.table?.[section]; + if (sourceSection === targetSection) continue; throw new Error( - `${tag} Storybook section mismatch for ${control}: React=${sourceSection ?? 'unsectioned'}, Web Component=${ - targetSection ?? 'unsectioned' - }.` + `${tag} Storybook ${section} mismatch for ${control}: React=${sourceSection ?? 'unsectioned'}, Web Component=${targetSection ?? 'unsectioned'}.` ); } } - for (const control of targetSections.keys()) { - if (!targetControls.has(control)) throw new Error(`${tag} has a stale Storybook section for ${control}.`); - } const exported = [...reactStories.matchAll(/^export const (\w+)/gm)].map((match) => match[1]); const missing = exported.filter((story) => !contract.stories.includes(story)); const stale = contract.stories.filter((story) => !exported.includes(story)); diff --git a/libs/web-components/scripts/generate-react-storybook-metadata.mjs b/libs/web-components/scripts/generate-react-storybook-metadata.mjs new file mode 100644 index 0000000..0dbbf86 --- /dev/null +++ b/libs/web-components/scripts/generate-react-storybook-metadata.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env node +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import ts from 'typescript'; + +const packageRoot = resolve(import.meta.dirname, '..'); +const repoRoot = resolve(packageRoot, '../..'); +const parity = JSON.parse(await readFile(resolve(packageRoot, 'react-parity.json'), 'utf8')); +const outputPath = resolve(packageRoot, 'stories/react-storybook-arg-types.generated.ts'); +const snapshotPath = resolve(packageRoot, 'stories/react-storybook-arg-types.snapshot.json'); + +const knownExpressions = new Map([ + ['Object.keys(WrapperVariant)', ['Inline', 'Section', 'FullPage']], + ['WrapperVariant.Inline', 'inline'], + ['Object.values(TextareaResize)', ['none', 'both', 'horizontal', 'vertical']], + ['Object.values(Orientation)', ['vertical', 'horizontal']], + ['Object.values(SizeVariant)', ['xs', 'sm', 'md', 'lg', 'xl', 'xxl']], + ['Object.values(SeparatorVariant)', ['solid', 'dashed', 'dotted']], + ['Object.values(SeparatorLabelPosition)', ['start', 'center', 'end']], + ['SkeletonVariant.Rounded', 'rounded'], + ['Object.values(LinkVariant)', ['primary', 'secondary', 'inverted', 'inherit']], + ['Object.values(LinkTarget)', ['_blank', '_self', '_parent', '_top', 'framename']], + ['Object.values(SizeVariant).filter((size) => size !== SizeVariant.Xxl)', ['xs', 'sm', 'md', 'lg', 'xl']], + ['Object.values(WrapperVariant)', ['inline', 'section', 'fullPage']], + ['Object.values(ButtonTypes)', ['button', 'submit', 'reset']], + ['ButtonTypes.Button', 'button'], + ['Object.values(ButtonVariant)', ['primary', 'secondary', 'tertiary', 'outlined', 'text', 'inherit']], + ['Object.values(ButtonRole)', ['button', 'link', 'checkbox', 'switch', 'tab']], + ['ButtonRole.Button', 'button'], + [ + 'Object.values(InputVariantType)', + [ + 'text', + 'password', + 'email', + 'search', + 'url', + 'tel', + 'date', + 'time', + 'month', + 'week', + 'color', + 'range', + 'number', + 'radio', + 'checkbox', + ], + ], + ['InputVariantType.Text', 'text'], + ['Object.values(InputRole)', ['textbox', 'spinbutton', 'checkbox', 'radio', 'radiogroup', 'combobox']], + ['InputRole.Textbox', 'textbox'], +]); + +const propertyName = (node) => + ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) ? node.text : undefined; + +function extractArgTypes(fileName, source) { + const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); + const variables = new Map(); + for (const statement of sourceFile.statements) { + if (!ts.isVariableStatement(statement)) continue; + for (const declaration of statement.declarationList.declarations) { + if (ts.isIdentifier(declaration.name) && declaration.initializer) + variables.set(declaration.name.text, declaration.initializer); + } + } + + let argTypes; + const visit = (node) => { + if ( + !argTypes && + ts.isPropertyAssignment(node) && + propertyName(node.name) === 'argTypes' && + ts.isObjectLiteralExpression(node.initializer) + ) + argTypes = node.initializer; + ts.forEachChild(node, visit); + }; + visit(sourceFile); + + const evaluate = (node, seen = []) => { + if (!node) return undefined; + const sourceText = node.getText(sourceFile); + if (knownExpressions.has(sourceText)) return knownExpressions.get(sourceText); + if (sourceText.startsWith('Object.keys(SkeletonVariant).map(')) return ['rectangular', 'rounded', 'circular']; + if (sourceText === 'Object.keys(IconsList)') return undefined; // The target story supplies the shared catalog. + if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node)) + return evaluate(node.expression, seen); + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text; + if (ts.isNumericLiteral(node)) return Number(node.text); + if (node.kind === ts.SyntaxKind.TrueKeyword) return true; + if (node.kind === ts.SyntaxKind.FalseKeyword) return false; + if (node.kind === ts.SyntaxKind.NullKeyword) return null; + if (ts.isIdentifier(node)) { + if (node.text === 'undefined') return undefined; + if (variables.has(node.text) && !seen.includes(node.text)) + return evaluate(variables.get(node.text), [...seen, node.text]); + return undefined; + } + if (ts.isArrayLiteralExpression(node)) return node.elements.map((element) => evaluate(element, seen)); + if (ts.isObjectLiteralExpression(node)) { + const value = {}; + for (const property of node.properties) { + if (!ts.isPropertyAssignment(property)) continue; + const child = evaluate(property.initializer, seen); + if (child !== undefined) value[propertyName(property.name)] = child; + } + return value; + } + if (ts.isPrefixUnaryExpression(node)) { + const value = evaluate(node.operand, seen); + return node.operator === ts.SyntaxKind.MinusToken ? -value : value; + } + return undefined; + }; + + return Object.fromEntries( + (argTypes?.properties ?? []) + .filter(ts.isPropertyAssignment) + .map((property) => [propertyName(property.name), evaluate(property.initializer)]) + ); +} + +const metadata = JSON.parse(await readFile(snapshotPath, 'utf8')); +for (const [tag, contract] of Object.entries(parity)) { + const category = tag === 'gd-counter' || tag === 'gd-menu' ? 'molecules' : 'atoms'; + const fileName = resolve( + repoRoot, + 'libs/ui/src/components', + category, + contract.react, + `${contract.react}.stories.tsx` + ); + const explicitArgTypes = extractArgTypes(fileName, await readFile(fileName, 'utf8')); + const publishedArgTypes = metadata[contract.react]; + if (!publishedArgTypes) throw new Error(`${contract.react} is missing from the published Storybook snapshot.`); + const missing = Object.keys(explicitArgTypes).filter((control) => !(control in publishedArgTypes)); + if (missing.length) + throw new Error(`${contract.react} published Storybook snapshot is missing controls: ${missing.join(', ')}.`); +} + +const output = `/* This file is generated from the React Storybook metadata. Do not edit it by hand. */\nexport const reactStorybookArgTypes = ${JSON.stringify( + metadata, + null, + 2 +)} as const;\n`; + +if (process.argv.includes('--check')) { + const current = await readFile(outputPath, 'utf8').catch(() => ''); + const serialized = current.match(/reactStorybookArgTypes = ([\s\S]+) as const;/)?.[1]; + const currentMetadata = serialized ? Function(`"use strict"; return (${serialized});`)() : undefined; + if (!currentMetadata || JSON.stringify(currentMetadata) !== JSON.stringify(metadata)) + throw new Error('React Storybook metadata is stale. Run generate-react-storybook-metadata.mjs.'); +} else { + await writeFile(outputPath, output); +} diff --git a/libs/web-components/stories/Avatar.stories.ts b/libs/web-components/stories/Avatar.stories.ts index 4ad2403..93368be 100644 --- a/libs/web-components/stories/Avatar.stories.ts +++ b/libs/web-components/stories/Avatar.stories.ts @@ -2,7 +2,9 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdAvatar } from '../src'; import { defaultTokenViewer, element, portrait, sectionedArgTypes } from './helpers'; -type Args = Pick; +type Args = Pick & { + fallbackComponent: string; +}; const meta = { title: 'Atoms/Avatar', tags: ['autodocs'], @@ -17,32 +19,34 @@ const meta = { args: { src: '', alt: 'Ada Lovelace', - fallback: 'AL', - size: 'md', + fallbackComponent: 'AL', + sizeVariant: 'md', withBadge: false, badgeColor: 'bg.fill.success.primary.default', backgroundColor: undefined, }, argTypes: sectionedArgTypes('Avatar', { - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] }, + sizeVariant: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] }, badgeColor: { control: 'color' }, backgroundColor: { control: 'color' }, }), - render: (args: Args) => element('gd-avatar', args), + render: ({ fallbackComponent, ...args }: Args) => element('gd-avatar', { ...args, fallback: fallbackComponent }), } satisfies Meta; export default meta; type Story = StoryObj; export const Default: Story = {}; export const Image: Story = { args: { src: portrait } }; -export const Badge: Story = { args: { withBadge: true, size: 'xl' } }; -export const Small: Story = { args: { size: 'sm' } }; -export const Large: Story = { args: { size: 'lg' } }; +export const Badge: Story = { args: { withBadge: true, sizeVariant: 'xl' } }; +export const Small: Story = { args: { sizeVariant: 'sm' } }; +export const Large: Story = { args: { sizeVariant: 'lg' } }; export const AllSizes: Story = { render: (args) => { const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:16px;align-items:center'; for (const size of ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const) - row.append(element('gd-avatar', { ...args, size, alt: `${size} avatar` })); + row.append( + element('gd-avatar', { ...args, fallback: args.fallbackComponent, sizeVariant: size, alt: `${size} avatar` }) + ); return row; }, }; @@ -59,10 +63,12 @@ export const SlottedFallback: Story = { }; export const WithBadge: Story = { args: { withBadge: true } }; export const WithCustomBadgeColor: Story = { args: { withBadge: true, badgeColor: '#22c55e' } }; -export const WithInitials: Story = { args: { src: '', fallback: 'AL' } }; -export const WithCustomBackgroundColor: Story = { args: { src: '', fallback: 'AL', backgroundColor: '#6b46c1' } }; +export const WithInitials: Story = { args: { src: '', fallbackComponent: 'AL' } }; +export const WithCustomBackgroundColor: Story = { + args: { src: '', fallbackComponent: 'AL', backgroundColor: '#6b46c1' }, +}; export const WithIcon: Story = SlottedFallback; -export const WithImageAndFallback: Story = { args: { src: portrait, fallback: 'AL' } }; +export const WithImageAndFallback: Story = { args: { src: portrait, fallbackComponent: 'AL' } }; export const WithDifferentSize: Story = AllSizes; export const WithAccessibility: Story = { args: { alt: 'Ada Lovelace profile photo' } }; export const DefaultTokens: Story = { render: () => defaultTokenViewer('avatar') }; diff --git a/libs/web-components/stories/Button.stories.ts b/libs/web-components/stories/Button.stories.ts index 4f57de5..625b8f7 100644 --- a/libs/web-components/stories/Button.stories.ts +++ b/libs/web-components/stories/Button.stories.ts @@ -15,9 +15,11 @@ type Args = Pick< | 'tabIndex' | 'ariaLabel' | 'ariaPressed' -> & { label: string }; + | 'styles' +> & { children: string }; const variants = ['primary', 'secondary', 'tertiary', 'outlined', 'text', 'inherit'] as const; const roundedOptions = ['none', 'default', 'round', 'xs', 'sm', 'md', 'lg', 'xl'] as const; +const buttonElement = ({ children, ...props }: Args, text = children) => element('gd-button', props, text); const meta = { title: 'Atoms/Button', tags: ['autodocs'], @@ -41,13 +43,13 @@ const meta = { tabIndex: 0, ariaLabel: null, ariaPressed: null, - label: 'Button', + children: 'Button', }, argTypes: sectionedArgTypes('Button', { variant: { control: 'select', options: variants }, rounded: { control: 'select', options: roundedOptions }, }), - render: ({ label, ...props }: Args) => element('gd-button', props, label), + render: (args: Args) => buttonElement(args), } satisfies Meta; export default meta; type Story = StoryObj; @@ -60,7 +62,7 @@ export const AllVariants: Story = { render: (args) => { const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:12px;flex-wrap:wrap'; - for (const variant of variants) row.append(element('gd-button', { ...args, variant }, variant)); + for (const variant of variants) row.append(buttonElement({ ...args, variant }, variant)); return row; }, }; @@ -68,15 +70,15 @@ export const Rounded: Story = { render: (args) => { const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:12px;flex-wrap:wrap'; - for (const rounded of roundedOptions) row.append(element('gd-button', { ...args, rounded }, rounded)); + for (const rounded of roundedOptions) row.append(buttonElement({ ...args, rounded }, rounded)); return row; }, }; -export const FullWidth: Story = { args: { fullWidth: true, label: 'Full-width action' } }; -export const IconOnly: Story = { args: { isIcon: true, ariaLabel: 'Add item', label: '+' } }; +export const FullWidth: Story = { args: { fullWidth: true, children: 'Full-width action' } }; +export const IconOnly: Story = { args: { isIcon: true, ariaLabel: 'Add item', children: '+' } }; export const IconSlots: Story = { render: (args) => { - const button = element('gd-button', args, 'Continue'); + const button = buttonElement(args, 'Continue'); const icon = document.createElement('span'); icon.slot = 'icon-end'; icon.setAttribute('aria-hidden', 'true'); @@ -89,11 +91,11 @@ export const WithIcons: Story = IconSlots; export const FullWidthButton: Story = FullWidth; export const RoundedButton: Story = Rounded; export const DisabledButton: Story = Disabled; -export const ButtonStatesUsingClass: Story = { args: { ariaPressed: 'true', label: 'Pressed state' } }; +export const ButtonStatesUsingClass: Story = { args: { ariaPressed: 'true', children: 'Pressed state' } }; export const CustomStyledButton: Story = { - render: (args) => element('gd-button', { ...args, styles: { letterSpacing: '0.08em' } }, 'Custom styled'), + render: (args) => buttonElement({ ...args, styles: { letterSpacing: '0.08em' } }, 'Custom styled'), }; export const RealWorldExamples: Story = AllVariants; export const IsLoading: Story = Loading; -export const WithAccessibility: Story = { args: { ariaLabel: 'Save changes', label: 'Save' } }; +export const WithAccessibility: Story = { args: { ariaLabel: 'Save changes', children: 'Save' } }; export const DefaultTokens: Story = { render: () => defaultTokenViewer('button') }; diff --git a/libs/web-components/stories/Checkbox.stories.ts b/libs/web-components/stories/Checkbox.stories.ts index a0c1a07..bfe4ce8 100644 --- a/libs/web-components/stories/Checkbox.stories.ts +++ b/libs/web-components/stories/Checkbox.stories.ts @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdCheckbox } from '../src'; import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; -type Args = Pick; +type Args = Pick; const meta = { title: 'Atoms/Checkbox', tags: ['autodocs'], @@ -14,7 +14,7 @@ const meta = { }, }, }, - args: { checked: false, disabled: false, indeterminate: false, size: 'md', name: '', value: 'on', required: false }, + args: { checked: false, disabled: false, indeterminate: false, size: 'md', name: '', value: 'on' }, argTypes: sectionedArgTypes('Checkbox', { size: { control: 'select', options: ['sm', 'md'] } }), render: (args: Args) => { const checkbox = element('gd-checkbox', args, 'Accept terms'); diff --git a/libs/web-components/stories/Counter.stories.ts b/libs/web-components/stories/Counter.stories.ts index 77dd3f1..5c469df 100644 --- a/libs/web-components/stories/Counter.stories.ts +++ b/libs/web-components/stories/Counter.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdCounter } from '../src'; -import { defaultTokenViewer, element, observed } from './helpers'; +import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -15,6 +15,7 @@ const meta = { }, }, args: { min: 1, max: 5, initial: 1, isDisabled: false }, + argTypes: sectionedArgTypes('Counter', {}), render: (args: Args) => observed(element('gd-counter', args), 'gd-change', { value: args.initial }), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Input.stories.ts b/libs/web-components/stories/Input.stories.ts index c41241c..7b387be 100644 --- a/libs/web-components/stories/Input.stories.ts +++ b/libs/web-components/stories/Input.stories.ts @@ -4,7 +4,7 @@ import { defaultTokenViewer, element, observed, sectionedArgTypes } from './help type Args = Pick< GdInput, - | 'value' + | 'defaultValue' | 'variant' | 'label' | 'helperText' @@ -14,7 +14,6 @@ type Args = Pick< | 'required' | 'color' | 'width' - | 'inputmode' | 'role' | 'tabIndex' | 'debounceCallbackTime' @@ -49,7 +48,7 @@ const meta = { }, }, args: { - value: 'Default Input', + defaultValue: 'Default Input', variant: 'text', label: 'Label', helperText: 'Helper text', @@ -59,7 +58,6 @@ const meta = { required: false, color: 'primary', width: '320px', - inputmode: undefined, role: null, tabIndex: 0, debounceCallbackTime: undefined, @@ -73,13 +71,13 @@ const meta = { input.addEventListener('gd-input', (event) => { input.value = (event as CustomEvent<{ value: string }>).detail.value; }); - return observed(input, 'gd-input', { value: args.value }); + return observed(input, 'gd-input', { value: args.defaultValue }); }, } satisfies Meta; export default meta; type Story = StoryObj; export const Default: Story = {}; -export const Empty: Story = { args: { value: '' } }; +export const Empty: Story = { args: { defaultValue: '' } }; export const Disabled: Story = { args: { disabled: true } }; export const ReadOnly: Story = { args: { readOnly: true } }; export const ValidationColors: Story = { @@ -109,7 +107,7 @@ export const InputTypes: Story = { const section = document.createElement('section'); section.style.cssText = 'display:grid;gap:16px;max-width:360px'; for (const variant of ['text', 'email', 'password', 'search', 'number', 'date']) - section.append(element('gd-input', { ...args, variant, value: '', label: variant })); + section.append(element('gd-input', { ...args, variant, defaultValue: '', label: variant })); return section; }, }; @@ -133,8 +131,10 @@ export const DefaultWithTailwind: Story = { export const CustomStyles: Story = { render: (args) => element('gd-input', { ...args, styles: { letterSpacing: '0.05em' } }), }; -export const CheckboxWithLabel: Story = { args: { variant: 'checkbox', value: '', label: 'Accept terms' } }; -export const RadioControlledWithLabel: Story = { args: { variant: 'radio', value: 'one', label: 'Option one' } }; +export const CheckboxWithLabel: Story = { args: { variant: 'checkbox', defaultValue: '', label: 'Accept terms' } }; +export const RadioControlledWithLabel: Story = { + args: { variant: 'radio', defaultValue: 'one', label: 'Option one' }, +}; export const RadioGroupWithLabel: Story = InputTypes; export const WithAccessibility: Story = { args: { required: true, label: 'Email address', helperText: 'Required field' }, diff --git a/libs/web-components/stories/InputFile.stories.ts b/libs/web-components/stories/InputFile.stories.ts index f1a8273..1d87ea5 100644 --- a/libs/web-components/stories/InputFile.stories.ts +++ b/libs/web-components/stories/InputFile.stories.ts @@ -2,7 +2,9 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdInputFile } from '../src'; import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; -type Args = Pick; +type Args = Pick & { + buttonProps: { variant?: GdInputFile['buttonVariant'] }; +}; const meta = { title: 'Atoms/InputFile', tags: ['autodocs'], @@ -14,7 +16,14 @@ const meta = { }, }, }, - args: { accept: '', capture: undefined, multiple: false, disabled: false, isIcon: false, buttonVariant: 'outlined' }, + args: { + accept: '', + capture: undefined, + multiple: false, + disabled: false, + isIcon: false, + buttonProps: { variant: 'outlined' }, + }, argTypes: sectionedArgTypes('InputFile', { accept: { description: 'Accepted file MIME types or extensions', control: 'text' }, capture: { @@ -25,13 +34,10 @@ const meta = { multiple: { description: 'Allows selection of multiple files', control: 'boolean' }, disabled: { description: 'Disables the file picker', control: 'boolean' }, isIcon: { description: 'Uses icon-only button sizing', control: 'boolean' }, - buttonVariant: { - description: 'Visual variant passed to the internal button', - control: 'select', - options: ['primary', 'secondary', 'outlined'], - }, + buttonProps: { description: 'Properties passed to the internal button', control: 'object' }, }), - render: (args: Args) => observed(element('gd-input-file', args), 'gd-change', { files: [] }), + render: ({ buttonProps, ...args }: Args) => + observed(element('gd-input-file', { ...args, buttonVariant: buttonProps.variant }), 'gd-change', { files: [] }), } satisfies Meta; export default meta; type Story = StoryObj; diff --git a/libs/web-components/stories/Menu.stories.ts b/libs/web-components/stories/Menu.stories.ts index a69778c..fb9e1af 100644 --- a/libs/web-components/stories/Menu.stories.ts +++ b/libs/web-components/stories/Menu.stories.ts @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdMenu } from '../src'; import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; -type Args = Pick; +type Args = Pick; const placements = ['top-left', 'top-right', 'bottom-left', 'bottom-right'] as const; const meta = { title: 'Molecules/Menu', @@ -16,7 +16,6 @@ const meta = { }, }, args: { - open: false, closeOnSelect: true, placement: 'bottom-right', offsetX: 4, @@ -51,7 +50,9 @@ const meta = { export default meta; type Story = StoryObj; export const Default: Story = {}; -export const Open: Story = { args: { open: true } }; +export const Open: Story = { + render: (args) => element('gd-menu', { ...args, open: true }), +}; export const KeepOpenOnSelect: Story = { args: { closeOnSelect: false } }; export const Placement: Story = { render: (args) => { diff --git a/libs/web-components/stories/Select.stories.ts b/libs/web-components/stories/Select.stories.ts index d3c3e1f..dc69cde 100644 --- a/libs/web-components/stories/Select.stories.ts +++ b/libs/web-components/stories/Select.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdSelect } from '../src'; -import { defaultTokenViewer, element, observed, items } from './helpers'; +import { defaultTokenViewer, element, observed, items, sectionedArgTypes } from './helpers'; type Args = Pick< GdSelect, @@ -37,15 +37,13 @@ const meta = { color: 'primary', placeholder: 'Choose an option', multiple: false, - searchable: false, - searchPlaceholder: 'Search options', autoOpen: true, dropdownMaxHeight: '240px', width: '280px', minWidth: undefined, maxWidth: 'initial', }, - argTypes: { color: { control: 'select', options: colors } }, + argTypes: sectionedArgTypes('Select', { color: { control: 'select', options: colors } }), render: (args: Args) => { const select = element('gd-select', args); select.addEventListener('gd-change', (event) => { @@ -61,7 +59,7 @@ export const Selected: Story = { args: { value: items[1] } }; export const Disabled: Story = { args: { disabled: true, value: items[0] } }; export const Empty: Story = { args: { items: [] } }; export const Multiple: Story = { args: { multiple: true, value: [items[0], items[2]] } }; -export const Searchable: Story = { args: { searchable: true } }; +export const Searchable: Story = { args: { searchable: true, searchPlaceholder: 'Search options' } }; export const AutoOpen: Story = { args: { autoOpen: false }, name: 'Manual open (autoOpen=false)' }; export const ValidationColors: Story = { render: (args) => { diff --git a/libs/web-components/stories/Separator.stories.ts b/libs/web-components/stories/Separator.stories.ts index cf62b70..88be798 100644 --- a/libs/web-components/stories/Separator.stories.ts +++ b/libs/web-components/stories/Separator.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdSeparator } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick< GdSeparator, @@ -19,7 +19,7 @@ const meta = { }, }, args: { orientation: 'horizontal', size: 'sm', variant: 'solid', as: 'div', labelPosition: 'center' }, - argTypes: { + argTypes: sectionedArgTypes('Separator', { orientation: { description: 'The orientation of the separator.', control: 'select', @@ -46,7 +46,7 @@ const meta = { options: ['start', 'center', 'end'], }, labelColor: { description: 'A CSS color or GridKit theme color path for the label.', control: 'text' }, - }, + }), render: (args: Args) => { const wrapper = document.createElement('div'); wrapper.style.width = '300px'; diff --git a/libs/web-components/stories/Skeleton.stories.ts b/libs/web-components/stories/Skeleton.stories.ts index c56ad0c..f7c0be9 100644 --- a/libs/web-components/stories/Skeleton.stories.ts +++ b/libs/web-components/stories/Skeleton.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdSkeleton } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick< GdSkeleton, @@ -18,7 +18,7 @@ const meta = { }, }, args: { width: '250px', height: '15px', variant: 'rounded', animationName: 'blinkKeyframes' }, - argTypes: { + argTypes: sectionedArgTypes('Skeleton', { variant: { description: 'Controls the shape of the skeleton.', control: 'select', @@ -33,7 +33,7 @@ const meta = { }, animationProps: { description: 'Animation duration, easing, delay, and iteration string.', control: 'text' }, styles: { description: 'Custom CSS properties applied to the skeleton.', control: 'object' }, - }, + }), render: (args: Args) => element('gd-skeleton', args), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Slider.stories.ts b/libs/web-components/stories/Slider.stories.ts index 2d20590..e8c6803 100644 --- a/libs/web-components/stories/Slider.stories.ts +++ b/libs/web-components/stories/Slider.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdSlider } from '../src'; -import { defaultTokenViewer, element, observed } from './helpers'; +import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -16,14 +16,14 @@ const meta = { }, }, args: { min: 1, max: 100, value: 1, step: 1, disabled: false }, - argTypes: { + argTypes: sectionedArgTypes('Slider', { min: { description: 'Minimum value of the slider range', control: 'number' }, max: { description: 'Maximum value of the slider range', control: 'number' }, value: { description: 'Current slider value', control: 'number' }, step: { description: 'Increment between permitted values', control: 'number' }, disabled: { description: 'Whether the slider is disabled', control: 'boolean' }, styles: { description: 'Custom slider style overrides', control: 'object' }, - }, + }), render: (args: Args) => observed(element('gd-slider', args), 'gd-change', { value: args.value }), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Toggle.stories.ts b/libs/web-components/stories/Toggle.stories.ts index bdda506..d3bf6f8 100644 --- a/libs/web-components/stories/Toggle.stories.ts +++ b/libs/web-components/stories/Toggle.stories.ts @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GridKitIconName } from 'gd-design-core'; import type { GdToggle } from '../src'; -import { defaultTokenViewer, element, observed } from './helpers'; +import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; type Args = Pick; const items = ['Option 1', 'Option 2', 'Option 3']; @@ -18,13 +18,13 @@ const meta = { }, }, args: { items, value: 'Option 1', disabled: false }, - argTypes: { + argTypes: sectionedArgTypes('Toggle', { items: { description: 'String or label/value items', control: 'object' }, value: { description: 'Selected item value', control: 'text' }, disabled: { description: 'Disables all options', control: 'boolean' }, renderItemContent: { description: 'Optional item render function', control: false }, styles: { description: 'Custom group styles', control: 'object' }, - }, + }), render: (args: Args) => observed(element('gd-toggle', args), 'gd-change', { value: args.value }), } satisfies Meta; export default meta; diff --git a/libs/web-components/stories/Truncate.stories.ts b/libs/web-components/stories/Truncate.stories.ts index 5ad7237..a88efd2 100644 --- a/libs/web-components/stories/Truncate.stories.ts +++ b/libs/web-components/stories/Truncate.stories.ts @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdTruncate } from '../src'; import { element, sectionedArgTypes } from './helpers'; -type Args = Pick & { text: string }; +type Args = Pick & { children: string }; const long = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; const meta = { @@ -17,16 +17,16 @@ const meta = { }, }, }, - args: { lines: 1, text: long }, + args: { lines: 1, children: long }, argTypes: sectionedArgTypes('Truncate', { - text: { description: 'Text or slotted content to display', control: 'text' }, + children: { description: 'Text or slotted content to display', control: 'text' }, lines: { description: 'Maximum visible line count', control: 'number' }, styles: { description: 'Custom content styles', control: 'object' }, }), - render: ({ text, ...args }: Args) => { + render: ({ children, ...args }: Args) => { const box = document.createElement('div'); box.style.width = '240px'; - box.append(element('gd-truncate', args, text)); + box.append(element('gd-truncate', args, children)); return box; }, } satisfies Meta; @@ -36,13 +36,13 @@ export const Default: Story = { parameters: { docs: { description: { story: 'One-line truncation within a constrained container.' } } }, }; export const LineTruncation: Story = { - args: { lines: 2, text: 'This is a very long text that will wrap to multiple lines' }, + args: { lines: 2, children: 'This is a very long text that will wrap to multiple lines' }, parameters: { docs: { description: { story: 'CSS line-clamp limits content to two lines.' } } }, }; export const WithCustomStyling: Story = { args: { styles: { color: '#dc2626', fontWeight: 600, fontSize: '18px' }, - text: 'This text has custom styling applied via the styles property.', + children: 'This text has custom styling applied via the styles property.', }, }; export const RefAPIWithTooltipOnOverflow: Story = { diff --git a/libs/web-components/stories/Typography.stories.ts b/libs/web-components/stories/Typography.stories.ts index 451dbe1..4a3f7ce 100644 --- a/libs/web-components/stories/Typography.stories.ts +++ b/libs/web-components/stories/Typography.stories.ts @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdTypography } from '../src'; import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; -type Args = Pick & { text: string }; +type Args = Pick & { children: string }; const variants = [ 'h1', 'h2', @@ -35,6 +35,7 @@ const styleVariants = [ 'underline', 'strike', ] as const; +const typographyElement = ({ children, ...props }: Args, text = children) => element('gd-typography', props, text); const meta = { title: 'Atoms/Typography', tags: ['autodocs'], @@ -53,7 +54,7 @@ const meta = { align: 'start', color: '', styleVariant: [], - text: 'Body paragraph text for comparison.', + children: 'Body paragraph text for comparison.', }, argTypes: sectionedArgTypes('Typography', { variant: { control: 'select', options: variants }, @@ -63,19 +64,19 @@ const meta = { color: { control: 'color' }, styleVariant: { control: 'multi-select', options: styleVariants }, }), - render: ({ text, ...props }: Args) => element('gd-typography', props, text), + render: (args: Args) => typographyElement(args), } satisfies Meta; export default meta; type Story = StoryObj; export const Default: Story = {}; -export const Heading1: Story = { args: { variant: 'h1', as: 'h1', text: 'Heading 1' } }; -export const Heading2: Story = { args: { variant: 'h2', as: 'h2', text: 'Heading 2' } }; +export const Heading1: Story = { args: { variant: 'h1', as: 'h1', children: 'Heading 1' } }; +export const Heading2: Story = { args: { variant: 'h2', as: 'h2', children: 'Heading 2' } }; export const AllVariants: Story = { render: (args) => { const section = document.createElement('section'); section.style.cssText = 'display:grid;gap:8px'; for (const variant of variants) - section.append(element('gd-typography', { ...args, variant, as: variant }, `${variant} — GridKit typography`)); + section.append(typographyElement({ ...args, variant, as: variant }, `${variant} — GridKit typography`)); return section; }, }; @@ -83,7 +84,7 @@ export const DisplaySizes: Story = { render: (args) => { const section = document.createElement('section'); for (const size of ['xl', 'lg', 'md', 'sm', 'xs'] as const) - section.append(element('gd-typography', { ...args, variant: 'div', as: 'div', size }, `Display ${size}`)); + section.append(typographyElement({ ...args, variant: 'div', as: 'div', size }, `Display ${size}`)); return section; }, }; @@ -92,12 +93,9 @@ export const StyleVariants: Story = { const section = document.createElement('section'); section.style.cssText = 'display:grid;gap:8px'; for (const styleVariant of styleVariants) - section.append( - element('gd-typography', { ...args, variant: 'p', as: 'p', styleVariant }, `${styleVariant} text`) - ); + section.append(typographyElement({ ...args, variant: 'p', as: 'p', styleVariant }, `${styleVariant} text`)); section.append( - element( - 'gd-typography', + typographyElement( { ...args, variant: 'p', as: 'p', styleVariant: ['bold', 'italic', 'underline'] }, 'Combined: bold + italic + underline' ) @@ -106,22 +104,22 @@ export const StyleVariants: Story = { }, }; export const SemanticOverride: Story = { - args: { variant: 'h2', as: 'h3', text: 'H2 appearance rendered as an H3' }, + args: { variant: 'h2', as: 'h3', children: 'H2 appearance rendered as an H3' }, }; export const CodeAndKeyboard: Story = { render: (args) => { const section = document.createElement('section'); section.style.cssText = 'display:grid;gap:12px'; - section.append(element('gd-typography', { ...args, variant: 'code', as: 'code' }, 'const gridkit = true;')); - section.append(element('gd-typography', { ...args, variant: 'kbd', as: 'kbd' }, '⌘ + K')); + section.append(typographyElement({ ...args, variant: 'code', as: 'code' }, 'const gridkit = true;')); + section.append(typographyElement({ ...args, variant: 'kbd', as: 'kbd' }, '⌘ + K')); return section; }, }; export const CaptionAndHeader: Story = { render: (args) => { const section = document.createElement('section'); - section.append(element('gd-typography', { ...args, variant: 'caption', as: 'span' }, 'Caption text')); - section.append(element('gd-typography', { ...args, variant: 'header', as: 'header' }, 'Header text')); + section.append(typographyElement({ ...args, variant: 'caption', as: 'span' }, 'Caption text')); + section.append(typographyElement({ ...args, variant: 'header', as: 'header' }, 'Header text')); return section; }, }; @@ -131,6 +129,8 @@ export const CombinedStyleVariant: Story = StyleVariants; export const Disclaimers: Story = CaptionAndHeader; export const Display: Story = DisplaySizes; export const Heading: Story = AllVariants; -export const Body: Story = { args: { variant: 'p', as: 'p', text: 'Body paragraph text' } }; -export const WithAccessibility: Story = { args: { variant: 'h2', as: 'h2', text: 'Accessible section heading' } }; +export const Body: Story = { args: { variant: 'p', as: 'p', children: 'Body paragraph text' } }; +export const WithAccessibility: Story = { + args: { variant: 'h2', as: 'h2', children: 'Accessible section heading' }, +}; export const DefaultTokens: Story = { render: () => defaultTokenViewer('typography') }; diff --git a/libs/web-components/stories/Wrapper.stories.ts b/libs/web-components/stories/Wrapper.stories.ts index 7633fa0..e75578c 100644 --- a/libs/web-components/stories/Wrapper.stories.ts +++ b/libs/web-components/stories/Wrapper.stories.ts @@ -1,8 +1,8 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdWrapper } from '../src'; -import { defaultTokenViewer, element } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; -type Args = Pick & { text: string }; +type Args = Pick & { children: string }; const meta = { title: 'Atoms/Wrapper', tags: ['autodocs'], @@ -15,14 +15,14 @@ const meta = { }, }, }, - args: { variant: 'inline', as: 'div', text: 'This is a basic wrapper.' }, - argTypes: { + args: { variant: 'inline', as: 'div', children: 'This is a basic wrapper.' }, + argTypes: sectionedArgTypes('Wrapper', { variant: { description: 'Layout context', control: 'select', options: ['inline', 'section', 'fullPage'] }, as: { description: 'Internal semantic HTML tag', control: 'text' }, - text: { description: 'Slotted content', control: 'text' }, + children: { description: 'Slotted content', control: 'text' }, styles: { description: 'Custom wrapper styles', control: 'object' }, - }, - render: ({ text, ...args }: Args) => element('gd-wrapper', args, text), + }), + render: ({ children, ...args }: Args) => element('gd-wrapper', args, children), } satisfies Meta; export default meta; type Story = StoryObj; @@ -34,7 +34,7 @@ export const SectionWrapper: Story = { name: 'Variant: Section', args: { variant: 'section', - text: 'This content is semantically grouped within a section element.', + children: 'This content is semantically grouped within a section element.', styles: { padding: '20px', width: '400px', border: '1px solid #ccc', backgroundColor: '#f9f9f9' }, }, }; @@ -42,7 +42,7 @@ export const InlineWrapper: Story = { name: 'Variant: Inline (Span)', args: { variant: 'inline', - text: 'this part is wrapped in a span', + children: 'this part is wrapped in a span', styles: { padding: '4px 8px', borderRadius: '4px', @@ -56,14 +56,14 @@ export const FullPageWrapper: Story = { name: 'Variant: Full Page', args: { variant: 'fullPage', - text: 'This wrapper covers the entire viewport.', + children: 'This wrapper covers the entire viewport.', styles: { display: 'flex', alignItems: 'center', justifyContent: 'center' }, }, parameters: { layout: 'fullscreen' }, }; export const CustomTagWrapper: Story = { name: 'Custom tag Wrapper', - args: { as: 'section', text: 'Custom tag Wrapper' }, + args: { as: 'section', children: 'Custom tag Wrapper' }, }; export const WithAccessibility: Story = { args: { 'aria-label': 'Content wrapper' } as never, diff --git a/libs/web-components/stories/helpers.ts b/libs/web-components/stories/helpers.ts index 453d81a..cfd609e 100644 --- a/libs/web-components/stories/helpers.ts +++ b/libs/web-components/stories/helpers.ts @@ -1,6 +1,8 @@ import { defaultTheme } from 'gd-design-library/tokens'; import type { DesignCoreTheme } from 'gd-design-core'; +import type { ArgTypes as StorybookArgTypes } from '@storybook/web-components-vite'; import type {} from '../src/index'; +import { reactStorybookArgTypes } from './react-storybook-arg-types.generated'; type Tag = Extract; @@ -9,99 +11,22 @@ type ArgType = { [key: string]: unknown; }; -const reactArgTypeSections = { - Avatar: { badgeColor: 'Badge', backgroundColor: 'Appearance' }, - Badge: { variant: 'Visual Style', appearance: 'Visual Style', size: 'Visual Style', disabled: 'Visual Style' }, - Box: { - variant: 'Core Props', - isBordered: 'Visual Style', - isHighlighted: 'Visual Style', - withShadowHover: 'Visual Style', - }, - Button: { variant: 'Visual Style', rounded: 'Visual Style' }, - Checkbox: { size: 'Layout' }, - Icon: { name: 'Icon Selection', size: 'Size & Dimensions', fill: 'Styling & Colors' }, - Image: { - src: 'Core Properties', - alt: 'Core Properties', - width: 'Dimensions', - height: 'Dimensions', - caption: 'Content & Display', - objectFit: 'Styling', - }, - Input: { variant: 'Core Properties', color: 'Appearance & Styling' }, - InputFile: { - accept: 'File Handling', - capture: 'Mobile Features', - multiple: 'File Handling', - disabled: 'State', - isIcon: 'Button Customization', - }, - Label: { htmlFor: 'Core Properties', styles: 'Styling' }, - Link: { - variant: 'Core Properties', - size: 'Appearance', - underline: 'Appearance', - cursor: 'Appearance', - disabled: 'State & Behavior', - href: 'Core Properties', - target: 'Navigation & Behavior', - rel: 'Navigation & Behavior', - styles: 'Styling', - }, - Loader: { - name: 'Appearance', - variant: 'Layout & Positioning', - size: 'Appearance', - rounded: 'Appearance', - withWrapper: 'Layout & Positioning', - animationProps: 'Content & Customization', - styles: 'Custom Styling', - }, - Menu: { placement: 'Menu Options (Positioning & Dimensions)' }, - SliderDots: { count: 'Core Properties', activeIndex: 'Core Properties' }, - Switch: { checked: 'State', disabled: 'State', isLoading: 'State', label: 'Layout', name: 'Identification' }, - Textarea: { - name: 'Basics', - placeholder: 'Basics', - value: 'Basics', - defaultValue: 'Basics', - minHeight: 'Sizing & Layout', - maxHeight: 'Sizing & Layout', - disabled: 'Accessibility', - readOnly: 'Accessibility', - autoFocus: 'Accessibility', - resize: 'Behavior', - rows: 'Sizing & Layout', - dynamicHeightAdjustment: 'Behavior', - variant: 'Styling', - color: 'Styling', - maxLength: 'Behavior', - styles: 'Styling', - }, - Truncate: { lines: 'Truncation', styles: 'Appearance' }, - Typography: { - variant: 'Appearance', - as: 'Behavior', - size: 'Appearance', - align: 'Layout', - color: 'Appearance', - styleVariant: 'Appearance', - }, -} as const; - -/** Preserve the semantic control sections defined by the corresponding React story. */ +/** Preserve the complete control table defined by the corresponding React story. */ export function sectionedArgTypes< - Component extends keyof typeof reactArgTypeSections, + Component extends keyof typeof reactStorybookArgTypes, ArgTypes extends Record, ->(component: Component, argTypes: ArgTypes): ArgTypes { - const sections = reactArgTypeSections[component] as Partial>; +>(component: Component, _argTypes: ArgTypes): StorybookArgTypes { + void _argTypes; + const source = reactStorybookArgTypes[component] as Record; return Object.fromEntries( - Object.entries(argTypes).map(([name, argType]) => { - const category = sections[name]; - return [name, category ? { ...argType, table: { ...argType.table, category } } : argType]; + Object.keys(source).map((name) => { + const sourceArgType = source[name]; + return [ + name, + sourceArgType.control === undefined ? { ...sourceArgType, control: { disable: true } } : sourceArgType, + ]; }) - ) as ArgTypes; + ) as StorybookArgTypes; } const sourceKeyProperties: Partial> = { diff --git a/libs/web-components/stories/react-storybook-arg-types.generated.ts b/libs/web-components/stories/react-storybook-arg-types.generated.ts new file mode 100644 index 0000000..60d8008 --- /dev/null +++ b/libs/web-components/stories/react-storybook-arg-types.generated.ts @@ -0,0 +1,7878 @@ +/* This file is generated from the React Storybook metadata. Do not edit it by hand. */ +export const reactStorybookArgTypes = { + Toggle: { + items: { + control: { + type: 'object', + disable: false, + }, + name: 'items', + type: { + name: 'union', + value: [ + { + raw: 'string[]', + name: 'array', + value: [ + { + name: 'string', + }, + ], + }, + { + raw: 'ToggleItem[]', + name: 'array', + value: [ + { + name: 'other', + value: 'ToggleItem', + }, + ], + }, + ], + required: false, + raw: 'string[] | ToggleItem[]', + }, + description: 'List of items to toggle between', + table: { + type: { + summary: 'Array', + }, + defaultValue: { + summary: '[]', + }, + }, + }, + value: { + control: { + type: 'text', + disable: false, + }, + name: 'value', + type: { + name: 'other', + required: false, + value: 'unknown', + }, + description: 'Currently selected value', + table: { + type: { + summary: 'string | unknown', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + onValueChange: { + name: 'onValueChange', + type: { + name: 'function', + required: false, + raw: '(value: T) => void', + }, + description: 'Function triggered when the value changes', + table: { + type: { + summary: '(value: unknown) => void', + }, + defaultValue: { + summary: 'undefined', + }, + }, + action: 'value changed', + }, + disabled: { + control: { + type: 'boolean', + disable: false, + }, + name: 'disabled', + description: 'Disables interaction with the component', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + }, + }, + renderItemContent: { + name: 'renderItemContent', + description: 'Custom render function for each item', + type: { + required: false, + raw: '(item: ToggleItem | string, index: number) => ReactNode', + name: 'function', + }, + table: { + type: { + summary: '(item: unknown) => ReactNode', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + styles: { + name: 'styles', + description: 'Custom styles for the component', + table: { + type: { + summary: 'InlineBoxStyles', + }, + defaultValue: { + summary: '{}', + }, + }, + }, + }, + Truncate: { + children: { + control: { + type: 'text', + disable: true, + }, + name: 'children', + type: { + name: 'string', + }, + table: { + category: 'Content', + }, + description: 'ReactNode content to display. Accepts string text or any ReactNode for flexible content.', + }, + lines: { + control: { + type: 'number', + disable: false, + }, + name: 'lines', + description: 'Number of lines before truncation (default: 1)', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '1', + }, + category: 'Truncation', + }, + }, + styles: { + control: { + type: 'object', + disable: false, + }, + name: 'styles', + description: + 'Custom CSS styles object for one-off styling needs. Accepts Emotion CSS object syntax with support for nested selectors, pseudo-classes, and media queries.', + type: { + required: false, + }, + table: { + defaultValue: { + summary: '{}', + }, + category: 'Appearance', + }, + }, + ref: { + name: 'ref', + description: + 'Forwarded ref providing imperative access to truncation state. Exposes `isTruncated` (boolean) indicating if content is truncated. Useful for conditionally showing tooltips or for analytics.', + table: { + category: 'Ref API', + type: { + summary: 'Ref', + detail: 'TruncateRef: { ref: Ref; isTruncated: boolean; }', + }, + }, + control: { + disable: true, + }, + }, + }, + Wrapper: { + variant: { + control: { + type: 'select', + disable: false, + }, + name: 'variant', + type: { + name: 'other', + required: false, + raw: 'EnumOrPrimitive', + value: 'EnumOrPrimitive', + }, + description: 'Determines the semantic HTML element and associated styles for different layout contexts.', + table: { + type: { + summary: 'WrapperVariant', + }, + defaultValue: { + summary: 'inline', + }, + }, + options: ['Inline', 'Section', 'FullPage'], + }, + children: { + control: { + type: 'text', + disable: false, + }, + name: 'children', + type: { + name: 'string', + }, + description: 'Content to be rendered inside the wrapper. Can be text, components, or other elements.', + table: { + type: { + summary: 'ReactNode', + }, + }, + }, + styles: { + control: { + type: 'object', + disable: false, + }, + name: 'styles', + type: { + name: 'object', + value: { + padding: { + name: 'string', + }, + border: { + name: 'string', + }, + borderRadius: { + name: 'string', + }, + backgroundColor: { + name: 'string', + }, + }, + }, + description: 'Custom CSS styles object to override or extend default wrapper styling.', + table: { + type: { + summary: 'CSSProperties', + }, + }, + }, + as: { + control: { + type: 'text', + disable: false, + }, + name: 'as', + description: 'Overrides the default wrapper HTML element used for rendering (e.g., "div", "section", "article").', + type: { + required: false, + raw: 'keyof HTMLElementTagNameMap | ElementType', + name: 'union', + value: [ + { + name: 'other', + value: 'HTMLElementTagNameMap', + }, + { + name: 'other', + value: 'ElementType', + }, + ], + }, + table: { + type: { + summary: 'keyof HTMLElementTagNameMap | ElementType', + }, + }, + }, + className: { + name: 'className', + description: 'Additional CSS class names to be applied to the wrapper element.', + table: { + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + display: { + name: 'display', + options: ['block', 'flex', 'inline', 'inline-flex', 'grid'], + description: 'CSS display property to control the layout behavior of the wrapper.', + table: { + type: { + summary: 'string', + }, + }, + control: { + type: 'select', + disable: false, + }, + }, + position: { + name: 'position', + options: ['static', 'relative', 'absolute', 'fixed', 'sticky'], + description: 'CSS position property to control the positioning context of the wrapper.', + table: { + type: { + summary: 'string', + }, + }, + control: { + type: 'select', + disable: false, + }, + }, + }, + Slider: { + min: { + control: { + type: 'number', + }, + name: 'min', + type: { + name: 'number', + required: false, + }, + description: 'Minimum value of the slider range', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '1', + }, + }, + }, + max: { + control: { + type: 'number', + }, + name: 'max', + type: { + name: 'number', + required: false, + }, + description: 'Maximum value of the slider range', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '100', + }, + }, + }, + disabled: { + control: { + type: 'boolean', + }, + name: 'disabled', + type: { + name: 'boolean', + required: false, + }, + description: 'Whether the slider is disabled', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + }, + }, + styles: { + control: { + type: 'object', + }, + name: 'styles', + type: { + name: 'object', + value: {}, + }, + description: 'Custom styles to apply to the slider component', + table: { + type: { + summary: 'React.CSSProperties', + }, + defaultValue: { + summary: '{}', + }, + }, + }, + value: { + control: { + type: 'number', + }, + name: 'value', + type: { + name: 'number', + required: false, + }, + description: 'Current value of the slider', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '1', + }, + }, + }, + onChange: { + name: 'onChange', + type: { + name: 'function', + required: false, + raw: '(value: number) => void', + }, + description: 'Callback function triggered when slider value changes', + table: { + type: { + summary: '(value: number) => void', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + step: { + control: { + type: 'number', + }, + name: 'step', + description: '', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + }, + }, + }, + SliderDots: { + count: { + control: { + type: 'number', + disable: false, + }, + name: 'count', + type: { + name: 'number', + required: true, + }, + description: 'Total number of dots to render', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '0', + }, + category: 'Core Properties', + }, + }, + activeIndex: { + control: { + type: 'number', + disable: false, + }, + name: 'activeIndex', + type: { + name: 'number', + required: false, + }, + description: 'Index of the currently active dot (zero-based)', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '0', + }, + category: 'Core Properties', + }, + }, + onDotClick: { + name: 'onDotClick', + type: { + name: 'function', + required: false, + raw: '(index: number) => void', + }, + description: 'Callback fired when a dot is clicked, receives the dot index', + table: { + type: { + summary: '(index: number) => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + action: 'Dot clicked', + }, + }, + Switch: { + name: { + control: { + type: 'text', + }, + name: 'name', + type: { + name: 'string', + required: false, + }, + description: 'Name attribute of the switch input', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'switch', + }, + category: 'Identification', + }, + }, + disabled: { + control: { + type: 'boolean', + disable: false, + }, + name: 'disabled', + type: { + name: 'boolean', + required: false, + }, + description: 'Disables the switch interaction', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'State', + }, + }, + children: { + control: { + type: 'text', + }, + name: 'children', + type: { + name: 'string', + }, + description: 'Label content for the switch', + table: { + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: 'Label', + }, + category: 'Content', + }, + }, + onValueChange: { + name: 'onValueChange', + type: { + name: 'function', + required: false, + raw: '(value: boolean) => void', + }, + description: 'Callback fired when the switch value changes', + table: { + type: { + summary: '(checked: boolean) => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + action: 'Switch value changed', + }, + label: { + control: { + type: 'radio', + }, + options: ['left ', ' right'], + name: 'label', + description: 'Position of the label relative to the switch', + type: { + required: false, + raw: "'left' | 'right'", + name: 'enum', + value: ['left', 'right'], + }, + table: { + type: { + summary: 'LabelPosition', + detail: 'left | right', + }, + defaultValue: { + summary: 'right', + }, + category: 'Layout', + }, + }, + isLoading: { + control: { + type: 'boolean', + disable: false, + }, + name: 'isLoading', + description: 'Shows loading state and disables the switch', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'State', + }, + }, + checked: { + control: { + type: 'boolean', + disable: false, + }, + name: 'checked', + description: 'Controls the checked state of the switch', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'State', + }, + }, + }, + Textarea: { + name: { + control: { + type: 'text', + disable: false, + }, + name: 'name', + type: { + name: 'string', + }, + description: 'Input field name attribute', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'textarea', + }, + category: 'Basics', + }, + }, + placeholder: { + control: { + type: 'text', + disable: false, + }, + name: 'placeholder', + type: { + name: 'string', + }, + description: 'Placeholder text', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Basics', + }, + }, + defaultValue: { + control: { + type: 'text', + disable: false, + }, + name: 'defaultValue', + type: { + name: 'string', + }, + description: 'Initial textarea content', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Basics', + }, + }, + onChange: { + name: 'onChange', + type: { + name: 'function', + }, + action: 'changed', + description: 'Callback when textarea content changes', + table: { + type: { + summary: '(event: ChangeEvent) => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + }, + resize: { + control: { + type: 'select', + disable: false, + }, + name: 'resize', + description: 'Controls how the textarea can be resized', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'TextareaResize', + }, + defaultValue: { + summary: 'TextareaResize.None', + }, + category: 'Behavior', + }, + options: ['none', 'both', 'horizontal', 'vertical'], + }, + variant: { + control: { + type: 'select', + disable: false, + }, + options: ['default', 'inline'], + name: 'variant', + description: 'Visual variant of the textarea', + type: { + required: false, + raw: "'default' | 'inline'", + name: 'enum', + value: ['default', 'inline'], + }, + table: { + type: { + summary: 'TextareaVariant', + }, + defaultValue: { + summary: "'default'", + }, + category: 'Styling', + }, + }, + color: { + control: { + type: 'select', + disable: false, + }, + name: 'color', + description: 'Color variant of the textarea', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'InputColorVariant', + }, + defaultValue: { + summary: "'primary'", + }, + category: 'Styling', + }, + options: ['primary', 'success', 'warning', 'error'], + }, + ariaDescribedBy: { + control: { + type: 'text', + disable: false, + }, + name: 'ariaDescribedBy', + description: 'ID of element that describes this textarea', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Accessibility', + }, + }, + dynamicHeightAdjustment: { + control: { + type: 'boolean', + disable: false, + }, + name: 'dynamicHeightAdjustment', + description: 'Enable/disable dynamic height adjustment', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Behavior', + }, + }, + onCustomResize: { + name: 'onCustomResize', + description: 'Callback triggered when textarea is manually resized by user', + type: { + required: false, + raw: '(newSize: { height: number; width: number }) => void', + name: 'function', + }, + table: { + type: { + summary: '(newSize: { height: number; width: number }) => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + action: 'resized', + }, + minHeight: { + control: { + type: 'text', + disable: false, + }, + name: 'minHeight', + description: 'Minimum height of textarea', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Sizing & Layout', + }, + }, + maxHeight: { + control: { + type: 'text', + disable: false, + }, + name: 'maxHeight', + description: 'Maximum height of textarea', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Sizing & Layout', + }, + }, + maxCharacters: { + control: { + type: 'number', + }, + name: 'maxCharacters', + description: '', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + }, + }, + autoFocus: { + control: { + type: 'boolean', + disable: false, + }, + name: 'autoFocus', + description: 'Whether the textarea should auto focus', + type: { + required: false, + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Accessibility', + }, + }, + value: { + name: 'value', + description: 'Controlled value of the textarea', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Basics', + }, + control: { + type: 'text', + disable: false, + }, + }, + disabled: { + name: 'disabled', + description: 'Whether the textarea is disabled', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Accessibility', + }, + control: { + type: 'boolean', + disable: false, + }, + }, + readOnly: { + name: 'readOnly', + description: 'Whether the textarea is read only', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Accessibility', + }, + control: { + type: 'boolean', + disable: false, + }, + }, + rows: { + name: 'rows', + description: 'Number of rows', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Sizing & Layout', + }, + control: { + type: 'number', + disable: false, + }, + }, + maxLength: { + name: 'maxLength', + description: 'Maximum character limit', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Behavior', + }, + control: { + type: 'number', + disable: false, + }, + }, + styles: { + name: 'styles', + description: 'Custom CSS styles to override default styling', + table: { + type: { + summary: 'CSSProperties', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Styling', + }, + control: { + type: 'object', + disable: false, + }, + }, + }, + Separator: { + orientation: { + control: { + type: 'select', + disable: false, + }, + name: 'orientation', + description: 'The orientation of the separator.', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'Orientation', + }, + defaultValue: { + summary: 'horizontal', + }, + }, + options: ['vertical', 'horizontal'], + }, + length: { + control: { + type: 'text', + disable: false, + }, + name: 'length', + description: 'The length of the separator (e.g., "100px", "50%").', + type: { + required: false, + name: 'other', + value: 'literal', + }, + table: { + type: { + summary: '`${number}${Unit}`', + }, + }, + }, + color: { + control: { + type: 'text', + disable: false, + }, + name: 'color', + description: + 'The color of the separator line. Accepts any valid CSS color value (hex, rgb, named colors) or theme token path / palette-style alias (e.g., "border.error", "text.secondary", "brand.500", "theme.palette.warning.main").', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + }, + }, + size: { + control: { + type: 'select', + disable: false, + }, + name: 'size', + description: 'The thickness/size of the separator line.', + type: { + required: false, + name: 'other', + value: 'SizeVariant', + }, + table: { + type: { + summary: 'SizeVariant', + }, + }, + options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'], + }, + variant: { + control: { + type: 'select', + disable: false, + }, + name: 'variant', + description: 'The visual style variant of the separator (e.g., "solid", "dashed").', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'SeparatorVariant', + }, + }, + options: ['solid', 'dashed', 'dotted'], + }, + as: { + control: { + type: 'select', + disable: false, + }, + options: ['div', 'hr', 'span'], + name: 'as', + description: 'The HTML element to render the separator as.', + type: { + required: false, + raw: "'div' | 'hr' | 'span'", + name: 'enum', + value: ['div', 'hr', 'span'], + }, + table: { + type: { + summary: "'div' | 'hr' | 'span'", + }, + defaultValue: { + summary: 'div', + }, + }, + }, + label: { + control: { + type: 'text', + disable: false, + }, + name: 'label', + description: 'Text to display within the separator.', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + }, + }, + labelPosition: { + control: { + type: 'select', + disable: false, + }, + name: 'labelPosition', + description: 'The position of the label when present.', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'SeparatorLabelPosition', + }, + defaultValue: { + summary: 'center', + }, + }, + options: ['start', 'center', 'end'], + }, + labelColor: { + control: { + type: 'text', + disable: false, + }, + name: 'labelColor', + description: + 'The color of the label text. Accepts any valid CSS color value (hex, rgb, named colors) or theme token path / palette-style alias (e.g., "text.caption", "text.secondary", "brand.500").', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + }, + }, + styles: { + name: 'styles', + description: 'Custom styles for the separator.', + table: { + type: { + summary: 'CSSProperties', + }, + }, + subControls: { + backgroundColor: { + control: { + type: 'color', + }, + description: 'Background color of the separator.', + table: { + type: { + summary: 'string', + }, + }, + }, + border: { + control: { + type: 'text', + }, + description: 'Border style of the separator.', + table: { + type: { + summary: 'string', + }, + }, + }, + margin: { + control: { + type: 'text', + }, + description: 'Margin around the separator.', + table: { + type: { + summary: 'string', + }, + }, + }, + padding: { + control: { + type: 'text', + }, + description: 'Padding within the separator.', + table: { + type: { + summary: 'string', + }, + }, + }, + }, + control: { + type: 'object', + disable: false, + }, + }, + }, + Skeleton: { + width: { + control: { + type: 'text', + disable: false, + }, + name: 'width', + type: { + name: 'string', + required: false, + }, + description: 'Sets the width of the skeleton. Accepts any valid CSS width value.', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'auto', + }, + }, + }, + height: { + control: { + type: 'text', + disable: false, + }, + name: 'height', + type: { + name: 'string', + required: false, + }, + description: 'Sets the height of the skeleton. Accepts any valid CSS height value.', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'auto', + }, + }, + }, + variant: { + control: { + type: 'select', + disable: false, + }, + name: 'variant', + type: { + name: 'other', + required: false, + raw: 'EnumOrPrimitive', + value: 'EnumOrPrimitive', + }, + description: 'Controls the shape of the skeleton.', + table: { + type: { + summary: 'select', + }, + defaultValue: { + summary: 'rounded', + }, + }, + options: ['rectangular', 'rounded', 'circular'], + }, + animationName: { + control: { + type: 'text', + disable: false, + }, + name: 'animationName', + description: + 'Overrides the default animation keyframe name. Accepts theme animation token names such as "blinkKeyframes", raw CSS animation names, or `null` to disable the built-in animation.', + type: { + required: false, + raw: 'NullableType', + name: 'other', + value: 'NullableType', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'blinkKeyframes', + }, + }, + }, + animationProps: { + control: { + type: 'text', + disable: false, + }, + name: 'animationProps', + description: + 'Custom animation timing string appended to `animationName` (for example "1200ms ease-in-out 0ms infinite").', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + }, + }, + backgroundColor: { + control: { + type: 'text', + disable: false, + }, + name: 'backgroundColor', + description: + 'Sets the skeleton fill color. Accepts any valid CSS color value (hex, rgb, named colors) or theme token path / palette-style alias (e.g., "bg.fill.success.primary.default", "brand.500", "theme.palette.success.main").', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + }, + }, + className: { + name: 'className', + description: 'Provides a way to apply custom CSS classes, such as Tailwind utility classes for animations.', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: '', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + children: { + name: 'children', + description: 'Content to be rendered inside the skeleton wrapper.', + table: { + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: 'undefined', + }, + }, + control: { + type: 'object', + disable: false, + }, + }, + styles: { + name: 'styles', + description: 'Custom CSS properties to apply to the skeleton.', + table: { + type: { + summary: 'CSSProperties', + }, + defaultValue: { + summary: '{}', + }, + }, + subControls: { + backgroundColor: { + control: 'color', + description: 'Background color of the skeleton.', + table: { + type: { + summary: 'string', + }, + }, + }, + borderRadius: { + control: 'text', + description: 'Border radius of the skeleton.', + table: { + type: { + summary: 'string', + }, + }, + }, + opacity: { + control: 'number', + description: 'Opacity of the skeleton.', + table: { + type: { + summary: 'number', + }, + }, + }, + }, + control: { + type: 'object', + disable: false, + }, + }, + }, + Icon: { + name: { + name: 'name', + description: 'The name of the icon to display from the registered list.', + type: { + required: true, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Icon Selection', + }, + options: [ + 'cross', + 'success', + 'error', + 'warning', + 'info', + 'dot', + 'check', + 'arrowDown', + 'arrowRight', + 'arrowLeft', + 'mobileMenu', + 'home', + 'slash', + 'arrowForward', + 'localShipping', + 'favorite', + 'favoriteOutlined', + 'deleteOutlined', + 'accountCircle', + 'shoppingBag', + 'errorOutline', + 'star', + 'starOutlined', + 'starHalf', + 'minus', + 'plus', + 'filter', + 'ruler', + 'processing', + 'paymentCard', + 'eye', + 'attachment', + 'upload', + 'folder', + 'folderOpen', + 'wifiTethering', + 'portrait', + 'search', + 'edit', + 'volumeUp', + 'contentCopy', + 'thumbUp', + 'thumbDown', + 'fileCopy', + 'keyboardArrowDown', + 'keyboardArrowUp', + 'send', + 'thumbUpFilled', + 'thumbDownFilled', + 'chat', + 'chatBubble', + 'phone', + 'mic', + 'fullscreen', + 'fullscreenExit', + ], + control: { + type: 'select', + disable: false, + }, + }, + width: { + name: 'width', + description: 'Sets the width of the icon in pixels.', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Size & Dimensions', + }, + control: { + type: 'number', + disable: false, + }, + }, + height: { + name: 'height', + description: 'Sets the height of the icon in pixels.', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Size & Dimensions', + }, + control: { + type: 'number', + disable: false, + }, + }, + fill: { + name: 'fill', + description: 'Sets the `fill` color for specific paths within the SVG that are designed to inherit it.', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'currentColor', + }, + category: 'Styling & Colors', + }, + control: { + type: 'color', + disable: false, + }, + }, + fillSvg: { + name: 'fillSvg', + description: 'Sets the `fill` color of the SVG element itself. Useful for single-color icons.', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'none', + }, + category: 'Styling & Colors', + }, + control: { + type: 'color', + disable: false, + }, + }, + onClick: { + name: 'onClick', + description: 'Event handler for click events on the icon.', + type: { + required: false, + raw: 'MouseEventHandler', + name: 'other', + value: 'MouseEventHandler', + }, + table: { + type: { + summary: 'MouseEventHandler', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Interactions', + }, + action: 'clicked', + }, + size: { + name: 'size', + description: 'Predefined size variants that set both width and height according to the theme configuration.', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'SizeVariant', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Size & Dimensions', + }, + options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'], + control: { + type: 'select', + disable: false, + }, + }, + minWidth: { + name: 'minWidth', + description: 'CSS min-width property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + maxWidth: { + name: 'maxWidth', + description: 'CSS max-width property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + minHeight: { + name: 'minHeight', + description: 'CSS min-height property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + maxHeight: { + name: 'maxHeight', + description: 'CSS max-height property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + margin: { + name: 'margin', + description: 'CSS margin property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginTop: { + name: 'marginTop', + description: 'CSS margin-top property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginRight: { + name: 'marginRight', + description: 'CSS margin-right property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginBottom: { + name: 'marginBottom', + description: 'CSS margin-bottom property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginLeft: { + name: 'marginLeft', + description: 'CSS margin-left property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + padding: { + name: 'padding', + description: 'CSS padding property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingTop: { + name: 'paddingTop', + description: 'CSS padding-top property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingRight: { + name: 'paddingRight', + description: 'CSS padding-right property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingBottom: { + name: 'paddingBottom', + description: 'CSS padding-bottom property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingLeft: { + name: 'paddingLeft', + description: 'CSS padding-left property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexDirection: { + name: 'flexDirection', + description: 'CSS flex-direction property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + justifyContent: { + name: 'justifyContent', + description: 'CSS justify-content property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignItems: { + name: 'alignItems', + description: 'CSS align-items property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignContent: { + name: 'alignContent', + description: 'CSS align-content property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexWrap: { + name: 'flexWrap', + description: 'CSS flex-wrap property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + gap: { + name: 'gap', + description: 'CSS gap property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flex: { + name: 'flex', + description: 'CSS flex property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexGrow: { + name: 'flexGrow', + description: 'CSS flex-grow property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexShrink: { + name: 'flexShrink', + description: 'CSS flex-shrink property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexBasis: { + name: 'flexBasis', + description: 'CSS flex-basis property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignSelf: { + name: 'alignSelf', + description: 'CSS align-self property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + justifySelf: { + name: 'justifySelf', + description: 'CSS justify-self property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + order: { + name: 'order', + description: 'CSS order property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + position: { + name: 'position', + description: 'CSS position property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + top: { + name: 'top', + description: 'CSS top property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + right: { + name: 'right', + description: 'CSS right property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + bottom: { + name: 'bottom', + description: 'CSS bottom property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + left: { + name: 'left', + description: 'CSS left property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + overflow: { + name: 'overflow', + description: 'CSS overflow property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + zIndex: { + name: 'zIndex', + description: 'CSS z-index property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + border: { + name: 'border', + description: 'CSS border property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + styles: { + name: 'styles', + description: 'Custom styles object for the icon', + table: { + category: 'Custom Styling', + type: { + summary: 'CSSProperties', + }, + }, + control: { + type: 'object', + disable: false, + }, + }, + }, + InputFile: { + accept: { + control: { + type: 'select', + disable: false, + }, + name: 'accept', + type: { + name: 'string', + value: {}, + required: false, + }, + description: 'Accepted file types (MIME types or file extensions)', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'File Handling', + }, + options: ['image/*', 'video/*', 'audio/*', '.pdf', '.doc,.docx', '.txt'], + }, + capture: { + control: { + type: 'select', + disable: false, + }, + name: 'capture', + type: { + name: 'union', + value: [ + { + name: 'boolean', + }, + { + name: 'other', + value: 'literal', + }, + { + name: 'other', + value: 'literal', + }, + ], + required: false, + raw: "boolean | 'user' | 'environment'", + }, + description: 'Preferred media capture method on mobile devices', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Mobile Features', + }, + options: ['user', 'environment'], + }, + multiple: { + control: { + type: 'boolean', + disable: false, + }, + name: 'multiple', + type: { + name: 'boolean', + required: false, + }, + description: 'Allow selection of multiple files', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'File Handling', + }, + }, + disabled: { + control: { + type: 'boolean', + disable: false, + }, + name: 'disabled', + type: { + name: 'boolean', + required: false, + }, + description: 'Disable the input and button', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'State', + }, + }, + onClick: { + name: 'onClick', + type: { + name: 'function', + }, + action: 'button clicked', + description: 'Called when the button is clicked', + table: { + disable: true, + type: { + summary: '(event: MouseEvent) => void', + }, + category: 'Events', + }, + }, + onChange: { + name: 'onChange', + type: { + name: 'function', + required: false, + raw: '(event: ChangeEvent) => void', + }, + description: 'Called when files are selected', + table: { + type: { + summary: '(event: ChangeEvent) => void', + }, + disable: true, + category: 'Events', + }, + action: 'file(s) selected', + }, + buttonProps: { + control: { + type: 'object', + disable: false, + }, + name: 'buttonProps', + description: 'Props to customize the Button component', + type: { + required: false, + name: 'other', + value: 'ButtonProps', + }, + table: { + type: { + summary: 'ButtonProps', + detail: + "ButtonProps {\n variant?: 'text' | 'outlined' | 'contained';\n color?: 'primary' | 'secondary' | 'error';\n iconStart?: ReactNode;\n iconEnd?: ReactNode;\n onClick?: (event: MouseEvent) => void;\n type?: 'button' | 'submit' | 'reset';\n disabled?: boolean;\n fullWidth?: boolean;\n isIcon?: boolean;\n ariaLabel?: string;\n ariaPressed?: boolean;\n role?: 'button' | 'menuitem' | 'switch';\n tabIndex?: number;\n }", + }, + defaultValue: { + summary: '{}', + }, + category: 'Button Customization', + }, + }, + isIcon: { + control: { + type: 'boolean', + disable: false, + }, + name: 'isIcon', + description: 'Render the button as an icon-only button (no text label, square sizing)', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Button Customization', + }, + }, + styles: { + control: { + type: 'object', + disable: false, + }, + name: 'styles', + description: 'CSS style overrides for the wrapper element', + type: { + required: false, + }, + table: { + type: { + summary: 'CSSProperties', + }, + defaultValue: { + summary: '{}', + }, + category: 'Styling', + }, + }, + children: { + control: { + type: 'text', + disable: false, + }, + name: 'children', + description: 'Content for the button label (text or components)', + type: { + required: false, + }, + table: { + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: 'Browse Files', + }, + category: 'Content', + }, + }, + }, + Label: { + children: { + control: { + type: 'text', + disable: false, + }, + name: 'children', + type: { + name: 'string', + }, + description: 'Content to be rendered inside the label', + table: { + category: 'Content', + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + onClick: { + name: 'onClick', + description: 'Click event handler for the label', + type: { + required: false, + raw: '(event: MouseEvent) => void', + name: 'function', + }, + table: { + type: { + summary: '(event: React.MouseEvent) => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + action: 'clicked', + }, + ariaLabel: { + control: { + type: 'text', + disable: false, + }, + name: 'ariaLabel', + description: 'Accessible label for screen readers', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + htmlFor: { + control: { + type: 'text', + disable: false, + }, + name: 'htmlFor', + description: 'Associates the label with form control', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Core Properties', + }, + }, + gap: { + name: 'gap', + description: 'Gap between label elements', + table: { + category: 'Layout & Spacing', + type: { + summary: 'string', + }, + defaultValue: { + summary: '4px', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + className: { + name: 'className', + description: 'Additional CSS classes to apply', + table: { + category: 'Styling', + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + styles: { + name: 'styles', + description: 'Custom inline styles to apply to the label', + table: { + category: 'Styling', + type: { + summary: 'CSSProperties', + }, + defaultValue: { + summary: '{}', + }, + }, + control: { + type: 'object', + disable: false, + }, + }, + }, + Link: { + variant: { + control: { + type: 'select', + disable: false, + }, + name: 'variant', + type: { + name: 'other', + required: false, + raw: 'EnumOrPrimitive', + value: 'EnumOrPrimitive', + }, + description: 'Visual style variant of the link', + table: { + type: { + summary: 'LinkVariant', + }, + defaultValue: { + summary: 'LinkVariant.Primary', + }, + category: 'Core Properties', + }, + options: ['primary', 'secondary', 'inverted', 'inherit'], + }, + rel: { + control: { + type: 'text', + disable: false, + }, + name: 'rel', + type: { + name: 'union', + required: false, + raw: 'EnumOrPrimitive | string', + value: [ + { + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + { + name: 'string', + }, + ], + }, + description: 'Relationship attribute for the link', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: '', + }, + category: 'Navigation & Behavior', + }, + }, + children: { + control: { + type: 'text', + disable: false, + }, + name: 'children', + type: { + name: 'string', + }, + description: 'Content to be rendered inside the link', + table: { + category: 'Content', + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + onClick: { + name: 'onClick', + type: { + name: 'function', + required: false, + raw: '(event: MouseEvent) => void', + }, + description: 'Click handler for the anchor element.', + table: { + type: { + summary: '(event: MouseEvent) => void', + }, + category: 'Events', + }, + action: 'clicked', + }, + size: { + control: { + type: 'select', + disable: false, + }, + options: ['sm', 'md', 'lg'], + name: 'size', + description: 'Size variant of the link text.', + type: { + required: false, + raw: "'sm' | 'md' | 'lg'", + name: 'enum', + value: ['sm', 'md', 'lg'], + }, + table: { + type: { + summary: '"sm" | "md" | "lg"', + }, + category: 'Appearance', + }, + }, + underline: { + control: { + type: 'select', + disable: false, + }, + options: ['default', 'highlight', 'none'], + name: 'underline', + description: 'Underline behavior for the link.', + type: { + required: false, + raw: "'default' | 'highlight' | 'none'", + name: 'enum', + value: ['default', 'highlight', 'none'], + }, + table: { + type: { + summary: '"default" | "highlight" | "none"', + }, + category: 'Appearance', + }, + }, + color: { + control: { + type: 'color', + }, + name: 'color', + description: '', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + }, + }, + cursor: { + control: { + type: 'text', + disable: false, + }, + name: 'cursor', + description: 'Cursor style used when hovering the link.', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'Cursors', + }, + category: 'Appearance', + }, + }, + disabled: { + control: { + type: 'boolean', + disable: false, + }, + name: 'disabled', + description: 'Disables the link interaction', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'State & Behavior', + }, + }, + target: { + control: { + type: 'select', + disable: false, + }, + name: 'target', + description: 'Target window behavior for the link', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'LinkTarget', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Navigation & Behavior', + }, + options: ['_blank', '_self', '_parent', '_top', 'framename'], + }, + href: { + control: { + type: 'text', + disable: false, + }, + name: 'href', + description: 'URL or destination for the link', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Core Properties', + }, + }, + ariaLabel: { + control: { + type: 'text', + disable: false, + }, + name: 'ariaLabel', + description: 'Accessible label announced by assistive technologies.', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + category: 'Accessibility', + }, + }, + role: { + control: { + type: 'text', + disable: false, + }, + name: 'role', + description: 'Explicit ARIA role for the rendered anchor.', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + category: 'Accessibility', + }, + }, + tabindex: { + control: { + type: 'number', + disable: false, + }, + name: 'tabindex', + description: 'Tab order for keyboard navigation.', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + category: 'Accessibility', + }, + }, + className: { + name: 'className', + description: 'Additional CSS classes to apply', + table: { + category: 'Styling', + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + styles: { + name: 'styles', + description: 'Custom inline styles to apply to the link', + table: { + category: 'Styling', + type: { + summary: 'CSSProperties', + }, + defaultValue: { + summary: '{}', + }, + }, + control: { + type: 'object', + disable: false, + }, + }, + }, + Loader: { + withWrapper: { + control: { + type: 'boolean', + disable: false, + }, + name: 'withWrapper', + type: { + name: 'boolean', + required: false, + }, + description: + 'Whether to wrap the loader in a container element. When false, the loader renders directly without wrapper styling. Useful for embedding loaders in buttons or other custom containers', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'true', + }, + category: 'Layout & Positioning', + }, + }, + name: { + control: { + type: 'select', + disable: false, + }, + options: ['circle', 'dots'], + name: 'name', + type: { + name: 'enum', + required: false, + raw: "'circle' | 'dots'", + value: ['circle', 'dots'], + }, + description: + 'Type of loader animation to display. "circle" provides a smooth rotating animation, while "dots" offers a rhythmic pulsing sequence', + table: { + type: { + summary: '"circle" | "dots"', + }, + defaultValue: { + summary: 'circle', + }, + category: 'Appearance', + }, + }, + variant: { + control: { + type: 'select', + disable: false, + }, + name: 'variant', + type: { + name: 'other', + required: false, + raw: 'EnumOrPrimitive', + value: 'EnumOrPrimitive', + }, + description: + 'Layout wrapper variant that determines positioning and container behavior. Inline: flows naturally in content, Section: absolute positioned overlay within container, FullPage: fixed position full-screen overlay with portal', + table: { + type: { + summary: 'inline | section | fullPage', + }, + defaultValue: { + summary: 'inline', + }, + category: 'Layout & Positioning', + }, + options: ['inline', 'section', 'fullPage'], + }, + size: { + control: { + type: 'select', + disable: false, + }, + name: 'size', + type: { + name: 'other', + required: false, + raw: 'EnumOrPrimitive', + value: 'EnumOrPrimitive', + }, + description: + 'Size variant of the loader. Controls both the loader animation size and wrapper dimensions. Available sizes: xs, sm, md (default), lg, xl', + table: { + type: { + summary: 'SizeVariant', + }, + defaultValue: { + summary: 'SizeVariant.Md', + }, + category: 'Appearance', + }, + options: ['xs', 'sm', 'md', 'lg', 'xl'], + }, + rounded: { + control: { + type: 'select', + disable: false, + }, + name: 'rounded', + description: + 'Border radius style for the loader. Available options: none, default, round, xs, sm, md, lg, xl. Note: Only applies to "dots" animation type, not "circle"', + type: { + required: false, + name: 'other', + value: 'Rounded', + }, + table: { + type: { + summary: "'none' | 'default' | 'round' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'", + }, + defaultValue: { + summary: 'none', + }, + category: 'Appearance', + }, + options: ['none', 'default', 'round', 'xs', 'sm', 'md', 'lg', 'xl'], + }, + animationProps: { + control: { + type: 'text', + }, + name: 'animationProps', + description: + 'Custom CSS animation properties string. Overrides default animation timing and easing for advanced customization. Use sparingly—prefer size and variant props for standard use cases', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'BASE_ANIMATION', + }, + category: 'Content & Customization', + }, + }, + WrapperView: { + control: { + type: 'object', + }, + name: 'WrapperView', + description: + 'HTML element type or React component to use for the wrapper container. Defaults to "span" for inline usage. Use semantic elements like "div", "section", or custom components for better accessibility', + type: { + required: false, + raw: 'keyof HTMLElementTagNameMap | ElementType', + name: 'union', + value: [ + { + name: 'other', + value: 'HTMLElementTagNameMap', + }, + { + name: 'other', + value: 'ElementType', + }, + ], + }, + table: { + type: { + summary: 'keyof HTMLElementTagNameMap | ElementType', + }, + defaultValue: { + summary: 'span', + }, + category: 'Layout & Positioning', + }, + }, + children: { + name: 'children', + description: + 'Custom React content to replace the default loader animation. Use this to create custom loading indicators while maintaining Loader wrapper and positioning functionality', + table: { + category: 'Content & Customization', + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + styles: { + name: 'styles', + description: + 'Custom CSS styles object to override default theme tokens. Use for one-off styling needs. Prefer theme tokens and component props for consistent design system styling', + table: { + category: 'Custom Styling', + type: { + summary: 'CSSProperties', + }, + defaultValue: { + summary: '{}', + }, + }, + }, + className: { + name: 'className', + description: + 'Additional CSS class names to apply to the loader element. Use for custom styling, state management, or integration with CSS frameworks', + table: { + category: 'Custom Styling', + type: { + summary: 'string', + }, + defaultValue: { + summary: '""', + }, + }, + }, + }, + Badge: { + children: { + control: { + type: 'text', + disable: false, + }, + name: 'children', + type: { + name: 'string', + }, + description: 'Badge content (text or elements)', + table: { + category: 'Content', + type: { + summary: 'ReactNode', + }, + }, + }, + variant: { + control: { + type: 'select', + disable: false, + }, + name: 'variant', + description: 'Visual style variant of the badge', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'BadgeVariant', + }, + defaultValue: { + summary: 'primary', + }, + category: 'Visual Style', + }, + options: ['primary', 'secondary', 'tertiary', 'quaternary', 'quinary'], + }, + disabled: { + control: { + type: 'boolean', + disable: false, + }, + name: 'disabled', + description: 'Whether the badge is disabled', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Visual Style', + }, + }, + appearance: { + control: { + type: 'select', + disable: false, + }, + name: 'appearance', + description: 'Visual appearance/style of the badge (filled, filledLight, outline, outlineFilledLight)', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'BadgeAppearance', + }, + defaultValue: { + summary: 'filled', + }, + category: 'Visual Style', + }, + options: ['outline', 'outlineFilledLight', 'filled', 'filledLight'], + }, + size: { + control: { + type: 'select', + disable: false, + }, + name: 'size', + description: 'Size of the badge (xs, sm, md, lg)', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'BadgeSize', + }, + defaultValue: { + summary: 'md', + }, + category: 'Visual Style', + }, + options: ['xs', 'sm', 'md', 'lg'], + }, + iconStart: { + control: { + type: 'object', + disable: true, + }, + name: 'iconStart', + description: 'Icon element to display at the start of the badge', + type: { + required: false, + name: 'other', + value: 'ReactNode', + }, + table: { + type: { + summary: 'ReactNode', + }, + category: 'Icons', + }, + }, + iconEnd: { + control: { + type: 'object', + disable: true, + }, + name: 'iconEnd', + description: 'Icon element to display at the end of the badge', + type: { + required: false, + name: 'other', + value: 'ReactNode', + }, + table: { + type: { + summary: 'ReactNode', + }, + category: 'Icons', + }, + }, + styles: { + name: 'styles', + description: 'CSSObject for custom inline styles', + table: { + category: 'Box Styles', + type: { + summary: 'CSSObject', + }, + }, + control: { + disable: true, + }, + }, + display: { + name: 'display', + description: 'CSS display property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + overflow: { + name: 'overflow', + description: 'CSS overflow property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + minWidth: { + name: 'minWidth', + description: 'CSS min-width property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + width: { + name: 'width', + description: 'CSS width property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + maxWidth: { + name: 'maxWidth', + description: 'CSS max-width property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + minHeight: { + name: 'minHeight', + description: 'CSS min-height property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + height: { + name: 'height', + description: 'CSS height property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + maxHeight: { + name: 'maxHeight', + description: 'CSS max-height property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + margin: { + name: 'margin', + description: 'CSS margin property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginTop: { + name: 'marginTop', + description: 'CSS margin-top property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginRight: { + name: 'marginRight', + description: 'CSS margin-right property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginBottom: { + name: 'marginBottom', + description: 'CSS margin-bottom property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginLeft: { + name: 'marginLeft', + description: 'CSS margin-left property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + padding: { + name: 'padding', + description: 'CSS padding property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingTop: { + name: 'paddingTop', + description: 'CSS padding-top property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingRight: { + name: 'paddingRight', + description: 'CSS padding-right property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingBottom: { + name: 'paddingBottom', + description: 'CSS padding-bottom property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingLeft: { + name: 'paddingLeft', + description: 'CSS padding-left property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + zIndex: { + name: 'zIndex', + description: 'CSS z-index property', + table: { + category: 'Box Styles', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + position: { + name: 'position', + description: 'CSS position property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + top: { + name: 'top', + description: 'CSS top property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + right: { + name: 'right', + description: 'CSS right property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + bottom: { + name: 'bottom', + description: 'CSS bottom property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + left: { + name: 'left', + description: 'CSS left property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexDirection: { + name: 'flexDirection', + description: 'CSS flex-direction property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + justifyContent: { + name: 'justifyContent', + description: 'CSS justify-content property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + justifySelf: { + name: 'justifySelf', + description: 'CSS justify-self property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignItems: { + name: 'alignItems', + description: 'CSS align-items property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignSelf: { + name: 'alignSelf', + description: 'CSS align-self property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignContent: { + name: 'alignContent', + description: 'CSS align-content property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexWrap: { + name: 'flexWrap', + description: 'CSS flex-wrap property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flex: { + name: 'flex', + description: 'CSS flex property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexGrow: { + name: 'flexGrow', + description: 'CSS flex-grow property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexShrink: { + name: 'flexShrink', + description: 'CSS flex-shrink property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexBasis: { + name: 'flexBasis', + description: 'CSS flex-basis property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + order: { + name: 'order', + description: 'CSS order property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + gap: { + name: 'gap', + description: 'CSS gap property', + table: { + category: 'Box Styles', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + }, + Box: { + variant: { + control: { + type: 'select', + disable: false, + }, + options: ['horizontal', 'vertical'], + name: 'variant', + type: { + name: 'enum', + required: false, + raw: "'horizontal' | 'vertical'", + value: ['horizontal', 'vertical'], + }, + description: 'Box orientation variant', + table: { + type: { + summary: "'horizontal' | 'vertical'", + }, + defaultValue: { + summary: 'vertical', + }, + category: 'Core Props', + }, + }, + children: { + control: { + type: 'text', + disable: false, + }, + name: 'children', + type: { + name: 'string', + }, + description: 'Box content (any React node)', + table: { + category: 'Core Props', + type: { + summary: 'ReactNode', + }, + }, + }, + padding: { + control: { + type: 'text', + disable: false, + }, + name: 'padding', + type: { + name: 'string', + }, + description: 'CSS padding property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + }, + isBordered: { + control: { + type: 'boolean', + disable: false, + }, + name: 'isBordered', + description: 'Adds border to the box', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Visual Style', + }, + }, + isHighlighted: { + control: { + type: 'boolean', + disable: false, + }, + name: 'isHighlighted', + description: 'Enables highlight effect on hover (adds outline on hover)', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Visual Style', + }, + }, + withShadowHover: { + control: { + type: 'boolean', + disable: false, + }, + name: 'withShadowHover', + description: 'Adds box shadow on hover for elevation effect', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Visual Style', + }, + }, + tabIndex: { + control: { + type: 'number', + disable: false, + }, + name: 'tabIndex', + description: 'Tab index for keyboard navigation', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '0', + }, + category: 'Accessibility', + }, + }, + width: { + name: 'width', + description: 'CSS width property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + height: { + name: 'height', + description: 'CSS height property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + minWidth: { + name: 'minWidth', + description: 'CSS min-width property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + minHeight: { + name: 'minHeight', + description: 'CSS min-height property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + maxWidth: { + name: 'maxWidth', + description: 'CSS max-width property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + maxHeight: { + name: 'maxHeight', + description: 'CSS max-height property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingTop: { + name: 'paddingTop', + description: 'CSS padding-top property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingRight: { + name: 'paddingRight', + description: 'CSS padding-right property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingBottom: { + name: 'paddingBottom', + description: 'CSS padding-bottom property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingLeft: { + name: 'paddingLeft', + description: 'CSS padding-left property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + margin: { + name: 'margin', + description: 'CSS margin property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginTop: { + name: 'marginTop', + description: 'CSS margin-top property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginRight: { + name: 'marginRight', + description: 'CSS margin-right property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginBottom: { + name: 'marginBottom', + description: 'CSS margin-bottom property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginLeft: { + name: 'marginLeft', + description: 'CSS margin-left property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexDirection: { + name: 'flexDirection', + description: 'CSS flex-direction property', + options: ['row', 'row-reverse', 'column', 'column-reverse'], + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'select', + disable: false, + }, + }, + justifyContent: { + name: 'justifyContent', + description: 'CSS justify-content property', + options: ['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'space-evenly'], + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'select', + disable: false, + }, + }, + alignItems: { + name: 'alignItems', + description: 'CSS align-items property', + options: ['flex-start', 'flex-end', 'center', 'baseline', 'stretch'], + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'select', + disable: false, + }, + }, + alignContent: { + name: 'alignContent', + description: 'CSS align-content property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexWrap: { + name: 'flexWrap', + description: 'CSS flex-wrap property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + gap: { + name: 'gap', + description: 'CSS gap property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flex: { + name: 'flex', + description: 'CSS flex property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexGrow: { + name: 'flexGrow', + description: 'CSS flex-grow property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexShrink: { + name: 'flexShrink', + description: 'CSS flex-shrink property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexBasis: { + name: 'flexBasis', + description: 'CSS flex-basis property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + position: { + name: 'position', + description: 'CSS position property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + overflow: { + name: 'overflow', + description: 'CSS overflow property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + styles: { + name: 'styles', + description: 'Custom styles object', + table: { + category: 'Custom Styling', + type: { + summary: 'CSSProperties', + }, + }, + control: { + type: 'object', + disable: false, + }, + }, + }, + Image: { + width: { + control: { + type: 'number', + }, + name: 'width', + type: { + name: 'number', + required: false, + }, + description: 'Width of the image in pixels', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Dimensions', + }, + }, + height: { + control: { + type: 'number', + }, + name: 'height', + type: { + name: 'number', + required: false, + }, + description: 'Height of the image in pixels', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Dimensions', + }, + }, + alt: { + control: { + type: 'text', + }, + name: 'alt', + type: { + name: 'string', + required: false, + }, + description: 'Alternative text for the image', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'required', + }, + category: 'Core Properties', + }, + }, + src: { + control: { + type: 'text', + }, + name: 'src', + type: { + name: 'string', + required: false, + }, + description: 'Source URL of the image', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'required', + }, + category: 'Core Properties', + }, + }, + id: { + control: { + type: 'text', + }, + name: 'id', + type: { + name: 'string', + required: false, + }, + description: 'Unique identifier for the image', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Core Properties', + }, + }, + className: { + control: { + type: 'text', + }, + name: 'className', + type: { + name: 'string', + }, + description: 'Additional CSS classes to apply', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Styling', + }, + }, + placeholder: { + control: { + type: 'object', + }, + name: 'placeholder', + type: { + name: 'other', + required: false, + value: 'ReactNode', + }, + description: 'Text to display while image is loading', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Content & Display', + }, + }, + caption: { + control: { + type: 'text', + }, + name: 'caption', + type: { + name: 'string', + required: false, + }, + description: 'Caption text to display below the image', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Content & Display', + }, + }, + onClick: { + name: 'onClick', + description: 'Click event handler for the image', + type: { + required: false, + raw: '() => void', + name: 'function', + }, + table: { + type: { + summary: '(event: React.MouseEvent) => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + }, + onError: { + name: 'onError', + description: 'Error event handler when image fails to load', + type: { + required: false, + raw: '() => void', + name: 'function', + }, + table: { + type: { + summary: '() => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + }, + onLoad: { + name: 'onLoad', + description: 'Load event handler when image successfully loads', + type: { + required: false, + raw: '() => void', + name: 'function', + }, + table: { + type: { + summary: '() => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + }, + fallbackComponent: { + control: { + type: 'object', + }, + name: 'fallbackComponent', + description: 'Component to display when image fails to load', + type: { + required: false, + name: 'other', + value: 'ReactNode', + }, + table: { + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Content & Display', + }, + }, + objectFit: { + control: { + type: 'radio', + }, + options: ['cover', 'contain', 'fill', 'none', 'scale-down'], + name: 'objectFit', + description: 'CSS object-fit property for image scaling', + type: { + required: false, + raw: "'cover' | 'contain' | 'fill' | 'none' | 'scale-down'", + name: 'enum', + value: ['cover', 'contain', 'fill', 'none', 'scale-down'], + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Styling', + }, + }, + as: { + control: { + type: 'text', + disable: false, + }, + name: 'as', + description: + 'Polymorphic prop that changes the rendered wrapper element while preserving all Image styles. Accepts HTML tag names (e.g., "div", "span", "figure") or React components. Useful for semantic HTML and accessibility.', + type: { + required: false, + raw: 'keyof HTMLElementTagNameMap | ElementType', + name: 'union', + value: [ + { + name: 'other', + value: 'HTMLElementTagNameMap', + }, + { + name: 'other', + value: 'ElementType', + }, + ], + }, + table: { + type: { + summary: 'keyof HTMLElementTagNameMap | ElementType', + }, + defaultValue: { + summary: 'div', + }, + category: 'Core Properties', + }, + }, + captionAs: { + control: { + type: 'text', + disable: false, + }, + name: 'captionAs', + description: + 'Polymorphic prop that changes the rendered caption element while preserving all Image caption styles. Accepts HTML tag names (e.g., "figcaption", "p", "span") or React components. Defaults to "figcaption" for semantic HTML. Useful for custom caption styling or when not using semantic figure/caption structure.', + type: { + required: false, + raw: 'keyof HTMLElementTagNameMap | ElementType', + name: 'union', + value: [ + { + name: 'other', + value: 'HTMLElementTagNameMap', + }, + { + name: 'other', + value: 'ElementType', + }, + ], + }, + table: { + type: { + summary: 'keyof HTMLElementTagNameMap | ElementType', + }, + defaultValue: { + summary: 'figcaption', + }, + category: 'Core Properties', + }, + }, + styles: { + name: 'styles', + description: 'Custom CSS styles object', + table: { + type: { + summary: 'CSSObject', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Styling', + }, + }, + }, + Avatar: { + backgroundColor: { + control: { + type: 'color', + disable: false, + }, + name: 'backgroundColor', + type: { + name: 'string', + required: false, + }, + description: + 'The background color of the avatar when no image is loaded, or when the image fails. Also used for the fallback content container. Accepts CSS color values (e.g., hex, named colors) and supports theme colors (e.g. `primary.default`).', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Appearance', + }, + }, + src: { + control: { + type: 'text', + disable: false, + }, + name: 'src', + type: { + name: 'string', + }, + description: + 'The URL of the image to display in the avatar. If no `src` is provided or the image fails to load, the `fallback` content will be rendered.', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Image', + }, + }, + sizeVariant: { + control: { + type: 'select', + disable: false, + }, + name: 'sizeVariant', + type: { + name: 'other', + required: false, + raw: 'EnumOrPrimitive', + value: 'EnumOrPrimitive', + }, + description: 'Defines the predefined size of the avatar.', + table: { + type: { + summary: 'SizeVariant', + }, + defaultValue: { + summary: 'md', + }, + category: 'Appearance', + }, + options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'], + }, + withBadge: { + control: { + type: 'boolean', + disable: false, + }, + name: 'withBadge', + description: + 'If `true`, a small badge will be displayed at the top-right corner of the avatar, often indicating online status or notifications.', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Badge', + }, + }, + badgeColor: { + control: { + type: 'color', + disable: false, + }, + name: 'badgeColor', + description: + 'The color of the badge when `withBadge` is `true`. Accepts CSS color values (e.g., hex, named colors). Also supports theme colors (e.g. `primary.default`)', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: '#34A853', + }, + category: 'Badge', + }, + }, + fallbackComponent: { + control: { + type: 'text', + disable: false, + }, + name: 'fallbackComponent', + description: + 'Content to display when `src` is not provided or the image fails to load. Can be a string, number, or any ReactNode (e.g., an icon, initials, or a custom component).', + type: { + required: false, + name: 'other', + value: 'ReactNode', + }, + table: { + type: { + summary: 'ReactNode', + }, + category: 'Fallback', + }, + }, + onClick: { + name: 'onClick', + description: 'Callback function triggered when the avatar is clicked.', + type: { + required: false, + raw: '() => void', + name: 'function', + }, + table: { + type: { + summary: '() => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + action: 'clicked', + }, + alt: { + name: 'alt', + description: + 'Alternative text for the avatar image, important for accessibility. This will be used as the `aria-label` for the avatar.', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: '""', + }, + category: 'Image', + }, + control: { + type: 'text', + disable: false, + }, + }, + id: { + name: 'id', + description: 'A unique identifier for the avatar element.', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Core', + }, + control: { + type: 'text', + disable: false, + }, + }, + className: { + name: 'className', + description: 'Additional CSS class names to apply to the avatar component.', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Styling', + }, + control: { + type: 'text', + disable: false, + }, + }, + placeholder: { + name: 'placeholder', + description: 'Passed to the internal `Image` component.', + table: { + type: { + summary: 'string', + }, + category: 'Image', + }, + control: { + type: 'text', + disable: false, + }, + }, + styles: { + name: 'styles', + description: 'Custom CSS-in-JS styles to apply to the top-level container of the Avatar.', + table: { + type: { + summary: 'CSSObject', + }, + defaultValue: { + summary: '{}', + }, + category: 'Styling', + }, + control: { + type: 'object', + disable: false, + }, + }, + }, + Button: { + variant: { + name: 'variant', + description: 'Visual style variant of the button', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'ButtonVariant', + }, + defaultValue: { + summary: 'primary', + }, + category: 'Visual Style', + }, + options: ['primary', 'secondary', 'tertiary', 'outlined', 'text', 'inherit'], + control: { + type: 'select', + disable: false, + }, + }, + rounded: { + name: 'rounded', + description: 'Border radius style for the button', + type: { + required: false, + name: 'other', + value: 'Rounded', + }, + table: { + type: { + summary: "'none' | 'default' | 'round' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'", + }, + defaultValue: { + summary: 'none', + }, + category: 'Visual Style', + }, + options: ['none', 'default', 'round', 'xs', 'sm', 'md', 'lg', 'xl'], + control: { + type: 'select', + disable: false, + }, + }, + iconStart: { + name: 'iconStart', + description: 'Icon element to display at the start of the button', + type: { + required: false, + name: 'other', + value: 'ReactNode', + }, + table: { + type: { + summary: 'ReactNode', + }, + category: 'Icons', + }, + }, + iconEnd: { + name: 'iconEnd', + description: 'Icon element to display at the end of the button', + type: { + required: false, + name: 'other', + value: 'ReactNode', + }, + table: { + type: { + summary: 'ReactNode', + }, + category: 'Icons', + }, + }, + onClick: { + name: 'onClick', + description: 'Click event handler', + type: { + required: false, + raw: '(event: MouseEvent) => void', + name: 'function', + }, + table: { + type: { + summary: '(event: MouseEvent) => void', + }, + category: 'Content & Behavior', + }, + action: 'clicked', + }, + type: { + name: 'type', + description: 'HTML button type attribute', + type: { + required: false, + raw: 'ButtonTypes | `${ButtonTypes}`', + name: 'union', + value: [ + { + name: 'other', + value: 'ButtonTypes', + }, + { + name: 'other', + value: 'literal', + }, + ], + }, + table: { + type: { + summary: 'ButtonTypes', + }, + defaultValue: { + summary: 'button', + }, + category: 'Content & Behavior', + }, + options: ['button', 'submit', 'reset'], + control: { + type: 'select', + disable: false, + }, + }, + disabled: { + name: 'disabled', + description: 'Disables the button and prevents interaction', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Content & Behavior', + }, + control: { + type: 'boolean', + disable: false, + }, + }, + fullWidth: { + name: 'fullWidth', + description: 'Makes button take full width of its container', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Visual Style', + }, + control: { + type: 'boolean', + disable: false, + }, + }, + isIcon: { + name: 'isIcon', + description: 'Renders button as icon-only with equal width and height', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Visual Style', + }, + control: { + type: 'boolean', + disable: false, + }, + }, + isLoading: { + name: 'isLoading', + description: '', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + }, + }, + ariaLabel: { + name: 'ariaLabel', + description: 'Accessibility label for screen readers', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + category: 'Accessibility', + }, + control: { + type: 'text', + disable: false, + }, + }, + ariaPressed: { + name: 'ariaPressed', + description: 'Indicates the pressed state for toggle buttons', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + category: 'Accessibility', + }, + control: { + type: 'boolean', + disable: false, + }, + }, + role: { + name: 'role', + description: 'ARIA role for the button', + type: { + required: false, + raw: 'ButtonRole | `${ButtonRole}`', + name: 'union', + value: [ + { + name: 'other', + value: 'ButtonRole', + }, + { + name: 'other', + value: 'literal', + }, + ], + }, + table: { + type: { + summary: 'ButtonRole', + }, + defaultValue: { + summary: 'button', + }, + category: 'Accessibility', + }, + options: ['button', 'link', 'checkbox', 'switch', 'tab'], + control: { + type: 'select', + disable: false, + }, + }, + tabIndex: { + name: 'tabIndex', + description: 'Tab index for keyboard navigation', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '0', + }, + category: 'Accessibility', + }, + control: { + type: 'number', + disable: false, + }, + }, + children: { + name: 'children', + description: 'Button content (text or elements)', + table: { + category: 'Content & Behavior', + type: { + summary: 'ReactNode', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + width: { + name: 'width', + description: 'CSS width property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + height: { + name: 'height', + description: 'CSS height property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + minWidth: { + name: 'minWidth', + description: 'CSS min-width property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + minHeight: { + name: 'minHeight', + description: 'CSS min-height property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + maxWidth: { + name: 'maxWidth', + description: 'CSS max-width property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + maxHeight: { + name: 'maxHeight', + description: 'CSS max-height property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + margin: { + name: 'margin', + description: 'CSS margin property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginTop: { + name: 'marginTop', + description: 'CSS margin-top property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginRight: { + name: 'marginRight', + description: 'CSS margin-right property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginBottom: { + name: 'marginBottom', + description: 'CSS margin-bottom property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginLeft: { + name: 'marginLeft', + description: 'CSS margin-left property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + padding: { + name: 'padding', + description: 'CSS padding property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingTop: { + name: 'paddingTop', + description: 'CSS padding-top property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingRight: { + name: 'paddingRight', + description: 'CSS padding-right property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingBottom: { + name: 'paddingBottom', + description: 'CSS padding-bottom property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingLeft: { + name: 'paddingLeft', + description: 'CSS padding-left property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexDirection: { + name: 'flexDirection', + description: 'CSS flex-direction property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + justifyContent: { + name: 'justifyContent', + description: 'CSS justify-content property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignItems: { + name: 'alignItems', + description: 'CSS align-items property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignContent: { + name: 'alignContent', + description: 'CSS align-content property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexWrap: { + name: 'flexWrap', + description: 'CSS flex-wrap property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + gap: { + name: 'gap', + description: 'CSS gap property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flex: { + name: 'flex', + description: 'CSS flex property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexGrow: { + name: 'flexGrow', + description: 'CSS flex-grow property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexShrink: { + name: 'flexShrink', + description: 'CSS flex-shrink property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexBasis: { + name: 'flexBasis', + description: 'CSS flex-basis property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignSelf: { + name: 'alignSelf', + description: 'CSS align-self property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + justifySelf: { + name: 'justifySelf', + description: 'CSS justify-self property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + order: { + name: 'order', + description: 'CSS order property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + display: { + name: 'display', + options: ['block', 'flex', 'inline', 'inline-flex', 'grid', 'none'], + description: 'CSS display property to control the layout behavior', + table: { + type: { + summary: 'string', + }, + category: 'Box Styles - Position & Display', + }, + control: { + type: 'select', + disable: false, + }, + }, + position: { + name: 'position', + description: 'CSS position property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + top: { + name: 'top', + description: 'CSS top property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + right: { + name: 'right', + description: 'CSS right property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + bottom: { + name: 'bottom', + description: 'CSS bottom property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + left: { + name: 'left', + description: 'CSS left property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + overflow: { + name: 'overflow', + description: 'CSS overflow property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + zIndex: { + name: 'zIndex', + description: 'CSS z-index property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + styles: { + name: 'styles', + description: 'Custom styles object for the button', + table: { + category: 'Custom Styling', + type: { + summary: 'BoxCssComponentProps', + }, + }, + control: { + type: 'object', + disable: false, + }, + }, + className: { + name: 'className', + description: 'Custom CSS class names', + table: { + category: 'Custom Styling, predefined classes hover, active, and disabled', + type: { + summary: '("hover" | "active" | "disabled" | string)[]', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + }, + Checkbox: { + children: { + control: { + type: 'text', + }, + name: 'children', + type: { + name: 'string', + }, + }, + onValueChange: { + name: 'onValueChange', + type: { + name: 'function', + required: false, + raw: '(checked: boolean) => void', + }, + description: 'Callback fired when the checkbox value changes', + table: { + type: { + summary: '(checked: boolean) => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + action: 'Checkbox value changed', + }, + checked: { + control: { + type: 'boolean', + disable: false, + }, + name: 'checked', + description: 'Controls the checked state of the checkbox', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'State', + }, + }, + indeterminate: { + control: { + type: 'boolean', + disable: false, + }, + name: 'indeterminate', + description: 'Displays the checkbox in an indeterminate (mixed) state', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'State', + }, + }, + disabled: { + control: { + type: 'boolean', + disable: false, + }, + name: 'disabled', + description: 'Disables the checkbox interaction', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'State', + }, + }, + name: { + control: { + type: 'text', + disable: false, + }, + name: 'name', + description: 'Name attribute of the checkbox input', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Identification', + }, + }, + value: { + control: { + type: 'text', + disable: false, + }, + name: 'value', + description: 'Value attribute of the checkbox input', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Identification', + }, + }, + size: { + control: { + type: 'select', + disable: false, + }, + options: ['sm', 'md'], + name: 'size', + description: 'Size of the checkbox', + type: { + required: false, + raw: "'sm' | 'md'", + name: 'enum', + value: ['sm', 'md'], + }, + table: { + type: { + summary: 'CheckboxSize', + detail: 'sm | md', + }, + defaultValue: { + summary: 'md', + }, + category: 'Layout', + }, + }, + }, + Input: { + variant: { + control: { + type: 'select', + disable: false, + }, + name: 'variant', + type: { + name: 'string', + required: false, + }, + description: 'Input field type variant', + table: { + type: { + summary: 'InputVariantType', + }, + defaultValue: { + summary: 'text', + }, + category: 'Core Properties', + }, + options: [ + 'text', + 'password', + 'email', + 'search', + 'url', + 'tel', + 'date', + 'time', + 'month', + 'week', + 'color', + 'range', + 'number', + 'radio', + 'checkbox', + ], + }, + color: { + control: { + type: 'select', + disable: false, + }, + name: 'color', + type: { + name: 'string', + required: false, + }, + description: 'Color variant of the input', + table: { + type: { + summary: 'InputColorVariant', + }, + defaultValue: { + summary: "'primary'", + }, + category: 'Appearance & Styling', + }, + options: ['primary', 'success', 'warning', 'error'], + }, + role: { + control: { + type: 'select', + disable: false, + }, + name: 'role', + type: { + name: 'string', + }, + description: 'ARIA role of the input element', + options: ['textbox', 'spinbutton', 'checkbox', 'radio', 'radiogroup', 'combobox'], + table: { + category: 'Accessibility', + defaultValue: { + summary: 'textbox', + }, + type: { + summary: 'InputRole', + }, + }, + }, + name: { + control: { + type: 'text', + }, + name: 'name', + type: { + name: 'string', + }, + description: 'Input field name attribute', + table: { + category: 'Core Properties', + defaultValue: { + summary: 'input', + }, + type: { + summary: 'string', + }, + }, + }, + width: { + control: { + type: 'text', + }, + name: 'width', + type: { + name: 'string', + required: false, + }, + description: 'Width of the input field', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: '100%', + }, + category: 'Appearance & Styling', + }, + }, + disabled: { + control: { + type: 'boolean', + }, + name: 'disabled', + type: { + name: 'boolean', + required: false, + }, + description: 'Disabled state of the input', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'State & Behavior', + }, + }, + required: { + control: { + type: 'boolean', + }, + name: 'required', + type: { + name: 'boolean', + }, + description: 'Required state of the input', + table: { + category: 'State & Behavior', + defaultValue: { + summary: 'false', + }, + type: { + summary: 'boolean', + }, + }, + }, + readOnly: { + control: { + type: 'boolean', + }, + name: 'readOnly', + type: { + name: 'boolean', + required: false, + }, + description: 'Read-only state of the input', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'State & Behavior', + }, + }, + checked: { + control: { + type: 'boolean', + }, + name: 'checked', + type: { + name: 'boolean', + }, + description: 'Checked state for checkbox/radio inputs', + table: { + category: 'State & Behavior', + defaultValue: { + summary: 'false', + }, + type: { + summary: 'boolean', + }, + }, + }, + placeholder: { + control: { + type: 'text', + }, + name: 'placeholder', + type: { + name: 'string', + }, + description: 'Placeholder text', + table: { + category: 'Content & Labels', + defaultValue: { + summary: 'placeholder', + }, + type: { + summary: 'string', + }, + }, + }, + defaultValue: { + control: { + type: 'text', + }, + name: 'defaultValue', + type: { + name: 'string', + }, + description: 'Default value of the input', + table: { + category: 'Content & Labels', + defaultValue: { + summary: 'defaultValue', + }, + type: { + summary: 'string', + }, + }, + }, + tabIndex: { + control: { + type: 'number', + }, + name: 'tabIndex', + type: { + name: 'number', + required: false, + }, + description: 'Tab index for keyboard navigation', + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '0', + }, + category: 'Accessibility', + }, + }, + className: { + control: { + type: 'text', + }, + name: 'className', + type: { + name: 'string', + required: false, + }, + description: 'Additional CSS classes', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: '', + }, + category: 'Appearance & Styling', + }, + }, + ariaRequired: { + control: { + type: 'boolean', + }, + name: 'ariaRequired', + type: { + name: 'boolean', + required: false, + }, + description: 'ARIA required attribute', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Accessibility', + }, + }, + debounceCallbackTime: { + control: { + type: 'number', + }, + name: 'debounceCallbackTime', + type: { + name: 'number', + }, + description: 'Debounce timeout for onChange callback', + table: { + category: 'State & Behavior', + defaultValue: { + summary: '300', + }, + type: { + summary: 'number', + }, + }, + }, + styles: { + control: { + type: 'object', + }, + name: 'styles', + type: { + name: 'object', + value: {}, + required: false, + }, + description: 'Custom styles object', + table: { + type: { + summary: 'React.CSSProperties', + }, + defaultValue: { + summary: '{}', + }, + category: 'Appearance & Styling', + }, + }, + adornmentStart: { + control: { + type: 'object', + }, + name: 'adornmentStart', + type: { + name: 'other', + required: false, + value: 'ReactNode', + }, + description: 'Start adornment element', + table: { + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: '', + }, + category: 'Content & Labels', + }, + }, + adornmentEnd: { + control: { + type: 'object', + }, + name: 'adornmentEnd', + type: { + name: 'other', + required: false, + value: 'ReactNode', + }, + description: 'End adornment element', + table: { + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: '', + }, + category: 'Content & Labels', + }, + }, + label: { + control: { + type: 'object', + }, + name: 'label', + type: { + name: 'other', + required: false, + value: 'ReactNode', + }, + description: 'Input label text', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: '', + }, + category: 'Content & Labels', + }, + }, + helperText: { + control: { + type: 'object', + }, + name: 'helperText', + type: { + name: 'other', + required: false, + value: 'ReactNode', + }, + description: 'Helper text below input', + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: '', + }, + category: 'Content & Labels', + }, + }, + onChange: { + name: 'onChange', + type: { + name: 'function', + }, + description: 'onChange event handler', + table: { + category: 'Events', + type: { + summary: '(event: ChangeEvent) => void', + }, + }, + }, + onFocus: { + name: 'onFocus', + type: { + name: 'function', + }, + description: 'onFocus event handler', + table: { + category: 'Events', + type: { + summary: '(event: FocusEvent) => void', + }, + }, + }, + onBlur: { + name: 'onBlur', + type: { + name: 'function', + }, + description: 'onBlur event handler', + table: { + category: 'Events', + type: { + summary: '(event: FocusEvent) => void', + }, + }, + }, + onClick: { + name: 'onClick', + type: { + name: 'function', + }, + description: 'onClick event handler', + table: { + category: 'Events', + type: { + summary: '(event: MouseEvent) => void', + }, + }, + }, + wrapperAs: { + control: { + type: 'object', + }, + name: 'wrapperAs', + description: 'HTML element or component to wrap the input', + type: { + required: false, + raw: 'keyof HTMLElementTagNameMap | ElementType', + name: 'union', + value: [ + { + name: 'other', + value: 'HTMLElementTagNameMap', + }, + { + name: 'other', + value: 'ElementType', + }, + ], + }, + table: { + type: { + summary: 'keyof HTMLElementTagNameMap | ElementType', + }, + defaultValue: { + summary: 'label', + }, + category: 'Core Properties', + }, + }, + ariaDescribedBy: { + name: 'ariaDescribedBy', + description: 'ARIA describedby attribute', + table: { + category: 'Accessibility', + type: { + summary: 'string', + }, + }, + }, + }, + Select: { + items: { + name: 'items', + description: 'Array of items to be displayed in the select dropdown', + table: { + category: 'Core', + type: { + summary: '(never | Option)[]', + detail: 'Option { name: string; value: unknown; }', + }, + defaultValue: { + summary: '[]', + }, + }, + }, + value: { + name: 'value', + description: 'Currently selected value (Option for single select, Option[] for multiple select)', + table: { + category: 'Core', + type: { + summary: 'Option | Option[] | null', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + multiple: { + name: 'multiple', + description: 'Enable multiple selection mode', + table: { + category: 'Core', + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + }, + control: { + type: 'boolean', + disable: false, + }, + }, + placeholder: { + name: 'placeholder', + description: 'Text to display when no value is selected', + table: { + category: 'Core', + type: { + summary: 'string', + }, + defaultValue: { + summary: 'Select', + }, + }, + }, + onSelect: { + name: 'onSelect', + description: 'Callback when item is selected', + action: 'selected', + table: { + category: 'Events', + type: { + summary: 'SelectOnSelect = (props: OnSelectProps) => void', + detail: + 'OnSelectProps {\n event: MouseEvent | KeyboardEvent | ChangeEvent;\n data?: Option;\n index?: number;\n}', + }, + }, + }, + onChange: { + name: 'onChange', + description: + 'Callback fired when value changes (receives Option for single select, Option[] for multiple select)', + table: { + category: 'Events', + type: { + summary: '(data: Option | Option[]) => void', + detail: 'Option { name: string; value: unknown; }', + }, + }, + }, + onKeyDown: { + name: 'onKeyDown', + description: 'Callback fired on keyboard events', + table: { + category: 'Events', + type: { + summary: 'ReactEventHandler', + }, + }, + }, + onInitiatorClick: { + name: 'onInitiatorClick', + description: 'Callback fired when initiator is clicked', + table: { + category: 'Events', + type: { + summary: '(event: MouseEvent) => void', + }, + }, + }, + renderOption: { + name: 'renderOption', + description: 'Custom renderer for option items', + table: { + category: 'Customization', + type: { + summary: '(value: renderOptionType) => ReactNode', + detail: + 'renderOptionType = {\n item: Option;\n index: number;\n isActiveItem: boolean;\n className: string;\n};', + }, + }, + }, + itemStringifier: { + name: 'itemStringifier', + description: 'Function to convert item value to display string', + table: { + category: 'Customization', + type: { + summary: '(data: Option) => string', + detail: 'Option { name: string; value: unknown; }', + }, + }, + }, + itemIdentifier: { + name: 'itemIdentifier', + description: 'Function to compare items for equality', + table: { + category: 'Customization', + type: { + summary: 'ItemIdentifier', + detail: 'ItemIdentifier = (selected: Option, current: Option) => boolean', + }, + }, + }, + width: { + name: 'width', + description: 'Width of the select component', + table: { + category: 'Layout', + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + minWidth: { + name: 'minWidth', + description: 'Minimum width of the select component', + table: { + category: 'Layout', + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + maxWidth: { + name: 'maxWidth', + description: 'Maximum width of the select component', + table: { + category: 'Layout', + type: { + summary: 'string', + }, + defaultValue: { + summary: 'initial', + }, + }, + }, + dropdownMaxHeight: { + name: 'dropdownMaxHeight', + description: 'Maximum height of the dropdown list', + table: { + category: 'Layout', + type: { + summary: 'string', + }, + defaultValue: { + summary: '240px', + }, + }, + }, + adornmentStart: { + name: 'adornmentStart', + description: 'Element to be rendered at the start of the select', + table: { + category: 'Layout', + type: { + summary: 'ReactNode', + }, + }, + }, + adornmentEnd: { + name: 'adornmentEnd', + description: 'Element to be rendered at the end of the select', + table: { + category: 'Layout', + type: { + summary: 'ReactNode', + }, + }, + }, + disabled: { + name: 'disabled', + description: 'Whether the select is disabled', + table: { + category: 'State', + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + }, + }, + autoOpen: { + name: 'autoOpen', + description: 'Whether to automatically open the dropdown', + table: { + category: 'State', + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + activeIndex: { + name: 'activeIndex', + description: 'Currently active item index', + table: { + category: 'State', + type: { + summary: 'string | number', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + ref: { + name: 'ref', + description: + 'Forwarded ref that provides access to container methods: `isOpen`, `open()`, `close()`, `toggle() and onSelect()`.', + table: { + category: 'Advanced', + type: { + summary: 'Ref', + detail: + '{ isOpen: boolean; open: () => void; close: () => void; toggle: () => void; onSelect: (event: MouseEvent | KeyboardEvent | ChangeEvent, data: Option) => void}', + }, + }, + }, + initiator: { + name: 'initiator', + description: 'Custom initiator element to trigger the dropdown', + table: { + category: 'Advanced', + type: { + summary: 'ReactNode', + }, + }, + }, + children: { + name: 'children', + description: 'Child elements of the select component', + table: { + category: 'Advanced', + type: { + summary: 'ReactNode', + }, + }, + }, + emptyItemsResult: { + name: 'emptyItemsResult', + description: 'Text or Component to display when no items are available', + table: { + category: 'Advanced', + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + itemsCount: { + name: 'itemsCount', + description: 'Number of items in the select', + table: { + category: 'Advanced', + type: { + summary: 'number', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, + styles: { + name: 'styles', + description: 'Custom styles object for the select', + table: { + category: 'Appearance & Styling', + type: { + summary: 'BoxCssComponentProps', + }, + }, + control: { + type: 'object', + disable: false, + }, + }, + color: { + name: 'color', + description: 'Color variant of the select', + options: ['primary', 'success', 'warning', 'error'], + table: { + category: 'Appearance & Styling', + type: { + summary: 'InputColorVariant', + }, + defaultValue: { + summary: "'primary'", + }, + }, + control: { + type: 'select', + disable: false, + }, + }, + }, + Typography: { + children: { + control: { + type: 'text', + disable: false, + }, + name: 'children', + type: { + name: 'string', + }, + table: { + category: 'Content', + }, + description: + 'The content to render inside the Typography component. Accepts text strings, numbers, React elements, or other Typography components for nested styling. Typography components can be nested to create rich text formatting with different styles.', + }, + variant: { + control: { + type: 'select', + disable: false, + }, + name: 'variant', + type: { + name: 'other', + required: false, + raw: 'EnumOrPrimitive', + value: 'EnumOrPrimitive', + }, + description: + 'Determines the semantic HTML element and corresponding typography styles. Options include headings (h1-h6), body text (p, small), display (div), code (code, kbd), and special elements (span, strong, i, caption, header). Each variant maps to theme tokens for consistent styling.', + table: { + type: { + summary: 'EnumOrPrimitive', + }, + defaultValue: { + summary: 'p', + }, + category: 'Appearance', + }, + options: [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'p', + 'small', + 'div', + 'span', + 'strong', + 'i', + 'code', + 'kbd', + 'caption', + 'header', + 'sup', + 'sub', + ], + }, + align: { + control: { + type: 'select', + disable: false, + }, + name: 'align', + type: { + name: 'other', + required: false, + raw: 'EnumOrPrimitive', + value: 'EnumOrPrimitive', + }, + description: + 'Sets text-align CSS property. Options include "start" (default, respects RTL/LTR), "end", "center", "left", "right", "justify", "match-parent", and CSS keywords (initial, inherit, revert, unset). Use logical values (start/end) for international layouts.', + table: { + type: { + summary: 'EnumOrPrimitive', + }, + defaultValue: { + summary: 'start', + }, + category: 'Layout', + }, + options: [ + 'start', + 'end', + 'center', + 'left', + 'right', + 'justify', + 'match-parent', + 'inherit', + 'initial', + 'revert', + 'revert-layer', + 'unset', + ], + }, + as: { + control: { + type: 'text', + disable: false, + }, + name: 'as', + description: + 'Polymorphic prop that changes the rendered element/component while preserving all Typography styles. Accepts HTML tag names ("div", "span", "label") or React components (Row, Column, Link). Useful for maintaining visual consistency across different semantic elements (e.g., h1 styles on a div for SEO flexibility).', + type: { + required: false, + raw: 'keyof HTMLElementTagNameMap | ElementType', + name: 'union', + value: [ + { + name: 'other', + value: 'HTMLElementTagNameMap', + }, + { + name: 'other', + value: 'ElementType', + }, + ], + }, + table: { + type: { + summary: 'union', + }, + category: 'Behavior', + }, + }, + color: { + control: { + type: 'color', + disable: false, + }, + name: 'color', + description: + 'Controls the text color. Accepts any valid CSS color value (hex, rgb, named colors) or theme token path (e.g., "text.primary", "brand.500"). Defaults to theme token "colors.text.default" for consistent theming. Use "inherit" to inherit color from parent elements.', + type: { + required: false, + name: 'string', + }, + table: { + type: { + summary: 'string', + }, + defaultValue: { + summary: 'text.default', + }, + category: 'Appearance', + }, + }, + size: { + control: { + type: 'select', + disable: false, + }, + name: 'size', + description: + 'Controls font size and line height for Display variant (variant="div") ONLY. Available sizes: xs, sm, md (default), lg, xl. This enables fine-grained control over hero text and marketing headlines. Ignored for all other variants.', + type: { + required: false, + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + table: { + type: { + summary: 'EnumOrPrimitive', + }, + defaultValue: { + summary: 'md', + }, + category: 'Appearance', + }, + options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'], + }, + styleVariant: { + control: { + type: 'multi-select', + disable: false, + }, + name: 'styleVariant', + description: + 'Applies one or more style modifiers to enhance text appearance. Options: "light" (lighter weight), "normal" (normal weight), "semibold" (medium weight), "bold" (bold weight), "italic" (italic style), "small" (smaller font size), "uppercase", "lowercase", "underline", "strike" (strikethrough). Multiple values can be combined for rich formatting (e.g., ["bold", "italic", "uppercase"]).', + type: { + required: false, + raw: 'EnumOrPrimitive | EnumOrPrimitive[]', + name: 'union', + value: [ + { + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + { + raw: 'EnumOrPrimitive[]', + name: 'array', + value: [ + { + raw: 'EnumOrPrimitive', + name: 'other', + value: 'EnumOrPrimitive', + }, + ], + }, + ], + }, + table: { + type: { + summary: 'union', + }, + defaultValue: { + summary: '[]', + }, + category: 'Appearance', + }, + options: [ + 'light', + 'normal', + 'semibold', + 'bold', + 'italic', + 'small', + 'uppercase', + 'lowercase', + 'underline', + 'strike', + ], + }, + styles: { + name: 'styles', + table: { + defaultValue: { + summary: '{}', + }, + category: 'Appearance', + }, + description: + 'Custom CSS styles object for one-off styling needs. Use sparingly—prefer variant and styleVariant props for consistent design. Accepts Emotion CSS object syntax with support for nested selectors, pseudo-classes, and media queries.', + control: { + type: 'object', + disable: false, + }, + }, + }, + Counter: { + min: { + control: { + type: 'number', + }, + name: 'min', + description: 'Minimum allowed value', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '1', + }, + category: 'Value & Constraints', + }, + }, + max: { + control: { + type: 'number', + }, + name: 'max', + description: 'Maximum allowed value', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '999', + }, + category: 'Value & Constraints', + }, + }, + initial: { + control: { + type: 'number', + }, + name: 'initial', + description: 'Initial value of the counter', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '1', + }, + category: 'Value & Constraints', + }, + }, + isDisabled: { + control: { + type: 'boolean', + disable: false, + }, + name: 'isDisabled', + description: 'Disables the counter component, preventing user interaction', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'false', + }, + category: 'Value & Constraints', + }, + }, + onCounterChange: { + name: 'onCounterChange', + description: 'Callback function triggered when counter value changes', + type: { + required: false, + raw: '(qty: number) => void', + name: 'function', + }, + table: { + type: { + summary: '(value: number) => void', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Events', + }, + }, + styles: { + name: 'styles', + description: 'CSS properties applied to the counter container', + table: { + category: 'Styling', + type: { + summary: 'React.CSSProperties', + }, + defaultValue: { + summary: '{}', + }, + }, + }, + }, + Menu: { + onSelect: { + control: { + type: 'object', + }, + name: 'onSelect', + type: { + name: 'other', + required: false, + value: 'SelectOnSelect', + }, + description: 'Callback fired when a menu item is selected. Receives the selected item data', + table: { + type: { + summary: '(props: OnSelectProps) => void', + detail: 'OnSelectProps = { event: SyntheticBaseEvent, data: { name: string, value: unknown }', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Behavior & Interaction', + }, + action: 'selected', + }, + itemIdentifier: { + control: { + type: 'object', + }, + name: 'itemIdentifier', + description: 'Function to extract unique identifier from menu items for selection tracking', + type: { + required: false, + name: 'other', + value: 'ItemIdentifier', + }, + table: { + type: { + summary: '(item: unknown) => string | number', + }, + defaultValue: { + summary: 'undefined', + detail: '(selected, current) => selected?.value === current?.value', + }, + category: 'Behavior & Interaction', + }, + }, + content: { + control: { + type: 'object', + }, + name: 'content', + description: 'Menu content to be rendered inside the dropdown (typically DropdownItem components)', + type: { + required: false, + name: 'other', + value: 'ReactNode', + }, + table: { + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: 'undefined', + }, + category: 'Content & Trigger', + }, + }, + closeOnSelect: { + control: { + type: 'boolean', + disable: false, + }, + name: 'closeOnSelect', + description: + 'Whether to close the menu when an option is clicked. Set to false to keep menu open for multiple selections', + type: { + required: false, + name: 'boolean', + }, + table: { + type: { + summary: 'boolean', + }, + defaultValue: { + summary: 'true', + }, + category: 'Behavior & Interaction', + }, + }, + offsetX: { + control: { + type: 'number', + disable: false, + }, + name: 'offsetX', + description: + 'Horizontal distance in pixels between the menu and the trigger element. Used for positioning calculations', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '4', + }, + category: 'Menu Options (Positioning & Dimensions)', + }, + }, + offsetY: { + control: { + type: 'number', + disable: false, + }, + name: 'offsetY', + description: + 'Vertical distance in pixels between the menu and the trigger element. Used for positioning calculations', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '4', + }, + category: 'Menu Options (Positioning & Dimensions)', + }, + }, + minHeight: { + control: { + type: 'number', + disable: false, + }, + name: 'minHeight', + description: + 'Minimum height constraint for the menu dropdown in pixels. Used for dynamic height calculations based on available viewport space', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '80', + }, + category: 'Menu Options (Positioning & Dimensions)', + }, + }, + maxHeight: { + control: { + type: 'number', + disable: false, + }, + name: 'maxHeight', + description: + 'Maximum height constraint for the menu dropdown in pixels. Used for dynamic height calculations based on available viewport space', + type: { + required: false, + name: 'number', + }, + table: { + type: { + summary: 'number', + }, + defaultValue: { + summary: '400', + }, + category: 'Menu Options (Positioning & Dimensions)', + }, + }, + placement: { + control: { + type: 'select', + disable: false, + }, + options: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + name: 'placement', + description: 'Position of the menu relative to the trigger. Defaults to "bottom-right" if not specified', + type: { + required: false, + raw: "'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'", + name: 'enum', + value: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], + }, + table: { + type: { + summary: "'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'", + }, + defaultValue: { + summary: "'bottom-right'", + }, + category: 'Menu Options (Positioning & Dimensions)', + }, + }, + children: { + name: 'children', + description: 'Trigger element that opens the menu when clicked (button, text, icon, etc.)', + table: { + category: 'Content & Trigger', + type: { + summary: 'ReactNode', + }, + defaultValue: { + summary: 'undefined', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + width: { + name: 'width', + description: 'Sets the width of the menu container', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + height: { + name: 'height', + description: 'Sets the height of the menu container', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + minWidth: { + name: 'minWidth', + description: 'CSS min-width property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + maxWidth: { + name: 'maxWidth', + description: 'CSS max-width property', + table: { + category: 'Box Styles - Layout & Sizing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + margin: { + name: 'margin', + description: 'CSS margin property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginTop: { + name: 'marginTop', + description: 'CSS margin-top property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginRight: { + name: 'marginRight', + description: 'CSS margin-right property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginBottom: { + name: 'marginBottom', + description: 'CSS margin-bottom property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + marginLeft: { + name: 'marginLeft', + description: 'CSS margin-left property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + padding: { + name: 'padding', + description: 'CSS padding property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingTop: { + name: 'paddingTop', + description: 'CSS padding-top property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingRight: { + name: 'paddingRight', + description: 'CSS padding-right property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingBottom: { + name: 'paddingBottom', + description: 'CSS padding-bottom property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + paddingLeft: { + name: 'paddingLeft', + description: 'CSS padding-left property', + table: { + category: 'Box Styles - Spacing', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexDirection: { + name: 'flexDirection', + description: 'CSS flex-direction property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + justifyContent: { + name: 'justifyContent', + description: 'CSS justify-content property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + justifySelf: { + name: 'justifySelf', + description: 'CSS justify-self property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignItems: { + name: 'alignItems', + description: 'CSS align-items property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignSelf: { + name: 'alignSelf', + description: 'CSS align-self property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + alignContent: { + name: 'alignContent', + description: 'CSS align-content property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexWrap: { + name: 'flexWrap', + description: 'CSS flex-wrap property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexGrow: { + name: 'flexGrow', + description: 'CSS flex-grow property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexShrink: { + name: 'flexShrink', + description: 'CSS flex-shrink property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flexBasis: { + name: 'flexBasis', + description: 'CSS flex-basis property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + flex: { + name: 'flex', + description: 'CSS flex property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + order: { + name: 'order', + description: 'CSS order property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + gap: { + name: 'gap', + description: 'CSS gap property', + table: { + category: 'Box Styles - Flexbox', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + display: { + name: 'display', + options: ['block', 'flex', 'inline', 'inline-flex', 'grid', 'none'], + description: 'CSS display property to control the layout behavior', + table: { + type: { + summary: 'string', + }, + category: 'Box Styles - Position & Display', + }, + control: { + type: 'select', + disable: false, + }, + }, + position: { + name: 'position', + description: 'CSS position property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + top: { + name: 'top', + description: 'CSS top property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + right: { + name: 'right', + description: 'CSS right property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + bottom: { + name: 'bottom', + description: 'CSS bottom property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + left: { + name: 'left', + description: 'CSS left property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + overflow: { + name: 'overflow', + description: 'CSS overflow property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + zIndex: { + name: 'zIndex', + description: 'CSS z-index property', + table: { + category: 'Box Styles - Position & Display', + type: { + summary: 'string | number', + }, + }, + control: { + type: 'text', + disable: false, + }, + }, + styles: { + name: 'styles', + description: 'Custom CSS styles object to be applied to the menu dropdown container', + table: { + category: 'Custom Styling', + type: { + summary: 'CSSObject', + }, + defaultValue: { + summary: 'undefined', + }, + }, + control: { + type: 'object', + disable: false, + }, + }, + }, +} as const; diff --git a/libs/web-components/stories/react-storybook-arg-types.snapshot.json b/libs/web-components/stories/react-storybook-arg-types.snapshot.json new file mode 100644 index 0000000..779286b --- /dev/null +++ b/libs/web-components/stories/react-storybook-arg-types.snapshot.json @@ -0,0 +1,7833 @@ +{ + "Toggle": { + "items": { + "control": { + "type": "object", + "disable": false + }, + "name": "items", + "type": { + "name": "union", + "value": [ + { + "raw": "string[]", + "name": "array", + "value": [ + { + "name": "string" + } + ] + }, + { + "raw": "ToggleItem[]", + "name": "array", + "value": [ + { + "name": "other", + "value": "ToggleItem" + } + ] + } + ], + "required": false, + "raw": "string[] | ToggleItem[]" + }, + "description": "List of items to toggle between", + "table": { + "type": { + "summary": "Array" + }, + "defaultValue": { + "summary": "[]" + } + } + }, + "value": { + "control": { + "type": "text", + "disable": false + }, + "name": "value", + "type": { + "name": "other", + "required": false, + "value": "unknown" + }, + "description": "Currently selected value", + "table": { + "type": { + "summary": "string | unknown" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "onValueChange": { + "name": "onValueChange", + "type": { + "name": "function", + "required": false, + "raw": "(value: T) => void" + }, + "description": "Function triggered when the value changes", + "table": { + "type": { + "summary": "(value: unknown) => void" + }, + "defaultValue": { + "summary": "undefined" + } + }, + "action": "value changed" + }, + "disabled": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "disabled", + "description": "Disables interaction with the component", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + } + } + }, + "renderItemContent": { + "name": "renderItemContent", + "description": "Custom render function for each item", + "type": { + "required": false, + "raw": "(item: ToggleItem | string, index: number) => ReactNode", + "name": "function" + }, + "table": { + "type": { + "summary": "(item: unknown) => ReactNode" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "styles": { + "name": "styles", + "description": "Custom styles for the component", + "table": { + "type": { + "summary": "InlineBoxStyles" + }, + "defaultValue": { + "summary": "{}" + } + } + } + }, + "Truncate": { + "children": { + "control": { + "type": "text", + "disable": true + }, + "name": "children", + "type": { + "name": "string" + }, + "table": { + "category": "Content" + }, + "description": "ReactNode content to display. Accepts string text or any ReactNode for flexible content." + }, + "lines": { + "control": { + "type": "number", + "disable": false + }, + "name": "lines", + "description": "Number of lines before truncation (default: 1)", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "1" + }, + "category": "Truncation" + } + }, + "styles": { + "control": { + "type": "object", + "disable": false + }, + "name": "styles", + "description": "Custom CSS styles object for one-off styling needs. Accepts Emotion CSS object syntax with support for nested selectors, pseudo-classes, and media queries.", + "type": { + "required": false + }, + "table": { + "defaultValue": { + "summary": "{}" + }, + "category": "Appearance" + } + }, + "ref": { + "name": "ref", + "description": "Forwarded ref providing imperative access to truncation state. Exposes `isTruncated` (boolean) indicating if content is truncated. Useful for conditionally showing tooltips or for analytics.", + "table": { + "category": "Ref API", + "type": { + "summary": "Ref", + "detail": "TruncateRef: { ref: Ref; isTruncated: boolean; }" + } + }, + "control": { + "disable": true + } + } + }, + "Wrapper": { + "variant": { + "control": { + "type": "select", + "disable": false + }, + "name": "variant", + "type": { + "name": "other", + "required": false, + "raw": "EnumOrPrimitive", + "value": "EnumOrPrimitive" + }, + "description": "Determines the semantic HTML element and associated styles for different layout contexts.", + "table": { + "type": { + "summary": "WrapperVariant" + }, + "defaultValue": { + "summary": "inline" + } + }, + "options": ["Inline", "Section", "FullPage"] + }, + "children": { + "control": { + "type": "text", + "disable": false + }, + "name": "children", + "type": { + "name": "string" + }, + "description": "Content to be rendered inside the wrapper. Can be text, components, or other elements.", + "table": { + "type": { + "summary": "ReactNode" + } + } + }, + "styles": { + "control": { + "type": "object", + "disable": false + }, + "name": "styles", + "type": { + "name": "object", + "value": { + "padding": { + "name": "string" + }, + "border": { + "name": "string" + }, + "borderRadius": { + "name": "string" + }, + "backgroundColor": { + "name": "string" + } + } + }, + "description": "Custom CSS styles object to override or extend default wrapper styling.", + "table": { + "type": { + "summary": "CSSProperties" + } + } + }, + "as": { + "control": { + "type": "text", + "disable": false + }, + "name": "as", + "description": "Overrides the default wrapper HTML element used for rendering (e.g., \"div\", \"section\", \"article\").", + "type": { + "required": false, + "raw": "keyof HTMLElementTagNameMap | ElementType", + "name": "union", + "value": [ + { + "name": "other", + "value": "HTMLElementTagNameMap" + }, + { + "name": "other", + "value": "ElementType" + } + ] + }, + "table": { + "type": { + "summary": "keyof HTMLElementTagNameMap | ElementType" + } + } + }, + "className": { + "name": "className", + "description": "Additional CSS class names to be applied to the wrapper element.", + "table": { + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "display": { + "name": "display", + "options": ["block", "flex", "inline", "inline-flex", "grid"], + "description": "CSS display property to control the layout behavior of the wrapper.", + "table": { + "type": { + "summary": "string" + } + }, + "control": { + "type": "select", + "disable": false + } + }, + "position": { + "name": "position", + "options": ["static", "relative", "absolute", "fixed", "sticky"], + "description": "CSS position property to control the positioning context of the wrapper.", + "table": { + "type": { + "summary": "string" + } + }, + "control": { + "type": "select", + "disable": false + } + } + }, + "Slider": { + "min": { + "control": { + "type": "number" + }, + "name": "min", + "type": { + "name": "number", + "required": false + }, + "description": "Minimum value of the slider range", + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "1" + } + } + }, + "max": { + "control": { + "type": "number" + }, + "name": "max", + "type": { + "name": "number", + "required": false + }, + "description": "Maximum value of the slider range", + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "100" + } + } + }, + "disabled": { + "control": { + "type": "boolean" + }, + "name": "disabled", + "type": { + "name": "boolean", + "required": false + }, + "description": "Whether the slider is disabled", + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + } + } + }, + "styles": { + "control": { + "type": "object" + }, + "name": "styles", + "type": { + "name": "object", + "value": {} + }, + "description": "Custom styles to apply to the slider component", + "table": { + "type": { + "summary": "React.CSSProperties" + }, + "defaultValue": { + "summary": "{}" + } + } + }, + "value": { + "control": { + "type": "number" + }, + "name": "value", + "type": { + "name": "number", + "required": false + }, + "description": "Current value of the slider", + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "1" + } + } + }, + "onChange": { + "name": "onChange", + "type": { + "name": "function", + "required": false, + "raw": "(value: number) => void" + }, + "description": "Callback function triggered when slider value changes", + "table": { + "type": { + "summary": "(value: number) => void" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "step": { + "control": { + "type": "number" + }, + "name": "step", + "description": "", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + } + } + } + }, + "SliderDots": { + "count": { + "control": { + "type": "number", + "disable": false + }, + "name": "count", + "type": { + "name": "number", + "required": true + }, + "description": "Total number of dots to render", + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "0" + }, + "category": "Core Properties" + } + }, + "activeIndex": { + "control": { + "type": "number", + "disable": false + }, + "name": "activeIndex", + "type": { + "name": "number", + "required": false + }, + "description": "Index of the currently active dot (zero-based)", + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "0" + }, + "category": "Core Properties" + } + }, + "onDotClick": { + "name": "onDotClick", + "type": { + "name": "function", + "required": false, + "raw": "(index: number) => void" + }, + "description": "Callback fired when a dot is clicked, receives the dot index", + "table": { + "type": { + "summary": "(index: number) => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + }, + "action": "Dot clicked" + } + }, + "Switch": { + "name": { + "control": { + "type": "text" + }, + "name": "name", + "type": { + "name": "string", + "required": false + }, + "description": "Name attribute of the switch input", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "switch" + }, + "category": "Identification" + } + }, + "disabled": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "disabled", + "type": { + "name": "boolean", + "required": false + }, + "description": "Disables the switch interaction", + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "State" + } + }, + "children": { + "control": { + "type": "text" + }, + "name": "children", + "type": { + "name": "string" + }, + "description": "Label content for the switch", + "table": { + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "Label" + }, + "category": "Content" + } + }, + "onValueChange": { + "name": "onValueChange", + "type": { + "name": "function", + "required": false, + "raw": "(value: boolean) => void" + }, + "description": "Callback fired when the switch value changes", + "table": { + "type": { + "summary": "(checked: boolean) => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + }, + "action": "Switch value changed" + }, + "label": { + "control": { + "type": "radio" + }, + "options": ["left ", " right"], + "name": "label", + "description": "Position of the label relative to the switch", + "type": { + "required": false, + "raw": "'left' | 'right'", + "name": "enum", + "value": ["left", "right"] + }, + "table": { + "type": { + "summary": "LabelPosition", + "detail": "left | right" + }, + "defaultValue": { + "summary": "right" + }, + "category": "Layout" + } + }, + "isLoading": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "isLoading", + "description": "Shows loading state and disables the switch", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "State" + } + }, + "checked": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "checked", + "description": "Controls the checked state of the switch", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "State" + } + } + }, + "Textarea": { + "name": { + "control": { + "type": "text", + "disable": false + }, + "name": "name", + "type": { + "name": "string" + }, + "description": "Input field name attribute", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "textarea" + }, + "category": "Basics" + } + }, + "placeholder": { + "control": { + "type": "text", + "disable": false + }, + "name": "placeholder", + "type": { + "name": "string" + }, + "description": "Placeholder text", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Basics" + } + }, + "defaultValue": { + "control": { + "type": "text", + "disable": false + }, + "name": "defaultValue", + "type": { + "name": "string" + }, + "description": "Initial textarea content", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Basics" + } + }, + "onChange": { + "name": "onChange", + "type": { + "name": "function" + }, + "action": "changed", + "description": "Callback when textarea content changes", + "table": { + "type": { + "summary": "(event: ChangeEvent) => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + } + }, + "resize": { + "control": { + "type": "select", + "disable": false + }, + "name": "resize", + "description": "Controls how the textarea can be resized", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "TextareaResize" + }, + "defaultValue": { + "summary": "TextareaResize.None" + }, + "category": "Behavior" + }, + "options": ["none", "both", "horizontal", "vertical"] + }, + "variant": { + "control": { + "type": "select", + "disable": false + }, + "options": ["default", "inline"], + "name": "variant", + "description": "Visual variant of the textarea", + "type": { + "required": false, + "raw": "'default' | 'inline'", + "name": "enum", + "value": ["default", "inline"] + }, + "table": { + "type": { + "summary": "TextareaVariant" + }, + "defaultValue": { + "summary": "'default'" + }, + "category": "Styling" + } + }, + "color": { + "control": { + "type": "select", + "disable": false + }, + "name": "color", + "description": "Color variant of the textarea", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "InputColorVariant" + }, + "defaultValue": { + "summary": "'primary'" + }, + "category": "Styling" + }, + "options": ["primary", "success", "warning", "error"] + }, + "ariaDescribedBy": { + "control": { + "type": "text", + "disable": false + }, + "name": "ariaDescribedBy", + "description": "ID of element that describes this textarea", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Accessibility" + } + }, + "dynamicHeightAdjustment": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "dynamicHeightAdjustment", + "description": "Enable/disable dynamic height adjustment", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Behavior" + } + }, + "onCustomResize": { + "name": "onCustomResize", + "description": "Callback triggered when textarea is manually resized by user", + "type": { + "required": false, + "raw": "(newSize: { height: number; width: number }) => void", + "name": "function" + }, + "table": { + "type": { + "summary": "(newSize: { height: number; width: number }) => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + }, + "action": "resized" + }, + "minHeight": { + "control": { + "type": "text", + "disable": false + }, + "name": "minHeight", + "description": "Minimum height of textarea", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Sizing & Layout" + } + }, + "maxHeight": { + "control": { + "type": "text", + "disable": false + }, + "name": "maxHeight", + "description": "Maximum height of textarea", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Sizing & Layout" + } + }, + "maxCharacters": { + "control": { + "type": "number" + }, + "name": "maxCharacters", + "description": "", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + } + } + }, + "autoFocus": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "autoFocus", + "description": "Whether the textarea should auto focus", + "type": { + "required": false + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Accessibility" + } + }, + "value": { + "name": "value", + "description": "Controlled value of the textarea", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Basics" + }, + "control": { + "type": "text", + "disable": false + } + }, + "disabled": { + "name": "disabled", + "description": "Whether the textarea is disabled", + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Accessibility" + }, + "control": { + "type": "boolean", + "disable": false + } + }, + "readOnly": { + "name": "readOnly", + "description": "Whether the textarea is read only", + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Accessibility" + }, + "control": { + "type": "boolean", + "disable": false + } + }, + "rows": { + "name": "rows", + "description": "Number of rows", + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Sizing & Layout" + }, + "control": { + "type": "number", + "disable": false + } + }, + "maxLength": { + "name": "maxLength", + "description": "Maximum character limit", + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Behavior" + }, + "control": { + "type": "number", + "disable": false + } + }, + "styles": { + "name": "styles", + "description": "Custom CSS styles to override default styling", + "table": { + "type": { + "summary": "CSSProperties" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Styling" + }, + "control": { + "type": "object", + "disable": false + } + } + }, + "Separator": { + "orientation": { + "control": { + "type": "select", + "disable": false + }, + "name": "orientation", + "description": "The orientation of the separator.", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "Orientation" + }, + "defaultValue": { + "summary": "horizontal" + } + }, + "options": ["vertical", "horizontal"] + }, + "length": { + "control": { + "type": "text", + "disable": false + }, + "name": "length", + "description": "The length of the separator (e.g., \"100px\", \"50%\").", + "type": { + "required": false, + "name": "other", + "value": "literal" + }, + "table": { + "type": { + "summary": "`${number}${Unit}`" + } + } + }, + "color": { + "control": { + "type": "text", + "disable": false + }, + "name": "color", + "description": "The color of the separator line. Accepts any valid CSS color value (hex, rgb, named colors) or theme token path / palette-style alias (e.g., \"border.error\", \"text.secondary\", \"brand.500\", \"theme.palette.warning.main\").", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + } + } + }, + "size": { + "control": { + "type": "select", + "disable": false + }, + "name": "size", + "description": "The thickness/size of the separator line.", + "type": { + "required": false, + "name": "other", + "value": "SizeVariant" + }, + "table": { + "type": { + "summary": "SizeVariant" + } + }, + "options": ["xs", "sm", "md", "lg", "xl", "xxl"] + }, + "variant": { + "control": { + "type": "select", + "disable": false + }, + "name": "variant", + "description": "The visual style variant of the separator (e.g., \"solid\", \"dashed\").", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "SeparatorVariant" + } + }, + "options": ["solid", "dashed", "dotted"] + }, + "as": { + "control": { + "type": "select", + "disable": false + }, + "options": ["div", "hr", "span"], + "name": "as", + "description": "The HTML element to render the separator as.", + "type": { + "required": false, + "raw": "'div' | 'hr' | 'span'", + "name": "enum", + "value": ["div", "hr", "span"] + }, + "table": { + "type": { + "summary": "'div' | 'hr' | 'span'" + }, + "defaultValue": { + "summary": "div" + } + } + }, + "label": { + "control": { + "type": "text", + "disable": false + }, + "name": "label", + "description": "Text to display within the separator.", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + } + } + }, + "labelPosition": { + "control": { + "type": "select", + "disable": false + }, + "name": "labelPosition", + "description": "The position of the label when present.", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "SeparatorLabelPosition" + }, + "defaultValue": { + "summary": "center" + } + }, + "options": ["start", "center", "end"] + }, + "labelColor": { + "control": { + "type": "text", + "disable": false + }, + "name": "labelColor", + "description": "The color of the label text. Accepts any valid CSS color value (hex, rgb, named colors) or theme token path / palette-style alias (e.g., \"text.caption\", \"text.secondary\", \"brand.500\").", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + } + } + }, + "styles": { + "name": "styles", + "description": "Custom styles for the separator.", + "table": { + "type": { + "summary": "CSSProperties" + } + }, + "subControls": { + "backgroundColor": { + "control": { + "type": "color" + }, + "description": "Background color of the separator.", + "table": { + "type": { + "summary": "string" + } + } + }, + "border": { + "control": { + "type": "text" + }, + "description": "Border style of the separator.", + "table": { + "type": { + "summary": "string" + } + } + }, + "margin": { + "control": { + "type": "text" + }, + "description": "Margin around the separator.", + "table": { + "type": { + "summary": "string" + } + } + }, + "padding": { + "control": { + "type": "text" + }, + "description": "Padding within the separator.", + "table": { + "type": { + "summary": "string" + } + } + } + }, + "control": { + "type": "object", + "disable": false + } + } + }, + "Skeleton": { + "width": { + "control": { + "type": "text", + "disable": false + }, + "name": "width", + "type": { + "name": "string", + "required": false + }, + "description": "Sets the width of the skeleton. Accepts any valid CSS width value.", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "auto" + } + } + }, + "height": { + "control": { + "type": "text", + "disable": false + }, + "name": "height", + "type": { + "name": "string", + "required": false + }, + "description": "Sets the height of the skeleton. Accepts any valid CSS height value.", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "auto" + } + } + }, + "variant": { + "control": { + "type": "select", + "disable": false + }, + "name": "variant", + "type": { + "name": "other", + "required": false, + "raw": "EnumOrPrimitive", + "value": "EnumOrPrimitive" + }, + "description": "Controls the shape of the skeleton.", + "table": { + "type": { + "summary": "select" + }, + "defaultValue": { + "summary": "rounded" + } + }, + "options": ["rectangular", "rounded", "circular"] + }, + "animationName": { + "control": { + "type": "text", + "disable": false + }, + "name": "animationName", + "description": "Overrides the default animation keyframe name. Accepts theme animation token names such as \"blinkKeyframes\", raw CSS animation names, or `null` to disable the built-in animation.", + "type": { + "required": false, + "raw": "NullableType", + "name": "other", + "value": "NullableType" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "blinkKeyframes" + } + } + }, + "animationProps": { + "control": { + "type": "text", + "disable": false + }, + "name": "animationProps", + "description": "Custom animation timing string appended to `animationName` (for example \"1200ms ease-in-out 0ms infinite\").", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + } + } + }, + "backgroundColor": { + "control": { + "type": "text", + "disable": false + }, + "name": "backgroundColor", + "description": "Sets the skeleton fill color. Accepts any valid CSS color value (hex, rgb, named colors) or theme token path / palette-style alias (e.g., \"bg.fill.success.primary.default\", \"brand.500\", \"theme.palette.success.main\").", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + } + } + }, + "className": { + "name": "className", + "description": "Provides a way to apply custom CSS classes, such as Tailwind utility classes for animations.", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "children": { + "name": "children", + "description": "Content to be rendered inside the skeleton wrapper.", + "table": { + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "undefined" + } + }, + "control": { + "type": "object", + "disable": false + } + }, + "styles": { + "name": "styles", + "description": "Custom CSS properties to apply to the skeleton.", + "table": { + "type": { + "summary": "CSSProperties" + }, + "defaultValue": { + "summary": "{}" + } + }, + "subControls": { + "backgroundColor": { + "control": "color", + "description": "Background color of the skeleton.", + "table": { + "type": { + "summary": "string" + } + } + }, + "borderRadius": { + "control": "text", + "description": "Border radius of the skeleton.", + "table": { + "type": { + "summary": "string" + } + } + }, + "opacity": { + "control": "number", + "description": "Opacity of the skeleton.", + "table": { + "type": { + "summary": "number" + } + } + } + }, + "control": { + "type": "object", + "disable": false + } + } + }, + "Icon": { + "name": { + "name": "name", + "description": "The name of the icon to display from the registered list.", + "type": { + "required": true, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Icon Selection" + }, + "options": [ + "cross", + "success", + "error", + "warning", + "info", + "dot", + "check", + "arrowDown", + "arrowRight", + "arrowLeft", + "mobileMenu", + "home", + "slash", + "arrowForward", + "localShipping", + "favorite", + "favoriteOutlined", + "deleteOutlined", + "accountCircle", + "shoppingBag", + "errorOutline", + "star", + "starOutlined", + "starHalf", + "minus", + "plus", + "filter", + "ruler", + "processing", + "paymentCard", + "eye", + "attachment", + "upload", + "folder", + "folderOpen", + "wifiTethering", + "portrait", + "search", + "edit", + "volumeUp", + "contentCopy", + "thumbUp", + "thumbDown", + "fileCopy", + "keyboardArrowDown", + "keyboardArrowUp", + "send", + "thumbUpFilled", + "thumbDownFilled", + "chat", + "chatBubble", + "phone", + "mic", + "fullscreen", + "fullscreenExit" + ], + "control": { + "type": "select", + "disable": false + } + }, + "width": { + "name": "width", + "description": "Sets the width of the icon in pixels.", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Size & Dimensions" + }, + "control": { + "type": "number", + "disable": false + } + }, + "height": { + "name": "height", + "description": "Sets the height of the icon in pixels.", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Size & Dimensions" + }, + "control": { + "type": "number", + "disable": false + } + }, + "fill": { + "name": "fill", + "description": "Sets the `fill` color for specific paths within the SVG that are designed to inherit it.", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "currentColor" + }, + "category": "Styling & Colors" + }, + "control": { + "type": "color", + "disable": false + } + }, + "fillSvg": { + "name": "fillSvg", + "description": "Sets the `fill` color of the SVG element itself. Useful for single-color icons.", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "none" + }, + "category": "Styling & Colors" + }, + "control": { + "type": "color", + "disable": false + } + }, + "onClick": { + "name": "onClick", + "description": "Event handler for click events on the icon.", + "type": { + "required": false, + "raw": "MouseEventHandler", + "name": "other", + "value": "MouseEventHandler" + }, + "table": { + "type": { + "summary": "MouseEventHandler" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Interactions" + }, + "action": "clicked" + }, + "size": { + "name": "size", + "description": "Predefined size variants that set both width and height according to the theme configuration.", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "SizeVariant" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Size & Dimensions" + }, + "options": ["xs", "sm", "md", "lg", "xl", "xxl"], + "control": { + "type": "select", + "disable": false + } + }, + "minWidth": { + "name": "minWidth", + "description": "CSS min-width property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "maxWidth": { + "name": "maxWidth", + "description": "CSS max-width property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "minHeight": { + "name": "minHeight", + "description": "CSS min-height property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "maxHeight": { + "name": "maxHeight", + "description": "CSS max-height property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "margin": { + "name": "margin", + "description": "CSS margin property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginTop": { + "name": "marginTop", + "description": "CSS margin-top property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginRight": { + "name": "marginRight", + "description": "CSS margin-right property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginBottom": { + "name": "marginBottom", + "description": "CSS margin-bottom property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginLeft": { + "name": "marginLeft", + "description": "CSS margin-left property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "padding": { + "name": "padding", + "description": "CSS padding property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingTop": { + "name": "paddingTop", + "description": "CSS padding-top property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingRight": { + "name": "paddingRight", + "description": "CSS padding-right property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingBottom": { + "name": "paddingBottom", + "description": "CSS padding-bottom property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingLeft": { + "name": "paddingLeft", + "description": "CSS padding-left property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexDirection": { + "name": "flexDirection", + "description": "CSS flex-direction property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "justifyContent": { + "name": "justifyContent", + "description": "CSS justify-content property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignItems": { + "name": "alignItems", + "description": "CSS align-items property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignContent": { + "name": "alignContent", + "description": "CSS align-content property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexWrap": { + "name": "flexWrap", + "description": "CSS flex-wrap property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "gap": { + "name": "gap", + "description": "CSS gap property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flex": { + "name": "flex", + "description": "CSS flex property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexGrow": { + "name": "flexGrow", + "description": "CSS flex-grow property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexShrink": { + "name": "flexShrink", + "description": "CSS flex-shrink property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexBasis": { + "name": "flexBasis", + "description": "CSS flex-basis property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignSelf": { + "name": "alignSelf", + "description": "CSS align-self property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "justifySelf": { + "name": "justifySelf", + "description": "CSS justify-self property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "order": { + "name": "order", + "description": "CSS order property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "position": { + "name": "position", + "description": "CSS position property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "top": { + "name": "top", + "description": "CSS top property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "right": { + "name": "right", + "description": "CSS right property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "bottom": { + "name": "bottom", + "description": "CSS bottom property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "left": { + "name": "left", + "description": "CSS left property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "overflow": { + "name": "overflow", + "description": "CSS overflow property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "zIndex": { + "name": "zIndex", + "description": "CSS z-index property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "border": { + "name": "border", + "description": "CSS border property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "styles": { + "name": "styles", + "description": "Custom styles object for the icon", + "table": { + "category": "Custom Styling", + "type": { + "summary": "CSSProperties" + } + }, + "control": { + "type": "object", + "disable": false + } + } + }, + "InputFile": { + "accept": { + "control": { + "type": "select", + "disable": false + }, + "name": "accept", + "type": { + "name": "string", + "value": {}, + "required": false + }, + "description": "Accepted file types (MIME types or file extensions)", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "File Handling" + }, + "options": ["image/*", "video/*", "audio/*", ".pdf", ".doc,.docx", ".txt"] + }, + "capture": { + "control": { + "type": "select", + "disable": false + }, + "name": "capture", + "type": { + "name": "union", + "value": [ + { + "name": "boolean" + }, + { + "name": "other", + "value": "literal" + }, + { + "name": "other", + "value": "literal" + } + ], + "required": false, + "raw": "boolean | 'user' | 'environment'" + }, + "description": "Preferred media capture method on mobile devices", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Mobile Features" + }, + "options": ["user", "environment"] + }, + "multiple": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "multiple", + "type": { + "name": "boolean", + "required": false + }, + "description": "Allow selection of multiple files", + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "File Handling" + } + }, + "disabled": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "disabled", + "type": { + "name": "boolean", + "required": false + }, + "description": "Disable the input and button", + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "State" + } + }, + "onClick": { + "name": "onClick", + "type": { + "name": "function" + }, + "action": "button clicked", + "description": "Called when the button is clicked", + "table": { + "disable": true, + "type": { + "summary": "(event: MouseEvent) => void" + }, + "category": "Events" + } + }, + "onChange": { + "name": "onChange", + "type": { + "name": "function", + "required": false, + "raw": "(event: ChangeEvent) => void" + }, + "description": "Called when files are selected", + "table": { + "type": { + "summary": "(event: ChangeEvent) => void" + }, + "disable": true, + "category": "Events" + }, + "action": "file(s) selected" + }, + "buttonProps": { + "control": { + "type": "object", + "disable": false + }, + "name": "buttonProps", + "description": "Props to customize the Button component", + "type": { + "required": false, + "name": "other", + "value": "ButtonProps" + }, + "table": { + "type": { + "summary": "ButtonProps", + "detail": "ButtonProps {\n variant?: 'text' | 'outlined' | 'contained';\n color?: 'primary' | 'secondary' | 'error';\n iconStart?: ReactNode;\n iconEnd?: ReactNode;\n onClick?: (event: MouseEvent) => void;\n type?: 'button' | 'submit' | 'reset';\n disabled?: boolean;\n fullWidth?: boolean;\n isIcon?: boolean;\n ariaLabel?: string;\n ariaPressed?: boolean;\n role?: 'button' | 'menuitem' | 'switch';\n tabIndex?: number;\n }" + }, + "defaultValue": { + "summary": "{}" + }, + "category": "Button Customization" + } + }, + "isIcon": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "isIcon", + "description": "Render the button as an icon-only button (no text label, square sizing)", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Button Customization" + } + }, + "styles": { + "control": { + "type": "object", + "disable": false + }, + "name": "styles", + "description": "CSS style overrides for the wrapper element", + "type": { + "required": false + }, + "table": { + "type": { + "summary": "CSSProperties" + }, + "defaultValue": { + "summary": "{}" + }, + "category": "Styling" + } + }, + "children": { + "control": { + "type": "text", + "disable": false + }, + "name": "children", + "description": "Content for the button label (text or components)", + "type": { + "required": false + }, + "table": { + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "Browse Files" + }, + "category": "Content" + } + } + }, + "Label": { + "children": { + "control": { + "type": "text", + "disable": false + }, + "name": "children", + "type": { + "name": "string" + }, + "description": "Content to be rendered inside the label", + "table": { + "category": "Content", + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "onClick": { + "name": "onClick", + "description": "Click event handler for the label", + "type": { + "required": false, + "raw": "(event: MouseEvent) => void", + "name": "function" + }, + "table": { + "type": { + "summary": "(event: React.MouseEvent) => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + }, + "action": "clicked" + }, + "ariaLabel": { + "control": { + "type": "text", + "disable": false + }, + "name": "ariaLabel", + "description": "Accessible label for screen readers", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "htmlFor": { + "control": { + "type": "text", + "disable": false + }, + "name": "htmlFor", + "description": "Associates the label with form control", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Core Properties" + } + }, + "gap": { + "name": "gap", + "description": "Gap between label elements", + "table": { + "category": "Layout & Spacing", + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "4px" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "className": { + "name": "className", + "description": "Additional CSS classes to apply", + "table": { + "category": "Styling", + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "styles": { + "name": "styles", + "description": "Custom inline styles to apply to the label", + "table": { + "category": "Styling", + "type": { + "summary": "CSSProperties" + }, + "defaultValue": { + "summary": "{}" + } + }, + "control": { + "type": "object", + "disable": false + } + } + }, + "Link": { + "variant": { + "control": { + "type": "select", + "disable": false + }, + "name": "variant", + "type": { + "name": "other", + "required": false, + "raw": "EnumOrPrimitive", + "value": "EnumOrPrimitive" + }, + "description": "Visual style variant of the link", + "table": { + "type": { + "summary": "LinkVariant" + }, + "defaultValue": { + "summary": "LinkVariant.Primary" + }, + "category": "Core Properties" + }, + "options": ["primary", "secondary", "inverted", "inherit"] + }, + "rel": { + "control": { + "type": "text", + "disable": false + }, + "name": "rel", + "type": { + "name": "union", + "required": false, + "raw": "EnumOrPrimitive | string", + "value": [ + { + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + { + "name": "string" + } + ] + }, + "description": "Relationship attribute for the link", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "" + }, + "category": "Navigation & Behavior" + } + }, + "children": { + "control": { + "type": "text", + "disable": false + }, + "name": "children", + "type": { + "name": "string" + }, + "description": "Content to be rendered inside the link", + "table": { + "category": "Content", + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "onClick": { + "name": "onClick", + "type": { + "name": "function", + "required": false, + "raw": "(event: MouseEvent) => void" + }, + "description": "Click handler for the anchor element.", + "table": { + "type": { + "summary": "(event: MouseEvent) => void" + }, + "category": "Events" + }, + "action": "clicked" + }, + "size": { + "control": { + "type": "select", + "disable": false + }, + "options": ["sm", "md", "lg"], + "name": "size", + "description": "Size variant of the link text.", + "type": { + "required": false, + "raw": "'sm' | 'md' | 'lg'", + "name": "enum", + "value": ["sm", "md", "lg"] + }, + "table": { + "type": { + "summary": "\"sm\" | \"md\" | \"lg\"" + }, + "category": "Appearance" + } + }, + "underline": { + "control": { + "type": "select", + "disable": false + }, + "options": ["default", "highlight", "none"], + "name": "underline", + "description": "Underline behavior for the link.", + "type": { + "required": false, + "raw": "'default' | 'highlight' | 'none'", + "name": "enum", + "value": ["default", "highlight", "none"] + }, + "table": { + "type": { + "summary": "\"default\" | \"highlight\" | \"none\"" + }, + "category": "Appearance" + } + }, + "color": { + "control": { + "type": "color" + }, + "name": "color", + "description": "", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + } + } + }, + "cursor": { + "control": { + "type": "text", + "disable": false + }, + "name": "cursor", + "description": "Cursor style used when hovering the link.", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "Cursors" + }, + "category": "Appearance" + } + }, + "disabled": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "disabled", + "description": "Disables the link interaction", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "State & Behavior" + } + }, + "target": { + "control": { + "type": "select", + "disable": false + }, + "name": "target", + "description": "Target window behavior for the link", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "LinkTarget" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Navigation & Behavior" + }, + "options": ["_blank", "_self", "_parent", "_top", "framename"] + }, + "href": { + "control": { + "type": "text", + "disable": false + }, + "name": "href", + "description": "URL or destination for the link", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Core Properties" + } + }, + "ariaLabel": { + "control": { + "type": "text", + "disable": false + }, + "name": "ariaLabel", + "description": "Accessible label announced by assistive technologies.", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "category": "Accessibility" + } + }, + "role": { + "control": { + "type": "text", + "disable": false + }, + "name": "role", + "description": "Explicit ARIA role for the rendered anchor.", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "category": "Accessibility" + } + }, + "tabindex": { + "control": { + "type": "number", + "disable": false + }, + "name": "tabindex", + "description": "Tab order for keyboard navigation.", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "category": "Accessibility" + } + }, + "className": { + "name": "className", + "description": "Additional CSS classes to apply", + "table": { + "category": "Styling", + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "styles": { + "name": "styles", + "description": "Custom inline styles to apply to the link", + "table": { + "category": "Styling", + "type": { + "summary": "CSSProperties" + }, + "defaultValue": { + "summary": "{}" + } + }, + "control": { + "type": "object", + "disable": false + } + } + }, + "Loader": { + "withWrapper": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "withWrapper", + "type": { + "name": "boolean", + "required": false + }, + "description": "Whether to wrap the loader in a container element. When false, the loader renders directly without wrapper styling. Useful for embedding loaders in buttons or other custom containers", + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "true" + }, + "category": "Layout & Positioning" + } + }, + "name": { + "control": { + "type": "select", + "disable": false + }, + "options": ["circle", "dots"], + "name": "name", + "type": { + "name": "enum", + "required": false, + "raw": "'circle' | 'dots'", + "value": ["circle", "dots"] + }, + "description": "Type of loader animation to display. \"circle\" provides a smooth rotating animation, while \"dots\" offers a rhythmic pulsing sequence", + "table": { + "type": { + "summary": "\"circle\" | \"dots\"" + }, + "defaultValue": { + "summary": "circle" + }, + "category": "Appearance" + } + }, + "variant": { + "control": { + "type": "select", + "disable": false + }, + "name": "variant", + "type": { + "name": "other", + "required": false, + "raw": "EnumOrPrimitive", + "value": "EnumOrPrimitive" + }, + "description": "Layout wrapper variant that determines positioning and container behavior. Inline: flows naturally in content, Section: absolute positioned overlay within container, FullPage: fixed position full-screen overlay with portal", + "table": { + "type": { + "summary": "inline | section | fullPage" + }, + "defaultValue": { + "summary": "inline" + }, + "category": "Layout & Positioning" + }, + "options": ["inline", "section", "fullPage"] + }, + "size": { + "control": { + "type": "select", + "disable": false + }, + "name": "size", + "type": { + "name": "other", + "required": false, + "raw": "EnumOrPrimitive", + "value": "EnumOrPrimitive" + }, + "description": "Size variant of the loader. Controls both the loader animation size and wrapper dimensions. Available sizes: xs, sm, md (default), lg, xl", + "table": { + "type": { + "summary": "SizeVariant" + }, + "defaultValue": { + "summary": "SizeVariant.Md" + }, + "category": "Appearance" + }, + "options": ["xs", "sm", "md", "lg", "xl"] + }, + "rounded": { + "control": { + "type": "select", + "disable": false + }, + "name": "rounded", + "description": "Border radius style for the loader. Available options: none, default, round, xs, sm, md, lg, xl. Note: Only applies to \"dots\" animation type, not \"circle\"", + "type": { + "required": false, + "name": "other", + "value": "Rounded" + }, + "table": { + "type": { + "summary": "'none' | 'default' | 'round' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'" + }, + "defaultValue": { + "summary": "none" + }, + "category": "Appearance" + }, + "options": ["none", "default", "round", "xs", "sm", "md", "lg", "xl"] + }, + "animationProps": { + "control": { + "type": "text" + }, + "name": "animationProps", + "description": "Custom CSS animation properties string. Overrides default animation timing and easing for advanced customization. Use sparingly—prefer size and variant props for standard use cases", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "BASE_ANIMATION" + }, + "category": "Content & Customization" + } + }, + "WrapperView": { + "control": { + "type": "object" + }, + "name": "WrapperView", + "description": "HTML element type or React component to use for the wrapper container. Defaults to \"span\" for inline usage. Use semantic elements like \"div\", \"section\", or custom components for better accessibility", + "type": { + "required": false, + "raw": "keyof HTMLElementTagNameMap | ElementType", + "name": "union", + "value": [ + { + "name": "other", + "value": "HTMLElementTagNameMap" + }, + { + "name": "other", + "value": "ElementType" + } + ] + }, + "table": { + "type": { + "summary": "keyof HTMLElementTagNameMap | ElementType" + }, + "defaultValue": { + "summary": "span" + }, + "category": "Layout & Positioning" + } + }, + "children": { + "name": "children", + "description": "Custom React content to replace the default loader animation. Use this to create custom loading indicators while maintaining Loader wrapper and positioning functionality", + "table": { + "category": "Content & Customization", + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "styles": { + "name": "styles", + "description": "Custom CSS styles object to override default theme tokens. Use for one-off styling needs. Prefer theme tokens and component props for consistent design system styling", + "table": { + "category": "Custom Styling", + "type": { + "summary": "CSSProperties" + }, + "defaultValue": { + "summary": "{}" + } + } + }, + "className": { + "name": "className", + "description": "Additional CSS class names to apply to the loader element. Use for custom styling, state management, or integration with CSS frameworks", + "table": { + "category": "Custom Styling", + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "\"\"" + } + } + } + }, + "Badge": { + "children": { + "control": { + "type": "text", + "disable": false + }, + "name": "children", + "type": { + "name": "string" + }, + "description": "Badge content (text or elements)", + "table": { + "category": "Content", + "type": { + "summary": "ReactNode" + } + } + }, + "variant": { + "control": { + "type": "select", + "disable": false + }, + "name": "variant", + "description": "Visual style variant of the badge", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "BadgeVariant" + }, + "defaultValue": { + "summary": "primary" + }, + "category": "Visual Style" + }, + "options": ["primary", "secondary", "tertiary", "quaternary", "quinary"] + }, + "disabled": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "disabled", + "description": "Whether the badge is disabled", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Visual Style" + } + }, + "appearance": { + "control": { + "type": "select", + "disable": false + }, + "name": "appearance", + "description": "Visual appearance/style of the badge (filled, filledLight, outline, outlineFilledLight)", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "BadgeAppearance" + }, + "defaultValue": { + "summary": "filled" + }, + "category": "Visual Style" + }, + "options": ["outline", "outlineFilledLight", "filled", "filledLight"] + }, + "size": { + "control": { + "type": "select", + "disable": false + }, + "name": "size", + "description": "Size of the badge (xs, sm, md, lg)", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "BadgeSize" + }, + "defaultValue": { + "summary": "md" + }, + "category": "Visual Style" + }, + "options": ["xs", "sm", "md", "lg"] + }, + "iconStart": { + "control": { + "type": "object", + "disable": true + }, + "name": "iconStart", + "description": "Icon element to display at the start of the badge", + "type": { + "required": false, + "name": "other", + "value": "ReactNode" + }, + "table": { + "type": { + "summary": "ReactNode" + }, + "category": "Icons" + } + }, + "iconEnd": { + "control": { + "type": "object", + "disable": true + }, + "name": "iconEnd", + "description": "Icon element to display at the end of the badge", + "type": { + "required": false, + "name": "other", + "value": "ReactNode" + }, + "table": { + "type": { + "summary": "ReactNode" + }, + "category": "Icons" + } + }, + "styles": { + "name": "styles", + "description": "CSSObject for custom inline styles", + "table": { + "category": "Box Styles", + "type": { + "summary": "CSSObject" + } + }, + "control": { + "disable": true + } + }, + "display": { + "name": "display", + "description": "CSS display property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "overflow": { + "name": "overflow", + "description": "CSS overflow property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "minWidth": { + "name": "minWidth", + "description": "CSS min-width property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "width": { + "name": "width", + "description": "CSS width property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "maxWidth": { + "name": "maxWidth", + "description": "CSS max-width property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "minHeight": { + "name": "minHeight", + "description": "CSS min-height property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "height": { + "name": "height", + "description": "CSS height property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "maxHeight": { + "name": "maxHeight", + "description": "CSS max-height property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "margin": { + "name": "margin", + "description": "CSS margin property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginTop": { + "name": "marginTop", + "description": "CSS margin-top property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginRight": { + "name": "marginRight", + "description": "CSS margin-right property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginBottom": { + "name": "marginBottom", + "description": "CSS margin-bottom property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginLeft": { + "name": "marginLeft", + "description": "CSS margin-left property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "padding": { + "name": "padding", + "description": "CSS padding property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingTop": { + "name": "paddingTop", + "description": "CSS padding-top property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingRight": { + "name": "paddingRight", + "description": "CSS padding-right property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingBottom": { + "name": "paddingBottom", + "description": "CSS padding-bottom property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingLeft": { + "name": "paddingLeft", + "description": "CSS padding-left property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "zIndex": { + "name": "zIndex", + "description": "CSS z-index property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "position": { + "name": "position", + "description": "CSS position property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "top": { + "name": "top", + "description": "CSS top property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "right": { + "name": "right", + "description": "CSS right property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "bottom": { + "name": "bottom", + "description": "CSS bottom property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "left": { + "name": "left", + "description": "CSS left property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexDirection": { + "name": "flexDirection", + "description": "CSS flex-direction property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "justifyContent": { + "name": "justifyContent", + "description": "CSS justify-content property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "justifySelf": { + "name": "justifySelf", + "description": "CSS justify-self property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignItems": { + "name": "alignItems", + "description": "CSS align-items property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignSelf": { + "name": "alignSelf", + "description": "CSS align-self property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignContent": { + "name": "alignContent", + "description": "CSS align-content property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexWrap": { + "name": "flexWrap", + "description": "CSS flex-wrap property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flex": { + "name": "flex", + "description": "CSS flex property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexGrow": { + "name": "flexGrow", + "description": "CSS flex-grow property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexShrink": { + "name": "flexShrink", + "description": "CSS flex-shrink property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexBasis": { + "name": "flexBasis", + "description": "CSS flex-basis property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "order": { + "name": "order", + "description": "CSS order property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "gap": { + "name": "gap", + "description": "CSS gap property", + "table": { + "category": "Box Styles", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + } + }, + "Box": { + "variant": { + "control": { + "type": "select", + "disable": false + }, + "options": ["horizontal", "vertical"], + "name": "variant", + "type": { + "name": "enum", + "required": false, + "raw": "'horizontal' | 'vertical'", + "value": ["horizontal", "vertical"] + }, + "description": "Box orientation variant", + "table": { + "type": { + "summary": "'horizontal' | 'vertical'" + }, + "defaultValue": { + "summary": "vertical" + }, + "category": "Core Props" + } + }, + "children": { + "control": { + "type": "text", + "disable": false + }, + "name": "children", + "type": { + "name": "string" + }, + "description": "Box content (any React node)", + "table": { + "category": "Core Props", + "type": { + "summary": "ReactNode" + } + } + }, + "padding": { + "control": { + "type": "text", + "disable": false + }, + "name": "padding", + "type": { + "name": "string" + }, + "description": "CSS padding property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + } + }, + "isBordered": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "isBordered", + "description": "Adds border to the box", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Visual Style" + } + }, + "isHighlighted": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "isHighlighted", + "description": "Enables highlight effect on hover (adds outline on hover)", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Visual Style" + } + }, + "withShadowHover": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "withShadowHover", + "description": "Adds box shadow on hover for elevation effect", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Visual Style" + } + }, + "tabIndex": { + "control": { + "type": "number", + "disable": false + }, + "name": "tabIndex", + "description": "Tab index for keyboard navigation", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "0" + }, + "category": "Accessibility" + } + }, + "width": { + "name": "width", + "description": "CSS width property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "height": { + "name": "height", + "description": "CSS height property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "minWidth": { + "name": "minWidth", + "description": "CSS min-width property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "minHeight": { + "name": "minHeight", + "description": "CSS min-height property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "maxWidth": { + "name": "maxWidth", + "description": "CSS max-width property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "maxHeight": { + "name": "maxHeight", + "description": "CSS max-height property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingTop": { + "name": "paddingTop", + "description": "CSS padding-top property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingRight": { + "name": "paddingRight", + "description": "CSS padding-right property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingBottom": { + "name": "paddingBottom", + "description": "CSS padding-bottom property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingLeft": { + "name": "paddingLeft", + "description": "CSS padding-left property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "margin": { + "name": "margin", + "description": "CSS margin property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginTop": { + "name": "marginTop", + "description": "CSS margin-top property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginRight": { + "name": "marginRight", + "description": "CSS margin-right property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginBottom": { + "name": "marginBottom", + "description": "CSS margin-bottom property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginLeft": { + "name": "marginLeft", + "description": "CSS margin-left property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexDirection": { + "name": "flexDirection", + "description": "CSS flex-direction property", + "options": ["row", "row-reverse", "column", "column-reverse"], + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "select", + "disable": false + } + }, + "justifyContent": { + "name": "justifyContent", + "description": "CSS justify-content property", + "options": ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"], + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "select", + "disable": false + } + }, + "alignItems": { + "name": "alignItems", + "description": "CSS align-items property", + "options": ["flex-start", "flex-end", "center", "baseline", "stretch"], + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "select", + "disable": false + } + }, + "alignContent": { + "name": "alignContent", + "description": "CSS align-content property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexWrap": { + "name": "flexWrap", + "description": "CSS flex-wrap property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "gap": { + "name": "gap", + "description": "CSS gap property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flex": { + "name": "flex", + "description": "CSS flex property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexGrow": { + "name": "flexGrow", + "description": "CSS flex-grow property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexShrink": { + "name": "flexShrink", + "description": "CSS flex-shrink property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexBasis": { + "name": "flexBasis", + "description": "CSS flex-basis property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "position": { + "name": "position", + "description": "CSS position property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "overflow": { + "name": "overflow", + "description": "CSS overflow property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "styles": { + "name": "styles", + "description": "Custom styles object", + "table": { + "category": "Custom Styling", + "type": { + "summary": "CSSProperties" + } + }, + "control": { + "type": "object", + "disable": false + } + } + }, + "Image": { + "width": { + "control": { + "type": "number" + }, + "name": "width", + "type": { + "name": "number", + "required": false + }, + "description": "Width of the image in pixels", + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Dimensions" + } + }, + "height": { + "control": { + "type": "number" + }, + "name": "height", + "type": { + "name": "number", + "required": false + }, + "description": "Height of the image in pixels", + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Dimensions" + } + }, + "alt": { + "control": { + "type": "text" + }, + "name": "alt", + "type": { + "name": "string", + "required": false + }, + "description": "Alternative text for the image", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "required" + }, + "category": "Core Properties" + } + }, + "src": { + "control": { + "type": "text" + }, + "name": "src", + "type": { + "name": "string", + "required": false + }, + "description": "Source URL of the image", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "required" + }, + "category": "Core Properties" + } + }, + "id": { + "control": { + "type": "text" + }, + "name": "id", + "type": { + "name": "string", + "required": false + }, + "description": "Unique identifier for the image", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Core Properties" + } + }, + "className": { + "control": { + "type": "text" + }, + "name": "className", + "type": { + "name": "string" + }, + "description": "Additional CSS classes to apply", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Styling" + } + }, + "placeholder": { + "control": { + "type": "object" + }, + "name": "placeholder", + "type": { + "name": "other", + "required": false, + "value": "ReactNode" + }, + "description": "Text to display while image is loading", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Content & Display" + } + }, + "caption": { + "control": { + "type": "text" + }, + "name": "caption", + "type": { + "name": "string", + "required": false + }, + "description": "Caption text to display below the image", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Content & Display" + } + }, + "onClick": { + "name": "onClick", + "description": "Click event handler for the image", + "type": { + "required": false, + "raw": "() => void", + "name": "function" + }, + "table": { + "type": { + "summary": "(event: React.MouseEvent) => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + } + }, + "onError": { + "name": "onError", + "description": "Error event handler when image fails to load", + "type": { + "required": false, + "raw": "() => void", + "name": "function" + }, + "table": { + "type": { + "summary": "() => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + } + }, + "onLoad": { + "name": "onLoad", + "description": "Load event handler when image successfully loads", + "type": { + "required": false, + "raw": "() => void", + "name": "function" + }, + "table": { + "type": { + "summary": "() => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + } + }, + "fallbackComponent": { + "control": { + "type": "object" + }, + "name": "fallbackComponent", + "description": "Component to display when image fails to load", + "type": { + "required": false, + "name": "other", + "value": "ReactNode" + }, + "table": { + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Content & Display" + } + }, + "objectFit": { + "control": { + "type": "radio" + }, + "options": ["cover", "contain", "fill", "none", "scale-down"], + "name": "objectFit", + "description": "CSS object-fit property for image scaling", + "type": { + "required": false, + "raw": "'cover' | 'contain' | 'fill' | 'none' | 'scale-down'", + "name": "enum", + "value": ["cover", "contain", "fill", "none", "scale-down"] + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Styling" + } + }, + "as": { + "control": { + "type": "text", + "disable": false + }, + "name": "as", + "description": "Polymorphic prop that changes the rendered wrapper element while preserving all Image styles. Accepts HTML tag names (e.g., \"div\", \"span\", \"figure\") or React components. Useful for semantic HTML and accessibility.", + "type": { + "required": false, + "raw": "keyof HTMLElementTagNameMap | ElementType", + "name": "union", + "value": [ + { + "name": "other", + "value": "HTMLElementTagNameMap" + }, + { + "name": "other", + "value": "ElementType" + } + ] + }, + "table": { + "type": { + "summary": "keyof HTMLElementTagNameMap | ElementType" + }, + "defaultValue": { + "summary": "div" + }, + "category": "Core Properties" + } + }, + "captionAs": { + "control": { + "type": "text", + "disable": false + }, + "name": "captionAs", + "description": "Polymorphic prop that changes the rendered caption element while preserving all Image caption styles. Accepts HTML tag names (e.g., \"figcaption\", \"p\", \"span\") or React components. Defaults to \"figcaption\" for semantic HTML. Useful for custom caption styling or when not using semantic figure/caption structure.", + "type": { + "required": false, + "raw": "keyof HTMLElementTagNameMap | ElementType", + "name": "union", + "value": [ + { + "name": "other", + "value": "HTMLElementTagNameMap" + }, + { + "name": "other", + "value": "ElementType" + } + ] + }, + "table": { + "type": { + "summary": "keyof HTMLElementTagNameMap | ElementType" + }, + "defaultValue": { + "summary": "figcaption" + }, + "category": "Core Properties" + } + }, + "styles": { + "name": "styles", + "description": "Custom CSS styles object", + "table": { + "type": { + "summary": "CSSObject" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Styling" + } + } + }, + "Avatar": { + "backgroundColor": { + "control": { + "type": "color", + "disable": false + }, + "name": "backgroundColor", + "type": { + "name": "string", + "required": false + }, + "description": "The background color of the avatar when no image is loaded, or when the image fails. Also used for the fallback content container. Accepts CSS color values (e.g., hex, named colors) and supports theme colors (e.g. `primary.default`).", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Appearance" + } + }, + "src": { + "control": { + "type": "text", + "disable": false + }, + "name": "src", + "type": { + "name": "string" + }, + "description": "The URL of the image to display in the avatar. If no `src` is provided or the image fails to load, the `fallback` content will be rendered.", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Image" + } + }, + "sizeVariant": { + "control": { + "type": "select", + "disable": false + }, + "name": "sizeVariant", + "type": { + "name": "other", + "required": false, + "raw": "EnumOrPrimitive", + "value": "EnumOrPrimitive" + }, + "description": "Defines the predefined size of the avatar.", + "table": { + "type": { + "summary": "SizeVariant" + }, + "defaultValue": { + "summary": "md" + }, + "category": "Appearance" + }, + "options": ["xs", "sm", "md", "lg", "xl", "xxl"] + }, + "withBadge": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "withBadge", + "description": "If `true`, a small badge will be displayed at the top-right corner of the avatar, often indicating online status or notifications.", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Badge" + } + }, + "badgeColor": { + "control": { + "type": "color", + "disable": false + }, + "name": "badgeColor", + "description": "The color of the badge when `withBadge` is `true`. Accepts CSS color values (e.g., hex, named colors). Also supports theme colors (e.g. `primary.default`)", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "#34A853" + }, + "category": "Badge" + } + }, + "fallbackComponent": { + "control": { + "type": "text", + "disable": false + }, + "name": "fallbackComponent", + "description": "Content to display when `src` is not provided or the image fails to load. Can be a string, number, or any ReactNode (e.g., an icon, initials, or a custom component).", + "type": { + "required": false, + "name": "other", + "value": "ReactNode" + }, + "table": { + "type": { + "summary": "ReactNode" + }, + "category": "Fallback" + } + }, + "onClick": { + "name": "onClick", + "description": "Callback function triggered when the avatar is clicked.", + "type": { + "required": false, + "raw": "() => void", + "name": "function" + }, + "table": { + "type": { + "summary": "() => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + }, + "action": "clicked" + }, + "alt": { + "name": "alt", + "description": "Alternative text for the avatar image, important for accessibility. This will be used as the `aria-label` for the avatar.", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "\"\"" + }, + "category": "Image" + }, + "control": { + "type": "text", + "disable": false + } + }, + "id": { + "name": "id", + "description": "A unique identifier for the avatar element.", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Core" + }, + "control": { + "type": "text", + "disable": false + } + }, + "className": { + "name": "className", + "description": "Additional CSS class names to apply to the avatar component.", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Styling" + }, + "control": { + "type": "text", + "disable": false + } + }, + "placeholder": { + "name": "placeholder", + "description": "Passed to the internal `Image` component.", + "table": { + "type": { + "summary": "string" + }, + "category": "Image" + }, + "control": { + "type": "text", + "disable": false + } + }, + "styles": { + "name": "styles", + "description": "Custom CSS-in-JS styles to apply to the top-level container of the Avatar.", + "table": { + "type": { + "summary": "CSSObject" + }, + "defaultValue": { + "summary": "{}" + }, + "category": "Styling" + }, + "control": { + "type": "object", + "disable": false + } + } + }, + "Button": { + "variant": { + "name": "variant", + "description": "Visual style variant of the button", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "ButtonVariant" + }, + "defaultValue": { + "summary": "primary" + }, + "category": "Visual Style" + }, + "options": ["primary", "secondary", "tertiary", "outlined", "text", "inherit"], + "control": { + "type": "select", + "disable": false + } + }, + "rounded": { + "name": "rounded", + "description": "Border radius style for the button", + "type": { + "required": false, + "name": "other", + "value": "Rounded" + }, + "table": { + "type": { + "summary": "'none' | 'default' | 'round' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'" + }, + "defaultValue": { + "summary": "none" + }, + "category": "Visual Style" + }, + "options": ["none", "default", "round", "xs", "sm", "md", "lg", "xl"], + "control": { + "type": "select", + "disable": false + } + }, + "iconStart": { + "name": "iconStart", + "description": "Icon element to display at the start of the button", + "type": { + "required": false, + "name": "other", + "value": "ReactNode" + }, + "table": { + "type": { + "summary": "ReactNode" + }, + "category": "Icons" + } + }, + "iconEnd": { + "name": "iconEnd", + "description": "Icon element to display at the end of the button", + "type": { + "required": false, + "name": "other", + "value": "ReactNode" + }, + "table": { + "type": { + "summary": "ReactNode" + }, + "category": "Icons" + } + }, + "onClick": { + "name": "onClick", + "description": "Click event handler", + "type": { + "required": false, + "raw": "(event: MouseEvent) => void", + "name": "function" + }, + "table": { + "type": { + "summary": "(event: MouseEvent) => void" + }, + "category": "Content & Behavior" + }, + "action": "clicked" + }, + "type": { + "name": "type", + "description": "HTML button type attribute", + "type": { + "required": false, + "raw": "ButtonTypes | `${ButtonTypes}`", + "name": "union", + "value": [ + { + "name": "other", + "value": "ButtonTypes" + }, + { + "name": "other", + "value": "literal" + } + ] + }, + "table": { + "type": { + "summary": "ButtonTypes" + }, + "defaultValue": { + "summary": "button" + }, + "category": "Content & Behavior" + }, + "options": ["button", "submit", "reset"], + "control": { + "type": "select", + "disable": false + } + }, + "disabled": { + "name": "disabled", + "description": "Disables the button and prevents interaction", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Content & Behavior" + }, + "control": { + "type": "boolean", + "disable": false + } + }, + "fullWidth": { + "name": "fullWidth", + "description": "Makes button take full width of its container", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Visual Style" + }, + "control": { + "type": "boolean", + "disable": false + } + }, + "isIcon": { + "name": "isIcon", + "description": "Renders button as icon-only with equal width and height", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Visual Style" + }, + "control": { + "type": "boolean", + "disable": false + } + }, + "isLoading": { + "name": "isLoading", + "description": "", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + } + } + }, + "ariaLabel": { + "name": "ariaLabel", + "description": "Accessibility label for screen readers", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "category": "Accessibility" + }, + "control": { + "type": "text", + "disable": false + } + }, + "ariaPressed": { + "name": "ariaPressed", + "description": "Indicates the pressed state for toggle buttons", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "category": "Accessibility" + }, + "control": { + "type": "boolean", + "disable": false + } + }, + "role": { + "name": "role", + "description": "ARIA role for the button", + "type": { + "required": false, + "raw": "ButtonRole | `${ButtonRole}`", + "name": "union", + "value": [ + { + "name": "other", + "value": "ButtonRole" + }, + { + "name": "other", + "value": "literal" + } + ] + }, + "table": { + "type": { + "summary": "ButtonRole" + }, + "defaultValue": { + "summary": "button" + }, + "category": "Accessibility" + }, + "options": ["button", "link", "checkbox", "switch", "tab"], + "control": { + "type": "select", + "disable": false + } + }, + "tabIndex": { + "name": "tabIndex", + "description": "Tab index for keyboard navigation", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "0" + }, + "category": "Accessibility" + }, + "control": { + "type": "number", + "disable": false + } + }, + "children": { + "name": "children", + "description": "Button content (text or elements)", + "table": { + "category": "Content & Behavior", + "type": { + "summary": "ReactNode" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "width": { + "name": "width", + "description": "CSS width property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "height": { + "name": "height", + "description": "CSS height property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "minWidth": { + "name": "minWidth", + "description": "CSS min-width property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "minHeight": { + "name": "minHeight", + "description": "CSS min-height property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "maxWidth": { + "name": "maxWidth", + "description": "CSS max-width property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "maxHeight": { + "name": "maxHeight", + "description": "CSS max-height property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "margin": { + "name": "margin", + "description": "CSS margin property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginTop": { + "name": "marginTop", + "description": "CSS margin-top property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginRight": { + "name": "marginRight", + "description": "CSS margin-right property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginBottom": { + "name": "marginBottom", + "description": "CSS margin-bottom property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginLeft": { + "name": "marginLeft", + "description": "CSS margin-left property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "padding": { + "name": "padding", + "description": "CSS padding property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingTop": { + "name": "paddingTop", + "description": "CSS padding-top property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingRight": { + "name": "paddingRight", + "description": "CSS padding-right property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingBottom": { + "name": "paddingBottom", + "description": "CSS padding-bottom property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingLeft": { + "name": "paddingLeft", + "description": "CSS padding-left property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexDirection": { + "name": "flexDirection", + "description": "CSS flex-direction property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "justifyContent": { + "name": "justifyContent", + "description": "CSS justify-content property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignItems": { + "name": "alignItems", + "description": "CSS align-items property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignContent": { + "name": "alignContent", + "description": "CSS align-content property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexWrap": { + "name": "flexWrap", + "description": "CSS flex-wrap property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "gap": { + "name": "gap", + "description": "CSS gap property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flex": { + "name": "flex", + "description": "CSS flex property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexGrow": { + "name": "flexGrow", + "description": "CSS flex-grow property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexShrink": { + "name": "flexShrink", + "description": "CSS flex-shrink property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexBasis": { + "name": "flexBasis", + "description": "CSS flex-basis property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignSelf": { + "name": "alignSelf", + "description": "CSS align-self property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "justifySelf": { + "name": "justifySelf", + "description": "CSS justify-self property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "order": { + "name": "order", + "description": "CSS order property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "display": { + "name": "display", + "options": ["block", "flex", "inline", "inline-flex", "grid", "none"], + "description": "CSS display property to control the layout behavior", + "table": { + "type": { + "summary": "string" + }, + "category": "Box Styles - Position & Display" + }, + "control": { + "type": "select", + "disable": false + } + }, + "position": { + "name": "position", + "description": "CSS position property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "top": { + "name": "top", + "description": "CSS top property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "right": { + "name": "right", + "description": "CSS right property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "bottom": { + "name": "bottom", + "description": "CSS bottom property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "left": { + "name": "left", + "description": "CSS left property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "overflow": { + "name": "overflow", + "description": "CSS overflow property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "zIndex": { + "name": "zIndex", + "description": "CSS z-index property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "styles": { + "name": "styles", + "description": "Custom styles object for the button", + "table": { + "category": "Custom Styling", + "type": { + "summary": "BoxCssComponentProps" + } + }, + "control": { + "type": "object", + "disable": false + } + }, + "className": { + "name": "className", + "description": "Custom CSS class names", + "table": { + "category": "Custom Styling, predefined classes hover, active, and disabled", + "type": { + "summary": "(\"hover\" | \"active\" | \"disabled\" | string)[]" + } + }, + "control": { + "type": "text", + "disable": false + } + } + }, + "Checkbox": { + "children": { + "control": { + "type": "text" + }, + "name": "children", + "type": { + "name": "string" + } + }, + "onValueChange": { + "name": "onValueChange", + "type": { + "name": "function", + "required": false, + "raw": "(checked: boolean) => void" + }, + "description": "Callback fired when the checkbox value changes", + "table": { + "type": { + "summary": "(checked: boolean) => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + }, + "action": "Checkbox value changed" + }, + "checked": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "checked", + "description": "Controls the checked state of the checkbox", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "State" + } + }, + "indeterminate": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "indeterminate", + "description": "Displays the checkbox in an indeterminate (mixed) state", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "State" + } + }, + "disabled": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "disabled", + "description": "Disables the checkbox interaction", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "State" + } + }, + "name": { + "control": { + "type": "text", + "disable": false + }, + "name": "name", + "description": "Name attribute of the checkbox input", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Identification" + } + }, + "value": { + "control": { + "type": "text", + "disable": false + }, + "name": "value", + "description": "Value attribute of the checkbox input", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Identification" + } + }, + "size": { + "control": { + "type": "select", + "disable": false + }, + "options": ["sm", "md"], + "name": "size", + "description": "Size of the checkbox", + "type": { + "required": false, + "raw": "'sm' | 'md'", + "name": "enum", + "value": ["sm", "md"] + }, + "table": { + "type": { + "summary": "CheckboxSize", + "detail": "sm | md" + }, + "defaultValue": { + "summary": "md" + }, + "category": "Layout" + } + } + }, + "Input": { + "variant": { + "control": { + "type": "select", + "disable": false + }, + "name": "variant", + "type": { + "name": "string", + "required": false + }, + "description": "Input field type variant", + "table": { + "type": { + "summary": "InputVariantType" + }, + "defaultValue": { + "summary": "text" + }, + "category": "Core Properties" + }, + "options": [ + "text", + "password", + "email", + "search", + "url", + "tel", + "date", + "time", + "month", + "week", + "color", + "range", + "number", + "radio", + "checkbox" + ] + }, + "color": { + "control": { + "type": "select", + "disable": false + }, + "name": "color", + "type": { + "name": "string", + "required": false + }, + "description": "Color variant of the input", + "table": { + "type": { + "summary": "InputColorVariant" + }, + "defaultValue": { + "summary": "'primary'" + }, + "category": "Appearance & Styling" + }, + "options": ["primary", "success", "warning", "error"] + }, + "role": { + "control": { + "type": "select", + "disable": false + }, + "name": "role", + "type": { + "name": "string" + }, + "description": "ARIA role of the input element", + "options": ["textbox", "spinbutton", "checkbox", "radio", "radiogroup", "combobox"], + "table": { + "category": "Accessibility", + "defaultValue": { + "summary": "textbox" + }, + "type": { + "summary": "InputRole" + } + } + }, + "name": { + "control": { + "type": "text" + }, + "name": "name", + "type": { + "name": "string" + }, + "description": "Input field name attribute", + "table": { + "category": "Core Properties", + "defaultValue": { + "summary": "input" + }, + "type": { + "summary": "string" + } + } + }, + "width": { + "control": { + "type": "text" + }, + "name": "width", + "type": { + "name": "string", + "required": false + }, + "description": "Width of the input field", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "100%" + }, + "category": "Appearance & Styling" + } + }, + "disabled": { + "control": { + "type": "boolean" + }, + "name": "disabled", + "type": { + "name": "boolean", + "required": false + }, + "description": "Disabled state of the input", + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "State & Behavior" + } + }, + "required": { + "control": { + "type": "boolean" + }, + "name": "required", + "type": { + "name": "boolean" + }, + "description": "Required state of the input", + "table": { + "category": "State & Behavior", + "defaultValue": { + "summary": "false" + }, + "type": { + "summary": "boolean" + } + } + }, + "readOnly": { + "control": { + "type": "boolean" + }, + "name": "readOnly", + "type": { + "name": "boolean", + "required": false + }, + "description": "Read-only state of the input", + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "State & Behavior" + } + }, + "checked": { + "control": { + "type": "boolean" + }, + "name": "checked", + "type": { + "name": "boolean" + }, + "description": "Checked state for checkbox/radio inputs", + "table": { + "category": "State & Behavior", + "defaultValue": { + "summary": "false" + }, + "type": { + "summary": "boolean" + } + } + }, + "placeholder": { + "control": { + "type": "text" + }, + "name": "placeholder", + "type": { + "name": "string" + }, + "description": "Placeholder text", + "table": { + "category": "Content & Labels", + "defaultValue": { + "summary": "placeholder" + }, + "type": { + "summary": "string" + } + } + }, + "defaultValue": { + "control": { + "type": "text" + }, + "name": "defaultValue", + "type": { + "name": "string" + }, + "description": "Default value of the input", + "table": { + "category": "Content & Labels", + "defaultValue": { + "summary": "defaultValue" + }, + "type": { + "summary": "string" + } + } + }, + "tabIndex": { + "control": { + "type": "number" + }, + "name": "tabIndex", + "type": { + "name": "number", + "required": false + }, + "description": "Tab index for keyboard navigation", + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "0" + }, + "category": "Accessibility" + } + }, + "className": { + "control": { + "type": "text" + }, + "name": "className", + "type": { + "name": "string", + "required": false + }, + "description": "Additional CSS classes", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "" + }, + "category": "Appearance & Styling" + } + }, + "ariaRequired": { + "control": { + "type": "boolean" + }, + "name": "ariaRequired", + "type": { + "name": "boolean", + "required": false + }, + "description": "ARIA required attribute", + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Accessibility" + } + }, + "debounceCallbackTime": { + "control": { + "type": "number" + }, + "name": "debounceCallbackTime", + "type": { + "name": "number" + }, + "description": "Debounce timeout for onChange callback", + "table": { + "category": "State & Behavior", + "defaultValue": { + "summary": "300" + }, + "type": { + "summary": "number" + } + } + }, + "styles": { + "control": { + "type": "object" + }, + "name": "styles", + "type": { + "name": "object", + "value": {}, + "required": false + }, + "description": "Custom styles object", + "table": { + "type": { + "summary": "React.CSSProperties" + }, + "defaultValue": { + "summary": "{}" + }, + "category": "Appearance & Styling" + } + }, + "adornmentStart": { + "control": { + "type": "object" + }, + "name": "adornmentStart", + "type": { + "name": "other", + "required": false, + "value": "ReactNode" + }, + "description": "Start adornment element", + "table": { + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "" + }, + "category": "Content & Labels" + } + }, + "adornmentEnd": { + "control": { + "type": "object" + }, + "name": "adornmentEnd", + "type": { + "name": "other", + "required": false, + "value": "ReactNode" + }, + "description": "End adornment element", + "table": { + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "" + }, + "category": "Content & Labels" + } + }, + "label": { + "control": { + "type": "object" + }, + "name": "label", + "type": { + "name": "other", + "required": false, + "value": "ReactNode" + }, + "description": "Input label text", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "" + }, + "category": "Content & Labels" + } + }, + "helperText": { + "control": { + "type": "object" + }, + "name": "helperText", + "type": { + "name": "other", + "required": false, + "value": "ReactNode" + }, + "description": "Helper text below input", + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "" + }, + "category": "Content & Labels" + } + }, + "onChange": { + "name": "onChange", + "type": { + "name": "function" + }, + "description": "onChange event handler", + "table": { + "category": "Events", + "type": { + "summary": "(event: ChangeEvent) => void" + } + } + }, + "onFocus": { + "name": "onFocus", + "type": { + "name": "function" + }, + "description": "onFocus event handler", + "table": { + "category": "Events", + "type": { + "summary": "(event: FocusEvent) => void" + } + } + }, + "onBlur": { + "name": "onBlur", + "type": { + "name": "function" + }, + "description": "onBlur event handler", + "table": { + "category": "Events", + "type": { + "summary": "(event: FocusEvent) => void" + } + } + }, + "onClick": { + "name": "onClick", + "type": { + "name": "function" + }, + "description": "onClick event handler", + "table": { + "category": "Events", + "type": { + "summary": "(event: MouseEvent) => void" + } + } + }, + "wrapperAs": { + "control": { + "type": "object" + }, + "name": "wrapperAs", + "description": "HTML element or component to wrap the input", + "type": { + "required": false, + "raw": "keyof HTMLElementTagNameMap | ElementType", + "name": "union", + "value": [ + { + "name": "other", + "value": "HTMLElementTagNameMap" + }, + { + "name": "other", + "value": "ElementType" + } + ] + }, + "table": { + "type": { + "summary": "keyof HTMLElementTagNameMap | ElementType" + }, + "defaultValue": { + "summary": "label" + }, + "category": "Core Properties" + } + }, + "ariaDescribedBy": { + "name": "ariaDescribedBy", + "description": "ARIA describedby attribute", + "table": { + "category": "Accessibility", + "type": { + "summary": "string" + } + } + } + }, + "Select": { + "items": { + "name": "items", + "description": "Array of items to be displayed in the select dropdown", + "table": { + "category": "Core", + "type": { + "summary": "(never | Option)[]", + "detail": "Option { name: string; value: unknown; }" + }, + "defaultValue": { + "summary": "[]" + } + } + }, + "value": { + "name": "value", + "description": "Currently selected value (Option for single select, Option[] for multiple select)", + "table": { + "category": "Core", + "type": { + "summary": "Option | Option[] | null" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "multiple": { + "name": "multiple", + "description": "Enable multiple selection mode", + "table": { + "category": "Core", + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + } + }, + "control": { + "type": "boolean", + "disable": false + } + }, + "placeholder": { + "name": "placeholder", + "description": "Text to display when no value is selected", + "table": { + "category": "Core", + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "Select" + } + } + }, + "onSelect": { + "name": "onSelect", + "description": "Callback when item is selected", + "action": "selected", + "table": { + "category": "Events", + "type": { + "summary": "SelectOnSelect = (props: OnSelectProps) => void", + "detail": "OnSelectProps {\n event: MouseEvent | KeyboardEvent | ChangeEvent;\n data?: Option;\n index?: number;\n}" + } + } + }, + "onChange": { + "name": "onChange", + "description": "Callback fired when value changes (receives Option for single select, Option[] for multiple select)", + "table": { + "category": "Events", + "type": { + "summary": "(data: Option | Option[]) => void", + "detail": "Option { name: string; value: unknown; }" + } + } + }, + "onKeyDown": { + "name": "onKeyDown", + "description": "Callback fired on keyboard events", + "table": { + "category": "Events", + "type": { + "summary": "ReactEventHandler" + } + } + }, + "onInitiatorClick": { + "name": "onInitiatorClick", + "description": "Callback fired when initiator is clicked", + "table": { + "category": "Events", + "type": { + "summary": "(event: MouseEvent) => void" + } + } + }, + "renderOption": { + "name": "renderOption", + "description": "Custom renderer for option items", + "table": { + "category": "Customization", + "type": { + "summary": "(value: renderOptionType) => ReactNode", + "detail": "renderOptionType = {\n item: Option;\n index: number;\n isActiveItem: boolean;\n className: string;\n};" + } + } + }, + "itemStringifier": { + "name": "itemStringifier", + "description": "Function to convert item value to display string", + "table": { + "category": "Customization", + "type": { + "summary": "(data: Option) => string", + "detail": "Option { name: string; value: unknown; }" + } + } + }, + "itemIdentifier": { + "name": "itemIdentifier", + "description": "Function to compare items for equality", + "table": { + "category": "Customization", + "type": { + "summary": "ItemIdentifier", + "detail": "ItemIdentifier = (selected: Option, current: Option) => boolean" + } + } + }, + "width": { + "name": "width", + "description": "Width of the select component", + "table": { + "category": "Layout", + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "minWidth": { + "name": "minWidth", + "description": "Minimum width of the select component", + "table": { + "category": "Layout", + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "maxWidth": { + "name": "maxWidth", + "description": "Maximum width of the select component", + "table": { + "category": "Layout", + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "initial" + } + } + }, + "dropdownMaxHeight": { + "name": "dropdownMaxHeight", + "description": "Maximum height of the dropdown list", + "table": { + "category": "Layout", + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "240px" + } + } + }, + "adornmentStart": { + "name": "adornmentStart", + "description": "Element to be rendered at the start of the select", + "table": { + "category": "Layout", + "type": { + "summary": "ReactNode" + } + } + }, + "adornmentEnd": { + "name": "adornmentEnd", + "description": "Element to be rendered at the end of the select", + "table": { + "category": "Layout", + "type": { + "summary": "ReactNode" + } + } + }, + "disabled": { + "name": "disabled", + "description": "Whether the select is disabled", + "table": { + "category": "State", + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + } + } + }, + "autoOpen": { + "name": "autoOpen", + "description": "Whether to automatically open the dropdown", + "table": { + "category": "State", + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "activeIndex": { + "name": "activeIndex", + "description": "Currently active item index", + "table": { + "category": "State", + "type": { + "summary": "string | number" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "ref": { + "name": "ref", + "description": "Forwarded ref that provides access to container methods: `isOpen`, `open()`, `close()`, `toggle() and onSelect()`.", + "table": { + "category": "Advanced", + "type": { + "summary": "Ref", + "detail": "{ isOpen: boolean; open: () => void; close: () => void; toggle: () => void; onSelect: (event: MouseEvent | KeyboardEvent | ChangeEvent, data: Option) => void}" + } + } + }, + "initiator": { + "name": "initiator", + "description": "Custom initiator element to trigger the dropdown", + "table": { + "category": "Advanced", + "type": { + "summary": "ReactNode" + } + } + }, + "children": { + "name": "children", + "description": "Child elements of the select component", + "table": { + "category": "Advanced", + "type": { + "summary": "ReactNode" + } + } + }, + "emptyItemsResult": { + "name": "emptyItemsResult", + "description": "Text or Component to display when no items are available", + "table": { + "category": "Advanced", + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "itemsCount": { + "name": "itemsCount", + "description": "Number of items in the select", + "table": { + "category": "Advanced", + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "undefined" + } + } + }, + "styles": { + "name": "styles", + "description": "Custom styles object for the select", + "table": { + "category": "Appearance & Styling", + "type": { + "summary": "BoxCssComponentProps" + } + }, + "control": { + "type": "object", + "disable": false + } + }, + "color": { + "name": "color", + "description": "Color variant of the select", + "options": ["primary", "success", "warning", "error"], + "table": { + "category": "Appearance & Styling", + "type": { + "summary": "InputColorVariant" + }, + "defaultValue": { + "summary": "'primary'" + } + }, + "control": { + "type": "select", + "disable": false + } + } + }, + "Typography": { + "children": { + "control": { + "type": "text", + "disable": false + }, + "name": "children", + "type": { + "name": "string" + }, + "table": { + "category": "Content" + }, + "description": "The content to render inside the Typography component. Accepts text strings, numbers, React elements, or other Typography components for nested styling. Typography components can be nested to create rich text formatting with different styles." + }, + "variant": { + "control": { + "type": "select", + "disable": false + }, + "name": "variant", + "type": { + "name": "other", + "required": false, + "raw": "EnumOrPrimitive", + "value": "EnumOrPrimitive" + }, + "description": "Determines the semantic HTML element and corresponding typography styles. Options include headings (h1-h6), body text (p, small), display (div), code (code, kbd), and special elements (span, strong, i, caption, header). Each variant maps to theme tokens for consistent styling.", + "table": { + "type": { + "summary": "EnumOrPrimitive" + }, + "defaultValue": { + "summary": "p" + }, + "category": "Appearance" + }, + "options": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "p", + "small", + "div", + "span", + "strong", + "i", + "code", + "kbd", + "caption", + "header", + "sup", + "sub" + ] + }, + "align": { + "control": { + "type": "select", + "disable": false + }, + "name": "align", + "type": { + "name": "other", + "required": false, + "raw": "EnumOrPrimitive", + "value": "EnumOrPrimitive" + }, + "description": "Sets text-align CSS property. Options include \"start\" (default, respects RTL/LTR), \"end\", \"center\", \"left\", \"right\", \"justify\", \"match-parent\", and CSS keywords (initial, inherit, revert, unset). Use logical values (start/end) for international layouts.", + "table": { + "type": { + "summary": "EnumOrPrimitive" + }, + "defaultValue": { + "summary": "start" + }, + "category": "Layout" + }, + "options": [ + "start", + "end", + "center", + "left", + "right", + "justify", + "match-parent", + "inherit", + "initial", + "revert", + "revert-layer", + "unset" + ] + }, + "as": { + "control": { + "type": "text", + "disable": false + }, + "name": "as", + "description": "Polymorphic prop that changes the rendered element/component while preserving all Typography styles. Accepts HTML tag names (\"div\", \"span\", \"label\") or React components (Row, Column, Link). Useful for maintaining visual consistency across different semantic elements (e.g., h1 styles on a div for SEO flexibility).", + "type": { + "required": false, + "raw": "keyof HTMLElementTagNameMap | ElementType", + "name": "union", + "value": [ + { + "name": "other", + "value": "HTMLElementTagNameMap" + }, + { + "name": "other", + "value": "ElementType" + } + ] + }, + "table": { + "type": { + "summary": "union" + }, + "category": "Behavior" + } + }, + "color": { + "control": { + "type": "color", + "disable": false + }, + "name": "color", + "description": "Controls the text color. Accepts any valid CSS color value (hex, rgb, named colors) or theme token path (e.g., \"text.primary\", \"brand.500\"). Defaults to theme token \"colors.text.default\" for consistent theming. Use \"inherit\" to inherit color from parent elements.", + "type": { + "required": false, + "name": "string" + }, + "table": { + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "text.default" + }, + "category": "Appearance" + } + }, + "size": { + "control": { + "type": "select", + "disable": false + }, + "name": "size", + "description": "Controls font size and line height for Display variant (variant=\"div\") ONLY. Available sizes: xs, sm, md (default), lg, xl. This enables fine-grained control over hero text and marketing headlines. Ignored for all other variants.", + "type": { + "required": false, + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + "table": { + "type": { + "summary": "EnumOrPrimitive" + }, + "defaultValue": { + "summary": "md" + }, + "category": "Appearance" + }, + "options": ["xs", "sm", "md", "lg", "xl", "xxl"] + }, + "styleVariant": { + "control": { + "type": "multi-select", + "disable": false + }, + "name": "styleVariant", + "description": "Applies one or more style modifiers to enhance text appearance. Options: \"light\" (lighter weight), \"normal\" (normal weight), \"semibold\" (medium weight), \"bold\" (bold weight), \"italic\" (italic style), \"small\" (smaller font size), \"uppercase\", \"lowercase\", \"underline\", \"strike\" (strikethrough). Multiple values can be combined for rich formatting (e.g., [\"bold\", \"italic\", \"uppercase\"]).", + "type": { + "required": false, + "raw": "EnumOrPrimitive | EnumOrPrimitive[]", + "name": "union", + "value": [ + { + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + }, + { + "raw": "EnumOrPrimitive[]", + "name": "array", + "value": [ + { + "raw": "EnumOrPrimitive", + "name": "other", + "value": "EnumOrPrimitive" + } + ] + } + ] + }, + "table": { + "type": { + "summary": "union" + }, + "defaultValue": { + "summary": "[]" + }, + "category": "Appearance" + }, + "options": [ + "light", + "normal", + "semibold", + "bold", + "italic", + "small", + "uppercase", + "lowercase", + "underline", + "strike" + ] + }, + "styles": { + "name": "styles", + "table": { + "defaultValue": { + "summary": "{}" + }, + "category": "Appearance" + }, + "description": "Custom CSS styles object for one-off styling needs. Use sparingly—prefer variant and styleVariant props for consistent design. Accepts Emotion CSS object syntax with support for nested selectors, pseudo-classes, and media queries.", + "control": { + "type": "object", + "disable": false + } + } + }, + "Counter": { + "min": { + "control": { + "type": "number" + }, + "name": "min", + "description": "Minimum allowed value", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "1" + }, + "category": "Value & Constraints" + } + }, + "max": { + "control": { + "type": "number" + }, + "name": "max", + "description": "Maximum allowed value", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "999" + }, + "category": "Value & Constraints" + } + }, + "initial": { + "control": { + "type": "number" + }, + "name": "initial", + "description": "Initial value of the counter", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "1" + }, + "category": "Value & Constraints" + } + }, + "isDisabled": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "isDisabled", + "description": "Disables the counter component, preventing user interaction", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "false" + }, + "category": "Value & Constraints" + } + }, + "onCounterChange": { + "name": "onCounterChange", + "description": "Callback function triggered when counter value changes", + "type": { + "required": false, + "raw": "(qty: number) => void", + "name": "function" + }, + "table": { + "type": { + "summary": "(value: number) => void" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Events" + } + }, + "styles": { + "name": "styles", + "description": "CSS properties applied to the counter container", + "table": { + "category": "Styling", + "type": { + "summary": "React.CSSProperties" + }, + "defaultValue": { + "summary": "{}" + } + } + } + }, + "Menu": { + "onSelect": { + "control": { + "type": "object" + }, + "name": "onSelect", + "type": { + "name": "other", + "required": false, + "value": "SelectOnSelect" + }, + "description": "Callback fired when a menu item is selected. Receives the selected item data", + "table": { + "type": { + "summary": "(props: OnSelectProps) => void", + "detail": "OnSelectProps = { event: SyntheticBaseEvent, data: { name: string, value: unknown }" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Behavior & Interaction" + }, + "action": "selected" + }, + "itemIdentifier": { + "control": { + "type": "object" + }, + "name": "itemIdentifier", + "description": "Function to extract unique identifier from menu items for selection tracking", + "type": { + "required": false, + "name": "other", + "value": "ItemIdentifier" + }, + "table": { + "type": { + "summary": "(item: unknown) => string | number" + }, + "defaultValue": { + "summary": "undefined", + "detail": "(selected, current) => selected?.value === current?.value" + }, + "category": "Behavior & Interaction" + } + }, + "content": { + "control": { + "type": "object" + }, + "name": "content", + "description": "Menu content to be rendered inside the dropdown (typically DropdownItem components)", + "type": { + "required": false, + "name": "other", + "value": "ReactNode" + }, + "table": { + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "undefined" + }, + "category": "Content & Trigger" + } + }, + "closeOnSelect": { + "control": { + "type": "boolean", + "disable": false + }, + "name": "closeOnSelect", + "description": "Whether to close the menu when an option is clicked. Set to false to keep menu open for multiple selections", + "type": { + "required": false, + "name": "boolean" + }, + "table": { + "type": { + "summary": "boolean" + }, + "defaultValue": { + "summary": "true" + }, + "category": "Behavior & Interaction" + } + }, + "offsetX": { + "control": { + "type": "number", + "disable": false + }, + "name": "offsetX", + "description": "Horizontal distance in pixels between the menu and the trigger element. Used for positioning calculations", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "4" + }, + "category": "Menu Options (Positioning & Dimensions)" + } + }, + "offsetY": { + "control": { + "type": "number", + "disable": false + }, + "name": "offsetY", + "description": "Vertical distance in pixels between the menu and the trigger element. Used for positioning calculations", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "4" + }, + "category": "Menu Options (Positioning & Dimensions)" + } + }, + "minHeight": { + "control": { + "type": "number", + "disable": false + }, + "name": "minHeight", + "description": "Minimum height constraint for the menu dropdown in pixels. Used for dynamic height calculations based on available viewport space", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "80" + }, + "category": "Menu Options (Positioning & Dimensions)" + } + }, + "maxHeight": { + "control": { + "type": "number", + "disable": false + }, + "name": "maxHeight", + "description": "Maximum height constraint for the menu dropdown in pixels. Used for dynamic height calculations based on available viewport space", + "type": { + "required": false, + "name": "number" + }, + "table": { + "type": { + "summary": "number" + }, + "defaultValue": { + "summary": "400" + }, + "category": "Menu Options (Positioning & Dimensions)" + } + }, + "placement": { + "control": { + "type": "select", + "disable": false + }, + "options": ["top-left", "top-right", "bottom-left", "bottom-right"], + "name": "placement", + "description": "Position of the menu relative to the trigger. Defaults to \"bottom-right\" if not specified", + "type": { + "required": false, + "raw": "'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'", + "name": "enum", + "value": ["top-left", "top-right", "bottom-left", "bottom-right"] + }, + "table": { + "type": { + "summary": "'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'" + }, + "defaultValue": { + "summary": "'bottom-right'" + }, + "category": "Menu Options (Positioning & Dimensions)" + } + }, + "children": { + "name": "children", + "description": "Trigger element that opens the menu when clicked (button, text, icon, etc.)", + "table": { + "category": "Content & Trigger", + "type": { + "summary": "ReactNode" + }, + "defaultValue": { + "summary": "undefined" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "width": { + "name": "width", + "description": "Sets the width of the menu container", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "height": { + "name": "height", + "description": "Sets the height of the menu container", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "minWidth": { + "name": "minWidth", + "description": "CSS min-width property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "maxWidth": { + "name": "maxWidth", + "description": "CSS max-width property", + "table": { + "category": "Box Styles - Layout & Sizing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "margin": { + "name": "margin", + "description": "CSS margin property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginTop": { + "name": "marginTop", + "description": "CSS margin-top property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginRight": { + "name": "marginRight", + "description": "CSS margin-right property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginBottom": { + "name": "marginBottom", + "description": "CSS margin-bottom property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "marginLeft": { + "name": "marginLeft", + "description": "CSS margin-left property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "padding": { + "name": "padding", + "description": "CSS padding property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingTop": { + "name": "paddingTop", + "description": "CSS padding-top property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingRight": { + "name": "paddingRight", + "description": "CSS padding-right property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingBottom": { + "name": "paddingBottom", + "description": "CSS padding-bottom property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "paddingLeft": { + "name": "paddingLeft", + "description": "CSS padding-left property", + "table": { + "category": "Box Styles - Spacing", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexDirection": { + "name": "flexDirection", + "description": "CSS flex-direction property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "justifyContent": { + "name": "justifyContent", + "description": "CSS justify-content property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "justifySelf": { + "name": "justifySelf", + "description": "CSS justify-self property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignItems": { + "name": "alignItems", + "description": "CSS align-items property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignSelf": { + "name": "alignSelf", + "description": "CSS align-self property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "alignContent": { + "name": "alignContent", + "description": "CSS align-content property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexWrap": { + "name": "flexWrap", + "description": "CSS flex-wrap property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexGrow": { + "name": "flexGrow", + "description": "CSS flex-grow property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexShrink": { + "name": "flexShrink", + "description": "CSS flex-shrink property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flexBasis": { + "name": "flexBasis", + "description": "CSS flex-basis property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "flex": { + "name": "flex", + "description": "CSS flex property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "order": { + "name": "order", + "description": "CSS order property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "gap": { + "name": "gap", + "description": "CSS gap property", + "table": { + "category": "Box Styles - Flexbox", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "display": { + "name": "display", + "options": ["block", "flex", "inline", "inline-flex", "grid", "none"], + "description": "CSS display property to control the layout behavior", + "table": { + "type": { + "summary": "string" + }, + "category": "Box Styles - Position & Display" + }, + "control": { + "type": "select", + "disable": false + } + }, + "position": { + "name": "position", + "description": "CSS position property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "top": { + "name": "top", + "description": "CSS top property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "right": { + "name": "right", + "description": "CSS right property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "bottom": { + "name": "bottom", + "description": "CSS bottom property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "left": { + "name": "left", + "description": "CSS left property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "overflow": { + "name": "overflow", + "description": "CSS overflow property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "zIndex": { + "name": "zIndex", + "description": "CSS z-index property", + "table": { + "category": "Box Styles - Position & Display", + "type": { + "summary": "string | number" + } + }, + "control": { + "type": "text", + "disable": false + } + }, + "styles": { + "name": "styles", + "description": "Custom CSS styles object to be applied to the menu dropdown container", + "table": { + "category": "Custom Styling", + "type": { + "summary": "CSSObject" + }, + "defaultValue": { + "summary": "undefined" + } + }, + "control": { + "type": "object", + "disable": false + } + } + } +} From ee7761fa16c7a84c64f8f45bfceba6613ab90639 Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:28:11 +0200 Subject: [PATCH 04/17] fix(web-components): reuse migrated component primitives --- bin/storybook/native-checks.mjs | 11 +- .../scripts/check-ported-elements.mjs | 37 +- .../generate-react-storybook-metadata.mjs | 6 +- .../scripts/translate-storybook-metadata.mjs | 103 +++ .../atoms/gd-button/gd-button.spec.ts | 9 + .../components/atoms/gd-button/gd-button.ts | 34 +- .../atoms/gd-checkbox/gd-checkbox.spec.ts | 2 + .../atoms/gd-checkbox/gd-checkbox.ts | 30 +- .../atoms/gd-loader/gd-loader.spec.ts | 4 +- .../components/atoms/gd-loader/gd-loader.ts | 12 +- .../atoms/gd-select/gd-select.spec.ts | 4 + .../components/atoms/gd-select/gd-select.ts | 11 +- .../molecules/gd-counter/gd-counter.spec.ts | 21 + .../molecules/gd-counter/gd-counter.ts | 20 +- libs/web-components/stories/Loader.stories.ts | 13 +- .../react-storybook-arg-types.generated.ts | 689 +++++++++++------- 16 files changed, 681 insertions(+), 325 deletions(-) create mode 100644 libs/web-components/scripts/translate-storybook-metadata.mjs create mode 100644 libs/web-components/src/components/molecules/gd-counter/gd-counter.spec.ts diff --git a/bin/storybook/native-checks.mjs b/bin/storybook/native-checks.mjs index eeecd3a..d129756 100644 --- a/bin/storybook/native-checks.mjs +++ b/bin/storybook/native-checks.mjs @@ -1,10 +1,13 @@ import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; +import { translateStorybookMetadata } from '../../libs/web-components/scripts/translate-storybook-metadata.mjs'; -const reactArgTypes = JSON.parse( - await readFile( - new URL('../../libs/web-components/stories/react-storybook-arg-types.snapshot.json', import.meta.url), - 'utf8' +const reactArgTypes = translateStorybookMetadata( + JSON.parse( + await readFile( + new URL('../../libs/web-components/stories/react-storybook-arg-types.snapshot.json', import.meta.url), + 'utf8' + ) ) ); diff --git a/libs/web-components/scripts/check-ported-elements.mjs b/libs/web-components/scripts/check-ported-elements.mjs index 4511efe..a74536b 100644 --- a/libs/web-components/scripts/check-ported-elements.mjs +++ b/libs/web-components/scripts/check-ported-elements.mjs @@ -2,6 +2,7 @@ import { readdir, readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import ts from 'typescript'; +import { translateStorybookMetadata } from './translate-storybook-metadata.mjs'; const packageRoot = resolve(import.meta.dirname, '..'); const componentsRoot = resolve(packageRoot, 'src/components'); @@ -144,8 +145,30 @@ const generatedArgTypesSource = generatedArgTypesText.match(/reactStorybookArgTy const generatedArgTypes = generatedArgTypesSource ? Function(`"use strict"; return (${generatedArgTypesSource});`)() : {}; -if (JSON.stringify(generatedArgTypes) !== JSON.stringify(JSON.parse(sourceArgTypesText))) - throw new Error('Generated React Storybook metadata does not match the published source snapshot.'); +const translatedArgTypes = translateStorybookMetadata(JSON.parse(sourceArgTypesText)); +if (JSON.stringify(generatedArgTypes) !== JSON.stringify(translatedArgTypes)) + throw new Error('Generated Web Component Storybook metadata does not match the translated React source snapshot.'); +const reactOnlyMetadata = JSON.stringify(generatedArgTypes).match( + /ReactNode|ReactElement|ElementType|React\.|CSSProperties/g +); +if (reactOnlyMetadata) + throw new Error( + `Generated Web Component Storybook metadata contains React-only types: ${[...new Set(reactOnlyMetadata)].join( + ', ' + )}.` + ); + +const requiredComposition = { + 'gd-button': [' category === 'atoms').map(({ tag }) => tag); const publishedAtomTags = Object.keys(publishedAudit.components); const missingPublishedAtoms = publishedAtomTags.filter((tag) => !atomTags.includes(tag)); @@ -167,6 +190,12 @@ for (const { tag, category } of ports) { const contract = parity[tag]; if (!contract) throw new Error(`Missing React parity contract for ${tag}.`); const source = await readFile(resolve(componentsRoot, category, tag, `${tag}.ts`), 'utf8'); + for (const dependency of requiredComposition[tag] ?? []) { + if (!source.includes(dependency)) + throw new Error(`${tag} must compose the migrated ${dependency.slice(1)} component.`); + } + if (tag !== 'gd-icon' && source.includes(' letter.toUpperCase()); const stories = await readFile(resolve(packageRoot, 'stories', `${storyName}.stories.ts`), 'utf8'); if (fullStoryParity.has(tag) && !stories.includes('description:')) @@ -196,7 +225,9 @@ for (const { tag, category } of ports) { const targetSection = targetMetadata.table?.[section]; if (sourceSection === targetSection) continue; throw new Error( - `${tag} Storybook ${section} mismatch for ${control}: React=${sourceSection ?? 'unsectioned'}, Web Component=${targetSection ?? 'unsectioned'}.` + `${tag} Storybook ${section} mismatch for ${control}: React=${ + sourceSection ?? 'unsectioned' + }, Web Component=${targetSection ?? 'unsectioned'}.` ); } } diff --git a/libs/web-components/scripts/generate-react-storybook-metadata.mjs b/libs/web-components/scripts/generate-react-storybook-metadata.mjs index 0dbbf86..9bc39ab 100644 --- a/libs/web-components/scripts/generate-react-storybook-metadata.mjs +++ b/libs/web-components/scripts/generate-react-storybook-metadata.mjs @@ -2,6 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import ts from 'typescript'; +import { translateStorybookMetadata } from './translate-storybook-metadata.mjs'; const packageRoot = resolve(import.meta.dirname, '..'); const repoRoot = resolve(packageRoot, '../..'); @@ -122,7 +123,7 @@ function extractArgTypes(fileName, source) { ); } -const metadata = JSON.parse(await readFile(snapshotPath, 'utf8')); +const sourceMetadata = JSON.parse(await readFile(snapshotPath, 'utf8')); for (const [tag, contract] of Object.entries(parity)) { const category = tag === 'gd-counter' || tag === 'gd-menu' ? 'molecules' : 'atoms'; const fileName = resolve( @@ -133,12 +134,13 @@ for (const [tag, contract] of Object.entries(parity)) { `${contract.react}.stories.tsx` ); const explicitArgTypes = extractArgTypes(fileName, await readFile(fileName, 'utf8')); - const publishedArgTypes = metadata[contract.react]; + const publishedArgTypes = sourceMetadata[contract.react]; if (!publishedArgTypes) throw new Error(`${contract.react} is missing from the published Storybook snapshot.`); const missing = Object.keys(explicitArgTypes).filter((control) => !(control in publishedArgTypes)); if (missing.length) throw new Error(`${contract.react} published Storybook snapshot is missing controls: ${missing.join(', ')}.`); } +const metadata = translateStorybookMetadata(sourceMetadata); const output = `/* This file is generated from the React Storybook metadata. Do not edit it by hand. */\nexport const reactStorybookArgTypes = ${JSON.stringify( metadata, diff --git a/libs/web-components/scripts/translate-storybook-metadata.mjs b/libs/web-components/scripts/translate-storybook-metadata.mjs new file mode 100644 index 0000000..2565dde --- /dev/null +++ b/libs/web-components/scripts/translate-storybook-metadata.mjs @@ -0,0 +1,103 @@ +const slotNames = { + children: 'default', + iconStart: 'icon-start', + iconEnd: 'icon-end', + adornmentStart: 'adornment-start', + adornmentEnd: 'adornment-end', + initiator: 'initiator', + content: 'content', + emptyItemsResult: 'empty-items-result', + fallbackComponent: 'fallback', + placeholder: 'placeholder', +}; + +const eventNames = { + onClick: 'click', + onBlur: 'blur', + onFocus: 'focus', + onKeyDown: 'keydown', + onChange: 'gd-change', + onValueChange: 'gd-change', + onCounterChange: 'gd-change', + onDotClick: 'gd-change', + onSelect: 'gd-change', +}; + +function translateString(value) { + return value + .replaceAll('React.CSSProperties', 'Partial') + .replaceAll('CSSProperties', 'Partial') + .replaceAll('ReactNode', 'Node | string') + .replaceAll('ReactElement', 'Element') + .replaceAll('ElementType', 'keyof HTMLElementTagNameMap') + .replace(/(?:React\.)?(?:Change|Focus|Keyboard|Mouse)Event<[^>]+>/g, 'Event') + .replaceAll('React.MouseEvent', 'MouseEvent') + .replaceAll('ReactEventHandler', 'EventListener') + .replace(/React components?/gi, 'custom elements') + .replace(/React elements?/gi, 'DOM elements') + .replace(/React content/gi, 'slotted content') + .replace(/React nodes?/gi, 'DOM nodes') + .replaceAll('keyof HTMLElementTagNameMap | keyof HTMLElementTagNameMap', 'keyof HTMLElementTagNameMap'); +} + +function translateValue(value) { + if (typeof value === 'string') return translateString(value); + if (Array.isArray(value)) return value.map(translateValue); + if (!value || typeof value !== 'object') return value; + return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, translateValue(child)])); +} + +function slotArgType(source, slot) { + const label = slot === 'default' ? 'default slot' : `slot="${slot}"`; + return { + ...source, + description: `${translateString( + source.description ?? 'Content rendered by the component.' + )} Web Components provide this content through the ${label}.`, + control: { disable: true }, + type: { name: 'other', value: label }, + table: { + ...source.table, + type: { summary: label }, + }, + }; +} + +function eventArgType(source, eventName) { + const custom = eventName.startsWith('gd-'); + const type = custom ? `CustomEvent dispatched as "${eventName}"` : `native "${eventName}" event`; + return { + ...source, + description: `${translateString( + source.description ?? 'Event notification.' + )} Listen for the ${type} with addEventListener().`, + control: { disable: true }, + type: { name: 'function', value: custom ? 'EventListener' : 'EventListener' }, + table: { + ...source.table, + type: { summary: custom ? `CustomEvent ("${eventName}")` : `Event ("${eventName}")` }, + }, + }; +} + +export function translateStorybookMetadata(metadata) { + return Object.fromEntries( + Object.entries(metadata).map(([component, argTypes]) => [ + component, + Object.fromEntries( + Object.entries(argTypes).map(([name, raw]) => { + const source = translateValue(raw); + if (slotNames[name]) return [name, slotArgType(source, slotNames[name])]; + if (eventNames[name]) return [name, eventArgType(source, eventNames[name])]; + if (name === 'WrapperView' || name === 'as' || name.endsWith('As')) { + source.description = `${ + source.description ?? 'Rendered element.' + } Use a native HTML tag name in the Web Component API.`; + source.table = { ...source.table, type: { summary: 'keyof HTMLElementTagNameMap' } }; + } + return [name, source]; + }) + ), + ]) + ); +} diff --git a/libs/web-components/src/components/atoms/gd-button/gd-button.spec.ts b/libs/web-components/src/components/atoms/gd-button/gd-button.spec.ts index 410f399..41898a6 100644 --- a/libs/web-components/src/components/atoms/gd-button/gd-button.spec.ts +++ b/libs/web-components/src/components/atoms/gd-button/gd-button.spec.ts @@ -43,6 +43,15 @@ afterEach(() => { }); describe('gd-button', () => { + it('composes gd-loader for the React loading state', async () => { + const el = add('primary'); + (el as unknown as { isLoading: boolean }).isLoading = true; + await settle(el); + const loader = el.shadowRoot!.querySelector('gd-loader'); + expect(loader?.size).toBe('sm'); + expect(loader?.withWrapper).toBe(false); + }); + it('resolves the real primary token colour, not a placeholder string', async () => { const el = add('primary'); await settle(el); diff --git a/libs/web-components/src/components/atoms/gd-button/gd-button.ts b/libs/web-components/src/components/atoms/gd-button/gd-button.ts index 5fef0a5..5a8d22f 100644 --- a/libs/web-components/src/components/atoms/gd-button/gd-button.ts +++ b/libs/web-components/src/components/atoms/gd-button/gd-button.ts @@ -12,6 +12,7 @@ import { get, type DesignCoreTheme, } from 'gd-design-core'; +import '../gd-loader/gd-loader'; /** Mirrors `Button.types.ts`'s `ButtonStyledProps['$rounded']`. Button's own default is * `rounded="none"` (`button.ts`'s `attrs.rounded: 'none'`), i.e. square corners. */ @@ -32,7 +33,7 @@ export type ButtonRoleName = 'button' | 'link' | 'checkbox' | 'switch' | 'tab'; * {iconStart} --> * {children} --> * {iconEnd} --> - * {isLoading && } --> + * {isLoading && } --> * ``` * * Each of the three child spans is only rendered when its slot actually has assigned content @@ -70,9 +71,9 @@ export type ButtonRoleName = 'button' | 'link' | 'checkbox' | 'switch' | 'tab'; * `transition` is additionally applied directly to `.gd-button__content`/`.gd-button__icon-*`, * since a CSS `transition` only animates an element's OWN computed-style changes — a child span * whose `color` merely inherits the button's animated color still needs its own `transition` - * declaration to animate in step. Truly static, non-token structural CSS (`display`, `cursor` - * mechanics, the loading-spinner keyframes) stays in Lit's normal shared `static styles`, since - * it never varies per instance or theme. + * declaration to animate in step. Truly static, non-token structural CSS (`display` and cursor + * mechanics) stays in Lit's normal shared `static styles`, since it never varies per instance or + * theme. Loading composes the migrated `gd-loader`, matching the React component dependency. * * The focus ring matches the real component's actual mechanism — `getFocusStyles` in * `libs/ui/src/tokens/utils.ts` is a `::after` pseudo-element positioned with `inset: -4px` @@ -167,20 +168,6 @@ export class GdButton extends LitElement { button:focus-visible { outline: none; } - .spinner { - display: inline-block; - width: 14px; - height: 14px; - border: 2px solid currentColor; - border-right-color: transparent; - border-radius: 9999px; - animation: gd-button-spin 0.6s linear infinite; - } - @keyframes gd-button-spin { - to { - transform: rotate(360deg); - } - } `; @property({ type: String }) variant: ButtonVariantName = 'primary'; @@ -382,7 +369,16 @@ export class GdButton extends LitElement { >` : html``} - ${this.isLoading ? html`` : nothing} + ${this.isLoading + ? html`` + : nothing} `; } diff --git a/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.spec.ts b/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.spec.ts index fdb138a..42d2ee5 100644 --- a/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.spec.ts +++ b/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.spec.ts @@ -46,6 +46,7 @@ describe('gd-checkbox', () => { const indicator = el.shadowRoot!.querySelector('.indicator')!; // #FFB800 — the real brand gold from libs/ui/src/tokens/checkbox.ts expect(getComputedStyle(indicator).backgroundColor).toBe('rgb(255, 184, 0)'); + expect(el.shadowRoot!.querySelector('gd-icon')?.getAttribute('name')).toBe('check'); }); it('exposes label/input/indicator as CSS parts (CTORNDSD-646b)', async () => { @@ -192,5 +193,6 @@ describe('gd-checkbox', () => { expect(el.shadowRoot!.querySelector('input')!.indeterminate).toBe(true); expect(el.shadowRoot!.querySelector('input')!.getAttribute('aria-checked')).toBe('mixed'); + expect(el.shadowRoot!.querySelector('gd-icon')?.getAttribute('name')).toBe('minus'); }); }); diff --git a/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.ts b/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.ts index 82bed35..64888df 100644 --- a/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.ts +++ b/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.ts @@ -3,6 +3,7 @@ import { customElement, property, query } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import { checkbox } from 'gd-design-library/tokens'; import { resolveThemeTree, createCheckboxStore, type CheckboxSizeName, type DesignCoreTheme } from 'gd-design-core'; +import '../gd-icon/gd-icon'; interface ResolvedCheckboxTokens { indicatorSize: number; @@ -86,9 +87,6 @@ export class GdCheckbox extends LitElement { justify-content: center; box-sizing: border-box; } - svg { - display: block; - } `; /** @@ -293,19 +291,21 @@ export class GdCheckbox extends LitElement { /> ${this.indeterminate - ? html` - - ` + ? html`` : currentChecked - ? html` - - ` + ? html`` : nothing} + + +`, }, }, }, - args: { name: 'star' }, }; export const WithAccessibility: Story = { render: (args) => { diff --git a/libs/web-components/stories/react-storybook-arg-types.generated.ts b/libs/web-components/stories/react-storybook-arg-types.generated.ts index 41565af..40d2abb 100644 --- a/libs/web-components/stories/react-storybook-arg-types.generated.ts +++ b/libs/web-components/stories/react-storybook-arg-types.generated.ts @@ -5852,11 +5852,13 @@ export const reactStorybookArgTypes = { name: 'other', value: 'default slot', }, - description: 'Content rendered by the component. Web Components provide this content through the default slot.', + description: + 'Label content displayed next to the checkbox Web Components provide this content through the default slot.', table: { type: { summary: 'default slot', }, + category: 'Content', }, }, onValueChange: { diff --git a/libs/web-components/stories/react-storybook-arg-types.snapshot.json b/libs/web-components/stories/react-storybook-arg-types.snapshot.json index 779286b..dc8870d 100644 --- a/libs/web-components/stories/react-storybook-arg-types.snapshot.json +++ b/libs/web-components/stories/react-storybook-arg-types.snapshot.json @@ -5756,6 +5756,13 @@ "name": "children", "type": { "name": "string" + }, + "description": "Label content displayed next to the checkbox", + "table": { + "type": { + "summary": "ReactNode" + }, + "category": "Content" } }, "onValueChange": { From 1ce7eae7f22fb4783fec52781c22a13345568c1b Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:51:19 +0200 Subject: [PATCH 09/17] fix(storybook): match React icon library layout --- bin/storybook/native-checks.mjs | 51 +++++++++++++++++++++ libs/web-components/stories/Icon.stories.ts | 29 ++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/bin/storybook/native-checks.mjs b/bin/storybook/native-checks.mjs index 578aa7a..3510228 100644 --- a/bin/storybook/native-checks.mjs +++ b/bin/storybook/native-checks.mjs @@ -63,6 +63,57 @@ export async function checkNativeStories(page, base) { assert.equal(customIconExample.hasCircle, true, 'Custom icon example must render its registered SVG'); assert.match(customIconExample.source, /registerCustomIcons/); assert.match(customIconExample.source, / { + const rect = (element) => { + const bounds = element.getBoundingClientRect(); + return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }; + }; + return { + container: rect(document.querySelector('.icon-library')), + names: [...document.querySelectorAll('.icon-library-item gd-typography')].map((label) => label.textContent), + items: [...document.querySelectorAll('.icon-library-item')].map(rect), + icons: [...document.querySelectorAll('.icon-library-item gd-icon')].map((icon) => + rect(icon.shadowRoot.querySelector('svg')) + ), + labels: [...document.querySelectorAll('.icon-library-item gd-typography')].map((label) => + rect(label.shadowRoot.querySelector('small')) + ), + }; + }); + await page.goto(`${base}/iframe.html?id=react-atoms-icon--all-icons&viewMode=story`); + await page.locator('[data-testid="Column"]').first().waitFor({ state: 'attached' }); + const reactIconLibrary = await page.evaluate(() => { + const rect = (element) => { + const bounds = element.getBoundingClientRect(); + return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }; + }; + return { + container: rect(document.querySelector('[data-testid="FlexContainer"]')), + names: [...document.querySelectorAll('[data-testid="Column"] [data-testid="typography"]')].map( + (label) => label.textContent + ), + items: [...document.querySelectorAll('[data-testid="Column"]')].map(rect), + icons: [...document.querySelectorAll('[data-testid="Column"] svg')].map(rect), + labels: [...document.querySelectorAll('[data-testid="Column"] [data-testid="typography"]')].map(rect), + }; + }); + assert.deepEqual(webIconLibrary.names, reactIconLibrary.names, 'Icon library names and order must match React'); + const assertRects = (webRects, reactRects, label) => { + assert.equal(webRects.length, reactRects.length, `${label} count must match React`); + webRects.forEach((webRect, index) => { + for (const key of ['x', 'y', 'width', 'height']) + assert.ok( + Math.abs(webRect[key] - reactRects[index][key]) < 0.25, + `${label} ${index + 1} ${key} must match React` + ); + }); + }; + assertRects([webIconLibrary.container], [reactIconLibrary.container], 'Icon library container'); + assertRects(webIconLibrary.items, reactIconLibrary.items, 'Icon library item'); + assertRects(webIconLibrary.icons, reactIconLibrary.icons, 'Icon library SVG'); + assertRects(webIconLibrary.labels, reactIconLibrary.labels, 'Icon library label'); const badgeIcons = index.entries['atoms-badge--with-icons']; assert.ok(badgeIcons, 'Atoms/Badge WithIcons must exist'); await page.goto(`${base}/web-components/iframe.html?id=${badgeIcons.id}&viewMode=story`); diff --git a/libs/web-components/stories/Icon.stories.ts b/libs/web-components/stories/Icon.stories.ts index 406bae7..7e8ad8e 100644 --- a/libs/web-components/stories/Icon.stories.ts +++ b/libs/web-components/stories/Icon.stories.ts @@ -9,6 +9,7 @@ const meta = { title: 'Atoms/Icon', tags: ['autodocs'], parameters: { + layout: 'centered', docs: { description: { component: @@ -35,14 +36,34 @@ export const WithDefinedSize: Story = { args: { size: 'xl' } }; export const AllIcons: Story = { name: 'Icons Library', render: () => { + const container = document.createElement('div'); + container.className = 'icon-library'; + container.style.cssText = 'display:flex;min-width:480px;max-width:800px;width:100%'; const grid = document.createElement('div'); - grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:16px'; + grid.className = 'icon-library-grid'; + grid.style.cssText = 'display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:10px;width:100%'; for (const name of names) { - const item = document.createElement('label'); - item.append(element('gd-icon', { name, size: 'lg' }), document.createTextNode(name)); + const item = document.createElement('div'); + item.className = 'icon-library-item'; + item.style.cssText = + 'display:flex;flex-direction:column;flex-wrap:wrap;align-items:center;justify-content:flex-start;min-width:80px;max-width:100%;gap:10px;margin-bottom:10px;padding:5px;box-sizing:border-box'; + item.append( + element('gd-icon', { name, size: 'md' }), + element('gd-typography', { variant: 'small', as: 'small' }, name) + ); grid.append(item); } - return grid; + container.append(grid); + return container; + }, + parameters: { + controls: { hideNoControlsWarning: true }, + docs: { + description: { + story: + 'Browse every built-in icon with its registered name. Each icon uses the standard medium size and can be copied by name into component properties or markup.', + }, + }, }, }; export const RegisteringCustomIcons: Story = { From 9377a9ebb8055705c85665f16b36a0c16f326b2e Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:59:46 +0200 Subject: [PATCH 10/17] fix(image): match React stories and loading states --- .../atoms/gd-image/gd-image.spec.ts | 23 ++++ .../src/components/atoms/gd-image/gd-image.ts | 56 +++++--- libs/web-components/stories/Image.stories.ts | 127 ++++++++++++++---- 3 files changed, 159 insertions(+), 47 deletions(-) diff --git a/libs/web-components/src/components/atoms/gd-image/gd-image.spec.ts b/libs/web-components/src/components/atoms/gd-image/gd-image.spec.ts index 7842984..700775f 100644 --- a/libs/web-components/src/components/atoms/gd-image/gd-image.spec.ts +++ b/libs/web-components/src/components/atoms/gd-image/gd-image.spec.ts @@ -23,4 +23,27 @@ describe('gd-image', () => { expect(element.shadowRoot!.querySelector('figcaption')?.textContent).toBe('Portrait'); element.remove(); }); + + it('matches React wrapper, caption, and placeholder semantics', async () => { + const element = document.createElement('gd-image'); + element.src = '/portrait.png'; + element.placeholder = 'Loading image...'; + element.caption = 'Caption'; + document.body.append(element); + await element.updateComplete; + expect(element.shadowRoot!.querySelector('[part="root"]')?.tagName).toBe('DIV'); + expect(element.shadowRoot!.querySelector('gd-skeleton')?.textContent).toContain('Loading image...'); + const image = element.shadowRoot!.querySelector('img')!; + expect(image.getAttribute('src')).toBe('/portrait.png'); + image.dispatchEvent(new Event('load')); + await element.updateComplete; + expect(image.style.opacity).toBe('1'); + expect(element.shadowRoot!.querySelector('gd-skeleton')).toBeNull(); + element.as = 'figure'; + element.captionAs = 'p'; + await element.updateComplete; + expect(element.shadowRoot!.querySelector('[part="root"]')?.tagName).toBe('FIGURE'); + expect(element.shadowRoot!.querySelector('[part="caption"]')?.tagName).toBe('P'); + element.remove(); + }); }); diff --git a/libs/web-components/src/components/atoms/gd-image/gd-image.ts b/libs/web-components/src/components/atoms/gd-image/gd-image.ts index c3a5bed..993fb17 100644 --- a/libs/web-components/src/components/atoms/gd-image/gd-image.ts +++ b/libs/web-components/src/components/atoms/gd-image/gd-image.ts @@ -1,19 +1,25 @@ import { LitElement, html, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; +import { html as staticHtml, unsafeStatic } from 'lit/static-html.js'; import { image } from 'gd-design-library/tokens'; import { resolveThemeTree, type DesignCoreTheme } from 'gd-design-core'; +import '../gd-skeleton/gd-skeleton'; export type ImageObjectFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; type CssValues = Record; @customElement('gd-image') export class GdImage extends LitElement { + @property({ type: String, reflect: true }) as: 'div' | 'figure' = 'div'; + @property({ type: String, attribute: 'caption-as', reflect: true }) captionAs: 'figcaption' | 'p' | 'span' = + 'figcaption'; @property({ type: String, reflect: true }) src?: string; @property({ type: String, reflect: true }) alt = ''; @property({ type: Number, reflect: true }) width?: number; @property({ type: Number, reflect: true }) height?: number; @property({ type: String, reflect: true }) caption?: string; + @property({ type: String, reflect: true }) placeholder?: string; @property({ type: String, attribute: 'object-fit', reflect: true }) objectFit: ImageObjectFit = 'cover'; @property({ attribute: false }) styles: CssValues = {}; @property({ attribute: false }) theme: DesignCoreTheme = {}; @@ -54,26 +60,36 @@ export class GdImage extends LitElement { ...this.styles, }; const showFallback = !this.src || this._failed; - return html`
- ${this._loading && this.src - ? html`` - : nothing} - ${showFallback - ? html`` - : html`${this.alt}`} - ${this.caption - ? html`
${this.caption}
` - : nothing} -
`; + const wrapperTag = unsafeStatic(this.as === 'figure' ? 'figure' : 'div'); + const captionTag = unsafeStatic(['p', 'span'].includes(this.captionAs) ? this.captionAs : 'figcaption'); + return staticHtml`<${wrapperTag} part="root" style=${styleMap(wrapper)}> + ${ + this._loading && this.src && this.placeholder + ? html` + ${this.placeholder} + ` + : nothing + } + ${ + showFallback + ? html`` + : html`${this.alt}` + } + ${ + this.caption + ? staticHtml`<${captionTag} part="caption" style=${styleMap(tokens.caption)}>${this.caption}` + : nothing + } + `; } } diff --git a/libs/web-components/stories/Image.stories.ts b/libs/web-components/stories/Image.stories.ts index ed8704a..0c5e5d5 100644 --- a/libs/web-components/stories/Image.stories.ts +++ b/libs/web-components/stories/Image.stories.ts @@ -1,59 +1,127 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdImage } from '../src'; -import { defaultTokenViewer, element, portrait, sectionedArgTypes } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; -type Args = Pick; +type Args = Partial< + Pick +>; +const COMMON_ARGS: Args = { + src: 'https://picsum.photos/150/150', + alt: 'Test image', + width: 150, + height: 150, + placeholder: 'Loading...', + caption: 'Test Caption', + objectFit: 'cover', +}; const meta = { title: 'Atoms/Image', tags: ['autodocs'], parameters: { + layout: 'centered', docs: { description: { component: - 'Responsive image matching React Image loading, placeholder, fallback, caption, object-fit, click, load/error events, and accessibility behavior. Slots replace React node props; native figure/figcaption semantics replace polymorphic wrapper props.', + 'Responsive image with loading, placeholder, fallback, caption, object-fit, click, load/error events, slots, and native figure semantics.', }, }, }, - args: { src: portrait, alt: 'Portrait', width: 160, height: 160, caption: '', objectFit: 'cover' }, - argTypes: sectionedArgTypes('Image', { - src: { description: 'Image source URL', control: 'text' }, - alt: { description: 'Accessible image alternative text', control: 'text' }, - width: { description: 'Rendered image width in pixels', control: 'number' }, - height: { description: 'Rendered image height in pixels', control: 'number' }, - caption: { description: 'Optional visible image caption', control: 'text' }, + args: { + src: 'https://picsum.photos/300/300', + alt: 'Sample image', + width: 300, + height: 300, + objectFit: 'cover', + }, + argTypes: { + ...sectionedArgTypes('Image', {}), + as: { + description: 'Semantic element used by the internal image wrapper', + control: 'select', + options: ['div', 'figure'], + table: { type: { summary: `'div' | 'figure'` }, defaultValue: { summary: 'div' }, category: 'Core Properties' }, + }, + captionAs: { + description: 'Semantic element used for the caption', + control: 'select', + options: ['figcaption', 'p', 'span'], + table: { + type: { summary: `'figcaption' | 'p' | 'span'` }, + defaultValue: { summary: 'figcaption' }, + category: 'Core Properties', + }, + }, + placeholder: { + description: 'Text displayed in a Skeleton while the image loads', + control: 'text', + table: { type: { summary: 'string' }, category: 'Content & Display' }, + }, + fallbackComponent: { + description: 'Element assigned to the fallback slot and displayed when the source is missing or fails to load', + control: { disable: true }, + table: { type: { summary: 'HTMLElement (slot="fallback")' }, category: 'Content & Display' }, + }, + styles: { + description: 'CSS property values applied to the internal image element', + control: { disable: true }, + table: { type: { summary: 'Record' }, category: 'Styling' }, + }, + onClick: { + description: 'Listen for the native click event on gd-image', + control: { disable: true }, + table: { type: { summary: 'EventListener' }, category: 'Events' }, + }, + onError: { + description: 'Listen for the gd-error custom event', + control: { disable: true }, + table: { type: { summary: 'EventListener' }, category: 'Events' }, + }, + onLoad: { + description: 'Listen for the gd-load custom event', + control: { disable: true }, + table: { type: { summary: 'EventListener' }, category: 'Events' }, + }, objectFit: { description: 'Native object-fit behavior', control: 'select', options: ['cover', 'contain', 'fill', 'none', 'scale-down'], }, - }), + }, render: (args: Args) => element('gd-image', args), } satisfies Meta; export default meta; type Story = StoryObj; -export const Default: Story = {}; -export const WithCaption: Story = { args: { caption: 'Ada Lovelace' } }; +export const Default: Story = { name: 'Default Image', args: COMMON_ARGS }; +export const WithCaption: Story = { + name: 'Image With Caption', + args: { ...COMMON_ARGS, caption: 'This is a sample caption below the image.' }, +}; export const WithPlaceholder: Story = { - render: (args) => { - const image = element('gd-image', { ...args, src: 'data:image/png;base64,' }); - const placeholder = document.createElement('span'); - placeholder.slot = 'placeholder'; - placeholder.textContent = 'Loading…'; - image.append(placeholder); - return image; - }, + name: 'Image With Placeholder', + args: { ...COMMON_ARGS, placeholder: 'Loading image...' }, }; export const FallbackImage: Story = { + name: 'Fallback Image', + args: { ...COMMON_ARGS, src: undefined, caption: undefined }, render: (args) => { - const image = element('gd-image', { ...args, src: '' }); - const fallback = document.createElement('span'); + const image = element('gd-image', args); + const fallback = document.createElement('div'); fallback.slot = 'fallback'; - fallback.textContent = 'Image unavailable'; + fallback.setAttribute('role', 'alert'); + fallback.setAttribute('aria-live', 'assertive'); + fallback.style.cssText = 'display:inline-flex;align-items:center;gap:4px;width:max-content'; + const icon = element('gd-icon', { name: 'error', size: 'md', fill: 'icon.error' }); + const message = document.createElement('span'); + message.style.cssText = 'flex:1;font-family:"Fira Sans",sans-serif;font-weight:500;font-size:14px;line-height:20px'; + message.textContent = 'Fallback component'; + fallback.append(icon, message); image.append(fallback); return image; }, }; export const Clickable: Story = { + name: 'Clickable Image', + args: { ...COMMON_ARGS, caption: 'Click me!' }, render: (args) => { const image = element('gd-image', args); image.style.cursor = 'pointer'; @@ -63,7 +131,8 @@ export const Clickable: Story = { }, }; export const WithAsProp: Story = { - ...Default, + name: 'Image With As Prop', + args: { ...COMMON_ARGS, as: 'figure', caption: 'Image with semantic figure wrapper' }, parameters: { docs: { description: { @@ -73,7 +142,8 @@ export const WithAsProp: Story = { }, }; export const WithCaptionAsProp: Story = { - args: { caption: 'Semantic figcaption' }, + name: 'Image With captionAs Prop', + args: { ...COMMON_ARGS, caption: 'Image with custom caption element (p tag)', captionAs: 'p' }, parameters: { docs: { description: { @@ -82,7 +152,10 @@ export const WithCaptionAsProp: Story = { }, }, }; -export const WithAccessibility: Story = { args: { alt: 'Portrait of Ada Lovelace', caption: 'Ada Lovelace' } }; +export const WithAccessibility: Story = { + name: 'Image With Accessibility', + args: { ...COMMON_ARGS, alt: 'Test image' }, +}; export const DefaultTokens: Story = { render: () => defaultTokenViewer('image'), }; From 4e777e32662281dc9ad10868d92b4d98da9713f0 Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:25:58 +0200 Subject: [PATCH 11/17] feat(storybook): standardize component docs parity --- bin/storybook/check-links.mjs | 20 +- bin/storybook/check-story-parity.mjs | 20 + bin/storybook/native-checks.mjs | 233 +++++--- bin/storybook/smoke-test.mjs | 8 +- bin/storybook/story-parity.mjs | 219 ++++++++ libs/ui/.storybook/preview.ts | 4 + libs/ui/.storybook/public/defaultTheme.json | 1 + libs/ui/src/tokens/input.ts | 2 + libs/web-components/.storybook/preview.ts | 2 + libs/web-components/eslint.config.mjs | 1 + libs/web-components/react-parity.json | 18 +- .../scripts/check-ported-elements.mjs | 9 +- .../scripts/generate-story-fixtures.mjs | 216 ++++++++ .../scripts/translate-storybook-metadata.mjs | 1 + .../components/atoms/gd-button/gd-button.ts | 1 + .../atoms/gd-checkbox/gd-checkbox.ts | 2 +- .../atoms/gd-input/gd-input.spec.ts | 57 ++ .../src/components/atoms/gd-input/gd-input.ts | 84 ++- .../src/components/atoms/gd-label/gd-label.ts | 7 +- .../molecules/gd-menu/gd-menu.spec.ts | 15 + .../components/molecules/gd-menu/gd-menu.ts | 8 + libs/web-components/stories/Avatar.stories.ts | 87 +-- libs/web-components/stories/Badge.stories.ts | 97 +--- libs/web-components/stories/Box.stories.ts | 73 +-- libs/web-components/stories/Button.stories.ts | 129 +---- .../stories/Checkbox.stories.ts | 61 +- .../web-components/stories/Counter.stories.ts | 39 +- libs/web-components/stories/Icon.stories.ts | 6 +- libs/web-components/stories/Image.stories.ts | 66 +-- libs/web-components/stories/Input.stories.ts | 262 +++++---- .../stories/InputFile.stories.ts | 78 +-- libs/web-components/stories/Label.stories.ts | 57 +- libs/web-components/stories/Link.stories.ts | 102 +--- libs/web-components/stories/Loader.stories.ts | 90 +-- libs/web-components/stories/Menu.stories.ts | 187 ++++--- .../stories/Overview.stories.ts | 7 +- libs/web-components/stories/Select.stories.ts | 276 +++++++--- .../stories/Separator.stories.ts | 16 +- .../stories/Skeleton.stories.ts | 100 +--- libs/web-components/stories/Slider.stories.ts | 6 +- .../stories/SliderDots.stories.ts | 18 +- libs/web-components/stories/Switch.stories.ts | 71 ++- .../stories/Textarea.stories.ts | 11 +- libs/web-components/stories/Toggle.stories.ts | 19 +- .../stories/Truncate.stories.ts | 49 +- .../stories/Typography.stories.ts | 145 +---- .../web-components/stories/Wrapper.stories.ts | 79 +-- .../stories/fixtures/Avatar.generated.js | 149 +++++ .../stories/fixtures/Badge.generated.js | 256 +++++++++ .../stories/fixtures/Box.generated.js | 399 ++++++++++++++ .../stories/fixtures/Button.generated.js | 521 ++++++++++++++++++ .../stories/fixtures/Counter.generated.js | 103 ++++ .../stories/fixtures/InputFile.generated.js | 122 ++++ .../stories/fixtures/Label.generated.js | 106 ++++ .../stories/fixtures/Link.generated.js | 178 ++++++ .../stories/fixtures/Loader.generated.js | 283 ++++++++++ .../stories/fixtures/Skeleton.generated.js | 157 ++++++ .../stories/fixtures/Typography.generated.js | 339 ++++++++++++ .../stories/fixtures/Wrapper.generated.js | 207 +++++++ libs/web-components/stories/helpers.ts | 39 +- .../web-components/stories/loader-examples.ts | 51 ++ libs/web-components/stories/native-story.ts | 180 ++++++ .../react-storybook-arg-types.generated.ts | 2 +- libs/web-components/test/native-story.spec.ts | 56 ++ libs/web-components/test/story-source.spec.ts | 14 +- libs/web-components/tsconfig.storybook.json | 5 +- libs/web-components/vitest.config.ts | 2 +- package.json | 9 +- 68 files changed, 4835 insertions(+), 1402 deletions(-) create mode 100644 bin/storybook/check-story-parity.mjs create mode 100644 bin/storybook/story-parity.mjs create mode 100644 libs/web-components/scripts/generate-story-fixtures.mjs create mode 100644 libs/web-components/stories/fixtures/Avatar.generated.js create mode 100644 libs/web-components/stories/fixtures/Badge.generated.js create mode 100644 libs/web-components/stories/fixtures/Box.generated.js create mode 100644 libs/web-components/stories/fixtures/Button.generated.js create mode 100644 libs/web-components/stories/fixtures/Counter.generated.js create mode 100644 libs/web-components/stories/fixtures/InputFile.generated.js create mode 100644 libs/web-components/stories/fixtures/Label.generated.js create mode 100644 libs/web-components/stories/fixtures/Link.generated.js create mode 100644 libs/web-components/stories/fixtures/Loader.generated.js create mode 100644 libs/web-components/stories/fixtures/Skeleton.generated.js create mode 100644 libs/web-components/stories/fixtures/Typography.generated.js create mode 100644 libs/web-components/stories/fixtures/Wrapper.generated.js create mode 100644 libs/web-components/stories/loader-examples.ts create mode 100644 libs/web-components/stories/native-story.ts create mode 100644 libs/web-components/test/native-story.spec.ts diff --git a/bin/storybook/check-links.mjs b/bin/storybook/check-links.mjs index ff519dd..7f2fb06 100644 --- a/bin/storybook/check-links.mjs +++ b/bin/storybook/check-links.mjs @@ -67,12 +67,30 @@ if (built) { errors.push('Shared documentation must appear before React in the built sidebar order'); } if (!indexes.webComponents.entries['introduction-overview--overview']) errors.push('Missing Web Components overview'); + +// Docs is the component landing format in both catalogs: it keeps the overview, +// controls, source, and initial example together. Default remains an example +// story only where the source component defines one. +for (const [catalog, index] of Object.entries(indexes)) { + const componentEntries = Object.values(index.entries).filter((entry) => { + const title = entry.title.replace(/^React\//, ''); + return /^(Atoms|Molecules|Organisms|Layout & Structure|Templates)\//.test(title); + }); + const titles = [...new Set(componentEntries.map((entry) => entry.title))]; + for (const title of titles) { + const entries = componentEntries.filter((entry) => entry.title === title); + const docs = entries.find((entry) => entry.type === 'docs'); + if (!docs) errors.push(`${catalog}: ${title} is missing its Docs landing entry`); + if (built && docs && entries[0]?.id !== docs.id) + errors.push(`${catalog}: ${title} must open with Docs before its example stories`); + } +} for (const [group, names] of [ ['atoms', ['avatar', 'button', 'checkbox', 'input', 'select', 'typography']], ['molecules', ['counter', 'menu']], ]) { for (const name of names) { - for (const variant of ['docs', 'default']) { + for (const variant of ['docs']) { const id = `${group}-${name}--${variant}`; if (!indexes.webComponents.entries[id]) errors.push(`Missing native Web Components entry: ${id}`); } diff --git a/bin/storybook/check-story-parity.mjs b/bin/storybook/check-story-parity.mjs new file mode 100644 index 0000000..d67bde7 --- /dev/null +++ b/bin/storybook/check-story-parity.mjs @@ -0,0 +1,20 @@ +import { chromium } from 'playwright'; +import { checkStoryInventory, checkRenderedStoryParity } from './story-parity.mjs'; + +console.log(`Story inventory: ${await checkStoryInventory()} paired stories.`); +if (process.argv.includes('--browser')) { + const browser = await chromium.launch({ headless: true }); + try { + const report = await checkRenderedStoryParity( + browser, + process.env.REACT_STORYBOOK_URL ?? 'http://localhost:6006', + process.env.WEB_STORYBOOK_URL ?? 'http://localhost:6007', + process.env.STORY_FILTER ?? '' + ); + console.log(`Compared ${report.count} rendered stories.`); + report.failures.forEach((failure) => console.error(failure)); + if (report.failures.length) process.exitCode = 1; + } finally { + await browser.close(); + } +} diff --git a/bin/storybook/native-checks.mjs b/bin/storybook/native-checks.mjs index 3510228..d717a70 100644 --- a/bin/storybook/native-checks.mjs +++ b/bin/storybook/native-checks.mjs @@ -12,15 +12,27 @@ const reactArgTypes = translateStorybookMetadata( ); /** Trusted browser input is required for native popover light-dismiss. */ -export async function checkNativeStories(page, base) { - const index = await (await page.request.get(`${base}/web-components/index.json`)).json(); +export async function checkNativeStories(page, base, webBase = `${base}/web-components`) { + const visit = async (url) => { + await page.goto(url); + await page.locator('#storybook-root > *').first().waitFor({ state: 'attached' }); + await page.evaluate(async () => { + await document.fonts.ready; + for (let i = 0; i < 3; i++) await new Promise(requestAnimationFrame); + }); + }; + const index = await (await page.request.get(`${webBase}/index.json`)).json(); for (const [component, sourceArgTypes] of Object.entries(reactArgTypes)) { const title = `${component === 'Counter' || component === 'Menu' ? 'Molecules' : 'Atoms'}/${component}`; const entry = Object.values(index.entries).find( (candidate) => candidate.type === 'story' && candidate.title === title ); assert.ok(entry, `${title} must have a Web Component story`); - await page.goto(`${base}/web-components/iframe.html?id=${entry.id}&viewMode=story`); + assert.ok( + Object.values(index.entries).some((candidate) => candidate.type === 'docs' && candidate.title === title), + `${title} must expose an autodocs entry` + ); + await visit(`${webBase}/iframe.html?id=${entry.id}&viewMode=story`); const targetArgTypes = await page.evaluate(async (storyId) => { const preview = window.__STORYBOOK_PREVIEW__; await preview.storeInitializationPromise; @@ -40,12 +52,64 @@ export async function checkNativeStories(page, base) { assert.deepEqual(target.if, source.if, `${title}.${name} conditional visibility must match React`); } } + await visit(`${webBase}/iframe.html?id=atoms-inputfile--default&viewMode=story`); + await page.locator('gd-input-file').waitFor({ state: 'attached' }); + const webInputFileButton = await page.evaluate(() => { + const inputFile = document.querySelector('gd-input-file'); + const button = inputFile.shadowRoot.querySelector('gd-button').shadowRoot.querySelector('button'); + const rect = button.getBoundingClientRect(); + const style = getComputedStyle(button); + return { + width: rect.width, + height: rect.height, + color: style.color, + background: style.backgroundColor, + border: style.border, + padding: style.padding, + fontSize: style.fontSize, + fontWeight: style.fontWeight, + lineHeight: style.lineHeight, + }; + }); + await visit(`${base}/iframe.html?id=react-atoms-inputfile--default&viewMode=story`); + await page.locator('#storybook-root button').waitFor({ state: 'attached' }); + const reactInputFileButton = await page.evaluate(() => { + const button = document.querySelector('#storybook-root button'); + const rect = button.getBoundingClientRect(); + const style = getComputedStyle(button); + return { + width: rect.width, + height: rect.height, + color: style.color, + background: style.backgroundColor, + border: style.border, + padding: style.padding, + fontSize: style.fontSize, + fontWeight: style.fontWeight, + lineHeight: style.lineHeight, + }; + }); + assert.ok( + Math.abs(webInputFileButton.width - reactInputFileButton.width) < 0.25, + `InputFile button width must match React (Web ${webInputFileButton.width}px, React ${reactInputFileButton.width}px)` + ); + assert.ok( + Math.abs(webInputFileButton.height - reactInputFileButton.height) < 0.25, + `InputFile button height must match React (Web ${webInputFileButton.height}px, React ${reactInputFileButton.height}px)` + ); + for (const property of ['color', 'background', 'border', 'padding', 'fontSize', 'fontWeight', 'lineHeight']) { + assert.equal( + webInputFileButton[property], + reactInputFileButton[property], + `InputFile button ${property} must match React` + ); + } assert.equal( reactArgTypes.Checkbox.children.table.category, 'Content', 'Checkbox children must remain in the Content group' ); - await page.goto(`${base}/web-components/iframe.html?id=atoms-icon--registering-custom-icons&viewMode=story`); + await visit(`${webBase}/iframe.html?id=atoms-icon--registering-custom-icons&viewMode=story`); await page.locator('gd-icon[name="projectOrbit"]').waitFor({ state: 'attached' }); const customIconExample = await page.evaluate(async () => { const icon = document.querySelector('gd-icon[name="projectOrbit"]'); @@ -63,7 +127,7 @@ export async function checkNativeStories(page, base) { assert.equal(customIconExample.hasCircle, true, 'Custom icon example must render its registered SVG'); assert.match(customIconExample.source, /registerCustomIcons/); assert.match(customIconExample.source, / { const rect = (element) => { @@ -82,7 +146,7 @@ export async function checkNativeStories(page, base) { ), }; }); - await page.goto(`${base}/iframe.html?id=react-atoms-icon--all-icons&viewMode=story`); + await visit(`${base}/iframe.html?id=react-atoms-icon--all-icons&viewMode=story`); await page.locator('[data-testid="Column"]').first().waitFor({ state: 'attached' }); const reactIconLibrary = await page.evaluate(() => { const rect = (element) => { @@ -116,7 +180,7 @@ export async function checkNativeStories(page, base) { assertRects(webIconLibrary.labels, reactIconLibrary.labels, 'Icon library label'); const badgeIcons = index.entries['atoms-badge--with-icons']; assert.ok(badgeIcons, 'Atoms/Badge WithIcons must exist'); - await page.goto(`${base}/web-components/iframe.html?id=${badgeIcons.id}&viewMode=story`); + await visit(`${webBase}/iframe.html?id=${badgeIcons.id}&viewMode=story`); await page.locator('gd-badge').first().waitFor({ state: 'attached' }); await page.waitForFunction(() => document.querySelectorAll('gd-badge').length === 5); const badgeIconParity = await page.evaluate(() => ({ @@ -170,7 +234,7 @@ export async function checkNativeStories(page, base) { }; }) ); - await page.goto(`${base}/iframe.html?id=react-atoms-badge--with-icons&viewMode=story`); + await visit(`${base}/iframe.html?id=react-atoms-badge--with-icons&viewMode=story`); await page.locator('[data-testid="Badge"]').first().waitFor({ state: 'attached' }); const reactBadgeGeometry = await page.evaluate(() => [...document.querySelectorAll('[data-testid="Badge"]')].map((badge) => { @@ -224,7 +288,15 @@ export async function checkNativeStories(page, base) { for (const story of stories) { const name = story.title.split('/')[1].toLowerCase(); names.add(name); - await page.goto(`${base}/web-components/iframe.html?id=${story.id}&viewMode=story`); + await visit(`${webBase}/iframe.html?id=${story.id}&viewMode=story`); + if ( + [ + 'atoms-loader--loader-section-variant', + 'atoms-loader--section-loader-button-variant', + 'atoms-loader--inline-loader-button-variant', + ].includes(story.id) + ) + continue; const tag = `gd-${tags[name] ?? name}`; await page.locator(tag).first().waitFor({ state: 'attached' }); await page.waitForFunction((tag) => { @@ -263,7 +335,7 @@ export async function checkNativeStories(page, base) { (entry) => entry.type === 'story' && entry.id.endsWith('--default-tokens') ); for (const story of tokenStories) { - await page.goto(`${base}/web-components/iframe.html?id=${story.id}&viewMode=story`); + await visit(`${webBase}/iframe.html?id=${story.id}&viewMode=story`); const viewer = page.getByRole('region', { name: 'Token viewer' }); await viewer.waitFor(); const text = await viewer.textContent(); @@ -275,24 +347,29 @@ export async function checkNativeStories(page, base) { } assert.equal(tokenStories.length, 24, 'Every React token story port should render Web Component tokens'); const open = async (id) => { - await page.goto(`${base}/web-components/iframe.html?id=${id}&viewMode=story`); + await visit(`${webBase}/iframe.html?id=${id}&viewMode=story`); await page.locator('#storybook-root').waitFor(); + await page.evaluate(() => { + window.__nativeEvents = []; + for (const name of ['gd-change', 'gd-input']) + document.addEventListener(name, (event) => window.__nativeEvents.push(event.detail)); + }); }; const event = async (detail) => - page - .locator('output') - .filter({ hasText: JSON.stringify(detail) }) - .waitFor(); - await open('atoms-button--disabled'); - await page.getByRole('button', { name: 'Button', exact: true }).waitFor(); - assert.ok(await page.getByRole('button', { name: 'Button', exact: true }).isDisabled()); - await open('atoms-input--default'); + page.waitForFunction( + (expected) => window.__nativeEvents.some((value) => JSON.stringify(value) === JSON.stringify(expected)), + detail + ); + await open('atoms-button--disabled-button'); + await page.getByRole('button', { name: 'Disabled Button', exact: true }).waitFor(); + assert.ok(await page.getByRole('button', { name: 'Disabled Button', exact: true }).isDisabled()); + await open('atoms-input--primary-default-with-label-and-helper-text'); await page.getByRole('textbox', { name: 'Label', exact: true }).fill('Updated value'); await event({ value: 'Updated value' }); await open('atoms-input--read-only'); assert.ok( await page - .getByRole('textbox', { name: 'Label', exact: true }) + .getByRole('textbox') .isEditable() .then((value) => !value) ); @@ -302,61 +379,53 @@ export async function checkNativeStories(page, base) { await event({ checked: true }); await page.locator('gd-checkbox label').click(); await event({ checked: false }); + await open('atoms-checkbox--controlled'); + await page.locator('gd-checkbox label').click(); + await page.getByText('Current state: Checked', { exact: true }).waitFor(); + await open('atoms-input--radio-group-with-label'); + await page.getByText('Label 2', { exact: true }).click(); + assert.deepEqual( + await page.locator('input[type="radio"]').evaluateAll((nodes) => nodes.map((node) => node.checked)), + [false, true, false] + ); + await open('atoms-input--with-end-adornment-as-icon'); + await page.locator('[slot="adornment-end"]').click(); + assert.equal(await page.locator('input').getAttribute('type'), 'text'); await open('atoms-select--default'); - await page.getByRole('button', { name: 'Choose an option' }).click(); - await page.getByRole('option', { name: 'Beta' }).click(); - await event({ value: { name: 'Beta', value: 'b' } }); + await page.getByRole('button', { name: 'Select', exact: true }).click(); + await page.getByRole('option', { name: 'Option 2', exact: true }).click(); + await event({ value: { name: 'Option 2', value: { test: 'option2' } } }); await page.getByRole('listbox').waitFor({ state: 'hidden' }); - await open('atoms-select--multiple'); - await page.getByRole('button', { name: 'Alpha, Gamma' }).click(); - await page.getByRole('option', { name: 'Beta' }).click(); - await event({ - value: [ - { name: 'Alpha', value: 'a' }, - { name: 'Gamma', value: 'c' }, - { name: 'Beta', value: 'b' }, - ], - }); + await open('atoms-select--multiple-select'); + await page.getByRole('button', { name: 'Select multiple options' }).click(); + await page.getByRole('option').filter({ hasText: 'Option 2' }).click(); + await event({ value: [{ name: 'Option 2', value: { test: 'option2' } }] }); await page.getByRole('listbox').waitFor(); await open('atoms-select--searchable'); - await page.getByRole('button', { name: 'Choose an option' }).click(); - await page.getByRole('searchbox', { name: 'Search options' }).fill('bet'); - await page.getByRole('option', { name: 'Beta' }).waitFor(); - assert.equal(await page.getByRole('option', { name: 'Alpha' }).count(), 0); - await open('atoms-typography--all-variants'); - assert.equal(await page.locator('gd-typography').count(), 18); + await page.getByRole('button', { name: 'Select a fruit' }).click(); + await page.getByRole('searchbox', { name: 'Search fruits...' }).fill('ban'); + await page.getByRole('option', { name: 'Banana' }).waitFor(); + assert.equal(await page.getByRole('option', { name: 'Apple' }).count(), 0); + await open('atoms-typography--heading'); + await page.locator('gd-typography').first().waitFor(); assert.deepEqual( await page .locator('gd-typography') .evaluateAll((elements) => elements.map((element) => element.shadowRoot?.firstElementChild?.tagName.toLowerCase()) ), - [ - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'p', - 'small', - 'div', - 'span', - 'strong', - 'i', - 'code', - 'kbd', - 'span', - 'header', - 'sup', - 'sub', - ] + ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] + ); + await open('atoms-typography--display'); + await page.waitForFunction(() => + [...document.querySelectorAll('gd-typography')].some((element) => element.variant === 'div') ); - await open('atoms-typography--display-sizes'); const displayFontSizes = await page .locator('gd-typography') .evaluateAll((elements) => - elements.map((element) => getComputedStyle(element.shadowRoot.firstElementChild).fontSize) + elements + .filter((element) => element.variant === 'div') + .map((element) => getComputedStyle(element.shadowRoot.firstElementChild).fontSize) ); assert.equal(new Set(displayFontSizes).size, 5); await open('atoms-slider--default'); @@ -369,6 +438,12 @@ export async function checkNativeStories(page, base) { await open('atoms-switch--default'); await page.locator('gd-switch label').click(); await event({ checked: true }); + await open('atoms-switch--with-loading'); + await page.locator('gd-switch label').click(); + await page.getByText('Loading: Yes (3 seconds)', { exact: true }).waitFor(); + assert.ok(await page.locator('input').isDisabled()); + await page.getByText('Current state: ON', { exact: true }).waitFor(); + await page.getByText('Loading: No', { exact: true }).waitFor(); await open('atoms-textarea--default'); await page.getByRole('textbox').fill('Updated comment'); await event({ value: 'Updated comment' }); @@ -387,7 +462,7 @@ export async function checkNativeStories(page, base) { await page.locator('gd-wrapper').evaluate((element) => element.shadowRoot.firstElementChild.tagName), 'SECTION' ); - await open('molecules-counter--default'); + await open('molecules-counter--adjusted-max-value-5'); await page.getByRole('button', { name: 'Increment counter', exact: true }).click(); await event({ value: 2 }); await page.getByRole('button', { name: 'Decrement counter', exact: true }).click(); @@ -410,23 +485,31 @@ export async function checkNativeStories(page, base) { ); }); await open('molecules-menu--default'); - await page.getByRole('button', { name: 'Actions', exact: true }).click(); - await page.getByRole('button', { name: 'Edit', exact: true }).click(); - await event({ data: { name: 'Edit', value: 'edit' }, value: 'edit' }); + await page.getByRole('button', { name: 'Menu', exact: true }).click(); + await page.getByRole('button', { name: 'Profile', exact: true }).click(); + await event({ data: { name: 'Profile', value: 'profile' }, value: 'profile' }); await menuClosed(); - await page.getByRole('button', { name: 'Actions', exact: true }).click(); - await page.getByRole('button', { name: 'Edit', exact: true }).waitFor(); + await page.getByRole('button', { name: 'Menu', exact: true }).click(); + await page.getByRole('button', { name: 'Profile', exact: true }).waitFor(); await page.keyboard.press('Escape'); await menuClosed(); - await page.getByRole('button', { name: 'Actions', exact: true }).click(); - await page.getByRole('button', { name: 'Edit', exact: true }).waitFor(); - await page.getByRole('button', { name: 'Outside menu' }).click(); + await page.getByRole('button', { name: 'Menu', exact: true }).click(); + await page.getByRole('button', { name: 'Profile', exact: true }).waitFor(); + await page.mouse.click(1270, 890); await menuClosed(); - await open('molecules-menu--keep-open-on-select'); - await page.getByRole('button', { name: 'Actions', exact: true }).click(); - await page.getByRole('button', { name: 'Archive', exact: true }).click(); - await event({ data: { name: 'Archive', value: 'archive' }, value: 'archive' }); - await page.getByRole('button', { name: 'Edit', exact: true }).waitFor(); + await open('molecules-menu--close-on-select-false'); + await page.getByRole('button', { name: 'Menu', exact: true }).click(); + await page.getByRole('button', { name: 'Settings', exact: true }).click(); + await event({ data: { name: 'Settings', value: 'settings' }, value: 'settings' }); + await page.getByRole('button', { name: 'Profile', exact: true }).waitFor(); + await open('molecules-menu--with-edit-and-delete-modals'); + for (const action of ['Edit', 'Delete']) { + await page.getByRole('button', { name: 'Menu', exact: true }).click(); + await page.getByRole('button', { name: action, exact: true }).click(); + await page.getByRole('dialog').waitFor(); + await page.getByRole('dialog').getByRole('button', { name: action, exact: true }).click(); + await page.getByRole('dialog').waitFor({ state: 'hidden' }); + } console.log( `Verified ${stories.length} native component stories and representative interactions for every interactive atom and molecule.` ); diff --git a/bin/storybook/smoke-test.mjs b/bin/storybook/smoke-test.mjs index 6cc6aca..6a9caf2 100644 --- a/bin/storybook/smoke-test.mjs +++ b/bin/storybook/smoke-test.mjs @@ -5,6 +5,7 @@ import { extname, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { chromium } from 'playwright'; import { checkNativeStories } from './native-checks.mjs'; +import { checkRenderedStoryParity } from './story-parity.mjs'; // Serve the exact combined artifact. --url also checks the common dev command. const root = fileURLToPath(new URL('../../libs/ui/storybook-static/', import.meta.url)); @@ -117,7 +118,12 @@ try { await page.waitForURL((url) => url.searchParams.get('id') === 'react-atoms-button--default'); await page.getByRole('button').first().waitFor(); assert.equal(new URL(page.url()).searchParams.get('args'), 'size:sm'); - if (!process.argv.includes('--url')) await checkNativeStories(page, base); + if (!process.argv.includes('--url')) { + await checkNativeStories(page, base); + const parity = await checkRenderedStoryParity(browser, base, `${base}/web-components`); + assert.deepEqual(parity.failures, [], 'Every paired story must preserve its React example'); + console.log(`Compared ${parity.count} rendered React/Web Components stories.`); + } assert.deepEqual(errors, [], 'Storybook should not raise browser runtime errors'); console.log( 'Storybook smoke checks passed: React docs, AI docs, internal links, composition, and legacy manager/iframe routes.' diff --git a/bin/storybook/story-parity.mjs b/bin/storybook/story-parity.mjs new file mode 100644 index 0000000..fc23145 --- /dev/null +++ b/bin/storybook/story-parity.mjs @@ -0,0 +1,219 @@ +import assert from 'node:assert/strict'; +import { readFile, readdir } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; + +const root = fileURLToPath(new URL('../../', import.meta.url)); +function exportsOf(file, text) { + const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true); + return source.statements + .filter( + (node) => ts.isVariableStatement(node) && node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + ) + .flatMap((node) => node.declarationList.declarations.map((d) => d.name.getText(source))); +} + +/** Source ASTs, rather than a hand-maintained list, define the paired inventory. */ +export async function checkStoryInventory() { + const sourceFiles = await readdir(`${root}libs/ui/src/components`, { recursive: true }); + const contracts = JSON.parse(await readFile(`${root}libs/web-components/react-parity.json`, 'utf8')); + const targetFiles = await readdir(`${root}libs/web-components/stories`); + for (const contract of Object.values(contracts)) { + assert.ok(targetFiles.includes(`${contract.react}.stories.ts`), `${contract.react}: missing native story file`); + } + let count = 0; + for (const targetFile of targetFiles) { + if (!targetFile.endsWith('.stories.ts') || targetFile === 'Overview.stories.ts') continue; + const component = targetFile.replace('.stories.ts', ''); + const sourceFile = sourceFiles.find((file) => file.endsWith(`/${component}.stories.tsx`)); + assert.ok(sourceFile, `No React counterpart for ${targetFile}`); + const [source, target] = await Promise.all([ + readFile(`${root}libs/ui/src/components/${sourceFile}`, 'utf8'), + readFile(`${root}libs/web-components/stories/${targetFile}`, 'utf8'), + ]); + const excluded = Object.values(contracts).find((contract) => contract.react === component)?.excludedStories ?? []; + const expected = exportsOf(sourceFile, source) + .filter((name) => !excluded.includes(name)) + .sort(); + assert.deepEqual( + exportsOf(targetFile, target).sort(), + expected, + `${component}: stories must match React one for one (no extra aliases)` + ); + count += expected.length; + } + return count; +} + +// Walk the composed tree: slot content must be compared at its painted position, +// and hidden popovers/fallback slots must not count as visible example content. +export function canvasInventory() { + const texts = []; + const inputs = []; + const walk = (node) => { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent.replace(/\s+/g, ' ').trim(); + if (text && node.parentElement && getComputedStyle(node.parentElement).visibility !== 'hidden') { + const range = document.createRange(); + range.selectNodeContents(node); + if ([...range.getClientRects()].some((r) => r.width && r.height)) texts.push(text); + } + return; + } + if (node instanceof HTMLSlotElement) { + const assigned = node.assignedNodes({ flatten: true }); + (assigned.length ? assigned : [...node.childNodes]).forEach(walk); + return; + } + if (node instanceof Element) { + if ( + ['SCRIPT', 'STYLE'].includes(node.tagName) || + (getComputedStyle(node).display !== 'contents' && !node.checkVisibility({ checkVisibilityCSS: true })) + ) + return; + if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) + inputs.push({ + type: node.type, + value: ['checkbox', 'radio'].includes(node.type) ? undefined : node.value, + checked: node instanceof HTMLInputElement ? node.checked : undefined, + disabled: node.disabled, + }); + if (node.shadowRoot) { + [...node.shadowRoot.childNodes].forEach(walk); + return; + } + } + [...node.childNodes].forEach(walk); + }; + walk(document.querySelector('#storybook-root')); + return { text: texts.join(' ').replace(/\s+/g, ' ').trim(), inputs }; +} + +export async function checkRenderedStoryParity(browser, reactBase, webBase, filter = '') { + const reactIndex = await (await fetch(`${reactBase}/index.json`)).json(); + const webIndex = await (await fetch(`${webBase}/index.json`)).json(); + const pages = await Promise.all([ + browser.newPage({ viewport: { width: 1280, height: 900 } }), + browser.newPage({ viewport: { width: 1280, height: 900 } }), + ]); + for (const page of pages) { + page.setDefaultTimeout(15000); + await page.route('https://picsum.photos/**', (route) => + route.fulfill({ + contentType: 'image/svg+xml', + body: '', + }) + ); + } + const failures = []; + let count = 0; + try { + for (const target of Object.values(webIndex.entries).filter( + (entry) => + entry.type === 'story' && + /^(Atoms|Molecules)\//.test(entry.title) && + !entry.id.endsWith('--default-tokens') && + (!filter || entryMatches(entry, filter)) + )) { + const source = Object.values(reactIndex.entries).find( + (entry) => + entry.type === 'story' && + entry.title.replace(/^React\//, '') === target.title && + entry.id.split('--')[1] === target.id.split('--')[1] + ); + if (!source) { + failures.push(`${target.id}: no React counterpart`); + continue; + } + try { + const results = await Promise.allSettled( + pages.map(async (page, i) => { + const entry = i ? target : source; + await page.goto(`${i ? webBase : reactBase}/iframe.html?id=${entry.id}&viewMode=story&embed=true`, { + waitUntil: 'domcontentloaded', + }); + await page.waitForFunction( + () => + document.querySelector('#storybook-root')?.childNodes.length || + document.querySelector('body.sb-show-errordisplay') + ); + await page.evaluate(async () => { + await document.fonts.ready; + for (let i = 0; i < 4; i++) await new Promise(requestAnimationFrame); + }); + const error = (await page.locator('.sb-errordisplay').isVisible()) + ? await page.locator('.sb-errordisplay').textContent() + : ''; + if (error) throw new Error(error); + return page.evaluate(canvasInventory); + }) + ); + const failed = results.find((result) => result.status === 'rejected'); + if (failed) throw failed.reason; + const inventories = results.map((result) => result.value); + assert.deepEqual(inventories[1], inventories[0]); + assert.equal(target.name, source.name, 'Story display name differs'); + // Text equality cannot catch a checkbox rendered with text-field chrome. + // Measure the painted controls, not the deliberately hidden Checkbox input. + if ( + /^atoms-input--(checkbox-with-label|radio.*with-label|primary-default-with-label-and-helper-text)$/.test( + target.id + ) || + /^atoms-checkbox--(default|disabled|sizes|indeterminate)$/.test(target.id) + ) { + const isCheckbox = target.id.startsWith('atoms-checkbox--'); + const geometry = await Promise.all( + pages.map((page, index) => + page + .locator( + isCheckbox + ? index + ? 'gd-checkbox [part="indicator"]' + : '[data-testid="Checkbox-indicator"]' + : 'input' + ) + .evaluateAll((nodes) => + nodes.map((node) => { + const rect = node.getBoundingClientRect(); + const style = getComputedStyle(node); + return { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + appearance: style.appearance, + background: style.backgroundColor, + image: style.backgroundImage, + border: style.border, + }; + }) + ) + ) + ); + assert.equal(geometry[1].length, geometry[0].length, 'Painted control count'); + geometry[0].forEach((expected, i) => { + const actual = geometry[1][i]; + for (const field of ['x', 'y', 'width', 'height']) + assert.ok( + Math.abs(actual[field] - expected[field]) <= 0.6, + `Control ${i} ${field}: ${actual[field]} differs from React ${expected[field]}` + ); + for (const field of ['appearance', 'background', 'image', 'border']) + assert.equal(actual[field], expected[field], `Control ${i} ${field}`); + }); + } + } catch (error) { + failures.push(`${target.id}: ${error.message}`); + console.log(`Difference: ${target.id}`); + } + count++; + if (count % 25 === 0) console.log(`Compared ${count} stories (${failures.length} differences so far).`); + } + } finally { + await Promise.all(pages.map((page) => page.close())); + } + return { count, failures }; +} +function entryMatches(entry, filter) { + return entry.title.toLowerCase().includes(filter.toLowerCase()); +} diff --git a/libs/ui/.storybook/preview.ts b/libs/ui/.storybook/preview.ts index 7f396ac..adabe8b 100644 --- a/libs/ui/.storybook/preview.ts +++ b/libs/ui/.storybook/preview.ts @@ -1,7 +1,11 @@ import type { Preview } from '@storybook/react-vite'; const preview: Preview = { + tags: ['autodocs'], parameters: { + docs: { + defaultName: 'Docs', + }, controls: { matchers: { color: /(background|color)$/i, diff --git a/libs/ui/.storybook/public/defaultTheme.json b/libs/ui/.storybook/public/defaultTheme.json index eefeb84..56c08f4 100644 --- a/libs/ui/.storybook/public/defaultTheme.json +++ b/libs/ui/.storybook/public/defaultTheme.json @@ -1397,6 +1397,7 @@ }, "input": { "default": { + "margin": 0, "outline": "none", "position": "relative", "cursor": "text", diff --git a/libs/ui/src/tokens/input.ts b/libs/ui/src/tokens/input.ts index 0a1e459..ba3faaa 100644 --- a/libs/ui/src/tokens/input.ts +++ b/libs/ui/src/tokens/input.ts @@ -38,6 +38,8 @@ export const input = { }, input: { default: { + // Shadow-root inputs need the same reset as light-DOM inputs. + margin: 0, padding: (theme: Record) => get(theme, 'spacing.sm', 'theme.spacing.sm'), outline: 'none', position: 'relative', diff --git a/libs/web-components/.storybook/preview.ts b/libs/web-components/.storybook/preview.ts index 719c810..5b0576f 100644 --- a/libs/web-components/.storybook/preview.ts +++ b/libs/web-components/.storybook/preview.ts @@ -7,11 +7,13 @@ import '../src/index'; import { storySource } from '../stories/helpers'; const preview: Preview = { + tags: ['autodocs'], parameters: { layout: 'padded', options: { storySort: { order: ['Introduction', 'Atoms', 'Molecules'] } }, a11y: { test: 'todo' }, docs: { + defaultName: 'Docs', source: { language: 'html', transform: ( diff --git a/libs/web-components/eslint.config.mjs b/libs/web-components/eslint.config.mjs index 406ab3c..a721622 100644 --- a/libs/web-components/eslint.config.mjs +++ b/libs/web-components/eslint.config.mjs @@ -2,6 +2,7 @@ import baseConfig from '../../eslint.config.mjs'; export default [ ...baseConfig, + { ignores: ['stories/fixtures/*.generated.js'] }, { files: ['**/*.json'], rules: { diff --git a/libs/web-components/react-parity.json b/libs/web-components/react-parity.json index 0ca7a12..2ea4a6b 100644 --- a/libs/web-components/react-parity.json +++ b/libs/web-components/react-parity.json @@ -128,6 +128,7 @@ }, "gd-skeleton": { "react": "Skeleton", + "excludedStories": ["WithCustomAnimation"], "source": [ "variant", "width", @@ -144,7 +145,6 @@ "Circular", "Rectangular", "WithThemeColor", - "WithCustomAnimation", "WithChildren", "ComposedLayout", "WithAccessibility", @@ -180,18 +180,13 @@ }, "gd-label": { "react": "Label", + "excludedStories": ["DefaultWithTailwind"], "source": ["htmlFor", "styles", "slot", "part=\"label\""], - "stories": [ - "LabelDefault", - "LabelWithChildIcon", - "CustomStyles", - "DefaultWithTailwind", - "WithAccessibility", - "DefaultTokens" - ] + "stories": ["LabelDefault", "LabelWithChildIcon", "CustomStyles", "WithAccessibility", "DefaultTokens"] }, "gd-link": { "react": "Link", + "excludedStories": ["InheritWithTailwind"], "source": [ "variant", "size", @@ -212,7 +207,6 @@ "InheritWithButtonAsChild", "Inverted", "InheritWithTypographyAsChild", - "InheritWithTailwind", "CustomStyles", "WithUnderline", "WithSizes", @@ -222,6 +216,7 @@ }, "gd-loader": { "react": "Loader", + "excludedStories": ["CustomLoaderTailwindClassBounceAnimation"], "source": ["name", "variant", "size", "rounded", "animationProps", "withWrapper", "styles", "popover", "slot"], "stories": [ "Default", @@ -229,7 +224,6 @@ "LoaderSizes", "LoaderRounded", "LoaderVariantsWithWrapperViewAsHeaderTag", - "CustomLoaderTailwindClassBounceAnimation", "LoaderSectionVariant", "SectionLoaderButtonVariant", "InlineLoaderButtonVariant", @@ -368,6 +362,7 @@ }, "gd-input": { "react": "Input", + "excludedStories": ["DefaultWithTailwind"], "source": [ "variant", "color", @@ -406,7 +401,6 @@ "WithAdornments", "WithStartAdornment", "WithEndAdornmentAsIcon", - "DefaultWithTailwind", "CustomStyles", "CheckboxWithLabel", "RadioControlledWithLabel", diff --git a/libs/web-components/scripts/check-ported-elements.mjs b/libs/web-components/scripts/check-ported-elements.mjs index 5b3209c..890cc1a 100644 --- a/libs/web-components/scripts/check-ported-elements.mjs +++ b/libs/web-components/scripts/check-ported-elements.mjs @@ -200,7 +200,7 @@ for (const { tag, category } of ports) { const stories = await readFile(resolve(packageRoot, 'stories', `${storyName}.stories.ts`), 'utf8'); if (/textContent\s*=\s*['"][●★→]['"]/.test(stories)) throw new Error(`${tag} Storybook uses a glyph placeholder where a source icon must compose gd-icon.`); - if (fullStoryParity.has(tag) && !stories.includes('description:')) + if (fullStoryParity.has(tag) && !stories.includes('description:') && !stories.includes('nativeMeta(')) throw new Error(`${tag} Storybook docs are missing component/prop descriptions.`); for (const needle of contract.source) { if (!source.includes(needle)) throw new Error(`${tag} is missing React ${contract.react} API mapping: ${needle}`); @@ -233,7 +233,10 @@ for (const { tag, category } of ports) { ); } } - const exported = [...reactStories.matchAll(/^export const (\w+)/gm)].map((match) => match[1]); + const excludedStories = contract.excludedStories ?? []; + const exported = [...reactStories.matchAll(/^export const (\w+)/gm)] + .map((match) => match[1]) + .filter((story) => !excludedStories.includes(story)); const missing = exported.filter((story) => !contract.stories.includes(story)); const stale = contract.stories.filter((story) => !exported.includes(story)); if (missing.length || stale.length) { @@ -243,7 +246,7 @@ for (const { tag, category } of ports) { }.` ); } - if (category === 'atoms' && publishedAudit.components[tag] !== contract.stories.length) { + if (category === 'atoms' && publishedAudit.components[tag] !== contract.stories.length + excludedStories.length) { throw new Error( `${tag} published Storybook count mismatch: expected ${publishedAudit.components[tag]}, recorded ${contract.stories.length}.` ); diff --git a/libs/web-components/scripts/generate-story-fixtures.mjs b/libs/web-components/scripts/generate-story-fixtures.mjs new file mode 100644 index 0000000..e3f541c --- /dev/null +++ b/libs/web-components/scripts/generate-story-fixtures.mjs @@ -0,0 +1,216 @@ +import ts from 'typescript'; +import prettier from 'prettier'; +import { readFile, writeFile, readdir } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const formatOptions = (await prettier.resolveConfig(new URL('../../../.prettierrc', import.meta.url).pathname)) ?? {}; +const root = fileURLToPath(new URL('../../../', import.meta.url)); +const parity = JSON.parse(await readFile(resolve(root, 'libs/web-components/react-parity.json'), 'utf8')); +export const fixtureComponents = [ + 'Avatar', + 'Badge', + 'Box', + 'Button', + 'Counter', + 'InputFile', + 'Label', + 'Link', + 'Loader', + 'Skeleton', + 'Typography', + 'Wrapper', +]; +const nativeDescriptions = { + Avatar: 'Avatar with image, fallback initials or icon, badge, color, size, and accessible labeling.', + Badge: 'Status badge with variants, appearances, sizes, disabled state, icon positions, and style overrides.', + Box: 'Layout container with orientation, border, hover highlight, shadow, accessibility, and custom styling.', + Button: + 'Button with variants, icons, loading state, width, rounding, disabled behavior, custom styling, and accessibility.', + Counter: + 'Counter with minimum, maximum, initial value, external change handling, disabled state, and accessible native controls.', + InputFile: + 'File picker with accepted file types, capture, multiple selection, disabled and icon-label states, button customization, native events, and accessible controls.', + Label: + 'Native label with slotted content, child icons, custom styling, control association, and accessibility semantics.', + Link: 'Native anchor with variants, sizes, underline modes, colors, disabled behavior, targets, custom styling, accessibility, and child composition.', + Loader: + 'Loading indicator with circle and dots animations, five sizes, rounding, inline, section and full-page layouts, custom timing, content, styling, and status accessibility.', + Skeleton: + 'Loading placeholder with rounded, rectangular and circular variants, theme colors, child content, composed layouts, and accessibility semantics.', + Typography: + 'Typography with semantic overrides, combined style variants, disclaimer, display, heading, body, color, alignment, and accessibility options.', + Wrapper: + 'Layout wrapper with inline, section and full-page variants, semantic element selection, slotted content, and custom styling.', +}; +const componentFiles = await readdir(resolve(root, 'libs/ui/src/components'), { recursive: true }); +const enums = new Map(); +for (const file of await readdir(resolve(root, 'libs/ui/src/types'))) { + if (!file.endsWith('.ts')) continue; + const source = ts.createSourceFile(file, await readFile(resolve(root, 'libs/ui/src/types', file), 'utf8'), 99, true); + for (const node of source.statements) { + if (!ts.isEnumDeclaration(node)) continue; + for (const member of node.members) { + if (member.initializer && ts.isStringLiteral(member.initializer)) + enums.set(`${node.name.text}.${member.name.getText(source)}`, member.initializer.text); + } + } +} +let stale = false; +for (const component of fixtureComponents) { + const relative = componentFiles.find((file) => file.endsWith(`/${component}.stories.tsx`)); + const sourcePath = `libs/ui/src/components/${relative}`; + const source = ts.createSourceFile( + sourcePath, + await readFile(resolve(root, sourcePath), 'utf8'), + 99, + true, + ts.ScriptKind.TSX + ); + const contract = Object.values(parity).find((entry) => entry.react === component); + const excludedStories = new Set(contract?.excludedStories ?? []); + const names = source.statements + .filter((n) => ts.isVariableStatement(n) && n.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) + .flatMap((n) => n.declarationList.declarations.map((d) => d.name.getText(source))) + .filter((name) => !excludedStories.has(name)); + const displayNames = new Map(); + for (const node of source.statements) { + if (!ts.isVariableStatement(node)) continue; + for (const declaration of node.declarationList.declarations) { + let value = declaration.initializer; + while (value && (ts.isAsExpression(value) || ts.isSatisfiesExpression(value))) value = value.expression; + if (!value || !ts.isObjectLiteralExpression(value)) continue; + const name = value.properties.find((p) => ts.isPropertyAssignment(p) && p.name.getText(source) === 'name'); + if (name && ts.isStringLiteral(name.initializer)) + displayNames.set(declaration.name.getText(source), name.initializer.text); + } + } + const tags = new Set([component]); + const transformed = ts.transform(source, [ + (context) => (rootNode) => + ts.visitNode(rootNode, function visit(node) { + if (ts.isImportDeclaration(node)) return undefined; + if (ts.isVariableStatement(node)) { + const declarations = node.declarationList.declarations.filter( + (declaration) => !excludedStories.has(declaration.name.getText(source)) + ); + if (!declarations.length) return undefined; + if (declarations.length !== node.declarationList.declarations.length) { + return ts.factory.updateVariableStatement( + node, + node.modifiers, + ts.factory.updateVariableDeclarationList(node.declarationList, declarations) + ); + } + } + if ( + ts.isExpressionStatement(node) && + [...excludedStories].some((name) => node.expression.getText(source).startsWith(`${name}.`)) + ) + return undefined; + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { + const text = node.text.replace(/tailwind(?: css)?/gi, 'utility CSS'); + if (text !== node.text) return ts.factory.createStringLiteral(text); + } + if ( + component === 'Loader' && + ts.isVariableDeclaration(node) && + ['LoaderSectionVariant', 'SectionLoaderButtonVariant', 'InlineLoaderButtonVariant'].includes( + node.name.getText(source) + ) + ) + return ts.factory.updateVariableDeclaration( + node, + node.name, + undefined, + undefined, + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('loaderStories'), + node.name.getText(source) + ) + ); + if (ts.isPropertyAssignment(node) && ['argTypes', 'play', 'code'].includes(node.name.getText(source))) + return undefined; + if ( + ts.isExpressionStatement(node) && + ts.isBinaryExpression(node.expression) && + /\.play$/.test(node.expression.left.getText(source)) + ) + return undefined; + if (ts.isPropertyAccessExpression(node) && enums.has(node.getText(source))) + return ts.factory.createStringLiteral(enums.get(node.getText(source))); + if ( + ts.isPropertyAccessExpression(node) && + [...enums.keys()].some((key) => key.startsWith(node.expression.getText(source) + '.')) + ) + return ts.factory.createIdentifier('undefined'); + if ( + (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && + /^[A-Z]/.test(node.tagName.getText(source)) + ) + tags.add(node.tagName.getText(source)); + // Polymorphic Row uses a native flex div in the Web Component API. + if ( + ts.isPropertyAssignment(node) && + node.name.getText(source) === 'as' && + node.initializer.getText(source) === 'Row' + ) + return ts.factory.updatePropertyAssignment(node, node.name, ts.factory.createStringLiteral('div')); + return ts.visitEachChild(node, visit, context); + }), + ]).transformed[0]; + const code = ts.transpileModule(ts.createPrinter().printFile(transformed), { + compilerOptions: { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext, + jsx: ts.JsxEmit.React, + jsxFactory: 'demo', + jsxFragmentFactory: 'Fragment', + removeComments: true, + }, + }).outputText; + if (/\b(useState|useRef|useEffect|useCallback)\(/.test(code)) + throw new Error(`${component}: interactive stories require an explicit native port`); + const tagDeclarations = [...tags] + .map( + (tag) => + `const ${tag} = '${ + tag === 'TokenViewer' || tag === 'Row' || tag === 'Column' + ? tag + : `gd-${tag.replace(/[A-Z]/g, (c, i) => `${i ? '-' : ''}${c.toLowerCase()}`)}` + }';` + ) + .join('\n'); + const result = `// Generated from ${sourcePath}. Run npm run generate:web-component-stories.\n// Native DOM recipes only: no React runtime or React component implementation is imported.\nimport { demo, Fragment } from '../native-story';\nimport { defaultTheme } from 'gd-design-library/tokens';\nimport { fn } from 'storybook/test';\nimport { action } from 'storybook/actions';\n${ + component === 'Loader' ? "import { loaderStories } from '../loader-examples';\n" : '' + }const COMPONENT_NAME = '${component}';\n${tagDeclarations}\n${code}`; + // CSF indexes static object-literal names. Hiding name inside nativeStory() + // preserves runtime metadata but silently changes the sidebar's published name. + const wrapper = `// Generated from ${sourcePath}; edit the source example or native-story adapter.\nimport * as fixtures from './fixtures/${component}.generated.js';\nimport { nativeMeta, nativeStory } from './native-story';\nimport { sectionedArgTypes } from './helpers';\n\nconst sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('${component}', {}));\nconst meta = {\n title: '${ + component === 'Counter' ? 'Molecules' : 'Atoms' + }/${component}',\n tags: ['autodocs'],\n args: sourceMeta.args,\n parameters: {\n layout: 'centered',\n docs: {\n description: { component: ${JSON.stringify( + nativeDescriptions[component] + )} },\n },\n },\n argTypes: sourceMeta.argTypes,\n render: sourceMeta.render,\n};\nexport default meta;\n${names + .map( + (name) => + `export const ${name} = { ...nativeStory(fixtures.${name}, meta)${ + displayNames.has(name) ? `, name: ${JSON.stringify(displayNames.get(name))}` : '' + } };` + ) + .join('\n')}\n`; + for (const [relativePath, rawContents] of [ + [`stories/fixtures/${component}.generated.js`, result], + [`stories/${component}.stories.ts`, wrapper], + ]) { + const file = resolve(root, 'libs/web-components', relativePath); + const contents = await prettier.format(rawContents, { ...formatOptions, filepath: file }); + if (process.argv.includes('--check')) { + if ((await readFile(file, 'utf8').catch(() => '')) !== contents) { + console.error(`Stale story fixture: ${relativePath}`); + stale = true; + } + } else await writeFile(file, contents); + } +} +if (stale) process.exitCode = 1; +else console.log(`Verified native fixtures for ${fixtureComponents.length} React story files.`); diff --git a/libs/web-components/scripts/translate-storybook-metadata.mjs b/libs/web-components/scripts/translate-storybook-metadata.mjs index 2565dde..71520ed 100644 --- a/libs/web-components/scripts/translate-storybook-metadata.mjs +++ b/libs/web-components/scripts/translate-storybook-metadata.mjs @@ -37,6 +37,7 @@ function translateString(value) { .replace(/React elements?/gi, 'DOM elements') .replace(/React content/gi, 'slotted content') .replace(/React nodes?/gi, 'DOM nodes') + .replace(/Tailwind CSS|Tailwind/g, 'utility CSS') .replaceAll('keyof HTMLElementTagNameMap | keyof HTMLElementTagNameMap', 'keyof HTMLElementTagNameMap'); } diff --git a/libs/web-components/src/components/atoms/gd-button/gd-button.ts b/libs/web-components/src/components/atoms/gd-button/gd-button.ts index 5a8d22f..83e8ddb 100644 --- a/libs/web-components/src/components/atoms/gd-button/gd-button.ts +++ b/libs/web-components/src/components/atoms/gd-button/gd-button.ts @@ -314,6 +314,7 @@ export class GdButton extends LitElement { ...resolvedTree.default, fontFamily: get(this.theme, 'font.family', '"Fira Sans", sans-serif'), fontSize: get(this.theme, 'font.size.p', '16px'), + lineHeight: get(this.theme, 'font.line.height.p', '24px'), }, }; const radius = resolveButtonRadius(this.theme, this.rounded); diff --git a/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.ts b/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.ts index 64888df..d790b53 100644 --- a/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.ts +++ b/libs/web-components/src/components/atoms/gd-checkbox/gd-checkbox.ts @@ -63,7 +63,7 @@ function resolveCheckboxTokens(theme: DesignCoreTheme, size: CheckboxSizeName): export class GdCheckbox extends LitElement { static styles = css` :host { - display: inline-block; + display: inline-flex; } label { display: inline-flex; diff --git a/libs/web-components/src/components/atoms/gd-input/gd-input.spec.ts b/libs/web-components/src/components/atoms/gd-input/gd-input.spec.ts index a1ffa25..302c94b 100644 --- a/libs/web-components/src/components/atoms/gd-input/gd-input.spec.ts +++ b/libs/web-components/src/components/atoms/gd-input/gd-input.spec.ts @@ -27,6 +27,63 @@ afterEach(() => { }); describe('gd-input', () => { + it('keeps same-name radios mutually exclusive within their form', async () => { + mount( + '
' + ); + const [first, second, otherForm] = Array.from(host.querySelectorAll('gd-input')); + await Promise.all([first, second, otherForm].map(settle)); + const input = (element: GdInput) => element.shadowRoot!.querySelector('input')!; + input(first).click(); + input(otherForm).click(); + await Promise.all([first, second, otherForm].map(settle)); + expect(input(first).checked).toBe(true); + input(second).click(); + await Promise.all([first, second, otherForm].map(settle)); + expect(input(first).checked).toBe(false); + expect(input(second).checked).toBe(true); + expect(input(otherForm).checked).toBe(true); + }); + + it.each([ + ['checkbox', 18], + ['radio', 20], + ] as const)('uses the %s token artwork and dimensions instead of text-field chrome', async (variant, size) => { + mount(``); + const element = host.querySelector('gd-input')!; + await settle(element); + const control = element.shadowRoot!.querySelector('input')!; + const before = getComputedStyle(control); + expect(before.appearance).toBe('none'); + expect(before.width).toBe(`${size}px`); + expect(before.height).toBe(`${size}px`); + const artwork = before.backgroundImage; + expect(artwork).toContain('data:image/svg+xml'); + expect(element.shadowRoot!.querySelector('[part="border"]')).toBeNull(); + expect(element.shadowRoot!.querySelector('[part="outline"]')).toBeNull(); + control.click(); + await settle(element); + expect(control.checked).toBe(true); + expect(getComputedStyle(control).backgroundImage).not.toBe(artwork); + element.disabled = true; + await settle(element); + expect(control.disabled).toBe(true); + }); + + it('renders defaultValue on first mount and restores it on form reset', async () => { + host.innerHTML = '
'; + const element = host.querySelector('gd-input')!; + element.theme = defaultTheme; + await settle(element); + const control = element.shadowRoot!.querySelector('input')!; + expect(control.value).toBe('Initial example'); + control.value = 'Edited'; + control.dispatchEvent(new InputEvent('input', { bubbles: true })); + host.querySelector('form')!.reset(); + await settle(element); + expect(control.value).toBe('Initial example'); + }); + it('renders label and helper text with the same resolved font family as the input', async () => { mount(''); const el = host.querySelector('#i')!; diff --git a/libs/web-components/src/components/atoms/gd-input/gd-input.ts b/libs/web-components/src/components/atoms/gd-input/gd-input.ts index 618ae96..2c94dd0 100644 --- a/libs/web-components/src/components/atoms/gd-input/gd-input.ts +++ b/libs/web-components/src/components/atoms/gd-input/gd-input.ts @@ -1,5 +1,6 @@ import { LitElement, html, css, nothing, type PropertyValues } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; +import { html as staticHtml, unsafeStatic } from 'lit/static-html.js'; import { styleMap } from 'lit/directives/style-map.js'; import { input } from 'gd-design-library/tokens'; import { @@ -115,12 +116,13 @@ function resolveInputTokens(theme: DesignCoreTheme, color: InputColorVariantName export class GdInput extends LitElement { static styles = css` :host { - display: inline-block; + display: inline-flex; } .outer { display: inline-flex; flex-direction: column; align-items: flex-start; + justify-content: center; } .row { position: relative; @@ -182,6 +184,7 @@ export class GdInput extends LitElement { @property({ type: Boolean }) checked = false; @property({ type: Boolean, attribute: 'default-checked' }) defaultChecked = false; @property({ type: String }) width?: string; + @property({ type: String, attribute: 'wrapper-as' }) wrapperAs: 'div' | 'span' | 'label' = 'div'; /** Component-token layout overrides for composed ports; never consumer literal CSS. */ @property({ attribute: false }) styles: Record = {}; @property({ attribute: false }) inputStyles: Record = {}; @@ -303,11 +306,14 @@ export class GdInput extends LitElement { firstUpdated() { this._input.value = this.value; - if (this.defaultChecked && !this.checked) this.checked = true; this._syncFormState(); } willUpdate(changed: PropertyValues) { + if (!this.hasUpdated) { + if (this.defaultValue !== undefined && !this.value) this.value = this.defaultValue; + if (this.defaultChecked && !this.checked) this.checked = true; + } if (changed.has('debounceCallbackTime')) { this._store.getState().setDebounceCallbackTime(this.debounceCallbackTime); this._debouncedDispatch = undefined; @@ -316,7 +322,9 @@ export class GdInput extends LitElement { } updated(changed: PropertyValues) { - const cssText = buttonCssBlockToText('input', this.inputStyleRules); + const tokens = resolveThemeTree(input, this.theme) as unknown as { input: { default: ButtonCssBlock } }; + const cssText = + buttonCssBlockToText('input', tokens.input.default) + buttonCssBlockToText('input', this.inputStyleRules); if (this.shadowRoot && cssText !== this._ruleText) { this._ruleText = cssText; const sheet = new CSSStyleSheet(); @@ -357,6 +365,23 @@ export class GdInput extends LitElement { const control = event.target as HTMLInputElement; const newValue = control.value; this.checked = control.checked; + // Native radios in separate shadow roots do not share a group. Keep the + // public name/form contract consistent across custom-element boundaries. + if (control.type === 'radio' && control.checked && this.name) { + const peers = this.form + ? Array.from(this.form.elements) + : Array.from((this.getRootNode() as Document | ShadowRoot).querySelectorAll('gd-input')); + for (const peer of peers) { + if ( + peer instanceof GdInput && + peer !== this && + peer.name === this.name && + peer.form === this.form && + (peer.variant ?? peer.type) === 'radio' + ) + peer.checked = false; + } + } this._lastInternalValue = newValue; this.value = newValue; this._getDebouncedDispatch()(newValue); @@ -388,17 +413,22 @@ export class GdInput extends LitElement { render() { const inputType = this.variant ?? this.type; + const hasBorder = !['checkbox', 'radio', 'range'].includes(inputType); const resolved = resolveInputTokens(this.theme, this.color); const focusColor = (this.theme.colors as { border?: { focus?: string } } | undefined)?.border?.focus ?? '#0069B4'; const textColor = this.disabled ? resolved.disabledColor : resolved.color; - const outerStyle = { gap: `${resolved.wrapperGap}`, ...this.styles }; + const outerStyle = { + gap: this.label || this.helperText ? `${resolved.wrapperGap}` : '0', + width: this.width ? '100%' : undefined, + ...this.styles, + }; const rowStyle = { fontFamily: `${resolved.fontFamily}`, fontSize: `${resolved.fontSize}`, color: textColor, zIndex: `${resolved.zIndex}`, - padding: `0 ${resolved.horizontalPadding}`, + padding: '0', '--gd-input-placeholder-color': resolved.disabledColor, }; const borderStyle = { @@ -424,20 +454,24 @@ export class GdInput extends LitElement { lineHeight: `${resolved.helperLineHeight}`, }; + const wrapperTag = unsafeStatic(['label', 'span'].includes(this.wrapperAs) ? this.wrapperAs : 'div'); + // `part` attributes (CTORNDSD-646b) expose these internals to consumer CSS via // `gd-input::part(input)` etc. This is the only sanctioned way to style inside a shadow root // from outside it; without it, a consumer's only escape hatch is the `theme` property. - return html` -
- ${this.label - ? html`` - : nothing} + return staticHtml` + <${wrapperTag} class="outer" part="outer" style=${styleMap(outerStyle)}> + ${ + this.label + ? html`` + : nothing + }
- - + ${ + hasBorder + ? html` + ` + : nothing + }
- ${this.helperText - ? html`${this.helperText}` - : nothing} -
+ ${ + this.helperText + ? html`${this.helperText}` + : nothing + } + `; } } diff --git a/libs/web-components/src/components/atoms/gd-label/gd-label.ts b/libs/web-components/src/components/atoms/gd-label/gd-label.ts index 18441d0..181e3dc 100644 --- a/libs/web-components/src/components/atoms/gd-label/gd-label.ts +++ b/libs/web-components/src/components/atoms/gd-label/gd-label.ts @@ -2,7 +2,7 @@ import { LitElement, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import { label } from 'gd-design-library/tokens'; -import { resolveThemeTree, type DesignCoreTheme } from 'gd-design-core'; +import { get, resolveThemeTree, type DesignCoreTheme } from 'gd-design-core'; @customElement('gd-label') export class GdLabel extends LitElement { @@ -11,7 +11,10 @@ export class GdLabel extends LitElement { @property({ attribute: false }) theme: DesignCoreTheme = {}; render() { const tokens = resolveThemeTree(label, this.theme) as { default: Record }; - return html``; } diff --git a/libs/web-components/src/components/molecules/gd-menu/gd-menu.spec.ts b/libs/web-components/src/components/molecules/gd-menu/gd-menu.spec.ts index 2ff482a..3668f96 100644 --- a/libs/web-components/src/components/molecules/gd-menu/gd-menu.spec.ts +++ b/libs/web-components/src/components/molecules/gd-menu/gd-menu.spec.ts @@ -22,6 +22,21 @@ function mount() { } describe('gd-menu', () => { + it('closes on an outside pointer target after reopening', async () => { + const menu = mount(); + const outside = document.createElement('button'); + outside.textContent = 'Outside'; + host.append(outside); + await settle(menu); + for (let attempt = 0; attempt < 2; attempt++) { + menu.openMenu(); + await settle(menu); + await userEvent.click(outside); + await settle(menu); + expect(menu.open).toBe(false); + expect(menu.shadowRoot!.querySelector('[popover]')!.matches(':popover-open')).toBe(false); + } + }); it('exposes the trigger and content slots through stable CSS parts', async () => { const menu = mount(); await settle(menu); diff --git a/libs/web-components/src/components/molecules/gd-menu/gd-menu.ts b/libs/web-components/src/components/molecules/gd-menu/gd-menu.ts index e294953..8b4d950 100644 --- a/libs/web-components/src/components/molecules/gd-menu/gd-menu.ts +++ b/libs/web-components/src/components/molecules/gd-menu/gd-menu.ts @@ -83,12 +83,14 @@ export class GdMenu extends LitElement { // that native behavior; retain `click` below for keyboard activation. this.addEventListener('pointerdown', this._onSlottedItemClick, true); this.addEventListener('click', this._onSlottedItemClick); + this.ownerDocument.addEventListener('pointerdown', this._onOutsidePointerDown, true); } disconnectedCallback() { this.removeEventListener('gd-menu-select', this._onSelect); this.removeEventListener('pointerdown', this._onSlottedItemClick, true); this.removeEventListener('click', this._onSlottedItemClick); + this.ownerDocument.removeEventListener('pointerdown', this._onOutsidePointerDown, true); super.disconnectedCallback(); } @@ -170,6 +172,12 @@ export class GdMenu extends LitElement { this._select(detail?.data ?? detail); }; + // Some Chromium hosts do not light-dismiss a shadow-root popover on blank page + // space. Keep the public state consistent for all outside pointer targets. + private _onOutsidePointerDown = (event: PointerEvent) => { + if (this.open && !event.composedPath().includes(this)) this.closeMenu(); + }; + private _onSlottedItemClick = (event: Event) => { if (!this.open) return; const item = event diff --git a/libs/web-components/stories/Avatar.stories.ts b/libs/web-components/stories/Avatar.stories.ts index ed19ec8..710c5e5 100644 --- a/libs/web-components/stories/Avatar.stories.ts +++ b/libs/web-components/stories/Avatar.stories.ts @@ -1,77 +1,32 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdAvatar } from '../src'; -import { defaultTokenViewer, element, portrait, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/Avatar/Avatar.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Avatar.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick & { - fallbackComponent: string; -}; +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Avatar', {})); const meta = { title: 'Atoms/Avatar', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { - component: - 'Avatar matching the React image, fallback initials or icon, badge, color, size, and accessibility states.', + component: 'Avatar with image, fallback initials or icon, badge, color, size, and accessible labeling.', }, }, }, - args: { - src: '', - alt: 'Ada Lovelace', - fallbackComponent: 'AL', - sizeVariant: 'md', - withBadge: false, - badgeColor: 'bg.fill.success.primary.default', - backgroundColor: undefined, - }, - argTypes: sectionedArgTypes('Avatar', { - sizeVariant: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] }, - badgeColor: { control: 'color' }, - backgroundColor: { control: 'color' }, - }), - render: ({ fallbackComponent, ...args }: Args) => element('gd-avatar', { ...args, fallback: fallbackComponent }), -} satisfies Meta; -export default meta; -type Story = StoryObj; -export const Default: Story = {}; -export const Image: Story = { args: { src: portrait } }; -export const Badge: Story = { args: { withBadge: true, sizeVariant: 'xl' } }; -export const Small: Story = { args: { sizeVariant: 'sm' } }; -export const Large: Story = { args: { sizeVariant: 'lg' } }; -export const AllSizes: Story = { - render: (args) => { - const row = document.createElement('div'); - row.style.cssText = 'display:flex;gap:16px;align-items:center'; - for (const size of ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const) - row.append( - element('gd-avatar', { ...args, fallback: args.fallbackComponent, sizeVariant: size, alt: `${size} avatar` }) - ); - return row; - }, + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; -export const CustomColors: Story = { args: { withBadge: true, backgroundColor: '#6b46c1', badgeColor: '#22c55e' } }; -export const SlottedFallback: Story = { - render: () => { - const avatar = element('gd-avatar', { - alt: 'Custom fallback', - sizeVariant: 'xl', - backgroundColor: '#E0E0E0', - }); - const fallback = element('gd-icon', { name: 'star', width: 40, height: 40, fill: '#646464' }); - fallback.slot = 'fallback'; - avatar.append(fallback); - return avatar; - }, -}; -export const WithBadge: Story = { args: { withBadge: true } }; -export const WithCustomBadgeColor: Story = { args: { withBadge: true, badgeColor: '#22c55e' } }; -export const WithInitials: Story = { args: { src: '', fallbackComponent: 'AL' } }; -export const WithCustomBackgroundColor: Story = { - args: { src: '', fallbackComponent: 'AL', backgroundColor: '#6b46c1' }, -}; -export const WithIcon: Story = SlottedFallback; -export const WithImageAndFallback: Story = { args: { src: portrait, fallbackComponent: 'AL' } }; -export const WithDifferentSize: Story = AllSizes; -export const WithAccessibility: Story = { args: { alt: 'Ada Lovelace profile photo' } }; -export const DefaultTokens: Story = { render: () => defaultTokenViewer('avatar') }; +export default meta; +export const Default = { ...nativeStory(fixtures.Default, meta) }; +export const WithBadge = { ...nativeStory(fixtures.WithBadge, meta) }; +export const WithCustomBadgeColor = { ...nativeStory(fixtures.WithCustomBadgeColor, meta) }; +export const WithInitials = { ...nativeStory(fixtures.WithInitials, meta) }; +export const WithCustomBackgroundColor = { ...nativeStory(fixtures.WithCustomBackgroundColor, meta) }; +export const WithIcon = { ...nativeStory(fixtures.WithIcon, meta) }; +export const WithImageAndFallback = { ...nativeStory(fixtures.WithImageAndFallback, meta) }; +export const WithDifferentSize = { ...nativeStory(fixtures.WithDifferentSize, meta) }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Badge.stories.ts b/libs/web-components/stories/Badge.stories.ts index cd7ff26..36baf28 100644 --- a/libs/web-components/stories/Badge.stories.ts +++ b/libs/web-components/stories/Badge.stories.ts @@ -1,90 +1,31 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdBadge, GdIcon } from '../src'; -import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/Badge/Badge.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Badge.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick; +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Badge', {})); const meta = { title: 'Atoms/Badge', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { component: - 'Status badge matching React Badge variants, appearances, sizes, disabled state, icon positions, and Box style overrides.', + 'Status badge with variants, appearances, sizes, disabled state, icon positions, and style overrides.', }, }, }, - args: { variant: 'primary', appearance: 'filled', size: 'md', disabled: false }, - argTypes: sectionedArgTypes('Badge', { - variant: { - description: 'Visual style variant of the badge', - control: 'select', - options: ['primary', 'secondary', 'tertiary', 'quaternary', 'quinary'], - }, - appearance: { - description: 'Filled, light-filled, outline, or light-filled outline appearance', - control: 'select', - options: ['filled', 'filledLight', 'outline', 'outlineFilledLight'], - }, - size: { description: 'Badge size', control: 'select', options: ['xs', 'sm', 'md', 'lg'] }, - disabled: { description: 'Whether the badge uses disabled styling', control: 'boolean' }, - }), - render: (args: Args) => element('gd-badge', args, 'Badge'), -} satisfies Meta; -export default meta; -type Story = StoryObj; -export const Default: Story = {}; -export const Sizes: Story = { args: { size: 'lg' } }; -export const Variants: Story = { args: { variant: 'tertiary' } }; -export const WithIcons: Story = { - render: () => { - const row = document.createElement('div'); - row.style.cssText = 'display:flex;gap:16px;align-items:center;flex-wrap:wrap'; - - const withIcons = ( - text: string, - options: Partial & { iconStart?: GdIcon['name']; iconEnd?: GdIcon['name'] } = {} - ) => { - const { iconStart, iconEnd, ...badgeProps } = options; - const badge = element('gd-badge', badgeProps, text); - const appendIcon = (name: GdIcon['name'], slot: 'icon-start' | 'icon-end') => { - const icon = element('gd-icon', { name, size: 'md' }); - icon.slot = slot; - badge.append(icon); - }; - if (iconStart) appendIcon(iconStart, 'icon-start'); - if (iconEnd) appendIcon(iconEnd, 'icon-end'); - return badge; - }; - - row.append( - withIcons('With Start Icon', { iconStart: 'success' }), - withIcons('With End Icon', { variant: 'secondary', iconEnd: 'warning' }), - withIcons('Both Icons', { - variant: 'tertiary', - appearance: 'filled', - iconStart: 'info', - iconEnd: 'arrowRight', - }), - withIcons('Quaternary with Icon', { variant: 'quaternary', iconStart: 'error' }), - withIcons('Quinary with Icon', { variant: 'quinary', iconStart: 'accountCircle' }) - ); - return row; - }, - parameters: { docs: { description: { story: 'Badges with start and end icons.' } } }, -}; -export const Disabled: Story = { args: { disabled: true } }; -export const WithBoxStyles: Story = { - render: (args) => element('gd-badge', { ...args, styles: { margin: '12px', minWidth: '140px' } }, 'Custom styles'), -}; -export const WithAccessibility: Story = { - render: (args) => { - const badge = element('gd-badge', args, '3 unread messages'); - badge.setAttribute('role', 'status'); - badge.setAttribute('aria-label', '3 unread messages'); - return badge; - }, -}; -export const DefaultTokens: Story = { - render: () => defaultTokenViewer('badge'), + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; +export default meta; +export const Default = { ...nativeStory(fixtures.Default, meta) }; +export const Sizes = { ...nativeStory(fixtures.Sizes, meta) }; +export const Variants = { ...nativeStory(fixtures.Variants, meta) }; +export const WithIcons = { ...nativeStory(fixtures.WithIcons, meta) }; +export const WithBoxStyles = { ...nativeStory(fixtures.WithBoxStyles, meta) }; +export const Disabled = { ...nativeStory(fixtures.Disabled, meta) }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Box.stories.ts b/libs/web-components/stories/Box.stories.ts index 5d81659..6a97ba8 100644 --- a/libs/web-components/stories/Box.stories.ts +++ b/libs/web-components/stories/Box.stories.ts @@ -1,64 +1,33 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdBox } from '../src'; -import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/Box/Box.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Box.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick; +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Box', {})); const meta = { title: 'Atoms/Box', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { component: - 'Layout container matching React Box orientation, border, hover highlight, shadow, accessibility, and custom style behavior.', + 'Layout container with orientation, border, hover highlight, shadow, accessibility, and custom styling.', }, }, }, - args: { variant: 'vertical', isBordered: false, isHighlighted: false, withShadowHover: false }, - argTypes: sectionedArgTypes('Box', { - variant: { description: 'Box orientation variant', control: 'select', options: ['vertical', 'horizontal'] }, - isBordered: { description: 'Adds a border to the box', control: 'boolean' }, - isHighlighted: { description: 'Adds the React hover outline treatment', control: 'boolean' }, - withShadowHover: { description: 'Adds the React elevation shadow on hover', control: 'boolean' }, - }), - render: (args: Args) => element('gd-box', args, 'Box content'), -} satisfies Meta; -export default meta; -type Story = StoryObj; -export const Default: Story = {}; -export const Bordered: Story = { args: { isBordered: true } }; -export const Highlighted: Story = { args: { isBordered: true, isHighlighted: true } }; -export const WithShadowHover: Story = { args: { withShadowHover: true } }; -export const VerticalLayout: Story = {}; -export const HorizontalLayout: Story = { args: { variant: 'horizontal' } }; -export const CenteredContent: Story = { - render: (args) => - element( - 'gd-box', - { ...args, styles: { width: '240px', height: '120px', alignItems: 'center', justifyContent: 'center' } }, - 'Centered content' - ), -}; -export const Examples: Story = { - render: () => { - const root = document.createElement('div'); - root.style.cssText = 'display:grid;gap:16px'; - root.append( - element('gd-box', { isBordered: true }, 'Bordered'), - element('gd-box', { variant: 'horizontal', withShadowHover: true }, 'Horizontal hover') - ); - return root; - }, -}; -export const WithAccessibility: Story = { - render: (args) => { - const box = element('gd-box', args, 'Keyboard-focusable box'); - box.tabIndex = 0; - box.setAttribute('role', 'region'); - box.setAttribute('aria-label', 'Example box'); - return box; - }, -}; -export const DefaultTokens: Story = { - render: () => defaultTokenViewer('box'), + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; +export default meta; +export const Default = { ...nativeStory(fixtures.Default, meta) }; +export const Bordered = { ...nativeStory(fixtures.Bordered, meta) }; +export const Highlighted = { ...nativeStory(fixtures.Highlighted, meta) }; +export const WithShadowHover = { ...nativeStory(fixtures.WithShadowHover, meta) }; +export const VerticalLayout = { ...nativeStory(fixtures.VerticalLayout, meta) }; +export const HorizontalLayout = { ...nativeStory(fixtures.HorizontalLayout, meta) }; +export const CenteredContent = { ...nativeStory(fixtures.CenteredContent, meta) }; +export const Examples = { ...nativeStory(fixtures.Examples, meta) }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Button.stories.ts b/libs/web-components/stories/Button.stories.ts index 086c248..91673c0 100644 --- a/libs/web-components/stories/Button.stories.ts +++ b/libs/web-components/stories/Button.stories.ts @@ -1,117 +1,36 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdButton } from '../src'; -import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/Button/Button.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Button.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick< - GdButton, - | 'variant' - | 'rounded' - | 'disabled' - | 'isLoading' - | 'isIcon' - | 'fullWidth' - | 'type' - | 'role' - | 'tabIndex' - | 'ariaLabel' - | 'ariaPressed' - | 'styles' -> & { children: string }; -const variants = ['primary', 'secondary', 'tertiary', 'outlined', 'text', 'inherit'] as const; -const roundedOptions = ['none', 'default', 'round', 'xs', 'sm', 'md', 'lg', 'xl'] as const; -const buttonElement = ({ children, ...props }: Args, text = children) => element('gd-button', props, text); +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Button', {})); const meta = { title: 'Atoms/Button', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { component: - 'Button matching React variants, icons, loading, width, rounding, disabled, custom styling, states, and accessibility.', + 'Button with variants, icons, loading state, width, rounding, disabled behavior, custom styling, and accessibility.', }, }, }, - args: { - variant: 'primary', - rounded: 'none', - disabled: false, - isLoading: false, - isIcon: false, - fullWidth: false, - type: 'button', - role: 'button', - tabIndex: 0, - ariaLabel: null, - ariaPressed: null, - children: 'Button', - }, - argTypes: sectionedArgTypes('Button', { - variant: { control: 'select', options: variants }, - rounded: { control: 'select', options: roundedOptions }, - }), - render: (args: Args) => buttonElement(args), -} satisfies Meta; -export default meta; -type Story = StoryObj; -export const Default: Story = {}; -export const Secondary: Story = { args: { variant: 'secondary' } }; -export const Outlined: Story = { args: { variant: 'outlined' } }; -export const Disabled: Story = { args: { disabled: true } }; -export const Loading: Story = { args: { isLoading: true } }; -export const AllVariants: Story = { - render: (args) => { - const row = document.createElement('div'); - row.style.cssText = 'display:flex;gap:12px;flex-wrap:wrap'; - for (const variant of variants) row.append(buttonElement({ ...args, variant }, variant)); - return row; - }, -}; -export const Rounded: Story = { - render: (args) => { - const row = document.createElement('div'); - row.style.cssText = 'display:flex;gap:12px;flex-wrap:wrap'; - for (const rounded of roundedOptions) row.append(buttonElement({ ...args, rounded }, rounded)); - return row; - }, + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; -export const FullWidth: Story = { args: { fullWidth: true, children: 'Full-width action' } }; -export const IconOnly: Story = { - render: (args) => { - const button = buttonElement({ ...args, isIcon: true, ariaLabel: 'Close' }, ''); - const icon = element('gd-icon', { name: 'cross' }); - icon.slot = 'icon-start'; - button.append(icon); - return button; - }, -}; -export const WithIcons: Story = { - render: (args) => { - const button = buttonElement({ ...args, variant: 'primary' }, 'Save Changes'); - const start = element('gd-icon', { name: 'check' }); - start.slot = 'icon-start'; - const end = element('gd-icon', { name: 'arrowRight' }); - end.slot = 'icon-end'; - button.append(start, end); - return button; - }, - parameters: { - docs: { - description: { - story: - 'Buttons can include catalog-backed gd-icon elements in the icon-start and icon-end slots for visual context.', - }, - }, - }, -}; -export const IconSlots: Story = WithIcons; -export const FullWidthButton: Story = FullWidth; -export const RoundedButton: Story = Rounded; -export const DisabledButton: Story = Disabled; -export const ButtonStatesUsingClass: Story = { args: { ariaPressed: 'true', children: 'Pressed state' } }; -export const CustomStyledButton: Story = { - render: (args) => buttonElement({ ...args, styles: { letterSpacing: '0.08em' } }, 'Custom styled'), -}; -export const RealWorldExamples: Story = AllVariants; -export const IsLoading: Story = Loading; -export const WithAccessibility: Story = { args: { ariaLabel: 'Save changes', children: 'Save' } }; -export const DefaultTokens: Story = { render: () => defaultTokenViewer('button') }; +export default meta; +export const AllVariants = { ...nativeStory(fixtures.AllVariants, meta) }; +export const Default = { ...nativeStory(fixtures.Default, meta) }; +export const WithIcons = { ...nativeStory(fixtures.WithIcons, meta) }; +export const IconOnly = { ...nativeStory(fixtures.IconOnly, meta) }; +export const FullWidthButton = { ...nativeStory(fixtures.FullWidthButton, meta) }; +export const RoundedButton = { ...nativeStory(fixtures.RoundedButton, meta) }; +export const DisabledButton = { ...nativeStory(fixtures.DisabledButton, meta) }; +export const ButtonStatesUsingClass = { ...nativeStory(fixtures.ButtonStatesUsingClass, meta) }; +export const CustomStyledButton = { ...nativeStory(fixtures.CustomStyledButton, meta) }; +export const RealWorldExamples = { ...nativeStory(fixtures.RealWorldExamples, meta) }; +export const IsLoading = { ...nativeStory(fixtures.IsLoading, meta) }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Checkbox.stories.ts b/libs/web-components/stories/Checkbox.stories.ts index 2b8171b..91e3eb0 100644 --- a/libs/web-components/stories/Checkbox.stories.ts +++ b/libs/web-components/stories/Checkbox.stories.ts @@ -1,42 +1,57 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdCheckbox } from '../src'; -import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes, stack } from './helpers'; -type Args = Pick; +type Args = Partial> & { + children: string; +}; const meta = { title: 'Atoms/Checkbox', tags: ['autodocs'], parameters: { + layout: 'centered', docs: { description: { component: 'Checkbox with controlled, indeterminate, disabled, size, native form, and change-event behavior.', }, }, }, - args: { checked: false, disabled: false, indeterminate: false, size: 'md', name: '', value: 'on' }, - argTypes: sectionedArgTypes('Checkbox', { size: { control: 'select', options: ['sm', 'md'] } }), - render: (args: Args) => { - const checkbox = element('gd-checkbox', args, 'Accept terms'); - checkbox.addEventListener('gd-change', (event) => { - checkbox.checked = (event as CustomEvent<{ checked: boolean }>).detail.checked; - }); - return observed(checkbox, 'gd-change', { checked: args.checked }); - }, + args: { children: 'Accept terms' }, + argTypes: sectionedArgTypes('Checkbox', {}), + render: ({ children, ...args }: Args) => element('gd-checkbox', args, children), } satisfies Meta; export default meta; type Story = StoryObj; export const Default: Story = {}; -export const Checked: Story = { args: { checked: true } }; -export const Indeterminate: Story = { args: { indeterminate: true } }; -export const Disabled: Story = { args: { checked: true, disabled: true } }; -export const Sizes: Story = { - render: (args) => { - const row = document.createElement('div'); - row.style.cssText = 'display:flex;gap:20px;align-items:center'; - row.append(element('gd-checkbox', { ...args, size: 'sm' }, 'Small')); - row.append(element('gd-checkbox', { ...args, size: 'md' }, 'Medium')); - return row; +export const Controlled: Story = { + render: () => { + const state = element('gd-typography', {}, 'Current state: Unchecked'); + const checkbox = element('gd-checkbox', { checked: false }, 'Controlled Checkbox'); + checkbox.addEventListener('gd-change', (event) => { + checkbox.checked = (event as CustomEvent<{ checked: boolean }>).detail.checked; + state.textContent = `Current state: ${checkbox.checked ? 'Checked' : 'Unchecked'}`; + }); + return stack('column', '16px', state, checkbox); }, }; -export const Controlled: Story = Checked; -export const DefaultTokens: Story = { render: () => defaultTokenViewer('checkbox') }; +export const Indeterminate: Story = { args: { indeterminate: true, children: 'Select all' } }; +export const Disabled: Story = { + render: () => + stack( + 'column', + '16px', + element('gd-checkbox', { disabled: true }, 'Disabled unchecked'), + element('gd-checkbox', { disabled: true, checked: true }, 'Disabled checked'), + element('gd-checkbox', { disabled: true, indeterminate: true }, 'Disabled indeterminate') + ), +}; +export const Sizes: Story = { + render: () => + stack( + 'row', + '24px', + element('gd-checkbox', { size: 'sm' }, 'Small checkbox'), + element('gd-checkbox', { size: 'md' }, 'Medium checkbox') + ), +}; +export const DefaultTokens: Story = { parameters: { layout: 'padded' }, render: () => defaultTokenViewer('checkbox') }; diff --git a/libs/web-components/stories/Counter.stories.ts b/libs/web-components/stories/Counter.stories.ts index 5c469df..25f84d8 100644 --- a/libs/web-components/stories/Counter.stories.ts +++ b/libs/web-components/stories/Counter.stories.ts @@ -1,33 +1,30 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdCounter } from '../src'; -import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/molecules/Counter/Counter.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Counter.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick; +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Counter', {})); const meta = { title: 'Molecules/Counter', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { component: - 'Counter matching React minimum, maximum, initial value, external change handling, disabled state, and accessible native controls.', + 'Counter with minimum, maximum, initial value, external change handling, disabled state, and accessible native controls.', }, }, }, - args: { min: 1, max: 5, initial: 1, isDisabled: false }, - argTypes: sectionedArgTypes('Counter', {}), - render: (args: Args) => observed(element('gd-counter', args), 'gd-change', { value: args.initial }), -} satisfies Meta; + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, +}; export default meta; -type Story = StoryObj; -export const Default: Story = {}; -export const AtMaximum: Story = { args: { initial: 5 } }; -export const Disabled: Story = { args: { isDisabled: true } }; -export const AdjustedMaximum: Story = { args: { max: 10, initial: 8 } }; -export const AdjustedMinimum: Story = { args: { min: 3, initial: 3 } }; -export const CustomRange: Story = { args: { min: 2, max: 10, initial: 5 } }; -export const AdjustedMaxValue5: Story = { args: { max: 5, initial: 5 } }; -export const AdjustedMinValue3: Story = { args: { min: 3, initial: 3 } }; -export const AdjustedMin2MaxValue10: Story = CustomRange; -export const WithExternalCounterChangeHandler: Story = Default; -export const DefaultTokens: Story = { render: () => defaultTokenViewer('counter') }; +export const Default = { ...nativeStory(fixtures.Default, meta) }; +export const AdjustedMaxValue5 = { ...nativeStory(fixtures.AdjustedMaxValue5, meta) }; +export const AdjustedMinValue3 = { ...nativeStory(fixtures.AdjustedMinValue3, meta) }; +export const AdjustedMin2MaxValue10 = { ...nativeStory(fixtures.AdjustedMin2MaxValue10, meta) }; +export const WithExternalCounterChangeHandler = { ...nativeStory(fixtures.WithExternalCounterChangeHandler, meta) }; +export const Disabled = { ...nativeStory(fixtures.Disabled, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Icon.stories.ts b/libs/web-components/stories/Icon.stories.ts index 7e8ad8e..7474ad0 100644 --- a/libs/web-components/stories/Icon.stories.ts +++ b/libs/web-components/stories/Icon.stories.ts @@ -104,11 +104,7 @@ export const RegisteringCustomIcons: Story = { }, }; export const WithAccessibility: Story = { - render: (args) => { - const icon = element('gd-icon', args); - icon.setAttribute('aria-label', 'Favorite'); - return icon; - }, + render: AllIcons.render, }; export const DefaultTokens: Story = { render: () => defaultTokenViewer('icon'), diff --git a/libs/web-components/stories/Image.stories.ts b/libs/web-components/stories/Image.stories.ts index 0c5e5d5..02eaa2c 100644 --- a/libs/web-components/stories/Image.stories.ts +++ b/libs/web-components/stories/Image.stories.ts @@ -4,7 +4,8 @@ import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Partial< Pick ->; +> & + Record; const COMMON_ARGS: Args = { src: 'https://picsum.photos/150/150', alt: 'Test image', @@ -33,71 +34,16 @@ const meta = { height: 300, objectFit: 'cover', }, - argTypes: { - ...sectionedArgTypes('Image', {}), - as: { - description: 'Semantic element used by the internal image wrapper', - control: 'select', - options: ['div', 'figure'], - table: { type: { summary: `'div' | 'figure'` }, defaultValue: { summary: 'div' }, category: 'Core Properties' }, - }, - captionAs: { - description: 'Semantic element used for the caption', - control: 'select', - options: ['figcaption', 'p', 'span'], - table: { - type: { summary: `'figcaption' | 'p' | 'span'` }, - defaultValue: { summary: 'figcaption' }, - category: 'Core Properties', - }, - }, - placeholder: { - description: 'Text displayed in a Skeleton while the image loads', - control: 'text', - table: { type: { summary: 'string' }, category: 'Content & Display' }, - }, - fallbackComponent: { - description: 'Element assigned to the fallback slot and displayed when the source is missing or fails to load', - control: { disable: true }, - table: { type: { summary: 'HTMLElement (slot="fallback")' }, category: 'Content & Display' }, - }, - styles: { - description: 'CSS property values applied to the internal image element', - control: { disable: true }, - table: { type: { summary: 'Record' }, category: 'Styling' }, - }, - onClick: { - description: 'Listen for the native click event on gd-image', - control: { disable: true }, - table: { type: { summary: 'EventListener' }, category: 'Events' }, - }, - onError: { - description: 'Listen for the gd-error custom event', - control: { disable: true }, - table: { type: { summary: 'EventListener' }, category: 'Events' }, - }, - onLoad: { - description: 'Listen for the gd-load custom event', - control: { disable: true }, - table: { type: { summary: 'EventListener' }, category: 'Events' }, - }, - objectFit: { - description: 'Native object-fit behavior', - control: 'select', - options: ['cover', 'contain', 'fill', 'none', 'scale-down'], - }, - }, + argTypes: sectionedArgTypes('Image', {}), render: (args: Args) => element('gd-image', args), } satisfies Meta; export default meta; type Story = StoryObj; -export const Default: Story = { name: 'Default Image', args: COMMON_ARGS }; +export const Default: Story = { args: COMMON_ARGS }; export const WithCaption: Story = { - name: 'Image With Caption', args: { ...COMMON_ARGS, caption: 'This is a sample caption below the image.' }, }; export const WithPlaceholder: Story = { - name: 'Image With Placeholder', args: { ...COMMON_ARGS, placeholder: 'Loading image...' }, }; export const FallbackImage: Story = { @@ -120,7 +66,6 @@ export const FallbackImage: Story = { }, }; export const Clickable: Story = { - name: 'Clickable Image', args: { ...COMMON_ARGS, caption: 'Click me!' }, render: (args) => { const image = element('gd-image', args); @@ -131,7 +76,6 @@ export const Clickable: Story = { }, }; export const WithAsProp: Story = { - name: 'Image With As Prop', args: { ...COMMON_ARGS, as: 'figure', caption: 'Image with semantic figure wrapper' }, parameters: { docs: { @@ -142,7 +86,6 @@ export const WithAsProp: Story = { }, }; export const WithCaptionAsProp: Story = { - name: 'Image With captionAs Prop', args: { ...COMMON_ARGS, caption: 'Image with custom caption element (p tag)', captionAs: 'p' }, parameters: { docs: { @@ -153,7 +96,6 @@ export const WithCaptionAsProp: Story = { }, }; export const WithAccessibility: Story = { - name: 'Image With Accessibility', args: { ...COMMON_ARGS, alt: 'Test image' }, }; export const DefaultTokens: Story = { diff --git a/libs/web-components/stories/Input.stories.ts b/libs/web-components/stories/Input.stories.ts index 7b387be..264990f 100644 --- a/libs/web-components/stories/Input.stories.ts +++ b/libs/web-components/stories/Input.stories.ts @@ -1,142 +1,166 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdInput } from '../src'; -import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes, stack } from './helpers'; -type Args = Pick< - GdInput, - | 'defaultValue' - | 'variant' - | 'label' - | 'helperText' - | 'placeholder' - | 'disabled' - | 'readOnly' - | 'required' - | 'color' - | 'width' - | 'role' - | 'tabIndex' - | 'debounceCallbackTime' ->; -const inputVariants = [ - 'text', - 'password', - 'email', - 'search', - 'url', - 'tel', - 'date', - 'time', - 'month', - 'week', - 'color', - 'range', - 'number', - 'radio', - 'checkbox', -]; -const colors = ['primary', 'success', 'warning', 'error'] as const; +type Args = Partial> & { + wrapperAs?: 'div' | 'span' | 'label'; + ariaRequired?: boolean; + adornmentStart?: string; + adornmentEnd?: string; +}; +const common: Args = { + variant: 'text', + color: 'primary', + role: 'textbox', + name: 'input', + width: '100%', + disabled: false, + required: false, + readOnly: false, + checked: false, + placeholder: 'placeholder', + defaultValue: 'defaultValue', + tabIndex: 0, + ariaRequired: false, + debounceCallbackTime: 300, + styles: {}, + label: '', + helperText: '', +}; +function inputElement({ ariaRequired, adornmentStart, adornmentEnd, ...args }: Args) { + const input = element('gd-input', { ...args, ariaRequiredValue: ariaRequired }); + for (const [slot, text] of [ + ['adornment-start', adornmentStart], + ['adornment-end', adornmentEnd], + ]) { + if (!text) continue; + const adornment = document.createElement('span'); + adornment.slot = slot!; + adornment.style.cssText = `padding:${slot === 'adornment-start' ? '0 0 0 10px' : '0 10px 0 0'};display:flex;color:#838383`; + adornment.textContent = text; + input.append(adornment); + } + return input; +} const meta = { title: 'Atoms/Input', tags: ['autodocs'], parameters: { + layout: 'centered', docs: { description: { component: - 'Input matching React labels, helper text, validation colors, disabled/read-only states, adornments, styles, checkbox/radio modes, accessibility, and native events.', + 'Input with labels, helper text, validation colors, disabled and read-only states, adornments, checkbox/radio modes, accessibility, and native events.', }, }, }, - args: { - defaultValue: 'Default Input', - variant: 'text', - label: 'Label', - helperText: 'Helper text', - placeholder: 'Enter text', - disabled: false, - readOnly: false, - required: false, - color: 'primary', - width: '320px', - role: null, - tabIndex: 0, - debounceCallbackTime: undefined, - }, - argTypes: sectionedArgTypes('Input', { - variant: { control: 'select', options: inputVariants }, - color: { control: 'select', options: colors }, - }), - render: (args: Args) => { - const input = element('gd-input', args); - input.addEventListener('gd-input', (event) => { - input.value = (event as CustomEvent<{ value: string }>).detail.value; - }); - return observed(input, 'gd-input', { value: args.defaultValue }); - }, + argTypes: sectionedArgTypes('Input', {}), + render: (args: Args) => inputElement(args), } satisfies Meta; export default meta; type Story = StoryObj; -export const Default: Story = {}; -export const Empty: Story = { args: { defaultValue: '' } }; -export const Disabled: Story = { args: { disabled: true } }; -export const ReadOnly: Story = { args: { readOnly: true } }; -export const ValidationColors: Story = { - render: (args) => { - const section = document.createElement('section'); - section.style.cssText = 'display:grid;gap:20px;max-width:360px'; - for (const color of colors) - section.append(element('gd-input', { ...args, color, label: color, helperText: `${color} helper text` })); - return section; - }, -}; -export const WithAdornments: Story = { - render: (args) => { - const input = element('gd-input', args); - const start = document.createElement('span'); - start.slot = 'adornment-start'; - start.textContent = '🔎'; - const end = document.createElement('span'); - end.slot = 'adornment-end'; - end.textContent = '⌘K'; - input.append(start, end); +const createStory = (args: Args): Story => ({ args: { ...common, ...args } }); +export const PrimaryDefaultWithLabelAndHelperText = createStory({ + label: 'Label', + helperText: 'Helper text', + defaultValue: 'Default Input', +}); +export const SuccessWithLabelAndHelperText = createStory({ + label: 'Label', + helperText: 'Helper text', + defaultValue: 'Success Input', + color: 'success', +}); +export const WarningWithLabelAndHelperText = createStory({ + label: 'Label', + helperText: 'Helper text', + defaultValue: 'Warning Input', + color: 'warning', +}); +export const ErrorWithLabelAndHelperText = createStory({ + label: 'Label', + helperText: 'Helper text', + defaultValue: 'Error Input', + color: 'error', +}); +export const Disabled = createStory({ label: 'Label', defaultValue: 'Disabled Input', disabled: true }); +export const ReadOnly = createStory({ defaultValue: 'ReadOnly Input', readOnly: true }); +export const WrapperAsSpan = createStory({ defaultValue: 'Input wrapper span', wrapperAs: 'span' }); +export const WithAdornments = createStory({ + defaultValue: 'Input with adornments', + adornmentStart: '€', + adornmentEnd: '.00', +}); +export const WithStartAdornment = createStory({ + variant: 'email', + defaultValue: 'user@example.com', + adornmentStart: '@', +}); +export const WithEndAdornmentAsIcon: Story = { + render: () => { + const input = inputElement({ name: 'test', width: '150px', variant: 'password', defaultValue: 'qwerty' }); + const toggle = document.createElement('span'); + toggle.slot = 'adornment-end'; + toggle.style.cssText = 'padding:0 10px 0 0;cursor:pointer'; + toggle.append(element('gd-icon', { name: 'eye', width: 18, height: 18, fill: '#838383' })); + toggle.addEventListener('click', () => { + input.variant = input.variant === 'password' ? 'text' : 'password'; + }); + input.append(toggle); return input; }, }; -export const InputTypes: Story = { - render: (args) => { - const section = document.createElement('section'); - section.style.cssText = 'display:grid;gap:16px;max-width:360px'; - for (const variant of ['text', 'email', 'password', 'search', 'number', 'date']) - section.append(element('gd-input', { ...args, variant, defaultValue: '', label: variant })); - return section; +export const CustomStyles = createStory({ + defaultValue: 'CustomStyles Input', + styles: { backgroundColor: 'lightblue', border: 'black', padding: '1rem' }, +}); +function labelledChoice(variant: 'checkbox' | 'radio', text = 'Label', props: Args = {}) { + const input = inputElement({ name: 'test', variant, role: variant, ...props }); + const label = element('gd-label', {}); + label.append(input, document.createTextNode(text)); + // Labels cannot activate a control through another element's shadow root. + label.addEventListener('click', (event) => { + if (event.composedPath().includes(input)) return; + input.shadowRoot?.querySelector('input')?.click(); + }); + return label; +} +export const CheckboxWithLabel: Story = { render: () => labelledChoice('checkbox') }; +export const RadioControlledWithLabel: Story = { render: () => labelledChoice('radio') }; +export const RadioGroupWithLabel: Story = { + render: () => { + const root = document.createElement('div'); + const labels = ['1', '2', '3'].map((value) => + labelledChoice('radio', `Label ${value}`, { + name: 'test_group', + value, + defaultChecked: value === '1', + wrapperAs: 'span', + }) + ); + root.append(...labels); + return root; }, }; -export const PrimaryDefaultWithLabelAndHelperText: Story = Default; -export const SuccessWithLabelAndHelperText: Story = { args: { color: 'success' } }; -export const WarningWithLabelAndHelperText: Story = { args: { color: 'warning' } }; -export const ErrorWithLabelAndHelperText: Story = { args: { color: 'error' } }; -export const WrapperAsSpan: Story = { - ...Default, - parameters: { - docs: { - description: { story: 'The Custom Element host is stable; internal layout provides the React wrapper behavior.' }, - }, - }, -}; -export const WithStartAdornment: Story = WithAdornments; -export const WithEndAdornmentAsIcon: Story = WithAdornments; -export const DefaultWithTailwind: Story = { - render: (args) => element('gd-input', { ...args, styles: { fontWeight: 700 } }), -}; -export const CustomStyles: Story = { - render: (args) => element('gd-input', { ...args, styles: { letterSpacing: '0.05em' } }), -}; -export const CheckboxWithLabel: Story = { args: { variant: 'checkbox', defaultValue: '', label: 'Accept terms' } }; -export const RadioControlledWithLabel: Story = { - args: { variant: 'radio', defaultValue: 'one', label: 'Option one' }, -}; -export const RadioGroupWithLabel: Story = InputTypes; export const WithAccessibility: Story = { - args: { required: true, label: 'Email address', helperText: 'Required field' }, + render: (args) => + stack( + 'column', + '20px', + inputElement({ + ...args, + label: 'Email', + helperText: 'Error message', + color: 'error', + ariaDescribedBy: 'error-id', + }), + inputElement({ + variant: 'checkbox', + role: 'checkbox', + name: 'terms', + ariaLabel: 'terms', + label: 'Agree to terms', + }) + ), }; -export const DefaultTokens: Story = { render: () => defaultTokenViewer('input') }; +export const DefaultTokens: Story = { parameters: { layout: 'padded' }, render: () => defaultTokenViewer('input') }; diff --git a/libs/web-components/stories/InputFile.stories.ts b/libs/web-components/stories/InputFile.stories.ts index 77848bc..04bcdc7 100644 --- a/libs/web-components/stories/InputFile.stories.ts +++ b/libs/web-components/stories/InputFile.stories.ts @@ -1,70 +1,32 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdInputFile } from '../src'; -import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/InputFile/InputFile.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/InputFile.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick & { - buttonProps: { variant?: GdInputFile['buttonVariant'] }; -}; +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('InputFile', {})); const meta = { title: 'Atoms/InputFile', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { component: - 'File picker matching React InputFile accept, capture, multiple, disabled, icon-label, button variant, custom styles, and accessibility behavior. Emits gd-change with serializable file metadata.', + 'File picker with accepted file types, capture, multiple selection, disabled and icon-label states, button customization, native events, and accessible controls.', }, }, }, - args: { - accept: '', - capture: undefined, - multiple: false, - disabled: false, - isIcon: false, - buttonProps: { variant: 'outlined' }, - }, - argTypes: sectionedArgTypes('InputFile', { - accept: { description: 'Accepted file MIME types or extensions', control: 'text' }, - capture: { - description: 'Preferred camera capture source', - control: 'select', - options: [undefined, 'user', 'environment'], - }, - multiple: { description: 'Allows selection of multiple files', control: 'boolean' }, - disabled: { description: 'Disables the file picker', control: 'boolean' }, - isIcon: { description: 'Uses icon-only button sizing', control: 'boolean' }, - buttonProps: { description: 'Properties passed to the internal button', control: 'object' }, - }), - render: ({ buttonProps, ...args }: Args) => - observed(element('gd-input-file', { ...args, buttonVariant: buttonProps.variant }), 'gd-change', { files: [] }), -} satisfies Meta; -export default meta; -type Story = StoryObj; -export const Default: Story = {}; -export const Disabled: Story = { args: { disabled: true } }; -export const Multiple: Story = { args: { multiple: true } }; -export const WithAccept: Story = { args: { accept: 'image/*' } }; -export const CustomLabel: Story = { render: (args) => element('gd-input-file', args, 'Choose documents') }; -export const IconLabel: Story = { - render: (args) => { - const { buttonProps, ...props } = args; - void buttonProps; - const input = element('gd-input-file', { ...props, isIcon: false, buttonVariant: 'text' }); - input.append(element('gd-icon', { name: 'attachment' })); - return input; - }, -}; -export const WithCustomStyles: Story = { - render: (args) => element('gd-input-file', { ...args, styles: { borderRadius: '8px' } }, 'Styled file picker'), -}; -export const WithAccessibility: Story = { - render: (args) => { - const input = element('gd-input-file', args, 'Upload résumé'); - input.setAttribute('aria-label', 'Upload résumé'); - return input; - }, -}; -export const DefaultTokens: Story = { - render: () => defaultTokenViewer('inputfile'), + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; +export default meta; +export const Default = { ...nativeStory(fixtures.Default, meta) }; +export const Disabled = { ...nativeStory(fixtures.Disabled, meta) }; +export const Multiple = { ...nativeStory(fixtures.Multiple, meta) }; +export const WithAccept = { ...nativeStory(fixtures.WithAccept, meta) }; +export const CustomLabel = { ...nativeStory(fixtures.CustomLabel, meta) }; +export const IconLabel = { ...nativeStory(fixtures.IconLabel, meta) }; +export const WithCustomStyles = { ...nativeStory(fixtures.WithCustomStyles, meta) }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Label.stories.ts b/libs/web-components/stories/Label.stories.ts index dd26191..50d7f7c 100644 --- a/libs/web-components/stories/Label.stories.ts +++ b/libs/web-components/stories/Label.stories.ts @@ -1,53 +1,28 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdLabel } from '../src'; -import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/Label/Label.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Label.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick; +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Label', {})); const meta = { title: 'Atoms/Label', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { component: - 'Native label matching React Label content, child icons, custom styles, control association, and accessibility semantics.', + 'Native label with slotted content, child icons, custom styling, control association, and accessibility semantics.', }, }, }, - args: { htmlFor: 'example' }, - argTypes: sectionedArgTypes('Label', { - htmlFor: { description: 'ID of the associated form control', control: 'text' }, - styles: { description: 'Custom inline style overrides', control: 'object' }, - }), - render: (args: Args) => element('gd-label', args, 'Field label'), -} satisfies Meta; -export default meta; -type Story = StoryObj; -export const LabelDefault: Story = {}; -export const LabelWithChildIcon: Story = { - render: (args) => { - const label = element('gd-label', args, 'Label '); - label.prepend(element('gd-icon', { name: 'star' }), document.createTextNode(' ')); - return label; - }, -}; -export const CustomStyles: Story = { args: { styles: { color: '#0069b4', fontWeight: 700, gap: '8px' } } }; -export const DefaultWithTailwind: Story = { - args: { styles: { fontSize: '18px', color: '#1f843a' } }, - parameters: { - docs: { description: { story: 'Framework-neutral style properties reproduce the React utility-class example.' } }, - }, -}; -export const WithAccessibility: Story = { - render: (args) => { - const root = document.createElement('div'); - const label = element('gd-label', { ...args, htmlFor: 'label-story-input' }, 'Email address'); - const input = document.createElement('input'); - input.id = 'label-story-input'; - root.append(label, input); - return root; - }, -}; -export const DefaultTokens: Story = { - render: () => defaultTokenViewer('label'), + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; +export default meta; +export const LabelDefault = { ...nativeStory(fixtures.LabelDefault, meta) }; +export const LabelWithChildIcon = { ...nativeStory(fixtures.LabelWithChildIcon, meta) }; +export const CustomStyles = { ...nativeStory(fixtures.CustomStyles, meta) }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Link.stories.ts b/libs/web-components/stories/Link.stories.ts index 1f300c3..032d2b3 100644 --- a/libs/web-components/stories/Link.stories.ts +++ b/libs/web-components/stories/Link.stories.ts @@ -1,92 +1,34 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdLink } from '../src'; -import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/Link/Link.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Link.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick< - GdLink, - 'variant' | 'size' | 'underline' | 'color' | 'cursor' | 'disabled' | 'href' | 'target' | 'rel' | 'styles' ->; +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Link', {})); const meta = { title: 'Atoms/Link', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { component: - 'Native anchor matching React Link variants, sizes, underline modes, colors, cursor, disabled behavior, targets, relationships, custom styles, accessibility, and child composition.', + 'Native anchor with variants, sizes, underline modes, colors, disabled behavior, targets, custom styling, accessibility, and child composition.', }, }, }, - args: { - variant: 'primary', - size: 'md', - underline: 'default', - disabled: false, - href: 'https://example.com', - target: undefined, - rel: '', - }, - argTypes: sectionedArgTypes('Link', { - variant: { - description: 'Design variant', - control: 'select', - options: ['primary', 'secondary', 'inverted', 'inherit'], - }, - size: { description: 'Link typography size', control: 'select', options: ['sm', 'md', 'lg'] }, - underline: { description: 'Underline presentation', control: 'select', options: ['default', 'highlight', 'none'] }, - color: { description: 'CSS color or theme color token', control: 'text' }, - cursor: { description: 'CSS cursor', control: 'text' }, - disabled: { description: 'Prevents navigation and applies disabled styling', control: 'boolean' }, - href: { description: 'Navigation URL', control: 'text' }, - target: { description: 'Browsing context target', control: 'text' }, - rel: { description: 'Anchor relationship attribute', control: 'text' }, - styles: { description: 'Custom inline style overrides', control: 'object' }, - }), - render: (args: Args) => element('gd-link', args, 'GridKit link'), -} satisfies Meta; -export default meta; -type Story = StoryObj; -export const Variants: Story = {}; -export const Disabled: Story = { args: { disabled: true } }; -export const WithUnderline: Story = { args: { underline: 'highlight' } }; -export const WithSizes: Story = { args: { size: 'lg' } }; -export const TargetBlankVisited: Story = { - args: { - variant: 'inherit', - href: 'https://storybook.cto-rnd-system-design.griddynamics.net/', - target: '_blank', - rel: 'noopener noreferrer', - }, -}; -export const InheritWithButtonAsChild: Story = { - render: (args) => { - const link = element('gd-link', { ...args, variant: 'inherit' }); - link.append(element('gd-button', { variant: 'primary' }, 'Button child')); - return link; - }, -}; -export const Inverted: Story = { args: { variant: 'inverted' } }; -export const InheritWithTypographyAsChild: Story = { - render: (args) => { - const link = element('gd-link', { ...args, variant: 'inherit' }); - link.append(element('gd-typography', { variant: 'p' }, 'Typography child')); - return link; - }, -}; -export const InheritWithTailwind: Story = { - args: { variant: 'inherit', styles: { fontWeight: 700, textTransform: 'uppercase' } }, - parameters: { - docs: { description: { story: 'Framework-neutral style properties reproduce the React utility-class example.' } }, - }, -}; -export const CustomStyles: Story = { args: { styles: { color: '#7b2cbf', letterSpacing: '0.08em' } } }; -export const WithAccessibility: Story = { - render: (args) => { - const link = element('gd-link', args, 'Read the accessibility guide'); - link.setAttribute('aria-label', 'Read the accessibility guide'); - return link; - }, -}; -export const DefaultTokens: Story = { - render: () => defaultTokenViewer('link'), + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; +export default meta; +export const Variants = { ...nativeStory(fixtures.Variants, meta) }; +export const Disabled = { ...nativeStory(fixtures.Disabled, meta) }; +export const TargetBlankVisited = { ...nativeStory(fixtures.TargetBlankVisited, meta) }; +export const InheritWithButtonAsChild = { ...nativeStory(fixtures.InheritWithButtonAsChild, meta) }; +export const Inverted = { ...nativeStory(fixtures.Inverted, meta) }; +export const InheritWithTypographyAsChild = { ...nativeStory(fixtures.InheritWithTypographyAsChild, meta) }; +export const CustomStyles = { ...nativeStory(fixtures.CustomStyles, meta) }; +export const WithUnderline = { ...nativeStory(fixtures.WithUnderline, meta) }; +export const WithSizes = { ...nativeStory(fixtures.WithSizes, meta) }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Loader.stories.ts b/libs/web-components/stories/Loader.stories.ts index a3dfa0e..0e1a8a9 100644 --- a/libs/web-components/stories/Loader.stories.ts +++ b/libs/web-components/stories/Loader.stories.ts @@ -1,81 +1,35 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdLoader } from '../src'; -import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/Loader/Loader.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Loader.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick< - GdLoader, - 'name' | 'variant' | 'size' | 'rounded' | 'withWrapper' | 'wrapperAs' | 'animationProps' | 'styles' -> & { WrapperView?: keyof HTMLElementTagNameMap }; +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Loader', {})); const meta = { title: 'Atoms/Loader', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { component: - 'Loading indicator matching React Loader circle/dots animations, five sizes, rounding, inline/section/full-page wrappers, custom animation timing, custom content, styles, and status accessibility.', - }, - }, - }, - args: { name: 'circle', variant: 'inline', size: 'md', rounded: 'none', withWrapper: true }, - argTypes: sectionedArgTypes('Loader', { - name: { control: 'select', options: ['circle', 'dots'] }, - variant: { control: 'select', options: ['inline', 'section', 'fullPage'] }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl'] }, - rounded: { - description: 'Radius token used by dot loaders', - control: 'select', - options: ['none', 'default', 'round', 'xs', 'sm', 'md', 'lg', 'xl'], - }, - withWrapper: { description: 'Whether the positioning wrapper is rendered', control: 'boolean' }, - animationProps: { description: 'CSS animation timing, easing, and iteration values', control: 'text' }, - styles: { description: 'Custom loader style overrides', control: 'object' }, - }), - render: ({ WrapperView, ...args }: Args) => - element('gd-loader', { ...args, wrapperAs: WrapperView ?? args.wrapperAs }), -} satisfies Meta; -export default meta; -type Story = StoryObj; -export const Default: Story = {}; -export const LoaderNames: Story = { args: { name: 'dots' } }; -export const LoaderSizes: Story = { args: { size: 'xl' } }; -export const LoaderRounded: Story = { args: { name: 'dots', rounded: 'round' } }; -export const LoaderSectionVariant: Story = { args: { variant: 'section' } }; -export const LoaderVariantsWithWrapperViewAsHeaderTag: Story = { - args: { variant: 'inline', WrapperView: 'header' }, - parameters: { - docs: { - description: { - story: - 'The React WrapperView prop maps to the Web Component wrapperAs property and composes gd-wrapper with a semantic header element.', + 'Loading indicator with circle and dots animations, five sizes, rounding, inline, section and full-page layouts, custom timing, content, styling, and status accessibility.', }, }, }, + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; -export const CustomLoaderTailwindClassBounceAnimation: Story = { - args: { name: 'dots', animationProps: '600ms ease-in-out infinite', styles: { color: '#7b2cbf' } }, -}; -export const SectionLoaderButtonVariant: Story = { - render: (args) => { - const button = element('gd-button', { variant: 'primary' }, 'Loading'); - button.append(element('gd-loader', { ...args, variant: 'section', size: 'sm' })); - return button; - }, -}; -export const InlineLoaderButtonVariant: Story = { - render: (args) => { - const button = element('gd-button', { variant: 'primary' }, 'Save '); - button.append(element('gd-loader', { ...args, variant: 'inline', size: 'sm', withWrapper: false })); - return button; - }, -}; -export const WithAccessibility: Story = { - render: (args) => { - const loader = element('gd-loader', args); - loader.setAttribute('aria-label', 'Saving changes'); - return loader; - }, -}; -export const DefaultTokens: Story = { - render: () => defaultTokenViewer('loader'), +export default meta; +export const Default = { ...nativeStory(fixtures.Default, meta) }; +export const LoaderNames = { ...nativeStory(fixtures.LoaderNames, meta) }; +export const LoaderSizes = { ...nativeStory(fixtures.LoaderSizes, meta) }; +export const LoaderRounded = { ...nativeStory(fixtures.LoaderRounded, meta) }; +export const LoaderVariantsWithWrapperViewAsHeaderTag = { + ...nativeStory(fixtures.LoaderVariantsWithWrapperViewAsHeaderTag, meta), }; +export const LoaderSectionVariant = { ...nativeStory(fixtures.LoaderSectionVariant, meta) }; +export const SectionLoaderButtonVariant = { ...nativeStory(fixtures.SectionLoaderButtonVariant, meta) }; +export const InlineLoaderButtonVariant = { ...nativeStory(fixtures.InlineLoaderButtonVariant, meta) }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Menu.stories.ts b/libs/web-components/stories/Menu.stories.ts index fb9e1af..07e6bdc 100644 --- a/libs/web-components/stories/Menu.stories.ts +++ b/libs/web-components/stories/Menu.stories.ts @@ -1,82 +1,137 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { buttonCssBlockToText, resolveThemeTree, type ButtonCssBlock } from 'gd-design-core'; +import { defaultTheme, select } from 'gd-design-library/tokens'; import type { GdMenu } from '../src'; -import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes, stack } from './helpers'; -type Args = Pick; -const placements = ['top-left', 'top-right', 'bottom-left', 'bottom-right'] as const; +type Args = Partial>; +function menuExample(args: Args, label = 'Menu', names = ['Profile', 'Settings', 'Logout']) { + const menu = element('gd-menu', args); + const trigger = document.createElement('span'); + trigger.slot = 'trigger'; + trigger.textContent = label; + const content = document.createElement('div'); + content.slot = 'content'; + const style = document.createElement('style'); + const itemTokens = resolveThemeTree(select.item, defaultTheme) as unknown as { default: ButtonCssBlock }; + style.textContent = buttonCssBlockToText('.story-menu-item', itemTokens.default); + content.append(style); + for (const name of names) { + const item = document.createElement('div'); + item.className = 'story-menu-item'; + item.role = 'button'; + item.tabIndex = 0; + item.textContent = name; + item.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + item.click(); + } + }); + item.dataset.gdMenuName = name; + item.dataset.gdMenuValue = name.toLowerCase(); + content.append(item); + } + menu.append(trigger, content); + return menu; +} const meta = { title: 'Molecules/Menu', tags: ['autodocs'], + argTypes: sectionedArgTypes('Menu', {}), parameters: { - docs: { - description: { - component: - 'Menu matching React close-on-select, placement, imperative controls, offsets, height constraints, positioning, modal actions, and native popover behavior.', - }, - }, - }, - args: { - closeOnSelect: true, - placement: 'bottom-right', - offsetX: 4, - offsetY: 4, - minHeight: 80, - maxHeight: 400, - }, - argTypes: sectionedArgTypes('Menu', { placement: { control: 'select', options: placements } }), - render: (args: Args) => { - const menu = element('gd-menu', args); - const trigger = document.createElement('span'); - trigger.slot = 'trigger'; - trigger.textContent = 'Actions'; - const content = document.createElement('div'); - content.slot = 'content'; - content.style.cssText = 'display:grid;gap:8px;padding:12px'; - for (const name of ['Edit', 'Archive']) { - const button = document.createElement('button'); - button.textContent = name; - button.dataset.gdMenuName = name; - button.dataset.gdMenuValue = name.toLowerCase(); - content.append(button); - } - menu.append(trigger, content); - const section = observed(menu, 'gd-change', null); - const outside = document.createElement('button'); - outside.textContent = 'Outside menu'; - section.append(outside); - return section; + layout: 'centered', + docs: { description: { component: 'Menu with selection, positioning, imperative controls and modal actions.' } }, }, + render: (args: Args) => menuExample(args), } satisfies Meta; export default meta; type Story = StoryObj; export const Default: Story = {}; -export const Open: Story = { - render: (args) => element('gd-menu', { ...args, open: true }), +export const CloseOnSelectFalse: Story = { args: { closeOnSelect: false } }; +export const Placement: Story = { render: (args) => menuExample(args, 'Menu (Placement)') }; +export const WithRefControl: Story = { + render: () => { + const menu = menuExample({}, 'Menu (Controlled via Ref)'); + const trigger = menu.querySelector('[slot="trigger"]')!; + trigger.textContent = ''; + trigger.append(element('gd-typography', { styleVariant: 'bold' }, 'Menu (Controlled via Ref)')); + const open = element('gd-button', { variant: 'secondary' }, 'Open Menu'); + const close = element('gd-button', { variant: 'outlined' }, 'Close Menu'); + open.addEventListener('click', () => { + menu.open = true; + }); + close.addEventListener('click', () => { + menu.open = false; + }); + return stack('column', '16px', stack('row', '8px', open, close), stack('row', '8px', menu)); + }, +}; +export const WithOffset: Story = { + args: { offsetX: 12, offsetY: 8 }, + render: (args) => menuExample(args, 'Menu (Custom Offset)'), }; -export const KeepOpenOnSelect: Story = { args: { closeOnSelect: false } }; -export const Placement: Story = { +export const WithHeightConstraints: Story = { + args: { minHeight: 100, maxHeight: 300 }, render: (args) => { - const grid = document.createElement('div'); - grid.style.cssText = 'display:grid;grid-template-columns:repeat(2,180px);gap:120px;padding:120px'; - for (const placement of placements) { - const menu = element('gd-menu', { ...args, placement }); - const trigger = document.createElement('span'); - trigger.slot = 'trigger'; - trigger.textContent = placement; - const content = document.createElement('div'); - content.slot = 'content'; - content.style.padding = '12px'; - content.textContent = `${placement} content`; - menu.append(trigger, content); - grid.append(menu); - } - return grid; + const box = element('gd-box', { styles: { minHeight: '100vh' } }); + box.append(menuExample(args, 'Menu (Height Constraints)', ['Profile', 'Settings'])); + return box; + }, +}; +export const WithPositioningOptions: Story = { + args: { placement: 'bottom-right', offsetX: 8, offsetY: 8, minHeight: 120, maxHeight: 350 }, + render: (args) => menuExample(args, 'Menu (All Positioning Options)'), +}; +export const WithEditAndDeleteModals: Story = { + render: () => { + const box = element('gd-box', { styles: { height: '100vh' } }); + const menu = menuExample({}, 'Menu', ['Edit', 'Delete', 'View']); + // Until Modal is ported, use its shared tokens with native dialog semantics. + const dialog = document.createElement('dialog'); + dialog.className = 'story-menu-dialog'; + const tokens = resolveThemeTree(defaultTheme.modal, defaultTheme) as unknown as Record< + string, + { default: ButtonCssBlock } + >; + const style = document.createElement('style'); + style.textContent = + '.story-menu-dialog{border:0;margin:auto}.story-menu-dialog:not([open]){display:none}' + + buttonCssBlockToText('.story-menu-dialog[open]', tokens.content.default) + + buttonCssBlockToText('.story-menu-dialog::backdrop', { background: tokens.overlay.default.background }) + + buttonCssBlockToText('.story-menu-dialog-body', tokens.body.default) + + buttonCssBlockToText('.story-menu-dialog-footer', tokens.footer.default); + const body = document.createElement('div'); + body.className = 'story-menu-dialog-body'; + const footer = document.createElement('div'); + footer.className = 'story-menu-dialog-footer'; + const text = element('gd-typography', {}); + const close = element('gd-button', { variant: 'text' }); + close.addEventListener('click', () => dialog.close()); + body.append(text); + footer.append(close); + dialog.append(body, footer); + dialog.addEventListener('click', (event) => { + const bounds = dialog.getBoundingClientRect(); + if ( + event.target === dialog && + (event.clientX < bounds.left || + event.clientX > bounds.right || + event.clientY < bounds.top || + event.clientY > bounds.bottom) + ) + dialog.close(); + }); + menu.addEventListener('gd-change', (event) => { + const { data } = (event as CustomEvent<{ data: { name: string; value: string } }>).detail; + if (!['edit', 'delete'].includes(data.value)) return; + text.textContent = + data.value === 'edit' ? `Edit modal for item: ${data.name}` : `Are you sure you want to delete: ${data.name}?`; + close.textContent = data.name; + dialog.showModal(); + }); + box.append(style, menu, dialog); + return box; }, }; -export const WithOffset: Story = { args: { offsetX: 20, offsetY: 16 } }; -export const WithHeightConstraints: Story = { args: { minHeight: 120, maxHeight: 160 } }; -export const CloseOnSelectFalse: Story = KeepOpenOnSelect; -export const WithRefControl: Story = Open; -export const WithPositioningOptions: Story = Placement; -export const WithEditAndDeleteModals: Story = Default; -export const DefaultTokens: Story = { render: () => defaultTokenViewer('menu') }; +export const DefaultTokens: Story = { parameters: { layout: 'padded' }, render: () => defaultTokenViewer('menu') }; diff --git a/libs/web-components/stories/Overview.stories.ts b/libs/web-components/stories/Overview.stories.ts index 0478c7f..d132ee2 100644 --- a/libs/web-components/stories/Overview.stories.ts +++ b/libs/web-components/stories/Overview.stories.ts @@ -23,10 +23,9 @@ const meta = {

Using Web Components

- GridKit Web Components are framework-independent custom HTML elements built with Lit. They expose the - same design tokens, variants, states, and interaction patterns as the React components while using - standard browser APIs. Use them in plain HTML or inside Angular, Vue, React, and other web frameworks - without a framework-specific GridKit adapter. + GridKit Web Components are framework-independent custom HTML elements built with Lit. They provide + GridKit design tokens, variants, states, and interaction patterns through standard browser APIs. Use + them in plain HTML or inside any web framework without a framework-specific GridKit adapter.

Component names start with gd-, for example <gd-button>, diff --git a/libs/web-components/stories/Select.stories.ts b/libs/web-components/stories/Select.stories.ts index aa6d3de..d52786b 100644 --- a/libs/web-components/stories/Select.stories.ts +++ b/libs/web-components/stories/Select.stories.ts @@ -1,111 +1,227 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdSelect } from '../src'; -import { defaultTokenViewer, element, observed, items, sectionedArgTypes } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes, stack } from './helpers'; -type Args = Pick< - GdSelect, - | 'items' - | 'value' - | 'disabled' - | 'color' - | 'placeholder' - | 'multiple' - | 'searchable' - | 'searchPlaceholder' - | 'autoOpen' - | 'dropdownMaxHeight' - | 'width' - | 'minWidth' - | 'maxWidth' ->; -const colors = ['primary', 'success', 'warning', 'error'] as const; +type Args = Partial; +const items = [ + { name: 'Option 1', value: 'option1' }, + { name: 'Option 2', value: { test: 'option2' } }, + { name: 'Option 3', value: 'option3' }, +]; +const small = (text: string) => element('gd-typography', { as: 'div', variant: 'small' }, text); +function frame(...children: Node[]) { + const root = stack('column', '10px', ...children); + root.style.cssText += ';width:100%;min-width:306px;margin:0 auto'; + return root; +} +function select(args: Args = {}) { + return element('gd-select', { items, ...args }); +} const meta = { title: 'Atoms/Select', tags: ['autodocs'], + argTypes: sectionedArgTypes('Select', {}), parameters: { + layout: 'centered', docs: { description: { - component: - 'Select matching React single/multiple selection, adornments, custom initiator, refs, custom identifiers, validation colors, search, disabled/empty states, and accessibility.', + component: 'Selection examples with custom content, imperative controls, disabled and searchable states.', }, }, }, - args: { - items, - value: null, - disabled: false, - color: 'primary', - placeholder: 'Choose an option', - multiple: false, - autoOpen: true, - dropdownMaxHeight: '240px', - width: '280px', - minWidth: undefined, - maxWidth: 'initial', - }, - argTypes: sectionedArgTypes('Select', { color: { control: 'select', options: colors } }), - render: (args: Args) => { - const select = element('gd-select', args); - select.addEventListener('gd-change', (event) => { - select.value = (event as CustomEvent<{ value: GdSelect['value'] }>).detail.value; - }); - return observed(select, 'gd-change', { value: args.value }); - }, + render: (args: Args) => frame(select({ maxWidth: '400px', ...args })), } satisfies Meta; export default meta; type Story = StoryObj; export const Default: Story = {}; -export const Selected: Story = { args: { value: items[1] } }; -export const Disabled: Story = { args: { disabled: true, value: items[0] } }; -export const Empty: Story = { args: { items: [] } }; -export const Multiple: Story = { args: { multiple: true, value: [items[0], items[2]] } }; -export const Searchable: Story = { args: { searchable: true, searchPlaceholder: 'Search options' } }; -export const AutoOpen: Story = { args: { autoOpen: false }, name: 'Manual open (autoOpen=false)' }; -export const ValidationColors: Story = { +export const MultipleSelect: Story = { render: (args) => { - const section = document.createElement('section'); - section.style.cssText = 'display:grid;gap:16px;width:280px'; - for (const color of colors) section.append(element('gd-select', { ...args, color, value: items[0] })); - return section; + const control = select({ + ...args, + multiple: true, + value: [], + placeholder: 'Select multiple options', + itemStringifier: (option) => + `${option.name} (${typeof option.value === 'object' ? JSON.stringify(option.value) : option.value})`, + }); + control.addEventListener('gd-change', () => { + const selected = control.value as typeof items; + control.title = selected.length > 1 ? selected.map(control.itemStringifier).join(', ') : ''; + }); + const root = frame(control); + root.style.width = '306px'; + return root; }, }; -export const WithAdornments: Story = { +export const WithAdornmentsAndResetOptionStory: Story = { render: (args) => { - const select = element('gd-select', args); + const control = select({ ...args, items: [{ name: 'Reset', value: null }, ...items] }); const start = element('gd-icon', { name: 'folder', fill: "green['50']" }); start.slot = 'adornment-start'; - const end = document.createElement('span'); + const end = element('gd-typography', { variant: 'small', color: 'icon.success' }, '(k)'); end.slot = 'adornment-end'; - end.textContent = '(k)'; - select.append(start, end); - return select; + control.append(start, end); + const output = small('Selected value: null'); + control.addEventListener('gd-change', () => { + output.textContent = `Selected value: ${JSON.stringify((control.value as { value: unknown } | null)?.value ?? null)}`; + }); + return frame(control, output); }, }; -export const CustomInitiator: Story = { +export const WithCustomInitiatorAndSelectedOutputValue: Story = { render: (args) => { - const select = element('gd-select', args); - const initiator = document.createElement('span'); - initiator.slot = 'initiator'; - initiator.textContent = 'Custom trigger content'; - select.append(initiator); - return select; + const control = select(args); + const input = element('gd-input', { placeholder: 'Choose an option', value: 'Choose an option', readOnly: true }); + input.slot = 'initiator'; + for (const [slot, text, padding] of [ + ['adornment-start', '€', '0 0 0 10px'], + ['adornment-end', '.00', '0 10px 0 0'], + ]) { + const node = document.createElement('div'); + node.slot = slot; + node.textContent = text; + node.style.cssText = `padding:${padding};display:flex;color:#838383`; + input.append(node); + } + control.append(input); + const output = small('Output: null'); + control.addEventListener('gd-change', () => { + input.value = (control.value as { name: string } | null)?.name ?? 'Select an item'; + output.textContent = `Output: ${JSON.stringify(control.value)}`; + }); + return frame(control, output); + }, +}; +export const UsingRefComponentWithCustomPlaceholder: Story = { + render: (args) => { + const control = select({ placeholder: 'Placeholder', ...args }); + const close = document.createElement('button'); + close.textContent = 'Close'; + close.onclick = () => control.close(); + const open = document.createElement('button'); + open.textContent = 'Open'; + open.onclick = () => control.open(); + return frame(close, control, open); }, }; -export const MultipleSelect: Story = Multiple; -export const WithAdornmentsAndResetOptionStory: Story = WithAdornments; -export const WithCustomInitiatorAndSelectedOutputValue: Story = CustomInitiator; -export const UsingRefComponentWithCustomPlaceholder: Story = { args: { placeholder: 'Choose through component API' } }; export const WithFileInputAndCustomDropdown: Story = { - ...Default, - parameters: { - docs: { description: { story: 'Native slots and item rendering provide the custom-dropdown composition point.' } }, + name: 'With File Input And Custom Dropdown', + render: (args) => { + const control = select({ ...args, items: [] }); + const content = stack('column', '0'); + content.slot = 'empty'; + const file = element('gd-input-file', { isIcon: true, buttonVariant: 'inherit' }, 'Select file'); + file.prepend(element('gd-icon', { name: 'attachment' })); + file.addEventListener('gd-change', (event) => { + event.stopPropagation(); + const files = (event as CustomEvent<{ files: { name: string }[] }>).detail.files; + control.value = { name: files[0]?.name ?? 'No file selected', value: { id: 1 } }; + control.close(); + }); + const option = element('gd-button', { variant: 'inherit' }, 'Custom Opt 1'); + option.prepend( + element('gd-image', { + src: 'https://picsum.photos/100/100', + alt: 'Custom Option 2', + width: 20, + placeholder: '...', + }) + ); + option.onclick = () => { + control.value = { name: 'Custom Opt 1', value: { id: 2 } }; + control.close(); + }; + const disabled = element('gd-button', { variant: 'inherit', disabled: true }, 'Disabled Option'); + disabled.prepend( + element('gd-image', { + src: 'https://picsum.photos/100/100', + alt: 'Custom Option 4', + width: 20, + placeholder: '...', + }) + ); + content.append(file, option, disabled); + control.append(content); + return frame(control); }, }; -export const CustomItemIdentifierComponent: Story = { args: { value: items[0] } }; -export const WithAccessibility: Story = { args: { placeholder: 'Choose an accessible option' } }; -export const EmptyItems: Story = Empty; -export const ColorPrimary: Story = { args: { color: 'primary' } }; -export const ColorSuccess: Story = { args: { color: 'success' } }; -export const ColorWarning: Story = { args: { color: 'warning' } }; -export const ColorError: Story = { args: { color: 'error' } }; -export const DefaultTokens: Story = { render: () => defaultTokenViewer('select') }; +export const CustomItemIdentifierComponent: Story = { + render: (args) => { + const objects = [ + { value: 1, name: 'Object 1' }, + { + value: 2, + name: 'The Select component is a versatile dropdown selector that provides various interaction patterns and customization options. It supports both simple selection scenarios ', + }, + { value: 3, name: 'Object 3' }, + ]; + const control = select({ + ...args, + items: objects.map((item) => ({ name: item.name, value: item })), + placeholder: 'Select an item', + itemIdentifier: (selected, current) => + (selected?.value as { value?: number })?.value === (current?.value as { value?: number })?.value, + renderOption: ({ item }) => `ID: ${(item.value as { value: number }).value} - ${item.name}`, + }); + const output = small('Selected value: null'); + control.addEventListener('gd-change', () => { + output.textContent = `Selected value: ${JSON.stringify(control.value)}`; + }); + const root = frame(control, frame(output)); + root.style.width = '306px'; + return root; + }, +}; +export const WithAccessibility: Story = { + render: (args) => frame(select({ placeholder: 'Select an option', ...args })), +}; +export const Disabled: Story = { + render: (args) => + frame( + select({ ...args, disabled: true, placeholder: 'Select an option (disabled)' }), + small('This Select is disabled and cannot be interacted with. Click events and keyboard navigation are blocked.') + ), +}; +export const EmptyItems: Story = { + render: (args) => { + const control = select({ ...args, items: [], placeholder: 'No options available' }); + const empty = small('No items available. Please add items to the list.'); + empty.slot = 'empty'; + empty.style.padding = '16px'; + control.append(empty); + return frame( + control, + small( + 'This Select has an empty items array. The `emptyItemsResult` prop provides custom content to display when no items are available, offering better user guidance than an empty dropdown.' + ) + ); + }, +}; +export const ColorPrimary: Story = { + render: (args) => frame(select({ color: 'primary', placeholder: 'Primary color select', ...args })), +}; +export const ColorSuccess: Story = { + render: (args) => frame(select({ color: 'success', placeholder: 'Success color select', ...args })), +}; +export const ColorWarning: Story = { + render: (args) => frame(select({ color: 'warning', placeholder: 'Warning color select', ...args })), +}; +export const ColorError: Story = { + render: (args) => frame(select({ color: 'error', placeholder: 'Error color select', ...args })), +}; +export const Searchable: Story = { + render: (args) => + frame( + select({ + items: ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape', 'Honeydew'].map((name) => ({ + name, + value: name.toLowerCase(), + })), + searchable: true, + searchPlaceholder: 'Search fruits...', + placeholder: 'Select a fruit', + ...args, + }) + ), +}; +export const DefaultTokens: Story = { parameters: { layout: 'padded' }, render: () => defaultTokenViewer('select') }; diff --git a/libs/web-components/stories/Separator.stories.ts b/libs/web-components/stories/Separator.stories.ts index 88be798..41320a9 100644 --- a/libs/web-components/stories/Separator.stories.ts +++ b/libs/web-components/stories/Separator.stories.ts @@ -80,14 +80,22 @@ export const WithLabel: Story = { return wrapper; }, }; +function cardExample(index: number) { + const card = element('gd-box', { isBordered: true, styles: { padding: '20px' } }); + card.append( + element('gd-typography', { variant: 'h5', styles: { margin: '0', padding: '0' } }, `Title ${index}`), + element('gd-typography', { variant: 'p', styles: { margin: '0', padding: '0' } }, `Description ${index}`) + ); + return card; +} export const Vertical: Story = { render: () => { const wrapper = document.createElement('div'); wrapper.style.cssText = 'display:flex;gap:20px;align-items:center'; wrapper.append( - 'Title 1 — Description 1', + cardExample(1), element('gd-separator', { size: 'md', length: '200px', variant: 'solid', orientation: 'vertical' }), - 'Title 2 — Description 2' + cardExample(2) ); return wrapper; }, @@ -97,9 +105,9 @@ export const VerticalWithLabel: Story = { const wrapper = document.createElement('div'); wrapper.style.cssText = 'display:flex;gap:20px;align-items:center'; wrapper.append( - 'Title 1 — Description 1', + cardExample(1), element('gd-separator', { size: 'md', length: '200px', variant: 'solid', orientation: 'vertical', label: 'Or' }), - 'Title 2 — Description 2' + cardExample(2) ); return wrapper; }, diff --git a/libs/web-components/stories/Skeleton.stories.ts b/libs/web-components/stories/Skeleton.stories.ts index f7c0be9..c61a01d 100644 --- a/libs/web-components/stories/Skeleton.stories.ts +++ b/libs/web-components/stories/Skeleton.stories.ts @@ -1,93 +1,31 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdSkeleton } from '../src'; -import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/Skeleton/Skeleton.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Skeleton.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick< - GdSkeleton, - 'variant' | 'width' | 'height' | 'backgroundColor' | 'animationName' | 'animationProps' | 'styles' ->; +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Skeleton', {})); const meta = { title: 'Atoms/Skeleton', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { component: - 'A theme-aware animated loading placeholder with rounded, rectangular, and circular shapes, flexible dimensions, custom colors and animations, and slotted child content.', + 'Loading placeholder with rounded, rectangular and circular variants, theme colors, child content, composed layouts, and accessibility semantics.', }, }, }, - args: { width: '250px', height: '15px', variant: 'rounded', animationName: 'blinkKeyframes' }, - argTypes: sectionedArgTypes('Skeleton', { - variant: { - description: 'Controls the shape of the skeleton.', - control: 'select', - options: ['rounded', 'rectangular', 'circular'], - }, - width: { description: 'Sets the width using any valid CSS width.', control: 'text' }, - height: { description: 'Sets the height using any valid CSS height.', control: 'text' }, - backgroundColor: { description: 'A CSS color or GridKit theme/palette color path.', control: 'text' }, - animationName: { - description: 'A theme animation token, raw CSS animation name, or null to disable animation.', - control: 'text', - }, - animationProps: { description: 'Animation duration, easing, delay, and iteration string.', control: 'text' }, - styles: { description: 'Custom CSS properties applied to the skeleton.', control: 'object' }, - }), - render: (args: Args) => element('gd-skeleton', args), -} satisfies Meta; -export default meta; -type Story = StoryObj; - -export const Default: Story = { - parameters: { docs: { description: { story: 'The standard rounded skeleton for text-line placeholders.' } } }, -}; -export const Circular: Story = { - args: { width: '80px', height: '80px', variant: 'circular' }, - parameters: { docs: { description: { story: 'A circular placeholder for avatars and icons.' } } }, -}; -export const Rectangular: Story = { - args: { width: '250px', height: '125px', variant: 'rectangular' }, - parameters: { docs: { description: { story: 'A rectangular placeholder for images, cards, and video.' } } }, -}; -export const WithThemeColor: Story = { - args: { width: '250px', height: '40px', variant: 'rectangular', backgroundColor: 'theme.palette.success.main' }, - parameters: { docs: { description: { story: 'Uses the top-level theme-aware backgroundColor property.' } } }, -}; -export const WithCustomAnimation: Story = { - name: 'Animation with Tailwind Class', - args: { width: '250px', height: '15px', animationName: null }, - parameters: { - docs: { description: { story: 'Disables the built-in animation so a consumer class can supply one.' } }, - }, -}; -export const WithChildren: Story = { - name: 'Skeleton with Child Content', - render: (args) => element('gd-skeleton', { ...args, width: '250px', height: '50px' }, 'Loading Content...'), - parameters: { - docs: { description: { story: 'The default slot preserves child content inside the animated wrapper.' } }, - }, -}; -export const ComposedLayout: Story = { - name: 'Example: Article Placeholder', - render: () => { - const wrapper = document.createElement('div'); - wrapper.style.cssText = 'display:grid;grid-template-columns:100px 1fr;gap:10px;max-width:480px'; - wrapper.append(element('gd-skeleton', { variant: 'circular', width: '100px', height: '100px' })); - const lines = document.createElement('div'); - lines.style.cssText = 'display:grid;gap:10px'; - lines.append(element('gd-skeleton'), element('gd-skeleton'), element('gd-skeleton')); - wrapper.append(lines); - return wrapper; - }, - parameters: { docs: { description: { story: 'Composes multiple skeletons as an article or profile placeholder.' } } }, -}; -export const WithAccessibility: Story = { - args: { 'aria-label': 'Loading content' } as never, - parameters: { a11y: { test: 'error' }, docs: { disable: true } }, - tags: ['a11y'], -}; -export const DefaultTokens: Story = { - render: () => defaultTokenViewer('skeleton'), - parameters: { layout: 'padded' }, + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; +export default meta; +export const Default = { ...nativeStory(fixtures.Default, meta) }; +export const Circular = { ...nativeStory(fixtures.Circular, meta) }; +export const Rectangular = { ...nativeStory(fixtures.Rectangular, meta) }; +export const WithThemeColor = { ...nativeStory(fixtures.WithThemeColor, meta) }; +export const WithChildren = { ...nativeStory(fixtures.WithChildren, meta), name: 'Skeleton with Child Content' }; +export const ComposedLayout = { ...nativeStory(fixtures.ComposedLayout, meta), name: 'Example: Article Placeholder' }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Slider.stories.ts b/libs/web-components/stories/Slider.stories.ts index e8c6803..9969e2f 100644 --- a/libs/web-components/stories/Slider.stories.ts +++ b/libs/web-components/stories/Slider.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdSlider } from '../src'; -import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -24,7 +24,7 @@ const meta = { disabled: { description: 'Whether the slider is disabled', control: 'boolean' }, styles: { description: 'Custom slider style overrides', control: 'object' }, }), - render: (args: Args) => observed(element('gd-slider', args), 'gd-change', { value: args.value }), + render: (args: Args) => element('gd-slider', args), } satisfies Meta; export default meta; type Story = StoryObj; @@ -38,7 +38,7 @@ export const Controlled: Story = { render: (args) => { const slider = element('gd-slider', { ...args, value: 25 }); slider.addEventListener('gd-change', (e) => (slider.value = (e as CustomEvent<{ value: number }>).detail.value)); - return observed(slider, 'gd-change', { value: 25 }); + return slider; }, parameters: { docs: { description: { story: 'The value property is updated externally from each gd-change event.' } }, diff --git a/libs/web-components/stories/SliderDots.stories.ts b/libs/web-components/stories/SliderDots.stories.ts index 9b226a0..9c15715 100644 --- a/libs/web-components/stories/SliderDots.stories.ts +++ b/libs/web-components/stories/SliderDots.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdSliderDots } from '../src'; -import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; +import { defaultTokenViewer, element, stack, sectionedArgTypes } from './helpers'; type Args = Pick; const meta = { @@ -21,7 +21,7 @@ const meta = { activeIndex: { description: 'Zero-based index of the active dot', control: 'number' }, styles: { description: 'Custom container styles', control: 'object' }, }), - render: (args: Args) => observed(element('gd-slider-dots', args), 'gd-change', { index: args.activeIndex }), + render: (args: Args) => element('gd-slider-dots', args), } satisfies Meta; export default meta; type Story = StoryObj; @@ -31,14 +31,14 @@ export const Default: Story = { export const Interactive: Story = { render: () => { const dots = element('gd-slider-dots', { count: 5 }); - const output = document.createElement('output'); + const output = element('gd-typography', {}); output.textContent = 'Active slide: 1 of 5'; - dots.addEventListener( - 'gd-change', - (e) => (output.textContent = `Active slide: ${(e as CustomEvent<{ index: number }>).detail.index + 1} of 5`) - ); - const section = document.createElement('section'); - section.append(output, dots); + dots.addEventListener('gd-change', (e) => { + dots.activeIndex = (e as CustomEvent<{ index: number }>).detail.index; + output.textContent = `Active slide: ${dots.activeIndex + 1} of 5`; + }); + const section = stack('column', '16px', output, dots); + section.style.alignItems = 'center'; return section; }, parameters: { docs: { description: { story: 'Clicking a dot updates the active slide output.' } } }, diff --git a/libs/web-components/stories/Switch.stories.ts b/libs/web-components/stories/Switch.stories.ts index 5d5cef2..91cf1d2 100644 --- a/libs/web-components/stories/Switch.stories.ts +++ b/libs/web-components/stories/Switch.stories.ts @@ -1,8 +1,8 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdSwitch } from '../src'; -import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; +import { defaultTokenViewer, element, stack, sectionedArgTypes } from './helpers'; -type Args = Pick; +type Args = Pick & { children: string }; const meta = { title: 'Atoms/Switch', tags: ['autodocs'], @@ -15,7 +15,7 @@ const meta = { }, }, }, - args: { name: 'switch', checked: false, disabled: false, isLoading: false, label: 'right' }, + args: { name: 'switch', checked: false, disabled: false, isLoading: false, label: 'right', children: 'Label' }, argTypes: sectionedArgTypes('Switch', { checked: { description: 'Checked state', control: 'boolean' }, disabled: { description: 'Disables interaction', control: 'boolean' }, @@ -24,7 +24,7 @@ const meta = { name: { description: 'Native checkbox name', control: 'text' }, styles: { description: 'Custom wrapper styles', control: 'object' }, }), - render: (args: Args) => observed(element('gd-switch', args, 'Label'), 'gd-change', { checked: args.checked }), + render: ({ children, ...args }: Args) => element('gd-switch', args, children), } satisfies Meta; export default meta; type Story = StoryObj; @@ -32,31 +32,52 @@ export const Default: Story = {}; export const Checked: Story = { args: { checked: true } }; export const Disabled: Story = { args: { disabled: true } }; export const LabelLeft: Story = { - args: { label: 'left' }, - render: (args) => element('gd-switch', args, 'Label on the left'), -}; -export const Controlled: Story = { - render: () => { - const sw = element('gd-switch', { checked: false }, 'Controlled Switch, with 1.5 seconds delay'); - sw.addEventListener('gd-change', (e) => - setTimeout(() => (sw.checked = (e as CustomEvent<{ checked: boolean }>).detail.checked), 1500) - ); - return observed(sw, 'gd-change', { checked: false }); - }, - parameters: { docs: { description: { story: 'Controlled state is written back externally after a delay.' } } }, + args: { label: 'left', children: 'Label on the left' }, }; +function controlledSwitch(delay: number, withLoading = false) { + const state = element('gd-typography', {}, 'Current state: OFF'); + const loading = element('gd-typography', {}, 'Loading: No'); + const control = element( + 'gd-switch', + { checked: false }, + withLoading ? 'Switch with Loading State (3 seconds)' : 'Controlled Switch, with 1.5 seconds delay' + ); + let checked = false; + control.addEventListener('gd-change', (event) => { + const next = (event as CustomEvent<{ checked: boolean }>).detail.checked; + control.checked = checked; + if (withLoading) { + control.isLoading = true; + loading.textContent = 'Loading: Yes (3 seconds)'; + } + setTimeout(() => { + if (!control.isConnected) return; + checked = next; + control.checked = next; + control.isLoading = false; + state.textContent = `Current state: ${next ? 'ON' : 'OFF'}`; + loading.textContent = 'Loading: No'; + }, delay); + }); + return stack('column', '16px', state, ...(withLoading ? [loading] : []), control); +} +export const Controlled: Story = { render: () => controlledSwitch(1500) }; export const Uncontrolled: Story = { - render: () => observed(element('gd-switch', {}, 'Uncontrolled Switch'), 'gd-change', { checked: false }), - parameters: { docs: { description: { story: 'The element manages its checked state and emits each change.' } } }, -}; -export const WithLoading: Story = { - args: { isLoading: true }, - render: (args) => element('gd-switch', args, 'Switch with Loading State (3 seconds)'), - parameters: { docs: { description: { story: 'Loading composes gd-loader and disables the native checkbox.' } } }, + render: () => + stack( + 'column', + '16px', + element( + 'gd-typography', + {}, + 'Uncontrolled Switch - state is managed internally. Check the Actions panel to see onChange events.' + ), + element('gd-switch', {}, 'Uncontrolled Switch') + ), }; +export const WithLoading: Story = { render: () => controlledSwitch(3000, true) }; export const WithAccessibility: Story = { - args: { name: 'notifications' }, - render: (args) => element('gd-switch', args, 'Enable notifications'), + args: { name: 'notifications', children: 'Enable notifications' }, parameters: { a11y: { test: 'error' }, docs: { disable: true } }, tags: ['a11y'], }; diff --git a/libs/web-components/stories/Textarea.stories.ts b/libs/web-components/stories/Textarea.stories.ts index 9108cf7..43b26bb 100644 --- a/libs/web-components/stories/Textarea.stories.ts +++ b/libs/web-components/stories/Textarea.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GdTextarea } from '../src'; -import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick< GdTextarea, @@ -66,8 +66,7 @@ const meta = { maxCharacters: { description: 'Visual character counter threshold', control: 'number' }, styles: { description: 'Custom textarea styles', control: 'object' }, }), - render: (args: Args) => - observed(element('gd-textarea', args), 'gd-input', { value: args.value ?? args.defaultValue ?? '' }), + render: (args: Args) => element('gd-textarea', args), } satisfies Meta; export default meta; type Story = StoryObj; @@ -103,7 +102,11 @@ export const ColorError: Story = { }; export const AutoFocus: Story = { args: { autoFocus: true, placeholder: 'This will be focused on load' } }; export const DynamicHeight: Story = { - args: { dynamicHeightAdjustment: true, placeholder: 'Start typing to expand... Start typing to expand...' }, + args: { + dynamicHeightAdjustment: true, + placeholder: + 'Start typing to expand... Start typing to expand... Start typing to expand... Start typing to expand...', + }, }; export const WithCharLimit: Story = { args: { maxLength: 50, placeholder: 'Max 50 characters...' } }; export const Resizable: Story = { args: { placeholder: 'Resizable Textarea placeholder text...', resize: 'both' } }; diff --git a/libs/web-components/stories/Toggle.stories.ts b/libs/web-components/stories/Toggle.stories.ts index 061118a..9336a11 100644 --- a/libs/web-components/stories/Toggle.stories.ts +++ b/libs/web-components/stories/Toggle.stories.ts @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite'; import type { GridKitIconName } from 'gd-design-core'; import type { GdToggle } from '../src'; -import { defaultTokenViewer, element, observed, sectionedArgTypes } from './helpers'; +import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; type Args = Pick; const items = ['Option 1', 'Option 2', 'Option 3']; @@ -25,7 +25,7 @@ const meta = { renderItemContent: { description: 'Optional item render function', control: false }, styles: { description: 'Custom group styles', control: 'object' }, }), - render: (args: Args) => observed(element('gd-toggle', args), 'gd-change', { value: args.value }), + render: (args: Args) => element('gd-toggle', args), } satisfies Meta; export default meta; type Story = StoryObj; @@ -49,7 +49,20 @@ export const WithCustomRender: Story = { parameters: { docs: { description: { story: 'Object items can supply values and consumer-rendered content.' } } }, }; export const WithAccessibility: Story = { - args: { 'aria-label': 'Toggle options' } as never, + render: (args) => { + const root = document.createElement('div'); + const label = document.createElement('label'); + label.textContent = 'Select an option:'; + label.htmlFor = 'toggle-group'; + label.style.cssText = 'display:block;margin-bottom:10px;font-weight:bold'; + const group = document.createElement('div'); + group.id = 'toggle-group'; + group.setAttribute('role', 'group'); + group.setAttribute('aria-label', 'Toggle options'); + group.append(element('gd-toggle', args)); + root.append(label, group); + return root; + }, parameters: { a11y: { test: 'error' }, docs: { disable: true } }, tags: ['a11y'], }; diff --git a/libs/web-components/stories/Truncate.stories.ts b/libs/web-components/stories/Truncate.stories.ts index a88efd2..fafb554 100644 --- a/libs/web-components/stories/Truncate.stories.ts +++ b/libs/web-components/stories/Truncate.stories.ts @@ -4,7 +4,7 @@ import { element, sectionedArgTypes } from './helpers'; type Args = Pick & { children: string }; const long = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'; const meta = { title: 'Atoms/Truncate', tags: ['autodocs'], @@ -24,8 +24,7 @@ const meta = { styles: { description: 'Custom content styles', control: 'object' }, }), render: ({ children, ...args }: Args) => { - const box = document.createElement('div'); - box.style.width = '240px'; + const box = element('gd-box', { styles: { width: '50%', margin: '0 auto' } }); box.append(element('gd-truncate', args, children)); return box; }, @@ -36,29 +35,51 @@ export const Default: Story = { parameters: { docs: { description: { story: 'One-line truncation within a constrained container.' } } }, }; export const LineTruncation: Story = { - args: { lines: 2, children: 'This is a very long text that will wrap to multiple lines' }, + args: { lines: 2, children: 'This is a very long text that will wrap to multiple ' }, parameters: { docs: { description: { story: 'CSS line-clamp limits content to two lines.' } } }, }; export const WithCustomStyling: Story = { args: { styles: { color: '#dc2626', fontWeight: 600, fontSize: '18px' }, - children: 'This text has custom styling applied via the styles property.', + children: + 'This text has custom styling applied via the styles prop. Lorem ipsum dolor sit amet, consectetur adipiscing elit.', }, }; export const RefAPIWithTooltipOnOverflow: Story = { render: () => { - const section = document.createElement('section'); - section.style.width = '220px'; - const output = document.createElement('output'); + const section = element('gd-box', { styles: { width: '100%', margin: '0 auto' } }); const value = element( 'gd-truncate', { lines: 1 }, - '(Resize window): Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + '(Resize window): Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt' ); - const update = () => (output.textContent = `isTruncated: ${value.isTruncated}`); - new ResizeObserver(update).observe(value); - setTimeout(update); - section.append(value, output); + const stateBox = element('gd-box', { + styles: { padding: '12px', backgroundColor: '#f0f0f0', borderRadius: '4px' }, + }); + const text = element('gd-typography', { variant: 'small' }); + const output = document.createElement('span'); + text.append( + element('gd-typography', { variant: 'strong' }, 'Ref API State:'), + document.createElement('br'), + output + ); + stateBox.append(text); + const update = () => { + output.textContent = `isTruncated: ${value.isTruncated}`; + value.title = value.isTruncated ? 'Content is truncated' : ''; + }; + const observer = new ResizeObserver(() => { + if (!value.isConnected) { + observer.disconnect(); + return; + } + update(); + }); + requestAnimationFrame(() => { + observer.observe(value); + update(); + }); + section.append(value, stateBox); return section; }, parameters: { @@ -66,7 +87,7 @@ export const RefAPIWithTooltipOnOverflow: Story = { }, }; export const WithAccessibility: Story = { - args: { 'aria-label': 'Truncated summary' } as never, + ...Default, parameters: { a11y: { test: 'error' }, docs: { disable: true } }, tags: ['a11y'], }; diff --git a/libs/web-components/stories/Typography.stories.ts b/libs/web-components/stories/Typography.stories.ts index 4a3f7ce..da95aa5 100644 --- a/libs/web-components/stories/Typography.stories.ts +++ b/libs/web-components/stories/Typography.stories.ts @@ -1,136 +1,33 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdTypography } from '../src'; -import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/Typography/Typography.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Typography.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick & { children: string }; -const variants = [ - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'p', - 'small', - 'div', - 'span', - 'strong', - 'i', - 'code', - 'kbd', - 'caption', - 'header', - 'sup', - 'sub', -] as const; -const styleVariants = [ - 'light', - 'normal', - 'semibold', - 'bold', - 'italic', - 'small', - 'uppercase', - 'lowercase', - 'underline', - 'strike', -] as const; -const typographyElement = ({ children, ...props }: Args, text = children) => element('gd-typography', props, text); +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Typography', {})); const meta = { title: 'Atoms/Typography', tags: ['autodocs'], + args: sourceMeta.args, parameters: { + layout: 'centered', docs: { description: { component: - 'Typography matching React semantic overrides, combined style variants, disclaimer, display, heading, body, color, alignment, and accessibility states.', + 'Typography with semantic overrides, combined style variants, disclaimer, display, heading, body, color, alignment, and accessibility options.', }, }, }, - args: { - variant: 'p', - as: 'p', - size: 'md', - align: 'start', - color: '', - styleVariant: [], - children: 'Body paragraph text for comparison.', - }, - argTypes: sectionedArgTypes('Typography', { - variant: { control: 'select', options: variants }, - as: { control: 'select', options: variants }, - size: { control: 'select', options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] }, - align: { control: 'select', options: ['start', 'end', 'center', 'left', 'right', 'justify'] }, - color: { control: 'color' }, - styleVariant: { control: 'multi-select', options: styleVariants }, - }), - render: (args: Args) => typographyElement(args), -} satisfies Meta; -export default meta; -type Story = StoryObj; -export const Default: Story = {}; -export const Heading1: Story = { args: { variant: 'h1', as: 'h1', children: 'Heading 1' } }; -export const Heading2: Story = { args: { variant: 'h2', as: 'h2', children: 'Heading 2' } }; -export const AllVariants: Story = { - render: (args) => { - const section = document.createElement('section'); - section.style.cssText = 'display:grid;gap:8px'; - for (const variant of variants) - section.append(typographyElement({ ...args, variant, as: variant }, `${variant} — GridKit typography`)); - return section; - }, -}; -export const DisplaySizes: Story = { - render: (args) => { - const section = document.createElement('section'); - for (const size of ['xl', 'lg', 'md', 'sm', 'xs'] as const) - section.append(typographyElement({ ...args, variant: 'div', as: 'div', size }, `Display ${size}`)); - return section; - }, -}; -export const StyleVariants: Story = { - render: (args) => { - const section = document.createElement('section'); - section.style.cssText = 'display:grid;gap:8px'; - for (const styleVariant of styleVariants) - section.append(typographyElement({ ...args, variant: 'p', as: 'p', styleVariant }, `${styleVariant} text`)); - section.append( - typographyElement( - { ...args, variant: 'p', as: 'p', styleVariant: ['bold', 'italic', 'underline'] }, - 'Combined: bold + italic + underline' - ) - ); - return section; - }, + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; -export const SemanticOverride: Story = { - args: { variant: 'h2', as: 'h3', children: 'H2 appearance rendered as an H3' }, -}; -export const CodeAndKeyboard: Story = { - render: (args) => { - const section = document.createElement('section'); - section.style.cssText = 'display:grid;gap:12px'; - section.append(typographyElement({ ...args, variant: 'code', as: 'code' }, 'const gridkit = true;')); - section.append(typographyElement({ ...args, variant: 'kbd', as: 'kbd' }, '⌘ + K')); - return section; - }, -}; -export const CaptionAndHeader: Story = { - render: (args) => { - const section = document.createElement('section'); - section.append(typographyElement({ ...args, variant: 'caption', as: 'span' }, 'Caption text')); - section.append(typographyElement({ ...args, variant: 'header', as: 'header' }, 'Header text')); - return section; - }, -}; -export const AsCustomComponent: Story = SemanticOverride; -export const AsCustomHtmlTag: Story = SemanticOverride; -export const CombinedStyleVariant: Story = StyleVariants; -export const Disclaimers: Story = CaptionAndHeader; -export const Display: Story = DisplaySizes; -export const Heading: Story = AllVariants; -export const Body: Story = { args: { variant: 'p', as: 'p', children: 'Body paragraph text' } }; -export const WithAccessibility: Story = { - args: { variant: 'h2', as: 'h2', children: 'Accessible section heading' }, -}; -export const DefaultTokens: Story = { render: () => defaultTokenViewer('typography') }; +export default meta; +export const Default = { ...nativeStory(fixtures.Default, meta) }; +export const AsCustomComponent = { ...nativeStory(fixtures.AsCustomComponent, meta) }; +export const AsCustomHtmlTag = { ...nativeStory(fixtures.AsCustomHtmlTag, meta) }; +export const CombinedStyleVariant = { ...nativeStory(fixtures.CombinedStyleVariant, meta) }; +export const Disclaimers = { ...nativeStory(fixtures.Disclaimers, meta) }; +export const Display = { ...nativeStory(fixtures.Display, meta) }; +export const Heading = { ...nativeStory(fixtures.Heading, meta) }; +export const Body = { ...nativeStory(fixtures.Body, meta) }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Wrapper.stories.ts b/libs/web-components/stories/Wrapper.stories.ts index e75578c..e78d5eb 100644 --- a/libs/web-components/stories/Wrapper.stories.ts +++ b/libs/web-components/stories/Wrapper.stories.ts @@ -1,75 +1,30 @@ -import type { Meta, StoryObj } from '@storybook/web-components-vite'; -import type { GdWrapper } from '../src'; -import { defaultTokenViewer, element, sectionedArgTypes } from './helpers'; +// Generated from libs/ui/src/components/atoms/Wrapper/Wrapper.stories.tsx; edit the source example or native-story adapter. +import * as fixtures from './fixtures/Wrapper.generated.js'; +import { nativeMeta, nativeStory } from './native-story'; +import { sectionedArgTypes } from './helpers'; -type Args = Pick & { children: string }; +const sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('Wrapper', {})); const meta = { title: 'Atoms/Wrapper', tags: ['autodocs'], + args: sourceMeta.args, parameters: { layout: 'centered', docs: { description: { component: - 'A minimal token-backed layout wrapper with inline, section and full-page variants, semantic internal tag selection, slotted content, and style overrides.', + 'Layout wrapper with inline, section and full-page variants, semantic element selection, slotted content, and custom styling.', }, }, }, - args: { variant: 'inline', as: 'div', children: 'This is a basic wrapper.' }, - argTypes: sectionedArgTypes('Wrapper', { - variant: { description: 'Layout context', control: 'select', options: ['inline', 'section', 'fullPage'] }, - as: { description: 'Internal semantic HTML tag', control: 'text' }, - children: { description: 'Slotted content', control: 'text' }, - styles: { description: 'Custom wrapper styles', control: 'object' }, - }), - render: ({ children, ...args }: Args) => element('gd-wrapper', args, children), -} satisfies Meta; -export default meta; -type Story = StoryObj; -export const InteractiveExample: Story = { - name: 'Interactive Example', - args: { styles: { padding: '20px', border: '2px dashed #007bff', borderRadius: '8px', backgroundColor: '#f0f8ff' } }, -}; -export const SectionWrapper: Story = { - name: 'Variant: Section', - args: { - variant: 'section', - children: 'This content is semantically grouped within a section element.', - styles: { padding: '20px', width: '400px', border: '1px solid #ccc', backgroundColor: '#f9f9f9' }, - }, -}; -export const InlineWrapper: Story = { - name: 'Variant: Inline (Span)', - args: { - variant: 'inline', - children: 'this part is wrapped in a span', - styles: { - padding: '4px 8px', - borderRadius: '4px', - backgroundColor: '#fffbe6', - color: '#856404', - fontWeight: 'bold', - }, - }, -}; -export const FullPageWrapper: Story = { - name: 'Variant: Full Page', - args: { - variant: 'fullPage', - children: 'This wrapper covers the entire viewport.', - styles: { display: 'flex', alignItems: 'center', justifyContent: 'center' }, - }, - parameters: { layout: 'fullscreen' }, -}; -export const CustomTagWrapper: Story = { - name: 'Custom tag Wrapper', - args: { as: 'section', children: 'Custom tag Wrapper' }, -}; -export const WithAccessibility: Story = { - args: { 'aria-label': 'Content wrapper' } as never, - parameters: { a11y: { test: 'error' }, docs: { disable: true } }, - tags: ['a11y'], -}; -export const DefaultTokens: Story = { - render: () => defaultTokenViewer('wrapper'), + argTypes: sourceMeta.argTypes, + render: sourceMeta.render, }; +export default meta; +export const InteractiveExample = { ...nativeStory(fixtures.InteractiveExample, meta), name: 'Interactive Example' }; +export const SectionWrapper = { ...nativeStory(fixtures.SectionWrapper, meta), name: 'Variant: Section' }; +export const InlineWrapper = { ...nativeStory(fixtures.InlineWrapper, meta), name: 'Variant: Inline (Span)' }; +export const FullPageWrapper = { ...nativeStory(fixtures.FullPageWrapper, meta), name: 'Variant: Full Page' }; +export const CustomTagWrapper = { ...nativeStory(fixtures.CustomTagWrapper, meta), name: 'Custom tag Wrapper' }; +export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; +export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/fixtures/Avatar.generated.js b/libs/web-components/stories/fixtures/Avatar.generated.js new file mode 100644 index 0000000..a4ffc5f --- /dev/null +++ b/libs/web-components/stories/fixtures/Avatar.generated.js @@ -0,0 +1,149 @@ +// Generated from libs/ui/src/components/atoms/Avatar/Avatar.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'Avatar'; +const Avatar = 'gd-avatar'; +const Icon = 'gd-icon'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/Avatar', + component: Avatar, + args: { + backgroundColor: 'bg.default', + }, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: ` + The \`Avatar\` component displays a user's profile picture, initials, or a generic icon. It supports various sizes, a status badge, and a customizable fallback when an image is not available or fails to load. +
+
+

Key Features:

+
    +
  • + Display Options +
      +
    • Profile images
    • +
    • Fallback content (initials, icons)
    • +
    • Status badge integration
    • +
    +
  • +
  • + Size Variations +
      +
    • XS to XXL predefined sizes
    • +
    • Consistent scaling
    • +
    +
  • +
  • Accessibility – ARIA labels and alt text
  • +
  • Customization – Colors and fallback content
  • +
  • Error Handling – Graceful fallback display
  • +
+
+

Layout Props:

+
    +
  • Dimensions +
      +
    • Size variants control width/height
    • +
    • Aspect ratio maintained
    • +
    +
  • +
  • Styling +
      +
    • className: Custom CSS classes
    • +
    • styles: CSS-in-JS styling
    • +
    +
  • +
  • Position +
      +
    • Badge positioning
    • +
    • Content alignment
    • +
    +
  • +
+ `, + }, + }, + }, +}; +export default meta; +const Template = (args) => demo(Avatar, { ...args }); +const AllSizeTemplate = (args) => + demo( + 'div', + { style: { display: 'flex', gap: '20px', justifyItems: 'center', alignItems: 'center' } }, + demo(Avatar, { sizeVariant: 'xs', fallbackComponent: 'XS', ...args }), + demo(Avatar, { sizeVariant: 'sm', fallbackComponent: 'SM', ...args }), + demo(Avatar, { sizeVariant: 'md', fallbackComponent: 'MD', ...args }), + demo(Avatar, { sizeVariant: 'lg', fallbackComponent: 'LG', ...args }), + demo(Avatar, { sizeVariant: 'xl', fallbackComponent: 'XL', ...args }), + demo(Avatar, { sizeVariant: 'xxl', fallbackComponent: 'XXL', ...args }) + ); +export const Default = Template.bind({}); +Default.args = { + src: 'https://picsum.photos/200?random=1', + sizeVariant: 'xl', +}; +export const WithBadge = Template.bind({}); +WithBadge.args = { + sizeVariant: 'xl', + src: 'https://picsum.photos/200?random=2', + withBadge: true, +}; +export const WithCustomBadgeColor = Template.bind({}); +WithCustomBadgeColor.args = { + sizeVariant: 'xl', + src: 'https://picsum.photos/200?random=2', + withBadge: true, + badgeColor: '#901313', +}; +export const WithInitials = Template.bind({}); +WithInitials.args = { + sizeVariant: 'xl', + fallbackComponent: 'TG', +}; +export const WithCustomBackgroundColor = Template.bind({}); +WithCustomBackgroundColor.args = { + sizeVariant: 'xl', + fallbackComponent: 'TG', + backgroundColor: 'grey', +}; +export const WithIcon = Template.bind({}); +WithIcon.args = { + sizeVariant: 'xl', + fallbackComponent: demo(Icon, { name: 'star', width: 40, height: 40, fill: '#646464' }), + backgroundColor: '#E0E0E0', +}; +export const WithImageAndFallback = Template.bind({}); +WithImageAndFallback.args = { + sizeVariant: 'xl', + src: 'invalid_url', + fallbackComponent: 'NI', +}; +export const WithDifferentSize = AllSizeTemplate.bind({}); +WithDifferentSize.args = {}; +export const WithAccessibility = Template.bind({}); +WithAccessibility.args = { + src: 'https://picsum.photos/200?random=1', + sizeVariant: 'xl', + alt: 'User Profile: Name', + role: 'img', +}; +WithAccessibility.parameters = { + a11y: { + test: 'error', + }, + docs: { + disable: true, + }, +}; +WithAccessibility.tags = ['a11y']; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { avatar: defaultTheme.avatar } }); +DefaultTokens.parameters = { + layout: 'padded', +}; diff --git a/libs/web-components/stories/fixtures/Badge.generated.js b/libs/web-components/stories/fixtures/Badge.generated.js new file mode 100644 index 0000000..d822047 --- /dev/null +++ b/libs/web-components/stories/fixtures/Badge.generated.js @@ -0,0 +1,256 @@ +// Generated from libs/ui/src/components/atoms/Badge/Badge.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'Badge'; +const Badge = 'gd-badge'; +const Row = 'Row'; +const Column = 'Column'; +const Typography = 'gd-typography'; +const Icon = 'gd-icon'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/Badge', + component: Badge, + tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: ` +The \`Badge\` component is used to display small pieces of information, such as status indicators, counts, or labels. + +

Key Features:

+
    +
  • Variants: Primary, Secondary, Tertiary, Quaternary, Quinary
  • +
  • Appearances: Filled, FilledLight, Outline, OutlineFilledLight
  • +
  • Sizes: Extra Small (xs), Small (sm), Medium (md), Large (lg)
  • +
  • Icons: Support for start and end icons
  • +
  • Disabled: Support for disabled state
  • +
  • Flexible: Consumes Box styles for full layout control
  • +
+ `, + }, + }, + }, +}; +export default meta; +export const Default = { + args: { + children: 'Badge', + }, + parameters: { + docs: { + source: {}, + }, + }, +}; +export const Sizes = { + render: () => + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Badge, { size: 'xs' }, 'Extra Small'), + demo(Badge, { size: 'sm' }, 'Small'), + demo(Badge, { size: 'md' }, 'Medium'), + demo(Badge, { size: 'lg' }, 'Large') + ), + parameters: { + docs: { + description: { + story: 'Badge component in all available sizes: extra small, small, medium, and large.', + }, + source: {}, + }, + }, +}; +export const Variants = { + render: () => + demo( + Column, + { gap: '16px' }, + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Typography, null, 'Primary:'), + demo(Badge, null, 'Primary'), + demo(Badge, { variant: 'primary', appearance: 'filledLight' }, 'Primary Light'), + demo(Badge, { variant: 'primary', appearance: 'outline' }, 'Primary Outline'), + demo(Badge, { variant: 'primary', appearance: 'outlineFilledLight' }, 'Primary Outline Filled Light') + ), + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Typography, null, 'Secondary:'), + demo(Badge, { variant: 'secondary', appearance: 'filled' }, 'Secondary'), + demo(Badge, { variant: 'secondary', appearance: 'filledLight' }, 'Secondary Light'), + demo(Badge, { variant: 'secondary', appearance: 'outline' }, 'Secondary Outline'), + demo(Badge, { variant: 'secondary', appearance: 'outlineFilledLight' }, 'Secondary Outline Filled Light') + ), + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Typography, null, 'Tertiary:'), + demo(Badge, { variant: 'tertiary', appearance: 'filled' }, 'Tertiary'), + demo(Badge, { variant: 'tertiary', appearance: 'filledLight' }, 'Tertiary Light'), + demo(Badge, { variant: 'tertiary', appearance: 'outline' }, 'Tertiary Outline'), + demo(Badge, { variant: 'tertiary', appearance: 'outlineFilledLight' }, 'Tertiary Outline Filled Light') + ), + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Typography, null, 'Quaternary:'), + demo(Badge, { variant: 'quaternary', appearance: 'filled' }, 'Quaternary'), + demo(Badge, { variant: 'quaternary', appearance: 'filledLight' }, 'Quaternary Light'), + demo(Badge, { variant: 'quaternary', appearance: 'outline' }, 'Quaternary Outline'), + demo(Badge, { variant: 'quaternary', appearance: 'outlineFilledLight' }, 'Quaternary Outline Filled Light') + ), + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Typography, null, 'Quinary:'), + demo(Badge, { variant: 'quinary', appearance: 'filled' }, 'Quinary'), + demo(Badge, { variant: 'quinary', appearance: 'filledLight' }, 'Quinary Light'), + demo(Badge, { variant: 'quinary', appearance: 'outline' }, 'Quinary Outline'), + demo(Badge, { variant: 'quinary', appearance: 'outlineFilledLight' }, 'Quinary Outline Filled Light') + ) + ), + parameters: { + docs: { + description: { + story: 'All badge variants and options displayed together.', + }, + source: {}, + }, + }, +}; +export const WithIcons = { + render: () => + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Badge, { iconStart: demo(Icon, { name: 'success', size: 'md' }) }, 'With Start Icon'), + demo(Badge, { variant: 'secondary', iconEnd: demo(Icon, { name: 'warning', size: 'md' }) }, 'With End Icon'), + demo( + Badge, + { + variant: 'tertiary', + appearance: 'filled', + iconStart: demo(Icon, { name: 'info', size: 'md' }), + iconEnd: demo(Icon, { name: 'arrowRight', size: 'md' }), + }, + 'Both Icons' + ), + demo( + Badge, + { variant: 'quaternary', iconStart: demo(Icon, { name: 'error', size: 'md' }) }, + 'Quaternary with Icon' + ), + demo( + Badge, + { variant: 'quinary', iconStart: demo(Icon, { name: 'accountCircle', size: 'md' }) }, + 'Quinary with Icon' + ) + ), + parameters: { + docs: { + description: { + story: 'Badges with start and end icons.', + }, + source: {}, + }, + }, +}; +export const WithBoxStyles = { + render: () => + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Badge, { appearance: 'outline', margin: '8px' }, 'With Margin'), + demo(Badge, { variant: 'secondary', appearance: 'outline', padding: '12px 24px' }, 'Custom Padding'), + demo( + Badge, + { variant: 'tertiary', appearance: 'outline', width: '200px', justifyContent: 'center' }, + 'Fixed Width' + ) + ), + parameters: { + docs: { + description: { + story: 'Badges with custom Box styles for layout control.', + }, + source: {}, + }, + }, +}; +export const Disabled = { + render: () => + demo( + Column, + { gap: '16px' }, + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Typography, null, 'All Variants Disabled:'), + demo(Badge, { variant: 'primary', appearance: 'filled', disabled: true }, 'Primary'), + demo(Badge, { variant: 'secondary', appearance: 'filled', disabled: true }, 'Secondary'), + demo(Badge, { variant: 'tertiary', appearance: 'filled', disabled: true }, 'Tertiary'), + demo(Badge, { variant: 'quaternary', appearance: 'filled', disabled: true }, 'Quaternary'), + demo(Badge, { variant: 'quinary', appearance: 'filled', disabled: true }, 'Quinary') + ), + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Typography, null, 'All Appearances Disabled:'), + demo(Badge, { variant: 'primary', appearance: 'filled', disabled: true }, 'Filled'), + demo(Badge, { variant: 'primary', appearance: 'filledLight', disabled: true }, 'Filled Light'), + demo(Badge, { variant: 'primary', appearance: 'outline', disabled: true }, 'Outline'), + demo(Badge, { variant: 'primary', appearance: 'outlineFilledLight', disabled: true }, 'Outline Filled Light') + ), + demo( + Row, + { gap: '16px', alignItems: 'center' }, + demo(Typography, null, 'Sizes Disabled:'), + demo(Badge, { variant: 'primary', appearance: 'filled', size: 'xs', disabled: true }, 'Extra Small'), + demo(Badge, { variant: 'primary', appearance: 'filled', size: 'sm', disabled: true }, 'Small'), + demo(Badge, { variant: 'primary', appearance: 'filled', size: 'md', disabled: true }, 'Medium'), + demo(Badge, { variant: 'primary', appearance: 'filled', size: 'lg', disabled: true }, 'Large'), + demo( + Badge, + { + variant: 'primary', + appearance: 'filled', + disabled: true, + iconStart: demo(Icon, { name: 'check', size: 'md' }), + }, + 'With Icon' + ) + ) + ), + parameters: { + docs: { + description: { + story: 'Badge component in disabled state across all variants, appearances, and sizes.', + }, + source: {}, + }, + }, +}; +export const WithAccessibility = { + ...Variants, + parameters: { + a11y: { + test: 'error', + }, + docs: { + disable: true, + }, + }, + tags: ['a11y'], +}; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { badge: defaultTheme.badge } }); +DefaultTokens.parameters = { + layout: 'padded', +}; diff --git a/libs/web-components/stories/fixtures/Box.generated.js b/libs/web-components/stories/fixtures/Box.generated.js new file mode 100644 index 0000000..efcb1f8 --- /dev/null +++ b/libs/web-components/stories/fixtures/Box.generated.js @@ -0,0 +1,399 @@ +// Generated from libs/ui/src/components/atoms/Box/Box.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'Box'; +const Box = 'gd-box'; +const Typography = 'gd-typography'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/Box', + component: Box, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +The \`Box\` component is a foundational layout primitive that provides a flexible container with built-in support for flexbox, spacing, and sizing. It serves as the base for more complex components like Card. + +
+
+ +

🎨 Key Features

+
    +
  • Flexible Container: Display flex by default, perfect for layout composition
  • +
  • Box Model Props: Full control over width, height, padding, margin, and more
  • +
  • Flexbox Support: Built-in flexbox properties for alignment and distribution
  • +
  • Focus Management: Automatic focus-visible styles for accessibility
  • +
  • Theme Integration: Seamless integration with the design system
  • +
  • Composable: Can be used as a base for other components
  • +
+ +
+ +

🎯 Common Use Cases

+
    +
  • Layout Containers: Wrapper for creating flexible layouts
  • +
  • Card Base: Foundation for card-like components
  • +
  • Sections: Semantic sections with consistent spacing
  • +
  • Custom Components: Base for building domain-specific components
  • +
+ +
+ +

💡 Best Practices

+
    +
  • Semantic HTML: Box renders as a div, use it for layout purposes
  • +
  • Composition: Combine with other components for complex UIs
  • +
  • Accessibility: Use tabIndex when the box needs to be focusable
  • +
  • Performance: Prefer Box props over inline styles for better optimization
  • +
+ + +
+
+ +

🧩 Web Components track (CTORNDSD-646)

+Verdict — No abstraction — shared utility CSS. Zero behavior and no visual surface of its own. Layout primitives are the highest-count nodes on a page, so per-instance shadow-root cost scales worst here, and a percentage width on a shadow child resolves against an auto-width host — the measured cause of a real width-collapse bug in the Select port. +
+Decision rule and full rationale: docs/webcomponents-migration/05-native-html-guidelines.md. +`, + }, + }, + }, +}; +export default meta; +export const Default = { + args: { + variant: 'vertical', + children: 'Box Content', + padding: '20px', + }, + parameters: { + docs: { + description: { + story: 'Basic Box with default vertical variant. Use the controls to experiment with different properties.', + }, + }, + }, +}; +export const Bordered = { + args: { + variant: 'vertical', + isBordered: true, + padding: '20px', + children: 'Bordered Box', + }, + parameters: { + docs: { + description: { + story: 'Box with `isBordered` enabled, adding a border from theme tokens.', + }, + }, + }, +}; +export const Highlighted = { + args: { + variant: 'vertical', + isHighlighted: true, + isBordered: true, + padding: '20px', + children: 'Hover over me to see the highlight effect!', + }, + parameters: { + docs: { + description: { + story: + 'Box with `isHighlighted` enabled. When you hover over it, an outline appears. This is useful for interactive card-like containers.', + }, + }, + }, +}; +export const WithShadowHover = { + args: { + variant: 'vertical', + withShadowHover: true, + padding: '20px', + children: 'Hover over me to see the shadow effect!', + styles: { + backgroundColor: '#fff', + }, + }, + parameters: { + docs: { + description: { + story: + 'Box with `withShadowHover` enabled. When you hover over it, a box shadow appears creating an elevation effect. Perfect for cards that lift on hover.', + }, + }, + }, +}; +export const VerticalLayout = { + args: { + variant: 'vertical', + gap: '10px', + padding: '20px', + isBordered: true, + children: demo( + Fragment, + null, + demo('div', { style: { padding: '10px', background: '#e0e0e0', borderRadius: '4px' } }, 'Item 1'), + demo('div', { style: { padding: '10px', background: '#e0e0e0', borderRadius: '4px' } }, 'Item 2'), + demo('div', { style: { padding: '10px', background: '#e0e0e0', borderRadius: '4px' } }, 'Item 3') + ), + }, + parameters: { + docs: { + description: { + story: 'Vertical variant stacks children in a column with gap spacing. Default flexDirection is column.', + }, + }, + }, +}; +export const HorizontalLayout = { + args: { + variant: 'horizontal', + gap: '10px', + padding: '20px', + isBordered: true, + children: demo( + Fragment, + null, + demo('div', { style: { padding: '10px', background: '#e0e0e0', borderRadius: '4px', flex: 1 } }, 'Item 1'), + demo('div', { style: { padding: '10px', background: '#e0e0e0', borderRadius: '4px', flex: 1 } }, 'Item 2'), + demo('div', { style: { padding: '10px', background: '#e0e0e0', borderRadius: '4px', flex: 1 } }, 'Item 3') + ), + }, + parameters: { + docs: { + description: { + story: 'Horizontal variant arranges children in a row with gap spacing. Default flexDirection is row.', + }, + }, + }, +}; +export const CenteredContent = { + args: { + variant: 'vertical', + justifyContent: 'center', + alignItems: 'center', + width: '300px', + height: '200px', + isBordered: true, + children: demo( + Fragment, + null, + demo(Typography, { variant: 'h5' }, 'Centered'), + demo(Typography, { variant: 'small' }, 'Content is centered both horizontally and vertically') + ), + }, + parameters: { + docs: { + description: { + story: 'Box with centered content using flexbox alignment properties.', + }, + }, + }, +}; +export const Examples = () => { + return demo( + Box, + { variant: 'vertical', gap: '40px', padding: '20px' }, + demo( + Box, + { variant: 'vertical', gap: '10px' }, + demo(Typography, { variant: 'h3' }, 'Box Component Examples'), + demo(Typography, { variant: 'p' }, 'Various use cases demonstrating the flexibility of the Box component') + ), + demo( + Box, + { variant: 'vertical', gap: '10px' }, + demo(Typography, { variant: 'h5' }, 'Vertical vs Horizontal Variants'), + demo( + Box, + { variant: 'vertical', gap: '10px' }, + demo( + Box, + { variant: 'vertical', gap: '10px', padding: '20px', isBordered: true }, + demo(Typography, { variant: 'small', styles: { fontWeight: 600 } }, 'Vertical (Default)'), + demo(Typography, { variant: 'small' }, 'Stacks children vertically') + ), + demo( + Box, + { variant: 'horizontal', gap: '10px', padding: '20px', isBordered: true }, + demo(Typography, { variant: 'small', styles: { fontWeight: 600 } }, 'Horizontal'), + demo(Typography, { variant: 'small' }, 'Arranges children horizontally') + ) + ) + ), + demo( + Box, + { variant: 'vertical', gap: '10px' }, + demo(Typography, { variant: 'h5' }, 'Border & Hover Effects'), + demo( + Typography, + { variant: 'small', styles: { marginBottom: '10px' } }, + 'Hover over each box to see different effects' + ), + demo( + Box, + { variant: 'horizontal', gap: '10px' }, + demo( + Box, + { variant: 'vertical', padding: '20px', styles: { backgroundColor: '#f5f5f5' } }, + demo(Typography, { variant: 'small', styles: { fontWeight: 600 } }, 'Default'), + demo(Typography, { variant: 'small' }, 'No hover effect') + ), + demo( + Box, + { variant: 'vertical', padding: '20px', isBordered: true }, + demo(Typography, { variant: 'small', styles: { fontWeight: 600 } }, 'Bordered'), + demo(Typography, { variant: 'small' }, 'Static border') + ), + demo( + Box, + { variant: 'vertical', padding: '20px', isBordered: true, isHighlighted: true }, + demo(Typography, { variant: 'small', styles: { fontWeight: 600 } }, 'Highlighted'), + demo(Typography, { variant: 'small' }, 'Outline on hover') + ), + demo( + Box, + { variant: 'vertical', padding: '20px', withShadowHover: true, styles: { backgroundColor: '#fff' } }, + demo(Typography, { variant: 'small', styles: { fontWeight: 600 } }, 'Shadow Hover'), + demo(Typography, { variant: 'small' }, 'Shadow on hover') + ), + demo( + Box, + { + variant: 'vertical', + padding: '20px', + isBordered: true, + withShadowHover: true, + styles: { backgroundColor: '#fff' }, + }, + demo(Typography, { variant: 'small', styles: { fontWeight: 600 } }, 'Combined'), + demo(Typography, { variant: 'small' }, 'Border + shadow') + ) + ) + ), + demo( + Box, + { variant: 'vertical', gap: '10px' }, + demo(Typography, { variant: 'h5' }, 'Horizontal Layout with Gap'), + demo( + Box, + { variant: 'horizontal', gap: '10px' }, + demo(Box, { variant: 'vertical', padding: '20px', isBordered: true, styles: { flex: 1 } }, 'Box 1'), + demo(Box, { variant: 'vertical', padding: '20px', isBordered: true, styles: { flex: 1 } }, 'Box 2'), + demo(Box, { variant: 'vertical', padding: '20px', isBordered: true, styles: { flex: 1 } }, 'Box 3') + ) + ), + demo( + Box, + { variant: 'vertical', gap: '10px' }, + demo(Typography, { variant: 'h5' }, 'Vertical Stack'), + demo( + Box, + { variant: 'vertical', gap: '10px', width: '300px' }, + demo(Box, { variant: 'vertical', padding: '15px', isBordered: true }, 'Item 1'), + demo(Box, { variant: 'vertical', padding: '15px', isBordered: true }, 'Item 2'), + demo(Box, { variant: 'vertical', padding: '15px', isBordered: true }, 'Item 3') + ) + ), + demo( + Box, + { variant: 'vertical', gap: '10px' }, + demo(Typography, { variant: 'h5' }, 'Interactive Card-like Containers'), + demo( + Box, + { variant: 'horizontal', gap: '15px' }, + demo( + Box, + { variant: 'vertical', gap: '15px', padding: '20px', width: '280px', isBordered: true, isHighlighted: true }, + demo(Typography, { variant: 'h6' }, 'Outline Effect'), + demo(Typography, { variant: 'small' }, 'Uses `isBordered` and `isHighlighted` for outline hover effect.') + ), + demo( + Box, + { + variant: 'vertical', + gap: '15px', + padding: '20px', + width: '280px', + withShadowHover: true, + styles: { + backgroundColor: '#fff', + border: '1px solid #e0e0e0', + }, + }, + demo(Typography, { variant: 'h6' }, 'Elevation Effect'), + demo(Typography, { variant: 'small' }, 'Uses `withShadowHover` for shadow elevation on hover.') + ) + ) + ), + demo( + Box, + { variant: 'vertical', gap: '10px' }, + demo(Typography, { variant: 'h5' }, 'Complex Nested Layout'), + demo( + Box, + { variant: 'vertical', gap: '10px', padding: '20px', isBordered: true }, + demo(Typography, { variant: 'h6' }, 'Dashboard Section'), + demo( + Box, + { variant: 'horizontal', gap: '10px' }, + demo( + Box, + { variant: 'vertical', gap: '5px', padding: '15px', isBordered: true }, + demo(Typography, { variant: 'small', styles: { fontWeight: 600 } }, 'Metric 1'), + demo(Typography, { variant: 'p' }, '1,234') + ), + demo( + Box, + { variant: 'vertical', gap: '5px', padding: '15px', isBordered: true }, + demo(Typography, { variant: 'small', styles: { fontWeight: 600 } }, 'Metric 2'), + demo(Typography, { variant: 'p' }, '5,678') + ) + ) + ) + ) + ); +}; +Examples.parameters = { + layout: 'padded', + docs: { + description: { + story: 'Collection of examples showing different ways to use the Box component for various layout needs.', + }, + }, +}; +export const WithAccessibility = { + render: Examples, + parameters: { + a11y: { + test: 'error', + options: { + rules: { + 'heading-order': { enabled: false }, + }, + }, + }, + docs: { + disable: true, + }, + }, + tags: ['a11y'], +}; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { box: defaultTheme.box } }); +DefaultTokens.parameters = { + layout: 'padded', + docs: { + description: { + story: + 'View the default theme tokens used by the Box component. These tokens control the base display and focus styles.', + }, + }, +}; diff --git a/libs/web-components/stories/fixtures/Button.generated.js b/libs/web-components/stories/fixtures/Button.generated.js new file mode 100644 index 0000000..c2c147c --- /dev/null +++ b/libs/web-components/stories/fixtures/Button.generated.js @@ -0,0 +1,521 @@ +// Generated from libs/ui/src/components/atoms/Button/Button.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'Button'; +const Button = 'gd-button'; +const Column = 'Column'; +const Typography = 'gd-typography'; +const Row = 'Row'; +const Icon = 'gd-icon'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/Button', + component: Button, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +The \`Button\` component is a production-ready, versatile UI element that provides comprehensive interaction patterns while maintaining accessibility best practices and design system consistency. + +
+
+ +

🎨 Design Variants

+The Button component offers six distinct visual styles to accommodate different UI hierarchies and use cases: + +
    +
  • Primary: High-emphasis button for main call-to-action elements (e.g., "Submit", "Save", "Continue")
  • +
  • Secondary: Medium-emphasis button for important but not primary actions (e.g., "Cancel", "Back")
  • +
  • Tertiary: Low-emphasis button for less prominent actions
  • +
  • Outlined: Border-only style providing clear boundaries without heavy visual weight
  • +
  • Text: Minimal text-only style for subtle interactions and tertiary actions
  • +
  • Inherit: Inherits parent styles for maximum flexibility in custom implementations
  • +
+ +
+ +

⚙️ Key Features

+
    +
  • Icon Integration: Support for leading (\`iconStart\`) and trailing (\`iconEnd\`) icons, plus dedicated icon-only mode with \`isIcon\` prop. When using \`isIcon\`, pass the icon via \`iconStart\` (not as children)
  • +
  • Flexible Sizing: Full-width mode via \`fullWidth\` prop, plus support for custom dimensions through Box props
  • +
  • Border Radius Control: Eight rounded options from \`none\` to fully \`round\`, enabling consistent corner styling
  • +
  • Interactive States: Built-in hover, focus, active, and disabled states with smooth transitions. Use Box props like \`justifyContent\` to control content alignment
  • +
  • Box Props Support: Extends Box component props including \`justifyContent\`, \`alignItems\`, \`gap\`, \`margin\`, \`padding\`, and all layout properties for flexible button layouts
  • +
  • Custom Styling: Use \`className\` for custom CSS classes and \`styles\` prop for inline styles
  • +
  • Theme Integration: Automatic color and spacing inheritance from theme tokens
  • +
  • Accessibility First: ARIA attributes, keyboard navigation, focus management, and screen reader support
  • +
  • Type Safety: Full TypeScript support with comprehensive prop types
  • +
+ +
+ +

🎯 Common Use Cases

+
    +
  • Forms: Submit, reset, and cancel actions with appropriate \`type\` attribute
  • +
  • Navigation: Page transitions, modal triggers, and menu items
  • +
  • Actions: Delete, edit, save, and other CRUD operations
  • +
  • Icon Buttons: Close buttons, menu toggles, and toolbar actions using \`isIcon\` mode
  • +
  • Call-to-Action: Primary conversion buttons with high visual emphasis
  • +
+ +
+ +

💡 Best Practices

+
    +
  • Visual Hierarchy: Use Primary for main actions, Secondary for alternatives, and Text for low-priority actions
  • +
  • Icon Usage: Always provide \`ariaLabel\` for icon-only buttons. When using \`isIcon\`, pass the icon via \`iconStart\` (not as children)
  • +
  • Loading States: Disable buttons during async operations to prevent duplicate submissions
  • +
  • Touch Targets: Maintain minimum 44px touch targets for mobile accessibility
  • +
  • Consistent Styling: Use theme tokens and rounded variants for design system consistency
  • +
  • Box Props: Use \`justifyContent\`, \`alignItems\`, and other Box layout props to control button content alignment and spacing
  • +
  • Custom Styling: Use \`className\` for CSS classes and \`styles\` prop for inline styles when needed
  • +
  • Interactive States: Buttons automatically handle hover, active, and disabled states with smooth transitions
  • +
+ +
+ +

🔧 Technical Implementation

+Built with React's \`forwardRef\` for proper ref handling, the component integrates seamlessly with the theme system via \`useTheme\` hook. It extends Box component props for flexible layout control and uses styled-components for dynamic theming. + + +
+
+ +

🧩 Web Components track (CTORNDSD-646)

+Verdict — Lit custom element. Owns variant, loading and focus-ring state, and CTORNDSD-286 was a button style-collision bug — Shadow DOM isolation is the reason. Ported as <gd-button>: 1.87 kB gzip vs 19.07 kB. +
+Decision rule and full rationale: docs/webcomponents-migration/05-native-html-guidelines.md. +`, + }, + }, + }, +}; +export default meta; +export const AllVariants = () => { + return demo( + Column, + { gap: '30px' }, + demo( + Column, + { gap: '10px' }, + demo(Typography, { variant: 'h3' }, 'All Button Variants'), + demo( + Typography, + { variant: 'p' }, + 'The Button component supports six visual variants: Primary, Secondary, Tertiary, Outlined, Text, and Inherit' + ) + ), + demo( + Column, + { gap: '20px' }, + demo( + Column, + { gap: '10px' }, + demo(Typography, { variant: 'h6' }, 'Basic Variants'), + demo( + Row, + { gutter: '10px', align: 'center' }, + demo(Button, { variant: 'primary' }, 'Primary'), + demo(Button, { variant: 'secondary' }, 'Secondary'), + demo(Button, { variant: 'tertiary' }, 'Tertiary'), + demo(Button, { variant: 'outlined' }, 'Outlined'), + demo(Button, { variant: 'text' }, 'Text'), + demo(Button, { variant: 'inherit' }, 'Inherit') + ) + ), + demo( + Column, + { gap: '10px' }, + demo(Typography, { variant: 'h6' }, 'With Icons'), + demo( + Row, + { gutter: '10px', align: 'center' }, + demo(Button, { variant: 'primary', iconStart: demo(Icon, { name: 'check' }) }, 'Primary'), + demo(Button, { variant: 'secondary', iconStart: demo(Icon, { name: 'check' }) }, 'Secondary'), + demo(Button, { variant: 'tertiary', iconStart: demo(Icon, { name: 'check' }) }, 'Tertiary'), + demo(Button, { variant: 'outlined', iconStart: demo(Icon, { name: 'check' }) }, 'Outlined'), + demo(Button, { variant: 'text', iconStart: demo(Icon, { name: 'check' }) }, 'Text'), + demo(Button, { variant: 'inherit', iconStart: demo(Icon, { name: 'check' }) }, 'Inherit') + ) + ), + demo( + Column, + { gap: '10px' }, + demo(Typography, { variant: 'h6' }, 'Icon Only'), + demo( + Row, + { gutter: '10px', align: 'center' }, + demo(Button, { + variant: 'primary', + isIcon: true, + ariaLabel: 'Primary icon button', + iconStart: demo(Icon, { name: 'cross' }), + }), + demo(Button, { + variant: 'secondary', + isIcon: true, + ariaLabel: 'Secondary icon button', + iconStart: demo(Icon, { name: 'cross' }), + }), + demo(Button, { + variant: 'tertiary', + isIcon: true, + ariaLabel: 'Tertiary icon button', + iconStart: demo(Icon, { name: 'cross' }), + }), + demo(Button, { + variant: 'outlined', + isIcon: true, + ariaLabel: 'Outlined icon button', + iconStart: demo(Icon, { name: 'cross' }), + }), + demo(Button, { + variant: 'text', + isIcon: true, + ariaLabel: 'Text icon button', + iconStart: demo(Icon, { name: 'cross' }), + }), + demo(Button, { + variant: 'inherit', + isIcon: true, + ariaLabel: 'Inherit icon button', + iconStart: demo(Icon, { name: 'cross' }), + }) + ) + ), + demo( + Column, + { gap: '10px' }, + demo(Typography, { variant: 'h6' }, 'Disabled State'), + demo( + Row, + { gutter: '10px', align: 'center' }, + demo(Button, { variant: 'primary', disabled: true }, 'Primary'), + demo(Button, { variant: 'secondary', disabled: true }, 'Secondary'), + demo(Button, { variant: 'tertiary', disabled: true }, 'Tertiary'), + demo(Button, { variant: 'outlined', disabled: true }, 'Outlined'), + demo(Button, { variant: 'text', disabled: true }, 'Text'), + demo(Button, { variant: 'inherit', disabled: true }, 'Inherit') + ) + ) + ) + ); +}; +AllVariants.parameters = { + layout: 'padded', + backgrounds: { + default: 'transparent', + }, + docs: { + source: {}, + description: { + story: + 'A comprehensive showcase of all button variants in different states. This demonstrates the visual hierarchy: Primary for main actions, Secondary for important alternatives, Tertiary for less prominent actions, Outlined for clear boundaries, Text for minimal emphasis, and Inherit for custom styling.', + }, + }, +}; +export const Default = { + args: { + children: 'Button', + variant: 'primary', + onClick: fn(), + }, + parameters: { + docs: { + source: {}, + description: { + story: + 'This is the default interactive story. Use the controls panel to experiment with different props and see how the Button component behaves. Try changing the variant, adding icons, adjusting rounded corners, and toggling states.', + }, + }, + }, +}; +export const WithIcons = { + args: { + children: 'Save Changes', + variant: 'primary', + iconStart: demo(Icon, { name: 'check' }), + iconEnd: demo(Icon, { name: 'arrowRight' }), + onClick: fn(), + }, + parameters: { + docs: { + source: {}, + description: { + story: + 'Buttons can include icons at the start and/or end using the `iconStart` and `iconEnd` props. Icons automatically receive proper spacing and alignment. This is useful for adding visual context to button actions.', + }, + }, + }, +}; +export const IconOnly = { + args: { + variant: 'primary', + isIcon: true, + ariaLabel: 'Close', + iconStart: demo(Icon, { name: 'cross' }), + onClick: fn(), + }, + parameters: { + docs: { + source: {}, + description: { + story: + 'Icon-only buttons use the `isIcon` prop to render with equal width and height. When using `isIcon`, pass the icon via `iconStart` (not as children). Always provide an `ariaLabel` for accessibility.', + }, + }, + }, +}; +export const FullWidthButton = { + args: { + children: 'Full Width Button', + variant: 'primary', + fullWidth: true, + onClick: fn(), + }, + parameters: { + docs: { + source: {}, + description: { + story: + 'The `fullWidth` prop makes the button expand to fill its container width. This is commonly used in forms, mobile layouts, and call-to-action sections.', + }, + }, + }, +}; +export const RoundedButton = { + args: { + children: 'Rounded Button', + variant: 'primary', + rounded: 'md', + onClick: fn(), + }, + parameters: { + docs: { + source: {}, + description: { + story: + 'Control border radius using the `rounded` prop with options: none, xs, sm, md, lg, xl, and round (fully rounded). This helps maintain consistent corner styling across your design system.', + }, + }, + }, +}; +export const DisabledButton = { + args: { + children: 'Disabled Button', + variant: 'primary', + disabled: true, + }, + parameters: { + docs: { + source: {}, + description: { + story: + 'Disabled buttons have reduced opacity and prevent all interactions. Use this state during loading operations or when prerequisites are not met.', + }, + }, + }, +}; +export const ButtonStatesUsingClass = { + args: { + children: 'Press Me', + variant: 'primary', + justifyContent: 'start', + className: 'active', + onClick: fn(), + }, + parameters: { + docs: { + source: {}, + description: { + story: 'Button states using classes - active, hover, disabled', + }, + }, + }, +}; +export const CustomStyledButton = { + args: { + children: 'Custom Styled', + padding: '8px', + width: '300px', + justifyContent: 'start', + iconStart: demo(Icon, { name: 'folder' }), + onClick: fn(), + }, + parameters: { + docs: { + source: {}, + description: { + story: + 'Use the `styles` prop to apply custom CSS properties. The Button component extends Box props, giving you full control over layout, spacing, and styling.', + }, + }, + }, +}; +export const RealWorldExamples = () => { + return demo( + Column, + { gap: '40px' }, + demo( + Column, + { gap: '10px' }, + demo(Typography, { variant: 'h3' }, 'Real-World Examples'), + demo( + Typography, + { variant: 'p' }, + "Common button patterns and use cases you'll encounter in production applications" + ) + ), + demo( + Column, + { gap: '15px' }, + demo(Typography, { variant: 'h5' }, 'Form Actions'), + demo( + Row, + { gutter: '10px', align: 'center' }, + demo(Button, { variant: 'primary', type: 'submit' }, 'Submit'), + demo(Button, { variant: 'secondary', type: 'button' }, 'Cancel'), + demo(Button, { variant: 'text', type: 'reset' }, 'Reset') + ) + ), + demo( + Column, + { gap: '15px' }, + demo(Typography, { variant: 'h5' }, 'Navigation'), + demo( + Row, + { gutter: '10px', align: 'center' }, + demo(Button, { variant: 'secondary', iconStart: demo(Icon, { name: 'arrowLeft' }) }, 'Back'), + demo(Button, { variant: 'primary', iconEnd: demo(Icon, { name: 'arrowRight' }) }, 'Continue'), + demo(Button, { variant: 'text', iconEnd: demo(Icon, { name: 'arrowForward' }) }, 'Skip') + ) + ), + demo( + Column, + { gap: '15px' }, + demo(Typography, { variant: 'h5' }, 'File Operations'), + demo( + Row, + { gutter: '10px', align: 'center' }, + demo(Button, { variant: 'primary', iconStart: demo(Icon, { name: 'upload' }) }, 'Upload File'), + demo(Button, { variant: 'secondary', iconStart: demo(Icon, { name: 'attachment' }) }, 'Attach'), + demo(Button, { variant: 'outlined', iconStart: demo(Icon, { name: 'folder' }) }, 'Browse') + ) + ), + demo( + Column, + { gap: '15px' }, + demo(Typography, { variant: 'h5' }, 'CRUD Actions'), + demo( + Row, + { gutter: '10px', align: 'center' }, + demo(Button, { variant: 'primary', iconStart: demo(Icon, { name: 'check' }) }, 'Save'), + demo(Button, { variant: 'secondary', iconStart: demo(Icon, { name: 'edit' }) }, 'Edit'), + demo(Button, { variant: 'outlined', iconStart: demo(Icon, { name: 'deleteOutlined' }) }, 'Delete'), + demo(Button, { variant: 'text' }, 'Cancel') + ) + ), + demo( + Column, + { gap: '15px' }, + demo(Typography, { variant: 'h5' }, 'Social & Feedback'), + demo( + Row, + { gutter: '10px', align: 'center' }, + demo(Button, { variant: 'outlined', iconStart: demo(Icon, { name: 'thumbUpFilled' }) }, 'Like'), + demo(Button, { variant: 'outlined', iconStart: demo(Icon, { name: 'star' }) }, 'Favorite'), + demo(Button, { variant: 'outlined', iconStart: demo(Icon, { name: 'send' }) }, 'Share') + ) + ), + demo( + Column, + { gap: '15px' }, + demo(Typography, { variant: 'h5' }, 'Toolbar'), + demo( + Row, + { gutter: '10px', align: 'center' }, + demo(Button, { variant: 'text', isIcon: true, ariaLabel: 'Edit', iconStart: demo(Icon, { name: 'edit' }) }), + demo(Button, { variant: 'text', isIcon: true, ariaLabel: 'Search', iconStart: demo(Icon, { name: 'search' }) }), + demo(Button, { variant: 'text', isIcon: true, ariaLabel: 'Filter', iconStart: demo(Icon, { name: 'filter' }) }), + demo(Button, { + variant: 'text', + isIcon: true, + ariaLabel: 'More options', + iconStart: demo(Icon, { name: 'mobileMenu' }), + }) + ) + ), + demo( + Column, + { gap: '15px' }, + demo(Typography, { variant: 'h5' }, 'Call-to-Action'), + demo( + Column, + { gap: '10px' }, + demo( + Button, + { variant: 'primary', fullWidth: true, rounded: 'md', iconEnd: demo(Icon, { name: 'arrowRight' }) }, + 'Get Started' + ), + demo(Button, { variant: 'outlined', fullWidth: true, rounded: 'md' }, 'Learn More') + ) + ) + ); +}; +RealWorldExamples.parameters = { + layout: 'padded', + docs: { + source: {}, + description: { + story: + 'Practical examples of button usage in common scenarios including forms, navigation, file operations, CRUD actions, social interactions, toolbars, and call-to-action sections.', + }, + }, +}; +export const IsLoading = () => + demo( + Row, + { gap: '15px', alignItems: 'center' }, + demo(Button, { variant: 'primary', isLoading: true }, 'Primary'), + demo(Button, { variant: 'outlined', isLoading: true }, 'Outlined'), + demo(Button, { variant: 'text', isLoading: true }, 'Text') + ); +IsLoading.parameters = { + docs: { + description: { + story: 'When `isLoading` is set, the button displays a loading indicator and prevents interaction.', + }, + source: {}, + }, +}; +export const WithAccessibility = AllVariants; +WithAccessibility.tags = ['a11y']; +WithAccessibility.parameters = { + a11y: { + test: 'error', + options: { + rules: { + 'heading-order': { enabled: false }, + }, + }, + }, + docs: { + disable: true, + }, +}; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { button: defaultTheme.button } }); +DefaultTokens.parameters = { + layout: 'padded', + docs: { + source: {}, + description: { + story: + 'View the default theme tokens used by the Button component. These tokens control colors, spacing, borders, and other visual properties across all button variants.', + }, + }, +}; diff --git a/libs/web-components/stories/fixtures/Counter.generated.js b/libs/web-components/stories/fixtures/Counter.generated.js new file mode 100644 index 0000000..b6bb6be --- /dev/null +++ b/libs/web-components/stories/fixtures/Counter.generated.js @@ -0,0 +1,103 @@ +// Generated from libs/ui/src/components/molecules/Counter/Counter.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'Counter'; +const Counter = 'gd-counter'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Molecules/Counter', + component: Counter, + tags: ['autodocs', 'ecommerce'], + parameters: { + layout: 'centered', + docs: { + description: { + component: ` + The \`Counter\` component is a versatile UI element for managing numeric values within specified boundaries. +
+
+

Key Features:

+
    +
  • + Value Management +
      +
    • Configurable minimum and maximum bounds
    • +
    • Initial value setting
    • +
    • Real-time value updates
    • +
    +
  • +
  • + User Interaction +
      +
    • Increment/decrement buttons
    • +
    • Direct value input
    • +
    • Keyboard navigation
    • +
    +
  • +
  • Validation – Ensures values stay within bounds
  • +
  • Event Handling – Callbacks for value changes
  • +
  • Accessibility – ARIA labels and keyboard support
  • +
  • Customization – Themeable styles and layouts
  • +
+
+

Layout Props:

+
    +
  • Dimensions +
      +
    • width/height: Size control
    • +
    • minWidth/minHeight: Minimum constraints
    • +
    +
  • +
  • Spacing +
      +
    • margin: External spacing
    • +
    • padding: Internal spacing
    • +
    +
  • +
  • position: Element positioning
  • +
  • display: Layout behavior
  • +
+ `, + }, + }, + }, +}; +export default meta; +const Template = (args) => demo(Counter, { ...args }); +export const Default = Template.bind({}); +Default.args = {}; +export const AdjustedMaxValue5 = Template.bind({}); +AdjustedMaxValue5.args = { + max: 5, +}; +export const AdjustedMinValue3 = Template.bind({}); +AdjustedMinValue3.args = { + initial: 3, + min: 3, +}; +export const AdjustedMin2MaxValue10 = Template.bind({}); +AdjustedMin2MaxValue10.args = { + initial: 2, + min: 2, + max: 10, +}; +export const WithExternalCounterChangeHandler = (args) => { + return demo(Counter, { ...args, initial: 3 }); +}; +WithExternalCounterChangeHandler.args = { + onCounterChange: fn((newValue) => { + action('Counter Changed')(newValue); + }), +}; +export const Disabled = Template.bind({}); +Disabled.args = { + isDisabled: true, + initial: 5, +}; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { counter: defaultTheme.counter } }); +DefaultTokens.parameters = { + layout: 'padded', +}; diff --git a/libs/web-components/stories/fixtures/InputFile.generated.js b/libs/web-components/stories/fixtures/InputFile.generated.js new file mode 100644 index 0000000..84a13ba --- /dev/null +++ b/libs/web-components/stories/fixtures/InputFile.generated.js @@ -0,0 +1,122 @@ +// Generated from libs/ui/src/components/atoms/InputFile/InputFile.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'InputFile'; +const InputFile = 'gd-input-file'; +const Icon = 'gd-icon'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/InputFile', + component: InputFile, + args: { + accept: undefined, + capture: undefined, + multiple: false, + disabled: false, + }, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: ` + The \`InputFile\` component provides an enhanced file upload experience by combining a hidden native file input with a customizable button interface. This component follows design system guidelines while maintaining full browser file handling capabilities. +
+
+

Key Features:

+
    +
  • + File Selection +
      +
    • Flexible file selection with support for single or multiple files
    • +
    • Comprehensive file type filtering through MIME types or extensions
    • +
    • Direct camera/microphone access on mobile devices via capture attribute
    • +
    +
  • +
  • + Customization +
      +
    • Extensive button styling options through ButtonProps interface
    • +
    • Support for both text and icon-based labels
    • +
    • Full control over wrapper styling using design tokens
    • +
    +
  • +
  • Accessibility - Fully accessible implementation with proper ARIA attributes and keyboard navigation
  • +
  • State Management - Comprehensive disabled state handling across all interactive elements
  • +
+
+

Usage Examples:

+
    +
  • Basic file upload: \` console.log(e.target.files)} />\`
  • +
  • Image selection: \`\`
  • +
  • Enhanced styling: \`\`
  • +
  • Icon integration: \`\`
  • +
+ `, + }, + }, + }, +}; +export default meta; +export const Default = {}; +Default.args = { onClick: fn(), onChange: fn() }; +export const Disabled = { + args: { disabled: true }, +}; +export const Multiple = { + args: { + multiple: true, + }, +}; +export const WithAccept = { + args: { + accept: 'image/*', + }, +}; +export const CustomLabel = { + args: { + children: 'Pick a Photo', + }, +}; +export const IconLabel = { + args: { + children: demo(Icon, { name: 'attachment' }), + buttonProps: { + variant: 'text', + }, + onClick: fn(), + onChange: fn(), + }, +}; +export const WithCustomStyles = { + args: { + styles: { + border: '2px dashed #FF6347', + padding: '1rem', + borderRadius: '4px', + backgroundColor: '#fff8f0', + }, + }, +}; +export const WithAccessibility = { + args: { + children: 'Accessible Input File', + 'aria-label': 'Accessible InputFile', + }, + parameters: { + a11y: { + test: 'error', + }, + docs: { + disable: true, + }, + tags: ['a11y'], + }, +}; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { inputfile: defaultTheme.inputfile } }); +DefaultTokens.parameters = { + layout: 'padded', +}; diff --git a/libs/web-components/stories/fixtures/Label.generated.js b/libs/web-components/stories/fixtures/Label.generated.js new file mode 100644 index 0000000..5cbe490 --- /dev/null +++ b/libs/web-components/stories/fixtures/Label.generated.js @@ -0,0 +1,106 @@ +// Generated from libs/ui/src/components/atoms/Label/Label.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'Label'; +const Label = 'gd-label'; +const Row = 'Row'; +const Icon = 'gd-icon'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/Label', + component: Label, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: ` + The \`Label\` component is a highly flexible UI element designed to be fully customizable in both design and functionality. +
+
+

Key Features:

+
    +
  • + Design Agnostic +
      +
    • Adapts to any theme or custom styles
    • +
    • Supports both className and inline styles
    • +
    • Flexible content composition
    • +
    +
  • +
  • + Composable +
      +
    • Can include icons, tooltips, or other components
    • +
    • Supports nested component structures
    • +
    • Flexible content alignment
    • +
    +
  • +
  • Interactive – Supports click events and hover states
  • +
  • Theming – Compatible with design tokens and custom themes
  • +
+
+

Layout Props:

+
    +
  • Spacing +
      +
    • margin/padding: Overall spacing
    • +
    • marginTop/paddingTop: Top spacing
    • +
    • marginRight/paddingRight: Right spacing
    • +
    • marginBottom/paddingBottom: Bottom spacing
    • +
    • marginLeft/paddingLeft: Left spacing
    • +
    +
  • +
  • Display Properties +
      +
    • display: CSS display property
    • +
    • position: CSS position property
    • +
    • width/height: Size dimensions
    • +
    +
  • +
+ `, + }, + }, + }, +}; +export default meta; +const Template = (args) => demo(Label, { ...args }); +export const LabelDefault = Template.bind({}); +LabelDefault.args = { + children: COMPONENT_NAME, +}; +export const LabelWithChildIcon = Template.bind({}); +LabelWithChildIcon.args = { + children: demo(Row, null, demo(Icon, { name: 'star' }), ' ', COMPONENT_NAME), +}; +export const CustomStyles = Template.bind({}); +CustomStyles.args = { + children: COMPONENT_NAME, + styles: { + backgroundColor: 'lightblue', + color: 'white', + padding: '1rem', + }, +}; +export const WithAccessibility = Template.bind({}); +WithAccessibility.args = { + children: `Accessible ${COMPONENT_NAME}`, + ariaLabel: 'Navigate to Griddynamics Storybook', +}; +WithAccessibility.parameters = { + a11y: { + test: 'error', + }, + docs: { + disable: true, + }, +}; +WithAccessibility.tags = ['a11y']; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { label: defaultTheme.label } }); +DefaultTokens.parameters = { + layout: 'padded', +}; diff --git a/libs/web-components/stories/fixtures/Link.generated.js b/libs/web-components/stories/fixtures/Link.generated.js new file mode 100644 index 0000000..d5fd25a --- /dev/null +++ b/libs/web-components/stories/fixtures/Link.generated.js @@ -0,0 +1,178 @@ +// Generated from libs/ui/src/components/atoms/Link/Link.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'Link'; +const Link = 'gd-link'; +const Button = 'gd-button'; +const Typography = 'gd-typography'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/Link', + component: Link, + args: { + variant: 'primary', + rel: '', + }, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: ` + The \`Link\` component is a versatile navigation element that provides various interaction patterns and styling options. +
+
+

Key Features:

+
    +
  • + Design Variants +
      +
    • Primary - Default styled link
    • +
    • Inherit - Inherits parent styles
    • +
    • Inverted - Reversed color scheme
    • +
    • Button - Button-like appearance
    • +
    +
  • +
  • + Link Behavior +
      +
    • Internal navigation
    • +
    • Absolute URL linking
    • +
    • Target window control
    • +
    +
  • +
  • Accessibility – ARIA attributes and keyboard navigation
  • +
  • States – Hover, focus, active, disabled, visited states
  • +
  • Theming – Custom styling and theme integration
  • +
  • Composable – Supports nested components like Typography, Button
  • +
+
+

Layout Props:

+
    +
  • Styling +
      +
    • className: Custom CSS classes
    • +
    • styles: Inline style object
    • +
    +
  • +
  • Custom Attributes +
      +
    • data-*: Custom data attributes
    • +
    • aria-*: Accessibility attributes
    • +
    +
  • +
+ + +
+
+ +

🧩 Web Components track (CTORNDSD-646)

+Verdict — Native <a> + shared token CSS. Zero behavior. An <a> inside a shadow root is invisible to SEO crawlers, link checkers and a[href] queries, with no behavioral gain to offset it. +
+Decision rule and full rationale: docs/webcomponents-migration/05-native-html-guidelines.md. +`, + }, + }, + }, +}; +export default meta; +const Template = (args) => demo(Link, { ...args }); +export const Variants = Template.bind({}); +Variants.args = { + children: `Variants ${COMPONENT_NAME}`, + onClick: fn(), +}; +export const Disabled = Template.bind({}); +Disabled.args = { + children: `Disabled ${COMPONENT_NAME}`, + disabled: true, +}; +export const TargetBlankVisited = Template.bind({}); +TargetBlankVisited.args = { + children: `Outbound ${COMPONENT_NAME}`, + variant: 'inherit', + href: 'https://storybook.cto-rnd-system-design.griddynamics.net/', + target: '_blank', + rel: `${'noopener'} ${'noreferrer'}`, +}; +export const InheritWithButtonAsChild = Template.bind({}); +InheritWithButtonAsChild.args = { + variant: 'inherit', + children: demo(Button, null, `Button ${COMPONENT_NAME}`), +}; +export const Inverted = Template.bind({}); +Inverted.args = { + variant: 'inverted', + children: demo(Fragment, null, `Typography ${COMPONENT_NAME}`), +}; +export const InheritWithTypographyAsChild = Template.bind({}); +InheritWithTypographyAsChild.args = { + variant: 'inherit', + children: demo(Typography, { styleVariant: 'strike' }, `Typography ${COMPONENT_NAME}`), +}; +export const CustomStyles = Template.bind({}); +CustomStyles.args = { + variant: 'inherit', + children: `Custom Styled ${COMPONENT_NAME}`, + styles: { + backgroundColor: 'lightblue', + color: 'white', + padding: '1rem', + }, +}; +export const WithUnderline = () => + demo( + 'div', + { style: { display: 'flex', flexDirection: 'column', gap: '12px' } }, + demo(Link, { href: '#', underline: 'default' }, 'Default underline'), + demo(Link, { href: '#', underline: 'highlight' }, 'Highlight underline (on hover)'), + demo(Link, { href: '#', underline: 'none' }, 'No underline') + ); +WithUnderline.parameters = { + docs: { + description: { + story: + 'The `underline` prop controls underline behaviour: `default` (always), `highlight` (on hover), or `none`.', + }, + source: {}, + }, +}; +export const WithSizes = () => + demo( + 'div', + { style: { display: 'flex', gap: '16px', alignItems: 'center' } }, + demo(Link, { href: '#', size: 'sm' }, 'Small'), + demo(Link, { href: '#', size: 'md' }, 'Medium'), + demo(Link, { href: '#', size: 'lg' }, 'Large') + ); +WithSizes.parameters = { + docs: { + description: { + story: 'Link supports `sm`, `md`, and `lg` size variants controlling font size.', + }, + source: {}, + }, +}; +export const WithAccessibility = Template.bind({}); +WithAccessibility.args = { + children: 'Accessible Link', + href: 'https://storybook.cto-rnd-system-design.griddynamics.net/', + ariaLabel: 'Navigate to Griddynamics Storybook', +}; +WithAccessibility.tags = ['a11y']; +WithAccessibility.parameters = { + a11y: { + test: 'error', + }, + docs: { + disable: true, + }, +}; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { link: defaultTheme.link } }); +DefaultTokens.parameters = { + layout: 'padded', +}; diff --git a/libs/web-components/stories/fixtures/Loader.generated.js b/libs/web-components/stories/fixtures/Loader.generated.js new file mode 100644 index 0000000..5fa0677 --- /dev/null +++ b/libs/web-components/stories/fixtures/Loader.generated.js @@ -0,0 +1,283 @@ +// Generated from libs/ui/src/components/atoms/Loader/Loader.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +import { loaderStories } from '../loader-examples'; +const COMPONENT_NAME = 'Loader'; +const Loader = 'gd-loader'; +const Row = 'Row'; +const Column = 'Column'; +const Typography = 'gd-typography'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/Loader', + component: Loader, + tags: ['autodocs'], + args: { + withWrapper: true, + name: 'circle', + variant: 'inline', + size: 'md', + }, + parameters: { + layout: 'centered', + docs: { + description: { + component: ` + The \`Loader\` component is a versatile loading indicator designed to provide visual feedback during asynchronous operations. + It seamlessly integrates into any part of your application to enhance user experience by clearly showing loading states. +

+ +

🎯 Core Features

+
    +
  • Multiple Animation Types – Two built-in animations: "circle" (smooth rotation) and "dots" (rhythmic pulsing). Support for custom animations via children prop
  • +
  • Flexible Positioning – Three wrapper variants: Inline (natural flow), Section (overlay within container), FullPage (full-screen with portal)
  • +
  • Size Variants – Five responsive sizes (xs, sm, md, lg, xl) for different contexts from inline text to full-page overlays
  • +
  • Border Radius Control – Rounded prop for styling dots animation with border radius (none, default, round, xs, sm, md, lg, xl). Note: Only applies to "dots", not "circle"
  • +
  • Wrapper Control – Optional wrapper container with customizable HTML element or React component for semantic flexibility
  • +
  • Theme Integration – Full theme customization with design tokens for colors, sizes, and animations
  • +
  • Custom Content – Replace default animations with custom React content while maintaining wrapper functionality
  • +
+ +

📋 Usage Patterns

+
    +
  • Inline Loading – Use \`variant="inline"\` for text replacements and inline content: \`\`
  • +
  • Section Overlay – Use \`variant="section"\` for container-level loading states with absolute positioning
  • +
  • Full Page – Use \`variant="fullPage"\` for modal-style full-screen loading overlays with portal rendering
  • +
  • Button Integration – Use \`withWrapper={false}\` and \`size="sm"\` for compact button loaders: \`\`
  • +
  • Custom Animations – Pass custom React content via \`children\` prop for branded loading animations
  • +
+ +

💡 Best Practices

+
    +
  • Use "circle" animation for general loading states and "dots" for more playful or casual interfaces
  • +
  • Choose size based on context: xs/sm for inline, md for buttons, lg/xl for section/full-page
  • +
  • Use \`rounded\` prop to style dots animation corners when desired (does not apply to circle)
  • +
  • Set \`withWrapper={false}\` when embedding in buttons or custom containers to avoid wrapper styling
  • +
  • Use Section variant with relative positioned containers for localized loading states
  • +
  • Prefer theme tokens over custom styles for consistent design system integration
  • +
+ `, + }, + }, + }, +}; +export default meta; +const Template = (args) => demo(Loader, { ...args }); +export const Default = Template.bind({}); +Default.args = { + name: 'circle', + size: 'md', + variant: 'inline', +}; +Default.parameters = { + docs: { + source: {}, + description: { + story: + 'Default loader with circle animation, medium size, and inline variant. This is the standard loading indicator for most use cases.', + }, + }, +}; +export const LoaderNames = () => { + return demo( + Row, + { gap: '30px', align: 'center' }, + demo( + Column, + { gap: '10px' }, + demo(Typography, { variant: 'h6' }, 'Circle Animation'), + demo(Loader, { name: 'circle', size: 'md' }) + ), + demo( + Column, + { gap: '10px' }, + demo(Typography, { variant: 'h6' }, 'Dots Animation'), + demo(Loader, { name: 'dots', size: 'md' }) + ) + ); +}; +LoaderNames.parameters = { + docs: { + source: {}, + description: { + story: + 'Two animation types available: "circle" provides a smooth rotating animation ideal for general loading states, while "dots" offers a rhythmic pulsing sequence suitable for more playful interfaces.', + }, + }, +}; +export const LoaderSizes = () => { + return demo( + Column, + { gap: '20px' }, + demo( + Column, + { gap: '10px' }, + demo(Typography, { variant: 'h5' }, 'Loader Sizes'), + demo(Typography, { variant: undefined }, 'Five size variants from extra small to extra large') + ), + demo( + Row, + { gap: '20px', align: 'center' }, + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'circle', size: 'xs' }), + demo(Typography, { variant: undefined }, 'XS') + ), + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'circle', size: 'sm' }), + demo(Typography, { variant: undefined }, 'SM') + ), + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'circle', size: 'md' }), + demo(Typography, { variant: undefined }, 'MD') + ), + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'circle', size: 'lg' }), + demo(Typography, { variant: undefined }, 'LG') + ), + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'circle', size: 'xl' }), + demo(Typography, { variant: undefined }, 'XL') + ) + ) + ); +}; +LoaderSizes.parameters = { + layout: 'padded', + docs: { + source: {}, + description: { + story: + 'Five size variants available: xs (extra small) for inline text, sm (small) for buttons, md (medium) for standard use, lg (large) for sections, and xl (extra large) for full-page overlays.', + }, + }, +}; +export const LoaderRounded = () => { + return demo( + Column, + { gap: '20px' }, + demo( + Column, + { gap: '10px' }, + demo(Typography, { variant: 'h5' }, 'Loader with Rounded Border'), + demo( + Typography, + { variant: undefined }, + 'The rounded prop controls border radius for dots animation. Note: rounded prop only applies to "dots" animation type, not "circle"' + ) + ), + demo( + Row, + { gap: '20px', align: 'center' }, + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'dots', size: 'md', rounded: 'none' }), + demo(Typography, { variant: undefined }, 'None') + ), + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'dots', size: 'md', rounded: 'xs' }), + demo(Typography, { variant: undefined }, 'XS') + ), + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'dots', size: 'md', rounded: 'sm' }), + demo(Typography, { variant: undefined }, 'SM') + ), + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'dots', size: 'md', rounded: 'md' }), + demo(Typography, { variant: undefined }, 'MD') + ), + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'dots', size: 'md', rounded: 'lg' }), + demo(Typography, { variant: undefined }, 'LG') + ), + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'dots', size: 'md', rounded: 'xl' }), + demo(Typography, { variant: undefined }, 'XL') + ), + demo( + Column, + { gap: '8px', align: 'center' }, + demo(Loader, { name: 'dots', size: 'md', rounded: 'round' }), + demo(Typography, { variant: undefined }, 'Round') + ) + ) + ); +}; +LoaderRounded.parameters = { + layout: 'padded', + docs: { + source: {}, + description: { + story: + 'Rounded prop controls the border radius of dots animation. Available options: none, default, round, xs, sm, md, lg, xl. **Important**: The rounded prop only applies to "dots" animation type. Circle animations are always circular and do not use border radius.', + }, + }, +}; +export const LoaderVariantsWithWrapperViewAsHeaderTag = Template.bind({}); +LoaderVariantsWithWrapperViewAsHeaderTag.args = { + variant: 'inline', + WrapperView: 'header', +}; +LoaderVariantsWithWrapperViewAsHeaderTag.parameters = { + docs: { + source: {}, + description: { + story: + 'Customize the wrapper element using WrapperView prop. Use semantic HTML elements like "header", "section", "div", or React components for better accessibility and structure.', + }, + }, +}; +export const LoaderSectionVariant = loaderStories.LoaderSectionVariant; +export const SectionLoaderButtonVariant = loaderStories.SectionLoaderButtonVariant; +export const InlineLoaderButtonVariant = loaderStories.InlineLoaderButtonVariant; +export const WithAccessibility = () => { + return demo( + 'div', + { style: { display: 'flex', gap: '40px' }, role: 'status', 'aria-label': 'Loading content' }, + demo(Column, { gap: '10px' }, demo(Loader, { name: 'circle', size: 'md' })), + demo(Column, { gap: '10px' }, demo(Loader, { name: 'dots', size: 'md' })) + ); +}; +WithAccessibility.parameters = { + a11y: { + test: 'error', + }, + docs: { + disable: true, + }, +}; +WithAccessibility.tags = ['a11y']; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { loader: defaultTheme.loader } }); +DefaultTokens.parameters = { + layout: 'padded', + docs: { + source: {}, + description: { + story: + 'View the default theme tokens used by the Loader component. These tokens control colors, sizes, animations, and spacing for all loader variants.', + }, + }, +}; diff --git a/libs/web-components/stories/fixtures/Skeleton.generated.js b/libs/web-components/stories/fixtures/Skeleton.generated.js new file mode 100644 index 0000000..84b4361 --- /dev/null +++ b/libs/web-components/stories/fixtures/Skeleton.generated.js @@ -0,0 +1,157 @@ +// Generated from libs/ui/src/components/atoms/Skeleton/Skeleton.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'Skeleton'; +const Skeleton = 'gd-skeleton'; +const Typography = 'gd-typography'; +const Column = 'Column'; +const Row = 'Row'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/Skeleton', + component: Skeleton, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + '\n The `Skeleton` component is a visual placeholder used to indicate that content is loading.\n
\n It provides a better user experience by showing a loading state, preventing layout shifts and giving users a sense of progress.\n
\n
\n

Key Features:

\n
    \n
  • \n Customizable Variants\n
      \n
    • Rounded - Default style for text placeholders
    • \n
    • Rectangular - For content blocks and images
    • \n
    • Circular - For avatars and icons
    • \n
    \n
  • \n
  • \n Animation Options\n
      \n
    • Default theme loading animation
    • \n
    • Custom animation support
    • \n
    • Utility class compatibility (e.g. utility CSS)
    • \n
    \n
  • \n
  • Size Customization - Flexible width and height settings
  • \n
  • Theme-Aware Colors - Supports both raw CSS colors and theme/palette token aliases through the backgroundColor prop
  • \n
  • Child Content Support - Can wrap and animate child elements
  • \n
  • Layout Integration - Works within complex layouts
  • \n
\n ', + }, + }, + }, +}; +export default meta; +export const Default = { + args: { + width: '250px', + height: '15px', + variant: 'rounded', + }, + parameters: { + docs: { + description: { + story: 'The `Default` story shows the standard rounded skeleton, which is ideal for text-line placeholders.', + }, + }, + }, +}; +export const Circular = { + args: { + width: '80px', + height: '80px', + variant: 'circular', + }, + parameters: { + docs: { + description: { + story: 'The `Circular` variant is perfect for avatar or icon placeholders.', + }, + }, + }, +}; +export const Rectangular = { + args: { + width: '250px', + height: '125px', + variant: 'rectangular', + }, + parameters: { + docs: { + description: { + story: + 'The `Rectangular` variant is suitable for larger content blocks like images, cards, or video placeholders.', + }, + }, + }, +}; +export const WithThemeColor = { + args: { + width: '250px', + height: '40px', + variant: 'rectangular', + backgroundColor: 'theme.palette.success.main', + }, + parameters: { + docs: { + description: { + story: + 'This story shows the top-level `backgroundColor` prop using a theme-aware palette alias. Raw CSS colors like `#34A853` also work, but token values keep the component aligned with the design system.', + }, + }, + }, +}; +export const WithChildren = { + name: 'Skeleton with Child Content', + args: { + width: '250px', + height: '50px', + children: demo(Typography, { variant: 'small' }, 'Loading Content...'), + }, + parameters: { + docs: { + description: { + story: + 'The Skeleton can also act as a wrapper, applying its loading animation to any child elements. This is useful for creating more complex or custom loading states.', + }, + }, + }, +}; +export const ComposedLayout = { + name: 'Example: Article Placeholder', + render: () => + demo( + 'div', + { style: { maxWidth: '480px', width: '100%' } }, + demo( + Column, + { gutter: 10 }, + demo( + Row, + { gutter: 10, justify: 'between', align: 'center' }, + demo(Column, null, demo(Skeleton, { variant: 'circular', styles: { width: '100px', height: '100px' } })), + demo( + Column, + { flex: '1' }, + demo(Typography, { variant: 'h3', styles: { marginBottom: '10px' } }, demo(Skeleton, null)), + demo(Typography, { variant: 'p', styles: { marginBottom: '10px' } }, demo(Skeleton, null)), + demo(Typography, { variant: 'small', styles: { marginBottom: '10px' } }, demo(Skeleton, null)) + ) + ) + ), + demo( + Row, + { gutter: 10, align: 'center', styles: { marginTop: '20px' } }, + demo(Typography, { variant: 'p', styles: { width: '100%' } }, demo(Skeleton, null)), + demo(Typography, { variant: 'p', styles: { width: '100%' } }, demo(Skeleton, null)), + demo(Typography, { variant: 'p', styles: { marginBottom: '10px', width: '100%' } }, demo(Skeleton, null)) + ) + ), + parameters: { + docs: { + description: { + story: + 'This story demonstrates a real-world use case by composing multiple skeletons to create a placeholder for a complex layout, such as an article preview or a user profile card. This approach helps maintain the page structure while data is loading, preventing content from shifting.', + }, + }, + }, +}; +export const WithAccessibility = { + ...Default, + parameters: { + ...Default.parameters, + a11y: { + test: 'error', + }, + docs: { + disable: true, + }, + }, + tags: ['a11y'], +}; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { skeleton: defaultTheme.skeleton } }); +DefaultTokens.parameters = { + layout: 'padded', +}; diff --git a/libs/web-components/stories/fixtures/Typography.generated.js b/libs/web-components/stories/fixtures/Typography.generated.js new file mode 100644 index 0000000..c9eef1e --- /dev/null +++ b/libs/web-components/stories/fixtures/Typography.generated.js @@ -0,0 +1,339 @@ +// Generated from libs/ui/src/components/atoms/Typography/Typography.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'Typography'; +const Typography = 'gd-typography'; +const Column = 'Column'; +const Row = 'Row'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/Typography', + component: Typography, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +The \`Typography\` component is a powerful and flexible text element designed to provide consistent, theme-aware typography across your application. +It serves as the foundation for all text rendering, offering comprehensive control over styling, semantics, and accessibility. +

+ +

🎯 Core Features

+ +
    +
  • Semantic HTML Variants – Built-in variants map to semantic HTML elements: \`h1\`-\`h6\` (headings), \`p\` (body1), \`small\` (body2), \`code\`/\`kbd\` (code blocks), \`span\` (inherit), \`div\` (display), \`strong\` (bold), \`i\` (italic), \`caption\`, and \`header\`
  • +
  • Box Model Props Support – Apply layout styles directly without wrappers: \`\`. Supports all spacing, sizing, and positioning props (margin, padding, width, height, maxWidth, etc.)
  • +
  • Dynamic Element Rendering – Use the \`as\` prop to render any HTML element or React component while preserving all Typography styles (e.g., h1 styles on a span)
  • +
  • Flexible Display Sizing – The \`Display\` variant supports independent size control (xs, sm, md, lg, xl) for hero text and marketing content
  • +
  • Composable Style Variants – Apply multiple style modifiers simultaneously: \`light\`, \`normal\`, \`semibold\`, \`bold\`, \`italic\`, \`small\`, \`uppercase\`, \`lowercase\`, \`underline\`, \`strike\`
  • +
  • Theme Integration – Fully integrated with design tokens, supporting customizable colors, font families, sizes, weights, and line heights through the theme system
  • +
  • Text Alignment Control – Supports all standard CSS text alignments: \`start\`, \`end\`, \`center\`, \`left\`, \`right\`, \`justify\`, and more
  • +
  • Accessibility First – Proper semantic HTML rendering ensures screen reader compatibility and keyboard navigation support
  • +
+ +

📋 Typography Variants

+
    +
  • Headings: \`h1\`, \`h2\`, \`h3\`, \`h4\`, \`h5\`, \`h6\` – Semantic heading hierarchy with automatic margins and sizing
  • +
  • Body Text: \`p\` (body1), \`small\` (body2) – Primary and secondary body text for content
  • +
  • Display: \`div\` – Large display text with size variants (xs-xl) for hero sections and marketing content
  • +
  • Code: \`code\`, \`kbd\` – Monospace code blocks with specialized font family
  • +
  • Special: \`caption\`, \`header\`, \`span\` (inherit), \`strong\`, \`i\` – Specialized text elements
  • +
+ +

💡 Best Practices

+
    +
  • Use semantic variants (\`h1\`-\`h6\`) for headings to maintain proper document structure
  • +
  • Reserve \`h1\` for page titles (one per page) for SEO and accessibility
  • +
  • Use \`body1\` for primary content and \`body2\` for supporting/secondary text
  • +
  • Apply the \`as\` prop when you need different visual styles than semantic meaning (e.g., \`variant="h2" as="h3"\`)
  • +
  • Combine \`styleVariant\` for rich text formatting (e.g., \`["bold", "italic"]\`)
  • +
  • Use \`display\` variant with size control for impactful hero text and headlines
  • +
  • Leverage theme color tokens through the \`color\` prop for consistent brand colors
  • +
+ + +
+
+ +

🧩 Web Components track (CTORNDSD-646)

+Verdict — Native element + shared token CSS. The clearest verdict in the analysis, and the only one with direct measured support. Zero behavior — its whole job is mapping a variant to token-driven CSS. As a custom element it hides a real <h1> from every light-DOM query (confirmed: document.querySelector('h1') finds nothing), while a shared stylesheet delivers the same byte saving and keeps the heading discoverable. +
+Decision rule and full rationale: docs/webcomponents-migration/05-native-html-guidelines.md. +`, + }, + }, + }, +}; +export default meta; +const Template = (args) => demo(Typography, { ...args }); +export const Default = Template.bind({}); +Default.args = { + children: 'This is a default body1 text', + variant: 'p', + align: 'start', +}; +Default.parameters = { + docs: { + source: {}, + description: { + story: ` +The default Typography component renders as a \`

\` tag with standard body text styling. +

+**Key Features:** +- Uses theme tokens for consistent typography +- Inherits parent color by default +- Supports all box model props (margin, padding, etc.) directly without needing a wrapper + `, + }, + }, +}; +export const AsCustomComponent = Template.bind({}); +AsCustomComponent.args = { + children: 'H4 styles rendered as Row component', + variant: 'h4', + as: 'div', +}; +AsCustomComponent.parameters = { + docs: { + source: {}, + description: { + story: ` +The \`as\` prop enables polymorphic rendering—apply Typography styles to any React component. +

+This example shows \`h4\` typography styles applied to a \`Row\` component, combining text styling with layout capabilities. Perfect for creating styled headings that also serve as flex containers. + `, + }, + }, +}; +export const AsCustomHtmlTag = Template.bind({}); +AsCustomHtmlTag.args = { + children: 'H3 styles rendered as paragraph', + variant: 'h3', + as: 'p', +}; +AsCustomHtmlTag.parameters = { + docs: { + source: {}, + description: { + story: ` +Separate visual presentation from semantic meaning using the \`as\` prop. +

+This renders a \`

\` tag with \`h3\` visual styling. Useful when you need specific visual hierarchy but different semantic HTML (e.g., for SEO or accessibility when you already have an h1/h2 on the page). + `, + }, + }, +}; +export const CombinedStyleVariant = Template.bind({}); +CombinedStyleVariant.args = { + children: 'This is a combined style variant text: italic, semibold, strike', + styleVariant: ['italic', 'semibold', 'strike'], +}; +CombinedStyleVariant.parameters = { + docs: { + source: {}, + description: { + story: ` +Combine multiple \`styleVariant\` modifiers to create rich text formatting. +

+The \`styleVariant\` prop accepts an array of style modifiers that are applied simultaneously. This example combines italic style, semibold weight, and strikethrough decoration. Great for creating distinctive text treatments without custom CSS. + `, + }, + }, +}; +export const Disclaimers = () => { + return demo( + Column, + null, + demo(Typography, { styleVariant: ['italic', 'semibold', 'strike'] }, 'Italic SemiBold Strike Text'), + demo(Typography, { styleVariant: 'semibold' }, 'Semibold Text'), + demo(Typography, { styleVariant: 'normal' }, 'Normal Text'), + demo(Typography, { styleVariant: 'light' }, 'Light Text'), + demo(Typography, { styleVariant: 'underline' }, 'Underline Text'), + demo(Typography, { styleVariant: 'strike' }, 'Strike Text'), + demo(Typography, { styleVariant: 'uppercase' }, 'Uppercase Text'), + demo(Typography, { styleVariant: 'lowercase' }, 'Lowercase Text'), + demo(Typography, { styleVariant: 'small' }, 'Small Text') + ); +}; +Disclaimers.parameters = { + docs: { + source: {}, + description: { + story: ` +All available \`styleVariant\` options demonstrated in one place. +

+**Available Style Variants:** +- \`light\`, \`normal\`, \`semibold\`, \`bold\` - Font weight modifiers +- \`italic\` - Italic font style +- \`small\` - Smaller font size +- \`uppercase\`, \`lowercase\` - Text transformation +- \`underline\`, \`strike\` - Text decoration +

+Mix and match these modifiers to create custom text treatments. Each variant applies specific CSS properties via design tokens. + `, + }, + }, +}; +export const Display = () => { + return demo( + Fragment, + null, + demo( + Row, + { styles: { paddingBottom: '30px' } }, + demo(Typography, { variant: 'h3' }, 'Display(tag - span) - variant + size(only for Display variant)') + ), + demo(Row, null, demo(Typography, { variant: 'div', size: 'xl' }, 'Size - xl')), + demo( + Row, + null, + demo(Typography, { variant: 'div', size: 'xl' }, demo(Typography, { variant: 'strong' }, 'Size - xl')) + ), + demo(Row, null, demo(Typography, { variant: 'div', size: 'lg' }, 'Size - lg')), + demo( + Row, + null, + demo(Typography, { variant: 'div', size: 'lg' }, demo(Typography, { variant: 'strong' }, 'Size - lg')) + ), + demo(Row, null, demo(Typography, { variant: 'div', size: 'md' }, 'Size - md')), + demo( + Row, + null, + demo(Typography, { variant: 'div', size: 'md' }, demo(Typography, { variant: 'strong' }, 'Size - md')) + ), + demo(Row, null, demo(Typography, { variant: 'div', size: 'sm' }, 'Size - sm')), + demo( + Row, + null, + demo(Typography, { variant: 'div', size: 'sm' }, demo(Typography, { variant: 'strong' }, 'Size - sm')) + ), + demo(Row, null, demo(Typography, { variant: 'div', size: 'xs' }, 'Size - xs')), + demo( + Row, + null, + demo(Typography, { variant: 'div', size: 'xs' }, demo(Typography, { variant: 'strong' }, 'Size - xs')) + ) + ); +}; +Display.parameters = { + docs: { + source: {}, + description: { + story: ` +The \`Display\` variant (\`variant="div"\`) is designed for large, impactful text like hero headlines and marketing content. +

+**Key Features:** +- Renders as a \`

\` element +- Supports fine-grained size control via the \`size\` prop (xs, sm, md, lg, xl) +- Each size maps to specific font-size and line-height tokens +- Can be combined with \`styleVariant\` for bold or other effects +- Ideal for landing page headers, hero sections, and call-to-action text +

+**Note:** The \`size\` prop ONLY works with \`variant="div"\` (Display variant) and is ignored for all other variants. + `, + }, + }, +}; +export const Heading = () => { + return demo( + Fragment, + null, + demo(Typography, { variant: 'h1' }, 'Heading 1'), + demo(Typography, { variant: 'h2' }, 'Heading 2'), + demo(Typography, { variant: 'h3' }, 'Heading 3'), + demo(Typography, { variant: 'h4' }, 'Heading 4'), + demo(Typography, { variant: 'h5' }, 'Heading 5'), + demo(Typography, { variant: 'h6' }, 'Heading 6') + ); +}; +Heading.parameters = { + docs: { + source: {}, + description: { + story: ` +All six semantic heading levels demonstrated with their default styling. +

+**Semantic Heading Hierarchy:** +- \`h1\` - Page title (use only once per page) +- \`h2\` - Major section headings +- \`h3\` - Subsection headings +- \`h4\` - Sub-subsection headings +- \`h5\` - Minor headings +- \`h6\` - Smallest heading level +

+Each heading variant includes automatic margins and sizing from design tokens. Maintain proper heading order (h1 → h2 → h3) for SEO and accessibility. +

+**Pro Tip:** Apply box model props directly for spacing, e.g., \`\` + `, + }, + }, +}; +export const Body = () => { + return demo( + Column, + null, + demo(Typography, { variant: 'p' }, 'Body1(tag - p) - Default Text'), + demo(Typography, { variant: 'p' }, 'Body1 - with ', demo(Typography, { variant: 'sup' }, 'Sup Text')), + demo(Typography, { variant: 'p' }, 'Body1 - with ', demo(Typography, { variant: 'sub' }, 'Sub Text')), + demo(Typography, { variant: 'small' }, 'Body2 '), + demo(Typography, { variant: 'caption', as: 'div' }, 'Caption Text'), + demo(Typography, { variant: 'i' }, 'Inherit Italic Text'), + demo(Typography, { variant: 'strong' }, 'Bold Text') + ); +}; +Body.parameters = { + docs: { + source: {}, + description: { + story: ` +Body text and special text variants for everyday content. +

+**Body Text Variants:** +- \`p\` (Body1) - Primary body text, renders as \`

\` tag +- \`small\` (Body2) - Secondary text, renders as \`\` tag +- \`caption\` - Small caption text for labels and metadata +- \`strong\` - Bold emphasis, renders as \`\` tag +- \`i\` - Italic emphasis, renders as \`\` tag +- \`sup\` - Superscript text +- \`sub\` - Subscript text +

+**Layout Props:** Typography accepts all box model props directly (margin, padding, width, etc.), eliminating the need for wrapper elements: +\`\`\`tsx + + Text with spacing + +\`\`\` + `, + }, + }, +}; +export const WithAccessibility = { + render: Disclaimers, + parameters: { + a11y: { + test: 'error', + }, + docs: { + disable: true, + }, + }, + tags: ['a11y'], +}; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { typography: defaultTheme.typography } }); +DefaultTokens.parameters = { + layout: 'padded', + docs: { + source: {}, + description: { + story: ` +Explore all typography design tokens used by the Typography component. +

+These tokens define font sizes, weights, line heights, and spacing for each variant. Tokens are fully customizable through the theme system, allowing for brand-specific typography scales. + `, + }, + }, +}; diff --git a/libs/web-components/stories/fixtures/Wrapper.generated.js b/libs/web-components/stories/fixtures/Wrapper.generated.js new file mode 100644 index 0000000..419a899 --- /dev/null +++ b/libs/web-components/stories/fixtures/Wrapper.generated.js @@ -0,0 +1,207 @@ +// Generated from libs/ui/src/components/atoms/Wrapper/Wrapper.stories.tsx. Run npm run generate:web-component-stories. +// Native DOM recipes only: no React runtime or React component implementation is imported. +import { demo, Fragment } from '../native-story'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +const COMPONENT_NAME = 'Wrapper'; +const Wrapper = 'gd-wrapper'; +const Typography = 'gd-typography'; +const TokenViewer = 'TokenViewer'; +const meta = { + title: 'Atoms/Wrapper', + component: Wrapper, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: ` +

Overview

+ The \`Wrapper\` component serves as a foundational building block for creating flexible and accessible layouts. It encapsulates common container patterns while providing a rich API for customization and semantic structure. + + This versatile container adapts to different contexts through its variant system while maintaining consistent behavior and accessibility standards. It's designed to work seamlessly within your component hierarchy, whether you need a simple inline wrapper or a full-page overlay. + +

Key Features:

+
    +
  • + Layout Control +
      +
    • Flexible container sizing with responsive options
    • +
    • Direct theme integration for consistent styling
    • +
    • Predictable nesting behavior
    • +
    +
  • +
    +
  • Composability - Works seamlessly with other components
  • +
  • Semantic HTML - Appropriate element selection for accessibility
  • +
+ +

Accessibility

+
    +
  • Semantic HTML elements for improved screen reader navigation
  • +
  • ARIA attributes automatically applied based on variant
  • +
  • Keyboard focus management for interactive variants
  • +
+ +

Layout Props:

+
+ Dimensions +
+
    +
  • width/height: Set container size with various units (px, rem, %, vh/vw)
  • +
  • minWidth/minHeight: Define minimum size constraints
  • +
  • maxWidth/maxHeight: Set maximum size limits
  • +
+ + Spacing +
    +
  • margin/padding: Control inner and outer spacing
  • +
  • border: Comprehensive border customization
  • +
  • position: Adjust element positioning (relative, absolute, fixed)
  • +
  • display: Control layout behavior (flex, block, inline-flex)
  • +
+ + +
+
+ +

🧩 Web Components track (CTORNDSD-646)

+Verdict — No abstraction — shared utility CSS. Zero behavior and no visual surface of its own. Utility classes over the same tokens deliver the same result at zero runtime cost. +
+Decision rule and full rationale: docs/webcomponents-migration/05-native-html-guidelines.md. +`, + }, + }, + }, +}; +export default meta; +export const InteractiveExample = { + name: 'Interactive Example', + render: (args) => demo(Wrapper, { ...args }), + args: { + variant: 'inline', + children: 'This is a basic div wrapper. Change the variant in the controls to see different wrapper types.', + styles: { + padding: '20px', + border: '2px dashed #007bff', + borderRadius: '8px', + backgroundColor: '#f0f8ff', + }, + }, + parameters: { + docs: { + description: { + story: + 'This story provides an interactive demonstration of the `Wrapper` component. Use the controls in the Addons panel to change the `variant` and see how it affects the underlying HTML element and its appearance.', + }, + }, + }, +}; +export const SectionWrapper = { + name: 'Variant: Section', + args: { + variant: 'section', + children: demo( + Typography, + null, + 'This content is semantically grouped within a `section` element, which is ideal for thematically related content.' + ), + styles: { + padding: '20px', + width: '400px', + border: '1px solid #ccc', + backgroundColor: '#f9f9f9', + }, + }, + parameters: { + docs: { + description: { + story: + 'The `Section` variant renders a `
` HTML element. It is a block-level container used for grouping content that has a common theme or purpose, such as a chapter, a header, or a footer.', + }, + }, + }, +}; +export const InlineWrapper = { + name: 'Variant: Inline (Span)', + render: (args) => + demo(Typography, null, 'This is a line of text, and ', demo(Wrapper, { ...args }), ' is used for inline content.'), + args: { + variant: 'inline', + children: 'this part is wrapped in a span', + styles: { + padding: '4px 8px', + borderRadius: '4px', + backgroundColor: '#fffbe6', + color: '#856404', + fontWeight: 'bold', + }, + }, + parameters: { + docs: { + description: { + story: + 'The `Inline` variant renders a `` HTML element. It is used for wrapping small pieces of content within a larger block of text, without creating a line break. This is useful for applying styles or behavior to a specific word or phrase.', + }, + }, + }, +}; +export const FullPageWrapper = { + name: 'Variant: Full Page', + args: { + variant: 'fullPage', + children: demo( + Typography, + { styles: { color: 'white' } }, + 'This wrapper covers the entire viewport, perfect for modals or overlays.' + ), + styles: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + }, + parameters: { + layout: 'fullscreen', + docs: { + description: { + story: + 'The `FullPage` variant is designed to create a container that spans the entire viewport. It is typically used as a background for modal dialogs, loading spinners, or other UI elements that need to overlay the entire page content.', + }, + }, + }, +}; +export const CustomTagWrapper = { + name: 'Custom tag Wrapper', + args: { + as: 'section', + children: 'Custom tag Wrapper', + }, + parameters: { + layout: 'fullscreen', + docs: { + description: { + story: + 'The `FullPage` variant is designed to create a container that spans the entire viewport. It is typically used as a background for modal dialogs, loading spinners, or other UI elements that need to overlay the entire page content.', + }, + }, + }, +}; +export const WithAccessibility = { + ...InteractiveExample, + parameters: { + ...InteractiveExample.parameters, + a11y: { + test: 'error', + }, + docs: { + disable: true, + }, + }, + tags: ['a11y'], +}; +export const DefaultTokens = () => demo(TokenViewer, { tokens: { wrapper: defaultTheme.wrapper } }); +DefaultTokens.parameters = { + layout: 'padded', +}; diff --git a/libs/web-components/stories/helpers.ts b/libs/web-components/stories/helpers.ts index 73597b1..9629837 100644 --- a/libs/web-components/stories/helpers.ts +++ b/libs/web-components/stories/helpers.ts @@ -1,6 +1,7 @@ import { defaultTheme } from 'gd-design-library/tokens'; import type { DesignCoreTheme } from 'gd-design-core'; import type { ArgTypes as StorybookArgTypes } from '@storybook/web-components-vite'; +import { action } from 'storybook/actions'; import type {} from '../src/index'; import { reactStorybookArgTypes } from './react-storybook-arg-types.generated'; @@ -143,20 +144,12 @@ function lightDom(node: Element) { * Storybook cannot infer useful source from our imperative DOM render functions. */ export function storySource(canvas: HTMLElement | undefined, fallback: string) { if (!canvas) return fallback; - const isNestedCustomElement = (node: HTMLElement) => { - let parent = node.parentElement; - while (parent && parent !== canvas) { - if (parent.localName.startsWith('gd-')) return true; - parent = parent.parentElement; - } - return false; - }; - const nodes = Array.from(canvas.querySelectorAll('*')).filter( - (node) => node.localName.startsWith('gd-') && !isNestedCustomElement(node) + const nodes = Array.from(canvas.querySelectorAll('*')).filter((node) => + node.localName.startsWith('gd-') ); if (!nodes.length) return fallback; - const markup: string[] = []; + const markup = lightDom(canvas); const setup: string[] = ["import 'web-components';", "import { defaultTheme } from 'gd-design-library/tokens';", '']; nodes.forEach((node, index) => { const tag = node.localName as Tag; @@ -187,12 +180,6 @@ export function storySource(canvas: HTMLElement | undefined, fallback: string) { } else attributes.set(attribute, String(value)); } else if (typeof value !== 'function' && value !== undefined) assignments.push([property, value]); } - const attributeText = [...attributes] - .map(([name, value]) => (value === '' ? name : `${name}="${escapeHtml(value)}"`)) - .join(' '); - const content = lightDom(node); - markup.push(`<${tag}${attributeText ? ` ${attributeText}` : ''}>${content}`); - const variable = nodes.length === 1 ? 'component' : `component${index + 1}`; const sameTagIndex = nodes.slice(0, index).filter((candidate) => candidate.localName === tag).length; setup.push( @@ -209,9 +196,7 @@ export function storySource(canvas: HTMLElement | undefined, fallback: string) { setup.push(''); }); - return `${markup.join('\n\n')}\n\n`; + return `${markup}\n\n`; } /** Use the same property-based theme contract as application consumers. */ @@ -219,6 +204,9 @@ export function element(tag: T, props: Partial action(eventName)((event as CustomEvent).detail)); + } return node; } @@ -325,3 +313,14 @@ export const items = [ export const portrait = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"%3E%3Crect width="64" height="64" fill="%2391b8d8"/%3E%3Ccircle cx="32" cy="24" r="13" fill="%23f1c7a5"/%3E%3Cpath d="M8 64c3-18 14-27 24-27s21 9 24 27" fill="%233e6184"/%3E%3C/svg%3E'; + +/** Framework-neutral equivalent of the React stories' Row / Column layout. */ +export function stack(direction: 'row' | 'column', gap: string, ...children: Node[]) { + const node = document.createElement('div'); + node.style.display = 'flex'; + node.style.flexDirection = direction; + node.style.gap = gap; + if (direction === 'row') node.style.alignItems = 'center'; + node.append(...children); + return node; +} diff --git a/libs/web-components/stories/loader-examples.ts b/libs/web-components/stories/loader-examples.ts new file mode 100644 index 0000000..28d3561 --- /dev/null +++ b/libs/web-components/stories/loader-examples.ts @@ -0,0 +1,51 @@ +import { element, stack } from './helpers'; + +function example(mode: 'section' | 'button-section' | 'button-inline') { + const root = stack('column', '0', element('gd-typography', { variant: 'h3' }, 'Section 1')); + root.style.cssText += ';width:100%;margin:0 auto;background:#a5ffb4;padding:10px;position:relative'; + let showing = false; + const button = element('gd-button', { variant: 'outlined', styles: { position: 'relative' } }, 'Click me!'); + const link = element('gd-link', {}); + const innerLink = element('gd-link', {}); + const section = stack( + 'column', + '0', + stack('row', '0', element('gd-typography', { variant: 'h3' }, 'Section 2')), + stack('row', '0', innerLink) + ); + section.style.cssText += ';margin-top:20px;background:#ffa5a5;padding:10px;position:relative'; + const loader = element('gd-loader', { + variant: mode === 'button-inline' ? 'inline' : 'section', + name: mode === 'button-inline' ? 'circle' : 'dots', + size: mode === 'section' ? 'md' : 'sm', + withWrapper: mode !== 'button-inline', + }); + if (mode === 'button-inline') loader.slot = 'icon-end'; + const update = () => { + link.textContent = `${showing ? 'Hide' : 'Show'} Loader in ${mode === 'section' ? 'section 2' : 'button'}`; + innerLink.textContent = `${showing ? 'Hide' : 'Show'} Loader`; + button.textContent = showing ? 'Loading...' : 'Click me!'; + loader.remove(); + if (showing) (mode === 'section' ? section : button).append(loader); + }; + const toggle = () => { + showing = !showing; + update(); + }; + button.addEventListener('click', toggle); + link.addEventListener('click', toggle); + innerLink.addEventListener('click', toggle); + if (mode === 'section') root.append(link, section); + else { + if (mode === 'button-section') root.append(stack('row', '0', link)); + root.append(stack('row', '0', button)); + } + update(); + return root; +} + +export const loaderStories = { + LoaderSectionVariant: () => example('section'), + SectionLoaderButtonVariant: () => example('button-section'), + InlineLoaderButtonVariant: () => example('button-inline'), +}; diff --git a/libs/web-components/stories/native-story.ts b/libs/web-components/stories/native-story.ts new file mode 100644 index 0000000..35df9ed --- /dev/null +++ b/libs/web-components/stories/native-story.ts @@ -0,0 +1,180 @@ +import type { Meta, StoryObj, ArgTypes } from '@storybook/web-components-vite'; +import { defaultTheme } from 'gd-design-library/tokens'; +import { defaultTokenViewer } from './helpers'; + +type Props = Record; +type Recipe = { tag: string; props: Props; children: unknown[] }; +type SourceStory = { + args?: Props; + render?: (args: Props) => unknown; + parameters?: Props; + name?: string; + tags?: string[]; +}; +export const Fragment = 'Fragment'; +export function demo(tag: string, props: Props | null, ...children: unknown[]): Recipe { + return { tag, props: props ?? {}, children }; +} + +const layoutProps = new Set([ + 'padding', + 'paddingTop', + 'paddingBottom', + 'paddingLeft', + 'paddingRight', + 'margin', + 'marginTop', + 'marginBottom', + 'marginLeft', + 'marginRight', + 'gap', + 'width', + 'height', + 'minWidth', + 'maxWidth', + 'minHeight', + 'maxHeight', + 'flex', + 'alignItems', + 'justifyContent', +]); +const slots: Record = { + iconStart: 'icon-start', + iconEnd: 'icon-end', + fallbackComponent: 'fallback', + adornmentStart: 'adornment-start', + adornmentEnd: 'adornment-end', +}; +function cssValue(key: string, value: unknown) { + return typeof value === 'number' && !['flex', 'fontWeight', 'opacity', 'zIndex', 'lineHeight'].includes(key) + ? `${value}px` + : String(value); +} + +/** Render source example recipes using real gd-* elements, never React. */ +export function renderDemo(value: unknown): Node { + if (value instanceof Node) return value; + if (value == null || typeof value === 'boolean') return document.createTextNode(''); + if (Array.isArray(value)) { + const fragment = document.createDocumentFragment(); + fragment.append(...value.map(renderDemo)); + return fragment; + } + if (typeof value !== 'object') return document.createTextNode(String(value)); + const { tag, props, children } = value as Recipe; + if (!tag) throw new Error('A story child has no native recipe mapping'); + if (tag === Fragment) return renderDemo(children); + if (tag === 'TokenViewer') return defaultTokenViewer(Object.keys(props.tokens as Props)[0]); + const layout = tag === 'Row' || tag === 'Column'; + const node = document.createElement(layout ? 'div' : tag); + const custom = tag.startsWith('gd-'); + const target = node as unknown as Props; + const styles: Record = {}; + if (custom) target.theme = defaultTheme; + if (layout) { + const align = String(props.align ?? 'stretch'); + const justify = String(props.justify ?? 'start'); + Object.assign(node.style, { + display: 'flex', + flexDirection: tag === 'Row' ? 'row' : 'column', + flexWrap: 'wrap', + alignItems: align === 'start' || align === 'end' ? `flex-${align}` : align, + justifyContent: + justify === 'between' + ? 'space-between' + : justify === 'start' || justify === 'end' + ? `flex-${justify}` + : justify, + gap: cssValue('gap', props.gutter ?? 0), + }); + if (tag === 'Column') node.style.maxWidth = '100%'; + } + for (const [key, val] of Object.entries(props)) { + if (val === undefined || key === 'children' || key === 'key') continue; + if (slots[key]) { + if (val == null) continue; + const child = renderDemo(val); + const slotNode = child instanceof HTMLElement ? child : document.createElement('span'); + if (slotNode !== child) slotNode.append(child); + slotNode.slot = slots[key]; + node.append(slotNode); + } else if (key === 'buttonProps' && tag === 'gd-input-file') target.buttonVariant = (val as Props).variant; + else if (key === 'WrapperView' && tag === 'gd-loader') target.wrapperAs = val; + else if (key === 'styles' || key === 'style') Object.assign(styles, val); + else if (layoutProps.has(key) && (layout || !(key in node))) styles[key] = cssValue(key, val); + else if (/^on[A-Z]/.test(key) && typeof val === 'function') { + const callback = val as (value: unknown) => void; + const customEvent = ['onValueChange', 'onCounterChange', 'onChange', 'onDotClick'].includes(key) && custom; + node.addEventListener(customEvent ? 'gd-change' : key.slice(2).toLowerCase(), (event) => { + const detail = (event as CustomEvent).detail; + callback(customEvent ? (detail?.checked ?? detail?.value ?? detail?.index ?? detail) : event); + }); + } else if (key.startsWith('aria-') || key.startsWith('data-') || key === 'role') + node.setAttribute(key, String(val)); + else if (key === 'ariaLabel') { + node.setAttribute('aria-label', String(val)); + if (custom) target.ariaLabel = val; + } else if (layout && ['gutter', 'align', 'justify'].includes(key)) continue; + else target[key] = val; + } + if (custom) target.styles = { ...((target.styles as Props) ?? {}), ...styles }; + else + for (const [key, val] of Object.entries(styles)) + node.style.setProperty( + key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`), + cssValue(key, val) + ); + const content = children.length ? children : props.children; + if (content !== undefined && content !== null) node.append(renderDemo(content)); + return node; +} + +function nativeParameters(parameters: Props | undefined) { + if (!parameters) return parameters; + const docs = parameters.docs as Props | undefined; + const descriptions = docs?.description as Record | undefined; + if (!descriptions) return parameters; + return { + ...parameters, + docs: { + ...docs, + description: Object.fromEntries( + Object.entries(descriptions).map(([key, text]) => [ + key, + text.replace(/\s*(?:\s*)*

🧩[\s\S]*$/, '').replace(/React component/g, 'component'), + ]) + ), + }, + }; +} + +export function nativeMeta( + source: SourceStory & { title?: string; component?: string }, + argTypes: ArgTypes +): Meta { + return { + title: source.title, + tags: source.tags, + args: source.args, + parameters: nativeParameters(source.parameters), + argTypes, + render: (args) => renderDemo(demo(source.component!, args)) as HTMLElement, + }; +} + +export function nativeStory( + source: SourceStory | ((args: Props) => unknown), + meta: Meta, + displayName?: string +): StoryObj { + const story = source as SourceStory; + const render = typeof source === 'function' ? source : story.render; + return { + ...story, + args: story.args, + parameters: nativeParameters(story.parameters), + tags: story.tags, + ...(displayName ? { name: displayName } : {}), + ...(render ? { render: (args: Props) => renderDemo(render(args)) as HTMLElement } : { render: meta.render }), + } as StoryObj; +} diff --git a/libs/web-components/stories/react-storybook-arg-types.generated.ts b/libs/web-components/stories/react-storybook-arg-types.generated.ts index 40d2abb..e2cbdf6 100644 --- a/libs/web-components/stories/react-storybook-arg-types.generated.ts +++ b/libs/web-components/stories/react-storybook-arg-types.generated.ts @@ -1463,7 +1463,7 @@ export const reactStorybookArgTypes = { }, className: { name: 'className', - description: 'Provides a way to apply custom CSS classes, such as Tailwind utility classes for animations.', + description: 'Provides a way to apply custom CSS classes, such as utility CSS utility classes for animations.', table: { type: { summary: 'string', diff --git a/libs/web-components/test/native-story.spec.ts b/libs/web-components/test/native-story.spec.ts new file mode 100644 index 0000000..c3c5ca2 --- /dev/null +++ b/libs/web-components/test/native-story.spec.ts @@ -0,0 +1,56 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import '../src/index'; +import { demo, renderDemo } from '../stories/native-story'; + +const mounted: Node[] = []; +afterEach(() => mounted.splice(0).forEach((node) => node.parentNode?.removeChild(node))); +function mount(recipe: unknown) { + const node = renderDemo(recipe) as HTMLElement; + document.body.append(node); + mounted.push(node); + return node; +} + +describe('native story fixtures', () => { + it('creates independent native controls and retains nested slots and layout', async () => { + const recipe = demo( + 'Row', + { gap: '24px' }, + demo('gd-button', { variant: 'outlined', iconStart: demo('gd-icon', { name: 'check' }) }, 'Continue') + ); + const first = mount(recipe); + const second = mount(recipe); + expect(first.style.gap).toBe('24px'); + const button = first.querySelector('gd-button')!; + await button.updateComplete; + expect(button.variant).toBe('outlined'); + expect(button.querySelector('[slot="icon-start"]')?.localName).toBe('gd-icon'); + expect(button.theme).toBeTruthy(); + button.remove(); + expect(second.querySelector('gd-button')?.textContent).toBe('Continue'); + }); + + it('forwards changed args and native change details to the source callback', async () => { + const changes: unknown[] = []; + const control = mount( + demo( + 'gd-checkbox', + { checked: true, disabled: false, onValueChange: (value: unknown) => changes.push(value) }, + 'Accept' + ) + ) as HTMLElementTagNameMap['gd-checkbox']; + await control.updateComplete; + expect(control.shadowRoot?.querySelector('input')?.checked).toBe(true); + control.dispatchEvent(new CustomEvent('gd-change', { detail: { checked: false } })); + expect(changes).toEqual([false]); + }); + + it('does not mask fallback slot content with empty children', async () => { + const file = mount( + demo('gd-input-file', { buttonProps: { variant: 'secondary' } }) + ) as HTMLElementTagNameMap['gd-input-file']; + await file.updateComplete; + expect(file.childNodes.length).toBe(0); + expect(file.buttonVariant).toBe('secondary'); + }); +}); diff --git a/libs/web-components/test/story-source.spec.ts b/libs/web-components/test/story-source.spec.ts index 5b02e13..2649038 100644 --- a/libs/web-components/test/story-source.spec.ts +++ b/libs/web-components/test/story-source.spec.ts @@ -52,6 +52,18 @@ describe('Storybook custom-element source', () => { expect(generated).toContain('name="success"'); expect(generated).toContain('size="md"'); expect(generated).not.toContain("document.querySelector('gd-icon')"); - expect(generated).not.toContain('const component2'); + expect(generated.match(/const component2 =/g)).toHaveLength(1); + expect(generated).toContain('component2.theme = defaultTheme;'); + }); + + it('preserves layout wrappers and initializes nested property-only inputs', () => { + const wrapper = document.createElement('div'); + wrapper.style.cssText = 'display: flex; gap: 24px;'; + wrapper.append(element('gd-select', { items, multiple: true })); + const generated = source(wrapper); + expect(generated).toContain('
'); + expect(generated).toContain('component.items = ['); + expect(generated).toContain('component.theme = defaultTheme;'); + expect(generated.match(/ Date: Fri, 2 Oct 2026 13:27:29 +0200 Subject: [PATCH 12/17] test(input): update token snapshots --- .../components/atoms/Input/__snapshots__/Input.test.tsx.snap | 2 +- .../molecules/Counter/__snapshots__/Counter.test.tsx.snap | 2 +- .../components/molecules/Form/__snapshots__/Form.test.tsx.snap | 2 +- .../Card/CardCounter/__snapshots__/CardCounter.test.tsx.snap | 2 +- .../organisms/Search/__snapshots__/Search.test.tsx.snap | 2 +- .../SearchInput/__snapshots__/SearchInput.test.tsx.snap | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/ui/src/components/atoms/Input/__snapshots__/Input.test.tsx.snap b/libs/ui/src/components/atoms/Input/__snapshots__/Input.test.tsx.snap index 7c7f21d..f0d629f 100644 --- a/libs/ui/src/components/atoms/Input/__snapshots__/Input.test.tsx.snap +++ b/libs/ui/src/components/atoms/Input/__snapshots__/Input.test.tsx.snap @@ -12,7 +12,7 @@ exports[`Input > SHOULD match snapshot 1`] = ` > SHOULD match snapshot 1`] = ` aria-required="false" aria-valuemax="10" aria-valuemin="1" - class="css-1ojf3jn" + class="css-f4xd5q" data-testid="Counter-input" role="spinbutton" tabindex="0" diff --git a/libs/ui/src/components/molecules/Form/__snapshots__/Form.test.tsx.snap b/libs/ui/src/components/molecules/Form/__snapshots__/Form.test.tsx.snap index 6dac6df..fbdae60 100644 --- a/libs/ui/src/components/molecules/Form/__snapshots__/Form.test.tsx.snap +++ b/libs/ui/src/components/molecules/Form/__snapshots__/Form.test.tsx.snap @@ -17,7 +17,7 @@ exports[`Form > SHOULD match snapshot 1`] = ` > SHOULD match snapshot 1`] = ` aria-label="Quantity value" aria-required="false" aria-valuemin="1" - class="css-1ojf3jn" + class="css-f4xd5q" data-testid="Counter-input" role="spinbutton" tabindex="0" diff --git a/libs/ui/src/components/organisms/Search/__snapshots__/Search.test.tsx.snap b/libs/ui/src/components/organisms/Search/__snapshots__/Search.test.tsx.snap index 0a5c109..a75fee5 100644 --- a/libs/ui/src/components/organisms/Search/__snapshots__/Search.test.tsx.snap +++ b/libs/ui/src/components/organisms/Search/__snapshots__/Search.test.tsx.snap @@ -21,7 +21,7 @@ exports[`Search > SHOULD match snapshot 1`] = ` SHOULD match snapshot 1`] = ` > Date: Fri, 2 Oct 2026 13:44:17 +0200 Subject: [PATCH 13/17] fix(select): keep dropdown anchored while scrolling --- bin/storybook/native-checks.mjs | 33 ++++++ .../atoms/gd-select/gd-select.spec.ts | 52 +++++++++ .../components/atoms/gd-select/gd-select.ts | 100 ++++++++++++++++-- 3 files changed, 179 insertions(+), 6 deletions(-) diff --git a/bin/storybook/native-checks.mjs b/bin/storybook/native-checks.mjs index d717a70..78ec889 100644 --- a/bin/storybook/native-checks.mjs +++ b/bin/storybook/native-checks.mjs @@ -393,6 +393,39 @@ export async function checkNativeStories(page, base, webBase = `${base}/web-comp assert.equal(await page.locator('input').getAttribute('type'), 'text'); await open('atoms-select--default'); await page.getByRole('button', { name: 'Select', exact: true }).click(); + const selectAnchorBefore = await page.locator('gd-select').evaluate((select) => { + const trigger = select.shadowRoot.querySelector('.trigger').getBoundingClientRect(); + const dropdown = select.shadowRoot.querySelector('.dropdown').getBoundingClientRect(); + return { + placement: dropdown.top >= trigger.bottom ? 'bottom' : 'top', + gap: dropdown.top >= trigger.bottom ? dropdown.top - trigger.bottom : trigger.top - dropdown.bottom, + widthDelta: dropdown.width - trigger.width, + }; + }); + assert.ok(Math.abs(selectAnchorBefore.gap - 1) < 0.25, "Select dropdown must preserve React's 1px gap"); + assert.ok(Math.abs(selectAnchorBefore.widthDelta) < 0.25, 'Select dropdown width must match its trigger'); + await page.locator('gd-select').evaluate((select) => { + const scroller = select.parentElement; + const spacer = document.createElement('div'); + spacer.style.height = '120px'; + spacer.style.flex = '0 0 120px'; + scroller.prepend(spacer); + scroller.style.height = '140px'; + scroller.style.overflow = 'auto'; + scroller.scrollTop = 40; + scroller.dispatchEvent(new Event('scroll')); + }); + const selectAnchorAfter = await page.locator('gd-select').evaluate((select) => { + const trigger = select.shadowRoot.querySelector('.trigger').getBoundingClientRect(); + const dropdown = select.shadowRoot.querySelector('.dropdown').getBoundingClientRect(); + return { + placement: dropdown.top >= trigger.bottom ? 'bottom' : 'top', + gap: dropdown.top >= trigger.bottom ? dropdown.top - trigger.bottom : trigger.top - dropdown.bottom, + widthDelta: dropdown.width - trigger.width, + }; + }); + assert.ok(Math.abs(selectAnchorAfter.gap - 1) < 0.25, 'Select dropdown must stay anchored during ancestor scroll'); + assert.ok(Math.abs(selectAnchorAfter.widthDelta) < 0.25, 'Select dropdown width must track its scrolled trigger'); await page.getByRole('option', { name: 'Option 2', exact: true }).click(); await event({ value: { name: 'Option 2', value: { test: 'option2' } } }); await page.getByRole('listbox').waitFor({ state: 'hidden' }); diff --git a/libs/web-components/src/components/atoms/gd-select/gd-select.spec.ts b/libs/web-components/src/components/atoms/gd-select/gd-select.spec.ts index 0e0d580..809eb10 100644 --- a/libs/web-components/src/components/atoms/gd-select/gd-select.spec.ts +++ b/libs/web-components/src/components/atoms/gd-select/gd-select.spec.ts @@ -57,6 +57,58 @@ describe('gd-select React parity', () => { manual.open(); expect(manual.shadowRoot!.querySelector('.dropdown')!.matches(':popover-open')).toBe(true); }); + it('keeps the open dropdown anchored while a scroll ancestor moves the trigger', async () => { + host.style.cssText = 'height:120px;overflow:auto;width:360px'; + const before = document.createElement('div'); + before.style.height = '160px'; + host.append(before); + const el = await mount({ width: '300px' }); + const after = document.createElement('div'); + after.style.height = '300px'; + host.append(after); + host.scrollTop = 100; + el.open(); + await el.updateComplete; + + const trigger = el.shadowRoot!.querySelector('.trigger')!; + const dropdown = el.shadowRoot!.querySelector('.dropdown')!; + const initialGap = dropdown.getBoundingClientRect().top - trigger.getBoundingClientRect().bottom; + host.scrollTop += 24; + host.dispatchEvent(new Event('scroll')); + const movedGap = dropdown.getBoundingClientRect().top - trigger.getBoundingClientRect().bottom; + + expect(initialGap).toBeCloseTo(1, 0); + expect(movedGap).toBeCloseTo(initialGap, 0); + expect(dropdown.getBoundingClientRect().width).toBeCloseTo(trigger.getBoundingClientRect().width, 0); + el.close(); + expect(document.body.style.overflow).toBe(''); + }); + it('matches React viewport placement and flips above when there is more room', async () => { + const el = await mount({ width: '300px', dropdownMaxHeight: '240px' }); + el.style.cssText += ';position:fixed;bottom:8px;left:8px'; + el.open(); + await el.updateComplete; + + const trigger = el.shadowRoot!.querySelector('.trigger')!; + const dropdown = el.shadowRoot!.querySelector('.dropdown')!; + expect(trigger.getBoundingClientRect().top - dropdown.getBoundingClientRect().bottom).toBeCloseTo(1, 0); + expect(Number.parseFloat(dropdown.style.maxHeight)).toBeLessThanOrEqual(240); + }); + it('supports the React keyboard path through opening, option navigation, and selection', async () => { + const el = await mount(); + const trigger = el.shadowRoot!.querySelector('.trigger')!; + trigger.focus(); + trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); + await el.updateComplete; + trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); + const options = [...el.shadowRoot!.querySelectorAll('.option')]; + options[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); + expect(el.shadowRoot!.activeElement).toBe(options[1]); + options[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); + await el.updateComplete; + expect(el.value).toEqual(items[1]); + expect(el.shadowRoot!.querySelector('.dropdown')!.matches(':popover-open')).toBe(false); + }); it('supports placeholder, adornment, initiator, empty, and custom option extension points', async () => { const el = await mount({ items: [], placeholder: 'Choose', renderOption: ({ item }) => `Custom ${item.name}` }); el.innerHTML = diff --git a/libs/web-components/src/components/atoms/gd-select/gd-select.ts b/libs/web-components/src/components/atoms/gd-select/gd-select.ts index 1745163..ca73721 100644 --- a/libs/web-components/src/components/atoms/gd-select/gd-select.ts +++ b/libs/web-components/src/components/atoms/gd-select/gd-select.ts @@ -78,8 +78,9 @@ function resolveSelectTokens(theme: DesignCoreTheme, color: InputColorVariantNam * Platform-native replacement evaluated here: the HTML `popover` attribute gives native * top-layer rendering plus light-dismiss (outside-click/Escape close) for free, replacing * the React original's hand-rolled Portal + `document.addEventListener` outside-click - * logic. Positioning is still computed manually via `getBoundingClientRect()` on open - * (fixed-below only, no top-flip) — CSS Anchor Positioning could replace that too, but + * logic. Positioning is still computed manually via `getBoundingClientRect()` while open, + * including source-equivalent ancestor-scroll tracking, viewport resizing, top/bottom + * placement, and available-height constraints. CSS Anchor Positioning could replace that, but * its browser-support bar is higher than `popover`'s; record which one this repo's actual * browser-support matrix can rely on rather than assuming evergreen-only support. * @@ -203,6 +204,19 @@ export class GdSelect extends LitElement { private _store = createSelectStore({ disabled: this.disabled, value: this.value }); private _unsubscribe?: () => void; + private _ownsBodyScrollLock = false; + + private readonly _onViewportChange = () => this._positionDropdown(); + private readonly _onDocumentKeyDown = (event: KeyboardEvent) => { + if (!this._store.getState().isOpen || this.disabled) return; + if (event.key === 'Escape') { + event.preventDefault(); + this.close(); + this._trigger?.focus(); + } else if (event.key === 'Tab') { + this.close(); + } + }; connectedCallback() { super.connectedCallback(); @@ -214,6 +228,7 @@ export class GdSelect extends LitElement { disconnectedCallback() { this._unsubscribe?.(); + this._stopOpenStateEffects(); super.disconnectedCallback(); } @@ -241,19 +256,67 @@ export class GdSelect extends LitElement { if (isOpen) { this._positionDropdown(); if (!this._dropdown.matches(':popover-open')) this._dropdown.showPopover(); + this._startOpenStateEffects(); + if (this.searchable) queueMicrotask(() => this.shadowRoot?.querySelector('.search')?.focus()); } else if (this._dropdown.matches(':popover-open')) { this._dropdown.hidePopover(); + this._stopOpenStateEffects(); + } else { + this._stopOpenStateEffects(); + } + } + + private _startOpenStateEffects() { + window.addEventListener('resize', this._onViewportChange); + // Capture is required because scroll does not bubble and the trigger may live in any + // nested scroll container. This is the same listener contract as the React Select. + window.addEventListener('scroll', this._onViewportChange, true); + document.addEventListener('keydown', this._onDocumentKeyDown); + if (document.body.style.overflow !== 'hidden') { + document.body.style.overflow = 'hidden'; + this._ownsBodyScrollLock = true; + } + } + + private _stopOpenStateEffects() { + window.removeEventListener('resize', this._onViewportChange); + window.removeEventListener('scroll', this._onViewportChange, true); + document.removeEventListener('keydown', this._onDocumentKeyDown); + if (this._ownsBodyScrollLock) { + document.body.style.overflow = ''; + this._ownsBodyScrollLock = false; } } private _positionDropdown() { + if (!this._trigger || !this._dropdown) return; const rect = this._trigger.getBoundingClientRect(); + const viewportHeight = window.innerHeight || document.documentElement.clientHeight; + const requestedMax = Number.parseFloat(String(this.dropdownMaxHeight)); + const gap = 1; + const spaceBelow = viewportHeight - rect.bottom; + const spaceAbove = rect.top; + const canFitBelow = spaceBelow >= requestedMax + gap; + const canFitAbove = spaceAbove >= requestedMax + gap; + const placement = !canFitBelow && (canFitAbove || spaceAbove > spaceBelow) ? 'top' : 'bottom'; + const available = (placement === 'bottom' ? spaceBelow : spaceAbove) - gap; + const computedMaxHeight = Math.max(0, Math.min(requestedMax, available)); + this._dropdown.style.position = 'fixed'; - this._dropdown.style.top = `${rect.bottom + 2}px`; - this._dropdown.style.left = `${rect.left}px`; + this._dropdown.style.left = `${Math.floor(rect.left)}px`; this._dropdown.style.width = `${rect.width}px`; this._dropdown.style.maxWidth = this.maxWidth; this._dropdown.style.minWidth = this.minWidth ?? ''; + this._dropdown.style.maxHeight = `${Math.floor(computedMaxHeight)}px`; + if (placement === 'bottom') { + this._dropdown.style.top = `${Math.floor(rect.bottom + gap)}px`; + this._dropdown.style.bottom = 'auto'; + } else { + // Native popovers have a UA `inset: 0`; explicit `auto` is required or the unused + // top edge wins and pins an above-trigger dropdown to the viewport origin. + this._dropdown.style.top = 'auto'; + this._dropdown.style.bottom = `${Math.floor(viewportHeight - rect.top + gap)}px`; + } } /** Native light-dismiss (outside-click/Escape) fires this without going through `_toggle()`. */ @@ -285,11 +348,33 @@ export class GdSelect extends LitElement { } private _onTriggerKeyDown(event: KeyboardEvent) { - if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') { + if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter' || event.key === ' ') { event.preventDefault(); + const wasOpen = this._store.getState().isOpen; this.open(); + if (wasOpen && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) { + const options = Array.from(this.shadowRoot!.querySelectorAll('.option')); + options[event.key === 'ArrowDown' ? 0 : options.length - 1]?.focus(); + } } - if (event.key === 'Escape') this.close(); + } + + private _onDropdownKeyDown(event: KeyboardEvent) { + if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return; + event.preventDefault(); + const options = Array.from(this.shadowRoot!.querySelectorAll('.option')); + if (!options.length) return; + const current = options.indexOf(this.shadowRoot!.activeElement as HTMLElement); + const direction = event.key === 'ArrowDown' ? 1 : -1; + const next = + current < 0 ? (direction > 0 ? 0 : options.length - 1) : (current + direction + options.length) % options.length; + options[next].focus(); + } + + private _onOptionKeyDown(event: KeyboardEvent, option: SelectOption) { + if (event.key !== 'Enter') return; + event.preventDefault(); + this._select(option); } render() { @@ -362,6 +447,7 @@ export class GdSelect extends LitElement { aria-multiselectable=${this.multiple} style=${styleMap(dropdownStyle)} @toggle=${this._onDropdownToggle} + @keydown=${this._onDropdownKeyDown} > ${this.searchable ? html` this._select(item)} + @keydown=${(event: KeyboardEvent) => this._onOptionKeyDown(event, item)} > ${custom ?? this.itemStringifier(item)}
From 275fe6be73c7e6a7316ce56ce825df50e7818ea5 Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:49:50 +0200 Subject: [PATCH 14/17] feat(storybook): centralize component descriptions and refactor Loader story generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move component descriptions from individual story files to native-story transformer - Add React-to-web-component terminology replacements (children→slot, props→properties, etc.) - Remove manual loader-examples.ts in favor of generated fixture stories - Remove three custom Loader story variants (LoaderSectionVariant, SectionLoaderButtonVariant, InlineLoaderButtonVariant) - Refactor gd-loader to use theme-driven animation token resolution instead of hardcoded keyframe names - Add tests for custom animation token support and keyframe generation - Update generate-story-fixtures script to eliminate special Loader handling - Consolidate parameters.docs.description.component references to sourceMeta.parameters - Reorder Button stories (Default before AllVariants) to match React parity --- .../atoms/Loader/Loader.stories.tsx | 74 +------------------ libs/web-components/react-parity.json | 5 +- .../scripts/generate-story-fixtures.mjs | 47 +----------- .../atoms/gd-loader/gd-loader.spec.ts | 52 +++++++++++++ .../components/atoms/gd-loader/gd-loader.ts | 23 ++++-- libs/web-components/stories/Avatar.stories.ts | 9 +-- libs/web-components/stories/Badge.stories.ts | 10 +-- libs/web-components/stories/Box.stories.ts | 10 +-- libs/web-components/stories/Button.stories.ts | 12 +-- .../web-components/stories/Counter.stories.ts | 10 +-- .../stories/InputFile.stories.ts | 10 +-- libs/web-components/stories/Label.stories.ts | 10 +-- libs/web-components/stories/Link.stories.ts | 10 +-- libs/web-components/stories/Loader.stories.ts | 13 +--- .../stories/Skeleton.stories.ts | 10 +-- .../stories/Typography.stories.ts | 10 +-- .../web-components/stories/Wrapper.stories.ts | 10 +-- .../stories/fixtures/Loader.generated.js | 4 - .../web-components/stories/loader-examples.ts | 51 ------------- libs/web-components/stories/native-story.ts | 13 +++- 20 files changed, 98 insertions(+), 295 deletions(-) delete mode 100644 libs/web-components/stories/loader-examples.ts diff --git a/libs/ui/src/components/atoms/Loader/Loader.stories.tsx b/libs/ui/src/components/atoms/Loader/Loader.stories.tsx index 94b5562..ab0251b 100644 --- a/libs/ui/src/components/atoms/Loader/Loader.stories.tsx +++ b/libs/ui/src/components/atoms/Loader/Loader.stories.tsx @@ -1,8 +1,8 @@ -import { PropsWithChildren, useState } from 'react'; +import { PropsWithChildren } from 'react'; import { Meta, StoryFn } from '@storybook/react-vite'; import { TokenViewer } from '@stories/components/TokenViewer'; -import { Loader, LoaderProps, Button, Column, FlexContainer, Link, Row, Typography } from '@components'; +import { Loader, LoaderProps, Column, Row, Typography } from '@components'; import { SizeVariant, TypographyVariant, WrapperVariant } from '@types'; import { defaultTheme } from '@tokens'; @@ -388,76 +388,6 @@ CustomLoaderTailwindClassBounceAnimation.parameters = { }, }; -export const LoaderSectionVariant: StoryFn = () => { - const [showLoader, setShowLoader] = useState(false); - return ( - - Section 1 - setShowLoader((prevState) => !prevState)}> - {showLoader ? 'Hide' : 'Show'} Loader in section 2 - - - - Section 2 - - - setShowLoader((prevState) => !prevState)}>{showLoader ? 'Hide' : 'Show'} Loader - - {showLoader ? : null} - - - ); -}; - -export const SectionLoaderButtonVariant: StoryFn = () => { - const [showLoader, setShowLoader] = useState(false); - return ( - - Section 1 - - setShowLoader((prevState) => !prevState)}> - {showLoader ? 'Hide' : 'Show'} Loader in button - - - - - - - ); -}; - -export const InlineLoaderButtonVariant: StoryFn = () => { - const [showLoader, setShowLoader] = useState(false); - return ( - - Section 1 - - - - - ); -}; - export const WithAccessibility: StoryFn> = () => { return (
diff --git a/libs/web-components/react-parity.json b/libs/web-components/react-parity.json index 2ea4a6b..5380fa1 100644 --- a/libs/web-components/react-parity.json +++ b/libs/web-components/react-parity.json @@ -224,9 +224,6 @@ "LoaderSizes", "LoaderRounded", "LoaderVariantsWithWrapperViewAsHeaderTag", - "LoaderSectionVariant", - "SectionLoaderButtonVariant", - "InlineLoaderButtonVariant", "WithAccessibility", "DefaultTokens" ] @@ -340,8 +337,8 @@ "slot name=\"icon-end\"" ], "stories": [ - "AllVariants", "Default", + "AllVariants", "WithIcons", "IconOnly", "FullWidthButton", diff --git a/libs/web-components/scripts/generate-story-fixtures.mjs b/libs/web-components/scripts/generate-story-fixtures.mjs index e3f541c..ee9fe8a 100644 --- a/libs/web-components/scripts/generate-story-fixtures.mjs +++ b/libs/web-components/scripts/generate-story-fixtures.mjs @@ -21,28 +21,6 @@ export const fixtureComponents = [ 'Typography', 'Wrapper', ]; -const nativeDescriptions = { - Avatar: 'Avatar with image, fallback initials or icon, badge, color, size, and accessible labeling.', - Badge: 'Status badge with variants, appearances, sizes, disabled state, icon positions, and style overrides.', - Box: 'Layout container with orientation, border, hover highlight, shadow, accessibility, and custom styling.', - Button: - 'Button with variants, icons, loading state, width, rounding, disabled behavior, custom styling, and accessibility.', - Counter: - 'Counter with minimum, maximum, initial value, external change handling, disabled state, and accessible native controls.', - InputFile: - 'File picker with accepted file types, capture, multiple selection, disabled and icon-label states, button customization, native events, and accessible controls.', - Label: - 'Native label with slotted content, child icons, custom styling, control association, and accessibility semantics.', - Link: 'Native anchor with variants, sizes, underline modes, colors, disabled behavior, targets, custom styling, accessibility, and child composition.', - Loader: - 'Loading indicator with circle and dots animations, five sizes, rounding, inline, section and full-page layouts, custom timing, content, styling, and status accessibility.', - Skeleton: - 'Loading placeholder with rounded, rectangular and circular variants, theme colors, child content, composed layouts, and accessibility semantics.', - Typography: - 'Typography with semantic overrides, combined style variants, disclaimer, display, heading, body, color, alignment, and accessibility options.', - Wrapper: - 'Layout wrapper with inline, section and full-page variants, semantic element selection, slotted content, and custom styling.', -}; const componentFiles = await readdir(resolve(root, 'libs/ui/src/components'), { recursive: true }); const enums = new Map(); for (const file of await readdir(resolve(root, 'libs/ui/src/types'))) { @@ -112,23 +90,6 @@ for (const component of fixtureComponents) { const text = node.text.replace(/tailwind(?: css)?/gi, 'utility CSS'); if (text !== node.text) return ts.factory.createStringLiteral(text); } - if ( - component === 'Loader' && - ts.isVariableDeclaration(node) && - ['LoaderSectionVariant', 'SectionLoaderButtonVariant', 'InlineLoaderButtonVariant'].includes( - node.name.getText(source) - ) - ) - return ts.factory.updateVariableDeclaration( - node, - node.name, - undefined, - undefined, - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('loaderStories'), - node.name.getText(source) - ) - ); if (ts.isPropertyAssignment(node) && ['argTypes', 'play', 'code'].includes(node.name.getText(source))) return undefined; if ( @@ -181,16 +142,12 @@ for (const component of fixtureComponents) { }';` ) .join('\n'); - const result = `// Generated from ${sourcePath}. Run npm run generate:web-component-stories.\n// Native DOM recipes only: no React runtime or React component implementation is imported.\nimport { demo, Fragment } from '../native-story';\nimport { defaultTheme } from 'gd-design-library/tokens';\nimport { fn } from 'storybook/test';\nimport { action } from 'storybook/actions';\n${ - component === 'Loader' ? "import { loaderStories } from '../loader-examples';\n" : '' - }const COMPONENT_NAME = '${component}';\n${tagDeclarations}\n${code}`; + const result = `// Generated from ${sourcePath}. Run npm run generate:web-component-stories.\n// Native DOM recipes only: no React runtime or React component implementation is imported.\nimport { demo, Fragment } from '../native-story';\nimport { defaultTheme } from 'gd-design-library/tokens';\nimport { fn } from 'storybook/test';\nimport { action } from 'storybook/actions';\nconst COMPONENT_NAME = '${component}';\n${tagDeclarations}\n${code}`; // CSF indexes static object-literal names. Hiding name inside nativeStory() // preserves runtime metadata but silently changes the sidebar's published name. const wrapper = `// Generated from ${sourcePath}; edit the source example or native-story adapter.\nimport * as fixtures from './fixtures/${component}.generated.js';\nimport { nativeMeta, nativeStory } from './native-story';\nimport { sectionedArgTypes } from './helpers';\n\nconst sourceMeta = nativeMeta(fixtures.default, sectionedArgTypes('${component}', {}));\nconst meta = {\n title: '${ component === 'Counter' ? 'Molecules' : 'Atoms' - }/${component}',\n tags: ['autodocs'],\n args: sourceMeta.args,\n parameters: {\n layout: 'centered',\n docs: {\n description: { component: ${JSON.stringify( - nativeDescriptions[component] - )} },\n },\n },\n argTypes: sourceMeta.argTypes,\n render: sourceMeta.render,\n};\nexport default meta;\n${names + }/${component}',\n tags: ['autodocs'],\n args: sourceMeta.args,\n parameters: sourceMeta.parameters,\n argTypes: sourceMeta.argTypes,\n render: sourceMeta.render,\n};\nexport default meta;\n${names .map( (name) => `export const ${name} = { ...nativeStory(fixtures.${name}, meta)${ diff --git a/libs/web-components/src/components/atoms/gd-loader/gd-loader.spec.ts b/libs/web-components/src/components/atoms/gd-loader/gd-loader.spec.ts index 76dcc5c..6fe47d8 100644 --- a/libs/web-components/src/components/atoms/gd-loader/gd-loader.spec.ts +++ b/libs/web-components/src/components/atoms/gd-loader/gd-loader.spec.ts @@ -19,4 +19,56 @@ describe('gd-loader', () => { expect(el.shadowRoot!.querySelector('[part="wrapper"]')!.getAttribute('popover')).toBe('manual'); el.remove(); }); + + it('builds valid animations from the loader animation tokens', async () => { + const el = document.createElement('gd-loader'); + document.body.append(el); + await el.updateComplete; + + let cssText = el + .shadowRoot!.adoptedStyleSheets.flatMap((sheet) => [...sheet.cssRules].map((rule) => rule.cssText)) + .join('\n'); + expect(cssText).toContain('gd-loader-circle'); + expect(cssText).toContain('@keyframes gd-loader-circle'); + expect(cssText).toContain('rotate(360deg)'); + + el.name = 'dots'; + await el.updateComplete; + cssText = el + .shadowRoot!.adoptedStyleSheets.flatMap((sheet) => [...sheet.cssRules].map((rule) => rule.cssText)) + .join('\n'); + expect(cssText).toContain('gd-loader-dots'); + expect(cssText).toContain('@keyframes gd-loader-dots'); + expect(cssText).toContain('translateY(-15px)'); + el.remove(); + }); + + it('uses loader and keyframe overrides from the active theme', async () => { + const el = document.createElement('gd-loader'); + el.theme = { + loader: { + default: { display: 'block' }, + inline: {}, + circle: { default: {}, md: {} }, + animation: { circle: { name: 'customLoaderFrames' } }, + }, + animations: { + customLoaderFrames: { + from: { opacity: 0.2 }, + to: { opacity: 0.8 }, + }, + }, + }; + document.body.append(el); + await el.updateComplete; + + const cssText = el + .shadowRoot!.adoptedStyleSheets.flatMap((sheet) => [...sheet.cssRules].map((rule) => rule.cssText)) + .join('\n'); + expect(cssText).toContain('@keyframes gd-loader-circle'); + expect(cssText).toContain('opacity: 0.2'); + expect(cssText).toContain('opacity: 0.8'); + expect(cssText).not.toContain('rotate(360deg)'); + el.remove(); + }); }); diff --git a/libs/web-components/src/components/atoms/gd-loader/gd-loader.ts b/libs/web-components/src/components/atoms/gd-loader/gd-loader.ts index 3814f5d..8310409 100644 --- a/libs/web-components/src/components/atoms/gd-loader/gd-loader.ts +++ b/libs/web-components/src/components/atoms/gd-loader/gd-loader.ts @@ -28,13 +28,19 @@ export class GdLoader extends LitElement { } render() { - const tokens = resolveThemeTree(loader, this.theme) as unknown as Record & { + const loaderTokens = get(this.theme, 'loader', loader) as typeof loader; + const animationTokens = get(this.theme, 'animations', animations) as typeof animations; + const tokens = resolveThemeTree(loaderTokens, this.theme) as unknown as Record & { circle: Record; dots: Record; }; - const resolvedAnimations = resolveThemeTree(animations, this.theme) as unknown as Record; - const animationName = this.name === 'circle' ? 'spinKeyframes' : 'bounceKeyframes'; - const frames = resolvedAnimations[animationName]; + const resolvedAnimations = resolveThemeTree(animationTokens, this.theme) as unknown as Record; + const animationTokenName = get(tokens, ['animation', this.name, 'name'], ''); + const frames = resolvedAnimations[animationTokenName] ?? {}; + const localAnimationName = `gd-loader-${this.name}`; + const keyframes = Object.entries(frames) + .map(([step, block]) => buttonCssBlockToText(step, block as CssBlock)) + .join(''); const radius = get(this.theme, `radius.${this.rounded}`, '0px'); const cssText = [ buttonCssBlockToText('.loader', tokens.default), @@ -42,13 +48,16 @@ export class GdLoader extends LitElement { buttonCssBlockToText('.loader', tokens[this.name].default), buttonCssBlockToText('.loader', tokens[this.name][this.size]), this.name === 'circle' - ? buttonCssBlockToText('.loader', { animation: `gd-loader-spin ${this.animationProps}` }) + ? buttonCssBlockToText('.loader', { animation: `${localAnimationName} ${this.animationProps}` }) : '', this.name === 'dots' - ? buttonCssBlockToText('.dot', { borderRadius: radius, animation: `gd-loader-bounce ${this.animationProps}` }) + ? buttonCssBlockToText('.dot', { + borderRadius: radius, + animation: `${localAnimationName} ${this.animationProps}`, + }) : '', buttonCssBlockToText('.loader', this.styles), - buttonCssBlockToText(`@keyframes gd-loader-${this.name === 'circle' ? 'spin' : 'bounce'}`, frames), + keyframes ? `@keyframes ${localAnimationName} {\n${keyframes}}` : '', ].join('\n'); const root = this.shadowRoot; if (root && typeof CSSStyleSheet !== 'undefined') { diff --git a/libs/web-components/stories/Avatar.stories.ts b/libs/web-components/stories/Avatar.stories.ts index 710c5e5..83d1366 100644 --- a/libs/web-components/stories/Avatar.stories.ts +++ b/libs/web-components/stories/Avatar.stories.ts @@ -8,14 +8,7 @@ const meta = { title: 'Atoms/Avatar', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: 'Avatar with image, fallback initials or icon, badge, color, size, and accessible labeling.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; diff --git a/libs/web-components/stories/Badge.stories.ts b/libs/web-components/stories/Badge.stories.ts index 36baf28..a1e34e0 100644 --- a/libs/web-components/stories/Badge.stories.ts +++ b/libs/web-components/stories/Badge.stories.ts @@ -8,15 +8,7 @@ const meta = { title: 'Atoms/Badge', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Status badge with variants, appearances, sizes, disabled state, icon positions, and style overrides.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; diff --git a/libs/web-components/stories/Box.stories.ts b/libs/web-components/stories/Box.stories.ts index 6a97ba8..ce7c64a 100644 --- a/libs/web-components/stories/Box.stories.ts +++ b/libs/web-components/stories/Box.stories.ts @@ -8,15 +8,7 @@ const meta = { title: 'Atoms/Box', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Layout container with orientation, border, hover highlight, shadow, accessibility, and custom styling.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; diff --git a/libs/web-components/stories/Button.stories.ts b/libs/web-components/stories/Button.stories.ts index 91673c0..2044b03 100644 --- a/libs/web-components/stories/Button.stories.ts +++ b/libs/web-components/stories/Button.stories.ts @@ -8,21 +8,13 @@ const meta = { title: 'Atoms/Button', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Button with variants, icons, loading state, width, rounding, disabled behavior, custom styling, and accessibility.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; export default meta; -export const AllVariants = { ...nativeStory(fixtures.AllVariants, meta) }; export const Default = { ...nativeStory(fixtures.Default, meta) }; +export const AllVariants = { ...nativeStory(fixtures.AllVariants, meta) }; export const WithIcons = { ...nativeStory(fixtures.WithIcons, meta) }; export const IconOnly = { ...nativeStory(fixtures.IconOnly, meta) }; export const FullWidthButton = { ...nativeStory(fixtures.FullWidthButton, meta) }; diff --git a/libs/web-components/stories/Counter.stories.ts b/libs/web-components/stories/Counter.stories.ts index 25f84d8..db82f22 100644 --- a/libs/web-components/stories/Counter.stories.ts +++ b/libs/web-components/stories/Counter.stories.ts @@ -8,15 +8,7 @@ const meta = { title: 'Molecules/Counter', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Counter with minimum, maximum, initial value, external change handling, disabled state, and accessible native controls.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; diff --git a/libs/web-components/stories/InputFile.stories.ts b/libs/web-components/stories/InputFile.stories.ts index 04bcdc7..4b5e31b 100644 --- a/libs/web-components/stories/InputFile.stories.ts +++ b/libs/web-components/stories/InputFile.stories.ts @@ -8,15 +8,7 @@ const meta = { title: 'Atoms/InputFile', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'File picker with accepted file types, capture, multiple selection, disabled and icon-label states, button customization, native events, and accessible controls.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; diff --git a/libs/web-components/stories/Label.stories.ts b/libs/web-components/stories/Label.stories.ts index 50d7f7c..07c5cbf 100644 --- a/libs/web-components/stories/Label.stories.ts +++ b/libs/web-components/stories/Label.stories.ts @@ -8,15 +8,7 @@ const meta = { title: 'Atoms/Label', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Native label with slotted content, child icons, custom styling, control association, and accessibility semantics.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; diff --git a/libs/web-components/stories/Link.stories.ts b/libs/web-components/stories/Link.stories.ts index 032d2b3..3b6f95c 100644 --- a/libs/web-components/stories/Link.stories.ts +++ b/libs/web-components/stories/Link.stories.ts @@ -8,15 +8,7 @@ const meta = { title: 'Atoms/Link', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Native anchor with variants, sizes, underline modes, colors, disabled behavior, targets, custom styling, accessibility, and child composition.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; diff --git a/libs/web-components/stories/Loader.stories.ts b/libs/web-components/stories/Loader.stories.ts index 0e1a8a9..0867ef6 100644 --- a/libs/web-components/stories/Loader.stories.ts +++ b/libs/web-components/stories/Loader.stories.ts @@ -8,15 +8,7 @@ const meta = { title: 'Atoms/Loader', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Loading indicator with circle and dots animations, five sizes, rounding, inline, section and full-page layouts, custom timing, content, styling, and status accessibility.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; @@ -28,8 +20,5 @@ export const LoaderRounded = { ...nativeStory(fixtures.LoaderRounded, meta) }; export const LoaderVariantsWithWrapperViewAsHeaderTag = { ...nativeStory(fixtures.LoaderVariantsWithWrapperViewAsHeaderTag, meta), }; -export const LoaderSectionVariant = { ...nativeStory(fixtures.LoaderSectionVariant, meta) }; -export const SectionLoaderButtonVariant = { ...nativeStory(fixtures.SectionLoaderButtonVariant, meta) }; -export const InlineLoaderButtonVariant = { ...nativeStory(fixtures.InlineLoaderButtonVariant, meta) }; export const WithAccessibility = { ...nativeStory(fixtures.WithAccessibility, meta) }; export const DefaultTokens = { ...nativeStory(fixtures.DefaultTokens, meta) }; diff --git a/libs/web-components/stories/Skeleton.stories.ts b/libs/web-components/stories/Skeleton.stories.ts index c61a01d..88e0900 100644 --- a/libs/web-components/stories/Skeleton.stories.ts +++ b/libs/web-components/stories/Skeleton.stories.ts @@ -8,15 +8,7 @@ const meta = { title: 'Atoms/Skeleton', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Loading placeholder with rounded, rectangular and circular variants, theme colors, child content, composed layouts, and accessibility semantics.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; diff --git a/libs/web-components/stories/Typography.stories.ts b/libs/web-components/stories/Typography.stories.ts index da95aa5..11260bd 100644 --- a/libs/web-components/stories/Typography.stories.ts +++ b/libs/web-components/stories/Typography.stories.ts @@ -8,15 +8,7 @@ const meta = { title: 'Atoms/Typography', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Typography with semantic overrides, combined style variants, disclaimer, display, heading, body, color, alignment, and accessibility options.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; diff --git a/libs/web-components/stories/Wrapper.stories.ts b/libs/web-components/stories/Wrapper.stories.ts index e78d5eb..fb98173 100644 --- a/libs/web-components/stories/Wrapper.stories.ts +++ b/libs/web-components/stories/Wrapper.stories.ts @@ -8,15 +8,7 @@ const meta = { title: 'Atoms/Wrapper', tags: ['autodocs'], args: sourceMeta.args, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'Layout wrapper with inline, section and full-page variants, semantic element selection, slotted content, and custom styling.', - }, - }, - }, + parameters: sourceMeta.parameters, argTypes: sourceMeta.argTypes, render: sourceMeta.render, }; diff --git a/libs/web-components/stories/fixtures/Loader.generated.js b/libs/web-components/stories/fixtures/Loader.generated.js index 5fa0677..9d0083f 100644 --- a/libs/web-components/stories/fixtures/Loader.generated.js +++ b/libs/web-components/stories/fixtures/Loader.generated.js @@ -4,7 +4,6 @@ import { demo, Fragment } from '../native-story'; import { defaultTheme } from 'gd-design-library/tokens'; import { fn } from 'storybook/test'; import { action } from 'storybook/actions'; -import { loaderStories } from '../loader-examples'; const COMPONENT_NAME = 'Loader'; const Loader = 'gd-loader'; const Row = 'Row'; @@ -250,9 +249,6 @@ LoaderVariantsWithWrapperViewAsHeaderTag.parameters = { }, }, }; -export const LoaderSectionVariant = loaderStories.LoaderSectionVariant; -export const SectionLoaderButtonVariant = loaderStories.SectionLoaderButtonVariant; -export const InlineLoaderButtonVariant = loaderStories.InlineLoaderButtonVariant; export const WithAccessibility = () => { return demo( 'div', diff --git a/libs/web-components/stories/loader-examples.ts b/libs/web-components/stories/loader-examples.ts deleted file mode 100644 index 28d3561..0000000 --- a/libs/web-components/stories/loader-examples.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { element, stack } from './helpers'; - -function example(mode: 'section' | 'button-section' | 'button-inline') { - const root = stack('column', '0', element('gd-typography', { variant: 'h3' }, 'Section 1')); - root.style.cssText += ';width:100%;margin:0 auto;background:#a5ffb4;padding:10px;position:relative'; - let showing = false; - const button = element('gd-button', { variant: 'outlined', styles: { position: 'relative' } }, 'Click me!'); - const link = element('gd-link', {}); - const innerLink = element('gd-link', {}); - const section = stack( - 'column', - '0', - stack('row', '0', element('gd-typography', { variant: 'h3' }, 'Section 2')), - stack('row', '0', innerLink) - ); - section.style.cssText += ';margin-top:20px;background:#ffa5a5;padding:10px;position:relative'; - const loader = element('gd-loader', { - variant: mode === 'button-inline' ? 'inline' : 'section', - name: mode === 'button-inline' ? 'circle' : 'dots', - size: mode === 'section' ? 'md' : 'sm', - withWrapper: mode !== 'button-inline', - }); - if (mode === 'button-inline') loader.slot = 'icon-end'; - const update = () => { - link.textContent = `${showing ? 'Hide' : 'Show'} Loader in ${mode === 'section' ? 'section 2' : 'button'}`; - innerLink.textContent = `${showing ? 'Hide' : 'Show'} Loader`; - button.textContent = showing ? 'Loading...' : 'Click me!'; - loader.remove(); - if (showing) (mode === 'section' ? section : button).append(loader); - }; - const toggle = () => { - showing = !showing; - update(); - }; - button.addEventListener('click', toggle); - link.addEventListener('click', toggle); - innerLink.addEventListener('click', toggle); - if (mode === 'section') root.append(link, section); - else { - if (mode === 'button-section') root.append(stack('row', '0', link)); - root.append(stack('row', '0', button)); - } - update(); - return root; -} - -export const loaderStories = { - LoaderSectionVariant: () => example('section'), - SectionLoaderButtonVariant: () => example('button-section'), - InlineLoaderButtonVariant: () => example('button-inline'), -}; diff --git a/libs/web-components/stories/native-story.ts b/libs/web-components/stories/native-story.ts index 35df9ed..3c2752d 100644 --- a/libs/web-components/stories/native-story.ts +++ b/libs/web-components/stories/native-story.ts @@ -141,7 +141,18 @@ function nativeParameters(parameters: Props | undefined) { description: Object.fromEntries( Object.entries(descriptions).map(([key, text]) => [ key, - text.replace(/\s*(?:\s*)*

🧩[\s\S]*$/, '').replace(/React component/g, 'component'), + text + .replace(/\s*(?:\s*)*

[🔧🧩][\s\S]*$/u, '') + .replace(/React component/g, 'component') + .replace(/`iconStart`/g, '`slot="icon-start"`') + .replace(/`iconEnd`/g, '`slot="icon-end"`') + .replace(/`isIcon`/g, '`is-icon`') + .replace(/`fullWidth`/g, '`full-width`') + .replace(/`ariaLabel`/g, '`aria-label`') + .replace(/`className`/g, '`class`') + .replace(/\bchildren\b/g, 'the default slot') + .replace(/\bprops\b/g, 'properties') + .replace(/\bprop\b/g, 'property'), ]) ), }, From 7682d3da3020fc737131e1c76b8d1530b6b7dc1c Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:03:55 +0200 Subject: [PATCH 15/17] feat(storybook): add legacy route aliases for retired Loader story variants - Add `legacyRouteAliases` mapping retired Loader story IDs to canonical default scene - Extend `legacyRedirect()` to resolve aliases before checking shared/scoped routes - Extract `keyframesCssBlockToText()` helper for serializing animation token keyframes - Refactor gd-skeleton and gd-loader to use extracted keyframes helper - Add tests for alias redirection and custom animation token rendering - Update link validation to respect legacy route aliases - Set default args for Button stories (children, variant) for React/WC parity --- bin/storybook/check-links.mjs | 4 +- bin/storybook/routes.mjs | 13 ++++- bin/storybook/routes.test.mjs | 19 +++++-- .../src/tokenResolvers/button.spec.ts | 23 +++++++- libs/design-core/src/tokenResolvers/button.ts | 12 +++++ libs/design-core/src/tokenResolvers/index.ts | 7 ++- .../atoms/Button/Button.stories.tsx | 53 +++++++++---------- .../components/atoms/gd-loader/gd-loader.ts | 14 +++-- .../atoms/gd-skeleton/gd-skeleton.spec.ts | 37 +++++++++++++ .../atoms/gd-skeleton/gd-skeleton.ts | 17 ++++-- .../stories/fixtures/Button.generated.js | 45 +++++++--------- 11 files changed, 172 insertions(+), 72 deletions(-) diff --git a/bin/storybook/check-links.mjs b/bin/storybook/check-links.mjs index 7f2fb06..d0d446f 100644 --- a/bin/storybook/check-links.mjs +++ b/bin/storybook/check-links.mjs @@ -3,7 +3,7 @@ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node import { tmpdir } from 'node:os'; import { extname, join, relative, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { legacyReactIds, sharedDocIds } from './routes.mjs'; +import { legacyReactIds, legacyRouteAliases, sharedDocIds } from './routes.mjs'; import { validateLinks } from './link-validation.mjs'; const root = fileURLToPath(new URL('../../', import.meta.url)); @@ -49,7 +49,7 @@ if (built) { const errors = []; for (const id of legacyReactIds) { - const canonical = sharedDocIds.includes(id) ? id : `react-${id}`; + const canonical = legacyRouteAliases[id] ?? (sharedDocIds.includes(id) ? id : `react-${id}`); if (!indexes.react.entries[canonical]) errors.push(`Missing migrated entry: ${canonical}`); } for (const id of sharedDocIds) { diff --git a/bin/storybook/routes.mjs b/bin/storybook/routes.mjs index eb501cd..eb939bd 100644 --- a/bin/storybook/routes.mjs +++ b/bin/storybook/routes.mjs @@ -6,14 +6,23 @@ export const legacyReactIds = JSON.parse(readFileSync(new URL('./legacy-react-id // Explicitly reviewed framework-neutral pages; other documentation stays React-scoped. export const sharedDocs = JSON.parse(readFileSync(new URL('./shared-docs.json', import.meta.url), 'utf8')); export const sharedDocIds = Object.values(sharedDocs); +// Retired stories keep explicit aliases so frozen legacy URLs remain valid +// without keeping obsolete scenes in the current Storybook inventory. +export const legacyRouteAliases = { + 'atoms-loader--inline-loader-button-variant': 'react-atoms-loader--default', + 'atoms-loader--loader-section-variant': 'react-atoms-loader--default', + 'atoms-loader--section-loader-button-variant': 'react-atoms-loader--default', +}; 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, sharedIds = []) { +export function legacyRedirect(href, ids, sharedIds = [], aliases = {}) { const url = new URL(href); const known = new Set(ids); const shared = new Set(sharedIds); const upgrade = (id) => { + const legacyId = id.startsWith('react-') ? id.slice(6) : id; + if (aliases[legacyId]) return aliases[legacyId]; if (shared.has(id)) return id; if (id.startsWith('react-') && shared.has(id.slice(6))) return id.slice(6); return known.has(id) ? `react-${id}` : id; @@ -44,5 +53,5 @@ export function legacyRedirect(href, ids, sharedIds = []) { } export function legacyRedirectHead() { - return ``; + return ``; } diff --git a/bin/storybook/routes.test.mjs b/bin/storybook/routes.test.mjs index 8213365..1a90f13 100644 --- a/bin/storybook/routes.test.mjs +++ b/bin/storybook/routes.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { legacyReactIds, sharedDocIds, legacyRedirect, legacyRedirectHead } from './routes.mjs'; +import { legacyReactIds, legacyRouteAliases, sharedDocIds, legacyRedirect, legacyRedirectHead } from './routes.mjs'; const origin = 'https://storybook.example'; @@ -8,14 +8,27 @@ 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 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}`}`); + const result = new URL(legacyRedirect(href, legacyReactIds, sharedDocIds, legacyRouteAliases) || href); + const canonical = legacyRouteAliases[id] ?? (sharedDocIds.includes(id) ? id : `react-${id}`); + assert.equal(result.searchParams.get('path'), `/${kind}/${canonical}`); assert.equal(result.searchParams.get('args'), 'size:sm'); assert.equal(result.searchParams.get('globals'), 'theme:dark'); assert.equal(result.hash, '#example'); } }); +test('retired Loader scenes redirect to the current default scene', () => { + for (const id of Object.keys(legacyRouteAliases)) { + for (const route of [`/?path=/story/${id}`, `/?path=/story/react-${id}`, `/iframe.html?id=${id}`]) { + const result = new URL(legacyRedirect(origin + route, legacyReactIds, sharedDocIds, legacyRouteAliases)); + assert.equal( + (result.searchParams.get('path') || result.searchParams.get('id')).replace(/^\/story\//, ''), + 'react-atoms-loader--default' + ); + } + } +}); + 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) diff --git a/libs/design-core/src/tokenResolvers/button.spec.ts b/libs/design-core/src/tokenResolvers/button.spec.ts index b5536cc..8dff7a7 100644 --- a/libs/design-core/src/tokenResolvers/button.spec.ts +++ b/libs/design-core/src/tokenResolvers/button.spec.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { resolveButtonVariantStyle, resolveButtonRadius, buttonCssBlockToText } from './button'; +import { + resolveButtonVariantStyle, + resolveButtonRadius, + buttonCssBlockToText, + keyframesCssBlockToText, +} from './button'; import type { DesignCoreTheme } from '../types'; const theme: DesignCoreTheme = { @@ -166,3 +171,19 @@ describe('buttonCssBlockToText', () => { expect(css).toBe(''); }); }); + +describe('keyframesCssBlockToText', () => { + it('wraps token steps in a valid named keyframes rule', () => { + expect( + keyframesCssBlockToText('gd-fade', { + '0%': { opacity: 0 }, + '100%': { opacity: 1 }, + }) + ).toBe('@keyframes gd-fade {\n0% {\nopacity: 0;\n}\n100% {\nopacity: 1;\n}\n}\n'); + }); + + it('omits empty or unnamed keyframes', () => { + expect(keyframesCssBlockToText('', { to: { opacity: 1 } })).toBe(''); + expect(keyframesCssBlockToText('gd-empty', {})).toBe(''); + }); +}); diff --git a/libs/design-core/src/tokenResolvers/button.ts b/libs/design-core/src/tokenResolvers/button.ts index 930275c..cb0a371 100644 --- a/libs/design-core/src/tokenResolvers/button.ts +++ b/libs/design-core/src/tokenResolvers/button.ts @@ -259,3 +259,15 @@ export function buttonCssBlockToText(selector: string, block: ButtonCssBlock | u return (declarations ? `${selector} {\n${declarations}}\n` : '') + nested; } + +/** Serializes token keyframe steps into a complete, valid CSS @keyframes rule. */ +export function keyframesCssBlockToText(name: string, frames: ButtonCssBlock | undefined): string { + if (!name || !frames) return ''; + + const steps = Object.entries(frames) + .filter((entry): entry is [string, ButtonCssBlock] => typeof entry[1] === 'object' && entry[1] !== null) + .map(([step, block]) => buttonCssBlockToText(step, block)) + .join(''); + + return steps ? `@keyframes ${name} {\n${steps}}\n` : ''; +} diff --git a/libs/design-core/src/tokenResolvers/index.ts b/libs/design-core/src/tokenResolvers/index.ts index 441bcc1..84ae0d0 100644 --- a/libs/design-core/src/tokenResolvers/index.ts +++ b/libs/design-core/src/tokenResolvers/index.ts @@ -1,4 +1,9 @@ -export { resolveButtonVariantStyle, resolveButtonRadius, buttonCssBlockToText } from './button'; +export { + resolveButtonVariantStyle, + resolveButtonRadius, + buttonCssBlockToText, + keyframesCssBlockToText, +} from './button'; export type { ButtonVariantName, ButtonRoundedName, diff --git a/libs/ui/src/components/atoms/Button/Button.stories.tsx b/libs/ui/src/components/atoms/Button/Button.stories.tsx index 7249886..6fcfdf0 100644 --- a/libs/ui/src/components/atoms/Button/Button.stories.tsx +++ b/libs/ui/src/components/atoms/Button/Button.stories.tsx @@ -19,6 +19,10 @@ const meta: Meta = { title: 'Atoms/Button', component: Button, tags: ['autodocs'], + args: { + children: 'Button', + variant: ButtonVariant.Primary, + }, argTypes: { // ============================================================================ // Content & Behavior @@ -557,15 +561,6 @@ The Button component offers six distinct visual styles to accommodate different

🔧 Technical Implementation

Built with React's \`forwardRef\` for proper ref handling, the component integrates seamlessly with the theme system via \`useTheme\` hook. It extends Box component props for flexible layout control and uses styled-components for dynamic theming. - - -
-
- -

🧩 Web Components track (CTORNDSD-646)

-Verdict — Lit custom element. Owns variant, loading and focus-ring state, and CTORNDSD-286 was a button style-collision bug — Shadow DOM isolation is the reason. Ported as <gd-button>: 1.87 kB gzip vs 19.07 kB. -
-Decision rule and full rationale: docs/webcomponents-migration/05-native-html-guidelines.md. `, }, }, @@ -580,6 +575,26 @@ type Story = StoryObj; // Interactive Stories (with Controls) // ============================================================================ +export const Default: Story = { + args: { + children: 'Button', + variant: 'primary', + onClick: fn(), + }, + parameters: { + docs: { + source: { + code: ``, + }, + description: { + story: + 'This is the default interactive story. Use the controls panel to experiment with different props and see how the Button component behaves. Try changing the variant, adding icons, adjusting rounded corners, and toggling states.', + }, + }, + }, +}; +Default.play = defaultActions; + export const AllVariants: StoryFn = () => { return ( @@ -733,26 +748,6 @@ AllVariants.parameters = { }, }; -export const Default: Story = { - args: { - children: 'Button', - variant: 'primary', - onClick: fn(), - }, - parameters: { - docs: { - source: { - code: ``, - }, - description: { - story: - 'This is the default interactive story. Use the controls panel to experiment with different props and see how the Button component behaves. Try changing the variant, adding icons, adjusting rounded corners, and toggling states.', - }, - }, - }, -}; -Default.play = defaultActions; - export const WithIcons: Story = { args: { children: 'Save Changes', diff --git a/libs/web-components/src/components/atoms/gd-loader/gd-loader.ts b/libs/web-components/src/components/atoms/gd-loader/gd-loader.ts index 8310409..464618d 100644 --- a/libs/web-components/src/components/atoms/gd-loader/gd-loader.ts +++ b/libs/web-components/src/components/atoms/gd-loader/gd-loader.ts @@ -1,7 +1,14 @@ import { LitElement, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { loader, animations } from 'gd-design-library/tokens'; -import { buttonCssBlockToText, get, resolveThemeTree, type ButtonCssBlock, type DesignCoreTheme } from 'gd-design-core'; +import { + buttonCssBlockToText, + get, + keyframesCssBlockToText, + resolveThemeTree, + type ButtonCssBlock, + type DesignCoreTheme, +} from 'gd-design-core'; import '../gd-wrapper/gd-wrapper'; type LoaderName = 'circle' | 'dots'; @@ -38,9 +45,6 @@ export class GdLoader extends LitElement { const animationTokenName = get(tokens, ['animation', this.name, 'name'], ''); const frames = resolvedAnimations[animationTokenName] ?? {}; const localAnimationName = `gd-loader-${this.name}`; - const keyframes = Object.entries(frames) - .map(([step, block]) => buttonCssBlockToText(step, block as CssBlock)) - .join(''); const radius = get(this.theme, `radius.${this.rounded}`, '0px'); const cssText = [ buttonCssBlockToText('.loader', tokens.default), @@ -57,7 +61,7 @@ export class GdLoader extends LitElement { }) : '', buttonCssBlockToText('.loader', this.styles), - keyframes ? `@keyframes ${localAnimationName} {\n${keyframes}}` : '', + keyframesCssBlockToText(localAnimationName, frames), ].join('\n'); const root = this.shadowRoot; if (root && typeof CSSStyleSheet !== 'undefined') { diff --git a/libs/web-components/src/components/atoms/gd-skeleton/gd-skeleton.spec.ts b/libs/web-components/src/components/atoms/gd-skeleton/gd-skeleton.spec.ts index 550a075..10094d4 100644 --- a/libs/web-components/src/components/atoms/gd-skeleton/gd-skeleton.spec.ts +++ b/libs/web-components/src/components/atoms/gd-skeleton/gd-skeleton.spec.ts @@ -12,6 +12,11 @@ describe('gd-skeleton', () => { const skeleton = el.shadowRoot!.querySelector('[part="skeleton"]')!; expect(skeleton).not.toBeNull(); expect(getComputedStyle(skeleton).animationName).toBe('gd-skeleton-animation'); + const cssText = el + .shadowRoot!.adoptedStyleSheets.flatMap((sheet) => [...sheet.cssRules].map((rule) => rule.cssText)) + .join('\n'); + expect(cssText).toContain('@keyframes gd-skeleton-animation'); + expect(cssText).toContain('opacity: 0.4'); expect(el.textContent).toBe('Loading Content...'); el.remove(); }); @@ -37,4 +42,36 @@ describe('gd-skeleton', () => { expect(style.backgroundColor).not.toBe(''); el.remove(); }); + + it('uses skeleton and animation overrides from the active theme', async () => { + const el = document.createElement('gd-skeleton'); + el.animationName = 'customSkeletonFrames'; + el.theme = { + skeleton: { + default: { display: 'block', backgroundColor: 'rgb(1, 2, 3)' }, + rounded: { borderRadius: '11px' }, + }, + animations: { + customSkeletonFrames: { + from: { opacity: 0.1 }, + to: { opacity: 0.9 }, + }, + }, + }; + document.body.append(el); + await el.updateComplete; + + const skeleton = el.shadowRoot!.querySelector('[part="skeleton"]')!; + const style = getComputedStyle(skeleton); + expect(style.borderRadius).toBe('11px'); + expect(style.backgroundColor).toBe('rgb(1, 2, 3)'); + const cssText = el + .shadowRoot!.adoptedStyleSheets.flatMap((sheet) => [...sheet.cssRules].map((rule) => rule.cssText)) + .join('\n'); + expect(cssText).toContain('@keyframes gd-skeleton-animation'); + expect(cssText).toContain('opacity: 0.1'); + expect(cssText).toContain('opacity: 0.9'); + expect(cssText).not.toContain('opacity: 0.4'); + el.remove(); + }); }); diff --git a/libs/web-components/src/components/atoms/gd-skeleton/gd-skeleton.ts b/libs/web-components/src/components/atoms/gd-skeleton/gd-skeleton.ts index f9ef01c..9245798 100644 --- a/libs/web-components/src/components/atoms/gd-skeleton/gd-skeleton.ts +++ b/libs/web-components/src/components/atoms/gd-skeleton/gd-skeleton.ts @@ -1,7 +1,14 @@ import { LitElement, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { animations, resolveThemeColor, skeleton } from 'gd-design-library/tokens'; -import { buttonCssBlockToText, resolveThemeTree, type ButtonCssBlock, type DesignCoreTheme } from 'gd-design-core'; +import { + buttonCssBlockToText, + get, + keyframesCssBlockToText, + resolveThemeTree, + type ButtonCssBlock, + type DesignCoreTheme, +} from 'gd-design-core'; type SkeletonVariant = 'rounded' | 'rectangular' | 'circular'; type CssBlock = ButtonCssBlock; @@ -18,8 +25,10 @@ export class GdSkeleton extends LitElement { @property({ attribute: false }) theme: DesignCoreTheme = {}; render() { - const tokens = resolveThemeTree(skeleton, this.theme) as unknown as Record; - const resolvedAnimations = resolveThemeTree(animations, this.theme) as unknown as Record; + const skeletonTokens = get(this.theme, 'skeleton', skeleton) as typeof skeleton; + const animationTokens = get(this.theme, 'animations', animations) as typeof animations; + const tokens = resolveThemeTree(skeletonTokens, this.theme) as unknown as Record; + const resolvedAnimations = resolveThemeTree(animationTokens, this.theme) as unknown as Record; const themeFrames = this.animationName ? resolvedAnimations[this.animationName] : undefined; const animationCssName = themeFrames ? 'gd-skeleton-animation' : this.animationName; const dimensions: CssBlock = { @@ -39,7 +48,7 @@ export class GdSkeleton extends LitElement { ? buttonCssBlockToText('.skeleton', { animation: `${animationCssName} ${this.animationProps}` }) : '', buttonCssBlockToText('.skeleton', this.styles), - themeFrames ? buttonCssBlockToText('@keyframes gd-skeleton-animation', themeFrames) : '', + keyframesCssBlockToText('gd-skeleton-animation', themeFrames), ].join('\n'); if (this.shadowRoot && typeof CSSStyleSheet !== 'undefined') { const sheet = new CSSStyleSheet(); diff --git a/libs/web-components/stories/fixtures/Button.generated.js b/libs/web-components/stories/fixtures/Button.generated.js index c2c147c..ade2704 100644 --- a/libs/web-components/stories/fixtures/Button.generated.js +++ b/libs/web-components/stories/fixtures/Button.generated.js @@ -15,6 +15,10 @@ const meta = { title: 'Atoms/Button', component: Button, tags: ['autodocs'], + args: { + children: 'Button', + variant: 'primary', + }, parameters: { layout: 'centered', docs: { @@ -81,21 +85,28 @@ The Button component offers six distinct visual styles to accommodate different

🔧 Technical Implementation

Built with React's \`forwardRef\` for proper ref handling, the component integrates seamlessly with the theme system via \`useTheme\` hook. It extends Box component props for flexible layout control and uses styled-components for dynamic theming. - - -
-
- -

🧩 Web Components track (CTORNDSD-646)

-Verdict — Lit custom element. Owns variant, loading and focus-ring state, and CTORNDSD-286 was a button style-collision bug — Shadow DOM isolation is the reason. Ported as <gd-button>: 1.87 kB gzip vs 19.07 kB. -
-Decision rule and full rationale: docs/webcomponents-migration/05-native-html-guidelines.md. `, }, }, }, }; export default meta; +export const Default = { + args: { + children: 'Button', + variant: 'primary', + onClick: fn(), + }, + parameters: { + docs: { + source: {}, + description: { + story: + 'This is the default interactive story. Use the controls panel to experiment with different props and see how the Button component behaves. Try changing the variant, adding icons, adjusting rounded corners, and toggling states.', + }, + }, + }, +}; export const AllVariants = () => { return demo( Column, @@ -219,22 +230,6 @@ AllVariants.parameters = { }, }, }; -export const Default = { - args: { - children: 'Button', - variant: 'primary', - onClick: fn(), - }, - parameters: { - docs: { - source: {}, - description: { - story: - 'This is the default interactive story. Use the controls panel to experiment with different props and see how the Button component behaves. Try changing the variant, adding icons, adjusting rounded corners, and toggling states.', - }, - }, - }, -}; export const WithIcons = { args: { children: 'Save Changes', From ca0215b101ae876012a84e77d45f7678772cbc81 Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:39:01 +0200 Subject: [PATCH 16/17] feat(label,button): add color and ariaLabel props, reflect isLoading attribute - Add `color` prop to gd-label for custom text color styling - Add `ariaLabel` prop to gd-label for accessibility - Reflect `isLoading` attribute on gd-button for proper HTML representation - Update react-parity.json to track new Label properties - Add tests for color/ariaLabel mapping and isLoading reflection - Update Storybook arg types and helpers for new properties - Enhance Button isLoading with description and control metadata - Fix React storybook metadata capture to support 'React/' prefix in titles --- .../atoms/Button/Button.stories.tsx | 9 ++++++ .../components/atoms/Label/Label.stories.tsx | 10 ++++++ libs/web-components/react-parity.json | 2 +- .../capture-react-storybook-metadata.mjs | 4 ++- .../atoms/gd-button/gd-button.spec.ts | 14 ++++++-- .../components/atoms/gd-button/gd-button.ts | 2 +- .../atoms/gd-label/gd-label.spec.ts | 13 ++++++++ .../src/components/atoms/gd-label/gd-label.ts | 12 +++++-- libs/web-components/stories/helpers.ts | 4 +-- .../react-storybook-arg-types.generated.ts | 28 +++++++++++++++- .../react-storybook-arg-types.snapshot.json | 32 +++++++++++++++++-- 11 files changed, 117 insertions(+), 13 deletions(-) diff --git a/libs/ui/src/components/atoms/Button/Button.stories.tsx b/libs/ui/src/components/atoms/Button/Button.stories.tsx index 6fcfdf0..01f4894 100644 --- a/libs/ui/src/components/atoms/Button/Button.stories.tsx +++ b/libs/ui/src/components/atoms/Button/Button.stories.tsx @@ -62,6 +62,15 @@ const meta: Meta = { type: { summary: 'boolean' }, }, }, + isLoading: { + description: 'Displays a loading indicator and prevents interaction', + control: 'boolean', + table: { + category: 'Content & Behavior', + defaultValue: { summary: 'false' }, + type: { summary: 'boolean' }, + }, + }, // ============================================================================ // Visual Style diff --git a/libs/ui/src/components/atoms/Label/Label.stories.tsx b/libs/ui/src/components/atoms/Label/Label.stories.tsx index 4f9cbea..8bcdf42 100644 --- a/libs/ui/src/components/atoms/Label/Label.stories.tsx +++ b/libs/ui/src/components/atoms/Label/Label.stories.tsx @@ -63,6 +63,15 @@ const meta: Meta = { defaultValue: { summary: '{}' }, }, }, + color: { + control: 'color', + description: 'Text color applied to the label', + table: { + category: 'Styling', + type: { summary: 'string' }, + defaultValue: { summary: 'undefined' }, + }, + }, // Events onClick: { @@ -78,6 +87,7 @@ const meta: Meta = { control: 'text', description: 'Accessible label for screen readers', table: { + category: 'Accessibility', type: { summary: 'string' }, defaultValue: { summary: 'undefined' }, }, diff --git a/libs/web-components/react-parity.json b/libs/web-components/react-parity.json index 5380fa1..5242cef 100644 --- a/libs/web-components/react-parity.json +++ b/libs/web-components/react-parity.json @@ -181,7 +181,7 @@ "gd-label": { "react": "Label", "excludedStories": ["DefaultWithTailwind"], - "source": ["htmlFor", "styles", "slot", "part=\"label\""], + "source": ["htmlFor", "color", "ariaLabel", "styles", "slot", "part=\"label\""], "stories": ["LabelDefault", "LabelWithChildIcon", "CustomStyles", "WithAccessibility", "DefaultTokens"] }, "gd-link": { diff --git a/libs/web-components/scripts/capture-react-storybook-metadata.mjs b/libs/web-components/scripts/capture-react-storybook-metadata.mjs index f91565e..f88d76c 100644 --- a/libs/web-components/scripts/capture-react-storybook-metadata.mjs +++ b/libs/web-components/scripts/capture-react-storybook-metadata.mjs @@ -18,7 +18,9 @@ try { for (const [tag, contract] of Object.entries(parity)) { const group = tag === 'gd-counter' || tag === 'gd-menu' ? 'Molecules' : 'Atoms'; const title = `${group}/${contract.react}`; - const entry = entries.find((candidate) => candidate.type === 'story' && candidate.title === title); + const entry = entries.find( + (candidate) => candidate.type === 'story' && (candidate.title === title || candidate.title === `React/${title}`) + ); if (!entry) throw new Error(`The source Storybook does not contain ${title}.`); const storyUrl = new URL('/iframe.html', sourceUrl); storyUrl.searchParams.set('id', entry.id); diff --git a/libs/web-components/src/components/atoms/gd-button/gd-button.spec.ts b/libs/web-components/src/components/atoms/gd-button/gd-button.spec.ts index 41898a6..bc41ab6 100644 --- a/libs/web-components/src/components/atoms/gd-button/gd-button.spec.ts +++ b/libs/web-components/src/components/atoms/gd-button/gd-button.spec.ts @@ -45,11 +45,21 @@ afterEach(() => { describe('gd-button', () => { it('composes gd-loader for the React loading state', async () => { const el = add('primary'); - (el as unknown as { isLoading: boolean }).isLoading = true; + el.setAttribute('is-loading', ''); await settle(el); - const loader = el.shadowRoot!.querySelector('gd-loader'); + const inner = el.shadowRoot!.querySelector('button')!; + const loader = el.shadowRoot!.querySelector< + HTMLElement & { size: string; withWrapper: boolean; updateComplete: Promise } + >('gd-loader'); + + await loader?.updateComplete; + + expect((el as unknown as { isLoading: boolean }).isLoading).toBe(true); + expect(inner.disabled).toBe(true); + expect(inner.getAttribute('aria-busy')).toBe('true'); expect(loader?.size).toBe('sm'); expect(loader?.withWrapper).toBe(false); + expect(getComputedStyle(loader!.shadowRoot!.querySelector('.loader')!).width).toBe('24px'); }); it('resolves the real primary token colour, not a placeholder string', async () => { diff --git a/libs/web-components/src/components/atoms/gd-button/gd-button.ts b/libs/web-components/src/components/atoms/gd-button/gd-button.ts index 83e8ddb..af80686 100644 --- a/libs/web-components/src/components/atoms/gd-button/gd-button.ts +++ b/libs/web-components/src/components/atoms/gd-button/gd-button.ts @@ -173,7 +173,7 @@ export class GdButton extends LitElement { @property({ type: String }) variant: ButtonVariantName = 'primary'; @property({ type: String }) rounded: ButtonRounded = 'none'; @property({ type: Boolean, reflect: true }) disabled = false; - @property({ type: Boolean }) isLoading = false; + @property({ type: Boolean, attribute: 'is-loading', reflect: true }) isLoading = false; @property({ type: Boolean }) isIcon = false; @property({ type: Boolean }) fullWidth = false; @property({ type: String }) type: ButtonType = 'button'; diff --git a/libs/web-components/src/components/atoms/gd-label/gd-label.spec.ts b/libs/web-components/src/components/atoms/gd-label/gd-label.spec.ts index 2e1e76c..10966ed 100644 --- a/libs/web-components/src/components/atoms/gd-label/gd-label.spec.ts +++ b/libs/web-components/src/components/atoms/gd-label/gd-label.spec.ts @@ -15,4 +15,17 @@ describe('gd-label', () => { el.remove(); input.remove(); }); + + it('maps color and ariaLabel to the rendered label', async () => { + const el = document.createElement('gd-label'); + el.color = '#123456'; + el.ariaLabel = 'Account name'; + document.body.append(el); + await el.updateComplete; + + const label = el.shadowRoot!.querySelector('label')!; + expect(label.style.color).toBe('rgb(18, 52, 86)'); + expect(label.getAttribute('aria-label')).toBe('Account name'); + el.remove(); + }); }); diff --git a/libs/web-components/src/components/atoms/gd-label/gd-label.ts b/libs/web-components/src/components/atoms/gd-label/gd-label.ts index 181e3dc..0043f25 100644 --- a/libs/web-components/src/components/atoms/gd-label/gd-label.ts +++ b/libs/web-components/src/components/atoms/gd-label/gd-label.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from 'lit'; +import { LitElement, html, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import { label } from 'gd-design-library/tokens'; @@ -7,6 +7,8 @@ import { get, resolveThemeTree, type DesignCoreTheme } from 'gd-design-core'; @customElement('gd-label') export class GdLabel extends LitElement { @property({ type: String, attribute: 'for', reflect: true }) htmlFor?: string; + @property({ type: String }) color?: string; + @property({ type: String, attribute: 'aria-label' }) override ariaLabel: string | null = null; @property({ attribute: false }) styles: Record = {}; @property({ attribute: false }) theme: DesignCoreTheme = {}; render() { @@ -14,7 +16,13 @@ export class GdLabel extends LitElement { return html``; } diff --git a/libs/web-components/stories/helpers.ts b/libs/web-components/stories/helpers.ts index 9629837..7663b8b 100644 --- a/libs/web-components/stories/helpers.ts +++ b/libs/web-components/stories/helpers.ts @@ -34,13 +34,13 @@ const sourceKeyProperties: Partial> = { 'gd-avatar': ['sizeVariant'], 'gd-badge': ['variant', 'appearance', 'size'], 'gd-box': ['variant', 'isBordered'], - 'gd-button': ['variant', 'rounded'], + 'gd-button': ['variant', 'rounded', 'isLoading'], 'gd-checkbox': ['size'], 'gd-input': ['variant', 'color'], 'gd-image': ['src', 'alt', 'objectFit'], 'gd-icon': ['name', 'size'], 'gd-input-file': ['accept', 'multiple'], - 'gd-label': ['htmlFor'], + 'gd-label': ['htmlFor', 'color', 'ariaLabel'], 'gd-link': ['variant', 'href'], 'gd-loader': ['name', 'variant', 'size'], 'gd-separator': ['orientation', 'variant', 'size', 'labelPosition'], diff --git a/libs/web-components/stories/react-storybook-arg-types.generated.ts b/libs/web-components/stories/react-storybook-arg-types.generated.ts index e2cbdf6..3832d9d 100644 --- a/libs/web-components/stories/react-storybook-arg-types.generated.ts +++ b/libs/web-components/stories/react-storybook-arg-types.generated.ts @@ -2546,6 +2546,7 @@ export const reactStorybookArgTypes = { defaultValue: { summary: 'undefined', }, + category: 'Accessibility', }, }, htmlFor: { @@ -2620,6 +2621,23 @@ export const reactStorybookArgTypes = { disable: false, }, }, + color: { + control: { + type: 'color', + disable: false, + }, + name: 'color', + description: 'Text color applied to the label', + table: { + category: 'Styling', + type: { + summary: 'string', + }, + defaultValue: { + summary: 'undefined', + }, + }, + }, }, Link: { variant: { @@ -5176,7 +5194,7 @@ export const reactStorybookArgTypes = { }, isLoading: { name: 'isLoading', - description: '', + description: 'Displays a loading indicator and prevents interaction', type: { required: false, name: 'boolean', @@ -5185,6 +5203,14 @@ export const reactStorybookArgTypes = { type: { summary: 'boolean', }, + defaultValue: { + summary: 'false', + }, + category: 'Content & Behavior', + }, + control: { + type: 'boolean', + disable: false, }, }, ariaLabel: { diff --git a/libs/web-components/stories/react-storybook-arg-types.snapshot.json b/libs/web-components/stories/react-storybook-arg-types.snapshot.json index dc8870d..63d5759 100644 --- a/libs/web-components/stories/react-storybook-arg-types.snapshot.json +++ b/libs/web-components/stories/react-storybook-arg-types.snapshot.json @@ -2495,7 +2495,8 @@ }, "defaultValue": { "summary": "undefined" - } + }, + "category": "Accessibility" } }, "htmlFor": { @@ -2569,6 +2570,23 @@ "type": "object", "disable": false } + }, + "color": { + "control": { + "type": "color", + "disable": false + }, + "name": "color", + "description": "Text color applied to the label", + "table": { + "category": "Styling", + "type": { + "summary": "string" + }, + "defaultValue": { + "summary": "undefined" + } + } } }, "Link": { @@ -5085,7 +5103,7 @@ }, "isLoading": { "name": "isLoading", - "description": "", + "description": "Displays a loading indicator and prevents interaction", "type": { "required": false, "name": "boolean" @@ -5093,7 +5111,15 @@ "table": { "type": { "summary": "boolean" - } + }, + "defaultValue": { + "summary": "false" + }, + "category": "Content & Behavior" + }, + "control": { + "type": "boolean", + "disable": false } }, "ariaLabel": { From fe3c2c764f4b2e65b4f0f87588098acf47da7543 Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:54:57 +0200 Subject: [PATCH 17/17] feat(storybook): style catalog heading to match design system typography - Add custom CSS to normalize root collapse button font styling - Apply font-size: 14px, font-weight: 700, and remove text-transform - Inject styles via managerHead configuration --- libs/ui/.storybook/main.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/libs/ui/.storybook/main.ts b/libs/ui/.storybook/main.ts index 5bd6e20..8d82297 100644 --- a/libs/ui/.storybook/main.ts +++ b/libs/ui/.storybook/main.ts @@ -6,6 +6,16 @@ import remarkGfm from 'remark-gfm'; import { legacyRedirectHead, sharedDocs } from '../../../bin/storybook/routes.mjs'; const sharedNames = Object.keys(sharedDocs).join('|'); +const catalogHeadingStyles = ``; const config: StorybookConfig = { stories: [ @@ -19,7 +29,7 @@ const config: StorybookConfig = { url: configType === 'DEVELOPMENT' ? 'http://localhost:6007' : './web-components/', }, }), - managerHead: (head) => `${legacyRedirectHead()}${head}`, + managerHead: (head) => `${legacyRedirectHead()}${catalogHeadingStyles}${head}`, previewHead: (head) => `${legacyRedirectHead()}${head}`, staticDirs: ['public', '../src/assets'], addons: [