@dowel-ui/react 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +1 -1
  2. package/dist/blocks/admin-dashboard/meta.js +31 -0
  3. package/dist/blocks/admin-dashboard/meta.js.map +1 -0
  4. package/dist/blocks/agent-console/meta.js +27 -0
  5. package/dist/blocks/agent-console/meta.js.map +1 -0
  6. package/dist/blocks/ai-dashboard/meta.js +25 -0
  7. package/dist/blocks/ai-dashboard/meta.js.map +1 -0
  8. package/dist/blocks/ai-workspace/meta.js +33 -0
  9. package/dist/blocks/ai-workspace/meta.js.map +1 -0
  10. package/dist/blocks/analytics/meta.js +24 -0
  11. package/dist/blocks/analytics/meta.js.map +1 -0
  12. package/dist/blocks/billing/meta.js +25 -0
  13. package/dist/blocks/billing/meta.js.map +1 -0
  14. package/dist/blocks/command-center/meta.js +28 -0
  15. package/dist/blocks/command-center/meta.js.map +1 -0
  16. package/dist/blocks/crm/meta.js +32 -0
  17. package/dist/blocks/crm/meta.js.map +1 -0
  18. package/dist/blocks/onboarding/meta.js +23 -0
  19. package/dist/blocks/onboarding/meta.js.map +1 -0
  20. package/dist/components/accordion/accordion.js +1 -1
  21. package/dist/components/accordion/accordion.js.map +1 -1
  22. package/dist/components/activity-feed/activity-feed.js +1 -1
  23. package/dist/components/activity-feed/activity-feed.js.map +1 -1
  24. package/dist/components/ai-agent-plan/ai-agent-plan.js +1 -1
  25. package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -1
  26. package/dist/components/ai-extraction-review/ai-extraction-review.js +1 -1
  27. package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -1
  28. package/dist/components/ai-prompt-input/ai-prompt-input.js +1 -1
  29. package/dist/components/ai-prompt-input/ai-prompt-input.js.map +1 -1
  30. package/dist/components/ai-reasoning/ai-reasoning.js +1 -1
  31. package/dist/components/ai-reasoning/ai-reasoning.js.map +1 -1
  32. package/dist/components/ai-response/ai-response.js +2 -2
  33. package/dist/components/ai-response/ai-response.js.map +1 -1
  34. package/dist/components/ai-tool/ai-tool.js +2 -2
  35. package/dist/components/ai-tool/ai-tool.js.map +1 -1
  36. package/dist/components/alert/alert.d.ts +1 -1
  37. package/dist/components/avatar/avatar.d.ts +1 -1
  38. package/dist/components/badge/badge.d.ts +1 -1
  39. package/dist/components/breadcrumb/breadcrumb.d.ts +52 -0
  40. package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -0
  41. package/dist/components/breadcrumb/breadcrumb.js +141 -0
  42. package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
  43. package/dist/components/breadcrumb/index.d.ts +2 -0
  44. package/dist/components/breadcrumb/index.js +2 -0
  45. package/dist/components/breadcrumb/meta.js +17 -0
  46. package/dist/components/breadcrumb/meta.js.map +1 -0
  47. package/dist/components/button/button.d.ts +1 -1
  48. package/dist/components/calendar/calendar.js +2 -2
  49. package/dist/components/calendar/calendar.js.map +1 -1
  50. package/dist/components/code-block/code-block.js +1 -1
  51. package/dist/components/code-block/code-block.js.map +1 -1
  52. package/dist/components/collapsible/collapsible.d.ts +30 -0
  53. package/dist/components/collapsible/collapsible.d.ts.map +1 -0
  54. package/dist/components/collapsible/collapsible.js +38 -0
  55. package/dist/components/collapsible/collapsible.js.map +1 -0
  56. package/dist/components/collapsible/index.d.ts +2 -0
  57. package/dist/components/collapsible/index.js +2 -0
  58. package/dist/components/collapsible/meta.js +17 -0
  59. package/dist/components/collapsible/meta.js.map +1 -0
  60. package/dist/components/combobox/combobox.js +2 -2
  61. package/dist/components/combobox/combobox.js.map +1 -1
  62. package/dist/components/command/command.js +1 -1
  63. package/dist/components/command/command.js.map +1 -1
  64. package/dist/components/data-table/data-table.d.ts.map +1 -1
  65. package/dist/components/data-table/data-table.js +4 -1
  66. package/dist/components/data-table/data-table.js.map +1 -1
  67. package/dist/components/dialog/dialog.js +2 -2
  68. package/dist/components/dialog/dialog.js.map +1 -1
  69. package/dist/components/diff-viewer/diff-viewer.js +4 -4
  70. package/dist/components/diff-viewer/diff-viewer.js.map +1 -1
  71. package/dist/components/direction/direction.d.ts +35 -0
  72. package/dist/components/direction/direction.d.ts.map +1 -0
  73. package/dist/components/direction/direction.js +14 -0
  74. package/dist/components/direction/direction.js.map +1 -0
  75. package/dist/components/direction/index.d.ts +2 -0
  76. package/dist/components/direction/index.js +2 -0
  77. package/dist/components/direction/meta.js +17 -0
  78. package/dist/components/direction/meta.js.map +1 -0
  79. package/dist/components/drawer/drawer.js +1 -1
  80. package/dist/components/drawer/drawer.js.map +1 -1
  81. package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -1
  82. package/dist/components/dropdown-menu/dropdown-menu.js +9 -8
  83. package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -1
  84. package/dist/components/input/input.js +1 -1
  85. package/dist/components/input/input.js.map +1 -1
  86. package/dist/components/log-viewer/log-viewer.js +2 -2
  87. package/dist/components/log-viewer/log-viewer.js.map +1 -1
  88. package/dist/components/meter/meter.js +1 -1
  89. package/dist/components/meter/meter.js.map +1 -1
  90. package/dist/components/pagination/pagination.d.ts.map +1 -1
  91. package/dist/components/pagination/pagination.js +3 -2
  92. package/dist/components/pagination/pagination.js.map +1 -1
  93. package/dist/components/permission-matrix/permission-matrix.js +3 -3
  94. package/dist/components/permission-matrix/permission-matrix.js.map +1 -1
  95. package/dist/components/progress/progress.d.ts +1 -1
  96. package/dist/components/record-diff/record-diff.js +4 -4
  97. package/dist/components/record-diff/record-diff.js.map +1 -1
  98. package/dist/components/select/select.js +2 -2
  99. package/dist/components/select/select.js.map +1 -1
  100. package/dist/components/sheet/sheet.d.ts.map +1 -1
  101. package/dist/components/sheet/sheet.js +2 -2
  102. package/dist/components/sheet/sheet.js.map +1 -1
  103. package/dist/components/sidebar/index.d.ts +2 -0
  104. package/dist/components/sidebar/index.js +2 -0
  105. package/dist/components/sidebar/meta.js +17 -0
  106. package/dist/components/sidebar/meta.js.map +1 -0
  107. package/dist/components/sidebar/sidebar.d.ts +69 -0
  108. package/dist/components/sidebar/sidebar.d.ts.map +1 -0
  109. package/dist/components/sidebar/sidebar.js +256 -0
  110. package/dist/components/sidebar/sidebar.js.map +1 -0
  111. package/dist/components/spinner/spinner.d.ts +1 -1
  112. package/dist/components/table/table.js +2 -2
  113. package/dist/components/table/table.js.map +1 -1
  114. package/dist/components/tags-input/tags-input.js +1 -1
  115. package/dist/components/tags-input/tags-input.js.map +1 -1
  116. package/dist/components/textarea/index.d.ts +2 -0
  117. package/dist/components/textarea/index.js +2 -0
  118. package/dist/components/textarea/meta.js +17 -0
  119. package/dist/components/textarea/meta.js.map +1 -0
  120. package/dist/components/textarea/textarea.d.ts +43 -0
  121. package/dist/components/textarea/textarea.d.ts.map +1 -0
  122. package/dist/components/textarea/textarea.js +119 -0
  123. package/dist/components/textarea/textarea.js.map +1 -0
  124. package/dist/components/time-range-picker/time-range-picker.js +3 -3
  125. package/dist/components/time-range-picker/time-range-picker.js.map +1 -1
  126. package/dist/components/toast/toast.d.ts +1 -1
  127. package/dist/components/toast/toast.d.ts.map +1 -1
  128. package/dist/components/toast/toast.js +2 -2
  129. package/dist/components/toast/toast.js.map +1 -1
  130. package/dist/index.d.ts +12 -2
  131. package/dist/index.js +7 -2
  132. package/dist/lib/styles.d.ts +16 -1
  133. package/dist/lib/styles.d.ts.map +1 -1
  134. package/dist/lib/styles.js +16 -1
  135. package/dist/lib/styles.js.map +1 -1
  136. package/dist/registry/blocks.d.ts.map +1 -1
  137. package/dist/registry/blocks.js +27 -9
  138. package/dist/registry/blocks.js.map +1 -1
  139. package/dist/registry/components.d.ts.map +1 -1
  140. package/dist/registry/components.js +60 -50
  141. package/dist/registry/components.js.map +1 -1
  142. package/dist/registry/schema.d.ts +8 -0
  143. package/dist/registry/schema.d.ts.map +1 -1
  144. package/dist/registry/schema.js.map +1 -1
  145. package/package.json +5 -4
  146. package/src/components/accordion/accordion.tsx +1 -1
  147. package/src/components/activity-feed/activity-feed.tsx +1 -1
  148. package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
  149. package/src/components/ai-extraction-review/ai-extraction-review.tsx +1 -1
  150. package/src/components/ai-prompt-input/ai-prompt-input.tsx +1 -1
  151. package/src/components/ai-reasoning/ai-reasoning.tsx +1 -1
  152. package/src/components/ai-response/ai-response.tsx +2 -2
  153. package/src/components/ai-tool/ai-tool.tsx +2 -2
  154. package/src/components/breadcrumb/breadcrumb.tsx +157 -0
  155. package/src/components/breadcrumb/index.ts +10 -0
  156. package/src/components/breadcrumb/meta.ts +19 -0
  157. package/src/components/calendar/calendar.tsx +2 -2
  158. package/src/components/code-block/code-block.tsx +1 -1
  159. package/src/components/collapsible/collapsible.tsx +63 -0
  160. package/src/components/collapsible/index.ts +6 -0
  161. package/src/components/collapsible/meta.ts +16 -0
  162. package/src/components/combobox/combobox.tsx +2 -2
  163. package/src/components/command/command.tsx +1 -1
  164. package/src/components/data-table/data-table.tsx +14 -3
  165. package/src/components/dialog/dialog.tsx +2 -2
  166. package/src/components/diff-viewer/diff-viewer.tsx +4 -4
  167. package/src/components/direction/direction.tsx +38 -0
  168. package/src/components/direction/index.ts +1 -0
  169. package/src/components/direction/meta.ts +19 -0
  170. package/src/components/drawer/drawer.tsx +1 -1
  171. package/src/components/dropdown-menu/dropdown-menu.tsx +14 -8
  172. package/src/components/input/input.tsx +1 -1
  173. package/src/components/log-viewer/log-viewer.tsx +2 -2
  174. package/src/components/meter/meter.tsx +1 -1
  175. package/src/components/pagination/pagination.tsx +13 -2
  176. package/src/components/permission-matrix/permission-matrix.tsx +4 -4
  177. package/src/components/record-diff/record-diff.tsx +4 -4
  178. package/src/components/select/select.tsx +2 -2
  179. package/src/components/sheet/sheet.tsx +8 -4
  180. package/src/components/sidebar/index.ts +19 -0
  181. package/src/components/sidebar/meta.ts +25 -0
  182. package/src/components/sidebar/sidebar.tsx +374 -0
  183. package/src/components/table/table.tsx +3 -3
  184. package/src/components/tags-input/tags-input.tsx +1 -1
  185. package/src/components/textarea/index.ts +1 -0
  186. package/src/components/textarea/meta.ts +21 -0
  187. package/src/components/textarea/textarea.tsx +210 -0
  188. package/src/components/time-range-picker/time-range-picker.tsx +3 -3
  189. package/src/components/toast/toast.tsx +14 -6
  190. package/src/index.ts +5 -0
  191. package/src/lib/styles.ts +16 -0
  192. package/src/registry/blocks.ts +18 -0
  193. package/src/registry/components.ts +10 -0
  194. package/src/registry/schema.ts +8 -0
  195. package/src/blocks/admin-users/admin-users.tsx +0 -251
  196. package/src/blocks/admin-users/index.ts +0 -1
  197. package/src/blocks/admin-users/meta.ts +0 -27
  198. package/src/blocks/ai-chat/ai-chat.tsx +0 -280
  199. package/src/blocks/ai-chat/index.ts +0 -7
  200. package/src/blocks/ai-chat/meta.ts +0 -32
  201. package/src/blocks/dashboard/dashboard.tsx +0 -192
  202. package/src/blocks/dashboard/index.ts +0 -6
  203. package/src/blocks/dashboard/meta.ts +0 -18
  204. package/src/blocks/forgot-password/forgot-password.tsx +0 -158
  205. package/src/blocks/forgot-password/index.ts +0 -1
  206. package/src/blocks/forgot-password/meta.ts +0 -18
  207. package/src/blocks/login/index.ts +0 -1
  208. package/src/blocks/login/login.tsx +0 -178
  209. package/src/blocks/login/meta.ts +0 -18
  210. package/src/blocks/pricing/index.ts +0 -1
  211. package/src/blocks/pricing/meta.ts +0 -18
  212. package/src/blocks/pricing/pricing.tsx +0 -176
  213. package/src/blocks/settings/index.ts +0 -6
  214. package/src/blocks/settings/meta.ts +0 -29
  215. package/src/blocks/settings/settings.tsx +0 -317
  216. package/src/blocks/signup/index.ts +0 -1
  217. package/src/blocks/signup/meta.ts +0 -18
  218. package/src/blocks/signup/signup.tsx +0 -240
@@ -146,7 +146,7 @@ export function PermissionMatrix({
146
146
  <th
147
147
  role="columnheader"
148
148
  scope="col"
149
- className="px-3 py-2 text-left text-xs font-medium"
149
+ className="px-3 py-2 text-start text-xs font-medium"
150
150
  >
151
151
  Permission
152
152
  </th>
@@ -186,7 +186,7 @@ export function PermissionMatrix({
186
186
  <th
187
187
  role="rowheader"
188
188
  scope="row"
189
- className="px-3 py-1.5 text-left text-xs font-medium"
189
+ className="px-3 py-1.5 text-start text-xs font-medium"
190
190
  >
191
191
  {row.label}
192
192
  <span className="sr-only">, all</span>
@@ -216,8 +216,8 @@ export function PermissionMatrix({
216
216
  role="rowheader"
217
217
  scope="row"
218
218
  className={cn(
219
- "px-3 py-2 text-left font-normal",
220
- row.permission.group !== undefined && "pl-6",
219
+ "px-3 py-2 text-start font-normal",
220
+ row.permission.group !== undefined && "ps-6",
221
221
  )}
222
222
  >
223
223
  {row.permission.label}
@@ -241,19 +241,19 @@ export function RecordDiffTable({
241
241
  <tr className="border-b border-border">
242
242
  <th
243
243
  scope="col"
244
- className="px-3 py-2 text-left text-xs font-medium text-muted-foreground"
244
+ className="px-3 py-2 text-start text-xs font-medium text-muted-foreground"
245
245
  >
246
246
  Field
247
247
  </th>
248
248
  <th
249
249
  scope="col"
250
- className="px-3 py-2 text-left text-xs font-medium text-muted-foreground"
250
+ className="px-3 py-2 text-start text-xs font-medium text-muted-foreground"
251
251
  >
252
252
  Before
253
253
  </th>
254
254
  <th
255
255
  scope="col"
256
- className="px-3 py-2 text-left text-xs font-medium text-muted-foreground"
256
+ className="px-3 py-2 text-start text-xs font-medium text-muted-foreground"
257
257
  >
258
258
  After
259
259
  </th>
@@ -271,7 +271,7 @@ export function RecordDiffTable({
271
271
  rather than as a loose cell in a wall of table. */}
272
272
  <th
273
273
  scope="row"
274
- className="px-3 py-2 text-left align-top font-medium whitespace-nowrap"
274
+ className="px-3 py-2 text-start align-top font-medium whitespace-nowrap"
275
275
  >
276
276
  {field.label}
277
277
  {/* The change kind must not be carried by row colour alone. */}
@@ -158,7 +158,7 @@ export function SelectItem({ className, label, children, ...props }: SelectItemP
158
158
  <SelectPrimitive.Item
159
159
  data-slot="select-item"
160
160
  className={cn(
161
- "relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-sm outline-none select-none",
161
+ "relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 ps-2 pe-8 text-sm outline-none select-none",
162
162
  "transition-colors duration-[var(--duration-instant)]",
163
163
  "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
164
164
  "data-[disabled]:pointer-events-none data-[disabled]:opacity-55",
@@ -176,7 +176,7 @@ export function SelectItem({ className, label, children, ...props }: SelectItemP
176
176
  </>
177
177
  )}
178
178
  {/* Decorative: selection is already announced through aria-selected. */}
179
- <span className="absolute right-2 grid size-4 place-items-center">
179
+ <span className="absolute end-2 grid size-4 place-items-center">
180
180
  <SelectPrimitive.ItemIndicator>
181
181
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="size-3.5">
182
182
  <path
@@ -49,8 +49,12 @@ const sheetVariants = cva(
49
49
  side: {
50
50
  top: "inset-x-0 top-0 border-b [--slide-y:-100%]",
51
51
  bottom: "inset-x-0 bottom-0 border-t [--slide-y:100%]",
52
- left: "inset-y-0 left-0 h-full w-3/4 max-w-sm border-r [--slide-x:-100%]",
53
- right: "inset-y-0 right-0 h-full w-3/4 max-w-sm border-l [--slide-x:100%]",
52
+ // rtl-ok: the variant is named `left`, and a sheet asked for on the
53
+ // left that opens on the right in Arabic is an API telling a lie. The
54
+ // logical version of this is a `start`/`end` side, which is a rename
55
+ // rather than a restyle.
56
+ left: "inset-y-0 left-0 h-full w-3/4 max-w-sm border-r [--slide-x:-100%]", // rtl-ok: named side
57
+ right: "inset-y-0 right-0 h-full w-3/4 max-w-sm border-l [--slide-x:100%]", // rtl-ok: named side
54
58
  },
55
59
  },
56
60
  defaultVariants: {
@@ -87,7 +91,7 @@ export function SheetContent({
87
91
  data-slot="sheet-close"
88
92
  aria-label="Close"
89
93
  className={cn(
90
- "absolute top-4 right-4 grid size-7 place-items-center rounded-md text-muted-foreground",
94
+ "absolute end-4 top-4 grid size-7 place-items-center rounded-md text-muted-foreground",
91
95
  "transition-colors duration-[var(--duration-fast)] hover:bg-accent hover:text-foreground",
92
96
  "[&_svg]:size-4",
93
97
  focusRing,
@@ -112,7 +116,7 @@ export function SheetHeader({ className, ...props }: ComponentPropsWithRef<"div"
112
116
  return (
113
117
  <div
114
118
  data-slot="sheet-header"
115
- className={cn("flex flex-col gap-1.5 pr-8 text-left", className)}
119
+ className={cn("flex flex-col gap-1.5 pe-8 text-start", className)}
116
120
  {...props}
117
121
  />
118
122
  );
@@ -0,0 +1,19 @@
1
+ export {
2
+ Sidebar,
3
+ SidebarContent,
4
+ SidebarFooter,
5
+ SidebarGroup,
6
+ SidebarGroupLabel,
7
+ SidebarHeader,
8
+ SidebarInset,
9
+ SidebarMenu,
10
+ SidebarMenuButton,
11
+ SidebarMenuItem,
12
+ SidebarMenuLabel,
13
+ SidebarProvider,
14
+ SidebarTrigger,
15
+ type SidebarMenuButtonProps,
16
+ type SidebarProps,
17
+ type SidebarProviderProps,
18
+ type SidebarTriggerProps,
19
+ } from "./sidebar";
@@ -0,0 +1,25 @@
1
+ import { defineMeta } from "@/registry/schema";
2
+
3
+ export const meta = defineMeta({
4
+ name: "sidebar",
5
+ title: "Sidebar",
6
+ description:
7
+ "The application's own navigation: a collapsible rail on a wide screen, an overlay on a narrow one.",
8
+ category: "navigation",
9
+ status: "stable",
10
+ dependencies: ["radix-ui"],
11
+ registryDependencies: ["sheet"],
12
+ files: ["sidebar.tsx"],
13
+ a11y:
14
+ "The navigation landmark must be named — a page has several, and three regions all called " +
15
+ '"navigation" is a list nobody can choose from. The active entry carries aria-current=' +
16
+ '"page" rather than only a background colour. On a narrow screen it is a Sheet, not a ' +
17
+ "styled div: an overlay needs a focus trap and an Escape key, and without them the page " +
18
+ "behind stays reachable by Tab while the menu covers it. When the rail collapses, labels " +
19
+ "are visually hidden rather than removed — removing them leaves controls whose only content " +
20
+ "is an icon, which is how a collapsed sidebar becomes a column of links all announced as " +
21
+ '"link". The trigger says what pressing it will do, not what the state currently is. The ' +
22
+ "overlay is mounted only on a narrow screen and has its own open state: a modal dialog that " +
23
+ "CSS hides is still modal, and mounting it always made every wide screen aria-hidden and " +
24
+ "unclickable whenever the rail was open.",
25
+ });
@@ -0,0 +1,374 @@
1
+ "use client";
2
+
3
+ import { Slot } from "radix-ui";
4
+ import {
5
+ createContext,
6
+ useContext,
7
+ useId,
8
+ useMemo,
9
+ useState,
10
+ useSyncExternalStore,
11
+ type ComponentPropsWithRef,
12
+ type ReactNode,
13
+ } from "react";
14
+
15
+ import { Sheet, SheetContent, SheetTitle } from "@/components/sheet";
16
+ import { disabledStyles, focusRing } from "@/lib/styles";
17
+ import { cn } from "@/lib/utils";
18
+
19
+ /**
20
+ * The application's own navigation.
21
+ *
22
+ * Two states, not one. On a wide screen it collapses to a rail of icons and
23
+ * stays in the page; on a narrow one it is an overlay, which needs a focus trap
24
+ * and an Escape key and is therefore a Sheet rather than a div with a
25
+ * transform. Building one behaviour and hiding it at a breakpoint gets the
26
+ * mobile case wrong every time — the page behind stays reachable by Tab while
27
+ * the menu covers it.
28
+ *
29
+ * The collapsed rail is where these usually fail. Hiding the label leaves a
30
+ * button whose only content is an icon, and an icon has no accessible name. The
31
+ * label here is not removed but visually hidden, so the control keeps its name
32
+ * whether or not the text is on screen.
33
+ *
34
+ * The overlay is mounted only on a narrow screen, and it has its own open
35
+ * state. Mounting it always and hiding it with CSS was the first version, and
36
+ * it was wrong in a way no visual check catches: a modal dialog that is
37
+ * `display: none` is still modal. It marks the rest of the page `aria-hidden`,
38
+ * traps focus in an element nothing can focus, and turns pointer events off on
39
+ * the body — so on a desktop the whole application was invisible to a screen
40
+ * reader and unclickable the moment the sidebar was open, which is its default.
41
+ */
42
+
43
+ /** Below Tailwind's `md` breakpoint, which is where the rail gives way to the overlay. */
44
+ const NARROW_QUERY = "(max-width: 767px)";
45
+
46
+ function subscribeToNarrow(callback: () => void): () => void {
47
+ const query = window.matchMedia(NARROW_QUERY);
48
+ query.addEventListener("change", callback);
49
+ return () => {
50
+ query.removeEventListener("change", callback);
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Whether the viewport is narrow enough for the overlay.
56
+ *
57
+ * The server snapshot says "wide": the rail renders in the HTML and CSS hides
58
+ * it below the breakpoint, so a narrow screen shows nothing wrong before
59
+ * hydration and the sheet mounts, closed, straight after.
60
+ */
61
+ function useIsNarrow(): boolean {
62
+ return useSyncExternalStore(
63
+ subscribeToNarrow,
64
+ () => window.matchMedia(NARROW_QUERY).matches,
65
+ () => false,
66
+ );
67
+ }
68
+
69
+ interface SidebarContextValue {
70
+ /** The rail is expanded rather than collapsed. */
71
+ open: boolean;
72
+ setOpen: (open: boolean) => void;
73
+ /** The viewport is narrow, so the navigation is an overlay. */
74
+ narrow: boolean;
75
+ /** The overlay is showing. Only meaningful when `narrow`. */
76
+ overlayOpen: boolean;
77
+ setOverlayOpen: (open: boolean) => void;
78
+ /** Whichever of the two states applies on this viewport. */
79
+ toggle: () => void;
80
+ /** Id of the sidebar element, for the trigger's aria-controls. */
81
+ sidebarId: string;
82
+ }
83
+
84
+ const SidebarContext = createContext<SidebarContextValue | null>(null);
85
+
86
+ function useSidebar(component: string): SidebarContextValue {
87
+ const context = useContext(SidebarContext);
88
+ if (!context) {
89
+ throw new Error(`${component} must be rendered inside <SidebarProvider>.`);
90
+ }
91
+ return context;
92
+ }
93
+
94
+ export interface SidebarProviderProps {
95
+ /** Controlled open state. */
96
+ open?: boolean;
97
+ defaultOpen?: boolean;
98
+ onOpenChange?: (open: boolean) => void;
99
+ children?: ReactNode;
100
+ }
101
+
102
+ export function SidebarProvider({
103
+ open: openProp,
104
+ defaultOpen = true,
105
+ onOpenChange,
106
+ children,
107
+ }: SidebarProviderProps) {
108
+ const [uncontrolled, setUncontrolled] = useState(defaultOpen);
109
+ const open = openProp ?? uncontrolled;
110
+ // Separate from `open`, and never true to begin with: "expanded by default"
111
+ // is a fact about the rail. An overlay that covers the page on first load is
112
+ // a menu nobody asked for.
113
+ const [overlayOpen, setOverlayOpen] = useState(false);
114
+ const narrow = useIsNarrow();
115
+ const sidebarId = useId();
116
+
117
+ const value = useMemo<SidebarContextValue>(() => {
118
+ const setOpen = (next: boolean) => {
119
+ if (openProp === undefined) setUncontrolled(next);
120
+ onOpenChange?.(next);
121
+ };
122
+
123
+ return {
124
+ open,
125
+ setOpen,
126
+ narrow,
127
+ overlayOpen,
128
+ setOverlayOpen,
129
+ toggle: () => {
130
+ if (narrow) setOverlayOpen(!overlayOpen);
131
+ else setOpen(!open);
132
+ },
133
+ sidebarId,
134
+ };
135
+ }, [open, openProp, onOpenChange, narrow, overlayOpen, sidebarId]);
136
+
137
+ return <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>;
138
+ }
139
+
140
+ export interface SidebarProps extends ComponentPropsWithRef<"aside"> {
141
+ /**
142
+ * Names the navigation landmark.
143
+ *
144
+ * Required, not optional. A page has more than one navigation region — this,
145
+ * a breadcrumb, a footer — and three landmarks all called "navigation" is a
146
+ * list a screen reader user cannot choose from.
147
+ */
148
+ label: string;
149
+ /** Title for the overlay on a narrow screen, where it is a dialog. */
150
+ mobileTitle?: string;
151
+ }
152
+
153
+ export function Sidebar({ className, label, mobileTitle, children, ...props }: SidebarProps) {
154
+ const { open, narrow, overlayOpen, setOverlayOpen, sidebarId } = useSidebar("Sidebar");
155
+
156
+ return (
157
+ <>
158
+ {/*
159
+ The overlay, on a narrow screen. A Sheet rather than a styled div: it
160
+ traps focus, closes on Escape and marks the rest of the page inert,
161
+ none of which a transform does — and without them the page behind stays
162
+ reachable by Tab while the menu covers it.
163
+
164
+ Mounted only when the screen is narrow. Those three behaviours are
165
+ exactly why it must not exist on a wide one: see the note at the top.
166
+ */}
167
+ {narrow ? (
168
+ <Sheet open={overlayOpen} onOpenChange={setOverlayOpen}>
169
+ <SheetContent side="left" className="w-72 p-0">
170
+ <SheetTitle className="sr-only">{mobileTitle ?? label}</SheetTitle>
171
+ <nav aria-label={label} className="flex h-full flex-col gap-2 p-3">
172
+ {children}
173
+ </nav>
174
+ </SheetContent>
175
+ </Sheet>
176
+ ) : null}
177
+
178
+ <aside
179
+ id={sidebarId}
180
+ data-slot="sidebar"
181
+ data-state={open ? "expanded" : "collapsed"}
182
+ className={cn(
183
+ "hidden shrink-0 border-e border-border bg-background md:block",
184
+ "transition-[width] duration-[var(--duration-normal)] ease-[var(--ease-out-quint)]",
185
+ open ? "w-60" : "w-14",
186
+ className,
187
+ )}
188
+ {...props}
189
+ >
190
+ <nav aria-label={label} className="flex h-full flex-col gap-2 p-3">
191
+ {children}
192
+ </nav>
193
+ </aside>
194
+ </>
195
+ );
196
+ }
197
+
198
+ export function SidebarHeader({ className, ...props }: ComponentPropsWithRef<"div">) {
199
+ return (
200
+ <div
201
+ data-slot="sidebar-header"
202
+ className={cn("flex min-h-9 items-center gap-2 px-1", className)}
203
+ {...props}
204
+ />
205
+ );
206
+ }
207
+
208
+ export function SidebarContent({ className, ...props }: ComponentPropsWithRef<"div">) {
209
+ return (
210
+ <div
211
+ data-slot="sidebar-content"
212
+ className={cn("flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto", className)}
213
+ {...props}
214
+ />
215
+ );
216
+ }
217
+
218
+ export function SidebarFooter({ className, ...props }: ComponentPropsWithRef<"div">) {
219
+ return <div data-slot="sidebar-footer" className={cn("mt-auto", className)} {...props} />;
220
+ }
221
+
222
+ export function SidebarGroup({ className, ...props }: ComponentPropsWithRef<"div">) {
223
+ return <div data-slot="sidebar-group" className={cn("grid gap-1", className)} {...props} />;
224
+ }
225
+
226
+ /**
227
+ * A heading for a group of links.
228
+ *
229
+ * Hidden rather than removed when the rail is collapsed, so the grouping
230
+ * survives for anyone not reading the layout visually.
231
+ */
232
+ export function SidebarGroupLabel({ className, ...props }: ComponentPropsWithRef<"div">) {
233
+ const { open, narrow } = useSidebar("SidebarGroupLabel");
234
+
235
+ return (
236
+ <div
237
+ data-slot="sidebar-group-label"
238
+ className={cn(
239
+ "px-2 text-xs font-medium tracking-wide text-muted-foreground uppercase",
240
+ // The rail's collapsed state is not the overlay's: a sheet is always
241
+ // wide enough for its labels.
242
+ !open && !narrow && "sr-only",
243
+ className,
244
+ )}
245
+ {...props}
246
+ />
247
+ );
248
+ }
249
+
250
+ export function SidebarMenu({ className, ...props }: ComponentPropsWithRef<"ul">) {
251
+ return <ul data-slot="sidebar-menu" className={cn("grid gap-0.5", className)} {...props} />;
252
+ }
253
+
254
+ export function SidebarMenuItem({ className, ...props }: ComponentPropsWithRef<"li">) {
255
+ return <li data-slot="sidebar-menu-item" className={cn("min-w-0", className)} {...props} />;
256
+ }
257
+
258
+ export interface SidebarMenuButtonProps extends ComponentPropsWithRef<"a"> {
259
+ /** Render the child as the control, for a router's own link or a button. */
260
+ asChild?: boolean;
261
+ /** The page currently being shown. */
262
+ isActive?: boolean;
263
+ }
264
+
265
+ /**
266
+ * One entry in the navigation.
267
+ *
268
+ * `aria-current="page"` when active, not merely a background colour: which page
269
+ * you are on is information, and a highlight conveys it to exactly one kind of
270
+ * reader.
271
+ */
272
+ export function SidebarMenuButton({
273
+ className,
274
+ asChild = false,
275
+ isActive = false,
276
+ children,
277
+ ...props
278
+ }: SidebarMenuButtonProps) {
279
+ const Component = asChild ? Slot.Root : "a";
280
+
281
+ return (
282
+ <Component
283
+ data-slot="sidebar-menu-button"
284
+ data-active={isActive || undefined}
285
+ aria-current={isActive ? "page" : undefined}
286
+ className={cn(
287
+ "flex h-9 w-full items-center gap-2.5 rounded-md px-2 text-sm",
288
+ "transition-colors duration-[var(--duration-fast)]",
289
+ "[&>svg]:size-4 [&>svg]:shrink-0",
290
+ isActive
291
+ ? "bg-accent font-medium text-accent-foreground"
292
+ : "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
293
+ focusRing,
294
+ disabledStyles,
295
+ className,
296
+ )}
297
+ {...props}
298
+ >
299
+ {children}
300
+ </Component>
301
+ );
302
+ }
303
+
304
+ /**
305
+ * The text of a menu entry.
306
+ *
307
+ * Visually hidden rather than removed when the rail is collapsed. Removing it
308
+ * leaves a control whose only content is an icon, and an icon has no accessible
309
+ * name — which is how a collapsed sidebar becomes a column of buttons all
310
+ * announced as "link".
311
+ */
312
+ export function SidebarMenuLabel({ className, ...props }: ComponentPropsWithRef<"span">) {
313
+ const { open, narrow } = useSidebar("SidebarMenuLabel");
314
+
315
+ return (
316
+ <span
317
+ data-slot="sidebar-menu-label"
318
+ className={cn("min-w-0 truncate", !open && !narrow && "sr-only", className)}
319
+ {...props}
320
+ />
321
+ );
322
+ }
323
+
324
+ export interface SidebarTriggerProps extends ComponentPropsWithRef<"button"> {
325
+ /** Overrides the label, which otherwise says what pressing it will do. */
326
+ label?: string;
327
+ }
328
+
329
+ export function SidebarTrigger({ className, label, ...props }: SidebarTriggerProps) {
330
+ const { open, narrow, overlayOpen, toggle, sidebarId } = useSidebar("SidebarTrigger");
331
+
332
+ const expanded = narrow ? overlayOpen : open;
333
+ const fallback = narrow
334
+ ? overlayOpen
335
+ ? "Close navigation"
336
+ : "Open navigation"
337
+ : open
338
+ ? "Collapse navigation"
339
+ : "Expand navigation";
340
+
341
+ return (
342
+ <button
343
+ type="button"
344
+ data-slot="sidebar-trigger"
345
+ aria-expanded={expanded}
346
+ // The rail is what this controls. The overlay is a dialog that names
347
+ // itself, and pointing at an element CSS has hidden helps nobody.
348
+ aria-controls={narrow ? undefined : sidebarId}
349
+ // Says what pressing it does, not what the state currently is. "Collapse
350
+ // navigation" while it is expanded is the useful half.
351
+ aria-label={label ?? fallback}
352
+ onClick={toggle}
353
+ className={cn(
354
+ "grid size-9 shrink-0 place-items-center rounded-md text-muted-foreground",
355
+ "transition-colors duration-[var(--duration-fast)] hover:bg-accent hover:text-foreground",
356
+ focusRing,
357
+ className,
358
+ )}
359
+ {...props}
360
+ >
361
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden className="size-4">
362
+ <rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" strokeWidth="2" />
363
+ <path d="M9 4v16" stroke="currentColor" strokeWidth="2" />
364
+ </svg>
365
+ </button>
366
+ );
367
+ }
368
+
369
+ /** The region the sidebar sits beside. */
370
+ export function SidebarInset({ className, ...props }: ComponentPropsWithRef<"main">) {
371
+ return (
372
+ <main data-slot="sidebar-inset" className={cn("min-w-0 flex-1", className)} {...props} />
373
+ );
374
+ }
@@ -98,8 +98,8 @@ export function TableHead({ className, ...props }: ComponentPropsWithRef<"th">)
98
98
  <th
99
99
  data-slot="table-head"
100
100
  className={cn(
101
- "h-10 px-3 text-left align-middle text-xs font-medium whitespace-nowrap text-muted-foreground",
102
- "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-0",
101
+ "h-10 px-3 text-start align-middle text-xs font-medium whitespace-nowrap text-muted-foreground",
102
+ "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pe-0",
103
103
  className,
104
104
  )}
105
105
  {...props}
@@ -113,7 +113,7 @@ export function TableCell({ className, ...props }: ComponentPropsWithRef<"td">)
113
113
  data-slot="table-cell"
114
114
  className={cn(
115
115
  "px-3 py-2.5 align-middle",
116
- "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-0",
116
+ "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pe-0",
117
117
  className,
118
118
  )}
119
119
  {...props}
@@ -232,7 +232,7 @@ export function TagsInput({
232
232
  removeAt(index);
233
233
  }}
234
234
  className={cn(
235
- "-mr-0.5 grid size-4 place-items-center rounded-full",
235
+ "-me-0.5 grid size-4 place-items-center rounded-full",
236
236
  "transition-colors hover:bg-foreground/10",
237
237
  focusRing,
238
238
  disabledStyles,
@@ -0,0 +1 @@
1
+ export { Textarea, textareaVariants, type TextareaProps } from "./textarea";
@@ -0,0 +1,21 @@
1
+ import { defineMeta } from "@/registry/schema";
2
+
3
+ export const meta = defineMeta({
4
+ name: "textarea",
5
+ title: "Textarea",
6
+ description:
7
+ "A multi-line text field, with optional auto-resize and a character count that knows when to stay quiet.",
8
+ category: "form",
9
+ status: "stable",
10
+ dependencies: ["class-variance-authority"],
11
+ registryDependencies: [],
12
+ files: ["textarea.tsx"],
13
+ a11y:
14
+ "Pair with a Label via htmlFor/id. The error state is driven by aria-invalid, so assistive " +
15
+ "technology and styling stay in sync. The character count is described by the field rather " +
16
+ "than announced on every keystroke — a permanently live counter reads the remaining number " +
17
+ "between every letter typed, which is unusable — so it goes live only once the limit is " +
18
+ "close, and states the remainder in words rather than as a bare ratio that is read aloud as " +
19
+ "two unlabelled numbers. Horizontal resizing is off by default, because a field dragged " +
20
+ "wider than its container is a broken layout.",
21
+ });