@devalok/shilp-sutra 0.37.0 → 0.37.1

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 (127) hide show
  1. package/dist/composed/inline-edit.d.ts.map +1 -1
  2. package/dist/composed/inline-edit.js +42 -40
  3. package/dist/composed/inline-edit.js.map +1 -1
  4. package/dist/test-utils/conformance.d.ts +37 -0
  5. package/dist/test-utils/conformance.d.ts.map +1 -0
  6. package/docs/components/composed/activity-feed.md +6 -0
  7. package/docs/components/composed/avatar-group.md +8 -0
  8. package/docs/components/composed/bulk-action-bar.md +6 -0
  9. package/docs/components/composed/command-palette.md +7 -0
  10. package/docs/components/composed/confirm-dialog.md +6 -0
  11. package/docs/components/composed/content-card.md +6 -0
  12. package/docs/components/composed/date-picker.md +10 -0
  13. package/docs/components/composed/deadline-indicator.md +7 -0
  14. package/docs/components/composed/emoji-picker.md +8 -0
  15. package/docs/components/composed/empty-state.md +8 -0
  16. package/docs/components/composed/error-boundary.md +7 -0
  17. package/docs/components/composed/file-preview.md +8 -0
  18. package/docs/components/composed/filter-bar.md +8 -0
  19. package/docs/components/composed/form-section.md +6 -0
  20. package/docs/components/composed/global-loading.md +6 -0
  21. package/docs/components/composed/inline-edit.md +8 -0
  22. package/docs/components/composed/loading-skeleton.md +7 -0
  23. package/docs/components/composed/markdown-viewer.md +9 -0
  24. package/docs/components/composed/master-detail.md +7 -0
  25. package/docs/components/composed/member-picker.md +7 -0
  26. package/docs/components/composed/multi-select-popover.md +11 -0
  27. package/docs/components/composed/page-header.md +7 -0
  28. package/docs/components/composed/page-skeletons.md +10 -0
  29. package/docs/components/composed/priority-indicator.md +7 -0
  30. package/docs/components/composed/responsive-overlay.md +7 -0
  31. package/docs/components/composed/rich-chat-input.md +12 -0
  32. package/docs/components/composed/rich-text-editor.md +9 -0
  33. package/docs/components/composed/schedule-view.md +8 -0
  34. package/docs/components/composed/simple-tooltip.md +7 -0
  35. package/docs/components/composed/status-badge.md +10 -0
  36. package/docs/components/shell/app-command-palette.md +8 -0
  37. package/docs/components/shell/bottom-navbar.md +12 -0
  38. package/docs/components/shell/command-registry.md +7 -0
  39. package/docs/components/shell/link-context.md +15 -0
  40. package/docs/components/shell/notification-center.md +10 -0
  41. package/docs/components/shell/notification-preferences.md +7 -0
  42. package/docs/components/shell/sidebar.md +11 -0
  43. package/docs/components/shell/top-bar.md +11 -0
  44. package/docs/components/ui/accordion.md +10 -0
  45. package/docs/components/ui/alert-dialog.md +10 -1
  46. package/docs/components/ui/alert.md +10 -2
  47. package/docs/components/ui/aspect-ratio.md +7 -0
  48. package/docs/components/ui/autocomplete.md +7 -0
  49. package/docs/components/ui/avatar.md +10 -0
  50. package/docs/components/ui/badge-group.md +7 -0
  51. package/docs/components/ui/badge-indicator.md +7 -0
  52. package/docs/components/ui/badge.md +12 -0
  53. package/docs/components/ui/banner.md +7 -0
  54. package/docs/components/ui/breadcrumb.md +8 -0
  55. package/docs/components/ui/button-group.md +24 -4
  56. package/docs/components/ui/button-processing.md +5 -0
  57. package/docs/components/ui/button.md +9 -0
  58. package/docs/components/ui/card.md +16 -4
  59. package/docs/components/ui/charts.md +9 -0
  60. package/docs/components/ui/chat.md +9 -0
  61. package/docs/components/ui/checkbox.md +15 -1
  62. package/docs/components/ui/chip.md +9 -0
  63. package/docs/components/ui/code.md +7 -0
  64. package/docs/components/ui/collapsible.md +9 -0
  65. package/docs/components/ui/color-input.md +8 -0
  66. package/docs/components/ui/color-swatch.md +7 -0
  67. package/docs/components/ui/combobox.md +16 -3
  68. package/docs/components/ui/container.md +11 -0
  69. package/docs/components/ui/context-menu.md +8 -0
  70. package/docs/components/ui/data-table-body.md +4 -0
  71. package/docs/components/ui/data-table-bulk-actions.md +4 -0
  72. package/docs/components/ui/data-table-card.md +4 -0
  73. package/docs/components/ui/data-table-context.md +4 -0
  74. package/docs/components/ui/data-table-header.md +4 -0
  75. package/docs/components/ui/data-table-pagination.md +4 -0
  76. package/docs/components/ui/data-table-toolbar.md +7 -0
  77. package/docs/components/ui/data-table.md +25 -0
  78. package/docs/components/ui/devalok-grain.md +8 -0
  79. package/docs/components/ui/dialog.md +10 -0
  80. package/docs/components/ui/dropdown-menu.md +10 -0
  81. package/docs/components/ui/file-upload.md +8 -0
  82. package/docs/components/ui/form.md +21 -0
  83. package/docs/components/ui/hover-card.md +9 -0
  84. package/docs/components/ui/icon-button.md +7 -0
  85. package/docs/components/ui/icon-context.md +7 -0
  86. package/docs/components/ui/icon-group.md +9 -0
  87. package/docs/components/ui/icon.md +7 -0
  88. package/docs/components/ui/input-otp.md +29 -7
  89. package/docs/components/ui/input.md +7 -0
  90. package/docs/components/ui/label.md +7 -1
  91. package/docs/components/ui/link.md +7 -0
  92. package/docs/components/ui/menubar.md +9 -0
  93. package/docs/components/ui/navigation-menu.md +9 -0
  94. package/docs/components/ui/number-input.md +10 -0
  95. package/docs/components/ui/pagination.md +10 -0
  96. package/docs/components/ui/popover.md +9 -0
  97. package/docs/components/ui/progress-ring.md +7 -0
  98. package/docs/components/ui/progress.md +7 -0
  99. package/docs/components/ui/radio.md +27 -2
  100. package/docs/components/ui/search-input.md +8 -0
  101. package/docs/components/ui/segmented-control.md +8 -0
  102. package/docs/components/ui/select.md +14 -3
  103. package/docs/components/ui/separator.md +7 -0
  104. package/docs/components/ui/sheet.md +10 -0
  105. package/docs/components/ui/sidebar.md +30 -0
  106. package/docs/components/ui/skeleton.md +7 -0
  107. package/docs/components/ui/slider.md +15 -1
  108. package/docs/components/ui/spinner.md +7 -0
  109. package/docs/components/ui/split-button.md +11 -0
  110. package/docs/components/ui/stack.md +8 -0
  111. package/docs/components/ui/stat-card.md +9 -0
  112. package/docs/components/ui/status-dot.md +9 -0
  113. package/docs/components/ui/stepper.md +8 -0
  114. package/docs/components/ui/switch.md +8 -0
  115. package/docs/components/ui/table.md +8 -0
  116. package/docs/components/ui/tabs.md +10 -2
  117. package/docs/components/ui/text.md +9 -4
  118. package/docs/components/ui/textarea.md +7 -1
  119. package/docs/components/ui/toast.md +9 -0
  120. package/docs/components/ui/toaster.md +8 -0
  121. package/docs/components/ui/toggle-group.md +13 -4
  122. package/docs/components/ui/toggle.md +10 -2
  123. package/docs/components/ui/tooltip.md +11 -1
  124. package/docs/components/ui/tree-view.md +12 -0
  125. package/docs/components/ui/visually-hidden.md +7 -0
  126. package/llms-full.txt +1116 -44
  127. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -5,7 +5,7 @@
5
5
  > All variant values and props verified from source CVA definitions.
6
6
  >
7
7
  > Package: @devalok/shilp-sutra
8
- > Version: 0.37.0
8
+ > Version: 0.37.1
9
9
  >
10
10
  > **If you are an AI agent reading this file top-to-bottom:** the Setup
11
11
  > section below is authoritative. If any later per-component doc or a
@@ -220,9 +220,19 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
220
220
  </Accordion>
221
221
  ```
222
222
 
223
+ ## Composability
224
+ - Built on Radix Accordion — accepts `value`/`defaultValue`/`onValueChange` matching the `type` discriminated union.
225
+ - **`type="single"`** — value is a string (which item is open). Only one item open at a time. Pair with `collapsible` to allow closing the current item by re-clicking it.
226
+ - **`type="multiple"`** — value is a string[] (which items are open). Multiple can be open at once.
227
+ - **AccordionTrigger chevron:** Inline SVG that auto-rotates via the `data-state` attribute Radix sets (`open` / `closed`). The `chevronPosition` prop controls render order inside the trigger (left / right) — rotation still works from either position.
228
+ - **AccordionContent:** Uses Radix CSS custom properties (`--radix-accordion-content-height`) for open/close height animation. Framer Motion handles the fade. Don't wrap AccordionContent children in additional motion components — doubles the animation.
229
+ - **Inline, not portalled** — parent `overflow: hidden` WILL clip an open accordion. Keep accordions out of tight overflow contexts or use `overflow-visible` on the container.
230
+
223
231
  ## Gotchas
224
232
  - type is REQUIRED — omitting it causes runtime error
225
233
  - collapsible only works with type="single"
234
+ - Each AccordionItem needs a unique `value` — duplicates silently break toggling
235
+ - Don't put focusable elements inside AccordionTrigger — the trigger IS the focusable button, nested focusables break screen reader navigation
226
236
 
227
237
  ## Changes
228
238
  ### v0.29.0
@@ -246,14 +256,15 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
246
256
  - Category: ui
247
257
 
248
258
  ## Props
249
- variant: "subtle" | "filled" | "outline"
259
+ variant: "subtle" | "solid" | "outline" | "filled" (deprecated alias for "solid")
250
260
  color: "info" | "success" | "warning" | "error" | "neutral"
261
+ size: "sm" | "md" | "lg"
251
262
  title: string (optional)
252
263
  onDismiss: () => void (optional, shows X button when provided)
253
264
  children: ReactNode (body text)
254
265
 
255
266
  ## Defaults
256
- variant="subtle", color="info"
267
+ variant="subtle", color="info", size="md"
257
268
 
258
269
  ## Example
259
270
  ```jsx
@@ -262,6 +273,13 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
262
273
  </Alert>
263
274
  ```
264
275
 
276
+ ## Composability
277
+ - **Flat, not compound** — unlike Dialog/Card, there's no `<AlertTitle>` / `<AlertDescription>`. Use the `title` prop for the heading and `children` for the body text. This simplifies the API and prevents composition traps (missing title, out-of-order header parts).
278
+ - **Auto-icon by color:** Icon is selected automatically from color (info→circle, success→check, warning→triangle, error→alert). Pass a custom `icon` prop to override.
279
+ - **Role=alert:** Announces assertively to screen readers. Don't stack multiple Alerts in a row — the last one wins.
280
+ - **Dismissal pattern:** Pass `onDismiss` for user-dismissable alerts; the library tracks the exit animation before calling back. For persistent alerts (system state), omit `onDismiss`.
281
+ - **Not for transient messages** — use Toast for transient/time-limited notifications and Banner for page-level announcements. Alert is inline, in-flow, block-level.
282
+
265
283
  ## Gotchas
266
284
  - NOT a compound component — use title prop, NOT <AlertTitle>
267
285
  - DO NOT use variant="destructive" — use color="error"
@@ -329,8 +347,17 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
329
347
  </AlertDialog>
330
348
  ```
331
349
 
350
+ ## Composability
351
+ - Built on Radix AlertDialog — like Dialog but **non-dismissible** by clicking outside or pressing Escape. The user must choose Cancel or Action.
352
+ - Same portal + trigger + asChild pattern as Dialog.
353
+ - `AlertDialogAction` and `AlertDialogCancel` are semantically distinct from generic buttons: they auto-close the dialog on click. Use them even if you wrap them around a styled Button via `asChild` so the close behavior stays wired.
354
+ - **Focus management:** Initial focus lands on `AlertDialogCancel` (the safe default) — destructive confirmation is always one tab away.
355
+ - Use AlertDialog for destructive / irreversible actions; use Dialog for everything else.
356
+
332
357
  ## Gotchas
333
- - AlertDialogAction does NOT have color="error" styling — add it yourself via className or wrap a Button
358
+ - AlertDialogAction does NOT have color="error" styling — add it yourself via className or wrap a Button with `asChild`
359
+ - Do NOT add a close-on-outside-click handler — the non-dismissible behavior is the whole point
360
+ - AlertDialogCancel receives initial focus; don't flip the convention
334
361
 
335
362
  ## Changes
336
363
  ### v0.19.1
@@ -365,8 +392,15 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
365
392
  </AspectRatio>
366
393
  ```
367
394
 
395
+ ## Composability
396
+ - Radix AspectRatio primitive — pure layout wrapper, no context, no cascade.
397
+ - **Child fill pattern:** Its child is absolutely positioned to fill. Pair with `object-cover` / `object-contain` on `<img>`/`<video>` and `w-full h-full` for anything else.
398
+ - Useful as a responsive-image container, a chart area (pair with Chart components), or a placeholder frame for media skeletons.
399
+ - Works inside Card, any layout component, or flex/grid containers — nothing to configure.
400
+
368
401
  ## Gotchas
369
402
  - ratio is a number, not a string — use `16/9` not `"16/9"`
403
+ - The child must be size-fluid (`w-full h-full` or absolutely positioned) — elements with intrinsic size escape the aspect-ratio box
370
404
 
371
405
  ## Changes
372
406
  ### v0.1.0
@@ -400,6 +434,13 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
400
434
  />
401
435
  ```
402
436
 
437
+ ## Composability
438
+ - **Autocomplete vs Combobox:** Autocomplete allows free-text input (users can type anything); Combobox enforces selection from the list. Pick by whether "off-list" values are valid (e.g. city field that accepts typos → Autocomplete; tag picker from a fixed vocabulary → Combobox).
439
+ - **Value shape is an object** (`{ value, label }`), not a plain string — this preserves label/value decoupling for display-vs-storage.
440
+ - **FormField:** Does NOT auto-consume FormField state. Set explicit error styling via className if needed.
441
+ - **Portal rendering:** Dropdown portals to body with z-popover (1400) — stacks above Dialog/Sheet.
442
+ - **Keyboard:** ArrowDown/Up navigate suggestions, Enter selects, Esc closes. Typeahead is the input's native filtering.
443
+
403
444
  ## Gotchas
404
445
  - Allows free-text input (no forced selection) — use Combobox for forced selection
405
446
  - value is an object { value, label }, NOT just a string
@@ -452,10 +493,20 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
452
493
  </Avatar>
453
494
  ```
454
495
 
496
+ ## Composability
497
+ - **Two internal contexts cascade to AvatarFallback:**
498
+ - `AvatarSizeContext` — carries the Avatar's `size` so AvatarFallback's font size scales without re-specifying (xs → `text-[9px]`, sm → `text-ds-xs`, md → `text-ds-sm`, lg → `text-ds-md`, xl → `text-ds-lg`)
499
+ - `AvatarShapeContext` — carries `shape` so AvatarFallback inherits the same border-radius (critical for `shape="square"` / `shape="rounded"` — without context the fallback would always be `rounded-ds-full` regardless of parent)
500
+ - **Status + Ring + Badge are independent** — you can stack all three on one Avatar. They render in separate layers so they don't conflict visually.
501
+ - **Use with AvatarGroup:** AvatarGroup applies its own `size` to children. If your Avatar has an explicit `size`, it overrides AvatarGroup's default — keep it consistent across the group.
502
+ - **AvatarFallback colorSeed:** Deterministic background color from the seed string — identical user IDs produce identical backgrounds across renders. Useful for user-identifying avatars without photos.
503
+ - **Ring offset color:** The ring renders with `ring-offset` matching the current surface — it assumes the Avatar sits on `bg-surface-raised`. On other backgrounds, add a ring-offset className to compensate.
504
+
455
505
  ## Gotchas
456
506
  - Status dot renders with role="img" and aria-label (accessible, not decorative)
457
507
  - Dot size scales automatically with avatar size
458
508
  - AvatarFallback font size scales automatically with avatar size (via AvatarSizeContext)
509
+ - AvatarFallback shape inherits from Avatar (via AvatarShapeContext) — setting `rounded-*` on AvatarFallback directly conflicts with the context
459
510
  - Ring offset color matches the surface context — ensure it sits on the expected background
460
511
  - Badge is hidden when value is `0` (falsy); use `"dot"` for presence without a count
461
512
  - Online status dot pulses with a CSS animation
@@ -513,6 +564,18 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
513
564
  <Badge color="custom" style={{ '--badge-color': '#8b5cf6' }}>Custom</Badge>
514
565
  ```
515
566
 
567
+ ## Composability
568
+ - **Badge.Group** (BadgeGroup) wraps a set of badges with overflow collapse (`max` + "+N" indicator) and gap control. Use it for filter chips, tag lists, user cluster avatars-equivalent.
569
+ - **Badge.Indicator** (BadgeIndicator) attaches a count / dot overlay to ANY child (icons, avatars, buttons). It's positioning-only — doesn't style the child.
570
+ - **Interactive modes:**
571
+ - `onClick` alone → renders as `<button>` (standard interactive badge)
572
+ - `onDismiss` alone → renders as `<span>` with an inner X button
573
+ - Both → renders as `<div role="button">` (avoids invalid nested buttons) — the X button inside handles its own click isolation
574
+ - **asChild:** Use with router Links for nav-style badges (`<Badge asChild><Link href="/tags/react">React</Link></Badge>`).
575
+ - **Custom colors via CSS variables:** Set `color="custom"` + inline style `style={{ '--badge-color': '#...' }}`. For solid variant, also set `--badge-fg-color` for the foreground.
576
+ - **Icon slots auto-size via IconProvider** — same cascade as Button. Don't set explicit size on nested `<Icon>`.
577
+ - **Chip (deprecated)** was merged into Badge — use Badge with `onClick` and `selected` for the old Chip use case.
578
+
516
579
  ## Gotchas
517
580
  - DO NOT use variant="destructive" — use variant="solid" color="error"
518
581
  - Badge is now interactive when `onClick` is provided (renders as `<button>`)
@@ -587,6 +650,13 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
587
650
  {/* Renders: React, TypeScript, Tailwind, +2 */}
588
651
  ```
589
652
 
653
+ ## Composability
654
+ - **Wrapper for Badge children** — doesn't style children, just lays them out and optionally collapses overflow into a "+N" indicator.
655
+ - **Server-safe** — can render in RSC trees (children still need to be server-safe themselves).
656
+ - **Overflow indicator is a Badge** — picks `variant="outline" color="neutral"` and the `size` prop on BadgeGroup. Child badges keep their own variant/color/size.
657
+ - **Click-to-reveal pattern:** Pair `onOverflowClick` with a Popover / Sheet / Dialog that shows the full list. Without the handler, the overflow badge is inert (decorative count).
658
+ - Doesn't accept arbitrary children — only Badges. Non-Badge children render but may look off (no gap rhythm matching).
659
+
590
660
  ## Gotchas
591
661
  - Overflow badge is always `variant="outline" color="neutral"`
592
662
  - `size` only applies to the overflow badge — child badges keep their own size
@@ -627,6 +697,13 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
627
697
  </Badge.Indicator>
628
698
  ```
629
699
 
700
+ ## Composability
701
+ - **Overlay wrapper** — takes any child and overlays a positioned count/dot at the specified corner. The child must be inline-friendly (anything that renders in a span-wrapped position: relative context).
702
+ - **Common pairings:** IconButton (notification bell with count), Avatar (online presence dot), Button (cart with item count), tabs (unread indicator).
703
+ - **Invisible for layout stability:** Use `invisible={true}` to keep the child's position consistent when the badge should disappear — avoids layout shift as counts appear/disappear.
704
+ - **showZero vs hide-on-zero:** By default `count={0}` hides the indicator. Set `showZero={true}` for use cases where "zero" is meaningful (scores, pending items that should display 0 explicitly).
705
+ - **Dot vs count:** `dot={true}` is a presence-only pulse indicator (online, new activity); count is a numeric badge. They're mutually exclusive — setting both uses dot.
706
+
630
707
  ## Gotchas
631
708
  - Wraps children in `position: relative` span — the indicator is absolutely positioned
632
709
  - Animation uses spring physics, respects `prefers-reduced-motion`
@@ -659,6 +736,13 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
659
736
  </Banner>
660
737
  ```
661
738
 
739
+ ## Composability
740
+ - **Banner vs Alert vs Toast:** Banner is full-width, top-of-page / top-of-region announcements (maintenance, quota, trial-expiring). Alert is inline in-flow. Toast is transient floating notifications. Pick by where the message lives in the visual hierarchy.
741
+ - **actions slot:** Pass one or more `<Button>` elements — the container flex-wraps on narrow viewports so actions drop below the message rather than squeezing horizontally.
742
+ - **Role=alert** announces assertively. Same constraint as Alert — don't stack multiple Banners.
743
+ - **Dismissal timing:** `onDismiss` fires AFTER the exit animation completes, not on click. Use it to remove the banner from state only after it's visually gone.
744
+ - Works inside Shell layouts (TopBar / Sidebar / main content) — usually sits above main content, below the TopBar.
745
+
662
746
  ## Gotchas
663
747
  - Banner is full-width (spans container). Alert is inline.
664
748
  - Renders role="alert" automatically
@@ -706,8 +790,16 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
706
790
  </Breadcrumb>
707
791
  ```
708
792
 
793
+ ## Composability
794
+ - **Compound structure only** — no context cascade, each part is standalone. The semantic shape (`<nav>` → `<ol>` → `<li>`) is what matters for a11y, not any internal state.
795
+ - **BreadcrumbLink vs BreadcrumbPage:** Use BreadcrumbLink for everything except the current page; BreadcrumbPage for the current page (not clickable, renders without href, announced as "current page" via aria-current).
796
+ - **Router integration:** Use `<BreadcrumbLink asChild><NextLink href="/x">...</NextLink></BreadcrumbLink>` (Radix Slot pattern).
797
+ - **Truncation:** For long paths, use BreadcrumbEllipsis between the first and last items instead of showing all. Pair with a DropdownMenu (open the ellipsis to show hidden intermediate items) if you need to expose them.
798
+ - **PageHeader (composed) auto-builds Breadcrumb** from its `breadcrumbs` prop — use that higher-level component for page-top breadcrumbs and save this component for inline or custom positioning.
799
+
709
800
  ## Gotchas
710
801
  - Use BreadcrumbPage for the current (non-clickable) page, BreadcrumbLink for navigable items
802
+ - Don't wrap BreadcrumbPage in a link — it's the current location by definition
711
803
 
712
804
  ## Changes
713
805
  ### v0.18.0
@@ -751,6 +843,15 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
751
843
  <Button variant="soft" color="warning" size="compact-sm" shape="pill">Overdue</Button>
752
844
  ```
753
845
 
846
+ ## Composability
847
+ - **ButtonGroup context consumption:** When nested inside `<ButtonGroup>`, Button auto-inherits variant/color/size/weight/shape/disabled. Explicit props on the individual Button override. The context also drives position-aware border-radius (first/middle/last within an attached group).
848
+ - **IconProvider cascade:** Icons in `startIcon`/`endIcon` auto-size via IconProvider per the button size (xs→sm, sm→sm, md→md, lg→md, icon-xs→xs, icon-lg→lg). Don't pass explicit `size` to `<Icon>` inside Button.
849
+ - **asChild for router links:** `<Button asChild><Link href="/foo">...</Link></Button>` transfers Button's styling to the Link while preserving navigation semantics. Required for Next.js `<Link>` / react-router `<Link>`.
850
+ - **onClickAsync state machine:** Overrides onClick. Auto-cycles `idle → loading (aria-busy, spinner) → success (checkmark) → idle` on resolve; `loading → error (X mark) → idle` on reject. Duration controlled by `asyncFeedbackDuration` (1500ms default). Auto-activates `processing='working'` during loading — marching-ants border keeps users visually aware.
851
+ - **Processing vs loading:** `loading` is a short async state (shows spinner, blocks clicks). `processing` is a longer-running state (marching ants border, may or may not block clicks based on `processingDisabled`). Use onClickAsync for simple request cases; use processing explicitly for long-running background operations.
852
+ - **DevalokGrain children:** Grain elements are auto-extracted and rendered as direct button children for absolute positioning — lets you layer grain texture on solid-variant buttons without breaking layout.
853
+ - **Prefer `variant="soft"` over `variant="outline"` for secondary actions** (see Gotchas for details). This is a design-system-wide convention.
854
+
754
855
  ## Gotchas
755
856
  - **Prefer `variant="soft"` over `variant="outline"` for secondary actions.** Soft (tinted step-3 bg, step-11 text) is the Devalok-recommended default — it feels warmer and brand-consistent. Use `outline` only when soft's tint would disappear (on colored/surface-raised bg), in toolbar/icon-dense contexts, or when you need outline's stronger hierarchy next to a primary action.
756
857
  - DO NOT use variant="destructive" — use variant="solid" color="error"
@@ -816,24 +917,44 @@ Note: getFormFieldA11y() was removed in favor of useFormField() hook.
816
917
  - Category: ui
817
918
 
818
919
  ## Props
819
- variant: ButtonProps['variant'] (propagated to children)
920
+ variant: ButtonProps['variant'] (propagated to children via context)
820
921
  color: ButtonProps['color'] (propagated to children)
821
922
  size: ButtonProps['size'] (propagated to children)
822
- orientation: "horizontal" | "vertical" (default: "horizontal")
923
+ weight: ButtonProps['weight'] (propagated to children)
924
+ shape: ButtonProps['shape'] (propagated to children)
925
+ disabled: boolean (propagates to all children)
926
+ orientation: "horizontal" | "vertical"
927
+ attached: boolean (true = buttons visually merge with shared borders; false = spaced apart via gap)
928
+ fullWidth: boolean (group stretches to parent width; children stretch equally)
823
929
 
824
930
  ## Defaults
825
- orientation="horizontal"
931
+ orientation="horizontal", attached=true
826
932
 
827
933
  ## Example
828
934
  ```jsx
829
935
  <ButtonGroup variant="outline" size="sm">
830
936
  <Button>Bold</Button>
831
937
  <Button>Italic</Button>
938
+ <Button>Underline</Button>
939
+ </ButtonGroup>
940
+
941
+ {/* Spaced, not attached */}
942
+ <ButtonGroup attached={false} variant="soft">
943
+ <Button>Save</Button>
944
+ <Button>Cancel</Button>
832
945
  </ButtonGroup>
833
946
  ```
834
947
 
948
+ ## Composability
949
+ - Every Button child reads variant/color/size/weight/shape/disabled from ButtonGroup context. Explicit props on individual children override context.
950
+ - **Position-aware border radius:** Child Buttons read their position in the group (first / middle / last) and apply appropriate corner radii inline. Works for both horizontal and vertical orientations.
951
+ - **Focus z-index isolation:** The focused button rises above its siblings so the focus ring isn't clipped by adjacent borders.
952
+ - **Tonal dividers:** For solid/soft/ghost variants without visible borders, ButtonGroup injects subtle divider elements between children.
953
+ - **Works with SplitButton:** `<SplitButton>` inside a `<ButtonGroup>` inherits the same context and position rules.
954
+
835
955
  ## Gotchas
836
- - Children can override variant/size individually
956
+ - Children can override variant/size individually — context is a default, not a lock
957
+ - `attached={false}` disables the position-aware border radius (children use their default corners)
837
958
 
838
959
  ## Changes
839
960
  ### v0.33.0
@@ -862,6 +983,11 @@ Internal component that renders the processing animation overlay for Button. Con
862
983
 
863
984
  See `Button` docs for the public API.
864
985
 
986
+ ## Composability
987
+ - **Internal only — do NOT import or use directly.** The Button component owns the lifecycle of this overlay.
988
+ - Drives Button's `processing` prop rendering. If you're building a custom button and want similar marching-ants feedback, copy the SVG/animation pattern — don't reach into this component.
989
+ - Listed here for reference so consumers grep'ing for "processing" find the behavior origin.
990
+
865
991
  ## Changes
866
992
  ### v0.29.0
867
993
  - **Added** Initial release — marching ants processing overlay (SVG dashed rect with animated stroke-dashoffset). Speed tiers: ambient (3s), working (2s), urgent (1s). Color maps to step-11 tokens for visibility on all variants.
@@ -873,18 +999,22 @@ See `Button` docs for the public API.
873
999
 
874
1000
  ## Props
875
1001
  variant: "default" | "elevated" | "outline" | "flat"
1002
+ color: "default" | "accent" | "error" | "success" | "warning" | "info" | "neutral" (border accent color)
1003
+ size: "sm" | "md" | "lg" (padding — propagated to CardHeader/CardContent/CardFooter via context)
876
1004
  interactive: boolean (enables hover shadow lift + pointer cursor)
1005
+ accent: "left" | "top" | "right" | "bottom" (render a colored accent bar on the specified edge)
1006
+ accentColor: "default" | "accent" | "error" | "success" | "warning" | "info" (color of the accent bar; default maps to accent-9)
877
1007
 
878
1008
  ## Compound Components
879
1009
  Card (root)
880
- CardHeader
1010
+ CardHeader ← inherits size from Card context
881
1011
  CardTitle
882
1012
  CardDescription
883
- CardContent
884
- CardFooter
1013
+ CardContent ← inherits size from Card context
1014
+ CardFooter ← inherits size from Card context
885
1015
 
886
1016
  ## Defaults
887
- variant="default"
1017
+ variant="default", color="default", size="md"
888
1018
 
889
1019
  ## Example
890
1020
  ```jsx
@@ -897,8 +1027,16 @@ See `Button` docs for the public API.
897
1027
  </Card>
898
1028
  ```
899
1029
 
1030
+ ## Composability
1031
+ - **Size cascades through context** — Card's `size` prop sets padding on CardHeader, CardContent, and CardFooter via `CardSizeContext`. Don't set padding classes on sub-components directly; override via `className` if needed.
1032
+ - **Not a compound state machine** — Card, CardHeader, CardTitle, etc. are purely structural. No open/close state.
1033
+ - **Accent bar is independent:** The `accent` / `accentColor` props render a decorative colored edge bar (absolutely positioned, `aria-hidden`). Works alongside `color` (which tints the border) — the two can stack for layered emphasis.
1034
+ - **Interactive cards:** Set `interactive={true}` + `onClick` for clickable cards (entire surface becomes the button). Add `aria-label` on the Card root when there's no visible heading. For complex multi-action cards, prefer standard Card with explicit buttons inside.
1035
+ - **ContentCard (composed)** is a higher-level wrapper with built-in header/footer slots and title/actions — use it for list-row-style cards; use Card directly for custom layouts.
1036
+
900
1037
  ## Gotchas
901
1038
  - Use `interactive` prop for clickable cards — adds hover lift and pointer cursor
1039
+ - Don't override CardHeader/CardContent/CardFooter padding via className if you want the size cascade to work — set size on Card instead
902
1040
 
903
1041
  ## Changes
904
1042
  ### v0.31.0
@@ -947,9 +1085,18 @@ import { BarChart } from '@devalok/shilp-sutra/ui/charts'
947
1085
  <BarChart data={salesData} />
948
1086
  ```
949
1087
 
1088
+ ## Composability
1089
+ - **ChartContainer wraps all chart primitives** — provides responsive sizing (fills parent width, configurable aspect ratio) and a consistent background/padding. Always render charts inside a ChartContainer unless you're building a custom container.
1090
+ - **Legend is a sibling, not a child** — render `<Legend items={...} />` next to your chart (above/below), not nested. Use `onHover` / `onClick` handlers in Legend + chart to sync hover highlights.
1091
+ - **Data shape is chart-specific** — BarChart wants `[{ label, value }]`, LineChart wants `[{ x, y, series }]`, etc. Each chart's props doc spells out the shape; don't assume cross-chart compatibility.
1092
+ - **Sparkline is the only chart safe to use inline** — it's a minimal 1-line SVG, no axes, no legend. Use it inside StatCard, tables, tight cells. For anything larger, reach for the full chart types.
1093
+ - **Entrance animations** — all charts use Framer Motion spring-based entry. Animations respect `prefers-reduced-motion`.
1094
+ - **Accessibility:** Charts render role="img" with aria-label from the chart title. For dense data, provide a hidden accessible table fallback manually (WCAG requirement for complex charts).
1095
+
950
1096
  ## Gotchas
951
1097
  - Barrel-isolated since v0.5.0 — must use `@devalok/shilp-sutra/ui/charts`, NOT the `ui` barrel
952
1098
  - Requires D3 as an optional peer dependency
1099
+ - Chart components do NOT auto-generate accessible data tables — add one manually for WCAG compliance on complex charts
953
1100
 
954
1101
  ## Changes
955
1102
  ### v0.18.0
@@ -1161,6 +1308,15 @@ Animated bouncing dots with a text description of who is typing.
1161
1308
 
1162
1309
  ---
1163
1310
 
1311
+ ## Composability
1312
+ - **Chat is a kit of 7 primitives, not a monolithic ChatWindow.** Compose MessageList (scroll container) + Message/SystemMessage (row variants) + DateSeparator/UnreadSeparator (visual dividers) + MessageInput (composer) + TypingIndicator (presence).
1313
+ - **Message is a compound component** — `Message.Avatar`, `Message.Content`, `Message.Author`, `Message.Body`, `Message.Reactions`, `Message.Actions`, `Message.Action`. Arrange them to match your design; most layouts only need Avatar + Content (with Author + Body inside).
1314
+ - **Message.Actions reveal on hover** via `group-hover/message` — ONLY works when Actions is nested inside a Message root. If you want a custom action bar, do it differently.
1315
+ - **MessageList ARIA:** `role="log" + aria-live="polite"` — screen readers announce new messages automatically. Don't manually add aria-live to children.
1316
+ - **grouped mode:** For consecutive messages from the same user, set `grouped={true}` to hide the avatar and author (typical chat UI pattern). Logic for "same user as previous" is consumer-side.
1317
+ - **MessageInput is specifically for chat** — auto-resizing textarea (up to 160px), Enter-to-send, Shift+Enter-for-newline, streaming support. For richer composition (formatting, attachments), use RichChatInput from composed (built on top of this + TipTap).
1318
+ - **TypingIndicator** accepts multiple users — handles pluralization of the "is/are typing" label automatically.
1319
+
1164
1320
  ## Gotchas
1165
1321
  - MessageList uses `role="log"` with `aria-live="polite"` — screen readers announce new messages
1166
1322
  - Message entrance animations use Framer Motion springs — AnimatePresence wraps children in MessageList
@@ -1183,20 +1339,34 @@ Animated bouncing dots with a text description of who is typing.
1183
1339
  ## Props
1184
1340
  checked: boolean | "indeterminate"
1185
1341
  onCheckedChange: (checked: boolean | "indeterminate") => void
1342
+ size: "sm" | "md" | "lg"
1186
1343
  error: boolean (shows red border)
1187
1344
  indeterminate: boolean (overrides checked, shows dash icon)
1188
1345
  disabled: boolean
1189
1346
 
1190
1347
  ## Defaults
1191
- none
1348
+ size="md"
1192
1349
 
1193
1350
  ## Example
1194
1351
  ```jsx
1195
1352
  <Checkbox checked={agreed} onCheckedChange={(v) => setAgreed(v === true)} />
1353
+
1354
+ {/* With label */}
1355
+ <label htmlFor="terms" className="flex items-center gap-ds-02">
1356
+ <Checkbox id="terms" checked={agreed} onCheckedChange={(v) => setAgreed(v === true)} />
1357
+ I agree to the terms
1358
+ </label>
1196
1359
  ```
1197
1360
 
1361
+ ## Composability
1362
+ - **FormField integration:** Unlike Input/Textarea, Checkbox does NOT auto-consume FormField state — the `error` prop must be set explicitly. (Form-library convention differs for checkboxes since they often don't share the same visual grouping.)
1363
+ - **Labels:** Checkbox doesn't carry its own label — pair it with `<Label htmlFor="x" />` + `<Checkbox id="x" />`, or wrap both in a `<label>` element. Screen readers rely on that association.
1364
+ - **Controlled vs uncontrolled:** Pass `checked` + `onCheckedChange` for controlled, `defaultChecked` for uncontrolled. Don't mix.
1365
+
1198
1366
  ## Gotchas
1199
1367
  - indeterminate overrides checked visually
1368
+ - Checkbox does NOT auto-consume FormField error state — pass `error` explicitly if using inside a FormField
1369
+ - WCAG 2.5.8 minimum target size met at md (24px); sm (20px) may fail if not paired with enough label hit area
1200
1370
 
1201
1371
  ## Changes
1202
1372
  ### v0.22.0
@@ -1229,8 +1399,15 @@ Animated bouncing dots with a text description of who is typing.
1229
1399
  <Code variant="block">{`const x = 1;\nconsole.log(x);`}</Code>
1230
1400
  ```
1231
1401
 
1402
+ ## Composability
1403
+ - **Server-safe.** No context, no cascade.
1404
+ - Inline variant composes inside Text, paragraphs, list items, table cells — wherever body text goes.
1405
+ - Block variant is a `<pre><code>` pair with overflow-x scroll; pair with a dark background Card or MarkdownViewer for long snippets.
1406
+ - Not a syntax-highlighted code viewer — for that, use RichTextEditor's code block or a dedicated library. Code here is typographic emphasis, not lexer-aware rendering.
1407
+
1232
1408
  ## Gotchas
1233
1409
  - "block" renders as `<pre><code>`, "inline" renders as `<code>`
1410
+ - Block variant doesn't syntax-highlight — it's a styled pre tag, nothing more
1234
1411
 
1235
1412
  ## Changes
1236
1413
  ### v0.1.1
@@ -1265,8 +1442,17 @@ Animated bouncing dots with a text description of who is typing.
1265
1442
  </Collapsible>
1266
1443
  ```
1267
1444
 
1445
+ ## Composability
1446
+ - Radix Collapsible primitive — smaller than Accordion (no grouping/value matching, just open/close).
1447
+ - **When to use:** A single show/hide section where the state is local (e.g. "Advanced options" inside a form, expandable row detail, show-more). For multi-section with mutual exclusion, use Accordion.
1448
+ - **Inline rendering:** CollapsibleContent is NOT portalled — it expands inline and pushes sibling content down. Respects `overflow` rules of the parent.
1449
+ - **No built-in chevron** — unlike AccordionTrigger, CollapsibleTrigger is bare. Add your own icon + rotate via `data-state` attribute Radix sets (`[data-state=open]:rotate-180`).
1450
+ - **Animation:** Height transition via `animate-collapsible-down/up` keyframes + Radix `--radix-collapsible-content-height` CSS custom property.
1451
+
1268
1452
  ## Gotchas
1269
1453
  - Standard Radix Collapsible API
1454
+ - No auto-chevron — add one manually if you want the affordance
1455
+ - Not portalled — clipped by parent `overflow: hidden`
1270
1456
 
1271
1457
  ## Changes
1272
1458
  ### v0.13.0
@@ -1341,6 +1527,14 @@ Animated bouncing dots with a text description of who is typing.
1341
1527
  <ColorInput value={color} onChange={setColor} showPicker={false} />
1342
1528
  ```
1343
1529
 
1530
+ ## Composability
1531
+ - **Built on Popover internally** — trigger opens a portal-rendered picker panel. z-popover (1400) stacking.
1532
+ - **variant="default" vs "inline":** Default variant is an input-style trigger with gradient swatch + hex label (fits in forms). Inline variant IS the selected color — the entire trigger takes the color as its background with contrast-aware text. Use inline for color-tags in chat, lists, or tight toolbar UIs.
1533
+ - **Controlled or uncontrolled:** Works both ways — pass `value` + `onChange` for controlled, or omit and let internal state track.
1534
+ - **Presets are compositional:** Pass `{ hex, label }[]` for named brand colors (accessible, keyboard-navigable). Pass `false` to hide the preset strip entirely (picker-only mode).
1535
+ - **react-colorful** is bundled (2.8KB gzipped) — zero additional setup. Picker itself is pointer-based; keyboard users edit via the HEX/RGB/HSL format inputs below.
1536
+ - **FormField:** Not auto-consumed (no `state` prop). Wrap in FormField for label + helper text; style error visuals via className.
1537
+
1344
1538
  ## Gotchas
1345
1539
  - Value must be a 6-character hex string (e.g. "#d33163")
1346
1540
  - Presets accept both `string[]` (backward-compatible) and `{ hex, label }[]` (recommended for accessibility)
@@ -1391,6 +1585,13 @@ Animated bouncing dots with a text description of who is typing.
1391
1585
  <ColorSwatch color="oklch(0.7 0.15 200)" shape="rounded" />
1392
1586
  ```
1393
1587
 
1588
+ ## Composability
1589
+ - **Server-safe, decorative-only primitive.** Accepts any valid CSS color string — hex, rgb, oklch, hsl, named. Pure presentation, no context.
1590
+ - **Usage patterns:** Inline color marker next to a label, preview dot in lists, color-indicator in a category chip, legend swatch for charts.
1591
+ - **`ring={true}`** adds a subtle outline — essential for white/very-light colors that would otherwise disappear on surface-raised backgrounds.
1592
+ - **Interactive color picking:** Use ColorInput (which opens a full picker). ColorSwatch is display-only.
1593
+ - No IconProvider cascade, no FormField consumption — composes freely with anything.
1594
+
1394
1595
  ## Gotchas
1395
1596
  - Color is applied via inline `backgroundColor` style, not a token class — accepts any runtime CSS color string
1396
1597
  - Renders `role="presentation"` — purely decorative, not interactive
@@ -1402,6 +1603,7 @@ Animated bouncing dots with a text description of who is typing.
1402
1603
 
1403
1604
  ## Props
1404
1605
  options: ComboboxOption[] (REQUIRED) — { value: string, label: string, description?: string, icon?: ReactNode, disabled?: boolean }
1606
+ size: "xs" | "sm" | "md" | "lg" (trigger height)
1405
1607
  DISCRIMINATED UNION — type depends on `multiple` flag:
1406
1608
  Single (default): multiple?: false, value?: string, onValueChange: (value: string) => void
1407
1609
  Multiple: multiple: true, value?: string[], onValueChange: (value: string[]) => void
@@ -1409,13 +1611,14 @@ Animated bouncing dots with a text description of who is typing.
1409
1611
  searchPlaceholder: string (default: "Search...")
1410
1612
  emptyMessage: string (default: "No results found")
1411
1613
  disabled: boolean
1412
- triggerClassName: string
1614
+ className: string (wrapper div — the positioning container, NOT the trigger)
1615
+ triggerClassName: string (the actual Popover trigger button)
1413
1616
  accessibleLabel: string (custom aria-label for trigger, falls back to placeholder)
1414
1617
  maxVisible: number (default: 6, max dropdown items before scroll)
1415
1618
  renderOption: (option, selected) => ReactNode
1416
1619
 
1417
1620
  ## Defaults
1418
- placeholder="Select...", searchPlaceholder="Search...", emptyMessage="No results found", maxVisible=6
1621
+ size="md", placeholder="Select...", searchPlaceholder="Search...", emptyMessage="No results found", maxVisible=6
1419
1622
 
1420
1623
  ## Example
1421
1624
  ```jsx
@@ -1428,9 +1631,20 @@ Animated bouncing dots with a text description of who is typing.
1428
1631
  />
1429
1632
  ```
1430
1633
 
1634
+ ## Composability
1635
+ - **Combobox vs Autocomplete vs Select:** Combobox = typeahead search + forced selection (the user picks from filtered options). Autocomplete = typeahead + free text allowed. Select = no typeahead, click-to-open with fixed options. Pick by user behavior, not visual style.
1636
+ - **Single vs multi mode** is a discriminated union — `multiple: true` changes the shape of value (`string[]`) and onValueChange. TypeScript enforces the pairing.
1637
+ - **Multi-select pills:** Capped at 2 visible + "+N more" overflow regardless of `maxVisible`. Clicking the +N opens a popover list of all selected items (handled by the component).
1638
+ - **className vs triggerClassName:** className lands on the wrapper (positioning); triggerClassName lands on the trigger button (styling the control). Know which you need.
1639
+ - **Portal + z-popover (1400):** content stacks above Dialog/Sheet; works inside scrolling containers without clipping.
1640
+ - **renderOption:** For complex option rendering (avatar + label + description), pass `renderOption: (option, selected) => <YourCustom />`. The selected state is a boolean flag.
1641
+ - **FormField:** Does NOT auto-consume FormField state. Wrap in FormField for label + helper text, but style error manually.
1642
+
1431
1643
  ## Gotchas
1432
1644
  - Enforces selection from list (unlike Autocomplete which allows free text)
1433
- - In multi mode, selected items appear as pills with "+N more" overflow
1645
+ - In multi mode, selected items appear as pills with "+N more" overflow (capped at 2 visible pills regardless of `maxVisible`)
1646
+ - `className` vs `triggerClassName`: `className` lands on the wrapper div (useful for width/positioning); `triggerClassName` lands on the actual Popover trigger button (useful for styling the control itself)
1647
+ - Dropdown content is portalled to document.body — parent styles like `overflow: hidden` don't clip it, and container-scoped test queries won't find it
1434
1648
 
1435
1649
  ## Changes
1436
1650
  ### v0.18.0
@@ -1468,8 +1682,19 @@ Animated bouncing dots with a text description of who is typing.
1468
1682
  </Container>
1469
1683
  ```
1470
1684
 
1685
+ ## Composability
1686
+ - **Server-safe layout primitive.** Safe in RSC trees — no hooks, no context.
1687
+ - **maxWidth choices:**
1688
+ - `"default"` (standard page container — matches the design system's layout grid)
1689
+ - `"body"` (narrower reading width — use for article/blog content)
1690
+ - `"full"` (no max — edge-to-edge, useful for full-bleed marketing sections)
1691
+ - **Polymorphic via `as`:** Change the rendered element for semantics (`as="main"` for page content, `as="section"` for major subdivisions, `as="article"` for standalone content).
1692
+ - **mx-auto centering is automatic** — Container handles horizontal centering; it does NOT add vertical spacing. Pair with `py-*` utility classes on the Container itself or inside it.
1693
+ - Nothing cascades — nesting Containers is fine but rarely useful (max-width constraints compound).
1694
+
1471
1695
  ## Gotchas
1472
1696
  - Server-safe component — can be imported directly in Next.js Server Components
1697
+ - Container does NOT add vertical padding — add it explicitly via className if needed
1473
1698
 
1474
1699
  ## Changes
1475
1700
  ### v0.1.0
@@ -1505,8 +1730,16 @@ Animated bouncing dots with a text description of who is typing.
1505
1730
  </ContextMenu>
1506
1731
  ```
1507
1732
 
1733
+ ## Composability
1734
+ - Radix ContextMenu — same item-variant surface as DropdownMenu (checkbox, radio group, sub-menus, label, separator) with the same keyboard model. Swap `Dropdown` → `Context` in imports and everything behaves the same.
1735
+ - **Trigger is not a button** — ContextMenuTrigger is a wrapper element (defaults to `<div>`) that listens for `contextmenu` events on itself and children. Use `asChild` to pass styling to your own element.
1736
+ - **Touch support:** Radix maps long-press to right-click. On mobile, hold-to-open just works without extra code.
1737
+ - **No visible trigger affordance** — unlike DropdownMenu/Popover, the user has to *know* the element is right-clickable. Pair with a visible hint (keyboard shortcut label, menu icon elsewhere) for discoverability.
1738
+ - **Portal + z-index:** z-popover (1400).
1739
+
1508
1740
  ## Gotchas
1509
1741
  - Triggered by right-click (or long-press on touch devices)
1742
+ - ContextMenuTrigger doesn't auto-indicate it's interactive — add visual affordance elsewhere on the page
1510
1743
 
1511
1744
  ## Changes
1512
1745
  ### v0.18.0
@@ -1574,6 +1807,30 @@ import { DataTable } from '@devalok/shilp-sutra/ui/data-table'
1574
1807
  />
1575
1808
  ```
1576
1809
 
1810
+ ## Composability
1811
+ **Server vs client mode is prop-driven, not explicit.**
1812
+ - Pass `onSort` → server-side sort (manual, rows stay in data order — you're responsible for re-fetching).
1813
+ - Pass `pagination` object → server-side pagination (manual, pass total count).
1814
+ - Omit both → client-side sort/pagination via TanStack react-table.
1815
+ - Mix-and-match: `onSort` + no pagination = server sort + client pagination.
1816
+
1817
+ **Companion components:**
1818
+ - `DataTableToolbar` — enabled via `toolbar={true}`. Provides column visibility, density switcher, CSV export. Reads table state via `DataTableContext` (internal). Rendered ABOVE the table automatically.
1819
+ - `BulkActionBar` (floating) — appears when rows are selected AND `bulkActions` array is non-empty. Synced with `selectedIds`; shows count + action buttons.
1820
+ - `EmptyState` from `@devalok/shilp-sutra/composed` — pass to `emptyState` prop. Takes precedence over `noResultsText` string.
1821
+
1822
+ **Controlled selection:**
1823
+ - Pass `selectedIds` (Set<string>) + `onSelectionChange` for controlled row selection.
1824
+ - Provide `getRowId: (row) => row.id` so selection survives data refetches (otherwise TanStack uses array index, which breaks on sort/filter).
1825
+ - `selectableFilter: (row) => boolean` disables selection on specific rows (e.g. archived items).
1826
+
1827
+ **Row click model:**
1828
+ - `onRowClick` fires on row-level click BUT excludes clicks on checkboxes, buttons, links, and inputs automatically. No manual `stopPropagation` needed for standard interactive elements.
1829
+
1830
+ **Virtualization:** `virtualRows={true}` enables row virtualization via `@tanstack/react-virtual`. Turn it on for 1000+ row datasets; the scroll container must have a bounded height.
1831
+
1832
+ **Density integration:** `defaultDensity="compact"` is the Karm-style dense mode (h-9 rows). DataTableToolbar's density switcher updates this at runtime; the prop sets the initial state only.
1833
+
1577
1834
  ## Gotchas
1578
1835
  - Barrel-isolated since v0.5.0 — must use `@devalok/shilp-sutra/ui/data-table`, NOT the `ui` barrel
1579
1836
  - Requires @tanstack/react-table and @tanstack/react-virtual as peer dependencies
@@ -1582,6 +1839,7 @@ import { DataTable } from '@devalok/shilp-sutra/ui/data-table'
1582
1839
  - selectedIds syncs via useEffect — provide getRowId for custom row IDs
1583
1840
  - onRowClick does NOT fire when clicking checkboxes, buttons, links, or inputs
1584
1841
  - Use defaultDensity="compact" for Karm-style h-9 rows
1842
+ - `virtualRows={true}` requires a bounded scroll container — unbounded height silently disables virtualization
1585
1843
 
1586
1844
  ## Changes
1587
1845
  ### v0.29.0
@@ -1620,6 +1878,10 @@ import { DataTable } from '@devalok/shilp-sutra/ui/data-table'
1620
1878
  This component is used internally by `<DataTable>` and should not be imported directly.
1621
1879
  See [DataTable](./data-table.md) for the public API.
1622
1880
 
1881
+ ## Composability
1882
+ - **Internal only.** Renders the tbody + rows for DataTable. Consumes DataTableContext for row data, selection state, expandable state, loading skeletons.
1883
+ - Customization routes through DataTable props: `renderExpanded`, `onRowClick`, `getRowId`, `loading`, `emptyState`.
1884
+
1623
1885
  ## Changes
1624
1886
 
1625
1887
  ### v0.32.0
@@ -1633,6 +1895,10 @@ See [DataTable](./data-table.md) for the public API.
1633
1895
  This component is used internally by `<DataTable>` and should not be imported directly.
1634
1896
  See [DataTable](./data-table.md) for the public API.
1635
1897
 
1898
+ ## Composability
1899
+ - **Internal only.** Renders the floating bulk-action bar that appears when rows are selected. Reads selection state + bulkActions config from DataTableContext.
1900
+ - Customization goes through DataTable's `bulkActions` prop — `{ label, onClick, color?, disabled? }[]`.
1901
+
1636
1902
  ## Changes
1637
1903
 
1638
1904
  ### v0.32.0
@@ -1646,6 +1912,10 @@ See [DataTable](./data-table.md) for the public API.
1646
1912
  This component is used internally by `<DataTable>` and should not be imported directly.
1647
1913
  See [DataTable](./data-table.md) for the public API.
1648
1914
 
1915
+ ## Composability
1916
+ - **Internal only.** Renders the card wrapper around DataTable (border, shadow, rounded corners matching the Card primitive's surface treatment).
1917
+ - DataTable uses this automatically — no consumer-facing prop toggles it.
1918
+
1649
1919
  ## Changes
1650
1920
 
1651
1921
  ### v0.32.0
@@ -1659,6 +1929,10 @@ See [DataTable](./data-table.md) for the public API.
1659
1929
  This component is used internally by `<DataTable>` and should not be imported directly.
1660
1930
  See [DataTable](./data-table.md) for the public API.
1661
1931
 
1932
+ ## Composability
1933
+ - **Internal React context** — binds DataTable's sub-components (header, body, pagination, toolbar, bulk-actions) to shared state: TanStack table instance, density, selection, loading.
1934
+ - Consumer code never reads this context — all interaction goes through DataTable's props.
1935
+
1662
1936
  ## Changes
1663
1937
 
1664
1938
  ### v0.32.0
@@ -1672,6 +1946,10 @@ See [DataTable](./data-table.md) for the public API.
1672
1946
  This component is used internally by `<DataTable>` and should not be imported directly.
1673
1947
  See [DataTable](./data-table.md) for the public API.
1674
1948
 
1949
+ ## Composability
1950
+ - **Internal only.** Renders the thead + column headers for DataTable. Reads sort state + column definitions from DataTableContext.
1951
+ - Customization goes through DataTable's `columns` prop (column definitions are TanStack ColumnDef<TData> — header, sortable, filter, etc. all declared there).
1952
+
1675
1953
  ## Changes
1676
1954
 
1677
1955
  ### v0.32.0
@@ -1685,6 +1963,10 @@ See [DataTable](./data-table.md) for the public API.
1685
1963
  This component is used internally by `<DataTable>` and should not be imported directly.
1686
1964
  See [DataTable](./data-table.md) for the public API.
1687
1965
 
1966
+ ## Composability
1967
+ - **Internal only.** Renders the pagination footer for DataTable. Reads pagination state from DataTableContext.
1968
+ - Customization goes through DataTable's `pagination` prop (server-side: pass `{ page, pageSize, total, onPageChange }`) or `pageSize` + `paginated` (client-side).
1969
+
1688
1970
  ## Changes
1689
1971
 
1690
1972
  ### v0.32.0
@@ -1722,10 +2004,17 @@ import { DataTableToolbar } from '@devalok/shilp-sutra/ui/data-table-toolbar'
1722
2004
  />
1723
2005
  ```
1724
2006
 
2007
+ ## Composability
2008
+ - **Companion to DataTable** — typically enabled via DataTable's `toolbar={true}` prop (which auto-renders this internally). Use DataTableToolbar directly only if you're building a custom DataTable integration that needs the toolbar positioned/styled differently.
2009
+ - **Reads table state via the `table` instance** (TanStack react-table) — you must pass it. For the auto-rendered version inside DataTable, this wiring is automatic.
2010
+ - **Feature toggles:** `globalFilter` (search across all columns), density switcher (compact/standard/comfortable row heights), CSV export. Turn each on/off independently.
2011
+ - **Density changes are runtime** — the switcher writes to the same `defaultDensity` that DataTable's prop seeds. State lives in DataTable context.
2012
+
1725
2013
  ## Gotchas
1726
2014
  - Barrel-isolated since v0.5.0 — must use `@devalok/shilp-sutra/ui/data-table-toolbar`, NOT the `ui` barrel
1727
2015
  - Companion to DataTable — provides column visibility, density toggle, and CSV export controls
1728
2016
  - Requires @tanstack/react-table as a peer dependency
2017
+ - Prefer DataTable's `toolbar={true}` prop over rendering this directly
1729
2018
 
1730
2019
  ## Changes
1731
2020
  ### v0.5.0
@@ -1776,6 +2065,14 @@ import { DataTableToolbar } from '@devalok/shilp-sutra/ui/data-table-toolbar'
1776
2065
  </div>
1777
2066
  ```
1778
2067
 
2068
+ ## Composability
2069
+ - **Brand texture overlay** — drops into any parent with `relative overflow-hidden isolate`. Auto-inherits the parent's border radius.
2070
+ - **Button auto-extracts Grain children** — if you nest `<DevalokGrain>` inside a `<Button>`, Button auto-separates it from the label slot and positions it correctly. No extra wrapping needed.
2071
+ - **Card, custom hero sections, landing-page tiles** — wrap in a positioned container (or add the `relative overflow-hidden isolate` classes to Card explicitly) and drop Grain in as a sibling to your content.
2072
+ - **z-layer contract:** Grain renders at `z-[1]`. Content on top needs `z-[2]+`. The component doesn't boost child z-index — that's consumer responsibility.
2073
+ - **`hoverIntensify` depends on parent** — the parent must have the Tailwind `group` class so `group-hover:` selectors apply. Forgetting this silently disables the effect.
2074
+ - **Tint composes with surface:** Pass an OKLCH color (or CSS variable reference like `var(--color-accent-9)`) to `tint` for a directional gradient on top of the noise. Without tint, just noise.
2075
+
1779
2076
  ## Gotchas
1780
2077
  - Parent element MUST have `relative overflow-hidden isolate` for the grain to render correctly
1781
2078
  - The grain layers are absolute-positioned at `z-[1]` — content that should appear above must use `z-[2]` or higher
@@ -1825,8 +2122,18 @@ import { DataTableToolbar } from '@devalok/shilp-sutra/ui/data-table-toolbar'
1825
2122
  </Dialog>
1826
2123
  ```
1827
2124
 
2125
+ ## Composability
2126
+ - Built on Radix Dialog primitives — every standard Radix prop passes through (`open`, `onOpenChange`, `defaultOpen`, `modal`).
2127
+ - **Trigger composition:** `<DialogTrigger asChild>` renders the wrapped element as the trigger — typical pattern is `<DialogTrigger asChild><Button>...</Button></DialogTrigger>`. Without `asChild`, a default button is injected.
2128
+ - **Portal rendering:** DialogContent portals to `document.body`. CSS containers (`overflow: hidden`, `transform`, stacking contexts) on ancestors of the trigger don't clip it. Container-scoped test queries miss portalled content — use `screen` or the portal root.
2129
+ - **Focus management:** Radix traps focus inside Content while open and restores it to the trigger on close. First focusable element inside receives focus automatically.
2130
+ - **Imperative close** from deep children: use `<DialogClose asChild>` around your own button, or `useDialogContext` — no prop drilling.
2131
+ - **z-index:** DialogContent uses `z-dialog` (configured in Tailwind theme). Nested overlays (Popover inside Dialog) stack on top because Popover uses `z-popover` (1400) higher than `z-dialog`.
2132
+
1828
2133
  ## Gotchas
1829
2134
  - DialogTitle is required for accessibility — screen readers announce it when the dialog opens
2135
+ - If your layout suppresses the title visually, use `<VisuallyHidden>` around DialogTitle — don't omit it
2136
+ - Focus returns to the trigger on close — if the trigger is conditionally unmounted, focus lands on body; handle manually if that matters
1830
2137
 
1831
2138
  ## Changes
1832
2139
  ### v0.19.1
@@ -1875,8 +2182,18 @@ import { DataTableToolbar } from '@devalok/shilp-sutra/ui/data-table-toolbar'
1875
2182
  </DropdownMenu>
1876
2183
  ```
1877
2184
 
2185
+ ## Composability
2186
+ - Radix DropdownMenu underneath — same prop surface (`open`, `onOpenChange`, `defaultOpen`, `modal`).
2187
+ - **Item variants** stack cleanly: `DropdownMenuCheckboxItem` for multi-select toggles, `DropdownMenuRadioGroup` + `DropdownMenuRadioItem` for single-select, `DropdownMenuSub` + `DropdownMenuSubTrigger/SubContent` for nested submenus. Each has its own keyboard model pre-wired.
2188
+ - **Keyboard:** Arrow keys navigate items, Enter/Space activates, Esc closes, typeahead jumps to first letter. All handled by Radix — don't re-implement.
2189
+ - **Trigger:** `<DropdownMenuTrigger asChild>` around any button. IconButton is the common pairing.
2190
+ - **Shortcut hints:** `<DropdownMenuShortcut>` inside an item renders a right-aligned `⌘K`-style kbd. Visual only — does NOT bind the shortcut globally.
2191
+ - **Closing from a handler:** Item onSelect auto-closes the menu by default. Pass `event.preventDefault()` inside the handler to keep it open (e.g. for checkbox items that shouldn't close on toggle).
2192
+
1878
2193
  ## Gotchas
1879
2194
  - Use `asChild` on DropdownMenuTrigger to render your own button element
2195
+ - DropdownMenuShortcut is decorative — bind keyboard shortcuts separately (e.g. with `useHotkeys`)
2196
+ - Sub-menus need BOTH DropdownMenuSubTrigger (visible item) and DropdownMenuSubContent (the submenu panel) — missing either silently breaks the hover-open behavior
1880
2197
 
1881
2198
  ## Changes
1882
2199
  ### v0.22.0
@@ -1924,9 +2241,17 @@ import { DataTableToolbar } from '@devalok/shilp-sutra/ui/data-table-toolbar'
1924
2241
  />
1925
2242
  ```
1926
2243
 
2244
+ ## Composability
2245
+ - **Two modes, one component:** `compact={false}` (default) = drag-and-drop zone with visible affordance; `compact={true}` = inline "Upload" button. Swap by prop, same callback.
2246
+ - **Progress integration:** Drive `uploading` + `progress` from your upload logic. The component renders an embedded Progress bar when uploading=true.
2247
+ - **Validation is built-in:** `accept` (MIME types / extensions) + `maxSize` are enforced before `onFiles` fires. Invalid files produce `error` state instead of calling through. Your onFiles never has to re-validate.
2248
+ - **Multi-file:** `multiple={true}` accepts File[] (array always, even for single-file mode — use `files[0]`).
2249
+ - **Composes with Toast for feedback:** Pair with toast.success on upload complete, toast.error on failure.
2250
+
1927
2251
  ## Gotchas
1928
2252
  - compact=true renders a small inline button; false (default) renders a large drag-and-drop zone
1929
2253
  - Client-side validation: invalid files are rejected before onFiles is called
2254
+ - `onFiles` always receives an array — use `files[0]` even when `multiple={false}`
1930
2255
 
1931
2256
  ## Changes
1932
2257
  ### v0.1.0
@@ -1964,10 +2289,31 @@ import { DataTableToolbar } from '@devalok/shilp-sutra/ui/data-table-toolbar'
1964
2289
  </FormField>
1965
2290
  ```
1966
2291
 
2292
+ ## Composability
2293
+ The FormField/useFormField pair is the central a11y wiring pattern. Components that consume it automatically become accessible inside a FormField without any explicit ARIA work on your end.
2294
+
2295
+ **FormField cascades through context to:**
2296
+ - **Input, Textarea, NumberInput, InputOTP** — auto-receive `aria-describedby` (wired to FormHelperText's id), `aria-invalid` (when state="error"), and `aria-required` (when required=true).
2297
+ - **FormHelperText** — auto-reads `state` and `helperTextId` from context; renders `role="alert"` on error so screen readers interrupt.
2298
+ - **Label** — pair it with an Input via `htmlFor` / `id` in the normal way; FormField doesn't auto-wire labels (labels need the explicit association to satisfy screen readers reliably).
2299
+
2300
+ **Explicit props always override context.** Setting `state="error"` on an Input inside a FormField with `state="helper"` makes only that Input look errored.
2301
+
2302
+ **Nesting is NOT supported.** Don't nest FormField inside FormField — only the outermost context wins, and some a11y wiring silently breaks.
2303
+
2304
+ **Consuming context in your own components:**
2305
+ ```tsx
2306
+ const field = useFormField() // returns { state, helperTextId, required } or undefined
2307
+ // Thread field?.state onto your control's state prop,
2308
+ // and field?.helperTextId onto aria-describedby.
2309
+ ```
2310
+
1967
2311
  ## Gotchas
1968
2312
  - getFormFieldA11y() was REMOVED — use useFormField() hook instead
1969
2313
  - FormHelperText auto-reads state and id from FormField context
1970
2314
  - FormHelperText renders role="alert" when state="error"
2315
+ - Don't nest FormField components — one FormField per field
2316
+ - FormField does NOT auto-wire Label→Input association; use `<Label htmlFor="x" />` + `<Input id="x" />` explicitly
1971
2317
 
1972
2318
  ## Changes
1973
2319
  ### v0.18.0
@@ -2001,8 +2347,17 @@ import { DataTableToolbar } from '@devalok/shilp-sutra/ui/data-table-toolbar'
2001
2347
  </HoverCard>
2002
2348
  ```
2003
2349
 
2350
+ ## Composability
2351
+ - Radix HoverCard — accepts `open`, `onOpenChange`, `defaultOpen`, `openDelay` (ms before show), `closeDelay` (ms before hide).
2352
+ - **Distinction from Tooltip:** HoverCardContent can contain interactive elements (links, buttons, form fields). Tooltip is for inert text labels. If you need a rich hover preview — user card, link preview, product card — use HoverCard.
2353
+ - **Distinction from Popover:** Popover opens on click/focus; HoverCard opens on hover/focus. Use HoverCard when the intent is a passive preview, Popover when the user must explicitly invoke the panel.
2354
+ - **Trigger:** `<HoverCardTrigger asChild>` wraps any element. Works on `<span>`, `<a>`, `<button>` — the trigger doesn't need to be interactive itself (unlike Popover where click matters).
2355
+ - **Accessibility:** Hover-only interactions are invisible on touch. Pair with a focus-visible state or use Popover instead for critical content.
2356
+ - **Portal + z-index:** z-popover (1400), same as DropdownMenu/Popover.
2357
+
2004
2358
  ## Gotchas
2005
2359
  - Overlay component — uses Framer Motion for enter/exit animations (v0.18.0)
2360
+ - Don't nest another HoverCard inside one — both open on the same hover and conflict
2006
2361
 
2007
2362
  ## Changes
2008
2363
  ### v0.18.0
@@ -2046,6 +2401,13 @@ import { DataTableToolbar } from '@devalok/shilp-sutra/ui/data-table-toolbar'
2046
2401
  <Icon icon={IconPlus} state="success" />
2047
2402
  ```
2048
2403
 
2404
+ ## Composability
2405
+ - **IconContext consumption** — Icon auto-reads `size` and `stroke` from the nearest IconProvider. Button, IconButton, IconGroup, Input (startSection/endSection), Badge, NumberInput, and more all wrap their children in IconProvider so nested Icons auto-size. Explicit props always override context.
2406
+ - **Use Tabler icon components** — the `icon` prop expects a ForwardRef SVG component (the shape Tabler React exports). Any icon lib following that shape works, but Tabler is the standard.
2407
+ - **Label for a11y:** Without `label`, Icon is `aria-hidden="true"` (decorative — appropriate inside labeled buttons). With `label`, renders `role="img"` + aria-label + `<title>` for standalone icons.
2408
+ - **State overrides animate:** When both are set, state wins. `state="loading"` renders a bare Spinner; success/error render animated glyphs via Framer Motion.
2409
+ - **Reduced motion:** All animations respect `prefers-reduced-motion` — animate props fall back to static render.
2410
+
2049
2411
  ## Gotchas
2050
2412
  - Without `label`, the icon renders `aria-hidden="true"` (decorative)
2051
2413
  - With `label`, the icon renders `role="img"` with `aria-label` and a `<title>` element
@@ -2088,6 +2450,13 @@ import { DataTableToolbar } from '@devalok/shilp-sutra/ui/data-table-toolbar'
2088
2450
  <IconButton icon={<Icon icon={IconTrash} />} variant="solid" color="error" aria-label="Delete" />
2089
2451
  ```
2090
2452
 
2453
+ ## Composability
2454
+ - **Built on Button internally** — inherits ALL Button features: variant, color, loading, onClickAsync, processing, ButtonGroup context consumption, asChild. Use IconButton when your button is icon-only; use Button for label-plus-icon.
2455
+ - **aria-label is TypeScript-enforced** — the library makes it impossible to forget. This is the key accessibility constraint for icon-only buttons.
2456
+ - **IconProvider cascade:** The `icon` prop expects `<Icon icon={...} />` — Icon auto-sizes based on IconButton's `size` via IconProvider context (no need to pass size to the nested Icon).
2457
+ - **Inside ButtonGroup:** Inherits variant/color/size/weight from ButtonGroup context just like Button does. Useful for icon-only toolbars.
2458
+ - **IconGroup vs ButtonGroup of IconButtons:** IconGroup is for static icon clusters (not interactive, or for decorative displays). ButtonGroup of IconButtons is for interactive toolbars where each icon is a button.
2459
+
2091
2460
  ## Gotchas
2092
2461
  - aria-label is enforced by TypeScript — you MUST provide it
2093
2462
  - Prefer IconButton over Button with size="icon-*" for icon-only buttons
@@ -2131,6 +2500,13 @@ import { IconProvider, useIconContext } from '@devalok/shilp-sutra/ui'
2131
2500
  const { size, stroke } = useIconContext()
2132
2501
  ```
2133
2502
 
2503
+ ## Composability
2504
+ - **Low-level context primitive** — rarely used directly. Most consumers want IconGroup (which wraps IconProvider + layout) or just nest Icons inside Button/Input/Badge (which provide their own IconProvider).
2505
+ - **When to reach for IconProvider manually:** Building a custom container that hosts many Icons (a dashboard tile row, a custom toolbar, a nav rail) and you want consistent icon sizing without repeating props. IconProvider is the escape hatch for that.
2506
+ - **Consumer contract:** `useIconContext()` returns `{}` (empty) when no provider is present — your consumer code MUST handle that (fall back to a default size/stroke). Don't assume a provider exists.
2507
+ - **Memoization:** The provider memoizes its value, so consumers don't re-render unless size or stroke actually change. Safe to use in tight loops / frequently-re-rendering trees.
2508
+ - **Composes down:** IconProvider nests — inner providers override outer ones. A `<Button size="lg"><IconProvider size="xs">...</IconProvider></Button>` overrides the button's icon sizing.
2509
+
2134
2510
  ## Gotchas
2135
2511
  - Used internally by IconGroup and Button to propagate icon sizing to children
2136
2512
  - If no provider is present, `useIconContext()` returns `{}` (empty object) — consumers should fall back to defaults
@@ -2167,6 +2543,15 @@ const { size, stroke } = useIconContext()
2167
2543
  </IconGroup>
2168
2544
  ```
2169
2545
 
2546
+ ## Composability
2547
+ - **IconProvider wrapper + layout** — IconGroup is IconProvider plus a flex container. All child `<Icon>` elements auto-size via the provider. Don't pass size to nested Icons.
2548
+ - **IconGroup vs ButtonGroup-of-IconButtons:**
2549
+ - IconGroup = static icon displays (legend rows, feature lists, decorative clusters). Not interactive.
2550
+ - ButtonGroup of IconButtons = interactive toolbar where each icon is a button.
2551
+ - If you want a formatting toolbar (interactive), use ButtonGroup or ToggleGroup, not IconGroup.
2552
+ - **Toolbar mode:** Set `role="toolbar"` + provide `label` to give the group a semantic role. Only then does `label` become `aria-label` on the container.
2553
+ - **Gap semantics:** tight=2px, default=4px, loose=8px — smaller values than ButtonGroup or Stack because icons are visually dense.
2554
+
2170
2555
  ## Gotchas
2171
2556
  - Wraps children in an IconProvider — all child Icons inherit size/stroke from the group
2172
2557
  - `label` is only applied as `aria-label` when `role="toolbar"` is set
@@ -2209,6 +2594,13 @@ const { size, stroke } = useIconContext()
2209
2594
  <Input endSection=".00" endSectionType="label" startSection={<Icon icon={IconCurrencyDollar} />} placeholder="0" />
2210
2595
  ```
2211
2596
 
2597
+ ## Composability
2598
+ - **FormField auto-consumption:** Inside a `<FormField>`, Input auto-inherits `state`, `aria-describedby` (linked to FormHelperText), `aria-invalid` (when state="error"), `aria-required`. Explicit props on Input override context.
2599
+ - **IconProvider cascade:** Icons in `startSection` / `endSection` are auto-sized via IconProvider per the input's `size` (xs → sm icon, sm → sm icon, md → md icon, lg → md icon). Don't pass explicit size to nested `<Icon>`.
2600
+ - **Container-first architecture (v0.29.0+):** Border, background, focus ring all live on the wrapper div (accessed via `wrapperClassName`). The raw `<input>` inside is transparent. `className` goes to the input element; `wrapperClassName` goes to the wrapper.
2601
+ - **Section types:** Strings in startSection/endSection auto-render as `label` (tinted bg + border separator); React elements auto-render as `icon` (fixed-width centered cell). Override via `startSectionType` / `endSectionType`.
2602
+ - **Label pairing:** Always pair with `<Label htmlFor="x" />` + `<Input id="x" />` or wrap both in a `<label>` — FormField doesn't auto-wire the visual label to the input.
2603
+
2212
2604
  ## Gotchas
2213
2605
  - HTML native "size" attribute is excluded — use CSS width instead
2214
2606
  - state="error" sets aria-invalid automatically
@@ -2256,17 +2648,32 @@ const { size, stroke } = useIconContext()
2256
2648
  - Category: ui
2257
2649
 
2258
2650
  ## Props
2259
- Standard input-otp props (maxLength, pattern, etc.)
2651
+ ### InputOTP
2652
+ maxLength: number (REQUIRED) — total number of slots
2653
+ value: string (controlled value; defaults to empty string)
2654
+ onChange: (value: string) => void
2655
+ onComplete: (value: string) => void — fires when all slots filled
2656
+ pattern: string | RegExp — restrict input (e.g. `REGEXP_ONLY_DIGITS`)
2657
+ state: "default" | "error" (error adds red border; auto-inherits from FormField)
2658
+ size: "sm" | "md" | "lg" (slot dimensions — propagates to InputOTPSlot via context)
2659
+ disabled: boolean
2660
+ containerClassName: string (on the outer group container — separate from inner input's className)
2661
+
2662
+ ### InputOTPSlot
2663
+ index: number (REQUIRED, 0-based) — which position this slot renders
2260
2664
 
2261
2665
  ## Compound Components
2262
- InputOTP (root)
2263
- InputOTPGroup
2264
- InputOTPSlot (index: number, REQUIRED)
2265
- InputOTPSeparator
2666
+ InputOTP (root — maxLength, value, onChange, size propagated via context)
2667
+ InputOTPGroup (visual group of slots)
2668
+ InputOTPSlot (index: number, REQUIRED — reads size from context)
2669
+ InputOTPSeparator (visual separator, e.g. between two groups of 3)
2670
+
2671
+ ## Defaults
2672
+ size="md", state="default"
2266
2673
 
2267
2674
  ## Example
2268
2675
  ```jsx
2269
- <InputOTP maxLength={6}>
2676
+ <InputOTP maxLength={6} onComplete={verifyCode}>
2270
2677
  <InputOTPGroup>
2271
2678
  <InputOTPSlot index={0} />
2272
2679
  <InputOTPSlot index={1} />
@@ -2281,8 +2688,15 @@ const { size, stroke } = useIconContext()
2281
2688
  </InputOTP>
2282
2689
  ```
2283
2690
 
2691
+ ## Composability
2692
+ - Size propagates from InputOTP → InputOTPSlot via `InputOTPSizeContext` — don't set size on individual slots.
2693
+ - Inside `<FormField>`: auto-inherits state, aria-describedby, aria-required from context. Explicit `state="error"` overrides.
2694
+ - Underlying library is `input-otp` (OTPInput) — all standard library props (pattern, inputMode, autoFocus, etc.) pass through.
2695
+
2284
2696
  ## Gotchas
2285
- - Each InputOTPSlot requires an `index` prop (0-based)
2697
+ - Each InputOTPSlot requires an `index` prop (0-based) matching its position
2698
+ - `onComplete` only fires when ALL slots are filled — use `onChange` for per-character reactivity
2699
+ - `containerClassName` (outer visual group) is distinct from `className` (the hidden input element itself)
2286
2700
 
2287
2701
  ## Changes
2288
2702
  ### v0.18.0
@@ -2309,8 +2723,14 @@ const { size, stroke } = useIconContext()
2309
2723
  <Label htmlFor="email" required>Email Address</Label>
2310
2724
  ```
2311
2725
 
2726
+ ## Composability
2727
+ - **Label is NOT auto-wired by FormField** — you must explicitly pair `<Label htmlFor="x" />` with the matching `<Input id="x" />` (or Checkbox/Radio/Switch/Select). Screen-reader label association depends on this.
2728
+ - Radix Label primitive underneath — clicking the Label focuses its associated control, which is why the `htmlFor`/`id` pairing matters.
2729
+ - `required={true}` only renders the red asterisk; it does NOT set `aria-required` on the associated control (that comes from FormField context on the control itself).
2730
+ - Works with any input-like component in the library — pair with Input, Textarea, NumberInput, Checkbox, Radio, Switch, Select, Combobox, Autocomplete.
2731
+
2312
2732
  ## Gotchas
2313
- - Use with FormField for automatic aria wiring
2733
+ - Use with FormField for automatic aria wiring on the control (but the Label-to-control association is always manual)
2314
2734
 
2315
2735
  ## Changes
2316
2736
  ### v0.2.0
@@ -2338,8 +2758,15 @@ const { size, stroke } = useIconContext()
2338
2758
  <Link asChild><NextLink href="/about">About</NextLink></Link>
2339
2759
  ```
2340
2760
 
2761
+ ## Composability
2762
+ - **Framework router integration via `asChild`:** `<Link asChild><NextLink href="/foo">...</NextLink></Link>` — Link's styling transfers to the child (NextLink/react-router Link/any `<a>`-like component) while preserving the child's navigation semantics. Don't use `asChild` for plain anchors — it's unnecessary and adds a layer.
2763
+ - **inline vs block:** Default `inline` (display: inline) so Link composes naturally inside paragraphs and mixed text. Set `inline={false}` for full-width link regions (card wrappers, list items).
2764
+ - No context, no cascade. Safe to use anywhere.
2765
+ - For icon + text links, nest Icon inside — Link doesn't auto-size via IconProvider (unlike Button), so pass an explicit `size` to the Icon.
2766
+
2341
2767
  ## Gotchas
2342
2768
  - Use asChild with framework-specific Link components (e.g. Next.js Link)
2769
+ - Don't nest interactive elements (buttons, form controls) inside a Link — that's invalid HTML and breaks screen-reader navigation
2343
2770
 
2344
2771
  ## Changes
2345
2772
  ### v0.18.0
@@ -2379,8 +2806,17 @@ const { size, stroke } = useIconContext()
2379
2806
  </Menubar>
2380
2807
  ```
2381
2808
 
2809
+ ## Composability
2810
+ - Radix Menubar — shares the item-variant vocabulary with DropdownMenu and ContextMenu (checkbox items, radio groups, sub-menus, labels, separators, shortcuts).
2811
+ - **Key difference from DropdownMenu:** Menubar is the horizontal top-bar-menu pattern (File / Edit / View / Help). Multiple MenubarMenu children sit side-by-side at the root; opening one closes the others, and arrow keys move between them.
2812
+ - **Typical use:** Desktop app-like UIs where the menu is always visible at the top (code editors, design tools). For a single collapsed trigger, use DropdownMenu instead.
2813
+ - **Controlled open:** Pass `value` + `onValueChange` to Menubar root to control which MenubarMenu is open (value = menu's `value` prop or falsy for none).
2814
+ - **Portal + z-index:** z-popover (1400).
2815
+
2382
2816
  ## Gotchas
2383
2817
  - Follows the standard Radix Menubar compound pattern
2818
+ - Don't use Menubar for a single dropdown — use DropdownMenu instead
2819
+ - MenubarShortcut is decorative (same as DropdownMenuShortcut) — bind shortcuts separately
2384
2820
 
2385
2821
  ## Changes
2386
2822
  ### v0.18.0
@@ -2424,8 +2860,17 @@ const { size, stroke } = useIconContext()
2424
2860
  </NavigationMenu>
2425
2861
  ```
2426
2862
 
2863
+ ## Composability
2864
+ - **Radix NavigationMenu** underneath — keyboard model (Tab between items, Arrow for panel navigation, Esc to close) is pre-wired.
2865
+ - **NavigationMenuLink vs NavigationMenuTrigger:** Use Link for simple nav items (no dropdown); use Trigger + Content for items with a panel of sub-links.
2866
+ - **NavigationMenuViewport** is the animated container that holds the active Content panel — it auto-positions below the triggers. NavigationMenuIndicator is the small arrow/caret pointing from trigger to viewport.
2867
+ - **Purpose:** Top-level site navigation (Products, Solutions, Resources) with rich dropdowns containing links grouped by category. Don't use for context menus (use DropdownMenu) or action toolbars (use ButtonGroup).
2868
+ - **Router integration:** Use `<NavigationMenuLink asChild><NextLink href="/...">...</NextLink></NavigationMenuLink>` for framework-specific Link components.
2869
+ - **Portal + z-popover (1400)** — content panels portal to body and stack above Dialog/Sheet.
2870
+
2427
2871
  ## Gotchas
2428
2872
  - Uses Framer Motion for enter/exit animations (v0.18.0)
2873
+ - NavigationMenu is NOT for sidebar nav — use Sidebar; NOT for mobile nav — use Sheet or BottomNavbar
2429
2874
 
2430
2875
  ## Changes
2431
2876
  ### v0.18.0
@@ -2446,6 +2891,8 @@ const { size, stroke } = useIconContext()
2446
2891
  ## Props
2447
2892
  value: number (default: 0)
2448
2893
  onValueChange: (value: number) => void
2894
+ size: "xs" | "sm" | "md" | "lg"
2895
+ state: "default" | "error" | "warning" | "success" (border color)
2449
2896
  min: number
2450
2897
  max: number
2451
2898
  step: number (default: 1)
@@ -2453,6 +2900,8 @@ const { size, stroke } = useIconContext()
2453
2900
 
2454
2901
  ## Defaults
2455
2902
  value: 0
2903
+ size: "md"
2904
+ state: "default"
2456
2905
  step: 1
2457
2906
  min: Number.MIN_SAFE_INTEGER
2458
2907
  max: Number.MAX_SAFE_INTEGER
@@ -2462,6 +2911,12 @@ const { size, stroke } = useIconContext()
2462
2911
  <NumberInput value={qty} onValueChange={setQty} min={1} max={99} />
2463
2912
  ```
2464
2913
 
2914
+ ## Composability
2915
+ - **FormField auto-consumption:** Inside `<FormField>`, inherits `aria-describedby`, `aria-invalid`, `aria-required`. `state` from FormField context drives the border color unless overridden.
2916
+ - **IconProvider cascade:** The `+` and `−` stepper icons auto-size via IconProvider per the input size (xs/sm/md/lg → xs/sm/sm/md icons).
2917
+ - **Controlled only** (design choice): no `defaultValue` — pair `value` + `onValueChange` every time. The min/max bounds disable the stepper buttons automatically when reached.
2918
+ - **Label pairing:** Manual via `<Label htmlFor="x" />` + `<NumberInput id="x" />`.
2919
+
2465
2920
  ## Gotchas
2466
2921
  - Controlled only — buttons won't work without onValueChange
2467
2922
 
@@ -2519,8 +2974,18 @@ const { size, stroke } = useIconContext()
2519
2974
  </PaginationRoot>
2520
2975
  ```
2521
2976
 
2977
+ ## Composability
2978
+ - **Two APIs in one component:**
2979
+ - Low-level compound (PaginationRoot + Content + Item + Link + Previous/Next/Ellipsis) — full control over rendering
2980
+ - High-level `PaginationNav` — pass `totalPages` + `currentPage` + `onPageChange` and it renders the whole thing using `generatePagination`
2981
+ - **Use PaginationNav for 95% of cases.** Reach for the compound API when you need custom page-button rendering (e.g. pagination with input "jump to page" or custom ellipsis handling).
2982
+ - **generatePagination** utility — pass `current`, `total`, `siblingCount` to get the `[1, ..., 5, 6, 7, ..., 20]` structure. Use it standalone if rendering pagination elsewhere (e.g. in a DataTable's footer).
2983
+ - **Router integration:** Each PaginationLink accepts `asChild` — wrap with NextLink/react-router Link for URL-based pagination.
2984
+ - **DataTable already has built-in pagination** — don't add Pagination separately; use DataTable's `pagination` prop.
2985
+
2522
2986
  ## Gotchas
2523
2987
  - Root component is PaginationRoot (NOT Pagination)
2988
+ - PaginationNav is the convenience wrapper — prefer it unless you need custom rendering
2524
2989
 
2525
2990
  ## Changes
2526
2991
  ### v0.1.1
@@ -2549,8 +3014,17 @@ const { size, stroke } = useIconContext()
2549
3014
  </Popover>
2550
3015
  ```
2551
3016
 
3017
+ ## Composability
3018
+ - Built on Radix Popover — accepts `open`, `onOpenChange`, `defaultOpen`, `modal`.
3019
+ - **Trigger:** `<PopoverTrigger asChild>` wraps any focusable element.
3020
+ - **PopoverAnchor:** Optional — decouples the visual anchor from the interactive trigger. Useful when the trigger is small (e.g. an icon button) but you want the popover to position relative to a larger surrounding element.
3021
+ - **Positioning:** PopoverContent accepts `side` (top / right / bottom / left), `align` (start / center / end), `sideOffset`, and `collisionPadding` — all forwarded to Floating UI via Radix.
3022
+ - **Portal rendering:** Content portals to body; z-index is `z-popover` (1400) — above Dialog (`z-dialog`) so nested popovers-in-dialogs work correctly.
3023
+ - **Not for tooltips** — use Tooltip for transient hover-label content; Popover is for interactive content (forms, menus, pickers).
3024
+
2552
3025
  ## Gotchas
2553
3026
  - Uses Framer Motion for enter/exit animations (v0.18.0)
3027
+ - If content needs to be modal (backdrop, focus trap), pass `modal={true}` to the root Popover
2554
3028
 
2555
3029
  ## Changes
2556
3030
  ### v0.18.0
@@ -2583,6 +3057,13 @@ const { size, stroke } = useIconContext()
2583
3057
  <Progress size="sm" /> {/* indeterminate */}
2584
3058
  ```
2585
3059
 
3060
+ ## Composability
3061
+ - **Radix Progress** underneath — `value` (determinate) or absent (indeterminate). Standard ARIA: role=progressbar, aria-valuenow/min/max.
3062
+ - **autoColor semantic signal:** Maps value thresholds to color so consumers don't have to manually compute: 0–59=default, 60–84=warning, 85–100=success, >100=error. Useful for storage meters, budget trackers, goal progress.
3063
+ - **Indeterminate:** Omit `value` to get a continuous animated indeterminate bar. Use for unknown-duration loads.
3064
+ - **Progress vs ProgressRing:** Progress is a linear bar (horizontal). ProgressRing is circular. ProgressRing offers multi-ring stacked variants; Progress doesn't.
3065
+ - **Inside Card/StatCard:** StatCard has a `progress` prop that renders a thin inline version — use that inside StatCards instead of a separate Progress.
3066
+
2586
3067
  ## Gotchas
2587
3068
  - Omit value (or pass undefined) for indeterminate animation
2588
3069
  - `autoColor` overrides `color` when `value` is set — do not pass both unless you want autoColor to win
@@ -2630,6 +3111,13 @@ const { size, stroke } = useIconContext()
2630
3111
  />
2631
3112
  ```
2632
3113
 
3114
+ ## Composability
3115
+ - **SVG-based circular progress** — pure SVG, no chart library dependency.
3116
+ - **MultiProgressRing** — concentric rings for multi-metric visualization (Apple Activity style: Move + Exercise + Stand). Don't stack more than 4-5 rings at a size; the innermost rings become too thin. Size auto-allocates ring stroke; pass `size="lg"` for more room.
3117
+ - **ProgressRing vs Progress:** Use ProgressRing for dashboard tiles, at-a-glance status, multi-metric visualizations. Use Progress for linear "percentage filled" UIs.
3118
+ - **`showValue={true}`** renders the percentage in the center (ProgressRing only). For MultiProgressRing, use external labels since center would conflict with multiple values.
3119
+ - **Label for a11y:** `label` falls back to `"{n}% progress"` so screen readers always get meaningful context.
3120
+
2633
3121
  ## Gotchas
2634
3122
  - Uses Framer Motion for the animated fill — not server-safe
2635
3123
  - Value is clamped to `[0, max]` internally
@@ -2640,9 +3128,26 @@ const { size, stroke } = useIconContext()
2640
3128
  - Server-safe: No
2641
3129
  - Category: ui
2642
3130
 
3131
+ ## Props
3132
+ ### RadioGroup
3133
+ value: string (controlled)
3134
+ onValueChange: (value: string) => void
3135
+ defaultValue: string
3136
+ disabled: boolean (propagates to all items)
3137
+ orientation: "horizontal" | "vertical"
3138
+ name: string (form name for all items)
3139
+
3140
+ ### RadioGroupItem
3141
+ value: string (REQUIRED — what's selected when this item is checked)
3142
+ size: "sm" | "md" | "lg"
3143
+ disabled: boolean (item-level override)
3144
+
2643
3145
  ## Compound Components
2644
- RadioGroup (root, value, onValueChange, defaultValue)
2645
- RadioGroupItem (value: string, REQUIRED)
3146
+ RadioGroup (root value, onValueChange, defaultValue, disabled propagated)
3147
+ RadioGroupItem (value REQUIRED, size/disabled individually overridable)
3148
+
3149
+ ## Defaults
3150
+ RadioGroupItem size="md"
2646
3151
 
2647
3152
  ## Example
2648
3153
  ```jsx
@@ -2658,9 +3163,17 @@ const { size, stroke } = useIconContext()
2658
3163
  </RadioGroup>
2659
3164
  ```
2660
3165
 
3166
+ ## Composability
3167
+ - Radix RadioGroup — keyboard navigation (arrow keys to move between items, space to select) is pre-wired.
3168
+ - **RadioGroup propagates** `disabled` to every RadioGroupItem; items can opt back in with `disabled={false}` for granular control (rare).
3169
+ - **Labels:** RadioGroupItem has no intrinsic label — pair each with `<Label htmlFor="x" />` + `<RadioGroupItem id="x" value="..." />`. Screen readers announce the group label (from FormField or aria-labelledby on RadioGroup) plus each item's label.
3170
+ - **Form libraries:** RadioGroup works with react-hook-form via Controller (onValueChange maps to field.onChange). The `name` prop puts a hidden form input per item for native form serialization.
3171
+ - **FormField integration:** RadioGroup does NOT auto-consume FormField error state. For error visuals, style the RadioGroup surround (e.g. via aria-invalid on a wrapping fieldset) — individual radios don't show red borders the way Inputs do.
3172
+
2661
3173
  ## Gotchas
2662
3174
  - Each RadioGroupItem needs a unique `value` prop
2663
3175
  - Pair each item with a Label for accessibility
3176
+ - RadioGroup does NOT auto-inherit FormField error state — handle error styling at the group level
2664
3177
 
2665
3178
  ## Changes
2666
3179
  ### v0.4.2
@@ -2696,8 +3209,16 @@ const { size, stroke } = useIconContext()
2696
3209
  />
2697
3210
  ```
2698
3211
 
3212
+ ## Composability
3213
+ - Purpose-built variant of Input for search — renders the leading search icon and a contextual clear/loading trailing slot. For anything more custom, use Input with explicit `startSection` / `endSection`.
3214
+ - **`onClear` makes the X button appear** only when `value` is non-empty. Pair them so users can reset.
3215
+ - **`loading={true}` swaps the clear button for a spinner** with `aria-busy="true"` on the input — useful for debounced/async search.
3216
+ - Doesn't auto-consume FormField (no `state` prop) — wrap a regular Input inside FormField for validated search fields.
3217
+ - Keyboard: Escape auto-triggers `onClear` when wired (handled via `type="search"`'s native behavior on most browsers).
3218
+
2699
3219
  ## Gotchas
2700
3220
  - HTML native "size" attribute is excluded — use CSS width instead
3221
+ - Clear button only appears when both `onClear` is provided AND `value` is non-empty
2701
3222
 
2702
3223
  ## Changes
2703
3224
  ### v0.15.0
@@ -2744,9 +3265,17 @@ const { size, stroke } = useIconContext()
2744
3265
  />
2745
3266
  ```
2746
3267
 
3268
+ ## Composability
3269
+ - **Data-driven, not compound** — unlike Tabs/ToggleGroup, SegmentedControl takes an `options` array rather than children. This makes it easier to render from a list but harder to customize per-option styling; use Tabs if you need compound children.
3270
+ - **When to use vs Tabs:** SegmentedControl is for mutually-exclusive VIEW-MODE toggles (List/Grid/Kanban) — short labels, no associated content panel. Tabs is for content switching where each tab has a corresponding TabsContent. Both render `role="tablist"`.
3271
+ - **Option icons** auto-size based on the `size` prop — don't set explicit icon sizes.
3272
+ - Fully controlled — there's no `defaultSelectedId`. Manage state in parent.
3273
+ - Built from scratch (no Radix primitive) — standard HTML buttons with `aria-selected` and roving tabindex.
3274
+
2747
3275
  ## Gotchas
2748
3276
  - Controlled only — selectedId + onSelect are required
2749
3277
  - Uses data-driven API (options prop), not compound children
3278
+ - Use Tabs (not SegmentedControl) when you need associated content panels per option
2750
3279
 
2751
3280
  ## Changes
2752
3281
  ### v0.18.0
@@ -2769,11 +3298,13 @@ const { size, stroke } = useIconContext()
2769
3298
 
2770
3299
  ## Props
2771
3300
  ### SelectTrigger
2772
- size: "sm" | "md" | "lg"
3301
+ variant: "default" | "outline" | "ghost"
3302
+ color: "default" | "error" | "success" | "warning" (sets aria-invalid when error)
3303
+ size: "xs" | "sm" | "md" | "lg"
2773
3304
 
2774
3305
  ## Compound Components
2775
3306
  Select (root — value, onValueChange, defaultValue)
2776
- SelectTrigger (size goes HERE, not on Select root)
3307
+ SelectTrigger (variant/color/size go HERE, not on Select root)
2777
3308
  SelectValue (placeholder)
2778
3309
  SelectContent
2779
3310
  SelectGroup (optional grouping)
@@ -2782,7 +3313,7 @@ const { size, stroke } = useIconContext()
2782
3313
  SelectSeparator
2783
3314
 
2784
3315
  ## Defaults
2785
- SelectTrigger size: "md"
3316
+ SelectTrigger variant="default", color="default", size="md"
2786
3317
 
2787
3318
  ## Example
2788
3319
  ```jsx
@@ -2797,6 +3328,15 @@ const { size, stroke } = useIconContext()
2797
3328
  </Select>
2798
3329
  ```
2799
3330
 
3331
+ ## Composability
3332
+ - **Radix Select** underneath — `value`/`onValueChange`/`defaultValue`/`open`/`onOpenChange` standard state.
3333
+ - **Styling props live on SelectTrigger, not Select root.** The Select root owns value/state; the Trigger owns appearance (variant/color/size). Setting `<Select size="lg">` silently does nothing — TypeScript won't catch it.
3334
+ - **SelectItem requires `value`** — unique within the Select. Labels are the children of SelectItem.
3335
+ - **SelectGroup + SelectLabel:** Organize options into sections with a non-interactive section heading.
3336
+ - **FormField integration:** Set `color="error"` on SelectTrigger for error visuals. Not auto-consumed from FormField (same as Checkbox/Radio — form-library convention for select controls).
3337
+ - **Portal + z-popover (1400):** content portals to body, stacks above Dialog/Sheet/other overlays.
3338
+ - **Select vs Combobox vs Autocomplete:** Select = short fixed list, click-to-open, no typeahead. Combobox = searchable, forced selection. Autocomplete = searchable, free text. Pick by list size and search need.
3339
+
2800
3340
  ## Gotchas
2801
3341
  - Size goes on SelectTrigger, NOT on Select root
2802
3342
  - `<Select size="lg">` is silently ignored (no TypeScript error)
@@ -2834,8 +3374,15 @@ const { size, stroke } = useIconContext()
2834
3374
  <Separator orientation="vertical" className="h-6" />
2835
3375
  ```
2836
3376
 
3377
+ ## Composability
3378
+ - Radix Separator — no context, no cascade. Drop it anywhere; it inherits its stretch dimension from its parent container (full width for horizontal, full height for vertical — but vertical needs an explicit height from the parent flexbox).
3379
+ - `decorative={true}` (default) sets `role="none"` — screen readers skip it. Set `decorative={false}` for semantic separators (e.g. between navigation sections) so screen readers announce the boundary.
3380
+ - Gradient variants compose cleanly with any surface color — they use `bg-transparent` + inline linear-gradient, so the underlying `bg-*` of the parent shows through.
3381
+ - Common inside Menu/DropdownMenu/Sheet components; their internal *Separator subcomponents already wrap this one.
3382
+
2837
3383
  ## Gotchas
2838
3384
  - When decorative is true, the separator is hidden from screen readers
3385
+ - Vertical separator needs an explicit height from the parent flex container (`h-6`, `h-full`, etc.)
2839
3386
 
2840
3387
  ## Changes
2841
3388
  ### v0.22.0
@@ -2880,8 +3427,18 @@ const { size, stroke } = useIconContext()
2880
3427
  </Sheet>
2881
3428
  ```
2882
3429
 
3430
+ ## Defaults
3431
+ side="right"
3432
+
3433
+ ## Composability
3434
+ - Same primitives as Dialog — open/onOpenChange/defaultOpen, trigger asChild, portal rendering, focus trap.
3435
+ - `side` controls both the enter-from edge and the layout: top/bottom sides full-width, left/right sides ~75% of viewport (capped at sm breakpoint).
3436
+ - Use Sheet for side-anchored drawers (settings panels, mobile navigation); use Dialog for centered modals.
3437
+ - On mobile (`isMobile` from use-mobile), consider Sheet as the mobile-friendly equivalent of a Dialog/Popover — more thumb-reachable on tall screens.
3438
+
2883
3439
  ## Gotchas
2884
3440
  - Uses Framer Motion for slide enter/exit animations (v0.18.0)
3441
+ - SheetTitle is required for accessibility (same rule as Dialog)
2885
3442
 
2886
3443
  ## Changes
2887
3444
  ### v0.18.0
@@ -2899,6 +3456,14 @@ const { size, stroke } = useIconContext()
2899
3456
  - Server-safe: No
2900
3457
  - Category: ui
2901
3458
 
3459
+ ## Props
3460
+ ### SidebarMenuButton
3461
+ variant: "default" | "outline"
3462
+ size: "sm" | "md" | "lg"
3463
+ isActive: boolean (highlights as current nav item)
3464
+ tooltip: ReactNode | string (shown when sidebar is collapsed)
3465
+ asChild: boolean (render as Slot — common with next/link)
3466
+
2902
3467
  ## Compound Components
2903
3468
  SidebarProvider (context provider — must wrap everything)
2904
3469
  Sidebar (root panel)
@@ -2927,6 +3492,9 @@ const { size, stroke } = useIconContext()
2927
3492
  ## Hook
2928
3493
  useSidebar() => { state, open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar }
2929
3494
 
3495
+ ## Defaults
3496
+ SidebarMenuButton variant="default", size="md"
3497
+
2930
3498
  ## Example
2931
3499
  ```jsx
2932
3500
  <SidebarProvider>
@@ -2954,9 +3522,28 @@ const { size, stroke } = useIconContext()
2954
3522
  </SidebarProvider>
2955
3523
  ```
2956
3524
 
3525
+ ## Composability
3526
+ The whole Sidebar ecosystem is driven by `SidebarContext` — the Provider owns the expand/collapse state and every descendant reads from it.
3527
+
3528
+ **Who reads the context:**
3529
+ - `Sidebar` — applies collapsed / expanded / off-canvas styles based on `state`
3530
+ - `SidebarTrigger` — calls `toggleSidebar()` on click; reflects aria-expanded
3531
+ - `SidebarMenuButton` — uses `state === 'collapsed'` to show its `tooltip` (hidden label appears only when sidebar is icon-width)
3532
+ - `SidebarInset` — main content area adjusts its left offset based on sidebar width + state
3533
+ - `SidebarRail` — invisible hit target on the outer edge for click-to-toggle
3534
+ - Any user component that calls `useSidebar()` — full access to open/isMobile/toggle
3535
+
3536
+ **Desktop vs mobile:** Context tracks `isMobile` via the `use-mobile` hook and branches behavior: on desktop the sidebar collapses to an icon rail; on mobile it becomes an off-canvas drawer controlled by `openMobile`.
3537
+
3538
+ **Controlled or uncontrolled:** `SidebarProvider` accepts `open` + `onOpenChange` (controlled) or `defaultOpen` (uncontrolled). State is synced to a cookie for cross-route persistence.
3539
+
3540
+ **Next.js / router integration:** `SidebarMenuButton` with `asChild` wraps any link element (`next/link`, `react-router Link`, plain `<a>`) — it transfers its styling + isActive state to the child while preserving the link's navigation semantics.
3541
+
2957
3542
  ## Gotchas
2958
3543
  - SidebarProvider MUST wrap both Sidebar and SidebarInset
2959
3544
  - Use SidebarMenuButton for nav items (supports tooltip in collapsed state)
3545
+ - `tooltip` is only visible when sidebar is collapsed — providing one doesn't duplicate the visible label
3546
+ - Cookie-based state persistence means the sidebar defaults to its prior state on page reload — use `defaultOpen` to override if needed
2960
3547
 
2961
3548
  ## Changes
2962
3549
  ### v0.18.0
@@ -3034,6 +3621,13 @@ const { size, stroke } = useIconContext()
3034
3621
  <SkeletonImage height="h-64" />
3035
3622
  ```
3036
3623
 
3624
+ ## Composability
3625
+ - Server-safe — can render in RSC trees during server-side loading states.
3626
+ - **Pattern:** One SkeletonGroup per load region (wraps with `role="status"` + `aria-busy="true"` + announces the `label`). Inside, compose the individual shape skeletons (Avatar, Text, Button, Chart, Image) to mirror the structure that will appear when loaded.
3627
+ - Use LoadingSkeleton (composed) instead for pre-built layouts — CardSkeleton, TableSkeleton, ListSkeleton, BoardSkeleton — or PageSkeletons for full-page placeholders.
3628
+ - `animation="none"` disables animation entirely — useful when placed inside components that already have their own load animation.
3629
+ - No context cascade — each skeleton is independent. If you want to drive multiple skeletons from one "reduced motion" setting, either set `animation="none"` on each or rely on the `useReducedMotion` hook (shimmer already respects it automatically).
3630
+
3037
3631
  ## Gotchas
3038
3632
  - shimmer respects prefers-reduced-motion
3039
3633
  - SkeletonGroup adds role="status" and aria-busy="true" — wrap multiple skeletons for a11y
@@ -3049,7 +3643,12 @@ const { size, stroke } = useIconContext()
3049
3643
  - Category: ui
3050
3644
 
3051
3645
  ## Props
3052
- Standard Radix Slider props (value, onValueChange, defaultValue, min, max, step, aria-label)
3646
+ size: "sm" | "md" | "lg" (track height and thumb dimensions)
3647
+ color: "accent" | "success" | "warning" | "error" (thumb border + focus ring color)
3648
+ (plus standard Radix Slider props: value, onValueChange, defaultValue, min, max, step, aria-label)
3649
+
3650
+ ## Defaults
3651
+ size="md", color="accent"
3053
3652
 
3054
3653
  ## Example
3055
3654
  ```jsx
@@ -3057,9 +3656,18 @@ const { size, stroke } = useIconContext()
3057
3656
  <Slider defaultValue={[25, 75]} max={100} step={1} /> {/* range slider */}
3058
3657
  ```
3059
3658
 
3659
+ ## Composability
3660
+ - Radix Slider primitive — keyboard navigation (arrow keys, Home/End, PageUp/PageDown) pre-wired.
3661
+ - **Value is always an array** — single-thumb: `[50]`; range: `[25, 75]`. Don't pass a plain number.
3662
+ - **Multi-thumb range:** Pass `[start, end]` — renders two thumbs that can cross each other by default. Use `minStepsBetweenThumbs` to enforce a gap.
3663
+ - **FormField:** Slider does NOT auto-consume FormField state. No validation UX — sliders usually don't need it (values are always valid by construction).
3664
+ - **No label pairing via Label** — use `aria-label` or `aria-labelledby` directly on the Slider. The thumb is the focusable/labeled element.
3665
+ - Not portal-rendered — inline; overflow rules of parents apply.
3666
+
3060
3667
  ## Gotchas
3061
3668
  - value is number[] (array), not a single number
3062
3669
  - Multi-thumb: Pass array `defaultValue={[25, 75]}` for range sliders — renders one thumb per value
3670
+ - Slider does NOT auto-consume FormField — sliders don't have validation state visuals by design
3063
3671
 
3064
3672
  ## Changes
3065
3673
  ### v0.18.0
@@ -3097,6 +3705,13 @@ const { size, stroke } = useIconContext()
3097
3705
  <Spinner variant="bare" /> {/* uses currentColor, for embedding in buttons */}
3098
3706
  ```
3099
3707
 
3708
+ ## Composability
3709
+ - **Prefer Button's `loading` prop** for inline button spinners — Button's integration is automatic (disables, sets aria-busy, positions the spinner). Manual Spinner inside Button is more work for no gain.
3710
+ - **`variant="bare"`** uses `currentColor` — drop it into any surface (button, toolbar, badge, icon group) and it picks up the parent's text color automatically.
3711
+ - **State transitions:** `spinning → success → idle` / `spinning → error → idle`. Use the state machine for async operation feedback (the success checkmark, error X animate in). Pair with `onComplete` to trigger next-step logic after the transition.
3712
+ - **delay for flicker prevention:** Set `delay={150}` so very fast operations don't flash a spinner. The spinner mounts only after the delay elapses — if the operation completes first, the user never sees it.
3713
+ - **Icon.state="loading" uses Spinner internally** — same underlying component. If you're inside an Icon context, prefer `<Icon state="loading" />` for consistent sizing.
3714
+
3100
3715
  ## Gotchas
3101
3716
  - Renders role="status" with sr-only "Loading..." text — no need for aria-label
3102
3717
  - Button has built-in loading prop — prefer that over manual Spinner composition
@@ -3162,6 +3777,17 @@ import { SplitButton } from '@devalok/shilp-sutra'
3162
3777
  | dropdownLabel | string | 'More options' | aria-label for trigger |
3163
3778
  | dropdownIcon | ReactNode | chevron-down | Custom trigger icon |
3164
3779
 
3780
+ ## Composability
3781
+ - **Two-in-one button:** visually unified `[Action | ▼]` with the left half being the primary click and the right half opening a dropdown. Use for actions that have a most-common choice plus alternatives (Save vs. Save-As-Draft, Send vs. Schedule).
3782
+ - **Built on Button + Popover internally** — inherits Button's variant/color/size vocabulary and Popover's placement prop (`top-end` default works for most top-of-page toolbars).
3783
+ - **Dropdown content is consumer-provided** — pass any JSX via `dropdownContent` (typically a DropdownMenu, list of actions, a custom panel, or a small form). Don't try to shove a full-featured menu into the chevron; keep it focused on 2–5 alternatives.
3784
+ - **ButtonGroup compatibility:** Put SplitButton inside a `<ButtonGroup>` — it inherits variant/color/size from the group context just like Button does. Position-aware corners work too.
3785
+ - **Controlled dropdown:** Pass `open` + `onOpenChange` for controlled state; omit for uncontrolled. Useful when the dropdown must close programmatically after a selection.
3786
+
3787
+ ## Gotchas
3788
+ - Always provide `dropdownLabel` (aria-label on the chevron trigger) — defaults to "More options" but context-specific labels are better
3789
+ - `triggerSide="left"` flips the chevron to the left — rare, but useful for RTL layouts or when the primary action is the secondary emphasis
3790
+
3165
3791
  ## Changes
3166
3792
 
3167
3793
  ### v0.33.0
@@ -3191,6 +3817,14 @@ import { SplitButton } from '@devalok/shilp-sutra'
3191
3817
  </Stack>
3192
3818
  ```
3193
3819
 
3820
+ ## Composability
3821
+ - **Server-safe layout primitive.** Nothing to hydrate — works anywhere.
3822
+ - **Pattern:** Stack wraps child elements in a flex container. Swap between vertical/horizontal without rewriting the child layout.
3823
+ - **Polymorphic via `as`:** Use `as="ul"` / `as="ol"` for list semantics with flex styling; `as="section"` for major page divisions; etc.
3824
+ - **gap is design-system tokenized** — use token strings (`"ds-04"`) or numeric shortcuts (`4` → `"ds-04"`). Raw numbers map 1:1 to the `ds-0N` token set.
3825
+ - **Responsive direction:** Not built in — for responsive direction changes use Tailwind's `flex-col md:flex-row` directly on a plain div, or wrap multiple Stacks with display-toggling.
3826
+ - Container vs Stack: Container centers + caps width; Stack arranges children. Compose them: `<Container><Stack>...</Stack></Container>`.
3827
+
3194
3828
  ## Gotchas
3195
3829
  - "row" and "column" are aliases for "horizontal" and "vertical"
3196
3830
  - gap accepts both token strings and numeric values
@@ -3254,6 +3888,15 @@ import { SplitButton } from '@devalok/shilp-sutra'
3254
3888
  />
3255
3889
  ```
3256
3890
 
3891
+ ## Composability
3892
+ - **High-density metric card** — optimized for dashboards. Everything optional except `value`. Mix and match features (delta, sparkline, progress, secondary label, footer) per metric's needs.
3893
+ - **Router integration via href:** Internally uses `LinkContext` to resolve framework-specific Link components (Next.js, react-router). Set `href` in a LinkProvider-wrapped tree to get seamless client-side navigation without custom asChild wiring.
3894
+ - **Interactive modes:** `onClick` makes the entire card a button; `href` makes it a link. Mutually exclusive — href wins if both are set.
3895
+ - **Accent bar semantic:** Use `accent` to signal metric health at a glance (success for positive, warning for at-risk, error for over-target). Combine with delta.direction for layered emphasis.
3896
+ - **Sparkline:** Pure SVG, lightweight — no chart library. For rich charts use Chart components. Minimum 2 data points.
3897
+ - **Icon auto-sizing:** Accepts `ComponentType<{ className }>` OR `ReactNode`. The component prop (e.g. `icon={IconBolt}`) is preferred — icon is rendered at a consistent size.
3898
+ - **Loading state:** `loading={true}` renders the full card skeleton — use during initial data fetch.
3899
+
3257
3900
  ## Gotchas
3258
3901
  - delta.direction "up" = green, "down" = red, "neutral" = grey
3259
3902
  - `label` and `title` are aliases — use either, not both
@@ -3289,6 +3932,15 @@ import { SplitButton } from '@devalok/shilp-sutra'
3289
3932
  <StatusDot status="warning" size="lg" label="Elevated load" />
3290
3933
  ```
3291
3934
 
3935
+ ## Composability
3936
+ - **Server-safe presentational dot** — simple status indicator, pairs with inline text.
3937
+ - **Status semantics map to color:** healthy=success, warning=warning, critical=error, neutral=muted surface, inactive=dimmed. The pulse animation calls attention to "healthy" by default (active presence); you can flip it for other statuses by setting pulse explicitly.
3938
+ - **StatusDot vs BadgeIndicator vs StatusBadge:**
3939
+ - StatusDot = tiny presence/status indicator with optional inline label (e.g. "Service online")
3940
+ - BadgeIndicator = overlay on another element (notification dot on a bell icon)
3941
+ - StatusBadge (composed) = full pill-style badge with colored bg and label text (e.g. ticket status)
3942
+ - **Label composability:** If you pass `label`, it renders inline after the dot. Use `labelClassName` for custom typography (e.g. font-mono for status codes).
3943
+
3292
3944
  ## Gotchas
3293
3945
  - The `pulse` prop auto-enables for "healthy" status — pass `pulse={false}` to suppress
3294
3946
  - Status type is exported as `StatusDotStatus` if you need it in consumer code
@@ -3331,8 +3983,16 @@ import { SplitButton } from '@devalok/shilp-sutra'
3331
3983
  </Stepper>
3332
3984
  ```
3333
3985
 
3986
+ ## Composability
3987
+ - `Stepper` passes `activeStep` + `orientation` + each step's index down to every `Step` child via `StepperContext`. Each Step derives its own status (completed / active / pending) from its position vs `activeStep`.
3988
+ - Step index is assigned by position in children — order matters. Don't conditionally render Steps via `&&` or `.filter()`; it shifts the indices and breaks the active highlight.
3989
+ - `StepperContent` is separate from `Stepper` — it's the animated panel surface that shows one child per step index. Pass the same `activeStep` value to both.
3990
+ - `orientation="vertical"` swaps the Step layout (stacked with connecting line on the left) AND changes how StepperContent animates (vertical crossfade instead of horizontal slide).
3991
+ - Custom icons via `Step.icon` override the default number/checkmark — the icon slot still receives the status-based styling (muted for pending, accent for active/completed).
3992
+
3334
3993
  ## Gotchas
3335
3994
  - Steps before activeStep are "completed", at activeStep is "active", after is "pending"
3995
+ - Don't conditionally render Step children — position is the index contract
3336
3996
 
3337
3997
  ## Changes
3338
3998
  ### v0.18.0
@@ -3365,9 +4025,17 @@ import { SplitButton } from '@devalok/shilp-sutra'
3365
4025
  <Switch size="lg" color="success" thumbIcon={<IconCheck size={14} />} />
3366
4026
  ```
3367
4027
 
4028
+ ## Composability
4029
+ - Radix Switch primitive — `checked` / `onCheckedChange` / `defaultChecked` standard control model.
4030
+ - **FormField:** Switch does NOT auto-consume FormField state (same as Checkbox/Radio — form-library convention for toggles). Pass `error` explicitly when needed.
4031
+ - **thumbIcon slot:** Any ReactNode renders inside the thumb circle — commonly used for check/X glyphs that animate with the thumb position.
4032
+ - **Label pairing:** Manual — pair with `<Label htmlFor="x" />` + `<Switch id="x" />`, or wrap both in a `<label>` for click-to-toggle.
4033
+ - Pair with FormHelperText (outside FormField wiring) for custom validation messages.
4034
+
3368
4035
  ## Gotchas
3369
4036
  - Use error prop for validation states (matches Checkbox API)
3370
4037
  - `error` overrides `color` — when error is true, checked state always uses error-9
4038
+ - Switch does NOT auto-consume FormField — pass `error` explicitly inside a FormField
3371
4039
 
3372
4040
  ## Changes
3373
4041
  ### v0.29.0
@@ -3423,8 +4091,16 @@ import { SplitButton } from '@devalok/shilp-sutra'
3423
4091
  </Table>
3424
4092
  ```
3425
4093
 
4094
+ ## Composability
4095
+ - **Server-safe pure HTML wrappers** — Table and sub-components are thin semantic wrappers around `<table>`, `<thead>`, `<tbody>`, etc. No state, no context. Safe in RSC trees.
4096
+ - **Table vs DataTable:** Table is presentational — you control every row, cell, header. DataTable (from `ui/data-table`) is feature-rich — sorting, filtering, pagination, selection, virtualization built in. Pick by whether you need that machinery.
4097
+ - **Use cases for bare Table:** Static data displays, marketing comparison tables, documentation tables, small lists where DataTable would be overkill.
4098
+ - **Composes with UI primitives inside cells:** Badge for status pills, Avatar for user cells, IconButton for row actions, StatusDot for state indicators. All server-safe if the table is server-rendered.
4099
+ - **TableCaption** renders as HTML `<caption>` — useful for a summary description that screen readers announce before the table content.
4100
+
3426
4101
  ## Gotchas
3427
4102
  - Table headers automatically have scope="col" for screen reader navigation
4103
+ - For anything beyond trivial display, prefer DataTable — don't rebuild sorting/pagination/selection on top of bare Table
3428
4104
 
3429
4105
  ## Changes
3430
4106
  ### v0.18.0
@@ -3449,6 +4125,8 @@ import { SplitButton } from '@devalok/shilp-sutra'
3449
4125
 
3450
4126
  ### TabsList
3451
4127
  variant: "line" | "contained"
4128
+ size: "sm" | "md" | "lg"
4129
+ orientation: "horizontal" | "vertical"
3452
4130
 
3453
4131
  ### TabsTrigger
3454
4132
  value: string (REQUIRED)
@@ -3464,7 +4142,7 @@ import { SplitButton } from '@devalok/shilp-sutra'
3464
4142
  TabsContent (value)
3465
4143
 
3466
4144
  ## Defaults
3467
- TabsList variant: "line"
4145
+ TabsList variant="line", size="md", orientation="horizontal"
3468
4146
 
3469
4147
  ## Example
3470
4148
  ```jsx
@@ -3478,8 +4156,14 @@ import { SplitButton } from '@devalok/shilp-sutra'
3478
4156
  </Tabs>
3479
4157
  ```
3480
4158
 
4159
+ ## Composability
4160
+ - TabsList propagates `variant`, `size`, and `orientation` to every TabsTrigger child via `TabsListContext`. TabsTrigger reads all three from context; explicit props on a trigger override the inherited value.
4161
+ - `orientation="vertical"` on TabsList changes the TabsList layout (flex-col + left border) AND the `roving-tabindex` keyboard behavior (ArrowUp/Down navigate, not ArrowLeft/Right).
4162
+ - Tabs (root) is just the Radix Tabs.Root — state props (value, defaultValue, onValueChange) live there; styling props live on TabsList.
4163
+ - Tabs content is rendered inline (not portalled) — container-scoped queries work fine in tests.
4164
+
3481
4165
  ## Gotchas
3482
- - variant goes on TabsList, NOT on individual TabsTrigger (propagates via context)
4166
+ - variant/size/orientation go on TabsList, NOT on Tabs root or individual TabsTrigger
3483
4167
  - Normally omit `variant` on TabsTrigger — it inherits from TabsList via context. You CAN set it per-trigger to override.
3484
4168
 
3485
4169
  ## Changes
@@ -3502,12 +4186,9 @@ import { SplitButton } from '@devalok/shilp-sutra'
3502
4186
  - Category: ui
3503
4187
 
3504
4188
  ## Props
3505
- variant: TextVariant
4189
+ variant: "heading-2xl" | "heading-xl" | "heading-lg" | "heading-md" | "heading-sm" | "heading-xs" | "body-lg" | "body-md" | "body-sm" | "body-xs" | "label-lg" | "label-md" | "label-sm" | "label-xs" | "label-plain-lg" | "label-plain-md" | "label-plain-sm" | "caption" | "overline" | "code"
3506
4190
  as: ElementType (override the auto-selected HTML element)
3507
4191
 
3508
- ## Types
3509
- TextVariant = 'heading-2xl' | 'heading-xl' | 'heading-lg' | 'heading-md' | 'heading-sm' | 'heading-xs' | 'body-lg' | 'body-md' | 'body-sm' | 'body-xs' | 'label-lg' | 'label-md' | 'label-sm' | 'label-xs' | 'caption' | 'overline'
3510
-
3511
4192
  ## Defaults
3512
4193
  variant: "body-md"
3513
4194
 
@@ -3522,9 +4203,17 @@ import { SplitButton } from '@devalok/shilp-sutra'
3522
4203
  <Text variant="label-sm" className="text-text-secondary">SECTION LABEL</Text>
3523
4204
  ```
3524
4205
 
4206
+ ## Composability
4207
+ - **Server-safe** (one of few components in the library that is). Can render in RSC trees without `"use client"`.
4208
+ - No context consumption, no context cascade — pure typography primitive.
4209
+ - **Semantic HTML by default:** Each variant maps to a meaningful HTML element (h1 through p, span, code). The `as` prop overrides for visual-only demotion: e.g. `<Text variant="heading-xl" as="div">` renders h2-sized text inside a div, useful when the element already has a heading ancestor but you want the visual weight.
4210
+ - **Underpins many components:** Card's CardTitle, Alert's title, PageHeader, EmptyState, SectionHeader all render Text internally with specific variants. Don't wrap another Text inside them — variants cascade structurally, not via context.
4211
+ - Pairs with Code for inline code spans inside body text: `<Text>Call <Code>onClick</Code> to...</Text>`.
4212
+
3525
4213
  ## Gotchas
3526
4214
  - label-* and overline variants are automatically uppercase
3527
4215
  - Use "as" prop to override the HTML element when needed
4216
+ - Don't use Text inside headings that already have semantic meaning (e.g. CardTitle) — the double-element wraps are redundant and break screen-reader heading navigation
3528
4217
 
3529
4218
  ## Changes
3530
4219
  ### v0.2.0
@@ -3539,7 +4228,7 @@ import { SplitButton } from '@devalok/shilp-sutra'
3539
4228
  - Category: ui
3540
4229
 
3541
4230
  ## Props
3542
- size: "sm" | "md" | "lg"
4231
+ size: "xs" | "sm" | "md" | "lg"
3543
4232
  state: "default" | "error" | "warning" | "success"
3544
4233
  (plus standard textarea attributes except native "size")
3545
4234
 
@@ -3551,6 +4240,12 @@ import { SplitButton } from '@devalok/shilp-sutra'
3551
4240
  <Textarea size="lg" state="error" placeholder="Describe the issue..." />
3552
4241
  ```
3553
4242
 
4243
+ ## Composability
4244
+ - **FormField auto-consumption:** Same pattern as Input — inside `<FormField>`, Textarea inherits `state`, `aria-describedby`, `aria-invalid`, `aria-required`. Explicit props override.
4245
+ - **Resize:** Vertically resizable by default (`resize-y`); override with `className="resize-none"` if needed.
4246
+ - **Label pairing:** Manual — `<Label htmlFor="x" />` + `<Textarea id="x" />`. FormField doesn't auto-wire.
4247
+ - No IconProvider cascade (no icon slots — different from Input).
4248
+
3554
4249
  ## Gotchas
3555
4250
  - state="error" sets aria-invalid automatically; all sizes are vertically resizable
3556
4251
  - Inside FormField: auto-inherits state, aria-describedby, aria-required from context (explicit props override)
@@ -3614,6 +4309,15 @@ toast.error('Upload failed', { description: 'File too large' })
3614
4309
  toast.undo('Task deleted', { onUndo: () => restoreTask(id) })
3615
4310
  ```
3616
4311
 
4312
+ ## Composability
4313
+ - **Imperative API only** — `toast.success(...)`, `toast.error(...)`, etc. NO JSX invocation. This is by design (Sonner-based) — you call from event handlers, not render.
4314
+ - **Requires Toaster mounted once** at app root. Without it, `toast.*` calls are no-ops. Render `<Toaster />` in the root layout, not inside route components.
4315
+ - **toast.promise** orchestrates async flows: `toast.promise(fetch(...), { loading: 'Saving', success: 'Saved', error: 'Failed' })`. One toast, three states. Beats manually calling `.loading()` + `.success()`/`.error()`.
4316
+ - **toast.undo** adds an inline Undo button with 8s default duration — pair with state management that supports reversal (soft-delete with restore, last-action redo).
4317
+ - **toast.upload** is specifically for file-upload progress — per-file progress bars, retry on error, remove from list. Replaces the old `UploadProgress` composed component.
4318
+ - **toast.custom** is the escape hatch — render arbitrary JSX. Use sparingly; prefer the typed methods for consistency.
4319
+ - **Distinction from Alert/Banner:** Toast = transient, auto-dismissing, non-interactive-dismissible floating notification. Alert = inline in-flow announcement. Banner = page-level strip. Pick by persistence + position.
4320
+
3617
4321
  ## Gotchas
3618
4322
  - DO NOT use useToast() hook — it is deprecated, use imperative toast.* methods
3619
4323
  - DO NOT use toast({ title, color }) object syntax — use toast.success('message') etc.
@@ -3671,9 +4375,17 @@ export default function RootLayout({ children }) {
3671
4375
  }
3672
4376
  ```
3673
4377
 
4378
+ ## Composability
4379
+ - **Mount once, use everywhere.** Render `<Toaster />` in the root layout file once. Every `toast.*` call anywhere in the tree routes to this single container.
4380
+ - **Global config lives here.** Set `position`, `duration`, `visibleToasts` on Toaster to control default behavior for all toasts. Individual `toast.*` calls can override `duration` per toast.
4381
+ - **Keyboard shortcut:** `hotkey` prop sets a global shortcut to focus the toast region (default: Alt+T). Useful for keyboard users to review recent notifications.
4382
+ - **Next.js / App Router:** Mount inside your root `app/layout.tsx`'s `<body>`. It's marked `'use client'` so it doesn't render on the server (Toaster is client-only).
4383
+ - **Portal rendering:** z-toast (highest layer) — Toaster content appears above Dialog, Popover, everything. Don't wrap it in a stacking context.
4384
+
3674
4385
  ## Gotchas
3675
4386
  - Must be mounted once at the layout root for toast notifications to work
3676
4387
  - Use the `toast` import from `@devalok/shilp-sutra/ui/toast` to trigger toasts
4388
+ - NOT server-safe — mounts with 'use client'; rendering during SSR has no visual effect
3677
4389
 
3678
4390
  ## Changes
3679
4391
  ### v0.18.0
@@ -3692,13 +4404,13 @@ export default function RootLayout({ children }) {
3692
4404
  ## Props
3693
4405
  variant: "default" | "outline"
3694
4406
  size: "sm" | "md" | "lg"
4407
+ color: "accent" | "error" | "success" | "neutral" (pressed-state bg + text color)
3695
4408
  pressed: boolean
3696
4409
  onPressedChange: (pressed: boolean) => void
3697
4410
  defaultPressed: boolean
3698
4411
 
3699
4412
  ## Defaults
3700
- variant: "default"
3701
- size: "md"
4413
+ variant="default", size="md", color="accent"
3702
4414
 
3703
4415
  ## Example
3704
4416
  ```jsx
@@ -3707,8 +4419,16 @@ export default function RootLayout({ children }) {
3707
4419
  </Toggle>
3708
4420
  ```
3709
4421
 
4422
+ ## Composability
4423
+ - Radix Toggle primitive — `pressed` / `onPressedChange` / `defaultPressed`. Not the same as Switch (boolean value doesn't map to on/off semantically — Toggle is "this action is currently active").
4424
+ - **Pairs with ToggleGroup** for mutually-exclusive or multi-select toggle clusters — ToggleGroupItem inherits variant + size from ToggleGroup via context. Don't set variant on a ToggleGroupItem directly.
4425
+ - **No context consumption as a standalone** — when rendered outside a ToggleGroup, Toggle is fully independent.
4426
+ - **Icon content:** Commonly wraps a single `<Icon>` for formatting toolbar toggles (Bold, Italic, AlignLeft). Pair with IconGroup for a horizontal cluster of independent toggles (vs. ToggleGroup for related state).
4427
+ - **aria-label is required** — icon-only toggles need an accessible name.
4428
+
3710
4429
  ## Gotchas
3711
4430
  - Always provide aria-label for accessibility
4431
+ - Toggle is not Switch — Toggle means "this action is active right now"; Switch means "this setting is on"
3712
4432
 
3713
4433
  ## Changes
3714
4434
  ### v0.18.0
@@ -3738,8 +4458,11 @@ export default function RootLayout({ children }) {
3738
4458
  value: string
3739
4459
 
3740
4460
  ## Compound Components
3741
- ToggleGroup (root)
3742
- ToggleGroupItem (value: string)
4461
+ ToggleGroup (root — variant, size propagated to items via context)
4462
+ ToggleGroupItem (value: string — reads variant/size from context)
4463
+
4464
+ ## Defaults
4465
+ variant="default", size="md"
3743
4466
 
3744
4467
  ## Example
3745
4468
  ```jsx
@@ -3750,9 +4473,15 @@ export default function RootLayout({ children }) {
3750
4473
  </ToggleGroup>
3751
4474
  ```
3752
4475
 
4476
+ ## Composability
4477
+ - ToggleGroup passes `variant` and `size` to every ToggleGroupItem via `ToggleGroupContext`. Items read both from context; explicit props on a child override.
4478
+ - `type="single"` enforces one-selected-at-a-time (value: string); `type="multiple"` allows many (value: string[]). Radix Toggle Group underpins this — it's the same role + keyboard model as Radix.
4479
+ - Built on top of the plain `Toggle` component's CVA — the `variant` and `size` axes of ToggleGroupItem match Toggle exactly (`default | outline`, `sm | md | lg`).
4480
+
3753
4481
  ## Gotchas
3754
- - type is required — "single" or "multiple"
3755
- - variant and size propagate from ToggleGroup to items
4482
+ - `type` is required — "single" or "multiple" — and drives the value shape
4483
+ - variant and size propagate from ToggleGroup to items; setting them on a ToggleGroupItem overrides that item only
4484
+ - Unlike Tabs (where TabsList is the styling surface), ToggleGroup itself is the styling surface — there's no intermediate List component
3756
4485
 
3757
4486
  ## Changes
3758
4487
  ### v0.18.0
@@ -3785,8 +4514,18 @@ export default function RootLayout({ children }) {
3785
4514
  </TooltipProvider>
3786
4515
  ```
3787
4516
 
4517
+ ## Composability
4518
+ - Built on Radix Tooltip — accepts `delayDuration`, `skipDelayDuration` (on TooltipProvider), and `open`/`onOpenChange`/`defaultOpen` (on Tooltip root).
4519
+ - **Auto-provider (v0.22.0+):** If no ancestor TooltipProvider exists, `<Tooltip>` auto-wraps itself with one. Explicit TooltipProvider is still recommended at layout root for shared `delayDuration` tuning.
4520
+ - **Trigger:** `<TooltipTrigger asChild>` around the element that should show the tooltip on hover/focus. Icon-only buttons are the canonical use case.
4521
+ - **Not interactive:** TooltipContent is for non-interactive text labels — don't put buttons/links inside. Use HoverCard or Popover when the popped content needs interaction.
4522
+ - **Positioning:** TooltipContent accepts `side`, `align`, `sideOffset`, and arrow via `<TooltipArrow>` (optional).
4523
+ - **Portal rendering:** z-index is `z-tooltip` (highest in the stack — above Popover and Dialog).
4524
+
3788
4525
  ## Gotchas
3789
- - TooltipProvider is REQUIRED — without it, tooltips won't show
4526
+ - TooltipProvider is REQUIRED at the layout level OR auto-created per-tooltip but having a single root provider is better for consistent delays
4527
+ - Don't use Tooltip for critical information — hover-triggered UI is invisible to touch users. Pair with a visible label or aria-description
4528
+ - TooltipContent children must be inert — no buttons, no links, no focusable elements
3790
4529
 
3791
4530
  ## Changes
3792
4531
  ### v0.22.0
@@ -3854,8 +4593,20 @@ export default function RootLayout({ children }) {
3854
4593
  </TreeView>
3855
4594
  ```
3856
4595
 
4596
+ ## Composability
4597
+ - **Two rendering modes:**
4598
+ - **Data-driven:** Pass `items: TreeNode[]` with nested `children`. Good for server-fetched or programmatic trees.
4599
+ - **Declarative:** Use `<TreeItem>` children directly. Good for hardcoded nav, readable JSX.
4600
+ - Don't mix — pick one per TreeView instance.
4601
+ - **useTree hook** lets you drive TreeView state externally (e.g. sync with URL, persist expanded state, drive from Redux). Pass `defaultExpanded`/`defaultSelected` initially OR manage state via the hook's return value.
4602
+ - **TreeItem composability:** Each TreeItem has `icon`, `label`, `secondaryLabel`, and `actions` slots — rich rows without custom render props. `actions` reveals on row hover (same pattern as Message.Actions).
4603
+ - **multiSelect + checkboxes:** Set both to turn TreeView into a file-picker style tree with checkboxes instead of single-select highlighting.
4604
+ - **Keyboard navigation:** Full `role="tree"` spec — Arrow Up/Down to move, Right to expand/descend, Left to collapse/ascend, Enter to select. Pre-wired; no manual key handling needed.
4605
+
3857
4606
  ## Gotchas
3858
4607
  - Supports both data-driven (items prop) and declarative (children) modes
4608
+ - Don't mix data-driven and declarative in the same TreeView — pick one
4609
+ - For a simple flat checkbox list, use RadioGroup/Checkbox + Stack — TreeView is overkill for non-hierarchical data
3859
4610
 
3860
4611
  ## Changes
3861
4612
  ### v0.4.2
@@ -3880,9 +4631,16 @@ export default function RootLayout({ children }) {
3880
4631
  <VisuallyHidden>Screen reader only text</VisuallyHidden>
3881
4632
  ```
3882
4633
 
4634
+ ## Composability
4635
+ - Server-safe. Works anywhere — no context, no cascade.
4636
+ - Canonical use: wrap a DialogTitle / SheetTitle that must exist for a11y but shouldn't show visually (`<DialogTitle asChild><VisuallyHidden>...</VisuallyHidden></DialogTitle>`).
4637
+ - Also useful inside IconButton to provide a text alternative when the icon already has an aria-label (redundant label is harmless and helps some screen readers).
4638
+ - Uses the `sr-only` CSS pattern under the hood — content is in the DOM, just positioned off-screen.
4639
+
3883
4640
  ## Gotchas
3884
4641
  - Content is visually hidden but accessible to screen readers
3885
4642
  - Useful for providing accessible labels without visual UI
4643
+ - Don't use for content you want hidden entirely — that's `hidden` or conditional rendering
3886
4644
 
3887
4645
  ## Changes
3888
4646
  ### v0.1.0
@@ -3933,6 +4691,12 @@ export default function RootLayout({ children }) {
3933
4691
  ## Exported Utilities
3934
4692
  groupItemsByTime(items: ActivityItem[], labels?: GroupLabels) — pure function that buckets items into time groups; returns { label: string, items: ActivityItem[] }[]
3935
4693
 
4694
+ ## Composability
4695
+ - **Built from ui primitives:** Avatar (actor), Button (Load more), Skeleton (loading), Text (body). Override via `renderItem` to use your own primitives per row.
4696
+ - **renderItem** is the composition hook — return your own JSX or `undefined` to fall back to default ActivityEntry. Timeline dot + layout wrapper stay consistent.
4697
+ - **groupBy="time"** wraps items in Today/Yesterday/This Week/Older buckets; `groupItemsByTime()` export reusable for custom renderers.
4698
+ - **Pagination is consumer-driven** — `hasMore` + `onLoadMore` for server-side, or `maxInitialItems` + "Show all" toggle for client-side truncation.
4699
+
3936
4700
  ## Gotchas
3937
4701
  - `items` is required — passing an empty array renders the `emptyState` content
3938
4702
  - `color` on each item controls the timeline dot color
@@ -3998,6 +4762,14 @@ export default function RootLayout({ children }) {
3998
4762
  />
3999
4763
  ```
4000
4764
 
4765
+ ## Composability
4766
+ - **Built on ui/Avatar** — each slot is an Avatar with overlap + border + optional Tooltip.
4767
+ - **Wraps TooltipProvider internally** — don't add another.
4768
+ - **renderAvatar** escape hatch for custom shapes. Wrapper handles overlap/border positioning; your renderer sets size + shape.
4769
+ - **borderColor** must match the surface the group sits on (`surface-raised` on Card, `surface-base` on page bg). Mismatch = visible seam.
4770
+ - **Overflow:** +N badge with optional `onOverflowClick` → pair with a Popover/Sheet for "show all members".
4771
+ - Per-user `ring` (lead/admin/client) surfaces Avatar's ring semantic at group level — use for role/presence hints.
4772
+
4001
4773
  ## Gotchas
4002
4774
  - Wraps TooltipProvider internally — no need to add one yourself
4003
4775
  - Users beyond `max` are collapsed into a "+N" overflow badge
@@ -4059,6 +4831,12 @@ export default function RootLayout({ children }) {
4059
4831
  />
4060
4832
  ```
4061
4833
 
4834
+ ## Composability
4835
+ - **Standalone floating toolbar** — use with DataTable, TreeView, or any selection-capable UI.
4836
+ - **DataTable auto-integration:** DataTable's `bulkActions` prop renders this internally — typically you don't render BulkActionBar directly when using DataTable.
4837
+ - **Data-driven actions:** `{ label, icon, onClick, color, disabled }[]`. For destructive confirmation, call ConfirmDialog from the onClick handler.
4838
+ - **Portal to body + fixed bottom-center z-50** — independent of parent layout. Check for other fixed elements that might overlap.
4839
+
4062
4840
  ## Gotchas
4063
4841
  - Renders via `createPortal` into `document.body` — will not appear during SSR (mounts only client-side)
4064
4842
  - Positioned fixed at bottom-center with `z-50`; ensure no other fixed elements conflict
@@ -4090,6 +4868,13 @@ CommandItem shape: { id, label, description?, icon?, shortcut?, onSelect: () =>
4090
4868
  />
4091
4869
  ```
4092
4870
 
4871
+ ## Composability
4872
+ - **Built on Dialog (portal) + cmdk-style fuzzy matching.** Global Ctrl+K / Cmd+K by default; customize via `keybinding`.
4873
+ - **Groups + items are data-driven.** Each item: `{ id, label, icon?, shortcut?, description?, onSelect }`. Labels accept ReactNode for rich rendering; pair with `filterValue` for typeahead matching against a text representation.
4874
+ - **renderLabel hook** lets you highlight matched characters in query (`(query) => ReactNode`).
4875
+ - **Controlled / uncontrolled** via `open`+`onOpenChange` or `defaultOpen`.
4876
+ - **AppCommandPalette (shell)** is the app-wide variant with CommandRegistry — use that for global palettes with dynamically registered commands.
4877
+
4093
4878
  ## Gotchas
4094
4879
  - Opens with Ctrl+K / Cmd+K by default
4095
4880
  - Items animate in with staggered slide-up (30ms delay cascade); groups fade in; active item icon/shortcut highlight in interactive color (v0.15.0)
@@ -4138,6 +4923,12 @@ const [open, setOpen] = useState(false)
4138
4923
  />
4139
4924
  ```
4140
4925
 
4926
+ ## Composability
4927
+ - **Built on AlertDialog** — non-dismissible (no click-outside, Cancel-first focus), portal-rendered.
4928
+ - **Controlled-only.** Parent owns `open` + `onOpenChange`.
4929
+ - **onConfirm can be async.** `loading=true` replaces confirm text with "Processing..." and disables both buttons. Typical: setLoading(true) → await action() → setLoading(false) + setOpen(false).
4930
+ - **Stays open after confirm** — close via onOpenChange. Keeps the modal up on errors or for chained confirmations.
4931
+
4141
4932
  ## Gotchas
4142
4933
  - Dialog stays open after confirm — consumer must close it via `onOpenChange`
4143
4934
  - Built on AlertDialog internally
@@ -4174,6 +4965,12 @@ const [open, setOpen] = useState(false)
4174
4965
  </ContentCard>
4175
4966
  ```
4176
4967
 
4968
+ ## Composability
4969
+ - **Server-safe higher-level Card** — wraps ui/Card with conventional header/title/actions/footer slots.
4970
+ - **When to use:** List rows, dashboard tiles, any "card with title + optional actions + body + optional footer" pattern. Use raw ui/Card for custom layouts or when you need accent bars.
4971
+ - **Slot precedence:** `header` beats `headerTitle`. Use `headerTitle` for simple strings, `header` for rich content.
4972
+ - **padding variants** (default/compact/spacious/none) set once on ContentCard; the underlying Card size cascade propagates.
4973
+
4177
4974
  ## Gotchas
4178
4975
  - Server-safe: can be imported directly in Next.js Server Components
4179
4976
  - Use `headerTitle` for simple text headers; use `header` for custom header content
@@ -4298,6 +5095,16 @@ PresetKey: 'today' | 'yesterday' | 'last7days' | 'last30days' | 'thisMonth' | 'l
4298
5095
  <TimePicker value={time} onChange={setTime} format="24h" minuteStep={15} />
4299
5096
  ```
4300
5097
 
5098
+ ## Composability
5099
+ - **7 sub-components in one kit:** DatePicker, DateRangePicker, DateTimePicker, TimePicker, CalendarGrid, MonthPicker, YearPicker, Presets. Built on Popover (trigger + calendar content) + date-fns.
5100
+ - **Layered abstractions:**
5101
+ - High-level: DatePicker / DateRangePicker / DateTimePicker / TimePicker (use these 95% of the time — Popover-wrapped, single-field pickers with formatted value display).
5102
+ - Low-level: CalendarGrid / MonthPicker / YearPicker (use for custom picker layouts, inline calendars, dashboards with embedded date selection).
5103
+ - **useCalendar hook** manages month navigation state — use it when building custom picker UI (e.g. inline calendar with custom header).
5104
+ - **Presets component** is standalone — drop it next to a DateRangePicker's calendar for quick-select sidebars (Today / Last 7 days / This month / etc.).
5105
+ - **Events on calendar:** CalendarGrid accepts `events: { date, color, label }[]` — renders up to 3 colored dots per day cell. Useful for availability / booked-days highlights.
5106
+ - **min/max/disabledDates** are shared contract across the whole family — pass the same bounds config to any date picker variant.
5107
+
4301
5108
  ## Gotchas
4302
5109
  - TimePicker stores time inside a Date object — only hours/minutes/seconds are meaningful
4303
5110
  - CalendarGrid is a low-level building block — prefer DatePicker/DateRangePicker for most use cases
@@ -4338,6 +5145,13 @@ PresetKey: 'today' | 'yesterday' | 'last7days' | 'last30days' | 'thisMonth' | 'l
4338
5145
  <DeadlineIndicator deadline={task.dueDate} warningThreshold={2880} criticalThreshold={480} />
4339
5146
  ```
4340
5147
 
5148
+ ## Composability
5149
+ - **Server-safe inline status** — renders a colored text label ("2d left" / "3h left" / "Overdue by 1d") with optional clock icon prefix.
5150
+ - **Threshold-driven color:** `warningThreshold` + `criticalThreshold` drive the green→yellow→red progression. Tune per use case (billing deadlines vs. task due dates have different urgency cadences).
5151
+ - **Composes inside Card, StatusBadge, DataTable cells** — anywhere a short inline deadline string fits.
5152
+ - **Doesn't live-update** — uses `Date.now()` at render time. For ticking timestamps, re-render via a parent interval or use a dedicated "time ago" library.
5153
+ - For static absolute timestamps (not relative), set `format="absolute"` — useful when you want the exact date rendered with semantic color coding.
5154
+
4341
5155
  ## Gotchas
4342
5156
  - Color is semantic: green (on-track) -> yellow (warning threshold) -> red (critical/overdue)
4343
5157
  - Overdue deadlines show bold red text with "Overdue by Xd/h/m"
@@ -4381,6 +5195,14 @@ EmojiPicker, EmojiPickerPopover
4381
5195
  <EmojiPicker onSelect={handleEmoji} theme="dark" />
4382
5196
  ```
4383
5197
 
5198
+ ## Composability
5199
+ - **Two exports:** `EmojiPicker` (inline grid, no trigger) and `EmojiPickerPopover` (trigger + popover wrapper). Use EmojiPickerPopover 95% of the time — trigger-on-click is the standard UX.
5200
+ - **Wraps @emoji-mart/react**, lazy-loaded with a Skeleton placeholder while the ~200KB bundle fetches. Don't pre-import unless you need it eagerly.
5201
+ - **Trigger composition:** EmojiPickerPopover's `children` is the trigger — wrap any Button/IconButton. Typical pairing is an icon-only IconButton with a 😀 label.
5202
+ - **TipTap integration:** Use `createEmojiSuggestion(set?)` factory to create a TipTap suggestion plugin that opens the picker on typing `:emoji`. Works with RichChatInput and RichTextEditor.
5203
+ - **Theme matching:** `theme="auto"` reads the `.dark` class on `<html>` — matches the DS dark mode toggle automatically. Override with explicit light/dark.
5204
+ - **Emoji sets:** Pass `set="apple" | "google" | ...` for consistent cross-platform emoji art (defaults to native OS glyphs).
5205
+
4384
5206
  ## Gotchas
4385
5207
  - Wraps `@emoji-mart/react` which is lazy-loaded — shows a Skeleton placeholder while loading
4386
5208
  - `theme="auto"` reads the `.dark` class on `<html>` to pick light/dark
@@ -4423,6 +5245,14 @@ Note: EmptyState was server-safe prior to v0.18.0 but is NO LONGER server-safe d
4423
5245
  />
4424
5246
  ```
4425
5247
 
5248
+ ## Composability
5249
+ - **Centered block for "no data" / "no results" states.** Renders icon + title + description + optional action slot.
5250
+ - **Icon composition:** Accepts both JSX (`<IconFolder />`) and component references (`IconFolder`). Component refs auto-instantiate with correct sizing. Default icon is the Devalok chakra.
5251
+ - **DataTable integration:** Pass an EmptyState component to DataTable's `emptyState` prop; takes precedence over `noResultsText` string.
5252
+ - **compact mode** for embedding inside Cards, sidebars, narrow sections. Defaults iconSize to 'sm' in compact mode automatically.
5253
+ - **Action slot** typically holds a single primary Button ("Create your first task", "Invite members"). For multi-action, use a div + flex with gap.
5254
+ - No longer server-safe (Framer Motion entrance animation).
5255
+
4426
5256
  ## Gotchas
4427
5257
  - `icon` accepts both JSX elements (`<IconFolder />`) and component references (`IconFolder`). Component references are auto-instantiated with correct sizing classes.
4428
5258
  - `iconSize` controls icon dimensions regardless of icon type. When `compact=true` and no `iconSize`, defaults to `'sm'`.
@@ -4465,6 +5295,13 @@ Note: EmptyState was server-safe prior to v0.18.0 but is NO LONGER server-safe d
4465
5295
  <ErrorDisplay error={error} onReset={() => refetch()} />
4466
5296
  ```
4467
5297
 
5298
+ ## Composability
5299
+ - **ErrorDisplay, not ErrorBoundary** — confusingly, the import path is `error-boundary` but the component is `ErrorDisplay`. It renders an error UI; it does NOT catch errors. Pair it with your own ErrorBoundary (from react-error-boundary, Next.js error.tsx, etc.) as the fallback UI.
5300
+ - **Auto-detects HTTP status codes** (404, 403, 500) when the `error` object has a `status` field — shows appropriate icon + message.
5301
+ - **Dev-only stack trace:** Renders the stack trace only when `process.env.NODE_ENV !== 'production'`. Production users see a clean error page.
5302
+ - **onReset for retry:** Pass a callback that re-fetches / resets state. Commonly wired to a react-query `refetch` or router `replace(...)`.
5303
+ - Composes with EmptyState for "no data" states (which aren't really errors) — use EmptyState for empty, ErrorDisplay for failed.
5304
+
4468
5305
  ## Gotchas
4469
5306
  - Auto-detects HTTP status codes (404, 403, 500) and shows appropriate icon/message
4470
5307
  - Shows stack trace in development mode only
@@ -4510,6 +5347,14 @@ Note: EmptyState was server-safe prior to v0.18.0 but is NO LONGER server-safe d
4510
5347
  <FilePreview url="https://youtube.com/watch?v=..." />
4511
5348
  ```
4512
5349
 
5350
+ ## Composability
5351
+ - **Type-auto-detection** from URL + mimeType routes to the right renderer (image / pdf / video / audio / embed). For ambiguous URLs, pass `type` explicitly.
5352
+ - **Heavy deps are lazy-loaded** — react-pdf (PDF), react-zoom-pan-pinch (Image zoom) are only fetched when first needed. Skeleton placeholder while loading. Don't pre-import.
5353
+ - **Composes inside Dialog/Sheet** — common pattern: thumbnail in a list → click to open a Dialog with `<FilePreview>` filling it.
5354
+ - **Embed URL normalization:** YouTube/Vimeo/Figma/Loom URLs auto-convert to embed format. Pass the original share URL; FilePreview handles it.
5355
+ - **PDF worker from unpkg CDN** — for offline apps, override `pdfjs.GlobalWorkerOptions.workerSrc` in your app entry.
5356
+ - **Download + error fallback** built in — if the file fails to load, users still get a download link.
5357
+
4513
5358
  ## Gotchas
4514
5359
  - Image/PDF lazy-loaded (Skeleton on first render)
4515
5360
  - PDF worker from unpkg CDN — configure workerSrc for offline apps
@@ -4569,6 +5414,14 @@ FilterBar, FilterSelect, FilterMultiSelect
4569
5414
  </FilterBar>
4570
5415
  ```
4571
5416
 
5417
+ ## Composability
5418
+ - **FilterBar + FilterSelect + FilterMultiSelect** — three-part kit. FilterBar is the toolbar container; FilterSelect/FilterMultiSelect are the individual filter controls.
5419
+ - **size propagates via context** from FilterBar to every FilterSelect/FilterMultiSelect child. Don't set size on individual filters.
5420
+ - **Children MUST be direct** — the size cascade breaks if filters are wrapped in extra divs. Use React fragments or let them be direct children.
5421
+ - **Active filter highlight:** FilterSelect/FilterMultiSelect auto-show an accent border when their value is set (non-empty array for multi, non-"all" for single).
5422
+ - **Pair with DataTable or any list:** FilterBar sits above a DataTable/list; `searchValue` + `onSearchChange` drive global filter; individual filters drive column filters via your own state management.
5423
+ - **"Clear all" convention:** Passing `onClearAll` renders a Reset button that's your escape hatch — implement it to clear all filter state in one call.
5424
+
4572
5425
  ## Gotchas
4573
5426
  - FilterSelect and FilterMultiSelect must be direct children of FilterBar to inherit the size context
4574
5427
  - FilterSelect uses `"all"` as the sentinel value for "no filter" — do not use `"all"` as a real option value
@@ -4601,6 +5454,12 @@ FilterBar, FilterSelect, FilterMultiSelect
4601
5454
  </FormSection>
4602
5455
  ```
4603
5456
 
5457
+ ## Composability
5458
+ - **Visual grouping of related FormFields.** Wraps its children with a title + description + horizontal rule separator. No state, no context.
5459
+ - **collapsible mode** wraps content in a Collapsible — useful for "Advanced settings" or optional form sections. `defaultOpen={false}` for initially-closed.
5460
+ - **Pairs with FormField:** Each form control inside should be a FormField for consistent label + helper text + validation. FormSection doesn't auto-wrap; you still structure each field yourself.
5461
+ - **Not a page-level header** — for a full form's main heading use PageHeader. FormSection is mid-form, between field groups.
5462
+
4604
5463
  ## Gotchas
4605
5464
  - `defaultOpen` only applies when `collapsible={true}` — otherwise the section is always open
4606
5465
  - Renders a horizontal rule between the header and content automatically
@@ -4621,6 +5480,12 @@ FilterBar, FilterSelect, FilterMultiSelect
4621
5480
  <GlobalLoading isLoading={isNavigating} />
4622
5481
  ```
4623
5482
 
5483
+ ## Composability
5484
+ - **Thin top-of-viewport progress bar** — for route-level navigation indicators (NProgress-style). Fixed at the top; z-toast layer.
5485
+ - **Wire to router events:** Pair with Next.js `useRouter` + navigation events, or react-router's `useNavigation`, or a custom global loading state in Redux/Zustand. Set `isLoading={true}` during transit, `false` when landed.
5486
+ - **Not for in-page loading** — use Spinner, Skeleton, or LoadingSkeleton for component-level loading states. GlobalLoading is strictly for cross-route / full-page transitions.
5487
+ - **Auto-unmounts when isLoading=false** — no need to manage visibility via classes or conditional rendering.
5488
+
4624
5489
  ## Gotchas
4625
5490
  - Fixed-position bar at top of viewport (z-toast layer)
4626
5491
  - Renders nothing when `isLoading` is false
@@ -4658,6 +5523,14 @@ FilterBar, FilterSelect, FilterMultiSelect
4658
5523
  />
4659
5524
  ```
4660
5525
 
5526
+ ## Composability
5527
+ - **contentEditable-based** — the text IS the editor (Notion / Linear / Figma layer-name pattern). No separate input field appears.
5528
+ - **Keyboard contract:** Click to focus (auto-selects text, like Finder rename). Type to edit. Enter saves, Escape reverts. Paste strips rich content.
5529
+ - **Async save:** `onSave` can return a Promise — InlineEdit shows a spinner and disables editing while pending. On rejection, text reverts to the original value automatically.
5530
+ - **Accessibility:** Accepts `aria-label` and `aria-labelledby` (forwarded to the role="textbox" span) — required when the text isn't self-descriptive. Falls back to `placeholder` as label when neither is provided.
5531
+ - **Not inside FormField** — InlineEdit is for in-place editing of existing content (task title, project name); use regular FormField + Input for traditional forms.
5532
+ - **textClassName for typography control:** Pass `"text-ds-lg font-semibold"` to make it look like a heading without changing the underlying element.
5533
+
4661
5534
  ## Gotchas
4662
5535
  - Uses contentEditable — the text IS the editor. No input field appears.
4663
5536
  - Click to focus → cursor appears in text. Type to edit. Enter saves. Escape reverts.
@@ -4707,6 +5580,13 @@ Exports: CardSkeleton, TableSkeleton, BoardSkeleton, ListSkeleton
4707
5580
  <ListSkeleton rows={10} showAvatar={false} />
4708
5581
  ```
4709
5582
 
5583
+ ## Composability
5584
+ - **Pre-composed skeleton layouts** — CardSkeleton, TableSkeleton, BoardSkeleton, ListSkeleton. Each mimics the shape of a common DS layout so users see meaningful loading placeholders.
5585
+ - **Built on ui/Skeleton** — for custom loading layouts, use Skeleton directly (rectangle/circle/text variants). These composed versions are just opinionated shape combinations.
5586
+ - **Server-safe** — use during SSR for initial-paint skeletons in Next.js app router (while server data streams in).
5587
+ - **Pairs with conditional rendering:** `{isLoading ? <TableSkeleton rows={8} /> : <DataTable data={rows} />}`.
5588
+ - **PageSkeletons (separate file)** provides full-page placeholders (DashboardSkeleton, ProjectListSkeleton, TaskDetailSkeleton) — use those for route-level loading states.
5589
+
4710
5590
  ## Gotchas
4711
5591
  - Server-safe: can be imported directly in Next.js Server Components
4712
5592
  - These are pre-composed skeleton layouts — for individual skeleton shapes, use the `Skeleton` UI component
@@ -4739,6 +5619,15 @@ Exports: CardSkeleton, TableSkeleton, BoardSkeleton, ListSkeleton
4739
5619
  <MarkdownViewer content={trustedHtml} allowHtml />
4740
5620
  ```
4741
5621
 
5622
+ ## Composability
5623
+ - **Read-only markdown renderer.** For editing, use RichTextEditor (TipTap-based). MarkdownViewer is strictly for display.
5624
+ - **Built on react-markdown + remark-gfm** — GFM tables, strikethrough, task lists supported out of the box.
5625
+ - **Syntax highlighting is lazy:** Code blocks with a language fence (```ts, ```python) lazy-load react-syntax-highlighter. First render shows a plain `<pre>` fallback. Don't pre-import.
5626
+ - **Security posture:** Raw HTML is stripped by default — `allowHtml={true}` must be explicit, and ONLY for trusted content (XSS vector otherwise).
5627
+ - **Links open external by default** (`target="_blank"` + `rel="noopener noreferrer"`). Override via `linkTarget`.
5628
+ - **compact mode** for inline use (comments, message bubbles). Default spacing is for article-body content.
5629
+ - **Pairs with Chat's Message.Body** — render markdown from user messages safely. Always keep `allowHtml={false}` for user-generated content.
5630
+
4742
5631
  ## Gotchas
4743
5632
  - Code blocks with a language fence are syntax-highlighted via `react-syntax-highlighter` (lazy-loaded) — the first render shows a plain `<pre>` fallback
4744
5633
  - GFM (tables, strikethrough, task lists) is supported via `remark-gfm`
@@ -4788,6 +5677,13 @@ MasterDetail (root), MasterDetail.List, MasterDetail.Detail, MasterDetail.ListIt
4788
5677
  </MasterDetail>
4789
5678
  ```
4790
5679
 
5680
+ ## Composability
5681
+ - **Responsive list+detail layout.** Desktop: side-by-side panels. Mobile (below `breakpoint`): stacked, mutually exclusive (list OR detail, controlled by `selected`).
5682
+ - **Compound structure:** `MasterDetail.List` contains `MasterDetail.ListItem[]` (interactive). `MasterDetail.Detail` holds the currently-selected view.
5683
+ - **onBack is required for mobile** — renders the back button in Detail pane. Omitting it leaves users stranded once they drill into an item on mobile.
5684
+ - **SSR gotcha:** Uses `window.matchMedia` — initial SSR render picks desktop mode; hydrates to mobile mode if viewport is narrow. If that causes layout shift, consider rendering this only after mount (via `useState(false)` + `useEffect`).
5685
+ - **Pairs with EmptyState** — render EmptyState inside Detail when `selected === null` on desktop ("Pick an item to get started").
5686
+
4791
5687
  ## Gotchas
4792
5688
  - On mobile (below breakpoint), List and Detail are mutually exclusive — selecting an item hides the list
4793
5689
  - The `onBack` callback is required for the mobile back button to appear in the Detail pane
@@ -4816,6 +5712,13 @@ MasterDetail (root), MasterDetail.List, MasterDetail.Detail, MasterDetail.ListIt
4816
5712
  </MemberPicker>
4817
5713
  ```
4818
5714
 
5715
+ ## Composability
5716
+ - **Trigger + Popover + search list.** Pass any button/element as `children` to act as the trigger. Typical pairing: outline Button that says "Assign" or an IconButton with avatar overflow.
5717
+ - **Single vs multi:** `multiple={true}` allows multiple selections; `false` (default) replaces the previous selection. `selectedIds` is always an array in both modes — consumer controls semantics.
5718
+ - **onSelect with a single ID:** Fires once per click with one memberId. Toggle logic in multi mode is consumer responsibility (derive the new array from the click).
5719
+ - **Avatar display:** Uses ui/Avatar internally — supply `avatar` URL in the member object, fallback to initials from `name`.
5720
+ - **For general-purpose multi-select** (non-members), use MultiSelectPopover which has the same popover+search pattern without member-specific avatar rendering.
5721
+
4819
5722
  ## Gotchas
4820
5723
  - `children` is used as the trigger element (e.g., a Button)
4821
5724
  - `onSelect` is called with a single `memberId` — toggle logic is up to the consumer
@@ -4876,6 +5779,17 @@ MasterDetail (root), MasterDetail.List, MasterDetail.Detail, MasterDetail.ListIt
4876
5779
  </MultiSelectPopover>
4877
5780
  ```
4878
5781
 
5782
+ ## Composability
5783
+ - **Generalized multi-select popover** — picks from a fixed list (items) or grouped list (groups), with search, async search, and custom rendering.
5784
+ - **Items vs groups (mutually exclusive):** Pass `items` for flat lists, `groups` for sectioned lists. Don't pass both.
5785
+ - **Async search via onSearch:** When provided, local filtering is disabled — the callback owns filtering and returns a new list. `searchDebounce` (default 300ms) throttles calls.
5786
+ - **renderItem escape hatch:** Pass `(item, selected) => ReactNode` for custom item rendering (avatar + multi-line descriptions, etc.). Built-in default renders image + label + description.
5787
+ - **maxSelections behavior:** At the limit, clicking a new item REPLACES the oldest selection (FIFO). `maxSelections={1}` effectively acts as single-select.
5788
+ - **MultiSelectPopover vs Combobox vs MemberPicker:**
5789
+ - Combobox = form-field multi-select (typeahead + selection in place)
5790
+ - MultiSelectPopover = button-triggered popup for bulk selection (good for "Assign to" / "Add tags" scenarios)
5791
+ - MemberPicker = MultiSelectPopover specialized for team-member UI
5792
+
4879
5793
  ## Gotchas
4880
5794
  - Supply either `items` (flat) or `groups` (sectioned), not both
4881
5795
  - When `onSearch` is provided, local filtering is disabled — the callback must return results
@@ -4919,6 +5833,13 @@ MasterDetail (root), MasterDetail.List, MasterDetail.Detail, MasterDetail.ListIt
4919
5833
  />
4920
5834
  ```
4921
5835
 
5836
+ ## Composability
5837
+ - **Server-safe page-top header** with title + subtitle + breadcrumbs + actions slot. Renders Breadcrumb internally from the `breadcrumbs` array.
5838
+ - **Auto-derived title:** If `title` is omitted, the last breadcrumb's `label` becomes the h1. Saves duplication when the page title matches the breadcrumb leaf.
5839
+ - **Breadcrumb contract:** Last item should NOT have an `href` (it's the current page — renders as BreadcrumbPage, not a link). Items with `href` render as BreadcrumbLink.
5840
+ - **actions slot:** Typically one to three buttons (Save, Cancel, More). For larger action groups, use ButtonGroup or a toolbar.
5841
+ - **Not for tabs / sub-navigation** — pair with Tabs rendered below PageHeader for that.
5842
+
4922
5843
  ## Gotchas
4923
5844
  - Server-safe: can be imported directly in Next.js Server Components
4924
5845
  - If `title` is omitted, the last breadcrumb's `label` is used as the page title
@@ -4951,6 +5872,16 @@ Exports: DashboardSkeleton, ProjectListSkeleton, TaskDetailSkeleton
4951
5872
  <TaskDetailSkeleton />
4952
5873
  ```
4953
5874
 
5875
+ ## Composability
5876
+ - **Full-page skeleton layouts** for route-level loading states. Each mimics a common page shape (dashboard tiles, project list with filters, task detail with sidebar).
5877
+ - **Server-safe** — use in Next.js app router `loading.tsx` files for instant route-transition feedback while data streams.
5878
+ - **Built on LoadingSkeleton + ui/Skeleton** — these just assemble the pre-built regional skeletons into page-shaped layouts.
5879
+ - **When to use which skeleton tier:**
5880
+ - `<Skeleton>` (ui) — single shape for a single element
5881
+ - `<CardSkeleton>` / `<TableSkeleton>` (LoadingSkeleton) — individual region shape
5882
+ - `<DashboardSkeleton>` / etc. (PageSkeletons) — full page placeholder
5883
+ - **Fixed layout structure** — the className prop adjusts the outer container, but internal layout isn't customizable. For custom page skeletons, compose LoadingSkeleton pieces yourself.
5884
+
4954
5885
  ## Gotchas
4955
5886
  - Server-safe: can be imported directly in Next.js Server Components
4956
5887
  - These are full-page skeleton layouts — for smaller skeleton sections, use LoadingSkeleton components
@@ -4983,6 +5914,13 @@ Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT' | 'low' | 'medium' | 'high' | 'u
4983
5914
  <PriorityIndicator priority="low" display="compact" />
4984
5915
  ```
4985
5916
 
5917
+ ## Composability
5918
+ - **Server-safe priority label** — icon + color + text for task / issue priority.
5919
+ - **Composes inside list rows, DataTable cells, Card headers, task panels** — anywhere a priority flag fits.
5920
+ - **display="compact"** shows only the icon (with priority text as title attribute for tooltip). Use in tight cells; use `display="full"` (default) in free space.
5921
+ - **Case-insensitive priority** — accepts both UPPERCASE (LOW/MEDIUM/HIGH/URGENT) and lowercase. Designed to match both backend conventions without manual coercion.
5922
+ - Color semantics: LOW=success, MEDIUM=warning, HIGH=error, URGENT=error with bolder icon.
5923
+
4986
5924
  ## Gotchas
4987
5925
  - Case-insensitive — "low" and "LOW" both work
4988
5926
  - Server-safe: can be imported directly in Next.js Server Components
@@ -5024,6 +5962,13 @@ Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT' | 'low' | 'medium' | 'high' | 'u
5024
5962
  </ResponsiveOverlay>
5025
5963
  ```
5026
5964
 
5965
+ ## Composability
5966
+ - **One component, two containers.** Above `breakpoint`: centered Dialog. Below: bottom-anchored Sheet. Same content slot, different surface treatment per viewport.
5967
+ - **Standard controlled model** — `open` + `onOpenChange` (same shape as Dialog/Sheet).
5968
+ - **title + description pattern** — matches Dialog/Sheet's requirement for an accessible heading. Omit both only when the surrounding context makes the purpose obvious (rare).
5969
+ - **SSR caveat:** `window.matchMedia` reads on mount — initial SSR render defaults to desktop (Dialog). Hydration flips to Sheet on mobile. Acceptable for most flows; visible layout shift on low-spec devices.
5970
+ - **When to use:** Any modal that needs to behave differently on mobile (edit dialogs, filter panels, quick-action menus). For always-the-same behavior, use Dialog or Sheet directly.
5971
+
5027
5972
  ## Gotchas
5028
5973
  - Renders a centered Dialog on desktop and a bottom Sheet on mobile — same content, different container
5029
5974
  - Uses `window.matchMedia` internally — SSR defaults to desktop (Dialog) until hydration
@@ -5097,6 +6042,18 @@ SlashCommandGroup: { label: string; commands: SlashCommand[] }
5097
6042
  />
5098
6043
  ```
5099
6044
 
6045
+ ## Composability
6046
+ - **Chat-specific TipTap editor** — purpose-built for AI + human messaging. Built on RichTextEditor primitives but pre-configured for the chat UX (auto-resize, Enter-to-send, inline toolbar).
6047
+ - **Variant drives the UX envelope:**
6048
+ - `compact` — 2-3 line inline (chat bubble composer)
6049
+ - `expanded` — 5+ lines with always-visible toolbar (AI prompt input)
6050
+ - `minimal` — single line, toolbar on focus (reply composer, quick comment)
6051
+ - **Toolbar is opt-in per feature:** Icons only appear when their corresponding handler/prop is set. `onFileUpload` → attach button appears. `slashCommands` → slash button appears. `mentions` or `onMentionSearch` → @ button appears.
6052
+ - **Composes with Message from ui/chat:** RichChatInput is the composer; Message is the read-only render of the message after send. Use them together for a complete chat UX (RichChatInput at the bottom, MessageList above).
6053
+ - **TipTap is bundled** — no need to install `@tiptap/*` directly.
6054
+ - **For general rich text editing** (not chat — long-form docs, notes), use RichTextEditor instead.
6055
+ - **isStreaming + onCancel** — when the receiving side is streaming a response, show a stop button in place of send. Standard AI chat pattern.
6056
+
5100
6057
  ## Gotchas
5101
6058
  - Tiptap is bundled — no need to install `@tiptap/*` packages separately
5102
6059
  - Enter sends by default; use `enterBehavior="newline"` for long-form composition (Cmd/Ctrl+Enter always sends)
@@ -5165,6 +6122,15 @@ MentionItem: { id: string; label: string; avatar?: string }
5165
6122
  <RichTextViewer content={savedHtml} />
5166
6123
  ```
5167
6124
 
6125
+ ## Composability
6126
+ - **Two exports — editor + viewer.** RichTextEditor for composition; RichTextViewer for read-only rendering of saved HTML. Both share the same prose styling so round-trip display matches the editor.
6127
+ - **TipTap v3 bundled** — no `@tiptap/*` install needed. Consumers can't mix in arbitrary TipTap extensions without forking.
6128
+ - **Toolbar whitelist via `toolbar` prop:** Pass an array of ToolbarItem names to show only those buttons. Omit to show all. Dividers auto-collapse between empty groups.
6129
+ - **Image upload:** Without `onImageUpload`, pasted/dropped images are inlined as base64 (HTML bloats fast). Provide the handler to upload and return a URL.
6130
+ - **Mentions:** Static `mentions` array OR async `onMentionSearch` (which takes precedence). The viewer always renders mentions correctly from saved HTML — no mention props needed on the viewer side.
6131
+ - **For chat composition specifically** (AI + team chat with streaming / slash commands), use RichChatInput — it's built on the same foundation but pre-configured for chat UX.
6132
+ - **Pairs with MarkdownViewer** — many teams use RichTextEditor for compose (WYSIWYG), but render saved content as markdown for simpler serialization. Convert HTML ↔ markdown at the storage boundary.
6133
+
5168
6134
  ## Gotchas
5169
6135
  - Tiptap is bundled — no need to install `@tiptap/*` packages separately
5170
6136
  - Emoji picker requires `@emoji-mart/react` + `@emoji-mart/data` peers
@@ -5229,6 +6195,14 @@ Event colors: "primary" | "success" | "warning" | "error" | "info" | "neutral"
5229
6195
  />
5230
6196
  ```
5231
6197
 
6198
+ ## Composability
6199
+ - **Day / Week calendar view** for time-block display (meetings, shifts, availability). Not a full calendar app — no month view, no drag-to-create.
6200
+ - **Event data is consumer-owned:** You pass `events` as an array; ScheduleView doesn't fetch, doesn't cache, doesn't expand recurring events. All scheduling logic lives in your app.
6201
+ - **Event click + slot click** — `onEventClick` for existing events; `onSlotClick` for creating new events (fires with start/end of the empty slot).
6202
+ - **Color vocabulary matches the DS** — `primary/success/warning/error/info/neutral`. Map your event types to these at the data layer.
6203
+ - **endHour is exclusive:** `endHour=18` means the last visible slot starts at 17:30 (with 30min slots). Match your UX expectation: 9-5 typically means `startHour=9, endHour=18`.
6204
+ - **Pairs with date-picker/composed** — use DatePicker or DateRangePicker to choose which date to show; pass that as ScheduleView's `date`.
6205
+
5232
6206
  ## Gotchas
5233
6207
  - `endHour` is exclusive — `endHour=18` means the last visible slot starts at 17:30 (with default 30min slots)
5234
6208
  - `onSlotClick` fires when clicking an empty time slot — useful for creating new events
@@ -5260,6 +6234,13 @@ Event colors: "primary" | "success" | "warning" | "error" | "info" | "neutral"
5260
6234
  </SimpleTooltip>
5261
6235
  ```
5262
6236
 
6237
+ ## Composability
6238
+ - **One-liner Tooltip** — wraps TooltipProvider + Tooltip + TooltipTrigger + TooltipContent so consumers don't have to manually compose them for a simple label.
6239
+ - **When to use:** 90% of tooltip use cases (icon-only button labels, abbreviated text expansions, secondary info). Use the ui/Tooltip compound for advanced cases (controlled open, nested triggers, custom animations).
6240
+ - **Auto-provides its own TooltipProvider** — safe to drop anywhere. You can still wrap a broader TooltipProvider at layout level for shared `delayDuration`; SimpleTooltip respects it if present.
6241
+ - **Content must be inert** — same rule as ui/Tooltip. For interactive popped content, use Popover or HoverCard.
6242
+ - **Pairs with IconButton** — the canonical pattern for labeled icon buttons.
6243
+
5263
6244
  ## Gotchas
5264
6245
  - Wraps the full Tooltip compound (Provider + Tooltip + Trigger + Content) into one component — no need for TooltipProvider
5265
6246
  - Unlike the low-level Tooltip, SimpleTooltip does not require wrapping in a TooltipProvider
@@ -5300,6 +6281,16 @@ Note: StatusBadge was server-safe prior to v0.18.0 but is NO LONGER server-safe
5300
6281
  <StatusBadge status="active" onClick={() => openStatusPicker()} />
5301
6282
  ```
5302
6283
 
6284
+ ## Composability
6285
+ - **Semantic pill for workflow status.** Built on ui/Badge but with opinionated status → color mapping (active=success, pending=warning, rejected=error, etc.).
6286
+ - **status vs color discriminated union:** Pass either `status` (auto-mapped color) or `color` (explicit). Don't pass both — `color` wins if you do.
6287
+ - **Interactive mode:** Passing `onClick` renders as a `<button>` with a trailing chevron — common pattern for opening a status picker dropdown. Pair with a Popover or DropdownMenu for the status selector.
6288
+ - **Pairs with DataTable, Card headers, activity feeds** — anywhere an inline status pill fits. Use `size="sm"` for dense rows.
6289
+ - **StatusBadge vs ui/Badge vs StatusDot:**
6290
+ - StatusBadge = workflow status pill with built-in status semantics
6291
+ - ui/Badge = generic badge with full color/variant/shape control
6292
+ - StatusDot = tiny presence indicator (no label pill)
6293
+
5303
6294
  ## Gotchas
5304
6295
  - When `color` is set, it takes priority over `status` for styling
5305
6296
  - Props use a discriminated union — pass either `status` or `color`, not both
@@ -5367,6 +6358,14 @@ AppCommandPaletteUser: { name: string, role?: string }
5367
6358
  />
5368
6359
  ```
5369
6360
 
6361
+ ## Composability
6362
+ - **Shell-level wrapper around composed/CommandPalette** — adds opinionated app conventions: user-aware admin command groups, search result integration, navigation dispatch.
6363
+ - **Required setup:** Place inside `<CommandRegistryProvider>` (which owns the list of page items). Typically at app root next to TopBar.
6364
+ - **Router integration via onNavigate:** Pass `(path) => router.push(path)` (Next.js) or equivalent for your framework. All page command clicks funnel through this callback.
6365
+ - **Server-search integration:** Pass `onSearch` + `searchResults` + `onSearchResultSelect` for async search (API calls). `isSearching` drives a loading state. When these props are omitted, AppCommandPalette falls back to local filtering of registered pages.
6366
+ - **Admin gating:** `isAdmin=true` surfaces `adminPages` from CommandRegistry. Takes precedence over `user.role`-based detection so you can force admin mode during testing / impersonation.
6367
+ - **For scoped, non-app-wide palettes** (per-page command trees, custom popups), use composed/CommandPalette directly without the Registry layer.
6368
+
5370
6369
  ## Gotchas
5371
6370
  - Uses CommandRegistry context for page navigation items (see CommandRegistryProvider)
5372
6371
  - `isAdmin` takes precedence over `user.role` for showing admin command groups
@@ -5408,6 +6407,18 @@ BottomNavbarUser: { name: string, role?: string }
5408
6407
  />
5409
6408
  ```
5410
6409
 
6410
+ ## Composability
6411
+ - **Mobile-only bottom nav** — fixed to bottom of viewport. Desktop should use AppSidebar instead.
6412
+ - **Responsive switch pattern:** Use `useIsMobile()` hook to conditionally render AppSidebar (desktop) or BottomNavbar (mobile). Example:
6413
+ ```jsx
6414
+ const isMobile = useIsMobile()
6415
+ return isMobile ? <BottomNavbar ... /> : <AppSidebar ... />
6416
+ ```
6417
+ - **Primary vs overflow:** `primaryItems` (max 4) for the always-visible slots; `moreItems` go into a "More" sheet that opens on tap. Don't exceed 4 primary — the bar becomes cramped.
6418
+ - **Router integration via LinkProvider:** Each nav item is rendered using the framework-specific Link component registered in LinkProvider. Without LinkProvider, you get full-page reloads on tap.
6419
+ - **Badge numbers** cap at 99+ (same as BadgeIndicator pattern).
6420
+ - **Not for desktop:** The viewport-fixed positioning + touch-optimized sizing don't translate well to desktop. Hide behind `md:hidden`.
6421
+
5411
6422
  ## Gotchas
5412
6423
  - Designed for mobile viewports — fixed to bottom of screen
5413
6424
  - Max 4 `primaryItems` recommended; overflow goes in `moreItems` shown in a "More" sheet
@@ -5470,6 +6481,13 @@ CommandPageItem: { id: string, label: string, icon: ReactNode, path: string, key
5470
6481
  </CommandRegistryProvider>
5471
6482
  ```
5472
6483
 
6484
+ ## Composability
6485
+ - **Context provider for AppCommandPalette.** Registers page-level navigation items that the command palette surfaces as commands.
6486
+ - **Place at app root** — wrap both AppCommandPalette and the rest of the app inside `<CommandRegistryProvider>`. Positioning matters: any AppCommandPalette outside the provider gets `useCommandRegistry() === null` and falls back to minimal functionality.
6487
+ - **Separation of pages vs adminPages** — the palette filters based on user role / `isAdmin` flag. Keep admin-only routes in the adminPages array to avoid leaking them to regular users.
6488
+ - **useCommandRegistry()** is the consumer hook — returns the full registry or null. Use in your own command-aware components (e.g. a Spotlight-style keyboard-search embed elsewhere in the app).
6489
+ - **Works with LinkProvider** — CommandPaletteItems navigate via `onNavigate` prop on AppCommandPalette, which routes to your framework's Link component.
6490
+
5473
6491
  ## Gotchas
5474
6492
  - Provides the command registry context consumed by AppCommandPalette
5475
6493
  - Place at app root, wrapping both AppCommandPalette and the rest of the app
@@ -5508,6 +6526,21 @@ import Link from 'next/link'
5508
6526
  </LinkProvider>
5509
6527
  ```
5510
6528
 
6529
+ ## Composability
6530
+ - **The framework router bridge for all shell components.** Without LinkProvider, AppSidebar / BottomNavbar / TopBar.UserMenu / AppCommandPalette render plain `<a>` tags — that means full page reloads instead of client-side navigation.
6531
+ - **Required setup:**
6532
+ ```tsx
6533
+ // Next.js
6534
+ import Link from 'next/link'
6535
+ <LinkProvider component={Link}>
6536
+ <App />
6537
+ </LinkProvider>
6538
+ ```
6539
+ - **Place at app root** — above every shell component that renders nav links.
6540
+ - **component must be forwardRef** (Next.js Link, Remix Link, react-router Link all qualify). Custom components need to forward ref + forward className + forward all anchor props.
6541
+ - **Consumer components use `useLink()`** — returns the registered component or falls back to plain `<a>`. Your own components can consume this hook to integrate with the same router abstraction.
6542
+ - **ui-category components don't consume LinkProvider** — they use `asChild` for router integration (e.g. `<Button asChild><NextLink ...>...</NextLink></Button>`). LinkProvider is shell-specific because shell components embed link arrays (can't use asChild per-item without breaking the data-driven API).
6543
+
5511
6544
  ## Gotchas
5512
6545
  - Without LinkProvider, shell components render plain `<a>` tags (full page reloads)
5513
6546
  - Place at app root, wrapping all shell components that render navigation links
@@ -5565,6 +6598,16 @@ NotificationAction: { label: string, variant?: 'primary' | 'default' | 'danger',
5565
6598
  />
5566
6599
  ```
5567
6600
 
6601
+ ## Composability
6602
+ - **Bell + Popover + notification list** — renders the bell button with unread count badge and a popover list on click.
6603
+ - **Typical placement:** Inside `<TopBar.Right>` — common pattern is `<TopBar.IconButton>` for utility actions PLUS `<NotificationCenter>` for the bell.
6604
+ - **No hardcoded routes** — `getNotificationRoute` is the consumer's routing decision. Return the correct path per notification type (task → `/tasks/:id`, comment → `/threads/:id`, etc.) or null for non-routable notifications.
6605
+ - **onNavigate** fires when a notification with a route is clicked — wire to your router's push/navigate call.
6606
+ - **Pagination:** Pass `hasMore` + `onFetchMore` for infinite-scroll of older notifications.
6607
+ - **emptySlot + footerSlot + headerActions** are content slots for customization — keep the bell+popover shell, swap the inside.
6608
+ - **Pairs with NotificationPreferences** (separate page component) for letting users configure which notification tiers/channels they want to receive.
6609
+ - **onDismiss** is optional — when provided, per-notification X buttons appear. Otherwise mark-as-read is the only dismissal mechanism.
6610
+
5568
6611
  ## Gotchas
5569
6612
  - Typically rendered inside TopBar's `notificationSlot` prop
5570
6613
  - `getNotificationRoute` must be provided for clickable notifications — no hardcoded routes
@@ -5616,6 +6659,13 @@ NotificationProject: { id: string, title: string }
5616
6659
  />
5617
6660
  ```
5618
6661
 
6662
+ ## Composability
6663
+ - **Full-page preferences UI** — typically rendered on a settings page (`/settings/notifications`). Not an overlay; not a dropdown.
6664
+ - **Per-project preferences** — each preference is tied to a project + channel + tier. Users can mute/unmute and adjust tier (INFO / IMPORTANT / CRITICAL threshold) per project-channel combo.
6665
+ - **All callback props support Promise<void>** — integrate with server actions / fetch calls without manual loading state management.
6666
+ - **Pairs with NotificationCenter** — NotificationCenter shows notifications in real time (driven by these preferences); NotificationPreferences lets users tune the rules.
6667
+ - **Data ownership is consumer-side** — you pass `preferences` + `projects`; NotificationPreferences renders. No built-in persistence or sync.
6668
+
5619
6669
  ## Gotchas
5620
6670
  - Manages per-project notification preferences (channel, tier, mute)
5621
6671
  - All callback props support async (Promise<void>) for server-side operations
@@ -5674,6 +6724,17 @@ SidebarPromo: { text: string, icon?: ReactNode, action?: { label: string, href?:
5674
6724
  />
5675
6725
  ```
5676
6726
 
6727
+ ## Composability
6728
+ - **Opinionated app sidebar** — data-driven via `navGroups`, with user profile header, navigation groups (with optional labels + group actions), and a configurable footer (links + version + optional promo banner).
6729
+ - **Required setup (three providers):**
6730
+ - `SidebarProvider` (from ui/sidebar) — owns expanded/collapsed state + cookie persistence
6731
+ - `LinkProvider` — framework router integration for nav item links
6732
+ - `CommandRegistryProvider` (if pairing with AppCommandPalette) — registers the same navGroups so keyboard search finds them
6733
+ - **renderItem escape hatch:** Custom per-item rendering (e.g. custom icons per item type, status decorations, hover previews). Return `null` to fall back to default.
6734
+ - **Nested navigation:** NavItem's `children: NavSubItem[]` renders a collapsible subsection. Auto-opens if a child matches the current path.
6735
+ - **Footer as slot:** `footer` can include links, version info, a ReactNode slot (e.g. theme toggle), and a dismissable promo banner — structure it once, compose.
6736
+ - **Pairs with TopBar + BottomNavbar:** Desktop layout = AppSidebar + TopBar. Mobile layout = hide AppSidebar, show BottomNavbar.
6737
+
5677
6738
  ## Gotchas
5678
6739
  - Must be wrapped in SidebarProvider (from ui/sidebar)
5679
6740
  - Requires LinkProvider for framework-specific link components
@@ -5829,6 +6890,17 @@ UserMenuItem fields:
5829
6890
  </TopBar>
5830
6891
  ```
5831
6892
 
6893
+ ## Composability
6894
+ - **Composition-based, NOT data-driven.** Use dot-notation subcomponents (`TopBar.Left`, `TopBar.Center`, `TopBar.Right`, `TopBar.Section`, `TopBar.IconButton`, `TopBar.Title`, `TopBar.UserMenu`) to assemble. No "props config" — explicit JSX.
6895
+ - **Two-zone vs three-zone layout** — adding `TopBar.Center` flips the root from `flex` to CSS grid (`1fr auto 1fr`) for true centering. Standard apps are two-zone (Left + Right); dashboards with prominent search are three-zone.
6896
+ - **Required providers:**
6897
+ - `SidebarProvider` — for SidebarTrigger inside `TopBar.Left` to work
6898
+ - `LinkProvider` — for TopBar.UserMenu's menu items that use `href`
6899
+ - **NotificationCenter lives inside TopBar.Right** — it's a complete bell+popover component. Drop it in a TopBar.Section next to other icon buttons.
6900
+ - **UserMenu is composable:** `userMenuItems` inserts custom items between the built-in Profile and the color-mode toggle. Each item can navigate (href), run an action (onClick), or show a separator/badge/color decoration.
6901
+ - **Responsive hiding:** Actions that shouldn't appear on mobile use `className="hidden md:flex"` on the IconButton — the component doesn't enforce mobile hiding; that's layout responsibility.
6902
+ - **Pairs with AppSidebar** — desktop app shell is typically `<TopBar>` + `<AppSidebar>` + main content region.
6903
+
5832
6904
  ## Gotchas
5833
6905
  - Without `TopBar.Center`, layout is flex (two-zone). With it, layout switches to CSS grid `1fr auto 1fr` for true centering.
5834
6906
  - `TopBar.IconButton` renders any number of action buttons — no artificial limit. Use responsive hiding (`className="hidden md:flex"`) for mobile.