From 4fa135954c75cd998ea66750abdf3e17a5ee2817 Mon Sep 17 00:00:00 2001 From: Shevchik Igor Date: Mon, 29 Jun 2026 15:15:24 +0000 Subject: [PATCH] perf(types): decouple useComponentProps from the component-types barrel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of upstream nuxt/ui 6576fb8 (#6648). Move the ThemeUI / ThemeDefaults / ThemeContextDefaults / ThemeSlotOverrides type definitions out of `composables/useComponentProps.ts` into a new `types/theme.ts`. The composable was the last file importing the `../types` barrel — only because those theme types reference every component's `*Props`. Relocating them lets the composable drop the barrel import, and its eslint `no-restricted-imports` exemption (added in #227) is removed. `ThemeContext.defaults` is re-typed to the loose `ComputedRef | undefined>>`; the strict shape now lives in `types/theme.ts`. b24ui's own 114-key `ThemeDefaults` registry was moved verbatim. Types-only; no snapshot changes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JS8ypVfQSFzYVZzkTHhURb --- ...576fb81dd34c25a8e6b2209b9df9b2bf935f99b.md | 63 ++++++ .sync/nuxt-ui.json | 10 +- eslint.config.mjs | 5 - src/runtime/components/Theme.vue | 2 +- src/runtime/composables/useComponentProps.ts | 179 +----------------- src/runtime/types/index.ts | 1 + src/runtime/types/theme.ts | 177 +++++++++++++++++ test/composables/useComponentProps.spec.ts | 2 +- 8 files changed, 252 insertions(+), 187 deletions(-) create mode 100644 .sync/log/6576fb81dd34c25a8e6b2209b9df9b2bf935f99b.md create mode 100644 src/runtime/types/theme.ts diff --git a/.sync/log/6576fb81dd34c25a8e6b2209b9df9b2bf935f99b.md b/.sync/log/6576fb81dd34c25a8e6b2209b9df9b2bf935f99b.md new file mode 100644 index 000000000..1d0da3c0e --- /dev/null +++ b/.sync/log/6576fb81dd34c25a8e6b2209b9df9b2bf935f99b.md @@ -0,0 +1,63 @@ +# Port: perf(types): decouple `useComponentProps` from the component-types barrel (#6648) + +**Upstream:** `6576fb81dd34c25a8e6b2209b9df9b2bf935f99b` (nuxt/ui) +**Decision:** port + +## Upstream change +Follow-up to #6646 (b24ui PR #227). `useComponentProps.ts` was the last file +still importing the `../types` barrel (`import type * as ComponentTypes`), kept +alive only because it hosts the `ThemeUI` / `ThemeDefaults` / +`ThemeContextDefaults` / `ThemeSlotOverrides` type definitions which reference +every component's `*Props`. This moves those definitions into a **new** +`src/runtime/types/theme.ts` (which may legitimately pull the barrel via +`import type * as ComponentTypes from './index'`), so the composable no longer +touches the barrel and its eslint `no-restricted-imports` exemption is removed. +`ThemeContext.defaults` is re-typed to the loose +`ComputedRef | undefined>>` (the strict +shape now lives in `theme.ts`). + +## b24ui port — adapted, not 1:1 +Same structure, but the moved `ThemeDefaults` interface had to be b24ui's +**own** — b24ui's `useComponentProps.ts` already carries a 114-key registry +that diverges from upstream (commented-out absent components like +`// authForm`/`// blogPost`/`// listbox`/`// tree`, the `range // this is slider` +remap, a `// custom ////` block for `advice`/`countdown`/`descriptionList`/ +`tableWrapper`, and the deprecated `navbar*`/`sidebar*` families). So I **moved +b24ui's existing block verbatim** rather than copying upstream's. + +### Changes +- **New `src/runtime/types/theme.ts`** — the `ThemeSlotOverrides` / `ThemeUI` / + `ThemeDefaults` / `ThemeContextDefaults` block moved out of + `useComponentProps.ts` verbatim, with imports adjusted for the new location: + `SlotClass` from `./tv`, `* as b24ui` from `#build/b24ui` (b24ui's registry + alias, vs upstream's `* as ui`/`#build/ui`), `* as ComponentTypes` from + `./index`. +- **`useComponentProps.ts`** — dropped the moved block and its three imports + (`* as ComponentTypes from '../types'`, `SlotClass from '../types/tv'`, + `* as b24ui from '#build/b24ui'`; none used elsewhere below the block) and + re-typed `ThemeContext.defaults` to + `ComputedRef | undefined>>`. `ComputedRef` + import retained (still used). −179 lines. +- **`types/index.ts`** — `export * from './theme'` (after `./prose`). +- **`components/Theme.vue`** — `ThemeContextDefaults`/`ThemeDefaults`/`ThemeUI` + now imported from `../types/theme` (value imports + `injectThemeContext`/`provideThemeContext` still from the composable). +- **`test/composables/useComponentProps.spec.ts`** — `ThemeDefaults` import + retargeted to `../../src/runtime/types/theme`. +- **`eslint.config.mjs`** — removed the `useComponentProps.ts` + `no-restricted-imports: off` exemption added in #227 (no longer needed; the + composable now passes the rule). + +The index↔theme type-only cycle (`index` `export *`s `theme`, `theme` +`import type`s `index`) is fine — exactly as upstream. + +## Tests +Types-only. The `ThemeDefaults registry` type-test (`useComponentProps.spec.ts`) +— which asserts every themable `#build/b24ui` component has a `ThemeDefaults` +entry and no extras — still passes against the moved definition. No snapshot +churn. + +## Verify (CI=true) +`dev:prepare` · `lint` (exemption removed, composable clean) · `typecheck` +(circular type import + registry type-test resolve) · `test` (225 files, 5143 +passed / 6 skipped) · `build` — all green. diff --git a/.sync/nuxt-ui.json b/.sync/nuxt-ui.json index 13909643c..e9afe1760 100644 --- a/.sync/nuxt-ui.json +++ b/.sync/nuxt-ui.json @@ -2,7 +2,7 @@ "upstream": "nuxt/ui", "branch": "v4", "sync_enabled": false, - "cursor": "82cd5b526cf87517d931439b8ce85ed70c88cc87", + "cursor": "6576fb81dd34c25a8e6b2209b9df9b2bf935f99b", "_cursor_note": "cursor = last upstream commit ported into b24ui (oldest-first, manual cadence). sync_enabled stays false until Phase 2 (porter workflow #67 + CLAUDE_CODE_OAUTH_TOKEN) is wired and trusted. `processed` is maintained per port from now on (backfilled #68-#72 on 2026-06-09).", "stats": { "queue_depth": 0, @@ -791,10 +791,16 @@ "summary": "perf(components): drop the redundant inner in component extend (#6647) — replace `extend: tv(theme)` -> `extend: theme` across all 162 occurrences in src/runtime/{components,components/prose,components/content,vue/overrides/*} (tv's extend takes a raw theme object; inner tv() wrapper redundant). Outer tv({...})(...) unchanged so tv stays imported. Direct 1:1 mechanical (1 line/file, 162 files, +162/-162; upstream 163, delta = diverging component set). appConfig.b24ui?. spread pre-existing/untouched. Behaviour-preserving, no snapshot churn" }, "82cd5b526cf87517d931439b8ce85ed70c88cc87": { + "pr": 229, + "b24ui_sha": "75348073", + "decision": "port", + "summary": "chore(deps): update dependency reka-ui to v2.10.1 (#6652) — exact-pin bump reka-ui 2.10.0->2.10.1 in root package.json (only manifest pinning it; playgrounds/docs get it transitively) + lockfile regen under CI gate (reka-ui entry + vaul-vue transitive, 8/+8-; integrity sha512-drcOQ4r... matches upstream byte-for-byte). Direct 1:1, no snapshot churn" + }, + "6576fb81dd34c25a8e6b2209b9df9b2bf935f99b": { "pr": null, "b24ui_sha": "pending-merge", "decision": "port", - "summary": "chore(deps): update dependency reka-ui to v2.10.1 (#6652) — exact-pin bump reka-ui 2.10.0->2.10.1 in root package.json (only manifest pinning it; playgrounds/docs get it transitively) + lockfile regen under CI gate (reka-ui entry + vaul-vue transitive, 8/+8-; integrity sha512-drcOQ4r... matches upstream byte-for-byte). Direct 1:1, no snapshot churn" + "summary": "perf(types): decouple useComponentProps from the component-types barrel (#6648) — move ThemeSlotOverrides/ThemeUI/ThemeDefaults/ThemeContextDefaults out of composables/useComponentProps.ts into NEW src/runtime/types/theme.ts (imports SlotClass from ./tv, * as b24ui from #build/b24ui, * as ComponentTypes from ./index); drop the 3 now-unused imports from useComponentProps.ts + re-type ThemeContext.defaults -> ComputedRef|undefined>>; +export * from './theme' in types/index.ts; Theme.vue + useComponentProps.spec.ts retarget type import to ../types/theme; REMOVE the useComponentProps.ts eslint no-restricted-imports exemption added in #227 (composable no longer touches barrel). Adapted: moved b24ui's OWN 114-key ThemeDefaults verbatim (diverges from upstream: commented-out absent comps, range=slider remap, custom advice/countdown/descriptionList/tableWrapper + deprecated navbar*/sidebar*). Types-only, no snapshot churn. 5 files +new theme.ts, +4/-185" } } } diff --git a/eslint.config.mjs b/eslint.config.mjs index 000034c63..d211e164a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -190,9 +190,4 @@ export default createConfigForNuxt({ ] }] } -}, { - files: ['src/runtime/composables/useComponentProps.ts'], - rules: { - 'no-restricted-imports': 'off' - } }) diff --git a/src/runtime/components/Theme.vue b/src/runtime/components/Theme.vue index 2879cfd41..3b157e434 100644 --- a/src/runtime/components/Theme.vue +++ b/src/runtime/components/Theme.vue @@ -3,7 +3,7 @@ import type { VNode } from 'vue' import { computed } from 'vue' import defu from 'defu' import { injectThemeContext, provideThemeContext } from '../composables/useComponentProps' -import type { ThemeContextDefaults, ThemeDefaults, ThemeUI } from '../composables/useComponentProps' +import type { ThemeContextDefaults, ThemeDefaults, ThemeUI } from '../types/theme' export interface ThemeProps { /** diff --git a/src/runtime/composables/useComponentProps.ts b/src/runtime/composables/useComponentProps.ts index 8e27b753e..22de23ea3 100644 --- a/src/runtime/composables/useComponentProps.ts +++ b/src/runtime/composables/useComponentProps.ts @@ -3,187 +3,10 @@ import { computed, getCurrentInstance } from 'vue' import defu from 'defu' import { createContext } from 'reka-ui' import { useAppConfig } from '#imports' -import type * as ComponentTypes from '../types' -import type { SlotClass } from '../types/tv' -import type * as b24ui from '#build/b24ui' import { get } from '../utils' -type ThemeSlotOverrides = T extends { slots: infer S extends Record } - ? { [K in keyof S]?: SlotClass } - : { [K in keyof T]?: T[K] extends any[] ? SlotClass : T[K] extends Record ? ThemeSlotOverrides : SlotClass } - -/** - * Flat slot-class override shape: `{ button: { base: '...' }, modal: {...} }`. - * Powers the `:b24ui` prop on ``, which remains the recommended way to - * scope class overrides without touching component prop defaults. - */ -export type ThemeUI = { - [K in keyof typeof b24ui]?: ThemeSlotOverrides<(typeof b24ui)[K]> -} - -/** - * Strict per-component defaults shape used by ``. Authored as - * a flat interface with literal keys (rather than a mapped type) so editors — - * Volar in particular — surface key completions inside template inline - * objects (`:props="{ button: { … } }"`). Volar reliably iterates interface - * members but not mapped-type members in this position. - * - * Keys mirror the theme registry exposed by `#build/ui` (see - * `src/templates.ts`): every themable component gets one camelCase entry - * whose value is a `Partial` of that component's `Props`. - */ -export interface ThemeDefaults { - accordion?: Partial - alert?: Partial - // authForm?: Partial - avatar?: Partial - avatarGroup?: Partial - badge?: Partial - banner?: Partial - // blogPost?: Partial - // blogPosts?: Partial - breadcrumb?: Partial - button?: Partial - calendar?: Partial - card?: Partial - // carousel?: Partial - // changelogVersion?: Partial - // changelogVersions?: Partial - chatMessage?: Partial - chatMessages?: Partial - chatPalette?: Partial - chatPrompt?: Partial - chatPromptSubmit?: Partial - chatReasoning?: Partial - chatShimmer?: Partial - chatTool?: Partial - checkbox?: Partial - checkboxGroup?: Partial - chip?: Partial - collapsible?: Partial - colorPicker?: Partial - commandPalette?: Partial - container?: Partial - // contentNavigation?: Partial - contentSearch?: Partial - contentSearchButton?: Partial - contentSurround?: Partial - contentToc?: Partial - contextMenu?: Partial - dashboardGroup?: Partial - dashboardNavbar?: Partial - dashboardPanel?: Partial - dashboardResizeHandle?: Partial - dashboardSearch?: Partial - dashboardSearchButton?: Partial - dashboardSidebar?: Partial - dashboardSidebarCollapse?: Partial - dashboardSidebarToggle?: Partial - dashboardToolbar?: Partial - drawer?: Partial - dropdownMenu?: Partial - editor?: Partial - editorDragHandle?: Partial - editorToolbar?: Partial - empty?: Partial - error?: Partial - fieldGroup?: Partial - fileUpload?: Partial - footer?: Partial - footerColumns?: Partial - // TODO: `FormProps` carries three generics for state, schema, and fields — - // none of which are themable defaults. Loosened to `any` so this entry stays - // assignable from any concrete `Form` instance. - form?: Partial> - formField?: Partial - header?: Partial - input?: Partial - inputDate?: Partial - inputMenu?: Partial - inputNumber?: Partial - inputTags?: Partial - inputTime?: Partial - kbd?: Partial - // listbox?: Partial - main?: Partial - // marquee?: Partial - modal?: Partial - navigationMenu?: Partial - page?: Partial - // pageAnchors?: Partial - pageAside?: Partial - pageBody?: Partial - // pageCTA?: Partial - pageCard?: Partial - pageCardGroup?: Partial - pageColumns?: Partial - pageFeature?: Partial - pageGrid?: Partial - pageHeader?: Partial - // pageHero?: Partial - pageLinks?: Partial - pageList?: Partial - // pageLogos?: Partial - pageSection?: Partial - pagination?: Partial - pinInput?: Partial - popover?: Partial - // pricingPlan?: Partial - // pricingPlans?: Partial - // pricingTable?: Partial - progress?: Partial - radioGroup?: Partial - range?: Partial // this is slider - scrollArea?: Partial - select?: Partial - selectMenu?: Partial - separator?: Partial - sidebar?: Partial - skeleton?: Partial - slideover?: Partial - // slider?: Partial - move to range - stepper?: Partial - switch?: Partial - table?: Partial - tabs?: Partial - textarea?: Partial - timeline?: Partial - toast?: Partial - toaster?: Partial - tooltip?: Partial - // tree?: Partial - user?: Partial - // custom //// - advice?: Partial - countdown?: Partial - descriptionList?: Partial - // modalDialogClose?: Partial - tableWrapper?: Partial - // @deprecated - navbar?: Partial - navbarDivider?: Partial - navbarSection?: Partial - navbarSpacer?: Partial - sidebarBody?: Partial - sidebarFooter?: Partial - sidebarHeader?: Partial - sidebarHeading?: Partial - sidebarLayout?: Partial - sidebarSection?: Partial - sidebarSpacer?: Partial -} - -/** - * Loose internal shape stored on the injected `ThemeContext`. Allows the - * `prose` namespace (lifted by `normalizeUi`) and any unknown keys to flow - * through without polluting the user-facing `ThemeDefaults` type. - */ -export type ThemeContextDefaults = ThemeDefaults & { - [name: string]: Record | undefined -} - export type ThemeContext = { - defaults: ComputedRef + defaults: ComputedRef | undefined>> } const [_injectThemeContext, provideThemeContext] = createContext('B24Theme', 'RootContext') diff --git a/src/runtime/types/index.ts b/src/runtime/types/index.ts index bdca7a9c3..260ec257f 100644 --- a/src/runtime/types/index.ts +++ b/src/runtime/types/index.ts @@ -127,5 +127,6 @@ export * from './input' export * from './icons' export * from './locale' export * from './prose' +export * from './theme' export * from './color-mode' export * from './utils' diff --git a/src/runtime/types/theme.ts b/src/runtime/types/theme.ts new file mode 100644 index 000000000..7914d375b --- /dev/null +++ b/src/runtime/types/theme.ts @@ -0,0 +1,177 @@ +import type { SlotClass } from './tv' +import type * as b24ui from '#build/b24ui' +import type * as ComponentTypes from './index' + +type ThemeSlotOverrides = T extends { slots: infer S extends Record } + ? { [K in keyof S]?: SlotClass } + : { [K in keyof T]?: T[K] extends any[] ? SlotClass : T[K] extends Record ? ThemeSlotOverrides : SlotClass } + +/** + * Flat slot-class override shape: `{ button: { base: '...' }, modal: {...} }`. + * Powers the `:b24ui` prop on ``, which remains the recommended way to + * scope class overrides without touching component prop defaults. + */ +export type ThemeUI = { + [K in keyof typeof b24ui]?: ThemeSlotOverrides<(typeof b24ui)[K]> +} + +/** + * Strict per-component defaults shape used by ``. Authored as + * a flat interface with literal keys (rather than a mapped type) so editors — + * Volar in particular — surface key completions inside template inline + * objects (`:props="{ button: { … } }"`). Volar reliably iterates interface + * members but not mapped-type members in this position. + * + * Keys mirror the theme registry exposed by `#build/ui` (see + * `src/templates.ts`): every themable component gets one camelCase entry + * whose value is a `Partial` of that component's `Props`. + */ +export interface ThemeDefaults { + accordion?: Partial + alert?: Partial + // authForm?: Partial + avatar?: Partial + avatarGroup?: Partial + badge?: Partial + banner?: Partial + // blogPost?: Partial + // blogPosts?: Partial + breadcrumb?: Partial + button?: Partial + calendar?: Partial + card?: Partial + // carousel?: Partial + // changelogVersion?: Partial + // changelogVersions?: Partial + chatMessage?: Partial + chatMessages?: Partial + chatPalette?: Partial + chatPrompt?: Partial + chatPromptSubmit?: Partial + chatReasoning?: Partial + chatShimmer?: Partial + chatTool?: Partial + checkbox?: Partial + checkboxGroup?: Partial + chip?: Partial + collapsible?: Partial + colorPicker?: Partial + commandPalette?: Partial + container?: Partial + // contentNavigation?: Partial + contentSearch?: Partial + contentSearchButton?: Partial + contentSurround?: Partial + contentToc?: Partial + contextMenu?: Partial + dashboardGroup?: Partial + dashboardNavbar?: Partial + dashboardPanel?: Partial + dashboardResizeHandle?: Partial + dashboardSearch?: Partial + dashboardSearchButton?: Partial + dashboardSidebar?: Partial + dashboardSidebarCollapse?: Partial + dashboardSidebarToggle?: Partial + dashboardToolbar?: Partial + drawer?: Partial + dropdownMenu?: Partial + editor?: Partial + editorDragHandle?: Partial + editorToolbar?: Partial + empty?: Partial + error?: Partial + fieldGroup?: Partial + fileUpload?: Partial + footer?: Partial + footerColumns?: Partial + // TODO: `FormProps` carries three generics for state, schema, and fields — + // none of which are themable defaults. Loosened to `any` so this entry stays + // assignable from any concrete `Form` instance. + form?: Partial> + formField?: Partial + header?: Partial + input?: Partial + inputDate?: Partial + inputMenu?: Partial + inputNumber?: Partial + inputTags?: Partial + inputTime?: Partial + kbd?: Partial + // listbox?: Partial + main?: Partial + // marquee?: Partial + modal?: Partial + navigationMenu?: Partial + page?: Partial + // pageAnchors?: Partial + pageAside?: Partial + pageBody?: Partial + // pageCTA?: Partial + pageCard?: Partial + pageCardGroup?: Partial + pageColumns?: Partial + pageFeature?: Partial + pageGrid?: Partial + pageHeader?: Partial + // pageHero?: Partial + pageLinks?: Partial + pageList?: Partial + // pageLogos?: Partial + pageSection?: Partial + pagination?: Partial + pinInput?: Partial + popover?: Partial + // pricingPlan?: Partial + // pricingPlans?: Partial + // pricingTable?: Partial + progress?: Partial + radioGroup?: Partial + range?: Partial // this is slider + scrollArea?: Partial + select?: Partial + selectMenu?: Partial + separator?: Partial + sidebar?: Partial + skeleton?: Partial + slideover?: Partial + // slider?: Partial - move to range + stepper?: Partial + switch?: Partial + table?: Partial + tabs?: Partial + textarea?: Partial + timeline?: Partial + toast?: Partial + toaster?: Partial + tooltip?: Partial + // tree?: Partial + user?: Partial + // custom //// + advice?: Partial + countdown?: Partial + descriptionList?: Partial + // modalDialogClose?: Partial + tableWrapper?: Partial + // @deprecated + navbar?: Partial + navbarDivider?: Partial + navbarSection?: Partial + navbarSpacer?: Partial + sidebarBody?: Partial + sidebarFooter?: Partial + sidebarHeader?: Partial + sidebarHeading?: Partial + sidebarLayout?: Partial + sidebarSection?: Partial + sidebarSpacer?: Partial +} + +/** + * Loose internal shape stored on the injected `ThemeContext`. Allows the + * `prose` namespace (lifted by `normalizeUi`) and any unknown keys to flow + * through without polluting the user-facing `ThemeDefaults` type. + */ +export type ThemeContextDefaults = ThemeDefaults & { + [name: string]: Record | undefined +} diff --git a/test/composables/useComponentProps.spec.ts b/test/composables/useComponentProps.spec.ts index 0c58fd9e9..b3333d825 100644 --- a/test/composables/useComponentProps.spec.ts +++ b/test/composables/useComponentProps.spec.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, test } from 'vitest' import type * as b24ui from '#build/b24ui' -import type { ThemeDefaults } from '../../src/runtime/composables/useComponentProps' +import type { ThemeDefaults } from '../../src/runtime/types/theme' /** * Hand-maintained list of `#build/b24ui` exports that intentionally don't