diff --git a/CLAUDE.md b/CLAUDE.md index 79dd69b..404c6bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,7 @@ Path aliases defined in `libs/ui/vitest.alias.ts` and `libs/ui/tsconfig.json`: ` **Build output** — Vite produces both ESM (`.js`, `preserveModules`) and CJS (`.cjs`) to `dist/libs/ui`. Post-build step (`scripts/postbuild-types.mjs`) patches `.d.ts` exports. React and `@emotion/*` are externalized. `bin/export-theme.js` serializes `defaultTheme` to JSON from the built dist; it runs automatically as part of `npm run storybook` and `npm run build-storybook`. -**Shared Storybook** — `npm run storybook` manages the React host (6006) and native Web Components preview (6007) together. Open only the host. Framework-neutral pages listed in `bin/storybook/shared-docs.json` appear at the root, before React, without a prefix. Other MDX and component stories receive a central `React/` title prefix; do not duplicate it in their titles. Keep React integration examples under React, even when their topic also has shared design guidance. The combined static artifact is `libs/ui/storybook-static`, with the child under `web-components/`. Use generated story IDs for links and run `storybook:check-links` after the combined build. Package-only validation uses `node bin/storybook/check-links.mjs --package` and indexes source stories without building previews. Do not regenerate the frozen legacy route inventory to conceal missing entries. Redirects support both original links and interim React-prefixed links to shared pages. Phase 1 contains only the native overview; component coverage and harness cleanup are later phases. +**Shared Storybook** — `npm run storybook` manages the React host (6006) and native Web Components preview (6007) together. Open only the host. Framework-neutral pages listed in `bin/storybook/shared-docs.json` appear at the root, before React, without a prefix. Other MDX and component stories receive a central `React/` title prefix; do not duplicate it in their titles. Keep React integration examples under React, even when their topic also has shared design guidance. The combined static artifact is `libs/ui/storybook-static`, with the child under `web-components/`. Use generated story IDs for links and run `storybook:check-links` after the combined build. Package-only validation uses `node bin/storybook/check-links.mjs --package` and indexes source stories without building previews. Do not regenerate the frozen legacy route inventory to conceal missing entries. Redirects support both original links and interim React-prefixed links to shared pages. The native catalog covers all 23 published atoms and two molecules. `node bin/storybook/smoke-test.mjs` verifies native rendering and representative interactions after the combined build. Keep Angular and Vue native-element harness coverage aligned with the supported catalog. **Package subpaths** — the published package exposes four explicit subpaths (wildcards are intentionally removed to prevent internal path coupling): diff --git a/README.md b/README.md index e464aaf..d62c857 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Shared **Introduction**, **Theme & Tokens**, and framework-neutral **Patterns & appear at the top level, before the framework catalogs. One entry point contains two peer catalogs: **React** (components and React-specific guides) and **Web Components** (native custom-element stories). The Web Components catalog currently -contains the Phase 1 overview; Phase 2 adds stories for the six existing atoms and two molecules. +contains native stories and controls for all 23 published atoms and two molecules. `npm run storybook` manages both preview servers automatically; open `http://localhost:6006`. Ports 6006 and 6007 must be free. There is no separate Web Components startup command. @@ -82,7 +82,7 @@ including the composed preview under `web-components/`. ```bash npm run storybook:check-links # Validate built Storybooks and shipped AI documentation links -npm run storybook:test # Existing React interaction tests (native coverage follows in Phase 2) +npm run storybook:test # React interaction tests npm run storybook:visual # Existing React visual tests ``` @@ -98,7 +98,10 @@ Use the IDs in the generated `index.json` for links. Both pre-migration URLs and list to refresh when a story disappears. Link checks cover source documentation and the `llms.txt`, AI Markdown/JSON, and AI JavaScript copied into the UI package. -The Angular/Vue and other Web Components harnesses are unchanged in Phase 1. +Native browser smoke checks run with `node bin/storybook/smoke-test.mjs` after the combined build. +They render every native story and exercise representative custom events, state changes, +semantic output, truncation, and popover dismissal. Angular and Vue harnesses render every +supported atom family and both molecules through native custom elements. ## Verification (gd-design-library) diff --git a/bin/generate-icon-catalog.ts b/bin/generate-icon-catalog.ts new file mode 100644 index 0000000..c755546 --- /dev/null +++ b/bin/generate-icon-catalog.ts @@ -0,0 +1,28 @@ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { renderToStaticMarkup } from 'react-dom/server'; +import React, { createElement } from 'react'; +import { IconsList } from '../libs/ui/src/components/atoms/Icon/constants'; + +Object.assign(globalThis, { React }); + +const catalog = Object.fromEntries( + Object.entries(IconsList).map(([name, Component]) => { + const markup = renderToStaticMarkup( + createElement(Component, { fill: 'var(--gd-icon-fill)', fillSvg: 'var(--gd-icon-fill-svg)' }) + ); + const match = markup.match(/^]*)>([\s\S]*)<\/svg>$/); + if (!match) throw new Error(`Icon ${name} did not render one SVG root.`); + const viewBox = match[1].match(/viewBox="([^"]+)"/)?.[1]; + if (!viewBox) throw new Error(`Icon ${name} has no viewBox.`); + return [name, { viewBox, body: match[2] }]; + }) +); + +const output = + `// Generated from GridKit SVG assets by bin/generate-icon-catalog.ts.\n` + + `// Edit the source SVG and regenerate; React and Web Components consume this catalog.\n` + + `export const iconCatalog = ${JSON.stringify(catalog, null, 2)} as const;\n\n` + + `export type GridKitIconName = keyof typeof iconCatalog;\n`; + +writeFileSync(resolve(import.meta.dirname, '../libs/design-core/src/icons/catalog.ts'), output); diff --git a/bin/storybook/check-links.mjs b/bin/storybook/check-links.mjs index af8ba35..ff519dd 100644 --- a/bin/storybook/check-links.mjs +++ b/bin/storybook/check-links.mjs @@ -67,6 +67,17 @@ 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'); +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']) { + const id = `${group}-${name}--${variant}`; + if (!indexes.webComponents.entries[id]) errors.push(`Missing native Web Components entry: ${id}`); + } + } +} const excluded = new Set(['node_modules', '.git', 'dist', 'storybook-static', 'coverage', 'output', '.nx']); const extensions = new Set([ diff --git a/bin/storybook/native-checks.mjs b/bin/storybook/native-checks.mjs new file mode 100644 index 0000000..6a58332 --- /dev/null +++ b/bin/storybook/native-checks.mjs @@ -0,0 +1,221 @@ +import assert from 'node:assert/strict'; + +/** 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(); + const stories = Object.values(index.entries).filter( + (entry) => + entry.type === 'story' && /^(Atoms|Molecules)\//.test(entry.title) && !entry.id.endsWith('--default-tokens') + ); + const names = new Set(); + const tags = { inputfile: 'input-file', sliderdots: 'slider-dots' }; + 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`); + const tag = `gd-${tags[name] ?? name}`; + await page.locator(tag).first().waitFor({ state: 'attached' }); + await page.waitForFunction((tag) => { + const el = document.querySelector(tag); + return el?.shadowRoot?.childElementCount > 0; + }, tag); + } + assert.deepEqual([...names].sort(), [ + 'avatar', + 'badge', + 'box', + 'button', + 'checkbox', + 'counter', + 'icon', + 'image', + 'input', + 'inputfile', + 'label', + 'link', + 'loader', + 'menu', + 'select', + 'separator', + 'skeleton', + 'slider', + 'sliderdots', + 'switch', + 'textarea', + 'toggle', + 'truncate', + 'typography', + 'wrapper', + ]); + const tokenStories = Object.values(index.entries).filter( + (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`); + const viewer = page.getByRole('region', { name: 'Token viewer' }); + await viewer.waitFor(); + const text = await viewer.textContent(); + assert.ok(text.length > 50, `${story.id} should render its theme token values`); + assert.doesNotMatch(text, /^defaultTheme\./, `${story.id} should not render a token placeholder`); + const branch = viewer.getByRole('button').first(); + await branch.click(); + assert.equal(await branch.getAttribute('aria-expanded'), 'false'); + } + 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 page.locator('#storybook-root').waitFor(); + }; + 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'); + 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 }) + .isEditable() + .then((value) => !value) + ); + await open('atoms-checkbox--default'); + // The native input is visually hidden; users activate its visible label. + await page.locator('gd-checkbox label').click(); + await event({ checked: true }); + await page.locator('gd-checkbox label').click(); + await event({ checked: false }); + 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('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 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); + 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', + ] + ); + await open('atoms-typography--display-sizes'); + const displayFontSizes = await page + .locator('gd-typography') + .evaluateAll((elements) => + elements.map((element) => getComputedStyle(element.shadowRoot.firstElementChild).fontSize) + ); + assert.equal(new Set(displayFontSizes).size, 5); + await open('atoms-slider--default'); + const slider = page.getByRole('slider', { name: 'Slider' }); + await slider.fill('55'); + await event({ value: 55 }); + await open('atoms-sliderdots--default'); + await page.getByRole('tab', { name: 'Go to slide 3' }).click(); + await event({ index: 2 }); + await open('atoms-switch--default'); + await page.locator('gd-switch label').click(); + await event({ checked: true }); + await open('atoms-textarea--default'); + await page.getByRole('textbox').fill('Updated comment'); + await event({ value: 'Updated comment' }); + await open('atoms-toggle--default'); + await page.getByRole('button', { name: 'Option 2', exact: true }).click(); + await event({ value: 'Option 2' }); + await open('atoms-truncate--line-truncation'); + assert.equal( + await page + .locator('gd-truncate') + .evaluate((element) => getComputedStyle(element.shadowRoot.querySelector('[part="content"]')).webkitLineClamp), + '2' + ); + await open('atoms-wrapper--custom-tag-wrapper'); + assert.equal( + await page.locator('gd-wrapper').evaluate((element) => element.shadowRoot.firstElementChild.tagName), + 'SECTION' + ); + await open('molecules-counter--default'); + await page.getByRole('button', { name: 'Increment counter', exact: true }).click(); + await event({ value: 2 }); + await page.getByRole('button', { name: 'Decrement counter', exact: true }).click(); + await event({ value: 1 }); + assert.ok(await page.getByRole('button', { name: 'Decrement counter', exact: true }).isDisabled()); + await page.getByRole('spinbutton', { name: 'Quantity value' }).fill('99'); + await page.getByRole('spinbutton', { name: 'Quantity value' }).press('Tab'); + await event({ value: 5 }); + assert.ok(await page.getByRole('button', { name: 'Increment counter', exact: true }).isDisabled()); + const menuClosed = () => + page.waitForFunction(() => { + const menu = document.querySelector('gd-menu'); + const content = menu?.shadowRoot.querySelector('[popover]'); + return ( + menu && + content && + !menu.open && + !content.matches(':popover-open') && + getComputedStyle(content).display === 'none' + ); + }); + 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 menuClosed(); + await page.getByRole('button', { name: 'Actions', exact: true }).click(); + await page.getByRole('button', { name: 'Edit', 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 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(); + 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 0591924..eac2bde 100644 --- a/bin/storybook/smoke-test.mjs +++ b/bin/storybook/smoke-test.mjs @@ -4,6 +4,7 @@ import { readFile, stat } from 'node:fs/promises'; import { extname, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { chromium } from 'playwright'; +import { checkNativeStories } from './native-checks.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)); @@ -97,12 +98,18 @@ try { // 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: 'GridKit Web Components', exact: true }).waitFor(); + await nativeFrame.getByRole('heading', { name: 'Using Web Components', exact: true }).waitFor(); + + await page.goto(`${base}/?path=/story/web-components_atoms-button--default`); + await nativeFrame.getByRole('button', { name: 'Button', exact: true }).waitFor(); + await page.goto(`${base}/?path=/docs/web-components_atoms-button--docs`); + await nativeFrame.getByRole('heading', { name: 'Button', exact: true }).first().waitFor(); await page.goto(`${base}/iframe.html?id=atoms-button--default&viewMode=story&args=size:sm`); 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); 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/libs/design-core/src/icons/catalog.ts b/libs/design-core/src/icons/catalog.ts new file mode 100644 index 0000000..1a27593 --- /dev/null +++ b/libs/design-core/src/icons/catalog.ts @@ -0,0 +1,226 @@ +// Generated from GridKit SVG assets by bin/generate-icon-catalog.ts. +// Edit the source SVG and regenerate; React and Web Components consume this catalog. +export const iconCatalog = { + cross: { + viewBox: '0 0 24 24', + body: '', + }, + success: { + viewBox: '0 0 24 24', + body: '', + }, + error: { + viewBox: '0 0 24 24', + body: '', + }, + warning: { + viewBox: '0 0 24 24', + body: '', + }, + info: { + viewBox: '0 0 24 24', + body: '', + }, + dot: { + viewBox: '0 0 6 6', + body: '', + }, + check: { + viewBox: '0 0 24 24', + body: '', + }, + arrowDown: { + viewBox: '0 0 24 24', + body: '', + }, + arrowRight: { + viewBox: '0 0 24 24', + body: '', + }, + arrowLeft: { + viewBox: '0 0 24 24', + body: '', + }, + mobileMenu: { + viewBox: '0 0 24 24', + body: '', + }, + home: { + viewBox: '0 0 24 24', + body: '', + }, + slash: { + viewBox: '0 0 5 15', + body: '', + }, + arrowForward: { + viewBox: '0 0 24 24', + body: '', + }, + localShipping: { + viewBox: '0 0 24 24', + body: '', + }, + favorite: { + viewBox: '0 0 24 24', + body: '', + }, + favoriteOutlined: { + viewBox: '0 0 24 24', + body: '', + }, + deleteOutlined: { + viewBox: '0 0 24 24', + body: '', + }, + accountCircle: { + viewBox: '0 0 24 24', + body: '', + }, + shoppingBag: { + viewBox: '0 0 24 24', + body: '', + }, + errorOutline: { + viewBox: '0 0 24 24', + body: '', + }, + star: { + viewBox: '0 0 24 24', + body: '', + }, + starOutlined: { + viewBox: '0 0 24 24', + body: '', + }, + starHalf: { + viewBox: '0 0 24 24', + body: '', + }, + minus: { + viewBox: '0 0 24 24', + body: '', + }, + plus: { + viewBox: '0 0 24 24', + body: '', + }, + filter: { + viewBox: '0 0 24 24', + body: '', + }, + ruler: { + viewBox: '0 0 16 16', + body: '', + }, + processing: { + viewBox: '0 0 20 20', + body: '', + }, + paymentCard: { + viewBox: '0 0 24 24', + body: '', + }, + eye: { + viewBox: '0 0 24 24', + body: '', + }, + attachment: { + viewBox: '0 0 24 24', + body: '', + }, + upload: { + viewBox: '0 0 24 24', + body: '', + }, + folder: { + viewBox: '0 0 24 24', + body: '', + }, + folderOpen: { + viewBox: '0 0 24 24', + body: '', + }, + wifiTethering: { + viewBox: '0 0 24 24', + body: '', + }, + portrait: { + viewBox: '0 0 24 24', + body: '', + }, + search: { + viewBox: '0 0 24 24', + body: '', + }, + edit: { + viewBox: '0 0 24 24', + body: '', + }, + volumeUp: { + viewBox: '0 0 24 24', + body: '', + }, + contentCopy: { + viewBox: '0 0 24 24', + body: '', + }, + thumbUp: { + viewBox: '0 0 16 16', + body: '', + }, + thumbDown: { + viewBox: '0 0 16 16', + body: '', + }, + fileCopy: { + viewBox: '0 0 24 24', + body: '', + }, + keyboardArrowDown: { + viewBox: '0 0 24 24', + body: '', + }, + keyboardArrowUp: { + viewBox: '0 0 24 24', + body: '', + }, + send: { + viewBox: '0 0 24 24', + body: '', + }, + thumbUpFilled: { + viewBox: '0 0 24 24', + body: '', + }, + thumbDownFilled: { + viewBox: '0 0 24 24', + body: '', + }, + chat: { + viewBox: '0 0 24 24', + body: '', + }, + chatBubble: { + viewBox: '0 0 24 24', + body: '', + }, + phone: { + viewBox: '0 0 24 24', + body: '', + }, + mic: { + viewBox: '0 0 24 24', + body: '', + }, + fullscreen: { + viewBox: '0 0 24 24', + body: '', + }, + fullscreenExit: { + viewBox: '0 0 24 24', + body: '', + }, +} as const; + +export type GridKitIconName = keyof typeof iconCatalog; diff --git a/libs/design-core/src/icons/index.ts b/libs/design-core/src/icons/index.ts new file mode 100644 index 0000000..1f72407 --- /dev/null +++ b/libs/design-core/src/icons/index.ts @@ -0,0 +1 @@ +export { iconCatalog, type GridKitIconName } from './catalog'; diff --git a/libs/design-core/src/index.ts b/libs/design-core/src/index.ts index 47946d3..cc0ca44 100644 --- a/libs/design-core/src/index.ts +++ b/libs/design-core/src/index.ts @@ -3,3 +3,4 @@ export * from './stores'; export type { DesignCoreTheme } from './types'; export { resolveThemeTree } from './utils/resolveThemeTree'; export { get } from './utils/get'; +export { iconCatalog, type GridKitIconName } from './icons'; diff --git a/libs/design-core/src/stores/createSelectStore.ts b/libs/design-core/src/stores/createSelectStore.ts index 6348efc..1c4ea0a 100644 --- a/libs/design-core/src/stores/createSelectStore.ts +++ b/libs/design-core/src/stores/createSelectStore.ts @@ -25,6 +25,7 @@ export interface SelectStoreActions { toggle: () => void; setSearchText: (text: string) => void; setDisabled: (disabled: boolean) => void; + setMultiple: (multiple: boolean) => void; /** Call when the consumer's external `value` prop changes — mirrors `Select.tsx`'s value-sync effect. */ syncExternalValue: (value: SelectValue) => void; /** Mirrors `Select.tsx`'s `_onSelect`: single-select replaces + closes, multi-select toggles membership. */ @@ -74,6 +75,10 @@ export function createSelectStore(options: CreateSelectStoreOptions = {}) { }, setSearchText: (text) => set({ searchText: text }), setDisabled: (next) => set({ disabled: next }), + setMultiple: (next) => { + const state = get(); + set({ multiple: next, internalValue: normalizeValue(state.internalValue, next) }); + }, syncExternalValue: (nextValue) => { const state = get(); diff --git a/libs/form-configurator/adapter-react/tsconfig.lib.json b/libs/form-configurator/adapter-react/tsconfig.lib.json index b9d6f7e..56a5ac5 100644 --- a/libs/form-configurator/adapter-react/tsconfig.lib.json +++ b/libs/form-configurator/adapter-react/tsconfig.lib.json @@ -11,6 +11,8 @@ ], "baseUrl": ".", "paths": { + "gd-design-core": ["../../design-core/src/index.ts"], + "gd-design-core/*": ["../../design-core/src/*"], "gd-form-configurator": ["../core/src/index.ts"], "@form-configurator/adapter-react": ["./src/index.ts"], "@griddynamics/ui": ["../../../libs/ui/src/index.ts"], diff --git a/libs/ui/package.json b/libs/ui/package.json index ce05976..fed0c2d 100644 --- a/libs/ui/package.json +++ b/libs/ui/package.json @@ -58,6 +58,7 @@ } }, "dependencies": { + "gd-design-core": "*", "@react-spring/web": "^10.1.2", "@visx/curve": "^3.12.0", "embla-carousel-react": "^8.6.0", diff --git a/libs/ui/project.json b/libs/ui/project.json index 96d0479..8607982 100644 --- a/libs/ui/project.json +++ b/libs/ui/project.json @@ -6,6 +6,13 @@ "tags": [], "// targets": "to see all targets run: nx show project ui --web", "targets": { + "typecheck": { + "executor": "nx:run-commands", + "options": { + "command": "tsc -b libs/design-core/tsconfig.lib.json && tsc --noEmit -p libs/ui/tsconfig.lib.json", + "cwd": "{workspaceRoot}" + } + }, "build-storybook": { "inputs": ["default", "^production", "{workspaceRoot}/bin/storybook/**/*"] }, diff --git a/libs/ui/src/components/atoms/Icon/Icon.tsx b/libs/ui/src/components/atoms/Icon/Icon.tsx index 6813016..a13d796 100644 --- a/libs/ui/src/components/atoms/Icon/Icon.tsx +++ b/libs/ui/src/components/atoms/Icon/Icon.tsx @@ -1,12 +1,13 @@ 'use client'; -import { forwardRef, ComponentType } from 'react'; +import { forwardRef, ComponentType, type CSSProperties, type Ref, type RefAttributes } from 'react'; +import { iconCatalog, type GridKitIconName } from 'gd-design-core'; import { get } from '@utils'; import { useTheme } from '@hooks/useTheme'; import { getBoxStyles, resolveThemeColor } from '@tokens/utils'; import type { BoxStyles } from '@types'; -import { IconsList as DefaultIconsList, COMPONENT_NAME } from './constants'; +import { COMPONENT_NAME } from './constants'; import { IconProps } from './Icon.types'; let CustomIconsList: Record> = {}; @@ -20,10 +21,10 @@ export const Icon = forwardRef( const { theme } = useTheme(); const { icon, colors } = theme || {}; const iconSize = get(icon, ['size', size], { width, height }); - const IconsList = { ...DefaultIconsList, ...CustomIconsList }; - const SelectedIcon = IconsList[name as keyof typeof IconsList]; + const CustomIcon = CustomIconsList[name]; + const definition = iconCatalog[name as GridKitIconName]; - if (!SelectedIcon) { + if (!CustomIcon && !definition) { console.warn(`Icon "${String(name)}" not found.`); return null; } @@ -31,15 +32,36 @@ export const Icon = forwardRef( const { boxStyles, restProps: restNotStyledProps } = getBoxStyles(rest as BoxStyles); const componentStyles = [boxStyles, styles]; + if (CustomIcon) { + const RegisteredIcon = CustomIcon as ComponentType>; + return ( + } + name={name} + data-testid={`${COMPONENT_NAME}-${name}`} + fill={resolveThemeColor(colors, fill)} + fillSvg={resolveThemeColor(colors, fillSvg)} + {...iconSize} + css={componentStyles} + {...restNotStyledProps} + /> + ); + } + + const resolvedFill = resolveThemeColor(colors, fill) || 'currentColor'; + const resolvedFillSvg = resolveThemeColor(colors, fillSvg) || 'none'; return ( - } data-testid={`${COMPONENT_NAME}-${name}`} - fill={resolveThemeColor(colors, fill)} - fillSvg={resolveThemeColor(colors, fillSvg)} + viewBox={definition.viewBox} + fill={resolvedFillSvg} + xmlns="http://www.w3.org/2000/svg" + style={{ '--gd-icon-fill': resolvedFill, '--gd-icon-fill-svg': resolvedFillSvg } as CSSProperties} {...iconSize} css={componentStyles} {...restNotStyledProps} + dangerouslySetInnerHTML={{ __html: definition.body }} /> ); } diff --git a/libs/ui/src/components/atoms/Icon/__snapshots__/Icon.test.tsx.snap b/libs/ui/src/components/atoms/Icon/__snapshots__/Icon.test.tsx.snap index 972262a..29b13b4 100644 --- a/libs/ui/src/components/atoms/Icon/__snapshots__/Icon.test.tsx.snap +++ b/libs/ui/src/components/atoms/Icon/__snapshots__/Icon.test.tsx.snap @@ -7,13 +7,14 @@ exports[`Icon Component > SHOULD render correctly when a valid icon name is prov data-testid="Icon-cross" fill="none" height="24" + style="--gd-icon-fill: #000; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -26,13 +27,14 @@ exports[`Icon Component > SHOULD render with specified color and fill 1`] = ` data-testid="Icon-cross" fill="#2ecc71" height="32" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: #2ecc71;" viewBox="0 0 24 24" width="32" xmlns="http://www.w3.org/2000/svg" > diff --git a/libs/ui/src/components/layout/ChatContainer/__snapshots__/ChatContainer.test.tsx.snap b/libs/ui/src/components/layout/ChatContainer/__snapshots__/ChatContainer.test.tsx.snap index 6de555c..f4b18cd 100644 --- a/libs/ui/src/components/layout/ChatContainer/__snapshots__/ChatContainer.test.tsx.snap +++ b/libs/ui/src/components/layout/ChatContainer/__snapshots__/ChatContainer.test.tsx.snap @@ -36,13 +36,14 @@ exports[`ChatContainer > SHOULD match snapshot 1`] = ` data-testid="Icon-arrowRight" fill="none" height="24" + style="--gd-icon-fill: #000000; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -89,13 +90,14 @@ exports[`ChatContainer > SHOULD match snapshot 1`] = ` data-testid="Icon-arrowRight" fill="none" height="24" + style="--gd-icon-fill: #000000; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > diff --git a/libs/ui/src/components/molecules/AttachmentFile/__snapshots__/AttachmentFile.test.tsx.snap b/libs/ui/src/components/molecules/AttachmentFile/__snapshots__/AttachmentFile.test.tsx.snap index 149819a..e5b3725 100644 --- a/libs/ui/src/components/molecules/AttachmentFile/__snapshots__/AttachmentFile.test.tsx.snap +++ b/libs/ui/src/components/molecules/AttachmentFile/__snapshots__/AttachmentFile.test.tsx.snap @@ -11,13 +11,14 @@ exports[`AttachmentFile > SHOULD match snapshot with all props 1`] = ` data-testid="Icon-fileCopy" fill="#474747" height="32" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: #474747;" viewBox="0 0 24 24" width="32" xmlns="http://www.w3.org/2000/svg" >
SHOULD match snapshot with all props 1`] = ` data-testid="Icon-cross" fill="none" height="18" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg" > @@ -98,13 +100,14 @@ exports[`AttachmentFile > SHOULD match snapshot without meta 1`] = ` data-testid="Icon-fileCopy" fill="#474747" height="32" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: #474747;" viewBox="0 0 24 24" width="32" xmlns="http://www.w3.org/2000/svg" >
SHOULD match snapshot without meta 1`] = ` data-testid="Icon-cross" fill="none" height="18" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg" > @@ -165,13 +169,14 @@ exports[`AttachmentFile > SHOULD match snapshot without remove button 1`] = ` data-testid="Icon-fileCopy" fill="#474747" height="32" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: #474747;" viewBox="0 0 24 24" width="32" xmlns="http://www.w3.org/2000/svg" >
isLoading > SHOULD match snapshot when isLoading 1`] = data-testid="Icon-fileCopy" fill="#474747" height="32" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: #474747;" viewBox="0 0 24 24" width="32" xmlns="http://www.w3.org/2000/svg" >
SHOULD match snapshot 1`] = ` data-testid="Icon-minus" fill="none" height="14" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="14" xmlns="http://www.w3.org/2000/svg" > @@ -80,13 +81,14 @@ exports[`Counter > SHOULD match snapshot 1`] = ` data-testid="Icon-plus" fill="none" height="14" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="14" xmlns="http://www.w3.org/2000/svg" > diff --git a/libs/ui/src/components/molecules/InlineNotification/__snapshots__/InlineNotification.test.tsx.snap b/libs/ui/src/components/molecules/InlineNotification/__snapshots__/InlineNotification.test.tsx.snap index fbb07ab..0834cd8 100644 --- a/libs/ui/src/components/molecules/InlineNotification/__snapshots__/InlineNotification.test.tsx.snap +++ b/libs/ui/src/components/molecules/InlineNotification/__snapshots__/InlineNotification.test.tsx.snap @@ -31,13 +31,14 @@ exports[`InlineNotification > SHOULD match error snapshot 1`] = ` data-testid="Icon-error" fill="none" height="18" + style="--gd-icon-fill: #D21C1C; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg" > SHOULD match error snapshot 1`] = ` /> SHOULD match info snapshot 1`] = ` data-testid="Icon-info" fill="none" height="18" + style="--gd-icon-fill: #0069B4; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg" > SHOULD match success snapshot 1`] = ` data-testid="Icon-success" fill="none" height="18" + style="--gd-icon-fill: #34A853; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg" > SHOULD match success snapshot 1`] = ` /> SHOULD match warning snapshot 1`] = ` data-testid="Icon-warning" fill="none" height="18" + style="--gd-icon-fill: #FF8700; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg" > SHOULD match snapshot 1`] = ` data-testid="Icon-star" fill="none" height="24" + style="--gd-icon-fill: #FFB800; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -52,13 +53,14 @@ exports[`Rating > SHOULD match snapshot 1`] = ` data-testid="Icon-star" fill="none" height="24" + style="--gd-icon-fill: #FFB800; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -81,13 +83,14 @@ exports[`Rating > SHOULD match snapshot 1`] = ` data-testid="Icon-star" fill="none" height="24" + style="--gd-icon-fill: #FFB800; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -109,13 +112,14 @@ exports[`Rating > SHOULD match snapshot 1`] = ` data-testid="Icon-starOutlined" fill="none" height="24" + style="--gd-icon-fill: #E5E5E5; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -137,13 +141,14 @@ exports[`Rating > SHOULD match snapshot 1`] = ` data-testid="Icon-starOutlined" fill="none" height="24" + style="--gd-icon-fill: #E5E5E5; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > diff --git a/libs/ui/src/components/molecules/Snackbar/__snapshots__/Snackbar.test.tsx.snap b/libs/ui/src/components/molecules/Snackbar/__snapshots__/Snackbar.test.tsx.snap index 958e92a..0421329 100644 --- a/libs/ui/src/components/molecules/Snackbar/__snapshots__/Snackbar.test.tsx.snap +++ b/libs/ui/src/components/molecules/Snackbar/__snapshots__/Snackbar.test.tsx.snap @@ -19,13 +19,14 @@ exports[`Snackbar > SHOULD render the snackbar with correct message and title 1` data-testid="Icon-cross" fill="none" height="10" + style="--gd-icon-fill: #0069B4; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="10" xmlns="http://www.w3.org/2000/svg" >
@@ -37,13 +38,14 @@ exports[`Snackbar > SHOULD render the snackbar with correct message and title 1` data-testid="Icon-info" fill="none" height="20" + style="--gd-icon-fill: #0069B4; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="20" xmlns="http://www.w3.org/2000/svg" >
SHOULD match snapshot 1`] = ` data-testid="Icon-arrowLeft" fill="none" height="16" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg" > @@ -147,13 +148,14 @@ exports[`TablePagination > SHOULD match snapshot 1`] = ` data-testid="Icon-arrowRight" fill="none" height="16" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg" > diff --git a/libs/ui/src/components/organisms/Card/CardCounter/__snapshots__/CardCounter.test.tsx.snap b/libs/ui/src/components/organisms/Card/CardCounter/__snapshots__/CardCounter.test.tsx.snap index 5946cbf..34bedc0 100644 --- a/libs/ui/src/components/organisms/Card/CardCounter/__snapshots__/CardCounter.test.tsx.snap +++ b/libs/ui/src/components/organisms/Card/CardCounter/__snapshots__/CardCounter.test.tsx.snap @@ -28,13 +28,14 @@ exports[`CardCounter > SHOULD match snapshot 1`] = ` data-testid="Icon-minus" fill="none" height="14" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="14" xmlns="http://www.w3.org/2000/svg" > @@ -83,13 +84,14 @@ exports[`CardCounter > SHOULD match snapshot 1`] = ` data-testid="Icon-plus" fill="none" height="14" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="14" xmlns="http://www.w3.org/2000/svg" > diff --git a/libs/ui/src/components/organisms/Card/CardRating/__snapshots__/CardRating.test.tsx.snap b/libs/ui/src/components/organisms/Card/CardRating/__snapshots__/CardRating.test.tsx.snap index 6e17858..24cfe0f 100644 --- a/libs/ui/src/components/organisms/Card/CardRating/__snapshots__/CardRating.test.tsx.snap +++ b/libs/ui/src/components/organisms/Card/CardRating/__snapshots__/CardRating.test.tsx.snap @@ -19,13 +19,14 @@ exports[`CardRating > SHOULD match snapshot 1`] = ` data-testid="Icon-star" fill="none" height="12" + style="--gd-icon-fill: #FFB800; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg" > SHOULD match snapshot 1`] = ` data-testid="Icon-star" fill="none" height="12" + style="--gd-icon-fill: #FFB800; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg" > SHOULD match snapshot 1`] = ` data-testid="Icon-star" fill="none" height="12" + style="--gd-icon-fill: #FFB800; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg" > SHOULD match snapshot 1`] = ` data-testid="Icon-star" fill="none" height="12" + style="--gd-icon-fill: #FFB800; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg" > SHOULD match snapshot 1`] = ` data-testid="Icon-star" fill="none" height="12" + style="--gd-icon-fill: #FFB800; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg" >
@@ -104,13 +109,14 @@ exports[`CardRating > SHOULD match snapshot 1`] = ` data-testid="Icon-starOutlined" fill="none" height="12" + style="--gd-icon-fill: #E5E5E5; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg" > @@ -133,13 +139,14 @@ exports[`CardRating > SHOULD match snapshot 1`] = ` data-testid="Icon-starOutlined" fill="none" height="12" + style="--gd-icon-fill: #E5E5E5; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg" > @@ -162,13 +169,14 @@ exports[`CardRating > SHOULD match snapshot 1`] = ` data-testid="Icon-starOutlined" fill="none" height="12" + style="--gd-icon-fill: #E5E5E5; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg" > @@ -191,13 +199,14 @@ exports[`CardRating > SHOULD match snapshot 1`] = ` data-testid="Icon-starOutlined" fill="none" height="12" + style="--gd-icon-fill: #E5E5E5; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg" > @@ -220,13 +229,14 @@ exports[`CardRating > SHOULD match snapshot 1`] = ` data-testid="Icon-starOutlined" fill="none" height="12" + style="--gd-icon-fill: #E5E5E5; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg" > diff --git a/libs/ui/src/components/organisms/Carousel/__snapshots__/Carousel.test.tsx.snap b/libs/ui/src/components/organisms/Carousel/__snapshots__/Carousel.test.tsx.snap index 59656e6..c8005cf 100644 --- a/libs/ui/src/components/organisms/Carousel/__snapshots__/Carousel.test.tsx.snap +++ b/libs/ui/src/components/organisms/Carousel/__snapshots__/Carousel.test.tsx.snap @@ -32,13 +32,14 @@ exports[`Carousel > SHOULD match snapshot 1`] = ` data-testid="Icon-arrowLeft" fill="none" height="24" + style="--gd-icon-fill: #8F8F8F; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -59,13 +60,14 @@ exports[`Carousel > SHOULD match snapshot 1`] = ` data-testid="Icon-arrowLeft" fill="none" height="24" + style="--gd-icon-fill: #474747; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -133,13 +135,14 @@ exports[`Carousel > SHOULD match snapshot 1`] = ` data-testid="Icon-arrowLeft" fill="none" height="24" + style="--gd-icon-fill: #8F8F8F; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > @@ -202,13 +205,14 @@ exports[`Carousel > SHOULD match snapshot 1`] = ` data-testid="Icon-arrowRight" fill="none" height="24" + style="--gd-icon-fill: #474747; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > diff --git a/libs/ui/src/components/organisms/Header/__snapshots__/Header.test.tsx.snap b/libs/ui/src/components/organisms/Header/__snapshots__/Header.test.tsx.snap index 8291d73..29ebc05 100644 --- a/libs/ui/src/components/organisms/Header/__snapshots__/Header.test.tsx.snap +++ b/libs/ui/src/components/organisms/Header/__snapshots__/Header.test.tsx.snap @@ -26,13 +26,14 @@ exports[`Header > SHOULD match snapshot 1`] = ` data-testid="Icon-mobileMenu" fill="none" height="40" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="40" xmlns="http://www.w3.org/2000/svg" >
diff --git a/libs/ui/src/components/organisms/Modal/__snapshots__/Modal.test.tsx.snap b/libs/ui/src/components/organisms/Modal/__snapshots__/Modal.test.tsx.snap index 3eefe39..a97e852 100644 --- a/libs/ui/src/components/organisms/Modal/__snapshots__/Modal.test.tsx.snap +++ b/libs/ui/src/components/organisms/Modal/__snapshots__/Modal.test.tsx.snap @@ -30,13 +30,14 @@ exports[`Modal > SHOULD match snapshot & render correctly when isOpen is true 1` data-testid="Icon-cross" fill="none" height="24" + style="--gd-icon-fill: #000000; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" > diff --git a/libs/ui/src/components/organisms/SearchModal/SearchInput/__snapshots__/SearchInput.test.tsx.snap b/libs/ui/src/components/organisms/SearchModal/SearchInput/__snapshots__/SearchInput.test.tsx.snap index 053c939..f807703 100644 --- a/libs/ui/src/components/organisms/SearchModal/SearchInput/__snapshots__/SearchInput.test.tsx.snap +++ b/libs/ui/src/components/organisms/SearchModal/SearchInput/__snapshots__/SearchInput.test.tsx.snap @@ -36,13 +36,14 @@ exports[`SearchInput > SHOULD match snapshot 1`] = ` data-testid="Icon-cross" fill="none" height="18" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg" >
diff --git a/libs/ui/src/components/organisms/SearchModal/SearchItems/__snapshots__/SearchItems.test.tsx.snap b/libs/ui/src/components/organisms/SearchModal/SearchItems/__snapshots__/SearchItems.test.tsx.snap index 413a775..241f4ca 100644 --- a/libs/ui/src/components/organisms/SearchModal/SearchItems/__snapshots__/SearchItems.test.tsx.snap +++ b/libs/ui/src/components/organisms/SearchModal/SearchItems/__snapshots__/SearchItems.test.tsx.snap @@ -15,13 +15,14 @@ exports[`SearchItems > SHOULD match snapshot 1`] = ` data-testid="Icon-search" fill="none" height="16" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg" >
diff --git a/libs/ui/src/components/widget/DragAndDrop/__snapshots__/DragAndDrop.test.tsx.snap b/libs/ui/src/components/widget/DragAndDrop/__snapshots__/DragAndDrop.test.tsx.snap index 67834ff..647b34e 100644 --- a/libs/ui/src/components/widget/DragAndDrop/__snapshots__/DragAndDrop.test.tsx.snap +++ b/libs/ui/src/components/widget/DragAndDrop/__snapshots__/DragAndDrop.test.tsx.snap @@ -23,13 +23,14 @@ exports[`DragAndDrop > SHOULD match snapshot 1`] = ` data-testid="Icon-upload" fill="none" height="24" + style="--gd-icon-fill: currentColor; --gd-icon-fill-svg: none;" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" >
+ storySource(context.canvasElement, source), + }, + }, }, }; diff --git a/libs/web-components/.storybook/vite.config.ts b/libs/web-components/.storybook/vite.config.ts index c049f46..c113a29 100644 --- a/libs/web-components/.storybook/vite.config.ts +++ b/libs/web-components/.storybook/vite.config.ts @@ -1,3 +1,11 @@ import { defineConfig } from 'vite'; +import { fileURLToPath } from 'node:url'; -export default defineConfig({}); +export default defineConfig({ + resolve: { + alias: { + 'gd-design-core': fileURLToPath(new URL('../../design-core/src/index.ts', import.meta.url)), + 'gd-design-library/tokens': fileURLToPath(new URL('../../../dist/libs/ui/tokens/index.js', import.meta.url)), + }, + }, +}); diff --git a/libs/web-components/MIGRATION-MANIFEST.loader-separator-skeleton.md b/libs/web-components/MIGRATION-MANIFEST.loader-separator-skeleton.md new file mode 100644 index 0000000..cd576d5 --- /dev/null +++ b/libs/web-components/MIGRATION-MANIFEST.loader-separator-skeleton.md @@ -0,0 +1,11 @@ +# Loader, Separator, and Skeleton migration manifest + +`react-component-ast` and `web-component-generator` were unavailable for this run. This reviewed manual manifest follows the self-migration-agent fallback and was derived from each React component's source, public types, tokens, tests, exports, local stories, and published Storybook inventory. + +| React component | Classification | Custom element | Attributes / properties | Slots | Parts | Events / methods | Accessibility and compatibility | +| --------------- | -------------- | -------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Loader | element | `gd-loader` | `name`, `variant`, `size`, `rounded`, `animationProps`, `withWrapper`, `styles`, `theme` | default custom content | `wrapper`, `loader`, `dot` | none | Inner loader exposes `role=status` and an accessible loading name. Full-page uses the native popover top layer. React `as` becomes the stable custom-element host plus `withWrapper`. | +| Separator | element | `gd-separator` | `orientation`, `length`, `color`, `size`, `variant`, `as`, `label`, `labelPosition`, `labelColor`, `styles`, `theme` | none; the React API accepts a string label | `separator`, `line`, `label` | none | `as` selects the internal `div`, `hr`, or `span`. An `hr` with a label uses a `div` wrapper and `hr` line segments, matching React. React box-spacing props remain available through `styles` or host CSS. | +| Skeleton | element | `gd-skeleton` | `variant`, `width`, `height`, `backgroundColor`, `animationName`, `animationProps`, `styles`, `theme` | default child content | `skeleton` | none | The native host accepts ARIA attributes. `animationName = null` disables the built-in animation. React `className` maps to the custom-element `class` attribute; children map to the default slot. | + +All three elements resolve their source token trees. They define no component-owned `static styles`; runtime stylesheets are generated from token blocks and documented public style properties. diff --git a/libs/web-components/MIGRATION-MANIFEST.slider-controls.md b/libs/web-components/MIGRATION-MANIFEST.slider-controls.md new file mode 100644 index 0000000..29f9ae8 --- /dev/null +++ b/libs/web-components/MIGRATION-MANIFEST.slider-controls.md @@ -0,0 +1,13 @@ +# Slider controls migration manifest + +The generators were unavailable. This reviewed manual manifest uses the complete React source, public types, tokens, tests, exports, and published Storybook evidence allowed by the self-migration-agent fallback. + +| Component | Classification | Platform contract | +| ---------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Skeleton | element, re-audited | `gd-skeleton`; scalar shape/dimension/animation attributes, object `styles`/`theme` properties, default slot, `skeleton` part, native host ARIA. | +| Slider | element | `gd-slider`; numeric `min`, `max`, `value`, `step`; boolean `disabled`; `styles`/`theme`; `slider` part; bubbling/composed `gd-change` with `{ value }`; native range keyboard and ARIA behavior. | +| SliderDots | element | `gd-slider-dots`; numeric `count` and `activeIndex`; `styles`/`theme`; `dots` and `dot` parts; bubbling/composed `gd-change` with `{ index }`; tablist/tab roles and accessible names. | +| Switch | element | `gd-switch`; `checked`, `disabled`, `isLoading`, `label`, `name`; default label slot; exposed parts; bubbling/composed `gd-change` with `{ checked }`; native checkbox keyboard behavior; composes `gd-loader`. | +| Textarea | element | `gd-textarea`; native value, naming, placeholder, disabled, readonly, rows and length properties plus resize, variant, color, dynamic height, sizing, counter and style properties; exposed parts; `gd-input`/`gd-change` with `{ value }`; public `focus()` method. | + +React callbacks map to native custom events. React children map to the Switch default slot. React synthetic change events map to value-bearing DOM custom events. No React-only runtime is embedded. diff --git a/libs/web-components/MIGRATION-MANIFEST.toggle-truncate-typography-wrapper.md b/libs/web-components/MIGRATION-MANIFEST.toggle-truncate-typography-wrapper.md new file mode 100644 index 0000000..9bb4cd8 --- /dev/null +++ b/libs/web-components/MIGRATION-MANIFEST.toggle-truncate-typography-wrapper.md @@ -0,0 +1,12 @@ +# Toggle, Truncate, Typography, and Wrapper migration manifest + +The generators were unavailable. This reviewed manual manifest uses the complete React source, public types, tokens, tests, exports, and the published React Storybook evidence allowed by the self-migration-agent fallback. + +| Component | Classification | Platform contract | +| ---------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Toggle | element | `gd-toggle`; string/object `items`, controlled `value`, `disabled`, `renderItemContent`, `styles` and `theme`; composes `gd-button`; bubbling/composed `gd-change` with `{ value }`; group semantics. | +| Truncate | element | `gd-truncate`; numeric `lines`, `styles` and `theme`; default slot; `content` part; reactive overflow measurement through `isTruncated` and public `measure()`; ResizeObserver support. | +| Typography | element, re-audited | `gd-typography`; all React variants, display sizes, style variants, alignment, color, semantic `as`, token typography selection, default slot, and `typography` part remain covered. | +| Wrapper | element | `gd-wrapper`; `inline`, `section`, and `fullPage` variants, semantic `as`, `styles` and `theme`; default slot and `wrapper` part. The explicit migration request overrides the earlier CSS-only classification. | + +React callbacks map to native custom events, React children map to default slots, and function/object props remain JavaScript properties. The published Atoms inventory is recorded in `published-atoms-audit.json` and enforced against the local atom ports and parity story counts. diff --git a/libs/web-components/README.md b/libs/web-components/README.md index 3cf68d0..45a15ea 100644 --- a/libs/web-components/README.md +++ b/libs/web-components/README.md @@ -2,7 +2,7 @@ GridKit Web Components provides design-system elements that work in any web application. They are native custom elements built with Lit, so they can be used in plain HTML, Angular, Vue, React, or another framework. -Available elements: `gd-button`, `gd-checkbox`, `gd-input`, `gd-select`, `gd-typography`, `gd-avatar`, `gd-menu`, and `gd-counter`. +Available elements: `gd-avatar`, `gd-badge`, `gd-box`, `gd-button`, `gd-checkbox`, `gd-icon`, `gd-image`, `gd-input`, `gd-input-file`, `gd-label`, `gd-link`, `gd-loader`, `gd-select`, `gd-separator`, `gd-skeleton`, `gd-slider`, `gd-slider-dots`, `gd-switch`, `gd-textarea`, `gd-toggle`, `gd-truncate`, `gd-typography`, `gd-wrapper`, `gd-counter`, and `gd-menu`. ## Install @@ -99,10 +99,54 @@ const accepted = ref(false); ## Run locally The shared Storybook is started from the repository root with `npm run storybook` and -opened at `http://localhost:6006`. Its **Web Components** section currently contains the -Phase 1 overview; native stories for the eight existing elements follow in Phase 2. -There is no separate Web Components Storybook command. The existing Angular/Vue harness -workflow below remains available until the later integration-check cleanup. +opened at `http://localhost:6006`. Its **Web Components** section contains native stories, Controls, and docs for all twenty-five +existing elements. Interactive stories show native event payloads below the component. + +| Storybook component | React-contract coverage | +| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| [Avatar](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-avatar--docs) | Image, fallback, badge, every size, custom colors | +| [Badge](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-badge--docs) | Variants, appearances, sizes, disabled state, and icon slots | +| [Box](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-box--docs) | Vertical/horizontal layout, borders, highlight, shadow hover, and slotted content | +| [Button](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-button--docs) | Every variant and radius, full-width, icon-only, loading, disabled, icon slots | +| [Checkbox](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-checkbox--docs) | Checked, indeterminate, disabled, both sizes; `gd-change` → `{ checked }` | +| [Input](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-input--docs) | Types, validation colors, read-only/disabled, adornments; `gd-input`/`gd-change` | +| [Image](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-image--docs) | Loading placeholder, fallback slot, caption, sizing, object fit, and load/error events | +| [Icon](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-icon--docs) | All built-in icons from the shared core catalog, token sizes, and theme-aware fills | +| [InputFile](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-inputfile--docs) | Accept/capture/multiple/disabled behavior, custom label, icon label, and `gd-change` file detail | +| [Label](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-label--docs) | Native label association, slotted content, icons, and style overrides | +| [Link component](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-link--docs) | Variants, sizes, underline states, disabled semantics, targets, and href | +| [Loader](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-loader--docs) | Circle/dots, sizes, rounded dots, wrappers, section, and native full-page top layer | +| [Select](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-select--docs) | Single/multiple, search, auto-open, colors, adornments, custom initiator; `gd-change` | +| [Separator](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-separator--docs) | Orientations, line variants, thickness, labels, semantic elements, lengths, and colors | +| [Skeleton](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-skeleton--docs) | Rounded/rectangular/circular shapes, dimensions, colors, animation control, and child content | +| [Slider](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-slider--docs) | Range limits, controlled values, keyboard input, disabled state, visual fill, and events | +| [SliderDots](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-sliderdots--docs) | Accessible carousel tabs, active state, larger sets, and selection events | +| [Switch](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-switch--docs) | Checked, disabled, loading, controlled/uncontrolled behavior, label placement, and events | +| [Textarea](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-textarea--docs) | Values, colors, resize modes, dynamic height, focus, character limits, and input/change events | +| [Toggle](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-toggle--docs) | String/object items, selected value, disabled state, custom item rendering, and change events | +| [Truncate](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-truncate--docs) | Single/multiple-line truncation, style overrides, overflow measurement, and accessibility | +| [Typography](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-typography--docs) | All 18 variants, display sizes, style variants, alignment/color controls, semantic override | +| [Wrapper](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_atoms-wrapper--docs) | Inline, section and full-page layout variants, semantic tag override, content, and styles | +| [Counter](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_molecules-counter--docs) | Minimum/maximum, custom range, disabled; `gd-change` → `{ value: number }` | +| [Menu](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/web-components_molecules-menu--docs) | Placement, offsets, height constraints, close/persist behavior; `gd-change` → `{ data, value }` | + +### React-to-custom-element contract + +The ports preserve each source component's component-specific public contract using native +custom-element equivalents. Primitive props remain attributes/properties; objects, arrays, +functions, themes, and style objects are JavaScript properties. React callbacks become native +DOM/custom events, `ReactNode` props become default or named slots, and imperative refs become +public element methods. Generic React/Emotion box props are supplied with host CSS or the +component's `styles` property rather than reproduced as React-only prop names. + +`react-parity.json` records the required mapping and Storybook examples for every shipped port. +`npm run check:web-components-ports` fails when an element, required mapping, or parity story is +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. + +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. Start the development server for all Web Components examples: diff --git a/libs/web-components/harness/fidelity-check-vue.ts b/libs/web-components/harness/fidelity-check-vue.ts index 2bb9a52..7d7678f 100644 --- a/libs/web-components/harness/fidelity-check-vue.ts +++ b/libs/web-components/harness/fidelity-check-vue.ts @@ -101,6 +101,141 @@ const App = { h('gd-counter', { min: 1, max: 5, initial: 1, theme: defaultTheme }), h('output', `Value: ${counterValue.value}`), ]), + section('Box — vertical / horizontal / bordered / highlighted', [ + h('gd-box', { variant: 'vertical', theme: defaultTheme }, 'Vertical box'), + h('gd-box', { variant: 'horizontal', isBordered: true, theme: defaultTheme }, 'Bordered horizontal box'), + h( + 'gd-box', + { isBordered: true, isHighlighted: true, withShadowHover: true, theme: defaultTheme }, + 'Interactive box' + ), + ]), + section('Badge — variants / sizes / icons / disabled', [ + h('gd-badge', { variant: 'primary', appearance: 'filled', size: 'xs', theme: defaultTheme }, 'Primary'), + h('gd-badge', { variant: 'secondary', appearance: 'outline', size: 'md', theme: defaultTheme }, [ + h('span', { slot: 'icon-start' }, '★'), + 'With icon', + ]), + h( + 'gd-badge', + { variant: 'tertiary', appearance: 'filledLight', size: 'lg', disabled: true, theme: defaultTheme }, + 'Disabled' + ), + ]), + section('Image — image / caption / fallback', [ + h('gd-image', { + src: portrait, + alt: 'Ada Lovelace', + width: 96, + height: 96, + caption: 'Portrait', + theme: defaultTheme, + }), + h( + 'gd-image', + { src: '/missing-image.png', alt: 'Unavailable image', width: 96, height: 96, theme: defaultTheme }, + [h('span', { slot: 'fallback' }, 'Image unavailable')] + ), + ]), + section('Icon — shared React/Web Components catalog', [ + h('gd-icon', { name: 'star', size: 'xs', theme: defaultTheme }), + h('gd-icon', { name: 'search', size: 'md', theme: defaultTheme }), + h('gd-icon', { name: 'edit', size: 'xl', 'aria-label': 'Edit', theme: defaultTheme }), + ]), + section('InputFile — default / multiple / accept / icon', [ + h('gd-input-file', { theme: defaultTheme }, 'Browse Files'), + h('gd-input-file', { multiple: true, accept: 'image/*', theme: defaultTheme }, 'Choose images'), + h('gd-input-file', { isIcon: true, 'aria-label': 'Upload file', theme: defaultTheme }, [ + h('gd-icon', { name: 'upload' }), + ]), + h('gd-input-file', { disabled: true, theme: defaultTheme }, 'Disabled'), + ]), + section('Label — text / icon / association', [ + h('gd-label', { for: 'vue-labelled-input', theme: defaultTheme }, 'Account name'), + h('input', { id: 'vue-labelled-input' }), + h('gd-label', { theme: defaultTheme }, [h('gd-icon', { name: 'star' }), ' Required label']), + ]), + section('Link — variants / underline / size / disabled', [ + h('gd-link', { href: '#vue-link', variant: 'primary', size: 'sm', theme: defaultTheme }, 'Primary'), + h( + 'gd-link', + { href: '#vue-link', variant: 'inherit', underline: 'highlight', size: 'md', theme: defaultTheme }, + 'Highlighted' + ), + h('gd-link', { variant: 'inverted', size: 'lg', disabled: true, theme: defaultTheme }, 'Disabled'), + ]), + section('Loader — circle / dots / sizes / wrapper variants', [ + h('gd-loader', { name: 'circle', size: 'xs', theme: defaultTheme }), + h('gd-loader', { name: 'dots', size: 'md', rounded: 'round', theme: defaultTheme }), + h('gd-loader', { name: 'circle', size: 'lg', variant: 'section', theme: defaultTheme }), + h('gd-loader', { name: 'circle', size: 'sm', withWrapper: false, theme: defaultTheme }), + ]), + section('Separator — horizontal / vertical / labels / variants', [ + h('gd-separator', { length: '180px', theme: defaultTheme }), + h('gd-separator', { + length: '180px', + label: 'OR', + labelPosition: 'center', + size: 'md', + variant: 'dashed', + theme: defaultTheme, + }), + h('gd-separator', { length: '80px', orientation: 'vertical', label: 'Or', size: 'md', theme: defaultTheme }), + ]), + section('Skeleton — rounded / circular / rectangular / children', [ + h('gd-skeleton', { width: '180px', height: '15px', theme: defaultTheme }), + h('gd-skeleton', { width: '60px', height: '60px', variant: 'circular', theme: defaultTheme }), + h( + 'gd-skeleton', + { + width: '180px', + height: '50px', + variant: 'rectangular', + backgroundColor: 'theme.palette.success.main', + theme: defaultTheme, + }, + 'Loading Content...' + ), + ]), + section('Slider / dots — range and carousel navigation', [ + h('gd-slider', { min: 0, max: 100, value: 45, 'aria-label': 'Volume', theme: defaultTheme }), + h('gd-slider-dots', { count: 5, activeIndex: 1, theme: defaultTheme }), + ]), + section('Switch — default / checked / loading', [ + h('gd-switch', { theme: defaultTheme }, 'Notifications'), + h('gd-switch', { checked: true, label: 'left', theme: defaultTheme }, 'Enabled'), + h('gd-switch', { isLoading: true, theme: defaultTheme }, 'Saving'), + ]), + section('Textarea — colors / counter / resize', [ + h('gd-textarea', { placeholder: 'Comment', theme: defaultTheme }), + h('gd-textarea', { color: 'success', maxCharacters: 100, defaultValue: 'Looks good', theme: defaultTheme }), + h('gd-textarea', { resize: 'both', rows: 3, theme: defaultTheme }), + ]), + section('Toggle — selected / disabled', [ + h('gd-toggle', { items: ['Option 1', 'Option 2', 'Option 3'], value: 'Option 1', theme: defaultTheme }), + h('gd-toggle', { + items: ['Option 1', 'Option 2', 'Option 3'], + value: 'Option 2', + disabled: true, + theme: defaultTheme, + }), + ]), + section('Truncate — single / multiple lines', [ + h( + 'gd-truncate', + { style: 'width: 180px', theme: defaultTheme }, + 'A long single-line value that must be truncated.' + ), + h( + 'gd-truncate', + { style: 'width: 180px', lines: 2, theme: defaultTheme }, + 'A longer block of content constrained to two lines for the Vue integration harness.' + ), + ]), + section('Wrapper — inline / section', [ + h('gd-wrapper', { variant: 'inline', theme: defaultTheme }, 'Inline content'), + h('gd-wrapper', { variant: 'section', as: 'section', theme: defaultTheme }, 'Section content'), + ]), ]); }, }; diff --git a/libs/web-components/harness/fidelity-check.ts b/libs/web-components/harness/fidelity-check.ts index 0377694..ce56013 100644 --- a/libs/web-components/harness/fidelity-check.ts +++ b/libs/web-components/harness/fidelity-check.ts @@ -79,6 +79,90 @@ const componentMetadata = { Value: {{ counterValue }} + +

Box — vertical / horizontal / bordered / highlighted

+ Vertical box + Bordered horizontal box + Interactive box +
+ +

Badge — variants / sizes / icons / disabled

+ Primary + ★With icon + Disabled +
+ +

Image — image / caption / fallback

+ + Image unavailable +
+ +

Icon — shared React/Web Components catalog

+ + + +
+ +

InputFile — default / multiple / accept / icon

+ Browse Files + Choose images + + Disabled +
+ +

Label — text / icon / association

+ Account name + + Required label +
+ +

Link — variants / underline / size / disabled

+ Primary + Highlighted + Disabled +
+ +

Loader — circle / dots / sizes / wrapper variants

+ + + + +
+ +

Separator — horizontal / vertical / labels / variants

+ + + +
+ +

Skeleton — rounded / circular / rectangular / children

+ + + Loading Content... +
+ +

Slider / dots — range and carousel navigation

+ + +
+

Switch — default / checked / loading

+ NotificationsEnabledSaving +
+

Textarea — colors / counter / resize

+ +
+

Toggle — selected / disabled

+ + +
+

Truncate — single / multiple lines

+ A long single-line value that must be truncated. + A longer block of content constrained to two lines for the Angular integration harness. +
+

Wrapper — inline / section / full page

+ Inline content + Section content +
`, }; @@ -89,6 +173,7 @@ class FidelityHarnessComponent { { name: 'Beta', value: 'b' }, { name: 'Gamma', value: 'c' }, ]; + readonly toggleItems = ['Option 1', 'Option 2', 'Option 3']; readonly 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'; inputValue = 'Default Input'; diff --git a/libs/web-components/published-atoms-audit.json b/libs/web-components/published-atoms-audit.json new file mode 100644 index 0000000..598b2ab --- /dev/null +++ b/libs/web-components/published-atoms-audit.json @@ -0,0 +1,29 @@ +{ + "source": "https://storybook.cto-rnd-system-design.griddynamics.net/index.json", + "auditedAt": "2026-10-01", + "components": { + "gd-avatar": 10, + "gd-badge": 8, + "gd-box": 10, + "gd-button": 13, + "gd-checkbox": 6, + "gd-icon": 6, + "gd-image": 9, + "gd-input": 17, + "gd-input-file": 9, + "gd-label": 6, + "gd-link": 12, + "gd-loader": 11, + "gd-select": 16, + "gd-separator": 8, + "gd-skeleton": 9, + "gd-slider": 7, + "gd-slider-dots": 4, + "gd-switch": 9, + "gd-textarea": 16, + "gd-toggle": 5, + "gd-truncate": 5, + "gd-typography": 10, + "gd-wrapper": 7 + } +} diff --git a/libs/web-components/react-parity.json b/libs/web-components/react-parity.json new file mode 100644 index 0000000..0ca7a12 --- /dev/null +++ b/libs/web-components/react-parity.json @@ -0,0 +1,526 @@ +{ + "gd-toggle": { + "react": "Toggle", + "source": ["items", "value", "disabled", "renderItemContent", "styles", "gd-change", "gd-button", "role=\"group\""], + "stories": ["Default", "Disabled", "WithCustomRender", "WithAccessibility", "DefaultTokens"] + }, + "gd-truncate": { + "react": "Truncate", + "source": ["lines", "styles", "slot", "isTruncated", "measure()", "part=\"content\""], + "stories": ["Default", "LineTruncation", "WithCustomStyling", "RefAPIWithTooltipOnOverflow", "WithAccessibility"] + }, + "gd-wrapper": { + "react": "Wrapper", + "source": ["variant", "as", "styles", "slot", "part=\"wrapper\""], + "stories": [ + "InteractiveExample", + "SectionWrapper", + "InlineWrapper", + "FullPageWrapper", + "CustomTagWrapper", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-slider": { + "react": "Slider", + "source": ["min", "max", "value", "step", "disabled", "styles", "gd-change", "part=\"slider\""], + "stories": [ + "Default", + "WithInitialValue", + "Disabled", + "CustomStyles", + "Controlled", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-slider-dots": { + "react": "SliderDots", + "source": ["count", "activeIndex", "styles", "gd-change", "role=\"tablist\"", "role=\"tab\""], + "stories": ["Default", "Interactive", "ManyDots", "DefaultTokens"] + }, + "gd-switch": { + "react": "Switch", + "source": ["checked", "disabled", "isLoading", "label", "name", "styles", "gd-change", "slot", "gd-loader"], + "stories": [ + "Default", + "Checked", + "Disabled", + "LabelLeft", + "Controlled", + "Uncontrolled", + "WithLoading", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-textarea": { + "react": "Textarea", + "source": [ + "value", + "defaultValue", + "name", + "placeholder", + "resize", + "variant", + "color", + "ariaDescribedBy", + "dynamicHeightAdjustment", + "autoFocus", + "disabled", + "readOnly", + "rows", + "maxLength", + "maxCharacters", + "minHeight", + "maxHeight", + "styles", + "gd-input", + "gd-change", + "focus(" + ], + "stories": [ + "Default", + "Disabled", + "ReadOnly", + "VariantInline", + "ColorPrimary", + "ColorSuccess", + "ColorWarning", + "ColorError", + "AutoFocus", + "DynamicHeight", + "WithCharLimit", + "Resizable", + "WithControlledStateEraseOnEnterClick", + "MaxCharacters", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-separator": { + "react": "Separator", + "source": [ + "orientation", + "length", + "color", + "size", + "variant", + "as", + "label", + "labelPosition", + "labelColor", + "styles", + "part=\"line\"", + "part=\"label\"" + ], + "stories": [ + "Default", + "WithCustomColor", + "WithDifferentVariants", + "WithLabel", + "Vertical", + "VerticalWithLabel", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-skeleton": { + "react": "Skeleton", + "source": [ + "variant", + "width", + "height", + "backgroundColor", + "animationName", + "animationProps", + "styles", + "slot", + "part=\"skeleton\"" + ], + "stories": [ + "Default", + "Circular", + "Rectangular", + "WithThemeColor", + "WithCustomAnimation", + "WithChildren", + "ComposedLayout", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-icon": { + "react": "Icon", + "source": ["name", "width", "height", "size", "fill", "fillSvg", "theme", "part=\"svg\""], + "stories": [ + "AllIcons", + "Default", + "WithDefinedSize", + "RegisteringCustomIcons", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-input-file": { + "react": "InputFile", + "source": ["accept", "capture", "multiple", "disabled", "isIcon", "buttonVariant", "styles", "gd-change", "open()"], + "stories": [ + "Default", + "Disabled", + "Multiple", + "WithAccept", + "CustomLabel", + "IconLabel", + "WithCustomStyles", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-label": { + "react": "Label", + "source": ["htmlFor", "styles", "slot", "part=\"label\""], + "stories": [ + "LabelDefault", + "LabelWithChildIcon", + "CustomStyles", + "DefaultWithTailwind", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-link": { + "react": "Link", + "source": [ + "variant", + "size", + "underline", + "color", + "cursor", + "disabled", + "target", + "href", + "rel", + "styles", + "slot" + ], + "stories": [ + "Variants", + "Disabled", + "TargetBlankVisited", + "InheritWithButtonAsChild", + "Inverted", + "InheritWithTypographyAsChild", + "InheritWithTailwind", + "CustomStyles", + "WithUnderline", + "WithSizes", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-loader": { + "react": "Loader", + "source": ["name", "variant", "size", "rounded", "animationProps", "withWrapper", "styles", "popover", "slot"], + "stories": [ + "Default", + "LoaderNames", + "LoaderSizes", + "LoaderRounded", + "LoaderVariantsWithWrapperViewAsHeaderTag", + "CustomLoaderTailwindClassBounceAnimation", + "LoaderSectionVariant", + "SectionLoaderButtonVariant", + "InlineLoaderButtonVariant", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-badge": { + "react": "Badge", + "source": [ + "variant", + "appearance", + "size", + "disabled", + "styles", + "slot name=\"icon-start\"", + "slot name=\"icon-end\"" + ], + "stories": [ + "Default", + "Sizes", + "Variants", + "WithIcons", + "WithBoxStyles", + "Disabled", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-box": { + "react": "Box", + "source": ["variant", "isBordered", "isHighlighted", "withShadowHover", "tabIndex", "styles", "slot"], + "stories": [ + "Default", + "Bordered", + "Highlighted", + "WithShadowHover", + "VerticalLayout", + "HorizontalLayout", + "CenteredContent", + "Examples", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-image": { + "react": "Image", + "source": [ + "src", + "alt", + "width", + "height", + "caption", + "objectFit", + "styles", + "slot name=\"placeholder\"", + "slot name=\"fallback\"", + "gd-load", + "gd-error" + ], + "stories": [ + "Default", + "WithCaption", + "WithPlaceholder", + "Clickable", + "FallbackImage", + "WithAsProp", + "WithCaptionAsProp", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-avatar": { + "react": "Avatar", + "source": [ + "src", + "alt", + "sizeVariant", + "withBadge", + "badgeColor", + "backgroundColor", + "styles", + "slot name=\"fallback\"" + ], + "stories": [ + "Default", + "WithBadge", + "WithCustomBadgeColor", + "WithInitials", + "WithCustomBackgroundColor", + "WithIcon", + "WithImageAndFallback", + "WithDifferentSize", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-button": { + "react": "Button", + "source": [ + "variant", + "rounded", + "disabled", + "fullWidth", + "isIcon", + "isLoading", + "type", + "role", + "tabIndex", + "ariaLabel", + "ariaPressed", + "styles", + "slot name=\"icon-start\"", + "slot name=\"icon-end\"" + ], + "stories": [ + "AllVariants", + "Default", + "WithIcons", + "IconOnly", + "FullWidthButton", + "RoundedButton", + "DisabledButton", + "ButtonStatesUsingClass", + "CustomStyledButton", + "RealWorldExamples", + "IsLoading", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-checkbox": { + "react": "Checkbox", + "source": ["checked", "indeterminate", "disabled", "name", "value", "size", "styles", "gd-change"], + "stories": ["Default", "Controlled", "Indeterminate", "Disabled", "Sizes", "DefaultTokens"] + }, + "gd-input": { + "react": "Input", + "source": [ + "variant", + "color", + "disabled", + "readOnly", + "value", + "defaultValue", + "checked", + "defaultChecked", + "name", + "width", + "styles", + "placeholder", + "label", + "helperText", + "required", + "ariaRequiredValue", + "ariaDescribedBy", + "role", + "inputmode", + "tabIndex", + "debounceCallbackTime", + "slot name=\"adornment-start\"", + "slot name=\"adornment-end\"", + "gd-input", + "gd-change" + ], + "stories": [ + "PrimaryDefaultWithLabelAndHelperText", + "SuccessWithLabelAndHelperText", + "WarningWithLabelAndHelperText", + "ErrorWithLabelAndHelperText", + "Disabled", + "ReadOnly", + "WrapperAsSpan", + "WithAdornments", + "WithStartAdornment", + "WithEndAdornmentAsIcon", + "DefaultWithTailwind", + "CustomStyles", + "CheckboxWithLabel", + "RadioControlledWithLabel", + "RadioGroupWithLabel", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-select": { + "react": "Select", + "source": [ + "items", + "itemsCount", + "value", + "disabled", + "color", + "placeholder", + "multiple", + "searchable", + "searchPlaceholder", + "autoOpen", + "activeIndex", + "dropdownMaxHeight", + "itemIdentifier", + "itemStringifier", + "renderOption", + "width", + "minWidth", + "maxWidth", + "styles", + "slot name=\"initiator\"", + "slot name=\"empty\"", + "slot name=\"adornment-start\"", + "slot name=\"adornment-end\"", + "gd-change", + "open()", + "close()" + ], + "stories": [ + "Default", + "MultipleSelect", + "WithAdornmentsAndResetOptionStory", + "WithCustomInitiatorAndSelectedOutputValue", + "UsingRefComponentWithCustomPlaceholder", + "WithFileInputAndCustomDropdown", + "CustomItemIdentifierComponent", + "WithAccessibility", + "Disabled", + "EmptyItems", + "ColorPrimary", + "ColorSuccess", + "ColorWarning", + "ColorError", + "Searchable", + "DefaultTokens" + ] + }, + "gd-typography": { + "react": "Typography", + "source": ["variant", "as", "color", "size", "align", "styleVariant", "styles", "slot"], + "stories": [ + "Default", + "AsCustomComponent", + "AsCustomHtmlTag", + "CombinedStyleVariant", + "Disclaimers", + "Display", + "Heading", + "Body", + "WithAccessibility", + "DefaultTokens" + ] + }, + "gd-counter": { + "react": "Counter", + "source": ["min", "max", "initial", "isDisabled", "styles", "gd-change"], + "stories": [ + "Default", + "AdjustedMaxValue5", + "AdjustedMinValue3", + "AdjustedMin2MaxValue10", + "WithExternalCounterChangeHandler", + "Disabled", + "DefaultTokens" + ] + }, + "gd-menu": { + "react": "Menu", + "source": [ + "itemIdentifier", + "closeOnSelect", + "offsetX", + "offsetY", + "minHeight", + "maxHeight", + "placement", + "selectedValue", + "styles", + "slot name=\"trigger\"", + "slot name=\"content\"", + "gd-change", + "openMenu()", + "closeMenu()" + ], + "stories": [ + "Default", + "CloseOnSelectFalse", + "Placement", + "WithRefControl", + "WithOffset", + "WithHeightConstraints", + "WithPositioningOptions", + "WithEditAndDeleteModals", + "DefaultTokens" + ] + } +} diff --git a/libs/web-components/scripts/check-ported-elements.mjs b/libs/web-components/scripts/check-ported-elements.mjs index 8a8c95a..62df637 100644 --- a/libs/web-components/scripts/check-ported-elements.mjs +++ b/libs/web-components/scripts/check-ported-elements.mjs @@ -4,6 +4,34 @@ import { resolve } from 'node:path'; const packageRoot = resolve(import.meta.dirname, '..'); const componentsRoot = resolve(packageRoot, 'src/components'); +const repoRoot = resolve(packageRoot, '../..'); +const fullStoryParity = new Set([ + 'gd-avatar', + 'gd-box', + 'gd-badge', + 'gd-button', + 'gd-checkbox', + 'gd-image', + 'gd-icon', + 'gd-input', + 'gd-input-file', + 'gd-label', + 'gd-link', + 'gd-loader', + 'gd-separator', + 'gd-skeleton', + 'gd-slider', + 'gd-slider-dots', + 'gd-switch', + 'gd-textarea', + 'gd-toggle', + 'gd-truncate', + 'gd-wrapper', + 'gd-select', + 'gd-typography', + 'gd-counter', + 'gd-menu', +]); // This is the deliberately supported Web Component surface. It used to live in // `migration-ir`, which was removed along with the other one-off migration @@ -11,9 +39,26 @@ const componentsRoot = resolve(packageRoot, 'src/components'); // accidental missing port, public export, or SSR fixture. const ports = [ { tag: 'gd-avatar', category: 'atoms' }, + { tag: 'gd-badge', category: 'atoms' }, + { tag: 'gd-box', category: 'atoms' }, { tag: 'gd-button', category: 'atoms' }, { tag: 'gd-checkbox', category: 'atoms' }, { tag: 'gd-input', category: 'atoms' }, + { tag: 'gd-image', category: 'atoms' }, + { tag: 'gd-icon', category: 'atoms' }, + { tag: 'gd-input-file', category: 'atoms' }, + { tag: 'gd-label', category: 'atoms' }, + { tag: 'gd-link', category: 'atoms' }, + { tag: 'gd-loader', category: 'atoms' }, + { tag: 'gd-separator', category: 'atoms' }, + { tag: 'gd-skeleton', category: 'atoms' }, + { tag: 'gd-slider', category: 'atoms' }, + { tag: 'gd-slider-dots', category: 'atoms' }, + { tag: 'gd-switch', category: 'atoms' }, + { tag: 'gd-textarea', category: 'atoms' }, + { tag: 'gd-toggle', category: 'atoms' }, + { tag: 'gd-truncate', category: 'atoms' }, + { tag: 'gd-wrapper', category: 'atoms' }, { tag: 'gd-select', category: 'atoms' }, { tag: 'gd-typography', category: 'atoms' }, { tag: 'gd-counter', category: 'molecules' }, @@ -33,18 +78,74 @@ const missingFromManifest = [...actual].filter((entry) => !expected.has(entry)); const missingFromSource = [...expected].filter((entry) => !actual.has(entry)); if (missingFromManifest.length || missingFromSource.length) { throw new Error( - `Port manifest mismatch. Missing manifest: ${missingFromManifest.join(', ') || 'none'}; missing source: ${missingFromSource.join(', ') || 'none'}.` + `Port manifest mismatch. Missing manifest: ${missingFromManifest.join(', ') || 'none'}; missing source: ${ + missingFromSource.join(', ') || 'none' + }.` ); } -const [index, ssr] = await Promise.all([ +const [index, ssr, parityText, publishedAuditText] = 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'), ]); +const parity = JSON.parse(parityText); +const publishedAudit = JSON.parse(publishedAuditText); +const atomTags = ports.filter(({ category }) => category === 'atoms').map(({ tag }) => tag); +const publishedAtomTags = Object.keys(publishedAudit.components); +const missingPublishedAtoms = publishedAtomTags.filter((tag) => !atomTags.includes(tag)); +const unpublishedPorts = atomTags.filter((tag) => !publishedAtomTags.includes(tag)); +if (missingPublishedAtoms.length || unpublishedPorts.length) { + throw new Error( + `Published Atoms inventory mismatch. Missing ports: ${ + missingPublishedAtoms.join(', ') || 'none' + }; unverified ports: ${unpublishedPorts.join(', ') || 'none'}.` + ); +} for (const { tag, category } of ports) { const modulePath = `./components/${category}/${tag}/${tag}`; if (!index.includes(modulePath)) throw new Error(`Missing public export for ${tag}: ${modulePath}`); if (!ssr.includes(`<${tag}`)) throw new Error(`SSR harness does not render ${tag}.`); } -console.log(`Verified ${ports.length} Web Component ports: ${ports.map(({ tag }) => tag).join(', ')}`); +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'); + const storyName = tag.slice(3).replace(/(^|-)([a-z])/g, (_match, _separator, letter) => letter.toUpperCase()); + const stories = await readFile(resolve(packageRoot, 'stories', `${storyName}.stories.ts`), 'utf8'); + if (fullStoryParity.has(tag) && !stories.includes('description:')) + 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}`); + } + for (const story of contract.stories) { + if (!stories.includes(`export const ${story}`)) throw new Error(`${tag} is missing parity story: ${story}`); + } + if (fullStoryParity.has(tag)) { + const reactStories = await readFile( + resolve(repoRoot, 'libs/ui/src/components', category, contract.react, `${contract.react}.stories.tsx`), + 'utf8' + ); + 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)); + if (missing.length || stale.length) { + throw new Error( + `${tag} React Storybook inventory mismatch. Missing: ${missing.join(', ') || 'none'}; stale: ${ + stale.join(', ') || 'none' + }.` + ); + } + if (category === 'atoms' && publishedAudit.components[tag] !== contract.stories.length) { + throw new Error( + `${tag} published Storybook count mismatch: expected ${publishedAudit.components[tag]}, recorded ${contract.stories.length}.` + ); + } + } +} + +console.log( + `Verified ${ports.length} Web Component ports and React parity contracts: ${ports.map(({ tag }) => tag).join(', ')}` +); diff --git a/libs/web-components/scripts/ssr-dsd-render.ts b/libs/web-components/scripts/ssr-dsd-render.ts index 6028d20..3169ef8 100644 --- a/libs/web-components/scripts/ssr-dsd-render.ts +++ b/libs/web-components/scripts/ssr-dsd-render.ts @@ -17,9 +17,26 @@ import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { defaultTheme } from 'gd-design-library/tokens'; import '../src/components/atoms/gd-avatar/gd-avatar'; +import '../src/components/atoms/gd-badge/gd-badge'; +import '../src/components/atoms/gd-box/gd-box'; import '../src/components/atoms/gd-button/gd-button'; import '../src/components/atoms/gd-checkbox/gd-checkbox'; import '../src/components/atoms/gd-input/gd-input'; +import '../src/components/atoms/gd-image/gd-image'; +import '../src/components/atoms/gd-icon/gd-icon'; +import '../src/components/atoms/gd-input-file/gd-input-file'; +import '../src/components/atoms/gd-label/gd-label'; +import '../src/components/atoms/gd-link/gd-link'; +import '../src/components/atoms/gd-loader/gd-loader'; +import '../src/components/atoms/gd-separator/gd-separator'; +import '../src/components/atoms/gd-skeleton/gd-skeleton'; +import '../src/components/atoms/gd-slider/gd-slider'; +import '../src/components/atoms/gd-slider-dots/gd-slider-dots'; +import '../src/components/atoms/gd-switch/gd-switch'; +import '../src/components/atoms/gd-textarea/gd-textarea'; +import '../src/components/atoms/gd-toggle/gd-toggle'; +import '../src/components/atoms/gd-truncate/gd-truncate'; +import '../src/components/atoms/gd-wrapper/gd-wrapper'; import '../src/components/atoms/gd-select/gd-select'; import '../src/components/atoms/gd-typography/gd-typography'; import '../src/components/molecules/gd-menu/gd-menu'; @@ -38,9 +55,26 @@ export async function runSsrDsdCheck() { // (`fidelity-check.ts`, `form-participation-check.ts`). const template = html` + Status + Box content Submit Accept terms + No image + + Upload + Name + Docs + + + + + + Notifications + + + Long content + Wrapped content Heading [tag, new RegExp(`<${tag}[^>]*>\\s*