From 17ef4428a3ee1d1d6cd686e1763d6f884ab8bf8b Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Sat, 27 Jun 2026 00:09:08 +0200 Subject: [PATCH 1/5] fix: forward caller-supplied data-slot to component root Make data-slot handling consistent across all components: a caller-supplied data-slot now always wins on the component root (with the component's own root/base value as fallback), and inner elements keep their own data-slot. Previously inheritAttrs: false components either dropped the caller's data-slot from the root or let it leak onto an inner element. --- .github/contributing/component-structure.md | 28 ++ AGENTS.md | 1 + src/runtime/components/AuthForm.vue | 4 +- src/runtime/components/Avatar.vue | 4 +- src/runtime/components/Banner.vue | 4 +- src/runtime/components/BlogPost.vue | 4 +- src/runtime/components/ChangelogVersion.vue | 4 +- src/runtime/components/ChatPrompt.vue | 2 +- src/runtime/components/Checkbox.vue | 2 +- src/runtime/components/Chip.vue | 4 +- src/runtime/components/CommandPalette.vue | 2 +- src/runtime/components/DashboardNavbar.vue | 2 +- src/runtime/components/DashboardPanel.vue | 2 +- src/runtime/components/DashboardSidebar.vue | 2 +- src/runtime/components/Editor.vue | 2 +- src/runtime/components/EditorDragHandle.vue | 2 +- src/runtime/components/FileUpload.vue | 4 +- src/runtime/components/Header.vue | 2 +- src/runtime/components/Input.vue | 4 +- src/runtime/components/InputDate.vue | 3 +- src/runtime/components/InputMenu.vue | 4 +- src/runtime/components/InputNumber.vue | 2 +- src/runtime/components/InputRating.vue | 3 +- src/runtime/components/InputTags.vue | 2 +- src/runtime/components/InputTime.vue | 3 +- src/runtime/components/Listbox.vue | 3 +- src/runtime/components/NavigationMenu.vue | 2 +- src/runtime/components/PageCard.vue | 4 +- src/runtime/components/PageFeature.vue | 4 +- src/runtime/components/PageLogos.vue | 2 +- src/runtime/components/PricingPlan.vue | 2 +- src/runtime/components/PricingTable.vue | 2 +- src/runtime/components/Separator.vue | 2 +- src/runtime/components/Sidebar.vue | 4 +- src/runtime/components/Switch.vue | 2 +- src/runtime/components/Table.vue | 2 +- src/runtime/components/Textarea.vue | 4 +- src/runtime/components/Tree.vue | 3 +- src/runtime/components/User.vue | 4 +- .../components/content/ContentNavigation.vue | 2 +- .../components/content/ContentSurround.vue | 2 +- src/runtime/components/content/ContentToc.vue | 2 +- test/components/Theme.spec.ts | 2 +- .../__snapshots__/Alert-vue.spec.ts.snap | 2 +- .../__snapshots__/Alert.spec.ts.snap | 2 +- .../__snapshots__/AuthForm-vue.spec.ts.snap | 82 ++-- .../__snapshots__/AuthForm.spec.ts.snap | 82 ++-- .../AvatarGroup-vue.spec.ts.snap | 42 +-- .../__snapshots__/AvatarGroup.spec.ts.snap | 42 +-- .../__snapshots__/Badge-vue.spec.ts.snap | 4 +- .../__snapshots__/Badge.spec.ts.snap | 4 +- .../__snapshots__/BlogPost-vue.spec.ts.snap | 34 +- .../__snapshots__/BlogPost.spec.ts.snap | 34 +- .../__snapshots__/BlogPosts-vue.spec.ts.snap | 8 +- .../__snapshots__/BlogPosts.spec.ts.snap | 8 +- .../__snapshots__/Breadcrumb-vue.spec.ts.snap | 22 +- .../__snapshots__/Breadcrumb.spec.ts.snap | 22 +- .../__snapshots__/Button-vue.spec.ts.snap | 6 +- .../__snapshots__/Button.spec.ts.snap | 6 +- .../ChangelogVersion-vue.spec.ts.snap | 2 +- .../ChangelogVersion.spec.ts.snap | 2 +- .../ChangelogVersions-vue.spec.ts.snap | 12 +- .../ChangelogVersions.spec.ts.snap | 12 +- .../ChatMessage-vue.spec.ts.snap | 2 +- .../__snapshots__/ChatMessage.spec.ts.snap | 2 +- .../ChatMessages-vue.spec.ts.snap | 4 +- .../__snapshots__/ChatMessages.spec.ts.snap | 4 +- .../__snapshots__/ChatPrompt-vue.spec.ts.snap | 22 +- .../__snapshots__/ChatPrompt.spec.ts.snap | 22 +- .../CheckboxGroup-vue.spec.ts.snap | 234 ++++++------ .../__snapshots__/CheckboxGroup.spec.ts.snap | 234 ++++++------ .../CommandPalette-vue.spec.ts.snap | 350 +++++++++--------- .../__snapshots__/CommandPalette.spec.ts.snap | 350 +++++++++--------- .../DashboardPanel-vue.spec.ts.snap | 24 +- .../__snapshots__/DashboardPanel.spec.ts.snap | 24 +- .../DashboardSearch-vue.spec.ts.snap | 28 +- .../DashboardSearch.spec.ts.snap | 28 +- .../DashboardSidebar-vue.spec.ts.snap | 44 +-- .../DashboardSidebar.spec.ts.snap | 44 +-- .../DropdownMenu-vue.spec.ts.snap | 46 +-- .../__snapshots__/DropdownMenu.spec.ts.snap | 46 +-- .../__snapshots__/Empty-vue.spec.ts.snap | 44 +-- .../__snapshots__/Empty.spec.ts.snap | 44 +-- .../__snapshots__/FieldGroup-vue.spec.ts.snap | 14 +- .../__snapshots__/FieldGroup.spec.ts.snap | 14 +- .../__snapshots__/FileUpload-vue.spec.ts.snap | 160 ++++---- .../__snapshots__/FileUpload.spec.ts.snap | 160 ++++---- .../__snapshots__/Form-vue.spec.ts.snap | 48 +-- .../__snapshots__/Form.spec.ts.snap | 48 +-- .../__snapshots__/Input-vue.spec.ts.snap | 104 +++--- .../__snapshots__/Input.spec.ts.snap | 104 +++--- .../__snapshots__/InputDate-vue.spec.ts.snap | 2 +- .../__snapshots__/InputDate.spec.ts.snap | 2 +- .../__snapshots__/InputMenu-vue.spec.ts.snap | 152 ++++---- .../__snapshots__/InputMenu.spec.ts.snap | 152 ++++---- .../InputRating-vue.spec.ts.snap | 4 +- .../__snapshots__/InputRating.spec.ts.snap | 4 +- .../__snapshots__/InputTime-vue.spec.ts.snap | 2 +- .../__snapshots__/InputTime.spec.ts.snap | 2 +- .../__snapshots__/Listbox-vue.spec.ts.snap | 4 +- .../__snapshots__/Listbox.spec.ts.snap | 4 +- .../NavigationMenu-vue.spec.ts.snap | 6 +- .../__snapshots__/NavigationMenu.spec.ts.snap | 6 +- .../__snapshots__/PageLogos-vue.spec.ts.snap | 10 +- .../__snapshots__/PageLogos.spec.ts.snap | 10 +- .../PricingPlan-vue.spec.ts.snap | 74 ++-- .../__snapshots__/PricingPlan.spec.ts.snap | 74 ++-- .../PricingPlans-vue.spec.ts.snap | 48 +-- .../__snapshots__/PricingPlans.spec.ts.snap | 48 +-- .../__snapshots__/Select-vue.spec.ts.snap | 6 +- .../__snapshots__/Select.spec.ts.snap | 6 +- .../__snapshots__/SelectMenu-vue.spec.ts.snap | 126 +++---- .../__snapshots__/SelectMenu.spec.ts.snap | 126 +++---- .../__snapshots__/Separator-vue.spec.ts.snap | 2 +- .../__snapshots__/Separator.spec.ts.snap | 2 +- .../__snapshots__/Tabs-vue.spec.ts.snap | 48 +-- .../__snapshots__/Tabs.spec.ts.snap | 48 +-- .../__snapshots__/Textarea-vue.spec.ts.snap | 104 +++--- .../__snapshots__/Textarea.spec.ts.snap | 104 +++--- .../__snapshots__/Timeline-vue.spec.ts.snap | 280 +++++++------- .../__snapshots__/Timeline.spec.ts.snap | 280 +++++++------- .../__snapshots__/Toast-vue.spec.ts.snap | 2 +- .../__snapshots__/Toast.spec.ts.snap | 2 +- .../__snapshots__/User-vue.spec.ts.snap | 40 +- .../__snapshots__/User.spec.ts.snap | 40 +- .../__snapshots__/ContentSearch.spec.ts.snap | 30 +- 126 files changed, 2347 insertions(+), 2323 deletions(-) diff --git a/.github/contributing/component-structure.md b/.github/contributing/component-structure.md index 2d6dab71a3..4fbd7734e4 100644 --- a/.github/contributing/component-structure.md +++ b/.github/contributing/component-structure.md @@ -238,6 +238,33 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.input || {}) The same `?? props.X` pattern applies to `useAvatarGroup` (`size`) and any other context composable whose contract is `props?.x ?? injected.x`. The composable itself stays untouched — the fallback lives at the `tv()` call site so the wrapper-vs-theme precedence is explicit and reviewable. +## `data-slot` on the root + +Parents label a child by passing `data-slot` (e.g. ``, ``). The rule is: **a caller-supplied `data-slot` always wins on the component's root element**, with the component's own value (`root`/`base`) as the fallback. Inner elements keep their own `data-slot`. + +How you achieve it depends on how the root receives attributes: + +- **Single root, default `inheritAttrs`** (Button, Badge, Card, …): nothing to do. Vue's attribute fallthrough already lets the caller's `data-slot` override the static one on the root. +- **`inheritAttrs: false`, `$attrs` spread on the root**: fallthrough is off, so a static `data-slot="root"` placed *after* `v-bind` would win over the caller. Put the default *before* the spread instead, so a caller value in `$attrs` overrides it: + + ```vue + + + + + + ``` + +- **`inheritAttrs: false`, `$attrs` forwarded to an inner element** (Avatar, Input, Checkbox, Switch, …): the root never receives `$attrs`, so read the caller's value on the root explicitly, and keep each inner element's own `data-slot` *after* its `$attrs` spread so the caller's value does not leak onto it: + + ```vue + + + + ``` + + For `` forwards and inner elements that have no `data-slot` of their own, strip it from what you forward so it cannot leak: `v-bind="{ ...$attrs, 'data-slot': undefined }"`. + ## Components with Icons ```vue @@ -290,6 +317,7 @@ Notes: | `useForwardProps(source, emits?)` (local) | Forward Reka UI props/emits without filtering theme defaults | | `withDefaults` | Runtime default values | | `defineOptions({ inheritAttrs: false })` | When spreading `$attrs` to inner element | +| Caller `data-slot` wins on root | Fold default into the root `v-bind` before `...$attrs`, or read `$attrs['data-slot']` on the root — see [`data-slot` on the root](#data-slot-on-the-root) | | `reactivePick` | Pick keys off `props` (the proxy) before forwarding | | `createReusableTemplate` | Complex template reuse (Table, Modal) | | `useTemplateRef` | Template refs (Vue 3.5+) | diff --git a/AGENTS.md b/AGENTS.md index 3d662e7359..e857f94825 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,6 +89,7 @@ Load these based on your task. **Do not load all files at once** — only load w | Type imports | Always separate: `import type { X }` on its own line | | Props defaults | Use `withDefaults()` for runtime, JSDoc `@defaultValue` for docs | | Template slots | Add `data-slot="name"` attributes on all elements | +| `data-slot` on root | A caller-supplied `data-slot` must win on the component's **root** (component's own value as fallback); inner elements keep theirs. Single-root components with default `inheritAttrs` get this free via Vue fallthrough. For `inheritAttrs: false`, fold the default into the root's `v-bind` before `...$attrs` (`v-bind="{ 'data-slot': 'root', ...$attrs }"`), or read `($attrs['data-slot'] as string \| undefined) ?? 'root'` on the root when `$attrs` is forwarded to an inner element. See [component-structure.md](.github/contributing/component-structure.md#data-slot-on-the-root). | | Computed ui | Always use `computed(() => tv(...))` for reactive theming | | Theme defaults | Wrap raw props with `useComponentProps(name, _props)` to resolve the priority chain (explicit prop > `` > `withDefaults` > `app.config.ui..defaultVariants`). The proxy deep-merges `ui` automatically — read `props.ui?.` in templates. `theme.defaultVariants` is **not** read by the proxy — it only feeds `tv()` class resolution. Pass the **raw** `_props` (not the proxy) to `useFormField` / `useFieldGroup` / `useAvatarGroup` so their injection precedence (closer context wins) stays correct. | | Form/group fallback | When consuming `size` / `color` / `highlight` from `useFormField`, `useFieldGroup`, or `useAvatarGroup`, always fall back to the proxy in `tv()` calls: `size: size.value ?? props.size`, `color: color.value ?? props.color`, `highlight: highlight.value ?? props.highlight`. This gives the full precedence `explicit > group/formField > > undefined`. Without the `?? props.X` fallback, `` is silently dropped when the closer context (FormField/FieldGroup/AvatarGroup) is absent. | diff --git a/src/runtime/components/AuthForm.vue b/src/runtime/components/AuthForm.vue index 07c3190cd3..1151f96a50 100644 --- a/src/runtime/components/AuthForm.vue +++ b/src/runtime/components/AuthForm.vue @@ -198,7 +198,7 @@ defineExpose({