harebear-ui 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/AGENTS.md +2 -2
  2. package/dist/components/admin/app-nav.d.ts +15 -1
  3. package/dist/components/admin/app-nav.js +28 -11
  4. package/dist/components/admin/avatar-upload.d.ts +32 -0
  5. package/dist/components/admin/avatar-upload.js +121 -0
  6. package/dist/components/admin/category.d.ts +17 -0
  7. package/dist/components/admin/category.js +35 -0
  8. package/dist/components/admin/celebration.d.ts +7 -0
  9. package/dist/components/admin/celebration.js +61 -0
  10. package/dist/components/admin/checklist-editor.d.ts +20 -0
  11. package/dist/components/admin/checklist-editor.js +69 -0
  12. package/dist/components/admin/color-swatch-picker.d.ts +15 -0
  13. package/dist/components/admin/color-swatch-picker.js +68 -0
  14. package/dist/components/admin/comment-thread.d.ts +26 -0
  15. package/dist/components/admin/comment-thread.js +68 -0
  16. package/dist/components/admin/copy-field.d.ts +3 -1
  17. package/dist/components/admin/copy-field.js +10 -5
  18. package/dist/components/admin/date-time-picker.d.ts +20 -0
  19. package/dist/components/admin/date-time-picker.js +97 -0
  20. package/dist/components/admin/detail-sheet.d.ts +18 -0
  21. package/dist/components/admin/detail-sheet.js +80 -0
  22. package/dist/components/admin/markdown-editor.d.ts +12 -0
  23. package/dist/components/admin/markdown-editor.js +46 -0
  24. package/dist/components/admin/page-header.d.ts +3 -1
  25. package/dist/components/admin/page-header.js +17 -9
  26. package/dist/components/admin/page.d.ts +16 -0
  27. package/dist/components/admin/page.js +42 -0
  28. package/dist/components/admin/prompt-dialog.d.ts +19 -0
  29. package/dist/components/admin/prompt-dialog.js +83 -0
  30. package/dist/components/admin/settings.d.ts +4 -2
  31. package/dist/components/admin/settings.js +37 -30
  32. package/dist/components/admin/time-zone-picker.d.ts +12 -0
  33. package/dist/components/admin/time-zone-picker.js +31 -0
  34. package/dist/components/admin/user-chip.d.ts +34 -3
  35. package/dist/components/admin/user-chip.js +66 -7
  36. package/dist/components/admin/user-menu.d.ts +6 -1
  37. package/dist/components/admin/user-menu.js +22 -10
  38. package/dist/components/ai/ask-box.d.ts +14 -2
  39. package/dist/components/ai/ask-box.js +90 -54
  40. package/dist/components/ai/assistant.d.ts +13 -3
  41. package/dist/components/ai/assistant.js +36 -8
  42. package/dist/components/ai/code-block.d.ts +10 -3
  43. package/dist/components/ai/code-block.js +57 -8
  44. package/dist/components/ai/conversation-list.d.ts +13 -3
  45. package/dist/components/ai/conversation-list.js +15 -26
  46. package/dist/components/ai/markdown.d.ts +13 -0
  47. package/dist/components/ai/markdown.js +41 -0
  48. package/dist/components/auth/auth.d.ts +41 -5
  49. package/dist/components/auth/auth.js +167 -159
  50. package/dist/components/data/agenda-row.d.ts +26 -0
  51. package/dist/components/data/agenda-row.js +62 -0
  52. package/dist/components/data/day-strip.d.ts +27 -0
  53. package/dist/components/data/day-strip.js +78 -0
  54. package/dist/components/data/sectioned-list.d.ts +21 -6
  55. package/dist/components/data/sectioned-list.js +44 -21
  56. package/dist/components/data/task-row.d.ts +3 -0
  57. package/dist/components/data/task-row.js +6 -14
  58. package/dist/components/layout/collapse.js +1 -1
  59. package/dist/components/layout/text.d.ts +3 -3
  60. package/dist/components/layout/text.js +6 -1
  61. package/dist/components/ui/avatar.js +1 -1
  62. package/dist/components/ui/button.js +1 -1
  63. package/dist/components/ui/check-circle.js +1 -1
  64. package/dist/components/ui/code-block.js +2 -2
  65. package/dist/components/ui/combobox.js +1 -0
  66. package/dist/components/ui/input-group.js +1 -1
  67. package/dist/components/ui/input.js +1 -1
  68. package/dist/components/ui/kbd.js +1 -1
  69. package/dist/components/ui/password-input.d.ts +8 -0
  70. package/dist/components/ui/password-input.js +32 -0
  71. package/dist/components/ui/sonner.d.ts +2 -2
  72. package/dist/components/ui/sonner.js +1 -1
  73. package/dist/components/ui/toggle.js +1 -1
  74. package/dist/index.d.ts +23 -7
  75. package/dist/index.js +21 -5
  76. package/dist/lib/internal/date.js +8 -0
  77. package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
  78. package/dist/styles.css +31 -1
  79. package/docs/COMPONENTS.md +302 -28
  80. package/docs/ROADMAP.md +13 -11
  81. package/llms.txt +10 -8
  82. package/package.json +3 -1
  83. package/skills/harebear-ui/SKILL.md +5 -3
@@ -15,6 +15,7 @@ Full working screens that combine these live in the repo under `playground/scree
15
15
  - **Compose, don't fork.** Every component accepts `className` (merged with `cn`, later classes win). Reach for a primitive before writing a new component.
16
16
  - **Icons are lucide-react elements.** Pass `<SearchIcon />`, not the component. Buttons size icons automatically.
17
17
  - **Status uses tones**, not colors: `"success" | "warning" | "danger" | "info" | "neutral"`.
18
+ - **Touch targets are built in.** Under `pointer: coarse`, `Button` (every size) and `Toggle`/`ToggleGroupItem` get a 44px hit area (an invisible `::after`, which an `overflow-hidden` ancestor or a later sibling clips: in such a spot, size the button itself, e.g. `pointer-coarse:size-11`, as the `CodeBlock` and `CodeSnippet` copy buttons do) and `Input`/`InputGroup` grow to 44px.
18
19
 
19
20
  ## Agent-first apps
20
21
 
@@ -27,7 +28,7 @@ A household or personal app where an assistant sits on every page and safe actio
27
28
  <AppNav
28
29
  brand={<AppBrand name="Homeclub" icon={<HouseIcon />} />}
29
30
  places={[{ label: "Today", icon: <SunIcon />, href: "/", active: true, count: 3 }, ...]}
30
- group={{ label: "Lists", items: lists, onAdd: newList, addLabel: "New list" }}
31
+ group={{ label: "Lists", href: "/lists", items: lists, onAdd: newList, addLabel: "New list" }}
31
32
  action={{ label: "New to-do", icon: <PlusIcon />, onSelect: openQuickAdd }}
32
33
  footer={<UserMenu name={user.name} onSignOut={signOut} />}
33
34
  >
@@ -40,10 +41,14 @@ A household or personal app where an assistant sits on every page and safe actio
40
41
  </ThemeProvider>
41
42
  ```
42
43
 
43
- - Lists: `SectionedList` (`TaskRow` rows); when the agent or a bulk action changes data, confirm with `toastUndo`.
44
+ - Pages: `Page` (the column) with a `PageHeader` (h1; `UserMenu compact className="md:hidden"` in `actions`, hidden from md up, since the sidebar footer is hidden on phones) and `PageSection`s (h2 regions). A chat page sets `fill` on `AppNav`.
45
+ - Lists: `SectionedList` (`TaskRow` rows); when the agent or a bulk action changes data, confirm with `toastUndo` and set `lingerMs` to its window so a ticked row stays struck through for about as long as Undo shows.
46
+ - A week agenda: `DayStrip sticky` above one `SectionedList` with a section per day (`empty` keeps empty days) and `AgendaRow` rows from `renderItem`.
47
+ - A record's details (`DetailSheet`): `ChecklistEditor` for steps, `CommentThread` for comments, `MarkdownEditor` for notes.
44
48
  - In the assistant thread, each action it took is an `ActionReceipt` with Undo (append it from `onSend`).
49
+ - A page with its own ask box (Today) answers under it: `AskBox` with `reply` and `pending` from `useAssistant()`, `onStop={stop}`, and `send(text, { open: false })`.
45
50
  - Settings and setup flows leave the frame: `FocusLayout` around `SettingsSection`s of `SettingsRow`s.
46
- - Members, lists and calendars take a `category-1..6` color; status colors never double as categories.
51
+ - Members, lists and calendars take a `category-1..6` color (`CategoryColor`); status colors never double as categories. People are `MemberAvatar` (`AvatarStack`, `VisibilityBadge`), list dots are `CategoryDot`, profile settings use `AvatarUpload` and `ColorSwatchPicker`.
47
52
  - Live example: `playground/screens/household.tsx` (`#household`) and `settings.tsx` (`#settings`).
48
53
 
49
54
  ## Theme
@@ -75,7 +80,7 @@ Every component in the shadcn registry is included. (shadcn's `form` item is an
75
80
  | Alert dialog | `AlertDialog`, `AlertDialogTrigger`, `AlertDialogContent`, `AlertDialogHeader`, `AlertDialogFooter`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogAction`, `AlertDialogCancel`, `AlertDialogMedia`, `AlertDialogOverlay`, `AlertDialogPortal` | Use for destructive confirmations. |
76
81
  | Aspect ratio | `AspectRatio` | |
77
82
  | Attachment | `Attachment`, `AttachmentGroup`, `AttachmentMedia`, `AttachmentContent`, `AttachmentTitle`, `AttachmentDescription`, `AttachmentActions`, `AttachmentAction`, `AttachmentTrigger` | File/image chips on messages and composers. `size`: `default`, `sm`, `xs`; `orientation`: `horizontal`, `vertical`. |
78
- | Avatar | `Avatar`, `AvatarImage`, `AvatarFallback`, `AvatarGroup`, `AvatarGroupCount`, `AvatarBadge` | `size`: `sm` 28px, `default` 36px, `lg` 56px. Fallback is tinted initials. |
83
+ | Avatar | `Avatar`, `AvatarImage`, `AvatarFallback`, `AvatarGroup`, `AvatarGroupCount`, `AvatarBadge` | `size`: `sm` 28px, `default` 36px, `lg` 56px. Fallback is tinted initials, 12px at `sm`. For people use `MemberAvatar`. |
79
84
  | Badge | `Badge`, `badgeVariants` | 24px pill. `variant`: `default` (primary), `secondary` (neutral, the common one), `soft` (tinted primary), `outline`, `success`, `warning`, `destructive`, `ghost`, `link`. harebear adds `size="lg"` (32px, lines up with sm buttons) and `tone` (`success \| warning \| danger \| info \| neutral`, the StatusBadge words; wins over `variant`). |
80
85
  | Bubble | `Bubble`, `BubbleGroup`, `BubbleContent`, `BubbleReactions` | Chat bubble. `variant`: `default` (primary), `secondary`, `muted`, `tinted`, `outline`, `ghost`, `destructive`; `align`: `start`, `end`. |
81
86
  | Breadcrumb | `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator`, `BreadcrumbEllipsis` | |
@@ -87,9 +92,9 @@ Every component in the shadcn registry is included. (shadcn's `form` item is an
87
92
  | Carousel | `Carousel`, `CarouselContent`, `CarouselItem`, `CarouselPrevious`, `CarouselNext`, `useCarousel`, type `CarouselApi` | embla. |
88
93
  | Chart | `ChartContainer`, `ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`, `ChartStyle` | Recharts wrappers. Prefer `TrendChart` for line/area; use these for custom charts. |
89
94
  | Checkbox | `Checkbox` | |
90
- | Check circle | `CheckCircle` | harebear. 24px round checkbox for to-do rows (44px touch target), pops when ticked. `tone="important"` tints it; pair with a word. Name it with `aria-labelledby` the row title. |
95
+ | Check circle | `CheckCircle` | harebear. 24px round checkbox for to-do rows (44px touch target), pops when ticked (no pop or fade under reduced motion). Focus shows a solid primary outline whatever the `tone`. `tone="important"` tints it; pair with a word. Name it with `aria-labelledby` the row title. |
91
96
  | Collapsible | `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | |
92
- | Combobox | `Combobox`, `ComboboxInput`, `ComboboxContent`, `ComboboxList`, `ComboboxItem`, `ComboboxGroup`, `ComboboxLabel`, `ComboboxCollection`, `ComboboxEmpty`, `ComboboxSeparator`, `ComboboxChips`, `ComboboxChip`, `ComboboxChipsInput`, `ComboboxTrigger`, `ComboboxValue`, `useComboboxAnchor` | Searchable select (Base UI). `<Combobox items={list}>` + `<ComboboxList>{(item) => <ComboboxItem value={item}>…}</ComboboxList>`. Chips for multi-select. |
97
+ | Combobox | `Combobox`, `ComboboxInput`, `ComboboxContent`, `ComboboxList`, `ComboboxItem`, `ComboboxGroup`, `ComboboxLabel`, `ComboboxCollection`, `ComboboxEmpty`, `ComboboxSeparator`, `ComboboxChips`, `ComboboxChip`, `ComboboxChipsInput`, `ComboboxTrigger`, `ComboboxValue`, `useComboboxAnchor` | Searchable select (Base UI). `<Combobox items={list}>` + `<ComboboxList>{(item) => <ComboboxItem value={item}>…}</ComboboxList>`. Chips for multi-select. `ComboboxInput`'s built-in trigger is named "Show options". |
93
98
  | Command | `Command`, `CommandDialog`, `CommandInput`, `CommandList`, `CommandEmpty`, `CommandGroup`, `CommandItem`, `CommandShortcut`, `CommandSeparator` | Use for ⌘K palettes. |
94
99
  | Direction | `DirectionProvider`, `useDirection` | RTL/LTR context for Radix components. |
95
100
  | Context menu | `ContextMenu`, `ContextMenuTrigger`, `ContextMenuContent`, `ContextMenuItem`, `ContextMenuCheckboxItem`, `ContextMenuRadioItem`, `ContextMenuRadioGroup`, `ContextMenuLabel`, `ContextMenuSeparator`, `ContextMenuShortcut`, `ContextMenuGroup`, `ContextMenuPortal`, `ContextMenuSub`, `ContextMenuSubContent`, `ContextMenuSubTrigger` | |
@@ -99,11 +104,11 @@ Every component in the shadcn registry is included. (shadcn's `form` item is an
99
104
  | Empty state | `Empty`, `EmptyHeader`, `EmptyMedia`, `EmptyTitle`, `EmptyDescription`, `EmptyContent` | Use for zero-data screens. harebear adds `size="sm"`: compact, no flex-grow, for empties inside cards and panels. |
100
105
  | Field | `Field`, `FieldLabel`, `FieldDescription`, `FieldError`, `FieldGroup`, `FieldLegend`, `FieldSeparator`, `FieldSet`, `FieldContent`, `FieldTitle` | Form layout. Works with react-hook-form or plain state. |
101
106
  | Hover card | `HoverCard`, `HoverCardTrigger`, `HoverCardContent` | |
102
- | Input | `Input` | 40px, filled `bg-muted`, turns `bg-card` with a primary ring on focus. |
107
+ | Input | `Input` | 40px, filled `bg-muted`, turns `bg-card` with a primary ring on focus. 44px under `pointer: coarse` (also `InputGroup`). |
103
108
  | Input group | `InputGroup`, `InputGroupAddon`, `InputGroupButton`, `InputGroupText`, `InputGroupInput`, `InputGroupTextarea` | Inputs with icons, prefixes, buttons. |
104
109
  | Input OTP | `InputOTP`, `InputOTPGroup`, `InputOTPSlot`, `InputOTPSeparator` | |
105
110
  | Item | `Item`, `ItemMedia`, `ItemContent`, `ItemTitle`, `ItemDescription`, `ItemActions`, `ItemGroup`, `ItemSeparator`, `ItemHeader`, `ItemFooter` | Generic list rows. harebear fix: `ItemTitle` clamps to one line (long words break) and shows the full text as a tooltip when clamped, `ItemContent` has `min-w-0`. harebear adds `ItemTitle mono` for commands, paths and IDs. |
106
- | Kbd | `Kbd`, `KbdGroup` | Mono keycap. |
111
+ | Kbd | `Kbd`, `KbdGroup` | Mono keycap, 12px text. |
107
112
  | Marker | `Marker`, `MarkerIcon`, `MarkerContent`, `markerVariants` | Inline status line in a thread ("Conversation saved"). `variant`: `default`, `separator` (date dividers), `border`. |
108
113
  | Label | `Label` | |
109
114
  | Menubar | `Menubar`, `MenubarMenu`, `MenubarTrigger`, `MenubarContent`, `MenubarItem`, `MenubarCheckboxItem`, `MenubarRadioGroup`, `MenubarRadioItem`, `MenubarLabel`, `MenubarSeparator`, `MenubarShortcut`, `MenubarGroup`, `MenubarPortal`, `MenubarSub`, `MenubarSubTrigger`, `MenubarSubContent` | |
@@ -129,8 +134,8 @@ Every component in the shadcn registry is included. (shadcn's `form` item is an
129
134
  | Table | `Table`, `TableHeader`, `TableBody`, `TableFooter`, `TableRow`, `TableHead`, `TableCell`, `TableCaption` | Borderless rows with rounded hover; quiet 12px headers. `data-state="selected"` tints a row. |
130
135
  | Tabs | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `tabsListVariants` | `TabsList variant`: `default` (segmented control), `pill` (rounded top-level nav), `line`. Use `Tabs` as a segmented filter too (date ranges, status filters). |
131
136
  | Textarea | `Textarea` | Same filled style as `Input`, auto-grows. |
132
- | Toast | `Toaster` | sonner. Mount once; call `toast("Saved")` from `sonner`. Follows the theme mode. `toastUndo(message, { onUndo, undoLabel?, duration = 9000 })` shows an Undo toast for something already done. |
133
- | Toggle | `Toggle`, `toggleVariants`, `ToggleGroup`, `ToggleGroupItem` | |
137
+ | Toast | `Toaster`, `toast`, `toastUndo` | sonner. Mount once; call `toast("Saved")`, `toast.success(...)`, `toast.error(...)` (sonner's `toast`, re-exported by harebear-ui). Follows the theme mode. `toastUndo(message, { onUndo, undoLabel?, duration = 9000 })` shows an Undo toast for something already done. |
138
+ | Toggle | `Toggle`, `toggleVariants`, `ToggleGroup`, `ToggleGroupItem` | 44px hit area under `pointer: coarse`. |
134
139
  | Tooltip | `TooltipProvider`, `Tooltip`, `TooltipTrigger`, `TooltipContent` | Mount `TooltipProvider` once near the root. |
135
140
  | Trace | `Trace`, `TraceLine`, `TraceIcon`, `TraceTitle`, `TraceDescription`, `TraceCollapsible`, `TraceCollapsibleTrigger`, `TraceCollapsibleContent` | Agent step log (searched, read, ran). `TraceLine status`: `running` (shimmer), `error`, `pending`. |
136
141
 
@@ -196,7 +201,31 @@ Logo tile + name. Props: `name`, `subtitle?`, `icon?`, `action?` (trailing contr
196
201
 
197
202
  ### `PageHeader`
198
203
 
199
- Title row. Props: `title`, `description?`, `count?` (badge after title), `eyebrow?` (small text above), `actions?` (right side, wraps on mobile).
204
+ Title row with the page's `h1`. Props: `title`, `description?`, `count?` (badge after title), `eyebrow?` (small text above), `icon?` (leading mark before the title: `CategoryDot`, `MemberAvatar`, a lucide icon at 24px), `actions?` (right side, wraps on mobile).
205
+
206
+ ### `Page`, `PageSection`
207
+
208
+ `Page`: the page column inside `AppNav` or `SidebarLayout`: centred, 16px gutters on phones (32px from 768px), children spaced `gap-7`. Props: `width?` (`sm | md | lg`, default `md` = 42rem, like `FocusLayout`), div props. `PageSection`: a region named by its `h2` (so headings go h1, h2 without skipping), with `title`, `action?` (right of the heading, 44px tall on touch screens) and `children`. Inside a card, use `Heading level={3}`.
209
+
210
+ ```tsx
211
+ ;<Page>
212
+ <PageHeader
213
+ title="Groceries"
214
+ icon={<CategoryDot color={1} />}
215
+ actions={<UserMenu compact name={user.name} className="md:hidden" />}
216
+ />
217
+ <PageSection
218
+ title="This week"
219
+ action={
220
+ <Button variant="ghost" size="sm">
221
+ See all
222
+ </Button>
223
+ }
224
+ >
225
+ <SectionedList sections={sections} onToggle={setDone} />
226
+ </PageSection>
227
+ </Page>
228
+ ```
200
229
 
201
230
  ### `StatCard`
202
231
 
@@ -242,11 +271,11 @@ Label-over-value pairs as a `<dl>`. Props: `items: { label; value }[]`, `columns
242
271
 
243
272
  ### `UserChip`
244
273
 
245
- Avatar + name + subtitle. Props: `name`, `subtitle?`, `avatarUrl?`, `action?`, `size?`. In a sidebar icon rail only the avatar shows. Also exports `initials(name)`.
274
+ Avatar + name + subtitle. Props: `name`, `subtitle?`, `avatarUrl?`, `color?` (`CategoryColor` behind the initials), `action?`, `size?`. In a sidebar icon rail only the avatar shows. Also exports `initials(name)`.
246
275
 
247
276
  ### `UserMenu`
248
277
 
249
- The signed-in user as a menu button (sidebar footer or top bar). The menu has a header, your items (`children`, e.g. a Settings `DropdownMenuItem`), a Light / Dark / System theme choice, and Sign out. In a sidebar icon rail the trigger is just the avatar and the menu opens to the side. Props: `name`, `subtitle?`, `avatarUrl?`, `children?`, `themeSwitch?` (default true, needs `ThemeProvider`), `onSignOut?`, `signOutLabel?`.
278
+ The signed-in user as a menu button (sidebar footer or top bar). The menu has a header, your items (`children`, e.g. a Settings `DropdownMenuItem`), a Light / Dark / System theme choice, and Sign out. The trigger is named by its visible name and subtitle (in a sidebar icon rail, where it is just the avatar, by `name`) and the menu opens to the side there. `compact` makes the trigger just the avatar anywhere (40px, 44px on touch screens, named "Account menu for {name}") with the menu below it, for a phone page header. Upgrading from 0.9: the non-compact trigger is no longer named "Account menu for {name}"; find it by `name` instead. Props: `name`, `subtitle?`, `avatarUrl?`, `color?` (`CategoryColor` behind the initials), `children?`, `themeSwitch?` (default true, needs `ThemeProvider`), `onSignOut?`, `signOutLabel?`, `compact?`.
250
279
 
251
280
  ```tsx
252
281
  ;<UserMenu name={email} subtitle="Signed in" onSignOut={signOut}>
@@ -257,28 +286,170 @@ The signed-in user as a menu button (sidebar footer or top bar). The menu has a
257
286
  </UserMenu>
258
287
  ```
259
288
 
289
+ ### `MemberAvatar`
290
+
291
+ A person's photo, or their initials on their category colour (`bg-category-N`, `text-category-foreground`). Props: `name`, `avatarUrl?`, `color?: CategoryColor` (1 to 6; without it the soft primary tint), `size?` (`sm` 28px, `default` 36px, `lg` 56px). It is `role="img"` named after the person; pass `aria-hidden` when the name is shown next to it. Used by `UserChip`, `UserMenu` and `TaskRow`.
292
+
293
+ ```tsx
294
+ ;<MemberAvatar name="Sam Rivera" avatarUrl={sam.photoUrl ?? undefined} color={sam.color} size="sm" />
295
+ ```
296
+
297
+ ### `AvatarStack`, `VisibilityBadge`
298
+
299
+ `AvatarStack`: overlapping small `MemberAvatar`s. Props: `members: { name; avatarUrl?; color? }[]`, `max?` (default 4): total slots; past it, `max - 1` faces and a "+N" chip (read as "N more"). `VisibilityBadge`: who can see something. Props: `visibility: "shared" | "private"`, `members?` (faces when shared), `label?` (default "Shared with everyone" or "Only you"; the lock shows when private).
300
+
301
+ ```tsx
302
+ ;<VisibilityBadge visibility={list.private ? "private" : "shared"} members={household} />
303
+ ```
304
+
305
+ ### `CategoryDot`
306
+
307
+ A 10px dot in a category colour, e.g. as the `icon` of a list in `AppNav`'s group. Props: `color: CategoryColor`, `label?` (accessible name when the colour means something on its own, e.g. whose event; decorative without it), `locked?` (adds a lock named `lockLabel?`, default "Only you").
308
+
309
+ ### `ColorSwatchPicker`
310
+
311
+ The six category colours as a radio group (arrow keys skip taken ones). 32px swatches with 44px touch targets; the chosen one has a check and a ring. Props: `value?: CategoryColor`, `onValueChange?(color)`, `taken?: Partial<Record<CategoryColor, string>>` (colour to who has it: shown with their initial, disabled, named "Violet, taken by Riley"). Name it with `aria-label` or `aria-labelledby`.
312
+
313
+ ```tsx
314
+ ;<ColorSwatchPicker aria-label="Your colour" value={me.color} onValueChange={setColor} taken={{ 3: "Riley" }} />
315
+ ```
316
+
317
+ ### `AvatarUpload`, `cropImageToSquare`
318
+
319
+ Profile photo: a 72px avatar, "Upload a photo" / "Change photo" and "Remove photo". The chosen image is cropped to its centred square at 256x256 in the browser (canvas, JPEG) and previewed at once. Props: `name`, `color?`, `avatarUrl?` (current photo), `onChange(photo: Blob | null)` (an event, not a controlled value) (`null` after Remove), `maxBytes?` (JPEG quality steps down to fit), `onError?(error)` ("Could not read that image." or "That photo is too big."), `description?`. `cropImageToSquare(file, { size?, maxBytes? })` is the crop on its own: a `Promise<Blob>`.
320
+
321
+ ```tsx
322
+ ;<AvatarUpload
323
+ name={me.name}
324
+ color={me.color}
325
+ avatarUrl={me.photoUrl ?? undefined}
326
+ maxBytes={512 * 1024}
327
+ onChange={(photo) => (photo ? uploadPhoto(photo) : removePhoto())}
328
+ onError={(e) => toast.error(e.message)}
329
+ description="Without a photo, your initial and colour are used."
330
+ />
331
+ ```
332
+
260
333
  ### `ConfirmDialog`
261
334
 
262
335
  Controlled "are you sure" dialog. Props: `open`, `onOpenChange`, `title`, `description?`, `confirmLabel?`, `cancelLabel?`, `destructive?` (red confirm), `onConfirm`.
263
336
 
337
+ ### `PromptDialog`
338
+
339
+ Controlled dialog that asks for one line of text (New list, Rename). Props: `open`, `onOpenChange`, `title`, `description?`, `label` (field label), `defaultValue?`, `submitLabel?` (default "Save"), `cancelLabel?` (default "Cancel"), `maxLength?`, `onSubmit(value): void | Promise<void>` (trimmed; empty is blocked). It closes when `onSubmit` returns or resolves and starts fresh on every open; a thrown error's message shows under the field and the dialog stays open.
340
+
341
+ ```tsx
342
+ ;<PromptDialog
343
+ open={renaming}
344
+ onOpenChange={setRenaming}
345
+ title="Rename list"
346
+ label="Name"
347
+ defaultValue={list.name}
348
+ submitLabel="Rename"
349
+ maxLength={60}
350
+ onSubmit={(name) => api.renameList(list.id, name)}
351
+ />
352
+ ```
353
+
264
354
  ### `CopyField`
265
355
 
266
- Read-only value with a copy button (IDs, paths, URLs). Props: `value` (copied), `display?` (shown instead, full value as tooltip), `label?` (leading addon), `aria-label?`, `mono?` (default true).
356
+ Read-only value with a copy button (IDs, paths, URLs). Props: `value` (copied), `display?` (shown instead, full value as tooltip), `label?` (leading addon), `aria-label?`, `mono?` (default true), `copiedLabel?` (default "Copied"; after copying, the button's name and a label beside it read this for a moment).
357
+
358
+ ### `DateTimePicker`
359
+
360
+ Date field: a button that opens a `Calendar` in a `Popover`, with an optional time field and a Clear button. Value is a floating local date, no time zone: `DateTimeValue = { date: "YYYY-MM-DD", time?: "HH:MM" }`. Props: `value: DateTimeValue | null`, `onValueChange(value | null)`, `withTime?` (time field under the calendar; enabled once a date is set), `placeholder?` (default "Pick a date"). Other props (`id`, `aria-label`, `disabled`) go to the trigger button. Day cells are 44px on phones.
361
+
362
+ ### `TimeZonePicker`
363
+
364
+ Searchable IANA time zone field built on `Combobox`, listing `Intl.supportedValuesOf("timeZone")` (underscores shown as spaces, so "new york" matches). A `value` not in that list (an alias such as "US/Pacific") is kept at the top so it still shows. Props: `value` (e.g. `"America/Vancouver"`), `onValueChange(zone)`, `id?` (for a `FieldLabel` or `SettingsRow` `htmlFor`), `className?`.
365
+
366
+ ```tsx
367
+ ;<SettingsRow
368
+ label="Time zone"
369
+ htmlFor="tz"
370
+ control={<TimeZonePicker id="tz" value={me.timeZone} onValueChange={(timeZone) => save({ timeZone })} />}
371
+ />
372
+ ```
373
+
374
+ ### `DetailSheet`
375
+
376
+ A record's details, one API for both sizes: a floating side sheet from 768px up, a full-screen drawer with a close button on phones. Props: `open`, `onOpenChange`, `title` (heading and accessible name), `description?`, `footer?` (pinned under the scrolling body, e.g. Delete), `children` (the fields). `data-layout` is `"sheet"` or `"drawer"`.
377
+
378
+ ```tsx
379
+ ;<DetailSheet
380
+ open={!!task}
381
+ onOpenChange={(o) => !o && close()}
382
+ title="To-do"
383
+ footer={
384
+ <Button variant="ghost" onClick={askDelete}>
385
+ Delete
386
+ </Button>
387
+ }
388
+ >
389
+ <Field>
390
+ <FieldLabel htmlFor="when">When</FieldLabel>
391
+ <DateTimePicker id="when" withTime value={due} onValueChange={setDue} />
392
+ </Field>
393
+ </DetailSheet>
394
+ ```
267
395
 
268
396
  ### `Callout`
269
397
 
270
398
  Quiet note with an icon (sparkle by default). Props: `icon?`, `tone?` (`"muted" | "soft"`).
271
399
 
400
+ ### `Celebration`, `celebrate`
401
+
402
+ An occasional confetti burst, e.g. when the last to-do of the day is ticked. Mount `<Celebration />` once near the root (like `Toaster`), then call `celebrate()` from anywhere. CSS only, about 1.5 s, renders nothing between bursts (`data-slot="celebration"` while it plays), and does nothing under `prefers-reduced-motion`. Keep it rare.
403
+
272
404
  ### `LinkTile`
273
405
 
274
406
  Muted tile that lifts on hover, for service launchers. Props: `title`, `subtitle?` (mono), `icon?`, `status?: { tone; label }`, `href`, or `asChild` with your router `<Link>` as the only child.
275
407
 
408
+ ### `MarkdownEditor`
409
+
410
+ Markdown field with Write and Preview tabs; Preview renders with `Markdown`. Props: `value`, `onValueChange(value)`, `renderPreview?(markdown)` (custom renderer, wrapped in `Prose`), `wrapperClassName?`. Other props (`id`, `placeholder`, `aria-*`, `className`) go to the `Textarea`.
411
+
412
+ ```tsx
413
+ ;<MarkdownEditor id="body" value={body} onValueChange={setBody} />
414
+ ```
415
+
416
+ ### `ChecklistEditor`
417
+
418
+ Steps inside a record, e.g. a to-do's checklist: `CheckCircle` per item (the text is its label), a Remove button, an add field (Enter adds the trimmed text and clears), and an "x of y done" line when there are items. Props: `items: { id; text; done }[]`, `onToggle(id, done)`, `onRemove(id)`, `onAdd(text)`, `addPlaceholder?` (also the field's name, default "Add an item"), `progressLabel?(done, total)`. The root is a `role="group"`: name it with `aria-label` or `aria-labelledby`.
419
+
420
+ ```tsx
421
+ ;<ChecklistEditor
422
+ aria-label="Checklist"
423
+ items={task.checklist}
424
+ onToggle={toggleStep}
425
+ onRemove={removeStep}
426
+ onAdd={addStep}
427
+ addPlaceholder="Add a step"
428
+ />
429
+ ```
430
+
431
+ ### `CommentThread`
432
+
433
+ Comments on a record, oldest first, then a box and a submit button (disabled while empty; submits the trimmed text and clears). Bodies render with `Markdown`. Props: `comments: { id; author: { name; color?; avatarUrl? }; meta?; body }[]` (`meta` is muted text after the name, e.g. "via HomeBot · 2 h ago"), `onSubmit(text)`, `placeholder?` (also the box's name, default "Add a comment"), `submitLabel?` (default "Comment"). Show your own empty state if you want one.
434
+
435
+ ```tsx
436
+ ;<CommentThread comments={task.comments} onSubmit={(text) => api.comment(task.id, text)} />
437
+ ```
438
+
276
439
  ### `AppNav`
277
440
 
278
441
  One nav config, two renderings: on desktop a `SidebarLayout` sidebar (brand, action button, places, one collapsible group, footer; active place is a raised card; collapses to the icon rail), under 768px a bottom tab bar (first 5 places, 56px targets, `aria-current="page"`) plus a round floating action button. Use it instead of `AppShell` for consumer apps that need phone navigation.
279
442
 
280
- Props: `places: AppNavPlace[]` (`label`, `icon?`, `href?` or `onSelect?`, `active?`, `count?`, `tab?: false` to keep out of the tab bar), `group?: AppNavGroup` (`label`, `items`, `defaultOpen?`, `onAdd?`, `addLabel?`), `action?: { label; icon; onSelect }`, `brand?`, `footer?` (usually `UserMenu`), `label?` (nav name, default "Main"), `linkComponent?`, `collapsible?`, `storageKey?`, `defaultOpen?`, `width?`, `children`.
281
- On phones the group and footer live in the sidebar sheet: put a `SidebarTrigger` in the page header if users need them there. Render `AssistantDock` inside `children` so it clears the tab bar. Toasts stack above the tab bar, the action button and the dock launcher on their own; no `Toaster` offset needed.
443
+ Props: `places: AppNavPlace[]` (`label`, `icon?`, `href?` or `onSelect?`, `active?`, `count?`, `tab?: false` to keep out of the tab bar, `sidebar?: false` for a tab-bar-only place such as Lists when the sidebar has a Lists group), `group?: AppNavGroup` (`label`, `href?` (heading becomes a link, e.g. to all lists; the chevron is a separate "Collapse Lists" button), `active?` (the heading link is the current page), `items`, `defaultOpen?`, `onAdd?`, `addLabel?`), `action?: { label; icon; onSelect; sidebar?: false }` (`sidebar: false` keeps only the round phone button), `brand?`, `footer?` (usually `UserMenu`), `label?` (nav name, default "Main"), `linkComponent?`, `collapsible?`, `storageKey?`, `defaultOpen?`, `width?`, `fill?` (main column is exactly the screen height minus the tab bar (and the round action button's space when `action` is set) and does not scroll, for a chat: the child takes it with `flex-1 min-h-0`, which `ChatPanel` already has), `children`.
444
+ On phones the group and footer live in the sidebar sheet: put a `SidebarTrigger` in the page header if users need them there. With `action`, the main column's bottom padding also clears the round button, so a trailing control on the last row is not under it. Render `AssistantDock` inside `children` so it clears the tab bar. Toasts stack above the tab bar, the action button and the dock launcher on their own; no `Toaster` offset needed.
445
+
446
+ ```tsx
447
+ ;<AppNav fill places={places} footer={<UserMenu name={user.name} />}>
448
+ <ChatPanel title="Assistant" composer={<PromptInput … />}>
449
+ {thread}
450
+ </ChatPanel>
451
+ </AppNav>
452
+ ```
282
453
 
283
454
  ### `FocusLayout`
284
455
 
@@ -286,7 +457,7 @@ A page outside the app frame (settings, onboarding, setup): slim sticky top bar,
286
457
 
287
458
  ### `SettingsSection`, `SettingsRow`
288
459
 
289
- `SettingsSection`: card with `title`, `description?` and rows divided by hairlines (a labelled `region`). `SettingsRow`: `label`, `description?`, `control?` (Switch, Select, ToggleGroup, Button), `htmlFor?` (makes the label a `<label>` for the control). Add `content` to expand inline (change password, add a connection): without a `control` the row gets a "Change"/"Cancel" button (`expandLabel?`); control it with `open`/`onOpenChange` or `defaultOpen`.
460
+ `SettingsSection`: card with `title`, `description?` and rows divided by hairlines (a labelled `region`). `SettingsRow`: `label`, `description?`, `control?` (Switch, Select, ToggleGroup, Button), `htmlFor?` (makes the label a `<label>` for the control), `body?` (always shown under the label, full width, no toggle: a list, a hint, a short form). Add `content` to expand inline (change password, add a connection): without a `control` the row gets a "Change"/"Cancel" button (`expandLabel?`); control it with `open`/`onOpenChange` or `defaultOpen`.
290
461
 
291
462
  ```tsx
292
463
  ;<FocusLayout
@@ -307,6 +478,7 @@ A page outside the app frame (settings, onboarding, setup): slim sticky top bar,
307
478
  control={<Switch id="daily" />}
308
479
  />
309
480
  <SettingsRow label="Password" content={<ChangePasswordForm />} />
481
+ <SettingsRow label="Connections" body={<ConnectionList />} />
310
482
  </SettingsSection>
311
483
  </FocusLayout>
312
484
  ```
@@ -321,7 +493,7 @@ Props instead of class names, for blocks that should look right without styling
321
493
 
322
494
  ### `Text`, `Heading`
323
495
 
324
- `Text` takes `size` (`xs | sm | base | lg | xl | 2xl`), `tone` (`default | muted | success | warning | danger | info`), `weight`, `mono`, `truncate`, `lines` (1 to 3) and `tabular`, and renders `as` a span, p or div. `Heading` is a card section title (`level={3}`) or a small uppercase group label (`level={4}`).
496
+ `Text` takes `size` (`xs | sm | base | lg | xl | 2xl`), `tone` (`default | muted | success | warning | danger | info`), `weight`, `mono`, `truncate`, `lines` (1 to 3) and `tabular`, and renders `as` a span, p or div. `Heading` is a page section title (`level={2}`, under the page `h1`; `PageSection` uses it), a card section title (`level={3}`, default) or a small uppercase group label (`level={4}`). Do not skip levels: a page with an h1 needs h2s before h3s.
325
497
 
326
498
  ### `Metric`, `MetricGroup`
327
499
 
@@ -337,7 +509,7 @@ Shows the first `initial` children (default 5) and a "Show N more" button for th
337
509
 
338
510
  ### `Collapse`
339
511
 
340
- Animates children between full height and 0 (CSS `grid-template-rows`), so neighbours slide instead of jumping. Closed content is `inert` and `aria-hidden`. Props: `open`, `contentClassName?`. No motion under `prefers-reduced-motion`.
512
+ Animates children between full height and 0 (CSS `grid-template-rows`), so neighbours slide instead of jumping. Closed content is `inert` and `aria-hidden`. Clips vertically only, so focus rings and hit areas at the sides are not cut off. Props: `open`, `contentClassName?`. No motion under `prefers-reduced-motion`.
341
513
 
342
514
  ## Data display
343
515
 
@@ -385,11 +557,16 @@ Mini bar history (pings, uptime). Props: `data: (number | { value; tone?; label?
385
557
 
386
558
  ### `TaskRow`
387
559
 
388
- One to-do: `CheckCircle` aligned with the first line, title, one meta line, optional assignee avatar. Props: `title`, `checked`, `onCheckedChange(checked)`, `meta?`, `assignee?: { name; avatarUrl? }`, `important?` (say it in `meta` too), `onOpen?` (title becomes a button, e.g. to open a sheet), `disabled?`.
560
+ One to-do: `CheckCircle` aligned with the first line, title, one meta line, optional assignee avatar. Props: `title`, `checked`, `onCheckedChange(checked)`, `meta?`, `assignee?: { name; avatarUrl?; color? }` (a `MemberAvatar`), `important?` (say it in `meta` too), `onOpen?` (title becomes a button, e.g. to open a sheet), `disabled?`.
389
561
 
390
562
  ### `SectionedList`
391
563
 
392
- A to-do list: one add box at the top, sections with headings, done items folded into a "Show N done" toggle, an empty state. A ticked row stays visible for a moment, then folds away with `Collapse`. Props: `sections: { id; title?; items: SectionedListItem[] }[]` (item: `id`, `title`, `meta?`, `done?`, `important?`, `assignee?`), `onToggle(id, done)`, `onAdd?(text)`, `addPlaceholder?` (default "Add a to-do"), `onOpen?(id)`, `empty?`, `defaultShowDone?`.
564
+ A to-do list: one add box at the top, sections with headings, done items folded into a "Show N done" toggle, an empty state. A ticked row stays struck through for `lingerMs`, then folds away with `Collapse`. Props: `sections: { id; title?; action?; empty?; items: SectionedListItem[] }[]` (item: `id`, `title`, `meta?`, `done?`, `important?`, `assignee?`), `onToggle(id, done)`, `lingerMs?` (default 600; 9000 matches `toastUndo`), `renderItem?(item, toggle)`, `onAdd?(text)`, `addPlaceholder?` (default "Add a to-do"), `addChips?(text)`, `onOpen?(id)`, `empty?`, `defaultShowDone?`.
565
+
566
+ - Section `id` is also the `<section>`'s DOM id, so `DayStrip` or an `#id` link can jump to it. Keep ids unique on the page.
567
+ - `action` sits at the right of the heading (a small ghost `Button`, e.g. "Move to today"; stretched to 44px on phones). `empty` keeps the section shown with that text when nothing in it is open ("Nothing planned").
568
+ - `renderItem` draws other rows, e.g. `AgendaRow`. Its `toggle(done)` lingers and folds the row like a `TaskRow`. Items without `done` (events) always stay. Type the items with `SectionedList<MyItem>`.
569
+ - `addChips(text)` returns chips shown at the right of the add box while it has text, like `AskBox` `chips` (the date or person parsed from the text).
393
570
 
394
571
  ```tsx
395
572
  ;<SectionedList
@@ -400,6 +577,39 @@ A to-do list: one add box at the top, sections with headings, done items folded
400
577
  />
401
578
  ```
402
579
 
580
+ ### `AgendaRow`
581
+
582
+ One line of a day's agenda: time column, event icon (or a `CheckCircle` for a to-do), title, one meta line, whose dot and name. Props: `title`, `time?` ("3:15", "All day"), `meta?`, `who?: { name; color: CategoryColor }` (`CategoryDot` before the name), `checked?` + `onCheckedChange?(checked)` (draws the check instead of the icon), `important?`, `onOpen?` (title becomes a button, e.g. to open an event dialog).
583
+
584
+ ### `DayStrip`
585
+
586
+ A row of day links (short weekday, date, a dot when `hasPlans`) that smooth-scroll to the element whose id is the day's `id` (instant under reduced motion) and move focus to it; scrolls sideways when the days do not fit. Props: `days: { id; date: "YYYY-MM-DD"; hasPlans? }[]` (floating local date, like `DateTimePicker`), `value?` (selected id, `aria-current`), `onValueChange?(id)`, `aria-label?` (default "Jump to a day"), `sticky?` (sticks at `top-0` with the page background while the page or any container scrolls, and gives the jumped-to element a `scroll-margin-top` as tall as the strip so its heading lands below it; add `className="top-14"` under a sticky header). Each link's name is the full date ("Thursday, November 12, has plans") as visually hidden text, so it contains what is shown.
587
+
588
+ ```tsx
589
+ ;<>
590
+ <DayStrip
591
+ sticky
592
+ days={week.map((d) => ({ id: d.key, date: d.date, hasPlans: d.items.length > 0 }))}
593
+ value={day}
594
+ onValueChange={setDay}
595
+ />
596
+ <SectionedList<WeekItem>
597
+ lingerMs={9000}
598
+ sections={week.map((d) => ({ id: d.key, title: d.label, empty: "Nothing planned", items: d.items }))}
599
+ onToggle={setDone}
600
+ renderItem={(i, toggle) => (
601
+ <AgendaRow
602
+ time={i.time}
603
+ title={i.title}
604
+ who={i.who}
605
+ {...(i.kind === "todo" && { checked: !!i.done, onCheckedChange: toggle })}
606
+ onOpen={() => open(i)}
607
+ />
608
+ )}
609
+ />
610
+ </>
611
+ ```
612
+
403
613
  ## AI and chat
404
614
 
405
615
  A chat screen is `ChatShell` (sidebar with `AppBrand`, `NewConversationButton`, `ConversationList`, `UserMenu`; optional details panel) around a `ChatPanel` (header, thread, `PromptInput`). Agent apps add `ToolGroup`/`ToolStep` for tool calls and `ApprovalCard`/`ApprovalResult` for permission prompts. See `playground/screens/chat.tsx` and `playground/screens/agent.tsx`.
@@ -495,7 +705,7 @@ Pill showing the active model with a status dot, opening a menu. Props: `models:
495
705
  Grouped thread list. Props: `groups: { label; items: Conversation[] }[]`, `activeId?`, `onSelect?(id)`, `onDelete?(conversation)` (adds a delete button per row that confirms first; `deleteDescription?` is the confirmation body), `renderAction?(conversation)` (any other trailing control; wins over `onDelete`; on desktop it shows on hover/focus), `empty?`.
496
706
  `Conversation = { id; title; description?; tone?; pulse?; deleteDisabled? }`: `description` is the second line (time, or a live state with a `tone` dot); `deleteDisabled` blocks delete for that row (a string is the reason, shown as the tooltip).
497
707
  Inside a `Sidebar` it closes the mobile sheet on select and hides in the icon rail.
498
- `groupByDate(items, getDate, toConversation, now?)` buckets items into Today / Yesterday / Previous 7 days / Earlier, newest first.
708
+ `groupByDate(items, getDate, toConversation, options?)` buckets items into Today / Yesterday / Previous 7 days / Earlier, newest first. `options` is `{ now?, buckets? }` (or just `now`); `buckets: "week"` gives Today / This week (the 6 days before today) / Earlier.
499
709
  `formatWhen(date, now?, locale?)`: short timestamp for rows and headers: the time today ("9:41 AM"), the date this year ("Nov 3"), else with the year.
500
710
 
501
711
  ### `NewConversationButton`
@@ -530,15 +740,32 @@ Agent tool calls as a bordered, collapsible card. `ToolStatus = "queued" | "wait
530
740
 
531
741
  ### `AssistantProvider`, `useAssistant`, `useAssistantPage`
532
742
 
533
- One assistant conversation across pages. `AssistantProvider` props: `onSend(text, { page, messages, append, update })` (return the reply, or `append`/`update` messages yourself for streaming and receipts; a thrown error becomes the reply), `initialMessages?`, `defaultOpen?`. `useAssistant()` returns `{ open, setOpen, toggle, messages, pending, page, setPage, send, append, update, clear }`. A page calls `useAssistantPage({ id, title?, data?, suggestions? })` to become the context while mounted.
743
+ One assistant conversation across pages. `AssistantProvider` props: `onSend(text, { page, messages, append, update, signal })` (return the reply, or `append`/`update` messages yourself for streaming and receipts; a thrown error becomes the reply; `signal` aborts when the user presses Stop, so pass it to `fetch` or listen for `abort` to tell your server), `initialMessages?`, `defaultOpen?`. `useAssistant()` returns `{ open, setOpen, toggle, messages, pending, reply, page, setPage, send, stop, append, update, clear }`: `send(text, { open: false }?)` skips opening the dock, `stop()` aborts the running send, ends `pending` and drops its late reply, `reply` is the assistant messages after the latest user message (null until there is one), for `AskBox`. A page calls `useAssistantPage({ id, title?, data?, suggestions? })` to become the context while mounted.
534
744
 
535
745
  ### `AssistantDock`
536
746
 
537
- The assistant as a floating panel over the page (never reflows it): header (icon, title, full-screen link, close), empty state with suggestions, thread, `AskBox`. Closed, it is a soft launcher button. Escape closes and returns focus to the launcher. Props: `title?` (default "Assistant"), `icon?`, `fullScreenHref?`, `emptyTitle?`, `emptyDescription?`, `suggestions?` (default: the page's), `placeholder?`, `dictation?`, `launcher?` (false when another control opens it), `launcherLabel?`.
747
+ The assistant as a floating panel over the page (never reflows it): header (icon, title, full-screen link, close), empty state with suggestions, thread, `AskBox` (Stop while the assistant works, wired to `useAssistant().stop`). Closed, it is a soft launcher button. Escape closes and returns focus to the launcher. Props: `title?` (default "Assistant"), `icon?`, `fullScreenHref?`, `emptyTitle?`, `emptyDescription?`, `suggestions?` (default: the page's), `placeholder?`, `dictation?`, `launcher?` (false when another control opens it), `launcherLabel?`.
538
748
 
539
749
  ### `AskBox`
540
750
 
541
- Large rounded composer: auto-growing text, `chips` shown while there is text, suggestion pills, `DictationButton`. The focus ring is on the box (`focus-box`). Props: `value`, `onValueChange`, `onSubmit(text)`, `placeholder?` (also the label, default "Ask anything"), `chips?`, `suggestions?`, `onSuggestion?` (default submits), `dictation?` (`SpeechSource`, or `false` to hide the mic), `lang?`, `disabled?`.
751
+ Large rounded composer: auto-growing text, `chips` shown while there is text, suggestion pills, `DictationButton`. The focus ring is on the box (`focus-box`). Props: `value`, `onValueChange`, `onSubmit(text)`, `placeholder?` (also the label, default "Ask anything"), `chips?`, `suggestions?`, `onSuggestion?` (default submits), `dictation?` (`SpeechSource`, or `false` to hide the mic), `lang?`, `disabled?`, `pending?` (blocks sending; with `onStop?` the send button becomes Stop), `reply?` (the answer under the box; passing it, even `null`, shows "Working" while `pending`), `chatHref?` (an "Open chat" link under the reply).
752
+
753
+ ```tsx
754
+ // Inline answer under the box (no dock), e.g. a Today page
755
+ const { send, pending, stop, reply } = useAssistant()
756
+ ;<AskBox
757
+ value={text}
758
+ onValueChange={setText}
759
+ onSubmit={(t) => {
760
+ setText("")
761
+ send(t, { open: false })
762
+ }}
763
+ pending={pending}
764
+ onStop={stop}
765
+ reply={reply}
766
+ chatHref={`/chat/${conversationId}`}
767
+ />
768
+ ```
542
769
 
543
770
  ### `DictationButton`, `useDictation`, `browserSpeechSource`
544
771
 
@@ -549,11 +776,20 @@ Large rounded composer: auto-growing text, `chips` shown while there is text, su
549
776
 
550
777
  What the assistant did, as a compact card: round check, `title` (past tense), `meta?`, Undo (`onUndo?`, `undoLabel?`). `undone` strikes it through and shows "Undone".
551
778
 
552
- ### `CodeSnippet`, `InlineCode`, `Prose`
779
+ ### `CodeSnippet`, `InlineCode`, `Prose`, `ProseBlockquote`
553
780
 
554
781
  `CodeSnippet`: one-prop code panel with language label, copy button and shiki syntax highlighting (each language loads on first use). Props: `code`, `language?` (`bash`, `tsx`, `ts`, `js`, `json`, `python`, `sql`, `yaml`, `css`, `html`, `markdown`, `diff`; others render plain), `highlight?` (default true), `lineNumbers?`, `hideCopy?`. Built on the `CodeBlock` primitives; drop to those for diffs or highlighted lines.
555
782
  `InlineCode`: monospace span. `truncate?: number` cuts longer string children with an ellipsis and puts the full text in the tooltip.
556
- `Prose`: typography for model output and markdown. Styles plain `p, ul, ol, li, strong, a, code, pre, h1-h4, blockquote, table` children, so it works with `react-markdown`.
783
+ `Prose`: typography for model output and markdown. Styles plain `p, ul, ol, li, strong, a, code, pre, h1-h4, blockquote, table` children.
784
+ Tip boxes: a blockquote whose first word is `Tip:` (`> Tip: Turn the power off first.`) renders as a `Callout` without the word. Prose does this for plain `<blockquote>` children; with your own react-markdown pass `components={{ blockquote: ProseBlockquote }}`.
785
+
786
+ ### `Markdown`
787
+
788
+ Renders a Markdown string inside `Prose`: GitHub flavoured (tables, task lists, strikethrough, autolinks), `> Tip:` blockquotes as a `Callout`. Safe for user and model text: raw HTML is dropped and `javascript:` links are removed. Props: `children: string`, `linkComponent?` (your router `Link`, gets `href`; used for links without a scheme such as `/lists/2`). Other props go to the `Prose` wrapper.
789
+
790
+ ```tsx
791
+ ;<Markdown linkComponent={Link}>{message.text}</Markdown>
792
+ ```
557
793
 
558
794
  ### Chat primitives vs. composites
559
795
 
@@ -567,13 +803,49 @@ Parameterized versions of shadcn's login-01..05 and signup-01..05 blocks.
567
803
 
568
804
  Full-page wrapper. Props: `variant` (`"centered"` card on the page, `"muted"` same on a muted background, `"split"` form left and cover image right, `"card-image"` one wide card with form and image), `brand?` (usually `AppBrand`), `image?: { src; alt }` (split and card-image), `footer?` (terms text), `children` (the form).
569
805
 
806
+ ### `AuthForm`
807
+
808
+ The shell the auth forms share; use it for other signed-out steps (join with a code, verify an email). Props: `title`, `description?`, `children` (fields, each with a `name`), `error?`, `submitLabel`, `loading?` (spinner, disables every control), `onSubmit?(data: FormData)` (after browser validation, which `noValidate` skips; omit to submit natively), `footer?` (under the button: links, provider buttons).
809
+
810
+ ```tsx
811
+ ;<AuthForm title="Join your household" submitLabel="Join" onSubmit={(d) => join(String(d.get("code")))}>
812
+ <Field>
813
+ <FieldLabel htmlFor="code">Invite code</FieldLabel>
814
+ <Input id="code" name="code" required />
815
+ </Field>
816
+ </AuthForm>
817
+ ```
818
+
570
819
  ### `LoginForm`
571
820
 
572
821
  Props: `onSubmit?({ email, password? })` (omit to submit natively to an `action`), `mode?` (`"password"` or `"email-only"` for magic links), `providers?: { id; label; icon? }[]` + `onProviderClick?(id)` (3+ providers with icons collapse to an icon row), `providersFirst?`, `forgotPasswordHref?`, `signupHref?`, `title?`, `description?`, `submitLabel?`, `loading?`, `error?`.
573
822
 
823
+ ### `PasswordInput`
824
+
825
+ Password `Input` with a Show/Hide button (`aria-label` "Show password" / "Hide password") that switches `type` between `password` and `text`. Input props go to the input; `wrapperClassName?` styles the wrapper. `LoginForm`, `SignupForm` and `NewPasswordForm` use it for every password field.
826
+
574
827
  ### `SignupForm`
575
828
 
576
- Props: `onSubmit?({ name?, email, password })`, `nameField?` (default true), `confirmPassword?` (default true; mismatches are blocked with a browser validation message), `minPasswordLength?` (default 8), `passwordHint?`, `providers?`, `onProviderClick?`, `providersFirst?`, `loginHref?`, `title?`, `description?`, `submitLabel?`, `loading?`, `error?`.
829
+ Props: `onSubmit?({ name?, email, password }, data: FormData)`, `children?` (extra fields before the name, e.g. a household name; read them from `data`), `nameField?` (default true), `confirmPassword?` (default true; mismatches are blocked with a browser validation message), `minPasswordLength?` (default 8), `passwordHint?`, `providers?`, `onProviderClick?`, `providersFirst?`, `loginHref?`, `title?`, `description?`, `submitLabel?`, `loading?`, `error?`.
830
+
831
+ ```tsx
832
+ ;<SignupForm onSubmit={(v, data) => create({ ...v, household: String(data.get("household")) })}>
833
+ <Field>
834
+ <FieldLabel htmlFor="household">Household name</FieldLabel>
835
+ <Input id="household" name="household" required />
836
+ </Field>
837
+ </SignupForm>
838
+ ```
839
+
840
+ ### `NewPasswordForm`
841
+
842
+ Reset or first password: "New password" + "Confirm password" (a mismatch is blocked with a browser validation message). Props: `onSubmit?({ password })`, `minPasswordLength?` (default 8), `passwordHint?`, `title?` (default "Choose a new password"), `description?`, `submitLabel?` (default "Save password"), `loading?`, `error?`.
843
+
844
+ ```tsx
845
+ ;<AuthLayout variant="centered">
846
+ <NewPasswordForm onSubmit={({ password }) => reset(token, password)} loading={pending} error={error} />
847
+ </AuthLayout>
848
+ ```
577
849
 
578
850
  `GitHubIcon`, `GoogleIcon`, `AppleIcon`: brand marks for provider buttons.
579
851
 
@@ -605,6 +877,7 @@ Props: `onSubmit?({ name?, email, password })`, `nameField?` (default true), `co
605
877
  | `useIsMobile()` | `true` under 768px. |
606
878
  | `useAssistant()`, `useAssistantPage(page)`, `useDictation(options)` | See [`AssistantProvider`](#assistantprovider-useassistant-useassistantpage) and [`DictationButton`](#dictationbutton-usedictation-browserspeechsource). |
607
879
  | `useMediaQuery(query)` | `true` while the media query matches; `false` during SSR. |
880
+ | `categoryBg[color]`, type `CategoryColor` | `bg-category-N` class for a colour `1..6`, as a complete class name. |
608
881
  | `cn(...classes)` | clsx + tailwind-merge. Use for every `className` you compose. |
609
882
 
610
883
  ## Tokens
@@ -623,6 +896,7 @@ Use these as Tailwind utilities (`bg-*`, `text-*`, `border-*`, `ring-*`, `fill-*
623
896
  | `border`, `input`, `ring` | Hairlines, input borders, focus rings. |
624
897
  | `chart-1` … `chart-5`, `chart-grid` | Data series and gridlines. |
625
898
  | `category-1` … `category-6` | Members, lists, calendars: blue, teal, violet, rose, slate, cyan, each with a dark value. Never status. |
899
+ | `category-foreground` | Text and icons on a category fill (initials, a swatch's check). |
626
900
  | `sidebar-*` | Sidebar surfaces (default to the page tokens). |
627
901
 
628
902
  Other theme values: `rounded-sm/md/lg/xl/2xl` scale from `--radius` (default 14px; cards use `xl`, buttons and inputs `md`), `shadow-card` (the one elevation), `font-sans` (Geist), `font-mono` (Geist Mono), `animate-typing`.