You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .claude/rules/components.md
+51-7Lines changed: 51 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -279,18 +279,62 @@ When a composable uses `useProxyModel`, its underlying registry/model must have
279
279
280
280
Never `export *` from `.vue` files — breaks Volar slot type inference. [intent:184, intent:338]
281
281
282
+
A barrel is one of two shapes — **compound** or **single** — chosen by whether the component exposes dotted sub-components. Audited 100% consistent across all 39 component barrels (the one historical violator, Tooltip's `Object.assign`, was normalized to this canon).
- The compound is a **plain object literal** built from short-aliased default imports. **Never `Object.assign`**, and never re-bind the compound name to a `.vue` default — the namespace object is *not* a renderable component. `<Component>` (bare) is invalid; only `<Component.Root>` renders.
314
+
- Every member of the literal carries a one-line `/** */` with `@example` (it surfaces in `<DocsApi />`). Member order is anatomy/usage order — the Root (or the outer region provider) first — not alphabetical.
315
+
-`// Context` is the (de-facto, slightly misnamed) header over the short-aliased sub-component imports — keep it for family consistency.
316
+
-`perfectionist/sort-imports` settles ordering *within* each export/import block; run `pnpm lint:fix`, don't hand-author order.
317
+
318
+
### Single (one renderable component — providers like Theme/Locale/Scrim, primitives like Atom/Form/Portal)
Type re-export at the top, JSDoc, single default at the bottom. No compound object, no `// Context` imports, no provide/use.
331
+
332
+
### Region/scope providers belong to the compound, never the namespace
333
+
334
+
A wrapper that supplies shared defaults or context to a compound family's Roots (delay defaults, a selection group, a notification queue) is a **dotted member** of the compound — living in its own `Component<Member>.vue` file and named-exported like any other sub-component. It is **never** the bare renderable `<Component>`, and the compound is never made renderable via `Object.assign` to host it. Precedent: `ExpansionPanel.Group`, `Toggle.Group`, `Switch.Group`, `Radio.Group`, `Checkbox.Group`, `Button.Group`, `Snackbar.Queue`.
335
+
336
+
**Naming.** Name the member with the **domain noun** for what it coordinates — `.Group` (a selection group), `.Queue` (a notification queue). When the member is a *pure* context/defaults provider with no domain verb of its own, `.Provider` is acceptable, mirroring the upstream React-ecosystem `.Provider` wrappers. Prefer a domain noun where one fits; reach for `.Provider` only for a defaults-only wrapper. No shipped v0 compound uses `.Provider` today — app-wide defaults are supplied by a framework plugin (e.g. `createTooltipPlugin`) rather than a Provider component, which is the v0 analog of the React-ecosystem Provider.
Hover a button and wait, then glide to its neighbors — they open instantly. One tooltip plugin keeps the whole toolbar's region warm, so only the first hover waits the open delay.
CloseScheduled -- "pointerenter (interactive content)" --> Open
57
+
```
58
+
59
+
## Examples
60
+
61
+
::: example
62
+
/components/tooltip/TooltipButton.vue 1
63
+
/components/tooltip/toolbar.vue 2
64
+
65
+
### Coordinated toolbar
66
+
67
+
A single tooltip hides one of v0's better tricks: the `createTooltipPlugin` registry coordinates *every* tooltip in the app from one place. Hover a toolbar button and wait out the open delay, then slide to a neighbor — it opens instantly, because the shared region is already warm. Without the plugin each tooltip would re-wait its own delay on every move, so the toolbar would feel sluggish. That shared skip-window is exactly why open and close timing lives in one plugin instead of per-tooltip state.
68
+
69
+
The `Link` control adds `interactive`: its content stays open while you move the pointer in to copy the URL, and Copy flips to a `bg-success` / `text-on-success` confirmation that stays legible in both light and dark themes. Reach for `interactive` only when the content genuinely needs pointer access — the strict WAI-ARIA APG tooltip pattern forbids focusable content, so a richer surface may belong in a future `HoverCard` instead.
70
+
71
+
| File | Role |
72
+
|------|------|
73
+
|`TooltipButton.vue`| Reusable wrapper pairing a `Button` activator with its tooltip |
74
+
|`toolbar.vue`| Formatting toolbar wiring several coordinated tooltips plus one interactive `Link`|
75
+
76
+
:::
77
+
78
+
## Accessibility
79
+
80
+
| Concern | Behavior |
81
+
|---------|----------|
82
+
| Role | Content renders `role="tooltip"`|
83
+
| Linkage | Activator always carries `aria-describedby={contentId}` so screen readers announce the description on focus |
84
+
| Keyboard | Focus opens instantly (no delay), Escape closes; Enter / Space activate the underlying control, which closes via click |
85
+
| Touch | Tooltips are not shown on touch interactions per the WAI-ARIA APG |
86
+
| Hoverable content | Off by default; opt-in with `interactive` on `<Tooltip.Root>`|
87
+
88
+
## FAQ
89
+
90
+
::: faq
91
+
92
+
??? Why don't tooltips show on touch?
93
+
94
+
Touch devices have no hover state, and showing a tooltip on tap competes with whatever action the underlying control performs. Both React Aria and the WAI-ARIA Authoring Practices Guide recommend skipping tooltips on touch and ensuring the UI is usable without them. v0 follows this guidance.
95
+
96
+
??? How do I set default open and close delays?
97
+
98
+
Two layers, and the narrower one wins. App-wide: install the plugin — `app.use(createTooltipPlugin({ openDelay: 500, closeDelay: 150 }))`. One tooltip: set props on its Root — `<Tooltip.Root :open-delay="0">`. Warmup coordination stays shared across every tooltip through the plugin registry. With no plugin installed it still works, falling back to the documented defaults.
99
+
100
+
??? Why doesn't Tooltip.Activator open when I focus it via mouse click?
101
+
102
+
The activator gates focus-driven opens on `:focus-visible`. A pointer click that incidentally moves focus into the activator does not match `:focus-visible`, so it doesn't open the tooltip. Keyboard-driven focus (Tab) sets `:focus-visible` and opens the tooltip instantly.
103
+
104
+
??? How do I render a non-button activator?
105
+
106
+
The activator defaults to `as="button"`; pass `as="a"`, `as="div"`, etc. to render a different element. Always ensure the activator is keyboard-focusable (`tabindex="0"` on a non-button if needed).
0 commit comments