From 8f6be542fadcd2f6810790f60675a930b1c5fea0 Mon Sep 17 00:00:00 2001 From: Isaque dos Santos Date: Mon, 3 Aug 2026 20:33:22 -0300 Subject: [PATCH 1/6] fix(webkit): [ENG-47001] catch the v4 paren spelling in the six token-check guardrails typography-raw-length, leading-raw, tracking-raw, font-family-raw, animate-arbitrary and motion-hardcoded keyed on a literal '[', so the canonical v4 paren syntax (duration-(--x), animate-(--x), text-(length:--x), font-(family-name:--x), leading-(--x), tracking-(--x)) walked straight through the typography / motion / animation gates. Each now matches both spellings. Regression tests pin both forms and assert the widened guards stay silent on a plain canonical token (bg-(--primary)). --- .../webkit/src/eslint-plugin/token-checks.js | 24 ++++++--- .../test/eslint-plugin/token-checks.test.mjs | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/packages/webkit/src/eslint-plugin/token-checks.js b/packages/webkit/src/eslint-plugin/token-checks.js index 936de89a4..7570e375b 100644 --- a/packages/webkit/src/eslint-plugin/token-checks.js +++ b/packages/webkit/src/eslint-plugin/token-checks.js @@ -30,7 +30,11 @@ export const TOKEN_CHECKS = [ }, { id: 'typography-raw-length', - regex: /text-\[length:var\(--text-/g, + // Both v4 spellings of the arbitrary typography token: the bracket form + // `text-[length:var(--text-…)]` and the canonical paren form + // `text-(length:--text-…)`. Keying on `[` alone let the paren spelling walk + // straight through the gate (ENG-47001). + regex: /text-(?:\[length:var\(|\(length:)--text-/g, message: 'Raw typography token. Use generated class from DESIGN.md (text-heading-md, text-body-sm, text-button-lg, ...).' }, @@ -39,7 +43,8 @@ export const TOKEN_CHECKS = [ // Any numeric step, not just 3-12: in Tailwind v4 `leading-` resolves to // calc(var(--spacing) * n), so `leading-1` is 0.25rem — not the line-height // of 1 it reads as. `none` stays out of the alternation (allowed on icons). - regex: /\bleading-(?:\d+|tight|snug|relaxed|loose|\[)/g, + // `[` and `(` both open an arbitrary value (`leading-[…]` / `leading-(--…)`). + regex: /\bleading-(?:\d+|tight|snug|relaxed|loose|[[(])/g, message: 'Raw leading-* class. Line-height is part of the generated typography class (DESIGN.md); do not override.' }, @@ -47,14 +52,17 @@ export const TOKEN_CHECKS = [ id: 'tracking-raw', // `tightest` is not a step on the tracking scale, so Tailwind emits nothing // for it and the class silently does nothing. It belongs here so the dead - // override is reported rather than read as working code. - regex: /\btracking-(?:tightest|tighter|tight|wide|wider|widest|\[)/g, + // override is reported rather than read as working code. `[`/`(` both open an + // arbitrary value (`tracking-[…]` / `tracking-(--…)`). + regex: /\btracking-(?:tightest|tighter|tight|wide|wider|widest|[[(])/g, message: 'Raw tracking-* class. Letter-spacing is part of the generated typography class (DESIGN.md); do not override.' }, { id: 'font-family-raw', - regex: /\b(?:font-(?:sans|serif|mono|sora|proto-mono)\b|font-\[family-name:)/g, + // Both spellings of the arbitrary family: `font-[family-name:…]` and + // `font-(family-name:--…)`. + regex: /\b(?:font-(?:sans|serif|mono|sora|proto-mono)\b|font-[[(]family-name:)/g, message: 'Raw font-family. Font family is part of the generated typography class (DESIGN.md); do not override.' }, @@ -126,13 +134,15 @@ export const TOKEN_CHECKS = [ }, { id: 'animate-arbitrary', - regex: /\banimate-\[/g, + // `animate-[…]` and the paren shorthand `animate-(--…)` are both arbitrary. + regex: /\banimate-[[(]/g, message: 'Arbitrary animate-[…] value. Use a catalogued animate-* utility, or add one via /add-animation (semantic/animations.js).' }, { id: 'motion-hardcoded', - regex: /\b(?:duration|delay|ease)-\[/g, + // `[` and `(` both open an arbitrary value (`duration-[…]` / `duration-(--…)`). + regex: /\b(?:duration|delay|ease)-[[(]/g, message: 'Hardcoded duration/ease/delay. Use the duration/curve/ease tokens from primitives/animations (DESIGN.md § Animations).' } diff --git a/packages/webkit/test/eslint-plugin/token-checks.test.mjs b/packages/webkit/test/eslint-plugin/token-checks.test.mjs index 59220b017..25ffc00db 100644 --- a/packages/webkit/test/eslint-plugin/token-checks.test.mjs +++ b/packages/webkit/test/eslint-plugin/token-checks.test.mjs @@ -76,6 +76,56 @@ test('the one-level nesting limit is pinned', () => { assert.ok(!found.includes('zero-unit-in-calc'), 'zero-unit-in-calc cannot see this deep') }) +// ENG-47001: the six `[`-keyed guardrails must catch the canonical v4 paren spelling +// (`prop-(…)`) too — accepting the IntelliSense suggestion must not walk through the +// typography / motion / animation gates. Each case is checked in both spellings. +test('the arbitrary-value guardrails catch the paren spelling as well as the bracket spelling', () => { + const cases = [ + ['typography-raw-length', 'text-[length:var(--text-body-md)]', 'text-(length:--text-body-md)'], + ['leading-raw', 'leading-[1.5]', 'leading-(--leading-body)'], + ['tracking-raw', 'tracking-[0.5px]', 'tracking-(--tracking-tight)'], + ['font-family-raw', 'font-[family-name:var(--font-sora)]', 'font-(family-name:--font-sora)'], + ['animate-arbitrary', 'animate-[spin_1s]', 'animate-(--animation-spin)'], + ['motion-hardcoded', 'duration-[200ms]', 'duration-(--duration-fast)'] + ] + for (const [id, bracket, paren] of cases) { + assert.ok(ids(bracket).includes(id), `expected ${id} for bracket form: ${bracket}`) + assert.ok(ids(paren).includes(id), `expected ${id} for paren form: ${paren}`) + } +}) + +test('the paren spellings named in ENG-47001 acceptance are all caught', () => { + for (const [content, id] of [ + ['duration-(--duration-fast)', 'motion-hardcoded'], + ['animate-(--x)', 'animate-arbitrary'], + ['leading-(--y)', 'leading-raw'], + ['tracking-(--z)', 'tracking-raw'], + ['text-(length:--text-body-md)', 'typography-raw-length'], + ['font-(family-name:--font-sora)', 'font-family-raw'] + ]) { + assert.ok(ids(content).includes(id), `expected ${id} for: ${content}`) + } +}) + +test('the widened guardrails do not fire on the canonical whole-value paren token', () => { + // `bg-(--primary)` and friends (family A) are the sanctioned canonical form — the + // motion/typography guards key on their own prefixes, so a plain color/spacing token + // in paren form must stay silent. + for (const content of ['bg-(--primary)', 'max-w-(--container-2xl)', 'ring-offset-(--bg-canvas)']) { + const found = ids(content) + for (const id of [ + 'typography-raw-length', + 'leading-raw', + 'tracking-raw', + 'font-family-raw', + 'animate-arbitrary', + 'motion-hardcoded' + ]) { + assert.ok(!found.includes(id), `expected ${id} silent for canonical token: ${content}`) + } + } +}) + test('tokenChecksApply scopes enforcement to component sources', () => { assert.ok(tokenChecksApply('packages/webkit/src/components/actions/button/button.vue')) assert.ok(tokenChecksApply('packages/webkit/src/components/data/table/injection-key.ts')) From 5903b311392ab09bc5f8aa4c1ee7fe1300848f7a Mon Sep 17 00:00:00 2001 From: Isaque dos Santos Date: Mon, 3 Aug 2026 20:33:37 -0300 Subject: [PATCH 2/6] refactor: [ENG-47001] adopt the canonical Tailwind v4 paren token syntax repo-wide Decision A: standardize on Tailwind v4's paren shorthand for design-token values (bg-(--primary), text-(length:--text-body-md), z-1), so IntelliSense stops flagging every styled line with suggestCanonicalClasses. The two spellings compile to byte-identical CSS. A codemod (scripts/codemods/canonicalize-tw-v4.mjs) rewrites the three safe families across packages/webkit/src, apps/storybook, .specs, packages/webkit/docs, .claude/**, cli-templates and packages/theme: A prop-[var(--x)] -> prop-(--x) (2470) B prop-[type:var(--x)] -> prop-(type:--x) (29) E z-[] -> z- (51) It provably leaves two families bracketed, because the paren form emits no CSS for them (silent style loss, no build or lint error): C custom-property declarations [--x:var(--y)] D expression values / var-with-fallback w-[calc(var(--a)*2)], bg-[var(--x,var(--y))] canonicalize-tw-v4.test.mjs pins the A/B/E conversions and the C/D skips. The decision is recorded in .claude/rules/styling.md and mirrored into the shipped cli-templates styling rule; DESIGN.md and the scaffolder examples teach the paren form. apps/icons-gallery (Tailwind v3) is untouched. --- .claude/AGENTS.md | 4 +- .claude/docs/COMPONENT_REQUIREMENTS.md | 26 ++-- .claude/docs/DESIGN.md | 70 ++++----- .claude/hooks/README.md | 2 +- .claude/rules/accessibility.md | 2 +- .claude/rules/dependencies.md | 2 +- .claude/rules/root-element.md | 2 +- .claude/rules/styling.md | 65 ++++++--- .claude/skills/component-scaffold/SKILL.md | 18 +-- .specs/_examples/tooltip.md | 4 +- .specs/_template.md | 2 +- .specs/accordion.md | 4 +- .specs/avatar.md | 2 +- .specs/box-grid-selection.md | 2 +- .specs/breadcrumb-item.md | 2 +- .specs/button-highlight.md | 2 +- .specs/button.md | 2 +- .specs/calendar.md | 2 +- .specs/card-box.md | 2 +- .specs/card-pricing.md | 2 +- .specs/checkbox.md | 2 +- .specs/chip.md | 2 +- .specs/code-block.md | 2 +- .specs/command-menu.md | 12 +- .specs/copy-button.md | 2 +- .specs/currency.md | 2 +- .specs/dialog.md | 2 +- .specs/drawer.md | 2 +- .specs/dropdown.md | 10 +- .specs/empty-state.md | 4 +- .specs/flow.md | 8 +- .specs/icon-button.md | 2 +- .specs/input-group.md | 20 +-- .specs/input-number.md | 2 +- .specs/input-password.md | 2 +- .specs/input-text.md | 2 +- .specs/item.md | 2 +- .specs/link.md | 2 +- .specs/menu-item.md | 2 +- .specs/message.md | 2 +- .specs/mini-button.md | 2 +- .specs/multi-select.md | 2 +- .specs/navigation-menu.md | 2 +- .specs/paginator.md | 2 +- .specs/panel.md | 2 +- .specs/pick-list.md | 2 +- .specs/popover.md | 6 +- .specs/radio-button.md | 2 +- .specs/scroll-area.md | 2 +- .specs/select.md | 2 +- .specs/sidebar.md | 2 +- .specs/sign-up-card.md | 2 +- .specs/spinner.md | 2 +- .specs/split-button.md | 2 +- .specs/status-indicator.md | 2 +- .specs/switch.md | 10 +- .specs/tab-view.md | 4 +- .specs/table.md | 2 +- .specs/tag.md | 2 +- .specs/theme-switcher.md | 4 +- .specs/toast.md | 2 +- .specs/tooltip.md | 6 +- .../components/ColorPaletteSection.vue | 16 +- .../src/foundations/components/IconGrid.vue | 8 +- .../components/SemanticSwatchGroup.vue | 26 ++-- .../foundations/components/SpacingPreview.vue | 2 +- .../components/TypographyPreview.vue | 22 +-- .../components/layout/PageContainer.vue | 2 +- .../code/log-view/LogView.stories.js | 6 +- .../content/avatar/Avatar.stories.js | 18 +-- .../content/card-box/CardBox.stories.js | 20 +-- .../card-pricing/CardPricing.stories.js | 10 +- .../components/content/item/Item.stories.js | 32 ++-- .../components/data/flow/Flow.stories.js | 12 +- .../data/paginator/Paginator.stories.js | 6 +- .../data/pick-list/PickList.stories.js | 2 +- .../components/data/table/Table.stories.js | 4 +- .../empty-state/EmptyState.stories.js | 4 +- .../feedback/skeleton/Skeleton.stories.js | 18 +-- .../StatusIndicator.stories.js | 2 +- .../inputs/checkbox/Checkbox.stories.js | 2 +- .../field-switch/FieldSwitch.stories.js | 2 +- .../inputs/input-group/InputGroup.stories.js | 2 +- .../input-number/InputNumber.stories.js | 4 +- .../radio-button/RadioButton.stories.js | 2 +- .../inputs/switch/Switch.stories.js | 2 +- .../inputs/textarea/Textarea.stories.js | 2 +- .../layout/scroll-area/ScrollArea.stories.js | 12 +- .../layout/sidebar/Sidebar.stories.js | 8 +- .../navigation/dropdown/Dropdown.stories.js | 24 +-- .../navigation-menu/NavigationMenu.stories.js | 20 +-- .../navigation/tab-view/TabView.stories.js | 40 ++--- .../overlay/drawer/Drawer.stories.js | 10 +- .../overlay/popover/Popover.stories.js | 54 +++---- .../overlay/tooltip/Tooltip.stories.js | 14 +- .../src/stories/foundations/Colors.stories.js | 8 +- .../src/stories/foundations/GetStarted.mdx | 2 +- .../src/stories/foundations/Motion.stories.js | 66 ++++----- .../templates/ChangePlanDrawer.stories.js | 20 +-- .../templates/DeleteDomainDialog.stories.js | 12 +- .../templates/PlatformShell.stories.js | 28 ++-- .../stories/utils/spinner/Spinner.stories.js | 4 +- packages/theme/src/scripts/build-tokens.mjs | 2 +- .../src/tokens/semantic/z-indices.data.js | 2 +- .../claude/rules/webkit-accessibility.md | 6 +- .../claude/rules/webkit-component-states.md | 2 +- .../claude/rules/webkit-root-element.md | 2 +- .../claude/rules/webkit-slots.md | 2 +- .../claude/rules/webkit-style-override.md | 4 +- .../claude/rules/webkit-styling.md | 33 +++-- .../claude/rules/webkit-testid.md | 2 +- .../claude/rules/webkit-tokens.md | 2 +- .../claude/skills/webkit-baseline-ui/SKILL.md | 46 +++--- .../claude/skills/webkit-data-viz/SKILL.md | 10 +- .../claude/skills/webkit-form/SKILL.md | 68 ++++----- .../claude/skills/webkit-navigation/SKILL.md | 16 +- .../claude/skills/webkit-tables/SKILL.md | 22 +-- .../skills/webkit-theming-dark-mode/SKILL.md | 32 ++-- .../claude/skills/webkit-ui-craft/SKILL.md | 2 +- .../claude/skills/webkit-ui-states/SKILL.md | 4 +- .../claude/skills/webkit-usage/SKILL.md | 2 +- packages/webkit/docs/DOC_LINTS.md | 2 +- packages/webkit/docs/GUIDELINES.md | 10 +- packages/webkit/docs/STYLEGUIDE.md | 18 +-- packages/webkit/src/cli/plan.js | 2 +- .../button-highlight/button-highlight.vue | 26 ++-- .../src/components/actions/button/button.vue | 30 ++-- .../actions/icon-button/icon-button.vue | 24 +-- .../icon-button/presets/icon-transition.js | 2 +- .../actions/mini-button/mini-button.vue | 16 +- .../segmented-button/segmented-button.vue | 22 +-- .../actions/split-button/split-button.vue | 4 +- .../webkit/src/components/avatar/avatar.vue | 6 +- .../code/log-view/log-view-content.vue | 36 ++--- .../code/log-view/log-view-footer.vue | 2 +- .../code/log-view/log-view-header.vue | 10 +- .../src/components/code/log-view/log-view.vue | 2 +- .../accordion-content/accordion-content.vue | 2 +- .../accordion-item/accordion-item.vue | 2 +- .../accordion-trigger/accordion-trigger.vue | 4 +- .../src/components/content/badge/badge.vue | 2 +- .../components/content/card-box/card-box.vue | 14 +- .../content/card-pricing/card-pricing.vue | 30 ++-- .../components/content/currency/currency.vue | 16 +- .../components/content/item/item-actions.vue | 2 +- .../components/content/item/item-content.vue | 2 +- .../content/item/item-description.vue | 2 +- .../components/content/item/item-footer.vue | 2 +- .../components/content/item/item-group.vue | 2 +- .../components/content/item/item-header.vue | 2 +- .../src/components/content/item/item-list.vue | 2 +- .../components/content/item/item-media.vue | 2 +- .../content/item/item-separator.vue | 2 +- .../components/content/item/item-title.vue | 2 +- .../webkit/src/components/content/kbd/kbd.vue | 2 +- .../components/data/code-block/code-block.vue | 44 +++--- .../data/code-block/presets/transitions.ts | 4 +- .../data/code-block/utils/highlight-code.ts | 14 +- .../data/flow/flow-node/flow-node.vue | 2 +- .../data/flow/flow-parallel/flow-parallel.vue | 2 +- .../webkit/src/components/data/flow/flow.vue | 8 +- .../pagination-button/pagination-button.vue | 4 +- .../paginator-info/paginator-info.vue | 2 +- .../paginator-page-size.vue | 6 +- .../components/data/paginator/paginator.vue | 6 +- .../pick-list-controls/pick-list-controls.vue | 10 +- .../pick-list-source/pick-list-source.vue | 12 +- .../pick-list-target/pick-list-target.vue | 12 +- .../components/data/pick-list/pick-list.vue | 2 +- .../table-applied-filters.vue | 2 +- .../table/table-caption/table-caption.vue | 2 +- .../data/table/table-cell/table-cell.vue | 2 +- .../table-column-selector.vue | 8 +- .../data/table/table-filter/table-filter.vue | 12 +- .../data/table/table-footer/table-footer.vue | 2 +- .../table/table-head-cell/table-head-cell.vue | 4 +- .../data/table/table-header/table-header.vue | 2 +- .../data/table/table-row/table-row.vue | 2 +- .../table/table-toolbar/table-toolbar.vue | 2 +- .../src/components/data/table/table.vue | 16 +- .../feedback/empty-state/empty-state.vue | 18 +-- .../components/feedback/message/message.vue | 6 +- .../feedback/progress-bar/progress-bar.vue | 4 +- .../components/feedback/skeleton/skeleton.vue | 2 +- .../status-indicator/status-indicator.vue | 8 +- .../toast-description/toast-description.vue | 2 +- .../feedback/toast/toast-item/toast-item.vue | 10 +- .../toast/toast-title/toast-title.vue | 2 +- .../box-grid-selection/box-grid-selection.vue | 18 +-- .../calendar-clear/calendar-clear.vue | 2 +- .../calendar-fields/calendar-fields.vue | 14 +- .../calendar/calendar-grid/calendar-grid.vue | 18 +-- .../calendar-period/calendar-period.vue | 28 ++-- .../calendar-preset/calendar-preset.vue | 2 +- .../calendar-timezone/calendar-timezone.vue | 8 +- .../components/inputs/calendar/calendar.vue | 52 +++---- .../components/inputs/checkbox/checkbox.vue | 6 +- .../src/components/inputs/chip/chip.vue | 6 +- .../field-checkbox-block.vue | 14 +- .../inputs/field-checkbox/field-checkbox.vue | 8 +- .../field-input-group/field-input-group.vue | 4 +- .../inputs/field-password/field-password.vue | 2 +- .../field-phone-number/field-phone-number.vue | 4 +- .../field-radio-block/field-radio-block.vue | 12 +- .../inputs/field-radio/field-radio.vue | 8 +- .../inputs/field-select/field-select.vue | 2 +- .../field-switch-block/field-switch-block.vue | 14 +- .../inputs/field-switch/field-switch.vue | 10 +- .../field-text-switch/field-text-switch.vue | 4 +- .../inputs/field-text/field-text.vue | 2 +- .../inputs/field-textarea/field-textarea.vue | 4 +- .../inputs/helper-text/helper-text.vue | 2 +- .../input-group-addon/input-group-addon.vue | 2 +- .../inputs/input-group/input-group.vue | 2 +- .../inputs/input-number/input-number.vue | 28 ++-- .../inputs/input-password/input-password.vue | 22 +-- .../inputs/input-text/input-text.vue | 22 +-- .../src/components/inputs/label/label.vue | 6 +- .../multi-select-content.vue | 8 +- .../multi-select-group/multi-select-group.vue | 2 +- .../multi-select-option.vue | 4 +- .../multi-select-trigger.vue | 6 +- .../inputs/presets/interactive-states.js | 36 ++--- .../inputs/radio-button/radio-button.vue | 10 +- .../select/select-content/select-content.vue | 8 +- .../select/select-group/select-group.vue | 2 +- .../select/select-option/select-option.vue | 6 +- .../select/select-trigger/select-trigger.vue | 6 +- .../components/inputs/select/select.test.ts | 2 +- .../src/components/inputs/switch/switch.vue | 8 +- .../components/inputs/textarea/textarea.vue | 18 +-- .../inputs/theme-switcher/theme-switcher.vue | 6 +- .../src/components/layout/divider/divider.vue | 2 +- .../global-header/global-header-container.vue | 2 +- .../global-header/global-header-left.vue | 2 +- .../global-header/global-header-right.vue | 2 +- .../layout/global-header/global-header.vue | 2 +- .../layout/scroll-area/scroll-area.vue | 2 +- .../layout/sidebar/sidebar-footer.vue | 2 +- .../src/components/layout/sidebar/sidebar.vue | 8 +- .../breadcrumb-item/breadcrumb-item.vue | 16 +- .../navigation/breadcrumb/breadcrumb-list.vue | 2 +- .../breadcrumb/breadcrumb-separator.vue | 2 +- .../navigation/breadcrumb/breadcrumb.vue | 8 +- .../dropdown-group/dropdown-group.vue | 8 +- .../dropdown-option/dropdown-option.vue | 4 +- .../dropdown-trigger/dropdown-trigger.vue | 2 +- .../navigation/dropdown/dropdown.test.ts | 2 +- .../navigation/dropdown/dropdown.vue | 6 +- .../src/components/navigation/link/link.vue | 14 +- .../navigation/menu-item/menu-item.vue | 22 +-- .../navigation-menu/presets/styles.js | 56 +++---- .../tab-view/presets/transitions.ts | 4 +- .../navigation/tab-view/tab-view-item.vue | 22 +-- .../navigation/tab-view/tab-view-list.vue | 8 +- .../command-menu-empty/command-menu-empty.vue | 2 +- .../command-menu-group/command-menu-group.vue | 2 +- .../command-menu-input/command-menu-input.vue | 2 +- .../command-menu-item/command-menu-item.vue | 2 +- .../command-menu-list/command-menu-list.vue | 2 +- .../command-menu-separator.vue | 2 +- .../overlay/dialog/dialog-content.vue | 2 +- .../overlay/dialog/dialog-description.vue | 2 +- .../overlay/dialog/dialog-overlay.vue | 2 +- .../overlay/dialog/dialog-title.vue | 2 +- .../overlay/drawer/drawer-content.vue | 6 +- .../overlay/drawer/drawer-description.vue | 2 +- .../overlay/drawer/drawer-overlay.vue | 2 +- .../overlay/drawer/drawer-title.vue | 2 +- .../overlay/panel/panel-content.vue | 2 +- .../components/overlay/panel/panel-footer.vue | 6 +- .../components/overlay/panel/panel-header.vue | 6 +- .../src/components/overlay/panel/panel.vue | 6 +- .../components/overlay/panel/presets/sizes.js | 12 +- .../popover-content/popover-content.vue | 2 +- .../popover-description.vue | 2 +- .../popover/popover-footer/popover-footer.vue | 2 +- .../popover/popover-header/popover-header.vue | 4 +- .../popover/popover-title/popover-title.vue | 2 +- .../overlay/presets/mobile-position.js | 6 +- .../components/overlay/tooltip/tooltip.vue | 4 +- packages/webkit/src/components/tag/tag.vue | 2 +- .../deploy-success/deploy-success.vue | 34 ++--- .../onboarding-form/onboarding-form.vue | 28 ++-- .../templates/plan-success/plan-success.vue | 30 ++-- .../platform-shell/platform-shell.vue | 6 +- .../templates/sign-up-card/sign-up-card.vue | 38 ++--- packages/webkit/src/styles.css | 2 +- packages/webkit/src/utils/cn.js | 6 +- .../test/codemods/canonicalize-tw-v4.test.mjs | 82 +++++++++++ scripts/codemods/canonicalize-tw-v4.mjs | 137 ++++++++++++++++++ 291 files changed, 1533 insertions(+), 1274 deletions(-) create mode 100644 packages/webkit/test/codemods/canonicalize-tw-v4.test.mjs create mode 100644 scripts/codemods/canonicalize-tw-v4.mjs diff --git a/.claude/AGENTS.md b/.claude/AGENTS.md index 77b965531..cb179848d 100644 --- a/.claude/AGENTS.md +++ b/.claude/AGENTS.md @@ -190,7 +190,7 @@ The trigger logic now lives in the orchestrator command [`.claude/commands/compo - If a needed module does **not** exist yet, do not import it speculatively. Either (1) install it first (`pnpm --filter add ...`), (2) create the file and its `package.json` entry first, or (3) annotate the gap explicitly in the report as `TODO: create ` or `TODO: install ` and skip the dependent code. The `validate-references.mjs` hook physically blocks imports that resolve to nothing — see § 12. - New components use ` @@ -124,7 +124,7 @@ function isCopied(name) {