From af48f53c35909515bff4e7e56f7e39912c2bf1ea Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:24:30 +0200 Subject: [PATCH 01/14] feat: CTORNDSD-675 web components --- libs/ui/llms.txt | 2 +- libs/ui/src/ai/README.md | 2 +- libs/ui/src/tokens/menu.ts | 39 ++ libs/web-components/harness/GdAvatarReact.tsx | 2 +- libs/web-components/harness/GdButtonReact.tsx | 2 +- .../harness/GdCheckboxReact.tsx | 2 +- libs/web-components/harness/GdInputReact.tsx | 2 +- libs/web-components/harness/GdMenuReact.tsx | 10 + libs/web-components/harness/GdSelectReact.tsx | 2 +- .../harness/GdTypographyReact.tsx | 2 +- .../web-components/harness/fidelity-check.tsx | 28 ++ .../harness/form-participation-check.ts | 4 +- .../harness/gd-form-elements-jsx.d.ts | 1 + libs/web-components/harness/perf-check.tsx | 5 +- .../harness/remaining-findings-repro.tsx | 2 +- .../harness/shell-isolation-check.tsx | 2 +- .../harness/ssr-dsd-hydrate-check.ts | 19 +- .../v1/gd-menu.react-component-ir.json | 347 ++++++++++++++++++ .../migration-ir/v1/ported-elements.json | 47 +++ .../scripts/check-ported-elements.mjs | 40 ++ libs/web-components/scripts/demo-index.mjs | 2 +- .../scripts/run-ssr-dsd-check.mjs | 9 +- libs/web-components/scripts/ssr-dsd-render.ts | 28 +- .../{ => atoms}/gd-avatar/gd-avatar.spec.ts | 0 .../{ => atoms}/gd-avatar/gd-avatar.ts | 0 .../{ => atoms}/gd-button/gd-button.spec.ts | 0 .../{ => atoms}/gd-button/gd-button.ts | 0 .../gd-checkbox/gd-checkbox.spec.ts | 0 .../{ => atoms}/gd-checkbox/gd-checkbox.ts | 0 .../{ => atoms}/gd-input/gd-input.spec.ts | 0 .../{ => atoms}/gd-input/gd-input.ts | 0 .../{ => atoms}/gd-select/gd-select.ts | 10 +- .../gd-typography/gd-typography.ts | 0 .../molecules/gd-menu/gd-menu.react.spec.tsx | 96 +++++ .../molecules/gd-menu/gd-menu.spec.ts | 161 ++++++++ .../components/molecules/gd-menu/gd-menu.ts | 244 ++++++++++++ libs/web-components/src/index.ts | 13 +- libs/web-components/vitest.config.ts | 9 +- package.json | 3 +- 39 files changed, 1094 insertions(+), 41 deletions(-) create mode 100644 libs/web-components/harness/GdMenuReact.tsx create mode 100644 libs/web-components/migration-ir/v1/gd-menu.react-component-ir.json create mode 100644 libs/web-components/migration-ir/v1/ported-elements.json create mode 100644 libs/web-components/scripts/check-ported-elements.mjs rename libs/web-components/src/components/{ => atoms}/gd-avatar/gd-avatar.spec.ts (100%) rename libs/web-components/src/components/{ => atoms}/gd-avatar/gd-avatar.ts (100%) rename libs/web-components/src/components/{ => atoms}/gd-button/gd-button.spec.ts (100%) rename libs/web-components/src/components/{ => atoms}/gd-button/gd-button.ts (100%) rename libs/web-components/src/components/{ => atoms}/gd-checkbox/gd-checkbox.spec.ts (100%) rename libs/web-components/src/components/{ => atoms}/gd-checkbox/gd-checkbox.ts (100%) rename libs/web-components/src/components/{ => atoms}/gd-input/gd-input.spec.ts (100%) rename libs/web-components/src/components/{ => atoms}/gd-input/gd-input.ts (100%) rename libs/web-components/src/components/{ => atoms}/gd-select/gd-select.ts (97%) rename libs/web-components/src/components/{ => atoms}/gd-typography/gd-typography.ts (100%) create mode 100644 libs/web-components/src/components/molecules/gd-menu/gd-menu.react.spec.tsx create mode 100644 libs/web-components/src/components/molecules/gd-menu/gd-menu.spec.ts create mode 100644 libs/web-components/src/components/molecules/gd-menu/gd-menu.ts diff --git a/libs/ui/llms.txt b/libs/ui/llms.txt index 3e4bc8c..030d729 100644 --- a/libs/ui/llms.txt +++ b/libs/ui/llms.txt @@ -84,7 +84,7 @@ Pre-built indexes for quick discovery: > Run `yarn build:ui` to regenerate. Do not edit the block between the markers manually. -66 components total — generated from libs/ui/src/ai/schemas/ on 2026-09-21. +66 components total — generated from libs/ui/src/ai/schemas/ on 2026-09-23. ## Other diff --git a/libs/ui/src/ai/README.md b/libs/ui/src/ai/README.md index 6a01a4d..476ce96 100644 --- a/libs/ui/src/ai/README.md +++ b/libs/ui/src/ai/README.md @@ -594,7 +594,7 @@ yarn test:ai:watch -_66 components total — generated from `libs/ui/src/ai/schemas/` on 2026-09-21._ +_66 components total — generated from `libs/ui/src/ai/schemas/` on 2026-09-23._ ### Other diff --git a/libs/ui/src/tokens/menu.ts b/libs/ui/src/tokens/menu.ts index cbe22cb..776e3d2 100644 --- a/libs/ui/src/tokens/menu.ts +++ b/libs/ui/src/tokens/menu.ts @@ -20,4 +20,43 @@ export const menu = { offsetX: 4, offsetY: 4, }, + webComponent: { + host: { display: 'inline-block' }, + trigger: { + default: { + alignItems: 'center', + background: 'none', + border: 0, + color: 'inherit', + cursor: 'pointer', + display: 'inline-flex', + font: 'inherit', + padding: 0, + }, + focusVisible: { + outline: (theme: Record) => `2px solid ${get(theme, 'colors.border.focus')}`, + outlineOffset: '2px', + }, + }, + content: { + border: 0, + boxSizing: 'border-box', + // Native popovers default to `inset: 0` with auto margins. Reset both so the + // runtime top/left geometry positions the menu like the React portal. + inset: 'auto', + // A positioned block with auto dimensions can expand to the viewport before Menu measures + // it. Keep the native popover intrinsic so the runtime overflow clamp positions the menu + // beside its trigger instead of pinning a viewport-sized box to the top-left. + height: 'fit-content', + // The React harness supplies one block wrapper for the content slot. A block popover lets + // that wrapper fill the viewport; inline-block preserves intrinsic sizing for both direct + // items and a slotted wrapper. + display: 'inline-block', + margin: 0, + overflow: 'auto', + padding: 0, + position: 'fixed', + width: 'fit-content', + }, + }, }; diff --git a/libs/web-components/harness/GdAvatarReact.tsx b/libs/web-components/harness/GdAvatarReact.tsx index 8518f10..213741f 100644 --- a/libs/web-components/harness/GdAvatarReact.tsx +++ b/libs/web-components/harness/GdAvatarReact.tsx @@ -1,6 +1,6 @@ import { createComponent } from '@lit/react'; import * as React from 'react'; -import { GdAvatar as GdAvatarElement } from '../src/components/gd-avatar/gd-avatar'; +import { GdAvatar as GdAvatarElement } from '../src/components/atoms/gd-avatar/gd-avatar'; /** Typed React adapter. Avatar emits no custom events; native click is consumed directly. */ export const GdAvatar = createComponent({ tagName: 'gd-avatar', elementClass: GdAvatarElement, react: React }); diff --git a/libs/web-components/harness/GdButtonReact.tsx b/libs/web-components/harness/GdButtonReact.tsx index 54b0b5c..4192e12 100644 --- a/libs/web-components/harness/GdButtonReact.tsx +++ b/libs/web-components/harness/GdButtonReact.tsx @@ -1,6 +1,6 @@ import { createComponent } from '@lit/react'; import * as React from 'react'; -import { GdButton as GdButtonElement } from '../src/components/gd-button/gd-button'; +import { GdButton as GdButtonElement } from '../src/components/atoms/gd-button/gd-button'; /** * React consumption harness Scope — proves `gd-button` is usable from React via diff --git a/libs/web-components/harness/GdCheckboxReact.tsx b/libs/web-components/harness/GdCheckboxReact.tsx index 9135d35..740b926 100644 --- a/libs/web-components/harness/GdCheckboxReact.tsx +++ b/libs/web-components/harness/GdCheckboxReact.tsx @@ -1,6 +1,6 @@ import { createComponent } from '@lit/react'; import * as React from 'react'; -import { GdCheckbox as GdCheckboxElement } from '../src/components/gd-checkbox/gd-checkbox'; +import { GdCheckbox as GdCheckboxElement } from '../src/components/atoms/gd-checkbox/gd-checkbox'; /** * React consumption harness for `gd-checkbox` — the React-19-scope nuance from the Migration diff --git a/libs/web-components/harness/GdInputReact.tsx b/libs/web-components/harness/GdInputReact.tsx index 7291835..bf087f1 100644 --- a/libs/web-components/harness/GdInputReact.tsx +++ b/libs/web-components/harness/GdInputReact.tsx @@ -1,6 +1,6 @@ import { createComponent } from '@lit/react'; import * as React from 'react'; -import { GdInput as GdInputElement } from '../src/components/gd-input/gd-input'; +import { GdInput as GdInputElement } from '../src/components/atoms/gd-input/gd-input'; /** * React consumption harness for `gd-input` — the cursor-jump repro harness (see diff --git a/libs/web-components/harness/GdMenuReact.tsx b/libs/web-components/harness/GdMenuReact.tsx new file mode 100644 index 0000000..4662176 --- /dev/null +++ b/libs/web-components/harness/GdMenuReact.tsx @@ -0,0 +1,10 @@ +import { createComponent } from '@lit/react'; +import * as React from 'react'; +import { GdMenu as GdMenuElement } from '../src/components/molecules/gd-menu/gd-menu'; + +export const GdMenu = createComponent({ + tagName: 'gd-menu', + elementClass: GdMenuElement, + react: React, + events: { onGdChange: 'gd-change' }, +}); diff --git a/libs/web-components/harness/GdSelectReact.tsx b/libs/web-components/harness/GdSelectReact.tsx index 409f445..88f2f5f 100644 --- a/libs/web-components/harness/GdSelectReact.tsx +++ b/libs/web-components/harness/GdSelectReact.tsx @@ -1,6 +1,6 @@ import { createComponent } from '@lit/react'; import * as React from 'react'; -import { GdSelect as GdSelectElement } from '../src/components/gd-select/gd-select'; +import { GdSelect as GdSelectElement } from '../src/components/atoms/gd-select/gd-select'; /** * React consumption harness for `gd-select` — object/array props (`items`, `value`) pass diff --git a/libs/web-components/harness/GdTypographyReact.tsx b/libs/web-components/harness/GdTypographyReact.tsx index c75534e..574a24e 100644 --- a/libs/web-components/harness/GdTypographyReact.tsx +++ b/libs/web-components/harness/GdTypographyReact.tsx @@ -1,6 +1,6 @@ import { createComponent } from '@lit/react'; import * as React from 'react'; -import { GdTypography as GdTypographyElement } from '../src/components/gd-typography/gd-typography'; +import { GdTypography as GdTypographyElement } from '../src/components/atoms/gd-typography/gd-typography'; /** * React consumption harness for `gd-typography`. No custom events to map — Typography is diff --git a/libs/web-components/harness/fidelity-check.tsx b/libs/web-components/harness/fidelity-check.tsx index 14c7280..ccf97ce 100644 --- a/libs/web-components/harness/fidelity-check.tsx +++ b/libs/web-components/harness/fidelity-check.tsx @@ -20,6 +20,7 @@ import { GdTypography } from './GdTypographyReact'; import { GdInput } from './GdInputReact'; import { GdSelect } from './GdSelectReact'; import { GdAvatar } from './GdAvatarReact'; +import { GdMenu } from './GdMenuReact'; /** * Visual-fidelity verification harness — renders each of the 6 ported atoms with @@ -174,6 +175,32 @@ function AvatarSection() { ); } +function MenuSection() { + const [value, setValue] = useState('None'); + return ( +
+ { + const { data } = (event as CustomEvent<{ data: { name?: string; value: string } }>).detail; + setValue(`${data.name ?? data.value} (${data.value})`); + }} + > + Actions ▾ +
+ + +
+
+ Selected: {value} +
+ ); +} + function App() { return (
@@ -183,6 +210,7 @@ function App() { +
); } diff --git a/libs/web-components/harness/form-participation-check.ts b/libs/web-components/harness/form-participation-check.ts index ca0d01c..77b6e54 100644 --- a/libs/web-components/harness/form-participation-check.ts +++ b/libs/web-components/harness/form-participation-check.ts @@ -16,8 +16,8 @@ */ import '../src/index'; import { defaultTheme } from 'gd-design-library/tokens'; -import type { GdCheckbox } from '../src/components/gd-checkbox/gd-checkbox'; -import type { GdInput } from '../src/components/gd-input/gd-input'; +import type { GdCheckbox } from '../src/components/atoms/gd-checkbox/gd-checkbox'; +import type { GdInput } from '../src/components/atoms/gd-input/gd-input'; const root = document.getElementById('root')!; diff --git a/libs/web-components/harness/gd-form-elements-jsx.d.ts b/libs/web-components/harness/gd-form-elements-jsx.d.ts index 7e2a292..d626e39 100644 --- a/libs/web-components/harness/gd-form-elements-jsx.d.ts +++ b/libs/web-components/harness/gd-form-elements-jsx.d.ts @@ -11,6 +11,7 @@ declare global { 'gd-input': React.DetailedHTMLProps, HTMLElement>; 'gd-select': React.DetailedHTMLProps, HTMLElement>; 'gd-avatar': React.DetailedHTMLProps, HTMLElement>; + 'gd-menu': React.DetailedHTMLProps, HTMLElement>; } } } diff --git a/libs/web-components/harness/perf-check.tsx b/libs/web-components/harness/perf-check.tsx index 6f935e1..2d8b84d 100644 --- a/libs/web-components/harness/perf-check.tsx +++ b/libs/web-components/harness/perf-check.tsx @@ -1,3 +1,6 @@ +// Harness-only built stylesheet: the package export resolves only after the UI build and the +// harness intentionally compares against that emitted visual baseline. +// eslint-disable-next-line @nx/enforce-module-boundaries import '../../../dist/libs/ui/styles.css'; import * as React from 'react'; import { createRoot } from 'react-dom/client'; @@ -11,7 +14,7 @@ import { defaultTheme } from 'gd-design-library/tokens'; import { Button } from '../../ui/src/components/atoms/Button/Button'; // eslint-disable-next-line @nx/enforce-module-boundaries import { ThemeProvider } from '../../ui/src/hooks/useTheme/useTheme'; -import '../src/components/gd-button/gd-button'; +import '../src/components/atoms/gd-button/gd-button'; import './gd-button-shell'; /** diff --git a/libs/web-components/harness/remaining-findings-repro.tsx b/libs/web-components/harness/remaining-findings-repro.tsx index 16a996c..98a9cf3 100644 --- a/libs/web-components/harness/remaining-findings-repro.tsx +++ b/libs/web-components/harness/remaining-findings-repro.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useState } from 'react'; import { createRoot } from 'react-dom/client'; -import '../src/components/gd-typography/gd-typography'; +import '../src/components/atoms/gd-typography/gd-typography'; import { GdInput } from './GdInputReact'; import { GdSelect } from './GdSelectReact'; diff --git a/libs/web-components/harness/shell-isolation-check.tsx b/libs/web-components/harness/shell-isolation-check.tsx index a5c4fe6..dffee0f 100644 --- a/libs/web-components/harness/shell-isolation-check.tsx +++ b/libs/web-components/harness/shell-isolation-check.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import { CacheProvider, Global, css } from '@emotion/react'; import createCache from '@emotion/cache'; -import '../src/components/gd-button/gd-button'; +import '../src/components/atoms/gd-button/gd-button'; import './gd-button-shell'; declare global { diff --git a/libs/web-components/harness/ssr-dsd-hydrate-check.ts b/libs/web-components/harness/ssr-dsd-hydrate-check.ts index a08f8ea..0c51a47 100644 --- a/libs/web-components/harness/ssr-dsd-hydrate-check.ts +++ b/libs/web-components/harness/ssr-dsd-hydrate-check.ts @@ -1,5 +1,10 @@ -import '../src/components/gd-button/gd-button'; -import '../src/components/gd-typography/gd-typography'; +import '../src/components/atoms/gd-button/gd-button'; +import '../src/components/atoms/gd-avatar/gd-avatar'; +import '../src/components/atoms/gd-checkbox/gd-checkbox'; +import '../src/components/atoms/gd-input/gd-input'; +import '../src/components/atoms/gd-select/gd-select'; +import '../src/components/atoms/gd-typography/gd-typography'; +import '../src/components/molecules/gd-menu/gd-menu'; import { defaultTheme } from 'gd-design-library/tokens'; /** @@ -34,9 +39,13 @@ window.addEventListener('DOMContentLoaded', () => { // only for this component; JS = fully styled. `gd-typography` is already styled from SSR // because it uses inline styles, so this changes nothing for it. const backgroundBeforeTheme = innerButtonBefore ? getComputedStyle(innerButtonBefore).backgroundColor : null; - document.querySelectorAll('gd-button, gd-typography').forEach((el) => { - el.theme = defaultTheme; - }); + document + .querySelectorAll< + HTMLElement & { theme?: unknown } + >('gd-avatar, gd-button, gd-checkbox, gd-input, gd-menu, gd-select, gd-typography') + .forEach((el) => { + el.theme = defaultTheme; + }); requestAnimationFrame(() => { setTimeout(async () => { diff --git a/libs/web-components/migration-ir/v1/gd-menu.react-component-ir.json b/libs/web-components/migration-ir/v1/gd-menu.react-component-ir.json new file mode 100644 index 0000000..27b722e --- /dev/null +++ b/libs/web-components/migration-ir/v1/gd-menu.react-component-ir.json @@ -0,0 +1,347 @@ +{ + "schemaVersion": "react-component-ir/v1", + "source": "/Users/alekseypovolotskiy/dev/gd/gridkit/libs/ui/src/components/molecules/Menu/Menu.tsx", + "imports": [ + { + "source": "react", + "specifiers": [ + { + "imported": "useEffect", + "local": "useEffect" + }, + { + "imported": "useRef", + "local": "useRef" + }, + { + "imported": "useState", + "local": "useState" + }, + { + "imported": "useImperativeHandle", + "local": "useImperativeHandle" + }, + { + "imported": "useCallback", + "local": "useCallback" + }, + { + "imported": "forwardRef", + "local": "forwardRef" + }, + { + "imported": "KeyboardEvent", + "local": "KeyboardEvent" + }, + { + "imported": "MutableRefObject", + "local": "MutableRefObject" + } + ] + }, + { + "source": "@hooks/useTheme", + "specifiers": [ + { + "imported": "useTheme", + "local": "useTheme" + } + ] + }, + { + "source": "@hooks/useLogger", + "specifiers": [ + { + "imported": "useLogger", + "local": "useLogger" + } + ] + }, + { + "source": "@constants", + "specifiers": [ + { + "imported": "KEYBOARD_KEYS", + "local": "KEYBOARD_KEYS" + } + ] + }, + { + "source": "@utils", + "specifiers": [ + { + "imported": "useAnimationFrame", + "local": "useAnimationFrame" + }, + { + "imported": "get", + "local": "get" + } + ] + }, + { + "source": "@tokens", + "specifiers": [ + { + "imported": "convertToInlineBoxStyles", + "local": "convertToInlineBoxStyles" + } + ] + }, + { + "source": "@components/atoms/Select/hooks", + "specifiers": [ + { + "imported": "SelectContext", + "local": "SelectContext" + } + ] + }, + { + "source": "@components/layout", + "specifiers": [ + { + "imported": "Portal", + "local": "Portal" + } + ] + }, + { + "source": "@components", + "specifiers": [ + { + "imported": "SelectContextType", + "local": "SelectContextType" + }, + { + "imported": "Option", + "local": "Option" + }, + { + "imported": "SelectOnSelect", + "local": "SelectOnSelect" + } + ] + }, + { + "source": "@types", + "specifiers": [ + { + "imported": "InlineBoxStyles", + "local": "InlineBoxStyles" + } + ] + }, + { + "source": "./constants", + "specifiers": [ + { + "imported": "COMPONENT_NAME", + "local": "COMPONENT_NAME" + }, + { + "imported": "EMPTY_CLIENT_RECT", + "local": "EMPTY_CLIENT_RECT" + } + ] + }, + { + "source": "./MenuStyled", + "specifiers": [ + { + "imported": "MenuTriggerStyled", + "local": "MenuTriggerStyled" + }, + { + "imported": "MenuContentStyled", + "local": "MenuContentStyled" + } + ] + }, + { + "source": "./", + "specifiers": [ + { + "imported": "MenuProps", + "local": "MenuProps" + }, + { + "imported": "MenuRef", + "local": "MenuRef" + } + ] + } + ], + "stylingDependencies": [ + { + "kind": "import", + "source": "./MenuStyled", + "loc": { + "start": { + "line": 25, + "column": 0, + "index": 710 + }, + "end": { + "line": 25, + "column": 68, + "index": 778 + } + } + } + ], + "components": [ + { + "name": "Menu", + "props": { + "source": "{\n onSelect,\n children,\n content,\n itemIdentifier = (selected, current) => selected?.value === current?.value,\n closeOnSelect = true,\n minHeight: defaultMinHeight,\n maxHeight: defaultMaxHeight,\n offsetX: defaultOffsetX,\n offsetY: defaultOffsetY,\n placement,\n ...rest\n }", + "bindings": [ + "onSelect", + "children", + "content", + "itemIdentifier", + "closeOnSelect", + "defaultMinHeight", + "defaultMaxHeight", + "defaultOffsetX", + "defaultOffsetY", + "placement", + "rest" + ], + "loc": { + "start": { + "line": 30, + "column": 4, + "index": 886 + }, + "end": { + "line": 42, + "column": 5, + "index": 1213 + } + } + }, + "state": [ + { + "value": "isOpen", + "setter": "setIsOpen", + "initialValue": "false", + "loc": { + "start": { + "line": 48, + "column": 10, + "index": 1318 + }, + "end": { + "line": 48, + "column": 47, + "index": 1355 + } + } + }, + { + "value": "internalValue", + "setter": "setInternalValue", + "initialValue": "undefined", + "loc": { + "start": { + "line": 49, + "column": 10, + "index": 1367 + }, + "end": { + "line": 49, + "column": 64, + "index": 1421 + } + } + }, + { + "value": "apiControlTarget", + "setter": "setApiControlTarget", + "initialValue": "undefined", + "loc": { + "start": { + "line": 50, + "column": 10, + "index": 1433 + }, + "end": { + "line": 50, + "column": 82, + "index": 1505 + } + } + } + ], + "effects": [ + { + "callback": "() => {\n if (isOpen) {\n document.addEventListener('keydown', handleKeyDown as any);\n document.addEventListener('mousedown', handleClickOutside);\n } else {\n document.removeEventListener('keydown', handleKeyDown as any);\n document.removeEventListener('mousedown', handleClickOutside);\n }\n\n return () => {\n document.removeEventListener('keydown', handleKeyDown as any);\n document.removeEventListener('mousedown', handleClickOutside);\n };\n }", + "dependencies": "[isOpen]", + "hasCleanup": true, + "loc": { + "start": { + "line": 208, + "column": 4, + "index": 7078 + }, + "end": { + "line": 221, + "column": 16, + "index": 7606 + } + } + } + ], + "jsxTemplates": [ + { + "type": "JSXElement", + "source": "\n \n {children}\n \n {isOpen && (\n \n \n {content}\n \n \n )}\n ", + "loc": { + "start": { + "line": 280, + "column": 6, + "index": 9537 + }, + "end": { + "line": 296, + "column": 31, + "index": 10148 + } + } + } + ], + "stylingDependencies": [], + "unsupported": [ + { + "kind": "portal", + "source": "\n \n {content}\n \n ", + "loc": { + "start": { + "line": 285, + "column": 10, + "index": 9784 + }, + "end": { + "line": 294, + "column": 19, + "index": 10105 + } + } + } + ], + "loc": { + "start": { + "line": 29, + "column": 2, + "index": 880 + }, + "end": { + "line": 298, + "column": 3, + "index": 10159 + } + } + } + ] +} diff --git a/libs/web-components/migration-ir/v1/ported-elements.json b/libs/web-components/migration-ir/v1/ported-elements.json new file mode 100644 index 0000000..c80fe38 --- /dev/null +++ b/libs/web-components/migration-ir/v1/ported-elements.json @@ -0,0 +1,47 @@ +{ + "schemaVersion": "gridkit-web-component-ports/v1", + "ports": [ + { + "component": "Avatar", + "tag": "gd-avatar", + "category": "atoms", + "reactSource": "libs/ui/src/components/atoms/Avatar/Avatar.tsx" + }, + { + "component": "Button", + "tag": "gd-button", + "category": "atoms", + "reactSource": "libs/ui/src/components/atoms/Button/Button.tsx" + }, + { + "component": "Checkbox", + "tag": "gd-checkbox", + "category": "atoms", + "reactSource": "libs/ui/src/components/atoms/Checkbox/Checkbox.tsx" + }, + { + "component": "Input", + "tag": "gd-input", + "category": "atoms", + "reactSource": "libs/ui/src/components/atoms/Input/Input.tsx" + }, + { + "component": "Select", + "tag": "gd-select", + "category": "atoms", + "reactSource": "libs/ui/src/components/atoms/Select/Select.tsx" + }, + { + "component": "Typography", + "tag": "gd-typography", + "category": "atoms", + "reactSource": "libs/ui/src/components/atoms/Typography/Typography.tsx" + }, + { + "component": "Menu", + "tag": "gd-menu", + "category": "molecules", + "reactSource": "libs/ui/src/components/molecules/Menu/Menu.tsx" + } + ] +} diff --git a/libs/web-components/scripts/check-ported-elements.mjs b/libs/web-components/scripts/check-ported-elements.mjs new file mode 100644 index 0000000..5a4eed1 --- /dev/null +++ b/libs/web-components/scripts/check-ported-elements.mjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node +import { readdir, readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const packageRoot = resolve(import.meta.dirname, '..'); +const componentsRoot = resolve(packageRoot, 'src/components'); +const manifest = JSON.parse(await readFile(resolve(packageRoot, 'migration-ir/v1/ported-elements.json'), 'utf8')); +if (manifest.schemaVersion !== 'gridkit-web-component-ports/v1') { + throw new Error(`Unsupported port manifest: ${manifest.schemaVersion}`); +} +const ports = manifest.ports; + +const discovered = []; +for (const category of ['atoms', 'molecules']) { + for (const entry of await readdir(resolve(componentsRoot, category), { withFileTypes: true })) { + if (entry.isDirectory() && entry.name.startsWith('gd-')) discovered.push({ tag: entry.name, category }); + } +} + +const expected = new Set(ports.map(({ tag, category }) => `${category}/${tag}`)); +const actual = new Set(discovered.map(({ tag, category }) => `${category}/${tag}`)); +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'}.` + ); +} + +const [index, ssr] = await Promise.all([ + readFile(resolve(packageRoot, 'src/index.ts'), 'utf8'), + readFile(resolve(packageRoot, 'scripts/ssr-dsd-render.ts'), 'utf8'), +]); +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(', ')}`); diff --git a/libs/web-components/scripts/demo-index.mjs b/libs/web-components/scripts/demo-index.mjs index b630130..1263621 100644 --- a/libs/web-components/scripts/demo-index.mjs +++ b/libs/web-components/scripts/demo-index.mjs @@ -52,7 +52,7 @@ const demos = [ }, { page: 'fidelity-check.html', - proves: 'All 5 atoms rendered with the real theme, for side-by-side comparison with Storybook', + proves: 'All 6 ported components rendered with the real theme, including gd-menu selection', findings: '§9, §16', needs: 'dist/libs/ui/styles.css', }, diff --git a/libs/web-components/scripts/run-ssr-dsd-check.mjs b/libs/web-components/scripts/run-ssr-dsd-check.mjs index bbb2f25..ff56b19 100644 --- a/libs/web-components/scripts/run-ssr-dsd-check.mjs +++ b/libs/web-components/scripts/run-ssr-dsd-check.mjs @@ -21,16 +21,15 @@ const server = await createServer({ try { const mod = await server.ssrLoadModule(resolve(__dirname, 'ssr-dsd-render.ts')); - const { out, hasButtonDSD, hasTypographyDSD, staticHtmlPath, hydratedHtmlPath } = await mod.runSsrDsdCheck(); + const { out, dsdByTag, staticHtmlPath, hydratedHtmlPath } = await mod.runSsrDsdCheck(); console.log('--- SSR output ---\n' + out + '\n------------------\n'); - console.log('DSD present for gd-button: ', hasButtonDSD); - console.log('DSD present for gd-typography: ', hasTypographyDSD); + for (const [tag, present] of Object.entries(dsdByTag)) console.log(`DSD present for ${tag}:`, present); console.log(`\nWrote no-JS static DSD reproduction → ${staticHtmlPath}`); console.log(`Wrote hydration reproduction → ${hydratedHtmlPath}`); - if (!hasButtonDSD || !hasTypographyDSD) { - console.error('\nFAIL: expected DSD template wrapper missing for one or both components.'); + if (Object.values(dsdByTag).some((present) => !present)) { + console.error('\nFAIL: expected DSD template wrapper missing for one or more components.'); process.exitCode = 1; } } finally { diff --git a/libs/web-components/scripts/ssr-dsd-render.ts b/libs/web-components/scripts/ssr-dsd-render.ts index 568be9e..a2d2dfb 100644 --- a/libs/web-components/scripts/ssr-dsd-render.ts +++ b/libs/web-components/scripts/ssr-dsd-render.ts @@ -1,5 +1,5 @@ /** - * SSR / Declarative Shadow DOM (DSD) smoke check for `gd-button` and `gd-typography`. Uses + * SSR / Declarative Shadow DOM (DSD) smoke check for every ported Web Component. Uses * Lit's own SSR tooling (`@lit-labs/ssr`), added only to this package's own package.json. * * Loaded via Vite's `ssrLoadModule` (see run-ssr-dsd-check.mjs) rather than run directly @@ -16,8 +16,13 @@ import { writeFileSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { defaultTheme } from 'gd-design-library/tokens'; -import '../src/components/gd-button/gd-button'; -import '../src/components/gd-typography/gd-typography'; +import '../src/components/atoms/gd-avatar/gd-avatar'; +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-select/gd-select'; +import '../src/components/atoms/gd-typography/gd-typography'; +import '../src/components/molecules/gd-menu/gd-menu'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -31,16 +36,27 @@ export async function runSsrDsdCheck() { // so the check is meaningless without a theme. Matches how every other harness supplies it // (`fidelity-check.tsx`, `form-participation-check.ts`). const template = html` + Submit + Accept terms + + Heading + ActionsMenu content `; const result = render(template); let out = ''; for await (const chunk of result) out += chunk; - const hasButtonDSD = /]*>\s*