diff --git a/.github/contributing/component-structure.md b/.github/contributing/component-structure.md index 2d6dab71a3..c48bfe9157 100644 --- a/.github/contributing/component-structure.md +++ b/.github/contributing/component-structure.md @@ -238,6 +238,35 @@ 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`** (Badge, Card, …): nothing to do. Vue's attribute fallthrough already lets the caller's `data-slot` override the static one on the root. This only holds when the template root renders an element: `Button`'s root is a renderless `ULink custom` that hands `$attrs` back as slot props, so its `ULinkBase` needs the default placed before the spread (``), same as the `$attrs` case below. +- **`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 attribute *before* the `v-bind` instead, so a caller value in `$attrs` overrides it: + + ```vue + + + + ``` + + Keep `:id`, `ref` and `v-slot` **before** `data-slot`: `vue/attributes-order` ranks them first, and its autofix moves `data-slot` past the `v-bind` (reverting the override) instead of moving them up. `test/components/DataSlot.spec.ts` catches this, but better not to trip it. + +- **`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 }"`. The same applies when attributes are forwarded from the script, like `Editor` spreading `useAttrs()` into tiptap's `editorProps.attributes`: use `omit(attrs, ['data-slot'])`. + +The rule is enforced by `test/components/DataSlot.spec.ts`, which mounts every component with a caller `data-slot` and asserts it lands exactly once, on the outermost rendered element. + ## Components with Icons ```vue @@ -290,6 +319,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 | Place the default `data-slot` before the root `v-bind`, 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..fd59ec3452 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`, place the default before the root's `v-bind` (`data-slot="root" v-bind="$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 bda4bd6078..db7da49de9 100644 --- a/src/runtime/components/AuthForm.vue +++ b/src/runtime/components/AuthForm.vue @@ -207,7 +207,7 @@ defineExpose({