feat(Theme): override component prop defaults#6031
feat(Theme): override component prop defaults#6031Bobakanoosh wants to merge 25 commits intonuxt:v4from
Conversation
|
@benjamincanac Here is a POC of the Seeking guidance on the following, the types are a bit tricky:
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughReplaces the per-component Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/runtime/composables/useComponentVariant.ts`:
- Around line 32-39: The computed in useComponentVariant currently merges theme
overrides with props.variants which doesn't exist, so component props (e.g.,
color, variant) are ignored and not tracked; update the merge to use the actual
props object (e.g., defu(props, themeOverrides)) so explicit props take
precedence and Vue reactivity tracks prop changes inside the computed returned
by useComponentVariant; optionally, to reduce over-tracking, pick only the
variant-related keys from props before calling defu.
🧹 Nitpick comments (1)
src/runtime/components/Theme.vue (1)
20-26: Consider whether nested<UTheme>should merge contexts.Currently, each
<UTheme>fully replaces both the UI and variant contexts for its descendants. If a user nests<UTheme :variants="{ button: { color: 'red' } }">inside<UTheme :variants="{ button: { variant: 'soft' } }">, the inner theme will lose the outer'svariant: 'soft'.This may be intentional for a POC, but worth considering whether
defu-merging with the parent context (similar to howuseComponentVariantmerges props with theme overrides) would provide a more intuitive cascading behavior.
|
@benjamincanac Any feedback on this POC? Would love to proceed with implementing on all components. Still Seeking guidance on the following, the types are a bit tricky:
|
|
We really need this. |
|
@Bobakanoosh Will look into it asap! |
variants prop
commit: |
|
@Bobakanoosh I pushed some improvements on top of your work to unify theme context. However, before rolling out to all components, I want to think about the broader direction. Right now A few open questions:
Thoughts? cc @sandros94 @jd-solanki |
My personal gut feeling is your second option: a clear separation between the overrides from the global configs (UTheme and UApp respectively) |
|
@sandros94 So you think we should keep UTheme only to override |
@benjamincanac yes indeed |
|
I don't see why not both - the problem The same could potentially be said for component prop defaults. I'd propose: <UTheme
:ui="..."
:variants="..."
:components="{
tooltip: {
delayDuration: 150
}
}"
>
...
</UTheme@benjamincanac in reference to your 1 option:
Following this argument, one could say that we should even have a |
|
Here are my thoughts:
<UTheme
:ui="{}"
:props="{
tooltip: {
delayDuration: 150
}
}"
/>
Instead of per component prop, having single
Supporting global app-wide props is better idea and less confusing than selecting props like variant only. It also becomes AI to understand that
|
Sweeps every component to use `useComponentProps` and the local proxy-aware `useForwardProps`, finalizing the `<UTheme :props>` API introduced in d12961b. - Rename `useComponentUI` to `useComponentProps`. The proxy now drops the unused `_theme` argument and advertises `__v_isReactive` / `__v_raw` so `reactiveOmit`, `reactivePick`, and `toRefs` don't warn when given the proxy. - Move `FormField.orientation`, `RadioGroup.orientation`, and `ChatTool.variant` from `theme.defaultVariants` into `withDefaults` — `theme.defaultVariants` is no longer in the proxy chain. - Loosen Checkbox / RadioGroup template conditions back to `(!props.variant || props.variant === 'list')` so they tolerate `undefined` again. - Pass raw `_props` (not the proxy) to `useFormField`, `useFieldGroup`, and `useAvatarGroup` in 9 components so the wrapping `<UFormField>` / `<UFieldGroup>` / `<UAvatarGroup>` keeps precedence over `<UTheme :props>`. - Add an inline `nuxt-ui/no-bare-prop-refs` ESLint rule with autofix to enforce `props.x` access in templates of components using `useComponentProps`. - Update CLI scaffolds, AGENTS.md, and the contributing guide to match the new pattern.
…rGroup
`useFormField`, `useFieldGroup`, and `useAvatarGroup` rely on the wrapping
component being the closer context (`_props.x ?? injected.x`). These two
tests pin down that wiring so any future regression to passing the proxy
`props` instead of raw `_props` fails loudly:
- `<UFieldGroup size="xl">` beats `<UTheme :props="{ button: { size: 'xs' }}}">`
- `<UAvatarGroup size="xl">` beats `<UTheme :props="{ avatar: { size: 'xs' }}}">`
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/contributing/component-structure.md:
- Around line 60-63: The docs incorrectly imply useComponentProps resolves
theme.file defaultVariants into runtime props; update the guidance to call
useResolvedVariants(name, props, theme, ['variant']) wherever variant values
drive template logic (e.g., <component :is>, v-if, computed) so props.variant
reflects theme-file defaults; mention that tv()/defaultVariants only affect
classes and that app.config.ui.<name>.defaultVariants alone does not populate
props.variant, and show using useResolvedVariants after useComponentProps to
merge runtime-resolved variants.
In `@AGENTS.md`:
- Line 93: Update the documentation to remove "theme.defaultVariants" from the
runtime prop precedence described for useComponentProps(name, _props) — clarify
that theme.defaultVariants only affect tv() class resolution and are not read by
useComponentProps at runtime, and instruct authors to pass raw _props (not the
proxy) to useFormField / useFieldGroup to preserve injection precedence;
additionally add guidance to use useResolvedVariants(name, props, theme,
['variant']) when variant values are needed for template logic (v-if, <component
:is>, computed) and note that tv() defaultVariants do not supply runtime props
such as props.variant.
In `@cli/templates.mjs`:
- Around line 85-87: ${upperName}Props currently extends ${upperName}RootProps
exposing props the scaffold never forwards; change the public type to only
include forwarded keys (e.g. use Pick<${upperName}RootProps, 'as'> or the exact
forwarded keys) so ${upperName}Props matches what rootProps actually forwards,
and update the reactivePick(...) key list to derive from the same forwarded-key
set (ensure both the type alias and the reactivePick invocation reference the
same single source of truth for forwarded prop names).
- Around line 61-66: Detect when the generated component is a prose variant and
prefix lookups with "prose.": change the use of
useComponentProps('${camelName}', _props) and appConfig.ui?.${camelName} in the
ui computed to use the dotted key when prose is set (e.g.,
useComponentProps('prose.${camelName}', _props) and
appConfig.ui?.prose?.${camelName} fallbacking to appConfig.ui?.${camelName}), so
the proxy resolves nested theme/app-config entries; apply the same adjustment to
the other occurrence block (lines 111-119) that builds ui/theme lookups.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ae2d9214-b2e3-44ec-8cd3-70631485c94e
📒 Files selected for processing (7)
.github/contributing/component-structure.mdAGENTS.mdcli/commands/make/component.mjscli/templates.mjscli/utils.mjsdocs/app/components/content/examples/theme/ThemePropsExample.vuedocs/content/docs/2.components/theme.md
… scaffold
Remove `theme.defaultVariants` from the documented `useComponentProps`
priority chain — it only feeds `tv()` class resolution, not runtime props.
Fix CLI scaffold for `--prose` (and `--content`): use dotted lookup
`useComponentProps('prose.<name>', _props)`, nested `appConfig.ui?.prose?.<name>`
fallback, the `'ui.prose'` `ComponentConfig` arg, and `../../` import paths
for the deeper directory.
Switch the `nuxt-ui/no-bare-prop-refs` ESLint rule from a static interface walker (which couldn't follow `extends`/`Pick`/`Omit` past imported types) to a binding-based check: in components using `useComponentProps`, any free identifier in a template that isn't a setup binding, slot-scoped variable, JS global, or PascalCase type reference must be a prop and gets flagged. The rule now catches inherited props from `Omit<LinkProps, …>`, `extends UseComponentIconsProps`, reka-ui `*RootProps`, etc. — exactly the gap that let these bugs slip through `<UTheme :props>`: - Button: `disabled`, `avatar` - Badge: `avatar` - Tabs: `modelValue`, `defaultValue`, `activationMode` - PinInput: `placeholder`, `modelValue`, `defaultValue` - InputNumber: `min`, `max`, `step`, `required` - Tree: `disabled` (×2), `selectionBehavior` - CommandPalette: `loading`, `loadingIcon` (×2), `selectionBehavior` - + 11 more components with assorted single bare refs Each of these silently bypassed the proxy, so `<UTheme :props>` overrides for those props were no-ops. Auto-fixed via `eslint --fix`.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/runtime/components/EditorToolbar.vue (1)
132-138:⚠️ Potential issue | 🟠 MajorUse resolved
props.layoutfor runtime component selection.Line 137 uses
_props.layout, which bypasses theuseComponentPropschain. That makes themed defaults forlayoutineffective at runtime whileuistill resolves fromprops.layout, causing behavior/style mismatch.As per coding guidelines: `src/runtime/components/*.vue`: Wrap raw props with `useComponentProps(name, _props)` to resolve the priority chain (explicit prop > `` > `withDefaults` > `app.config.ui..defaultVariants`).Suggested fix
const Component = computed(() => { return ({ bubble: BubbleMenu, floating: FloatingMenu, fixed: 'template' - }[_props.layout]) + }[props.layout]) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/components/EditorToolbar.vue` around lines 132 - 138, The computed Component currently reads layout directly from _props (bypassing the props resolution chain), so change it to first wrap raw props using useComponentProps with this component's name (e.g., useComponentProps('EditorToolbar', _props)) and then read layout from the resolved props; update the Component computed to use resolvedProps.layout instead of _props.layout so themed/defaulted values from useComponentProps are honored (refer to the computed named Component, the raw _props, and the useComponentProps helper).src/runtime/components/content/ContentSearch.vue (2)
248-253:⚠️ Potential issue | 🟠 MajorRegister the shortcut from
props, not_props.Using
_props.shortcuthere bypasses the resolved priority chain, soUTheme :props="{ contentSearch: { shortcut: 'meta_j' } }"becomes a no-op.Suggested fix
defineShortcuts({ - [_props.shortcut]: { + [props.shortcut]: { usingInput: true, handler: () => open.value = !open.value } })As per coding guidelines,
src/runtime/components/*.vue: Wrap raw props withuseComponentProps(name, _props)to resolve the priority chain (explicit prop ><UTheme :props>>withDefaults>app.config.ui.<name>.defaultVariants).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/components/content/ContentSearch.vue` around lines 248 - 253, The shortcut registration uses raw _props instead of resolved props, so change the code to wrap incoming _props with useComponentProps('ContentSearch', _props) and use that resolvedProps.shortcut when calling defineShortcuts; specifically, create const resolvedProps = useComponentProps('ContentSearch', _props) near the top of the setup and replace _props.shortcut with resolvedProps.shortcut in the defineShortcuts handler (keeping the existing handler logic that toggles open.value).
102-130:⚠️ Potential issue | 🟠 MajorImport the proxy-aware
useForwardPropsfrom composables instead ofreka-ui.Line 98 imports
useForwardPropsfrom'reka-ui', butcommandPalettePropsandmodalProps(lines 129–130) need the repo-local version from'../composables/useForwardProps'. The reka-ui helper filters out values not explicitly passed on the component's vnode, so theme defaults likefullscreen,close, andsearchDelaysupplied throughuseComponentPropsdisappear before reachingUModalandUCommandPalette. The repo-local version forwards all enumerable keys transparently, preserving proxy-injected values. Other similar components (Modal, Tooltip, Toast, Tree) correctly use the composables version.Also,
defineShortcutsreads_props.shortcutinstead ofprops.shortcut, bypassing the theme/app/default priority chain.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/components/content/ContentSearch.vue` around lines 102 - 130, Replace the incorrect import of useForwardProps from 'reka-ui' with the repo-local composable and use it to build commandPaletteProps and modalProps so proxy-injected/theme defaults (e.g., fullscreen, close, searchDelay) are preserved; locate where commandPaletteProps and modalProps are created and switch the imported symbol to ../composables/useForwardProps. Also update defineShortcuts (or the place where shortcuts are derived) to read props.shortcut instead of _props.shortcut so the theme/app/default resolution via useComponentProps is respected.src/runtime/components/DashboardSidebar.vue (1)
72-84:⚠️ Potential issue | 🟡 MinorAlign the
modedefault with the documented API.The prop docs say
modedefaults to'modal', but the runtime default here is'slideover'. That mismatch will surface in generated docs and makes theme-level defaults harder to reason about.As per coding guidelines,
src/runtime/components/*.vue: UsewithDefaults()for runtime prop defaults and JSDoc@defaultValueannotations for documentation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/components/DashboardSidebar.vue` around lines 72 - 84, The runtime default for the mode prop in the withDefaults(defineProps<DashboardSidebarProps<T>>()) block is incorrect—change the value from 'slideover' to 'modal' to match the documented API, and add/update the JSDoc `@defaultValue` 'modal' on the DashboardSidebarProps<T> mode prop declaration so docs and runtime defaults stay in sync.src/runtime/components/InputTags.vue (1)
93-116:⚠️ Potential issue | 🟠 MajorTheme defaults for form-state props are still bypassed here.
useFormField(_props)/useFieldGroup(_props)are fine for preserving raw explicit props, but the finalsize,color,highlight, anddisabledstate never falls back toprops.*. That makes<UTheme :props="{ inputTags: { size: 'lg', color: 'warning', disabled: true } }">a no-op for this component.Suggested fix
-const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formFieldSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputTagsProps>(_props) +const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formFieldSize, color: formColor, id, name, highlight: formHighlight, disabled: formDisabled, ariaAttrs } = useFormField<InputTagsProps>(_props) const { orientation, size: fieldGroupSize } = useFieldGroup<InputTagsProps>(_props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) -const inputSize = computed(() => fieldGroupSize.value || formFieldSize.value) +const inputSize = computed(() => fieldGroupSize.value ?? formFieldSize.value ?? props.size) +const color = computed(() => formColor.value ?? props.color) +const highlight = computed(() => formHighlight.value ?? props.highlight) +const disabled = computed(() => formDisabled.value || props.disabled)As per coding guidelines,
src/runtime/components/*.vue: Wrap raw props withuseComponentProps(name, _props)to resolve the priority chain (explicit prop ><UTheme :props>>withDefaults>app.config.ui.<name>.defaultVariants).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/components/InputTags.vue` around lines 93 - 116, The component is calling useFormField and useFieldGroup with raw _props so theme/default fallbacks from useComponentProps('inputTags', _props) are bypassed; replace the _props arguments with the resolved props variable and ensure the ui and inputSize computed use the resolved values (e.g., use inputSize.value || props.size, color.value || props.color, highlight.value ?? props.highlight, disabled.value ?? props.disabled) so theme defaults from useComponentProps take effect; update calls to useFormField(_props) -> useFormField(props) and useFieldGroup(_props) -> useFieldGroup(props) and adjust the ui computed to fall back to props.* where the form-field/group composables return undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/runtime/components/Button.vue`:
- Around line 62-70: The code currently passes pickLinkProps(props) through the
external useForwardProps which strips theme-injected values; update the import
to use the local helper from ../composables/useForwardProps so link props
injected via useComponentProps(...) are preserved. Specifically, where
useForwardProps is referenced for linkProps (used together with pickLinkProps
and props resolved by useComponentProps), replace the reka-ui import with the
local composable implementation to ensure to/href/target from UTheme :props are
not filtered out.
In `@src/runtime/components/ChatPrompt.vue`:
- Line 114: The template uses a logical OR fallback for the placeholder binding
in ChatPrompt.vue (the :placeholder expression using props.placeholder ||
t('chatPrompt.placeholder')), which treats an explicit empty string as missing;
change it to a nullish coalescing fallback (use ??) so an intentional '' is
preserved while still falling back when props.placeholder is null or undefined.
Ensure the updated expression references props.placeholder ??
t('chatPrompt.placeholder') in the template.
In `@src/runtime/components/ColorPicker.vue`:
- Around line 81-85: The runtime defaults for ColorPicker props were added via
withDefaults(defineProps<ColorPickerProps>()) — specifically throttle: 50 and
defaultValue: '#FFFFFF' — but the JSDoc for those props is missing `@defaultValue`
annotations; update the JSDoc on the ColorPickerProps prop declarations (or the
prop comments above the component) to add `@defaultValue` 50 for throttle and
`@defaultValue` '#FFFFFF' for defaultValue so docs match the runtime defaults.
In `@src/runtime/components/CommandPalette.vue`:
- Line 337: The code reads raw _props (e.g. in const fuseSearchTerm =
refDebounced(searchTerm, () => _props.searchDelay)) which bypasses
theme/priority resolution; wrap _props with useComponentProps("CommandPalette",
_props) and replace uses of _props (searchDelay, labelKey, and the other
occurrences around the fuse setup) with the resolved props (props.searchDelay,
props.labelKey) so debounce timing and highlight generation honor UTheme and
other prop priority sources; update all instances mentioned (including the reads
at the later 355–356 region) to use the resolved props.
In `@src/runtime/components/PinInput.vue`:
- Line 77: Wrap the raw _props with useComponentProps and pass the resolved
props into useFormField so theme/inherited values flow correctly: call const
resolved = useComponentProps('pinInput', _props) (or appropriate component name
used in your UI config) and replace useFormField<PinInputProps>(_props) with
useFormField<PinInputProps>(resolved) so color, size, highlight, id, name,
disabled, etc. come from the full priority chain.
---
Outside diff comments:
In `@src/runtime/components/content/ContentSearch.vue`:
- Around line 248-253: The shortcut registration uses raw _props instead of
resolved props, so change the code to wrap incoming _props with
useComponentProps('ContentSearch', _props) and use that resolvedProps.shortcut
when calling defineShortcuts; specifically, create const resolvedProps =
useComponentProps('ContentSearch', _props) near the top of the setup and replace
_props.shortcut with resolvedProps.shortcut in the defineShortcuts handler
(keeping the existing handler logic that toggles open.value).
- Around line 102-130: Replace the incorrect import of useForwardProps from
'reka-ui' with the repo-local composable and use it to build commandPaletteProps
and modalProps so proxy-injected/theme defaults (e.g., fullscreen, close,
searchDelay) are preserved; locate where commandPaletteProps and modalProps are
created and switch the imported symbol to ../composables/useForwardProps. Also
update defineShortcuts (or the place where shortcuts are derived) to read
props.shortcut instead of _props.shortcut so the theme/app/default resolution
via useComponentProps is respected.
In `@src/runtime/components/DashboardSidebar.vue`:
- Around line 72-84: The runtime default for the mode prop in the
withDefaults(defineProps<DashboardSidebarProps<T>>()) block is incorrect—change
the value from 'slideover' to 'modal' to match the documented API, and
add/update the JSDoc `@defaultValue` 'modal' on the DashboardSidebarProps<T> mode
prop declaration so docs and runtime defaults stay in sync.
In `@src/runtime/components/EditorToolbar.vue`:
- Around line 132-138: The computed Component currently reads layout directly
from _props (bypassing the props resolution chain), so change it to first wrap
raw props using useComponentProps with this component's name (e.g.,
useComponentProps('EditorToolbar', _props)) and then read layout from the
resolved props; update the Component computed to use resolvedProps.layout
instead of _props.layout so themed/defaulted values from useComponentProps are
honored (refer to the computed named Component, the raw _props, and the
useComponentProps helper).
In `@src/runtime/components/InputTags.vue`:
- Around line 93-116: The component is calling useFormField and useFieldGroup
with raw _props so theme/default fallbacks from useComponentProps('inputTags',
_props) are bypassed; replace the _props arguments with the resolved props
variable and ensure the ui and inputSize computed use the resolved values (e.g.,
use inputSize.value || props.size, color.value || props.color, highlight.value
?? props.highlight, disabled.value ?? props.disabled) so theme defaults from
useComponentProps take effect; update calls to useFormField(_props) ->
useFormField(props) and useFieldGroup(_props) -> useFieldGroup(props) and adjust
the ui computed to fall back to props.* where the form-field/group composables
return undefined.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2243f452-be06-457f-a74c-c6f4cdf57167
📒 Files selected for processing (20)
eslint.config.mjssrc/runtime/components/Accordion.vuesrc/runtime/components/Badge.vuesrc/runtime/components/Button.vuesrc/runtime/components/ChatMessage.vuesrc/runtime/components/ChatPrompt.vuesrc/runtime/components/ChatReasoning.vuesrc/runtime/components/ChatTool.vuesrc/runtime/components/ColorPicker.vuesrc/runtime/components/CommandPalette.vuesrc/runtime/components/DashboardPanel.vuesrc/runtime/components/DashboardSearch.vuesrc/runtime/components/DashboardSidebar.vuesrc/runtime/components/EditorToolbar.vuesrc/runtime/components/InputNumber.vuesrc/runtime/components/InputTags.vuesrc/runtime/components/PinInput.vuesrc/runtime/components/Tabs.vuesrc/runtime/components/Tree.vuesrc/runtime/components/content/ContentSearch.vue
A handful of components were still reading `_props.X` (raw) inside their `<script>` setup. Because the `nuxt-ui/no-bare-prop-refs` ESLint rule only walks templates, these silently bypassed the `useComponentProps` proxy and never picked up `<UTheme :props>` defaults: - `CommandPalette`: `searchDelay`, `labelKey` (×2) — fuzzy-search debounce and highlight pipeline now honor theme defaults. - `ContentSearch`: `shortcut` — `defineShortcuts` key now flows through the proxy. - `EditorToolbar`: `layout` — picks bubble/floating/fixed via theme. - `Form`: `loadingAuto`. - `Textarea`: `rows` (×2). The components above pass an explicit generic to `useComponentProps` (`useComponentProps<XProps<T>>(...)`) which drops the `withDefaults` narrowing, so TS sees `T['X'] | undefined`. The proxy guarantees the resolved value is non-undefined whenever `withDefaults` provides a default, hence the `!` non-null assertions. Also: - `Button` and `ContentSearch` now import `useForwardProps` from the local proxy-aware composable. Reka-UI's version filters by `vnode.props ∪ vm.type.props.<key>.default` and was silently dropping theme-supplied `to`/`href`/`target` (Button) and `fullscreen`/`close` /`searchDelay` (ContentSearch). Boolean leak risk no longer applies since the proxy gates the `withDefaults` fallback. - `ChatPrompt`: `props.placeholder || t(...)` → `??` so an explicit empty string isn't replaced by the i18n fallback. - `ColorPicker`: add `@defaultValue` JSDoc for `throttle` and `defaultValue` to match `withDefaults`. - `useComponentProps`: add a JSDoc note clarifying that `ownKeys` / `has` only enumerate the underlying schema (theme defaults aren't enumerable) — the contract our local `useForwardProps` relies on. - `ThemeDefaults['form']`: add a `// TODO` explaining the `<any, any, any>` is intentional (state/schema/fields generics aren't themable).
- Carousel: switch `_props.autoplay` / `_props.autoScroll` to `props.X` with optional chaining so the autoplay/autoScroll plugins honor `<UTheme :props>` defaults. Adds `?.stopOnInteraction ?? true` which also hardens the path against an undefined value reaching `.stopOnInteraction`. - DashboardSidebar: JSDoc said `@defaultValue 'modal'` but `withDefaults` and the published docs both say `'slideover'`. Update the JSDoc to match runtime, not the other way around — flipping the runtime would silently change the default mobile menu shape for every consumer.
`_props.level` (×6 in the template) bypassed the `useComponentProps` proxy. The `nuxt-ui/no-bare-prop-refs` ESLint rule explicitly skips identifiers starting with `_`, so this slipped past the autofixer. Switch all reads to `props.level!` for consistency with the rest of the migration — `level` has a `withDefaults` default of `0`, so the non-null assertion is runtime-safe.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/runtime/composables/useComponentProps.ts (2)
260-266: Clarify the distinction betweenpropDef.defaultandwithDefaultsin the comment.The comment on lines 255-259 mentions
withDefaults, but the check on line 261 looks forpropDef.default. WhilewithDefaultsdoes populatepropDef.defaultat runtime, this subtlety could confuse future maintainers. Consider refining the comment to explicitly note thatwithDefaultssetspropDef.defaultunder the hood.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/composables/useComponentProps.ts` around lines 260 - 266, Update the comment in useComponentProps above the check that reads propDef.default to explicitly state that Vue's withDefaults (used when defining components) populates the runtime prop definition's default field, so the code correctly checks (vm?.type as any)?.props?.[prop] and its own 'default' property; mention both symbols: propDef and withDefaults, and clarify that the runtime check for propDef.default is effectively detecting defaults provided via withDefaults rather than only static prop options.
203-207:propIsDefinedmay miss explicitly-passedundefinedvalues.The check
vnode.props[...] !== undefinedcannot distinguish between "prop not passed" and "prop explicitly passed asundefined". While rare, if a consumer writes:color="undefined"expecting to clear a theme default, this check will treat it as "not defined" and fall through to theme defaults.This is a known limitation of Vue's vnode inspection and may be acceptable, but worth documenting in the JSDoc.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/composables/useComponentProps.ts` around lines 203 - 207, Update the JSDoc for the function propIsDefined to document that vnode.props inspection cannot distinguish a prop that was not passed from one explicitly passed as undefined; reference the function name propIsDefined and the use of vnode.props[camelCase(prop)] / vnode.props[kebabCase(prop)] in the note, explain that an explicitly-passed undefined value will be treated as "not defined" by this check and recommend workarounds (e.g., pass null or an explicit flag) for callers who need to clear defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/runtime/composables/useComponentProps.ts`:
- Around line 260-266: Update the comment in useComponentProps above the check
that reads propDef.default to explicitly state that Vue's withDefaults (used
when defining components) populates the runtime prop definition's default field,
so the code correctly checks (vm?.type as any)?.props?.[prop] and its own
'default' property; mention both symbols: propDef and withDefaults, and clarify
that the runtime check for propDef.default is effectively detecting defaults
provided via withDefaults rather than only static prop options.
- Around line 203-207: Update the JSDoc for the function propIsDefined to
document that vnode.props inspection cannot distinguish a prop that was not
passed from one explicitly passed as undefined; reference the function name
propIsDefined and the use of vnode.props[camelCase(prop)] /
vnode.props[kebabCase(prop)] in the note, explain that an explicitly-passed
undefined value will be treated as "not defined" by this check and recommend
workarounds (e.g., pass null or an explicit flag) for callers who need to clear
defaults.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e5c1821a-8c39-4f39-87b2-f4d572f68e0d
📒 Files selected for processing (12)
src/runtime/components/Button.vuesrc/runtime/components/Carousel.vuesrc/runtime/components/ChatPrompt.vuesrc/runtime/components/ColorPicker.vuesrc/runtime/components/CommandPalette.vuesrc/runtime/components/DashboardSidebar.vuesrc/runtime/components/EditorToolbar.vuesrc/runtime/components/Form.vuesrc/runtime/components/Textarea.vuesrc/runtime/components/content/ContentNavigation.vuesrc/runtime/components/content/ContentSearch.vuesrc/runtime/composables/useComponentProps.ts
✅ Files skipped from review due to trivial changes (1)
- src/runtime/components/ChatPrompt.vue
🚧 Files skipped from review as they are similar to previous changes (1)
- src/runtime/components/ColorPicker.vue
Add proxy fallback at the tv() call site so theme size/color/highlight apply when no closer context (UFormField, UFieldGroup, UAvatarGroup) wraps the component: size: size.value ?? props.size, color: color.value ?? props.color, highlight: highlight.value ?? props.highlight Final precedence: explicit > closer-context > <UTheme :props> > withDefaults > app.config > tv defaults.
…onents that do use `useComponentProps`
Resolves #6359, resolves #2662, resolves #6335
❓ Type of change
📚 Description
Adds a
:propsprop to<UTheme>to override the default value of any prop on descendant components. Each key is typed against that component's ownPropsinterface.📝 Checklist