diff --git a/.gitignore b/.gitignore index d799a1d..c35a2fe 100644 --- a/.gitignore +++ b/.gitignore @@ -68,10 +68,7 @@ __screenshots__ .env # Claude -.claude/settings.local.json -.claude/config.json -.claude/.env -.claude/tools/node_modules +.claude/ /libs/ui/scripts/output/* /libs/ui/.size-limit.json /libs/web-components/bundle-size-report.json @@ -81,6 +78,7 @@ __screenshots__ # step 6 of the walkthrough in the root README. /libs/web-components/harness/ssr-dsd-static.html /libs/web-components/harness/ssr-dsd-hydrated.html +libs/web-components/bundle-size-baseline.json # Vitest artifacts .vitest-artifacts @@ -92,8 +90,9 @@ plans/ AGENTS.md /stats.html /libs/web-components/screenshots/ +/libs/web-components/migration-ir/ -# CTORNDSD-646b platform fixtures — build output and generated types only; the fixture +# Platform fixtures — build output and generated types only; the fixture # sources themselves are tracked. See docs/webcomponents-migration/08-react-and-nextjs.md. /fixtures/*/.next/ /fixtures/*/next-env.d.ts diff --git a/CLAUDE.md b/CLAUDE.md index dad2b01..74c0398 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,8 +44,6 @@ npm run build-storybook npm run verify:ui:full # All 10 verification phases + Verdaccio smoke test npm run verify:ui:ci # CI gate (non-zero exit on failure) -# Scaffolding -npm run crc ComponentName # Interactive scaffold for a new component (prompts for tier) ``` ## Architecture @@ -107,6 +105,5 @@ The form-configurator packages use Jest (`jest.config.ts` per package). - **Commits**: Conventional Commits (`feat:`, `fix:`, `chore:`, etc.) enforced by commitlint. - **Pre-commit**: prettier format check + lint-staged (ESLint + prettier on staged `libs/**` files, markdownlint on `*.md`). -- **New components**: `npm run crc ComponentName` launches an interactive prompt to choose the tier (atoms/molecules/organisms/widget) and scaffolds all required files. - **Styling**: never write raw hex or pixel literals in component styles. Access values through the theme object (`get(theme, 'component.variant', {})`). Use the Emotion `css` prop, not inline `style` or CSS modules. - **SSR compatibility**: components must render without errors in a Node environment. Mark client-only hooks with `'use client'` directive. The `ssr-check` and `rsc-render-check` scripts in `verify:ui` catch violations. diff --git a/DEMO.md b/DEMO.md deleted file mode 100644 index e6ea5f0..0000000 --- a/DEMO.md +++ /dev/null @@ -1,119 +0,0 @@ -# GridKit Demo — Run Card - -Hi there! - -Three weeks of work in short: - -we rebuilt five GridKit components twice — as Web Components in Lit, and as a React Native app on iOS — and pulled their shared token and state logic into `gd-design-core`, now covered by 121 tests. - -Alongside, two new Dynamic Builder checklists document - -- what runtime AI-generated UI costs and -- how to pick a model. - -## Pre-flight — Stops 3–8 only - -```bash -npm install && npm run demo:setup # ~3 min, do it before the meeting -npm run demo:harness # :5173 — Stops 3–5 -npm run check:web-components-ssr && npm run check:web-components-size # SSR pages + warm size cache -npm run demo:next # :5373 — Stop 5 insert -``` - -**Tabs to open in advance:** [1 · fidelity](http://localhost:5173/harness/fidelity-check.html) · [2 · shell-isolation](http://localhost:5173/harness/shell-isolation-check.html) · [3 · form-participation](http://localhost:5173/harness/form-participation-check.html) · [Next.js fixture](http://localhost:5373/) — plus the Simulator window and the two screenshots (Tabs 4–6). - -## Stop 1 — Dynamic Builder: performance - -- **Open** — [Performance Improvement Checklist](https://griddynamics.atlassian.net/wiki/spaces/RNDM/pages/4685791299/Dynamic+Builder+Performance+Improvement+Checklist) -- **Why** — one A2UI page load: ~74.6k tokens, ~21s, ~$0.09 -- **Shows** — nine levers: - - send only the schema a screen needs - - merge repeated component groups - - cap nesting - - patch in the browser instead of regenerating - - cache stable inputs - - feature-flag new surfaces - - ask whether the screen needs generated UI -- **Plus** — a proposed SLA: ≤8s, ≤8k tokens, ≤$0.02 per call. Not ratified — that is the decision to ask for - -## Stop 2 — Dynamic Builder: model choice - -- **Open** — [Model Benchmark & Selection Checklist](https://griddynamics.atlassian.net/wiki/spaces/RNDM/pages/4685955158/Dynamic+Builder+Model+Benchmark+Selection+Checklist) -- **Why** — we pick models by inheritance, not evidence -- **Shows** — which model for which job: - - classification and extraction — Haiku-class or o3-mini - - hard reasoning — Gemini 2.5 Pro or Sonnet-class - - A2UI — Flash Lite with a scoped schema - - preview models — not yet -- **Behind it** — timings from 38.65s to 204.57s, plus public pricing. No controlled study exists, and Haiku 3.5 is already retired - -## Stop 3 — Fidelity check - -- **Open** — Tab 1 · [localhost:5173/harness/fidelity-check.html](http://localhost:5173/harness/fidelity-check.html) -- **Why** — sets the visual bar before Stop 4 strips the theme. Keep it to 15 seconds -- **Shows** - - five components rebuilt in Lit — button, checkbox, typography, input, select - - running on the real GridKit theme, not lookalikes -- **Look for** — gold primary button, secondary/outlined/disabled/loading, gold checkboxes, Fira Sans headings, a working select -- **Note** — skip this and Stop 4 looks like a broken port - -## Stop 4 — Shell isolation - -- **Open** — Tab 2 · [localhost:5173/harness/shell-isolation-check.html](http://localhost:5173/harness/shell-isolation-check.html) -- **Why** — CTORNDSD-286: a host app's global CSS leaked into our components -- **Shows** - - at the top, a global reset wrecks the plain Emotion button — the old bug, reproduced - - the Lit component beside it is untouched -- **Read the JSON, not the buttons** — the theme is empty on purpose, so both render grey -- **Last two lines** — the Lit-shell shortcut blocked its own styling too. Why we dropped it - -### ↳ `npm run check:web-components-size` - -- **Why** — bundle cost is the main upside -- **Shows** — 7–16× smaller per component: - - Button 2.05 vs 18.66 kB - - Checkbox 2.11 vs 23.22 kB - - Typography 1.09 vs 17.64 kB - - Input 2.53 vs 18.51 kB - - Select 2.91 vs 27.69 kB -- **Headline** — all five together, **11.20 kB against 105.72 kB**. 18.08 kB counting Lit's runtime, which most React apps have not already paid. The last line is a CI gate - -## Stop 5 — Form participation - -- **Open** — Tab 3 · [localhost:5173/harness/form-participation-check.html](http://localhost:5173/harness/form-participation-check.html) -- **Why** — proves these are real form controls, not a mock-up -- **Shows** - - the app's own CSS reaches in where we allow it — `::part()` gives the magenta ring, cyan fill, red checkbox - - a plain descendant selector still cannot — 4 pixels changed, not 99 -- **Also** — they behave like inputs: listed in `form.elements`, submitted into `FormData`, block empty submit, reset cleanly - -### ↳ `curl -s http://localhost:5373/ | grep -c shadowrootmode` - -- **Against** — the Next.js fixture at [localhost:5373](http://localhost:5373/) -- **Why** — the honest cost. Lead with it -- **Shows** - - both counts come back `0` — no Declarative Shadow DOM, no server-rendered heading - - with JavaScript off, unstyled text and no headings — worse than React today for SEO and first paint -- **Two more** — our token barrel breaks inside a Server Component; mount is 2.3–3.4× slower; nested theming has no equivalent yet - -## Stop 6 — iOS Simulator, live - -``` -xcrun simctl boot "iPhone 16" && open -a Simulator # ~30s -npm run dev:react-native -- --ios --localhost -``` - -- **Turn to** — Tab 4 · the Simulator window, and scroll it. Live, not a screenshot -- **Why** — can we leave the DOM, not just React? -- **Shows** — the same five components off the same `gd-design-core`: - - gold button, GridKit type scale - - input with label and helper text - - select with chevron -- **The point** — one source, two outputs: 1.68 MB of native bytecode for the device, 392 kB of JavaScript for a browser. No DOM-specific code - -## If it breaks - -- Stops 3–5: `npm run verify:web-components` · Stops 6–8: `npm test --workspace=libs/react-native` + `npm run test:design-core` -- Ports: `kill $(lsof -ti:5173)` · `:5273` · `:5373` · `:8081` — teardown: `pkill -f "expo start"; xcrun simctl shutdown all` - -Full detail: `docs/webcomponents-migration/README.md` · `libs/react-native/FINDINGS.md` · the two Confluence pages above. diff --git a/README.md b/README.md index 57d956a..f3f08f3 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ Nx monorepo containing the GridKit design system packages. Not published, and under active investigation: -| Package | Description | -| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `gd-design-core` (`libs/design-core`) | Framework-agnostic state stores + token resolution. No React, no Lit, no `gd-design-library` dependency | -| `web-components` (`libs/web-components`) | Lit custom-element port of 5 GridKit atoms. `private: true` — see [Web Components spike](#web-components-spike-ctorndsd-646) | +| Package | Description | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `gd-design-core` (`libs/design-core`) | Framework-agnostic state stores + token resolution. No React, no Lit, no `gd-design-library` dependency | +| `web-components` (`libs/web-components`) | Lit custom-element port GridKit atoms. `private: true` — see [Web Components](libs/web-components/README.md) | ## Quick start @@ -83,203 +83,3 @@ npm run build:ui && npm run publish:ui # form-configurator — via GitHub Actions (publish-form-configurator.yaml) or: npm run build:form-configurator && npm run publish:form-configurator ``` - -## Scaffold a new component - -```bash -npm run crc ComponentName -``` - -## Web Components spike (CTORNDSD-646) - -An investigation into porting GridKit from React to Lit custom elements. **Every demo below runs from -the repo root — you never need to `cd` into a package or fixture.** - -### One-time setup - -```bash -npm install # if you haven't already -npm run demo:setup # builds dist/ + installs the two fixtures (~2-3 min) -``` - -`demo:setup` builds `gd-design-library`, `gd-design-core`, and `web-components`, then installs -`fixtures/react19-check` and `fixtures/next-ssr-check`. Some demos need those build artifacts; the -demo index tells you which. - -### Test it step by step - -Twelve steps, in order, all from the repo root. Each one lists the command, what you should see, and -which finding it proves. **Steps 1–6 are non-interactive** — run them and read the terminal. **Steps -7–11 open a browser.** Step 12 runs the whole automated set at once. - -If you only have five minutes, run **step 12**. - -#### 1. Type-check - -```bash -npm run type-check:web-components -echo $? # 0 -``` - -**Success is silent** — `tsc` prints nothing and exits `0`. Any output at all means a failure. This -checks two projects: the shipped library and the harness. The harness one was broken for a while and is -now covered. - -#### 2. Lint - -```bash -npx nx lint web-components -npx nx lint design-core -``` - -Expect `Successfully ran target lint` from both, with no rule violations listed above it. `nx` prints a -problem count only when there are problems. - -#### 3. Framework-agnostic core tests - -```bash -npm run test:design-core -``` - -Expect **73 passed (6 files)**. Includes the `resetTo` action added because form reset was silently -keeping a checkbox checked — see `FINDINGS.md` §17.1. - -#### 4. Component tests in a real browser - -```bash -npm run test:web-components -``` - -Expect **35 passed (4 files)**, running in real Chromium. This is the suite that found the `gd-input` -accessibility bug on its first run (§18.2). It covers the Input cursor guard, the checkbox -`attribute: false` constraint, form participation, the shared-stylesheet cache, and axe. - -> Real Chromium is a constraint, not a preference — jsdom does not reliably implement Constructable -> StyleSheets, the `popover` attribute, or Declarative Shadow DOM. - -#### 5. Bundle size and the regression gate - -```bash -npm run check:web-components-size -``` - -Prints a per-atom Lit-vs-React table, then expect `✓ no bundle-size regressions`. Ballpark: 5 atoms at -**~11 kB** gzip (**~18 kB** including the `lit` runtime) against **~106 kB** for React+Emotion — -§3, §18.5. - -Exact totals drift by a few dozen bytes between builds because Rollup redistributes shared-helper bytes -between chunks whenever any one chunk changes. That is why the gate's tolerance is 10% rather than 0 — -see [`10-performance-report.md`](./docs/webcomponents-migration/10-performance-report.md). - -To prove the gate actually fails, edit a number down in -`libs/web-components/bundle-size-baseline.json` and re-run: it should exit `1`. - -#### 6. Server rendering with zero client JavaScript - -```bash -npm run check:web-components-ssr -``` - -Generates `libs/web-components/harness/ssr-dsd-static.html` (**0** ` -``` - -A real theme is **mandatory**, not cosmetic: the components resolve the real token files, whose own -fallbacks are debug placeholder strings, so a themeless render produces invalid CSS (**measured**, -§§13, 16). - -### Lit - -```ts -html`Save`; -``` - -### React - -```tsx -import { createComponent } from '@lit/react'; -import * as React from 'react'; -import { GdCheckbox as GdCheckboxElement } from 'gd-design-web'; - -export const GdCheckbox = createComponent({ - tagName: 'gd-checkbox', - elementClass: GdCheckboxElement, - react: React, - events: { onGdChange: 'gd-change' }, -}); - - setChecked(e.detail.checked)}> - Accept terms -; -``` - -The `events` map is required on **both** React 18 and 19. - -### Next.js (App Router) - -```tsx -// app/page.tsx — server component: renders tags, imports nothing from the package -Save - -``` - -```tsx -// app/client-island.tsx -'use client'; -import { defaultTheme } from 'gd-design-library/tokens'; -import 'gd-design-web'; -``` - -`'use client'` is **mandatory** — the token barrel cannot be imported in a server component -(**measured**, §17.4). See `08-react-and-nextjs.md`. - -### Vue - -```vue - -``` - -Vue handles custom elements natively; `.prop` forces property assignment for objects, and `@gd-change` -binds custom events directly. **Reasoned, not verified** — no Vue fixture was built. - -### Angular - -```html -Save -``` - -Requires `CUSTOM_ELEMENTS_SCHEMA` in the consuming module. **Reasoned, not verified** — no Angular -fixture was built. diff --git a/docs/webcomponents-migration/05-native-html-guidelines.md b/docs/webcomponents-migration/05-native-html-guidelines.md deleted file mode 100644 index 34bb71d..0000000 --- a/docs/webcomponents-migration/05-native-html-guidelines.md +++ /dev/null @@ -1,158 +0,0 @@ -# 05 — Native HTML Versus Custom Element Guidelines - -**Owner:** CTORNDSD-646a · **Answers:** CTORNDSD-646 acceptance criterion 6 · **Status:** Delivered - -CTORNDSD-646 asks whether _all_ existing GridKit components should become custom elements. The -answer is **no**, and the dividing line is empirical rather than stylistic. - -This analysis uses only components that already exist. No component was built for it. - -## The question that actually decides it - -Shadow DOM is not free. It buys style isolation — the measured fix for CTORNDSD-286 (**measured**, -`FINDINGS.md` §1) — and it charges three things: - -1. **Light-DOM discoverability.** `document.querySelector('h1')` cannot reach a heading rendered - inside a shadow root (**measured**, §5). The accessibility tree is unaffected — screen readers see - a real heading — so this is a DOM-query gap, not an a11y regression. But SEO crawlers, link - checkers, browser extensions, analytics selectors, E2E selectors, and testing-library shortcuts - all use light-DOM queries. -2. **A containing block per element.** A percentage width on a shadow-DOM child resolves against the - host's box, and `:host` defaults to `auto`. This is not theoretical: `gd-select` collapsed to - icon-only width and clipped its dropdown text for exactly this reason, fixed by setting an - explicit width on the host (**measured**, §10). -3. **Per-instance setup cost.** Custom-element upgrade plus shadow-root attachment, paid per node. At - 300 instances this is measurable (**measured**, §14). - -So the decision rule is not "is this a component?" but **"does this element own behavior worth paying -isolation for, and does anything outside need to find its internals?"** - -## Decision rule - -Apply in order; stop at the first match. - -| Ship | When | -| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Lit custom element** | It owns interaction state or behavior the browser does not provide natively, **and** it renders a visual surface that must survive a hostile host reset, **and** nothing external needs to query its internals by light-DOM tag or attribute | -| **Native element + shared token CSS** | It renders a real semantic element whose **discoverability matters** (SEO, crawlers, link checkers, analytics, E2E selectors) — even if it carries a little behavior. Behavior small enough to express as a documented pattern or a thin hook does not justify a shadow root | -| **No abstraction — shared utility CSS** | It carries no behavior **and** no visual surface of its own. Pure layout and spacing wrappers | -| **Documentation only** | The value is a naming convention over CSS that already exists | -| **React wrapper** | Orthogonal, not exclusive — applies **on top of** a Lit element whenever consumers are React. See `08-react-and-nextjs.md` | - -## Verdicts - -Behavior signals below are **measured** — line counts and hook/state/effect counts read from each -component's `.tsx`. Bundle figures are **measured** from `FINDINGS.md` §3. - -| Group | Existing component(s) | Behavior signal | Verdict | Confidence | -| --------------- | ----------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------- | --------------- | -| Button | `atoms/Button` | 82 lines, 0 state | **Lit custom element** | High | -| Input | `atoms/Input` | 106 lines, 0 state, 7 hooks | **Lit custom element** — conditional | High | -| Select | `atoms/Select` | 533 lines, 10 state, 6 effects, 5 context | **Lit custom element** | High | -| Checkbox | `atoms/Checkbox` | 97 lines, 2 state, 3 effects | **Lit custom element** — conditional | High | -| Link | `atoms/Link` | 58 lines, 0 state | **Native `` + shared token CSS** | High | -| Image | `atoms/Image` | 91 lines, 3 state | **Native `` + shared token CSS** | Medium | -| Typography | `atoms/Typography` | 46 lines, 0 state | **Native element + shared token CSS** | High — measured | -| Layout and grid | `layout/Row`, `layout/Column`, `layout/FlexContainer` | 38 / 41 / 30 lines, 0 state each | **No abstraction — shared utility CSS** | High — measured | -| Containers | `atoms/Box`, `atoms/Wrapper` | 41 / 22 lines, 0 state | **No abstraction — shared utility CSS** | High | -| Containers | `layout/ChatContainer` | 163 lines, 2 state, `useMediaQuery`, `useImperativeHandle` | **Lit custom element** — it is not a container | Medium | - -### Ship as Lit custom elements - -**Button.** The most-used interactive primitive, and CTORNDSD-286 was a button bug — style isolation -is the whole point here. It carries variant, loading, and focus-ring states that native `