Stories will consume custom elements directly, without React adapters, using the production theme and styles.
-
The Angular and Vue integration checks remain in place during this scaffold phase.
+
Stories consume custom elements directly, without React adapters, using the production defaultTheme property and built stylesheet/fonts.
+
Use Controls to explore properties. Interactive stories display native gd-input / gd-change event details below the element. Objects such as theme and items are JavaScript properties, not string attributes.
+
Menu demonstrates trigger/content slots, selection, Escape and outside-click dismissal. Counter demonstrates bounded quantity changes. Other components are intentionally postponed.
+
The Angular and Vue integration checks remain in place; harness cleanup is a later phase.
`;
return page;
},
diff --git a/libs/web-components/stories/Select.stories.ts b/libs/web-components/stories/Select.stories.ts
new file mode 100644
index 0000000..16b19b7
--- /dev/null
+++ b/libs/web-components/stories/Select.stories.ts
@@ -0,0 +1,88 @@
+import type { Meta, StoryObj } from '@storybook/web-components-vite';
+import type { GdSelect } from '../src';
+import { element, observed, items } 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;
+const meta = {
+ title: 'Atoms/Select',
+ tags: ['autodocs'],
+ args: {
+ items,
+ value: null,
+ disabled: false,
+ 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 } },
+ 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 });
+ },
+} 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 } };
+export const AutoOpen: Story = { args: { autoOpen: false }, name: 'Manual open (autoOpen=false)' };
+export const ValidationColors: 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;
+ },
+};
+export const WithAdornments: Story = {
+ render: (args) => {
+ const select = element('gd-select', args);
+ const start = document.createElement('span');
+ start.slot = 'adornment-start';
+ start.textContent = '★';
+ const end = document.createElement('span');
+ end.slot = 'adornment-end';
+ end.textContent = 'Required';
+ select.append(start, end);
+ return select;
+ },
+};
+export const CustomInitiator: 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;
+ },
+};
diff --git a/libs/web-components/stories/Typography.stories.ts b/libs/web-components/stories/Typography.stories.ts
new file mode 100644
index 0000000..0ab4d67
--- /dev/null
+++ b/libs/web-components/stories/Typography.stories.ts
@@ -0,0 +1,119 @@
+import type { Meta, StoryObj } from '@storybook/web-components-vite';
+import type { GdTypography } from '../src';
+import { element } from './helpers';
+
+type Args = Pick & { text: 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 meta = {
+ title: 'Atoms/Typography',
+ tags: ['autodocs'],
+ args: {
+ variant: 'p',
+ as: 'p',
+ size: 'md',
+ align: 'start',
+ color: '',
+ styleVariant: [],
+ text: 'Body paragraph text for comparison.',
+ },
+ argTypes: {
+ 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;
+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 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`));
+ 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(element('gd-typography', { ...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(
+ element('gd-typography', { ...args, variant: 'p', as: 'p', styleVariant }, `${styleVariant} text`)
+ );
+ section.append(
+ element(
+ 'gd-typography',
+ { ...args, variant: 'p', as: 'p', styleVariant: ['bold', 'italic', 'underline'] },
+ 'Combined: bold + italic + underline'
+ )
+ );
+ return section;
+ },
+};
+export const SemanticOverride: Story = {
+ args: { variant: 'h2', as: 'h3', text: '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'));
+ 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'));
+ return section;
+ },
+};
diff --git a/libs/web-components/stories/helpers.ts b/libs/web-components/stories/helpers.ts
new file mode 100644
index 0000000..6c53b07
--- /dev/null
+++ b/libs/web-components/stories/helpers.ts
@@ -0,0 +1,147 @@
+import { defaultTheme } from 'gd-design-library/tokens';
+import type { DesignCoreTheme } from 'gd-design-core';
+import type {} from '../src/index';
+
+type Tag = Extract;
+
+const sourceKeyProperties: Partial> = {
+ 'gd-avatar': ['sizeVariant'],
+ 'gd-button': ['variant', 'rounded'],
+ 'gd-checkbox': ['size'],
+ 'gd-input': ['variant', 'color'],
+ 'gd-select': ['color'],
+ 'gd-typography': ['variant', 'as', 'size', 'align'],
+ 'gd-counter': ['min', 'max', 'initial'],
+ 'gd-menu': ['placement', 'closeOnSelect'],
+};
+
+const sourceEvents: Partial> = {
+ 'gd-checkbox': ['gd-change'],
+ 'gd-input': ['gd-input', 'gd-change'],
+ 'gd-select': ['gd-change'],
+ 'gd-counter': ['gd-change'],
+ 'gd-menu': ['gd-change'],
+};
+
+const escapeHtml = (value: string) =>
+ value.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>');
+
+const kebab = (value: string) => value.replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`);
+
+function equivalent(left: unknown, right: unknown) {
+ if (Object.is(left, right)) return true;
+ if (typeof left === 'object' && left !== null && typeof right === 'object' && right !== null) {
+ try {
+ return JSON.stringify(left) === JSON.stringify(right);
+ } catch {
+ return false;
+ }
+ }
+ return false;
+}
+
+function lightDom(node: Element) {
+ return Array.from(node.childNodes)
+ .map((child) => {
+ if (child.nodeType === Node.TEXT_NODE) return escapeHtml(child.textContent ?? '');
+ return child instanceof Element ? child.outerHTML : '';
+ })
+ .join('')
+ .trim();
+}
+
+/** Produce consumer-ready source from the custom elements rendered by a story.
+ * Storybook cannot infer useful source from our imperative DOM render functions. */
+export function storySource(canvas: HTMLElement | undefined, fallback: string) {
+ if (!canvas) return fallback;
+ const nodes = Array.from(canvas.querySelectorAll('*')).filter((node) =>
+ node.localName.startsWith('gd-')
+ );
+ if (!nodes.length) return fallback;
+
+ const markup: string[] = [];
+ const setup: string[] = ["import 'web-components';", "import { defaultTheme } from 'gd-design-library/tokens';", ''];
+ nodes.forEach((node, index) => {
+ const tag = node.localName as Tag;
+ const baseline = document.createElement(tag) as unknown as HTMLElement & Record;
+ const current = node as unknown as HTMLElement & Record;
+ const constructor = customElements.get(tag) as
+ | (CustomElementConstructor & {
+ elementProperties?: Map;
+ })
+ | undefined;
+ const attributes = new Map(Array.from(node.attributes).map(({ name, value }) => [name, value]));
+ const assignments: Array<[string, unknown]> = [];
+ for (const [property, options] of constructor?.elementProperties ?? []) {
+ if (typeof property !== 'string' || property === 'theme' || options.state || property.startsWith('_')) continue;
+ const value = current[property];
+ const requiredForUnderstanding = sourceKeyProperties[tag]?.includes(property) ?? false;
+ if (!requiredForUnderstanding && equivalent(value, baseline[property])) continue;
+ const attribute =
+ options.attribute === false
+ ? false
+ : typeof options.attribute === 'string'
+ ? options.attribute
+ : kebab(property);
+ if (attribute && (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')) {
+ if (typeof value === 'boolean') {
+ if (value) attributes.set(attribute, '');
+ else if (!equivalent(value, baseline[property])) assignments.push([property, value]);
+ } 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}${tag}>`);
+
+ const variable = nodes.length === 1 ? 'component' : `component${index + 1}`;
+ const sameTagIndex = nodes.slice(0, index).filter((candidate) => candidate.localName === tag).length;
+ setup.push(
+ nodes.length === 1
+ ? `const ${variable} = document.querySelector('${tag}');`
+ : `const ${variable} = document.querySelectorAll('${tag}')[${sameTagIndex}];`
+ );
+ setup.push(`${variable}.theme = defaultTheme;`);
+ for (const [property, value] of assignments)
+ setup.push(`${variable}.${property} = ${JSON.stringify(value, null, 2)};`);
+ for (const eventName of sourceEvents[tag] ?? []) {
+ setup.push(`${variable}.addEventListener('${eventName}', (event) => {`, ' console.log(event.detail);', '});');
+ }
+ setup.push('');
+ });
+
+ return `${markup.join('\n\n')}\n\n`;
+}
+
+/** Use the same property-based theme contract as application consumers. */
+export function element(tag: T, props: Partial = {}, text = '') {
+ const node = document.createElement(tag);
+ Object.assign(node, { theme: defaultTheme as DesignCoreTheme }, props);
+ node.textContent = text;
+ return node;
+}
+
+/** Each render owns its output and listeners; no state leaks between stories. */
+export function observed(node: HTMLElement, eventName: string, initial: unknown) {
+ const section = document.createElement('section');
+ section.style.cssText = 'display:grid;gap:16px;max-width:480px;font-family:"Fira Sans",sans-serif';
+ const output = document.createElement('output');
+ output.setAttribute('aria-label', 'Last event');
+ output.textContent = JSON.stringify(initial);
+ node.addEventListener(eventName, (event) => {
+ output.textContent = JSON.stringify((event as CustomEvent).detail);
+ });
+ section.append(node, output);
+ return section;
+}
+
+export const items = [
+ { name: 'Alpha', value: 'a' },
+ { name: 'Beta', value: 'b' },
+ { name: 'Gamma', value: 'c' },
+];
+
+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';
diff --git a/libs/web-components/test/story-source.spec.ts b/libs/web-components/test/story-source.spec.ts
new file mode 100644
index 0000000..373598e
--- /dev/null
+++ b/libs/web-components/test/story-source.spec.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from 'vitest';
+import '../src/index';
+import { element, items, storySource } from '../stories/helpers';
+
+function source(...nodes: HTMLElement[]) {
+ const canvas = document.createElement('main');
+ canvas.append(...nodes);
+ return storySource(canvas, 'fallback');
+}
+
+describe('Storybook custom-element source', () => {
+ it('shows the effective properties for each Button state without Lit internals', () => {
+ const secondary = source(element('gd-button', { variant: 'secondary' }, 'Button'));
+ const disabled = source(element('gd-button', { variant: 'primary', disabled: true }, 'Button'));
+
+ expect(secondary).toContain('Button');
+ expect(disabled).toMatch(/]*variant="primary"[^>]*disabled[^>]*>Button<\/gd-button>/);
+ expect(secondary).toContain('component.theme = defaultTheme;');
+ expect(secondary).toContain("import 'web-components';");
+ expect(secondary).not.toContain('_hasContent');
+ });
+
+ it('uses JavaScript properties and native events for non-attribute Select inputs', () => {
+ const select = element('gd-select', { items, value: items[1], multiple: true });
+ const generated = source(select);
+
+ expect(generated).toContain('component.items = [');
+ expect(generated).toContain('component.value = {');
+ expect(generated).toContain("component.addEventListener('gd-change'");
+ });
+
+ it('keeps named slots in the consumer markup', () => {
+ const button = element('gd-button', { variant: 'outlined' }, 'Continue');
+ const icon = document.createElement('span');
+ icon.slot = 'icon-start';
+ icon.textContent = '→';
+ button.prepend(icon);
+
+ expect(source(button)).toContain('→Continue');
+ });
+});
From dd14f770548c9ce21c6e7450ff43115ecb0e9555 Mon Sep 17 00:00:00 2001
From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com>
Date: Thu, 1 Oct 2026 09:38:24 +0200
Subject: [PATCH 05/10] feat: CTORNDSD-679: Add Icon catalog to design-core and
expand web components with 13 new atoms
Integrate shared icon catalog from gd-design-core into React Icon component. Add 13 new web components: badge, box, icon, image, input-file, label, link, loader, separator, skeleton, slider, slider-dots, switch, textarea, toggle, truncate, and wrapper. Update Storybook stories with detailed descriptions and expand native component test coverage. Include migration manifest for loader, separator, and skeleton components.
---
CLAUDE.md | 2 +-
README.md | 7 +-
bin/generate-icon-catalog.ts | 28 ++
bin/storybook/native-checks.mjs | 56 ++-
libs/design-core/src/icons/catalog.ts | 226 ++++++++++
libs/design-core/src/icons/index.ts | 1 +
libs/design-core/src/index.ts | 1 +
libs/ui/package.json | 1 +
libs/ui/src/components/atoms/Icon/Icon.tsx | 40 +-
.../Icon/__snapshots__/Icon.test.tsx.snap | 6 +-
libs/ui/src/tokens/index.ts | 3 +
libs/ui/tsconfig.json | 2 +
libs/ui/tsconfig.lib.json | 5 +-
libs/ui/vitest.alias.ts | 1 +
...TION-MANIFEST.loader-separator-skeleton.md | 11 +
.../MIGRATION-MANIFEST.slider-controls.md | 13 +
...FEST.toggle-truncate-typography-wrapper.md | 12 +
libs/web-components/README.md | 48 +-
.../harness/fidelity-check-vue.ts | 135 ++++++
libs/web-components/harness/fidelity-check.ts | 85 ++++
.../web-components/published-atoms-audit.json | 29 ++
libs/web-components/react-parity.json | 411 +++++++++++++++++-
.../scripts/check-ported-elements.mjs | 87 +++-
libs/web-components/scripts/ssr-dsd-render.ts | 64 ++-
.../components/atoms/gd-avatar/gd-avatar.ts | 14 +-
.../atoms/gd-badge/gd-badge.spec.ts | 24 +
.../src/components/atoms/gd-badge/gd-badge.ts | 55 +++
.../components/atoms/gd-box/gd-box.spec.ts | 21 +
.../src/components/atoms/gd-box/gd-box.ts | 50 +++
.../components/atoms/gd-icon/gd-icon.spec.ts | 16 +
.../src/components/atoms/gd-icon/gd-icon.ts | 46 ++
.../atoms/gd-image/gd-image.spec.ts | 26 ++
.../src/components/atoms/gd-image/gd-image.ts | 84 ++++
.../atoms/gd-input-file/gd-input-file.spec.ts | 16 +
.../atoms/gd-input-file/gd-input-file.ts | 64 +++
.../atoms/gd-label/gd-label.spec.ts | 18 +
.../src/components/atoms/gd-label/gd-label.ts | 23 +
.../components/atoms/gd-link/gd-link.spec.ts | 29 ++
.../src/components/atoms/gd-link/gd-link.ts | 66 +++
.../atoms/gd-loader/gd-loader.spec.ts | 20 +
.../components/atoms/gd-loader/gd-loader.ts | 76 ++++
.../atoms/gd-separator/gd-separator.spec.ts | 41 ++
.../atoms/gd-separator/gd-separator.ts | 102 +++++
.../atoms/gd-skeleton/gd-skeleton.spec.ts | 40 ++
.../atoms/gd-skeleton/gd-skeleton.ts | 57 +++
.../gd-slider-dots/gd-slider-dots.spec.ts | 21 +
.../atoms/gd-slider-dots/gd-slider-dots.ts | 57 +++
.../atoms/gd-slider/gd-slider.spec.ts | 22 +
.../components/atoms/gd-slider/gd-slider.ts | 64 +++
.../atoms/gd-switch/gd-switch.spec.ts | 29 ++
.../components/atoms/gd-switch/gd-switch.ts | 74 ++++
.../atoms/gd-textarea/gd-textarea.spec.ts | 40 ++
.../atoms/gd-textarea/gd-textarea.ts | 108 +++++
.../atoms/gd-toggle/gd-toggle.spec.ts | 21 +
.../components/atoms/gd-toggle/gd-toggle.ts | 64 +++
.../atoms/gd-truncate/gd-truncate.spec.ts | 16 +
.../atoms/gd-truncate/gd-truncate.ts | 48 ++
.../atoms/gd-wrapper/gd-wrapper.spec.ts | 18 +
.../components/atoms/gd-wrapper/gd-wrapper.ts | 34 ++
libs/web-components/src/index.ts | 17 +
libs/web-components/stories/Avatar.stories.ts | 17 +
libs/web-components/stories/Badge.stories.ts | 67 +++
libs/web-components/stories/Box.stories.ts | 68 +++
libs/web-components/stories/Button.stories.ts | 20 +
.../stories/Checkbox.stories.ts | 10 +
.../web-components/stories/Counter.stories.ts | 13 +
libs/web-components/stories/Icon.stories.ts | 73 ++++
libs/web-components/stories/Image.stories.ts | 92 ++++
libs/web-components/stories/Input.stories.ts | 35 ++
.../stories/InputFile.stories.ts | 66 +++
libs/web-components/stories/Label.stories.ts | 57 +++
libs/web-components/stories/Link.stories.ts | 96 ++++
libs/web-components/stories/Loader.stories.ts | 80 ++++
libs/web-components/stories/Menu.stories.ts | 13 +
.../stories/Overview.stories.ts | 8 +-
libs/web-components/stories/Select.stories.ts | 26 ++
.../stories/Separator.stories.ts | 118 +++++
.../stories/Skeleton.stories.ts | 97 +++++
libs/web-components/stories/Slider.stories.ts | 58 +++
.../stories/SliderDots.stories.ts | 56 +++
libs/web-components/stories/Switch.stories.ts | 69 +++
.../stories/Textarea.stories.ts | 159 +++++++
libs/web-components/stories/Toggle.stories.ts | 62 +++
.../stories/Truncate.stories.ts | 72 +++
.../stories/Typography.stories.ts | 17 +
.../web-components/stories/Wrapper.stories.ts | 79 ++++
libs/web-components/stories/helpers.ts | 28 +-
libs/web-components/vitest.config.ts | 3 +
package-lock.json | 1 +
89 files changed, 4289 insertions(+), 66 deletions(-)
create mode 100644 bin/generate-icon-catalog.ts
create mode 100644 libs/design-core/src/icons/catalog.ts
create mode 100644 libs/design-core/src/icons/index.ts
create mode 100644 libs/web-components/MIGRATION-MANIFEST.loader-separator-skeleton.md
create mode 100644 libs/web-components/MIGRATION-MANIFEST.slider-controls.md
create mode 100644 libs/web-components/MIGRATION-MANIFEST.toggle-truncate-typography-wrapper.md
create mode 100644 libs/web-components/published-atoms-audit.json
create mode 100644 libs/web-components/src/components/atoms/gd-badge/gd-badge.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-badge/gd-badge.ts
create mode 100644 libs/web-components/src/components/atoms/gd-box/gd-box.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-box/gd-box.ts
create mode 100644 libs/web-components/src/components/atoms/gd-icon/gd-icon.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-icon/gd-icon.ts
create mode 100644 libs/web-components/src/components/atoms/gd-image/gd-image.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-image/gd-image.ts
create mode 100644 libs/web-components/src/components/atoms/gd-input-file/gd-input-file.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-input-file/gd-input-file.ts
create mode 100644 libs/web-components/src/components/atoms/gd-label/gd-label.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-label/gd-label.ts
create mode 100644 libs/web-components/src/components/atoms/gd-link/gd-link.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-link/gd-link.ts
create mode 100644 libs/web-components/src/components/atoms/gd-loader/gd-loader.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-loader/gd-loader.ts
create mode 100644 libs/web-components/src/components/atoms/gd-separator/gd-separator.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-separator/gd-separator.ts
create mode 100644 libs/web-components/src/components/atoms/gd-skeleton/gd-skeleton.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-skeleton/gd-skeleton.ts
create mode 100644 libs/web-components/src/components/atoms/gd-slider-dots/gd-slider-dots.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-slider-dots/gd-slider-dots.ts
create mode 100644 libs/web-components/src/components/atoms/gd-slider/gd-slider.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-slider/gd-slider.ts
create mode 100644 libs/web-components/src/components/atoms/gd-switch/gd-switch.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-switch/gd-switch.ts
create mode 100644 libs/web-components/src/components/atoms/gd-textarea/gd-textarea.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-textarea/gd-textarea.ts
create mode 100644 libs/web-components/src/components/atoms/gd-toggle/gd-toggle.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-toggle/gd-toggle.ts
create mode 100644 libs/web-components/src/components/atoms/gd-truncate/gd-truncate.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-truncate/gd-truncate.ts
create mode 100644 libs/web-components/src/components/atoms/gd-wrapper/gd-wrapper.spec.ts
create mode 100644 libs/web-components/src/components/atoms/gd-wrapper/gd-wrapper.ts
create mode 100644 libs/web-components/stories/Badge.stories.ts
create mode 100644 libs/web-components/stories/Box.stories.ts
create mode 100644 libs/web-components/stories/Icon.stories.ts
create mode 100644 libs/web-components/stories/Image.stories.ts
create mode 100644 libs/web-components/stories/InputFile.stories.ts
create mode 100644 libs/web-components/stories/Label.stories.ts
create mode 100644 libs/web-components/stories/Link.stories.ts
create mode 100644 libs/web-components/stories/Loader.stories.ts
create mode 100644 libs/web-components/stories/Separator.stories.ts
create mode 100644 libs/web-components/stories/Skeleton.stories.ts
create mode 100644 libs/web-components/stories/Slider.stories.ts
create mode 100644 libs/web-components/stories/SliderDots.stories.ts
create mode 100644 libs/web-components/stories/Switch.stories.ts
create mode 100644 libs/web-components/stories/Textarea.stories.ts
create mode 100644 libs/web-components/stories/Toggle.stories.ts
create mode 100644 libs/web-components/stories/Truncate.stories.ts
create mode 100644 libs/web-components/stories/Wrapper.stories.ts
diff --git a/CLAUDE.md b/CLAUDE.md
index b434a21..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. The native catalog covers the six existing atoms and two molecules. `node bin/storybook/smoke-test.mjs` verifies native rendering and interactions after the combined build. Do not migrate additional components or remove Angular/Vue checks; harness cleanup is Phase 3.
+**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 309aff4..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 native stories and controls 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.
@@ -99,8 +99,9 @@ list to refresh when a story disappears. Link checks cover source documentation
`llms.txt`, AI Markdown/JSON, and AI JavaScript copied into the UI package.
Native browser smoke checks run with `node bin/storybook/smoke-test.mjs` after the combined build.
-They render every native story and exercise custom events and popover dismissal.
-The Angular/Vue and other Web Components harnesses remain unchanged until Phase 3.
+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(/^
@@ -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/tokens/index.ts b/libs/ui/src/tokens/index.ts
index 859036c..b62830d 100644
--- a/libs/ui/src/tokens/index.ts
+++ b/libs/ui/src/tokens/index.ts
@@ -54,6 +54,7 @@ import { checkbox } from './checkbox';
import { zIndex } from './zIndex';
import { sidebar } from './sidebar';
import { imagePreview } from './imagePreview';
+import { image } from './image';
import { inputArea } from './inputArea';
import { sliderDots } from './sliderDots';
import { attachmentFile } from './attachmentFile';
@@ -119,6 +120,7 @@ export * from './truncate';
export * from './zIndex';
export * from './sidebar';
export * from './imagePreview';
+export * from './image';
export * from './inputArea';
export * from './sliderDots';
export * from './attachmentFile';
@@ -133,6 +135,7 @@ export const defaultTokens = {
separator,
draganddrop,
avatar,
+ image,
chatbubble,
radiogroup,
draganddropfiles,
diff --git a/libs/ui/tsconfig.json b/libs/ui/tsconfig.json
index 4f5cb15..65b2206 100644
--- a/libs/ui/tsconfig.json
+++ b/libs/ui/tsconfig.json
@@ -13,6 +13,8 @@
"jsxImportSource": "@emotion/react",
"baseUrl": "./",
"paths": {
+ "gd-design-core": ["../design-core/src/index.ts"],
+ "gd-design-core/*": ["../design-core/src/*"],
"@/*": ["src/*"],
"@types": ["src/types/"],
"@types/*": ["src/types/*"],
diff --git a/libs/ui/tsconfig.lib.json b/libs/ui/tsconfig.lib.json
index bd30911..2da4c84 100644
--- a/libs/ui/tsconfig.lib.json
+++ b/libs/ui/tsconfig.lib.json
@@ -14,6 +14,8 @@
// Aliases
"baseUrl": ".",
"paths": {
+ "gd-design-core": ["../design-core/src/index.ts"],
+ "gd-design-core/*": ["../design-core/src/*"],
"@types": ["src/types/"],
"@types/*": ["src/types/*"],
"@constants": ["src/constants/"],
@@ -60,5 +62,6 @@
"**/*.integration.tsx",
"**/*.integration.helpers.tsx"
],
- "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx", "src/**/*.json"]
+ "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx", "src/**/*.json"],
+ "references": [{ "path": "../design-core/tsconfig.lib.json" }]
}
diff --git a/libs/ui/vitest.alias.ts b/libs/ui/vitest.alias.ts
index 978312d..b150b5d 100644
--- a/libs/ui/vitest.alias.ts
+++ b/libs/ui/vitest.alias.ts
@@ -1,6 +1,7 @@
import * as path from 'path';
export const sharedAlias = {
+ 'gd-design-core': path.resolve(__dirname, '../design-core/src/index.ts'),
'@': path.resolve(__dirname, './src'),
'@types': path.resolve(__dirname, './src/types/'),
'@constants': path.resolve(__dirname, './src/constants/'),
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 c61c2c2..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,19 +99,36 @@ 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 contains native stories, Controls, and docs for all eight
+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 |
-| [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` |
-| [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` |
-| [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 |
-| [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 }` |
+| 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
@@ -125,12 +142,11 @@ 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 multiple/searchable Select behavior and every Typography variant/display size in Chromium.
+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 plus input, checkbox,
-select, counter, and menu interactions in Chromium. The existing Angular/Vue harness
-workflow below remains available until the later integration-check cleanup.
+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 = {
+
+
Stories consume custom elements directly, without React adapters, using the production defaultTheme property and built stylesheet/fonts.
Use Controls to explore properties. Interactive stories display native gd-input / gd-change event details below the element. Objects such as theme and items are JavaScript properties, not string attributes.
-
Menu demonstrates trigger/content slots, selection, Escape and outside-click dismissal. Counter demonstrates bounded quantity changes. Other components are intentionally postponed.
-
The Angular and Vue integration checks remain in place; harness cleanup is a later phase.
+
Stories mirror the published React Storybook inventory. Menu demonstrates trigger/content slots, selection, Escape and outside-click dismissal. Counter demonstrates bounded quantity changes. Form controls expose native input/change details, while layout and display atoms demonstrate their token-backed variants and semantic output.
+
The Angular and Vue integration harnesses exercise the same custom-element package without framework adapters.
Stories consume custom elements directly, without React adapters, using the production defaultTheme property and built stylesheet/fonts.
-
Use Controls to explore properties. Interactive stories display native gd-input / gd-change event details below the element. Objects such as theme and items are JavaScript properties, not string attributes.
-
Stories mirror the published React Storybook inventory. Menu demonstrates trigger/content slots, selection, Escape and outside-click dismissal. Counter demonstrates bounded quantity changes. Form controls expose native input/change details, while layout and display atoms demonstrate their token-backed variants and semantic output.
-
The Angular and Vue integration harnesses exercise the same custom-element package without framework adapters.
+
+
+
+
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.
+
+
+ Component names start with gd-, for example <gd-button>,
+ <gd-input>, and <gd-select>. Browse the Atoms and
+ Molecules sections to inspect each component's properties, variants, events, slots,
+ accessibility behavior, and token defaults.
+
+
+
Install and register
+
Install the component package and the GridKit design tokens in your application:
+
npm install web-components gd-design-library
+
+ Import the package once in your application entry point. The import registers every
+ gd-* custom element with the browser.
+
+
import 'web-components';
+import { defaultTheme } from 'gd-design-library/tokens';
+
+
How values and events work
+
+
Pass text, numbers, and booleans through HTML attributes or framework bindings.
+
Assign objects, arrays, functions, controlled values, and the theme as JavaScript properties.
+
Listen for native events such as gd-input and gd-change.
+
Read component data from event.detail.
+
Use default and named slots for labels, icons, custom initiators, and other composed content.
+
+
+ The theme is an object, so it must be assigned as a property. Writing
+ theme="defaultTheme" in HTML passes only a string and does not apply the theme.
+
+ Import the package in main.ts, enable custom elements with
+ CUSTOM_ELEMENTS_SCHEMA, and use Angular property and event bindings. Property bindings are
+ required for themes, arrays, objects, and controlled values.
+
+ Import the package in the application entry point. In Vue templates, use .prop when a
+ value must be assigned to the custom element as a JavaScript property, and use Vue's normal
+ @event syntax for native custom events.
+
+
// main.ts
+import { createApp } from 'vue';
+import 'web-components';
+import App from './App.vue';
+
+createApp(App).mount('#app');
+ Open a component story and use Controls to change its public properties. Interactive
+ stories display emitted event details next to the component. Each documentation page also includes
+ accessibility examples and the default design tokens used by that component.
+
+
`;
return page;
},
From 082889f5f6bc207e13492017456ad9015587af35 Mon Sep 17 00:00:00 2001
From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com>
Date: Thu, 1 Oct 2026 11:23:22 +0200
Subject: [PATCH 09/10] test: update web components overview smoke check
---
bin/storybook/smoke-test.mjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bin/storybook/smoke-test.mjs b/bin/storybook/smoke-test.mjs
index 07a15a5..eac2bde 100644
--- a/bin/storybook/smoke-test.mjs
+++ b/bin/storybook/smoke-test.mjs
@@ -98,7 +98,7 @@ 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();
From f75829167c5878feeac007aad0994b2286195879 Mon Sep 17 00:00:00 2001
From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com>
Date: Thu, 1 Oct 2026 15:06:15 +0200
Subject: [PATCH 10/10] fix: render web component token stories
---
bin/storybook/native-checks.mjs | 15 ++++
libs/web-components/stories/Avatar.stories.ts | 4 +-
libs/web-components/stories/Badge.stories.ts | 8 +-
libs/web-components/stories/Box.stories.ts | 8 +-
libs/web-components/stories/Button.stories.ts | 4 +-
.../stories/Checkbox.stories.ts | 4 +-
.../web-components/stories/Counter.stories.ts | 4 +-
libs/web-components/stories/Icon.stories.ts | 8 +-
libs/web-components/stories/Image.stories.ts | 8 +-
libs/web-components/stories/Input.stories.ts | 4 +-
.../stories/InputFile.stories.ts | 8 +-
libs/web-components/stories/Label.stories.ts | 8 +-
libs/web-components/stories/Link.stories.ts | 8 +-
libs/web-components/stories/Loader.stories.ts | 8 +-
libs/web-components/stories/Menu.stories.ts | 4 +-
libs/web-components/stories/Select.stories.ts | 4 +-
.../stories/Separator.stories.ts | 8 +-
.../stories/Skeleton.stories.ts | 8 +-
libs/web-components/stories/Slider.stories.ts | 8 +-
.../stories/SliderDots.stories.ts | 8 +-
libs/web-components/stories/Switch.stories.ts | 8 +-
.../stories/Textarea.stories.ts | 8 +-
libs/web-components/stories/Toggle.stories.ts | 8 +-
.../stories/Typography.stories.ts | 4 +-
.../web-components/stories/Wrapper.stories.ts | 8 +-
libs/web-components/stories/helpers.ts | 81 +++++++++++++++++++
26 files changed, 144 insertions(+), 112 deletions(-)
diff --git a/bin/storybook/native-checks.mjs b/bin/storybook/native-checks.mjs
index 343da5f..6a58332 100644
--- a/bin/storybook/native-checks.mjs
+++ b/bin/storybook/native-checks.mjs
@@ -47,6 +47,21 @@ export async function checkNativeStories(page, base) {
'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();
diff --git a/libs/web-components/stories/Avatar.stories.ts b/libs/web-components/stories/Avatar.stories.ts
index b75e98b..ab7fee1 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 { element, portrait } from './helpers';
+import { defaultTokenViewer, element, portrait } from './helpers';
type Args = Pick;
const meta = {
@@ -65,4 +65,4 @@ export const WithIcon: Story = SlottedFallback;
export const WithImageAndFallback: Story = { args: { src: portrait, fallback: 'AL' } };
export const WithDifferentSize: Story = AllSizes;
export const WithAccessibility: Story = { args: { alt: 'Ada Lovelace profile photo' } };
-export const DefaultTokens: Story = { render: () => document.createElement('pre') };
+export const DefaultTokens: Story = { render: () => defaultTokenViewer('avatar') };
diff --git a/libs/web-components/stories/Badge.stories.ts b/libs/web-components/stories/Badge.stories.ts
index 13ebc15..a97549d 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 { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick;
const meta = {
@@ -59,9 +59,5 @@ export const WithAccessibility: Story = {
},
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.badge';
- return pre;
- },
+ render: () => defaultTokenViewer('badge'),
};
diff --git a/libs/web-components/stories/Box.stories.ts b/libs/web-components/stories/Box.stories.ts
index f6f4ea7..fa1d80c 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 { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick;
const meta = {
@@ -60,9 +60,5 @@ export const WithAccessibility: Story = {
},
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.box';
- return pre;
- },
+ render: () => defaultTokenViewer('box'),
};
diff --git a/libs/web-components/stories/Button.stories.ts b/libs/web-components/stories/Button.stories.ts
index 190dacb..9f94f2b 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 { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick<
GdButton,
@@ -96,4 +96,4 @@ export const CustomStyledButton: Story = {
export const RealWorldExamples: Story = AllVariants;
export const IsLoading: Story = Loading;
export const WithAccessibility: Story = { args: { ariaLabel: 'Save changes', label: 'Save' } };
-export const DefaultTokens: Story = { render: () => document.createElement('pre') };
+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 8a7d775..dfd56cb 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 { element, observed } from './helpers';
+import { defaultTokenViewer, element, observed } from './helpers';
type Args = Pick;
const meta = {
@@ -40,4 +40,4 @@ export const Sizes: Story = {
},
};
export const Controlled: Story = Checked;
-export const DefaultTokens: Story = { render: () => document.createElement('pre') };
+export const DefaultTokens: Story = { render: () => defaultTokenViewer('checkbox') };
diff --git a/libs/web-components/stories/Counter.stories.ts b/libs/web-components/stories/Counter.stories.ts
index e64586e..77dd3f1 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 { element, observed } from './helpers';
+import { defaultTokenViewer, element, observed } from './helpers';
type Args = Pick;
const meta = {
@@ -29,4 +29,4 @@ 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: () => document.createElement('pre') };
+export const DefaultTokens: Story = { render: () => defaultTokenViewer('counter') };
diff --git a/libs/web-components/stories/Icon.stories.ts b/libs/web-components/stories/Icon.stories.ts
index 744356b..5a68ea6 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 { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick;
const names = Object.keys(iconCatalog) as GridKitIconName[];
@@ -65,9 +65,5 @@ export const WithAccessibility: Story = {
},
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.icon';
- return pre;
- },
+ render: () => defaultTokenViewer('icon'),
};
diff --git a/libs/web-components/stories/Image.stories.ts b/libs/web-components/stories/Image.stories.ts
index e2e1ac5..08fef8f 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 { element, portrait } from './helpers';
+import { defaultTokenViewer, element, portrait } from './helpers';
type Args = Pick;
const meta = {
@@ -84,9 +84,5 @@ export const WithCaptionAsProp: Story = {
};
export const WithAccessibility: Story = { args: { alt: 'Portrait of Ada Lovelace', caption: 'Ada Lovelace' } };
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.image';
- return pre;
- },
+ render: () => defaultTokenViewer('image'),
};
diff --git a/libs/web-components/stories/Input.stories.ts b/libs/web-components/stories/Input.stories.ts
index c82e0a9..600694b 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 { element, observed } from './helpers';
+import { defaultTokenViewer, element, observed } from './helpers';
type Args = Pick<
GdInput,
@@ -136,4 +136,4 @@ export const RadioGroupWithLabel: Story = InputTypes;
export const WithAccessibility: Story = {
args: { required: true, label: 'Email address', helperText: 'Required field' },
};
-export const DefaultTokens: Story = { render: () => document.createElement('pre') };
+export const DefaultTokens: Story = { render: () => defaultTokenViewer('input') };
diff --git a/libs/web-components/stories/InputFile.stories.ts b/libs/web-components/stories/InputFile.stories.ts
index 0d39173..12bd498 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 { element, observed } from './helpers';
+import { defaultTokenViewer, element, observed } from './helpers';
type Args = Pick;
const meta = {
@@ -58,9 +58,5 @@ export const WithAccessibility: Story = {
},
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.inputfile';
- return pre;
- },
+ render: () => defaultTokenViewer('inputfile'),
};
diff --git a/libs/web-components/stories/Label.stories.ts b/libs/web-components/stories/Label.stories.ts
index f38fa34..bef2f96 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 { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick;
const meta = {
@@ -49,9 +49,5 @@ export const WithAccessibility: Story = {
},
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.label';
- return pre;
- },
+ render: () => defaultTokenViewer('label'),
};
diff --git a/libs/web-components/stories/Link.stories.ts b/libs/web-components/stories/Link.stories.ts
index c472061..127bde0 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 { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick<
GdLink,
@@ -88,9 +88,5 @@ export const WithAccessibility: Story = {
},
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.link';
- return pre;
- },
+ render: () => defaultTokenViewer('link'),
};
diff --git a/libs/web-components/stories/Loader.stories.ts b/libs/web-components/stories/Loader.stories.ts
index d156ce8..683b4b2 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 { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick;
const meta = {
@@ -72,9 +72,5 @@ export const WithAccessibility: Story = {
},
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.loader';
- return pre;
- },
+ render: () => defaultTokenViewer('loader'),
};
diff --git a/libs/web-components/stories/Menu.stories.ts b/libs/web-components/stories/Menu.stories.ts
index 4a08d89..7d6b6cc 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 { element, observed } from './helpers';
+import { defaultTokenViewer, element, observed } from './helpers';
type Args = Pick;
const placements = ['top-left', 'top-right', 'bottom-left', 'bottom-right'] as const;
@@ -78,4 +78,4 @@ 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: () => document.createElement('pre') };
+export const DefaultTokens: Story = { render: () => defaultTokenViewer('menu') };
diff --git a/libs/web-components/stories/Select.stories.ts b/libs/web-components/stories/Select.stories.ts
index d652673..d3c3e1f 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 { element, observed, items } from './helpers';
+import { defaultTokenViewer, element, observed, items } from './helpers';
type Args = Pick<
GdSelect,
@@ -111,4 +111,4 @@ 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: () => document.createElement('pre') };
+export const DefaultTokens: Story = { render: () => defaultTokenViewer('select') };
diff --git a/libs/web-components/stories/Separator.stories.ts b/libs/web-components/stories/Separator.stories.ts
index ea59093..cf62b70 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 { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick<
GdSeparator,
@@ -109,10 +109,6 @@ export const WithAccessibility: Story = {
tags: ['a11y'],
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.separator';
- return pre;
- },
+ render: () => defaultTokenViewer('separator'),
parameters: { layout: 'padded' },
};
diff --git a/libs/web-components/stories/Skeleton.stories.ts b/libs/web-components/stories/Skeleton.stories.ts
index f6fa6a5..c56ad0c 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 { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick<
GdSkeleton,
@@ -88,10 +88,6 @@ export const WithAccessibility: Story = {
tags: ['a11y'],
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.skeleton';
- return pre;
- },
+ render: () => defaultTokenViewer('skeleton'),
parameters: { layout: 'padded' },
};
diff --git a/libs/web-components/stories/Slider.stories.ts b/libs/web-components/stories/Slider.stories.ts
index df91869..2d20590 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 { element, observed } from './helpers';
+import { defaultTokenViewer, element, observed } from './helpers';
type Args = Pick;
const meta = {
@@ -50,9 +50,5 @@ export const WithAccessibility: Story = {
tags: ['a11y'],
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.slider';
- return pre;
- },
+ render: () => defaultTokenViewer('slider'),
};
diff --git a/libs/web-components/stories/SliderDots.stories.ts b/libs/web-components/stories/SliderDots.stories.ts
index a21eba9..9fbcdb4 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 { element, observed } from './helpers';
+import { defaultTokenViewer, element, observed } from './helpers';
type Args = Pick;
const meta = {
@@ -48,9 +48,5 @@ export const ManyDots: Story = {
parameters: { docs: { description: { story: 'Ten dots with the fifth active.' } } },
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.sliderDots';
- return pre;
- },
+ render: () => defaultTokenViewer('sliderDots'),
};
diff --git a/libs/web-components/stories/Switch.stories.ts b/libs/web-components/stories/Switch.stories.ts
index 051011a..100d545 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 { element, observed } from './helpers';
+import { defaultTokenViewer, element, observed } from './helpers';
type Args = Pick;
const meta = {
@@ -61,9 +61,5 @@ export const WithAccessibility: Story = {
tags: ['a11y'],
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.switchToken';
- return pre;
- },
+ render: () => defaultTokenViewer('switch', 'switchToken'),
};
diff --git a/libs/web-components/stories/Textarea.stories.ts b/libs/web-components/stories/Textarea.stories.ts
index ff39cc8..e5e41d8 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 { element, observed } from './helpers';
+import { defaultTokenViewer, element, observed } from './helpers';
type Args = Pick<
GdTextarea,
@@ -151,9 +151,5 @@ export const WithAccessibility: Story = {
tags: ['a11y'],
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.textarea';
- return pre;
- },
+ render: () => defaultTokenViewer('textarea'),
};
diff --git a/libs/web-components/stories/Toggle.stories.ts b/libs/web-components/stories/Toggle.stories.ts
index b0c929f..bdda506 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 { element, observed } from './helpers';
+import { defaultTokenViewer, element, observed } from './helpers';
type Args = Pick;
const items = ['Option 1', 'Option 2', 'Option 3'];
@@ -54,9 +54,5 @@ export const WithAccessibility: Story = {
tags: ['a11y'],
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.switchToggle';
- return pre;
- },
+ render: () => defaultTokenViewer('switchToggle'),
};
diff --git a/libs/web-components/stories/Typography.stories.ts b/libs/web-components/stories/Typography.stories.ts
index a309237..087c20f 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 { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick & { text: string };
const variants = [
@@ -133,4 +133,4 @@ 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 DefaultTokens: Story = { render: () => document.createElement('pre') };
+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 f286534..7633fa0 100644
--- a/libs/web-components/stories/Wrapper.stories.ts
+++ b/libs/web-components/stories/Wrapper.stories.ts
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/web-components-vite';
import type { GdWrapper } from '../src';
-import { element } from './helpers';
+import { defaultTokenViewer, element } from './helpers';
type Args = Pick & { text: string };
const meta = {
@@ -71,9 +71,5 @@ export const WithAccessibility: Story = {
tags: ['a11y'],
};
export const DefaultTokens: Story = {
- render: () => {
- const pre = document.createElement('pre');
- pre.textContent = 'defaultTheme.wrapper';
- return pre;
- },
+ render: () => defaultTokenViewer('wrapper'),
};
diff --git a/libs/web-components/stories/helpers.ts b/libs/web-components/stories/helpers.ts
index 733d693..2b42951 100644
--- a/libs/web-components/stories/helpers.ts
+++ b/libs/web-components/stories/helpers.ts
@@ -149,6 +149,87 @@ export function element(tag: T, props: Partial {
+ const expanded = keyElement.getAttribute('aria-expanded') === 'true';
+ keyElement.setAttribute('aria-expanded', String(!expanded));
+ brace.textContent = expanded ? '{...}' : '{';
+ content.hidden = expanded;
+ closingBrace.hidden = expanded;
+ };
+ keyElement.addEventListener('click', toggle);
+ keyElement.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ toggle();
+ }
+ });
+ line.append(brace, content, closingBrace);
+ } else {
+ line.append(tokenNode(childValue, indent + 1));
+ }
+ node.append(line);
+ }
+ return node;
+}
+
+/** Render the same default-theme branch shown by the React Storybook TokenViewer. */
+export function defaultTokenViewer(
+ label: string,
+ themeToken: keyof typeof defaultTheme = label as keyof typeof defaultTheme
+) {
+ const viewer = document.createElement('div');
+ viewer.style.cssText = tokenViewerStyles;
+ viewer.tabIndex = 0;
+ viewer.setAttribute('role', 'region');
+ viewer.setAttribute('aria-label', 'Token viewer');
+ viewer.append(tokenNode({ [label]: defaultTheme[themeToken] }));
+ return viewer;
+}
+
/** Each render owns its output and listeners; no state leaks between stories. */
export function observed(node: HTMLElement, eventName: string, initial: unknown) {
const section = document.createElement('section');