@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
@@ -1 +1 @@
1
- {"version":3,"file":"input.js","names":[],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { focusRingInset, invalidStyles } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nconst inputVariants = cva(\n cn(\n \"flex w-full min-w-0 rounded-md border border-input bg-background text-foreground shadow-xs\",\n \"transition-[border-color,box-shadow] duration-[var(--duration-fast)] ease-[var(--ease-out-quint)]\",\n \"placeholder:text-muted-foreground\",\n \"file:mr-3 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground\",\n \"focus-visible:border-ring\",\n \"disabled:cursor-not-allowed disabled:opacity-55\",\n focusRingInset,\n invalidStyles,\n ),\n {\n variants: {\n inputSize: {\n sm: \"h-8 px-2.5 text-sm\",\n md: \"h-9 px-3 text-sm\",\n lg: \"h-10 px-3.5 text-base\",\n },\n },\n defaultVariants: {\n inputSize: \"md\",\n },\n },\n);\n\nexport interface InputProps\n extends ComponentPropsWithRef<\"input\">, VariantProps<typeof inputVariants> {}\n\n/**\n * Single-line text field.\n *\n * The visual size prop is named `inputSize` rather than `size` because `size`\n * is a native `<input>` attribute with unrelated semantics (a character-count\n * width hint). Naming it apart keeps both available instead of shadowing one.\n *\n * Error styling is driven by the native `aria-invalid` attribute rather than a\n * bespoke prop, so form libraries wire it up without an adapter.\n */\nexport function Input({ className, inputSize, type = \"text\", ...props }: InputProps) {\n return (\n <input type={type} className={cn(inputVariants({ inputSize }), className)} {...props} />\n );\n}\n\nexport { inputVariants };\n"],"mappings":";;;;;AAMA,MAAM,gBAAgB,IACpB,GACE,8FACA,qGACA,qCACA,kGACA,6BACA,mDACA,gBACA,aACF,GACA;CACE,UAAU,EACR,WAAW;EACT,IAAI;EACJ,IAAI;EACJ,IAAI;CACN,EACF;CACA,iBAAiB,EACf,WAAW,KACb;AACF,CACF;;;;;;;;;;;AAeA,SAAgB,MAAM,EAAE,WAAW,WAAW,OAAO,QAAQ,GAAG,SAAqB;CACnF,OACE,oBAAC,SAAD;EAAa;EAAM,WAAW,GAAG,cAAc,EAAE,UAAU,CAAC,GAAG,SAAS;EAAG,GAAI;CAAQ,CAAA;AAE3F"}
1
+ {"version":3,"file":"input.js","names":[],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { focusRingInset, invalidStyles } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nconst inputVariants = cva(\n cn(\n \"flex w-full min-w-0 rounded-md border border-input bg-background text-foreground shadow-xs\",\n \"transition-[border-color,box-shadow] duration-[var(--duration-fast)] ease-[var(--ease-out-quint)]\",\n \"placeholder:text-muted-foreground\",\n \"file:me-3 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground\",\n \"focus-visible:border-ring\",\n \"disabled:cursor-not-allowed disabled:opacity-55\",\n focusRingInset,\n invalidStyles,\n ),\n {\n variants: {\n inputSize: {\n sm: \"h-8 px-2.5 text-sm\",\n md: \"h-9 px-3 text-sm\",\n lg: \"h-10 px-3.5 text-base\",\n },\n },\n defaultVariants: {\n inputSize: \"md\",\n },\n },\n);\n\nexport interface InputProps\n extends ComponentPropsWithRef<\"input\">, VariantProps<typeof inputVariants> {}\n\n/**\n * Single-line text field.\n *\n * The visual size prop is named `inputSize` rather than `size` because `size`\n * is a native `<input>` attribute with unrelated semantics (a character-count\n * width hint). Naming it apart keeps both available instead of shadowing one.\n *\n * Error styling is driven by the native `aria-invalid` attribute rather than a\n * bespoke prop, so form libraries wire it up without an adapter.\n */\nexport function Input({ className, inputSize, type = \"text\", ...props }: InputProps) {\n return (\n <input type={type} className={cn(inputVariants({ inputSize }), className)} {...props} />\n );\n}\n\nexport { inputVariants };\n"],"mappings":";;;;;AAMA,MAAM,gBAAgB,IACpB,GACE,8FACA,qGACA,qCACA,kGACA,6BACA,mDACA,gBACA,aACF,GACA;CACE,UAAU,EACR,WAAW;EACT,IAAI;EACJ,IAAI;EACJ,IAAI;CACN,EACF;CACA,iBAAiB,EACf,WAAW,KACb;AACF,CACF;;;;;;;;;;;AAeA,SAAgB,MAAM,EAAE,WAAW,WAAW,OAAO,QAAQ,GAAG,SAAqB;CACnF,OACE,oBAAC,SAAD;EAAa;EAAM,WAAW,GAAG,cAAc,EAAE,UAAU,CAAC,GAAG,SAAS;EAAG,GAAI;CAAQ,CAAA;AAE3F"}
@@ -134,7 +134,7 @@ function LogViewer({ className, lines, label, rowHeight = 22, height = 360, anno
134
134
  "data-index": item.index,
135
135
  "data-slot": "log-viewer-row",
136
136
  "data-level": line.level,
137
- className: "absolute top-0 left-0 w-full",
137
+ className: "absolute start-0 top-0 w-full",
138
138
  style: { transform: `translateY(${String(item.start)}px)` },
139
139
  children: /* @__PURE__ */ jsx(LogViewerRow, {
140
140
  line,
@@ -192,7 +192,7 @@ function LogViewerRow({ line, expanded = false, onToggle }) {
192
192
  ]
193
193
  }), expanded && line.fields ? /* @__PURE__ */ jsx("dl", {
194
194
  "data-slot": "log-viewer-fields",
195
- className: "mt-1 mb-1 ml-4 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 border-l border-border pl-3",
195
+ className: "ms-4 mt-1 mb-1 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 border-s border-border ps-3",
196
196
  children: Object.entries(line.fields).map(([key, value]) => /* @__PURE__ */ jsxs("div", {
197
197
  className: "contents",
198
198
  children: [/* @__PURE__ */ jsx("dt", {
@@ -1 +1 @@
1
- {"version":3,"file":"log-viewer.js","names":[],"sources":["../../../src/components/log-viewer/log-viewer.tsx"],"sourcesContent":["\"use client\";\n\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport {\n useEffect,\n useId,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type ReactNode,\n} from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nimport { LOG_LEVELS, segment, type LogLevel, type VisibleLine } from \"./log-stream\";\n\n/**\n * A streaming console: filter, follow, expand.\n *\n * The incumbent is react-lazylog, which was last published in 2022, is built on\n * react-virtualized, and cannot run on React 19 — while still taking about\n * 15,000 downloads a week. This is for those people.\n *\n * Two accessibility decisions worth stating rather than discovering.\n *\n * `role=\"log\"` implies `aria-live=\"polite\"`, which is right for a handful of\n * events and catastrophic for a console: a screen reader would read every line\n * of a build as it scrolls past, and nothing else would be audible. Announcing\n * is therefore off by default and opt-in through `announce`. The role stays,\n * because it still describes what the region is.\n *\n * And virtualization means most rows are not in the DOM. That is a real trade,\n * not an implementation detail: assistive technology cannot reach what is not\n * rendered, so a reader who needs the whole log needs an escape — which is what\n * `onDownload` is for. A log viewer with no way out of the virtual window is\n * not accessible however good its ARIA is.\n */\n\nconst LEVEL_STYLES: Record<LogLevel, string> = {\n trace: \"text-muted-foreground\",\n debug: \"text-muted-foreground\",\n info: \"text-foreground\",\n warn: \"text-warning\",\n error: \"text-destructive\",\n fatal: \"text-destructive font-semibold\",\n};\n\n/** Distance from the bottom, in pixels, that still counts as \"at the end\". */\nconst FOLLOW_THRESHOLD = 24;\n\nexport interface LogViewerProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n lines: VisibleLine[];\n /** Names the region. */\n label: string;\n /** Row height in pixels. Rows are one line; expansion is measured. */\n rowHeight?: number;\n height?: number | string;\n /**\n * Read new lines aloud. Off by default — a console that announces every line\n * makes a screen reader useless for anything else.\n */\n announce?: boolean;\n /** The way out of the virtual window, for anyone who needs the whole log. */\n onDownload?: () => void;\n children?: ReactNode;\n}\n\nexport function LogViewer({\n className,\n lines,\n label,\n rowHeight = 22,\n height = 360,\n announce = false,\n onDownload,\n children,\n ...props\n}: LogViewerProps) {\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const [following, setFollowing] = useState(true);\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n const labelId = useId();\n\n const virtualizer = useVirtualizer({\n count: lines.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 12,\n // Without a starting rect the virtualizer renders nothing until a\n // ResizeObserver fires, so the first paint is an empty box. Seeding it from\n // the declared height means rows are there immediately, and it is measured\n // properly a moment later.\n initialRect: { width: 0, height: typeof height === \"number\" ? height : 360 },\n });\n\n // Follow mode, hand-written rather than delegated. A spring-anchoring library\n // wants to own the scroll container and so does the virtualizer, and the two\n // fight; this is scrollToIndex on append plus a proximity check.\n useEffect(() => {\n if (!following || lines.length === 0) return;\n virtualizer.scrollToIndex(lines.length - 1, { align: \"end\" });\n }, [following, lines.length, virtualizer]);\n\n function handleScroll() {\n const element = scrollRef.current;\n if (!element) return;\n\n const distance = element.scrollHeight - element.scrollTop - element.clientHeight;\n // Scrolling up detaches, because the reader is looking at something and\n // yanking them back to the tail would lose it. Returning to the bottom\n // re-attaches, which is the gesture people already expect.\n setFollowing(distance <= FOLLOW_THRESHOLD);\n }\n\n function toggleExpanded(id: string) {\n setExpanded((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n }\n\n const items = virtualizer.getVirtualItems();\n\n return (\n <div data-slot=\"log-viewer\" className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span id={labelId} className=\"text-sm font-medium\">\n {label}\n </span>\n <div className=\"flex items-center gap-2\">\n {!following ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-jump\"\n onClick={() => {\n setFollowing(true);\n }}\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n >\n Jump to latest\n </button>\n ) : (\n <span className=\"text-xs text-muted-foreground\">Following</span>\n )}\n {onDownload ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-download\"\n onClick={onDownload}\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n >\n Download full log\n </button>\n ) : null}\n </div>\n </div>\n\n {children}\n\n <div\n ref={scrollRef}\n onScroll={handleScroll}\n role=\"log\"\n aria-labelledby={labelId}\n // Explicitly off unless asked for: role=\"log\" implies polite, and a\n // console that reads every line aloud drowns out everything else.\n aria-live={announce ? \"polite\" : \"off\"}\n // A scrollable region must be focusable, or its content is unreachable\n // without a pointer — WCAG 2.1.1, and axe's scrollable-region-focusable\n // requires exactly this. jsx-a11y's heuristic cannot see that this\n // element scrolls, so here the requirement outranks the rule.\n // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n tabIndex={0}\n style={{ height }}\n className={cn(\n \"overflow-auto rounded-lg border border-border bg-muted/30 font-mono text-xs\",\n focusRing,\n )}\n >\n {lines.length === 0 ? (\n <p className=\"p-3 text-muted-foreground\">No lines match the current filter.</p>\n ) : (\n <div style={{ height: virtualizer.getTotalSize(), position: \"relative\" }}>\n {items.map((item) => {\n const line = lines[item.index];\n if (!line) return null;\n\n return (\n <div\n key={line.id}\n ref={virtualizer.measureElement}\n data-index={item.index}\n data-slot=\"log-viewer-row\"\n data-level={line.level}\n className=\"absolute top-0 left-0 w-full\"\n style={{ transform: `translateY(${String(item.start)}px)` }}\n >\n <LogViewerRow\n line={line}\n expanded={expanded.has(line.id)}\n onToggle={() => {\n toggleExpanded(line.id);\n }}\n />\n </div>\n );\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport interface LogViewerRowProps {\n line: VisibleLine;\n expanded?: boolean;\n onToggle?: () => void;\n}\n\n/**\n * One line.\n *\n * Exported because the virtualized container cannot be measured outside a real\n * browser — jsdom reports every box as zero — so the row's behaviour would\n * otherwise be untestable. It is also genuinely useful on its own for anyone\n * rendering a short log without virtualization.\n */\nexport function LogViewerRow({ line, expanded = false, onToggle }: LogViewerRowProps) {\n const hasFields = line.fields !== undefined && Object.keys(line.fields).length > 0;\n const parts = segment(line.message, line.matches);\n\n return (\n <div className=\"px-3 py-0.5 hover:bg-accent/40\">\n <div className=\"flex items-baseline gap-2\">\n {line.timestamp ? (\n <time className=\"shrink-0 text-muted-foreground tabular-nums\">{line.timestamp}</time>\n ) : null}\n\n {line.level ? (\n // The level in text, not a coloured bar. Colour alone cannot\n // distinguish warn from error for everyone reading.\n <span className={cn(\"w-11 shrink-0 uppercase\", LEVEL_STYLES[line.level])}>\n {line.level}\n </span>\n ) : null}\n\n <span\n className={cn(\n \"min-w-0 flex-1 break-all whitespace-pre-wrap\",\n line.level && LEVEL_STYLES[line.level],\n )}\n >\n {parts.map((part, index) =>\n part.match ? (\n // A mark element, so the match is conveyed structurally rather\n // than only as a background colour.\n <mark key={index} className=\"rounded-[2px] bg-warning/35 text-inherit\">\n {part.text}\n </mark>\n ) : (\n <span key={index}>{part.text}</span>\n ),\n )}\n </span>\n\n {hasFields ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-expand\"\n aria-expanded={expanded}\n onClick={onToggle}\n className={cn(\n \"shrink-0 rounded px-1 text-2xs text-muted-foreground\",\n \"transition-colors hover:text-foreground\",\n focusRing,\n disabledStyles,\n )}\n >\n {expanded ? \"Hide fields\" : \"Fields\"}\n </button>\n ) : null}\n </div>\n\n {expanded && line.fields ? (\n <dl\n data-slot=\"log-viewer-fields\"\n className=\"mt-1 mb-1 ml-4 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 border-l border-border pl-3\"\n >\n {Object.entries(line.fields).map(([key, value]) => (\n <div key={key} className=\"contents\">\n <dt className=\"text-muted-foreground\">{key}</dt>\n <dd className=\"m-0 break-all\">\n {typeof value === \"string\" ? value : JSON.stringify(value)}\n </dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n );\n}\n\nexport interface LogViewerToolbarProps extends Omit<ComponentPropsWithRef<\"div\">, \"onChange\"> {\n query: string;\n onQueryChange: (query: string) => void;\n regex: boolean;\n onRegexChange: (regex: boolean) => void;\n levels: Set<LogLevel>;\n onToggleLevel: (level: LogLevel) => void;\n counts: Map<LogLevel, number>;\n invalidPattern?: boolean;\n /** Shown as \"N of M lines\". */\n showing: number;\n total: number;\n}\n\nexport function LogViewerToolbar({\n className,\n query,\n onQueryChange,\n regex,\n onRegexChange,\n levels,\n onToggleLevel,\n counts,\n invalidPattern = false,\n showing,\n total,\n ...props\n}: LogViewerToolbarProps) {\n const filterId = useId();\n const errorId = useId();\n\n return (\n <div\n data-slot=\"log-viewer-toolbar\"\n className={cn(\"flex flex-wrap items-center gap-2\", className)}\n {...props}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <label htmlFor={filterId} className=\"sr-only\">\n Filter log\n </label>\n <input\n id={filterId}\n type=\"search\"\n value={query}\n placeholder={regex ? \"Pattern…\" : \"Filter…\"}\n aria-invalid={invalidPattern || undefined}\n aria-describedby={invalidPattern ? errorId : undefined}\n onChange={(event) => {\n onQueryChange(event.target.value);\n }}\n className={cn(\n \"min-w-32 flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm\",\n invalidPattern && \"border-destructive\",\n focusRing,\n )}\n />\n <label className=\"flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <input\n type=\"checkbox\"\n checked={regex}\n onChange={(event) => {\n onRegexChange(event.target.checked);\n }}\n className={cn(\"size-3.5 rounded border-input\", focusRing)}\n />\n Regex\n </label>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-1\">\n {LOG_LEVELS.filter((level) => counts.has(level)).map((level) => {\n const on = levels.size === 0 || levels.has(level);\n return (\n <button\n key={level}\n type=\"button\"\n data-slot=\"log-viewer-facet\"\n aria-pressed={on}\n onClick={() => {\n onToggleLevel(level);\n }}\n className={cn(\n \"rounded-md border px-1.5 py-0.5 font-mono text-2xs uppercase transition-colors\",\n on\n ? \"border-transparent bg-secondary text-secondary-foreground\"\n : \"border-input bg-background text-muted-foreground line-through\",\n focusRing,\n )}\n >\n {level} {counts.get(level) ?? 0}\n </button>\n );\n })}\n </div>\n\n <p className=\"w-full text-xs text-muted-foreground tabular-nums\">\n {invalidPattern ? (\n // Said out loud rather than shown as an empty log, which reads as\n // \"nothing matched\" and sends the reader looking for the wrong thing.\n <span id={errorId} className=\"text-destructive\">\n Incomplete pattern — showing everything until it is valid\n </span>\n ) : (\n `${String(showing)} of ${String(total)} lines`\n )}\n </p>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"log-viewer.js","names":[],"sources":["../../../src/components/log-viewer/log-viewer.tsx"],"sourcesContent":["\"use client\";\n\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport {\n useEffect,\n useId,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type ReactNode,\n} from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nimport { LOG_LEVELS, segment, type LogLevel, type VisibleLine } from \"./log-stream\";\n\n/**\n * A streaming console: filter, follow, expand.\n *\n * The incumbent is react-lazylog, which was last published in 2022, is built on\n * react-virtualized, and cannot run on React 19 — while still taking about\n * 15,000 downloads a week. This is for those people.\n *\n * Two accessibility decisions worth stating rather than discovering.\n *\n * `role=\"log\"` implies `aria-live=\"polite\"`, which is right for a handful of\n * events and catastrophic for a console: a screen reader would read every line\n * of a build as it scrolls past, and nothing else would be audible. Announcing\n * is therefore off by default and opt-in through `announce`. The role stays,\n * because it still describes what the region is.\n *\n * And virtualization means most rows are not in the DOM. That is a real trade,\n * not an implementation detail: assistive technology cannot reach what is not\n * rendered, so a reader who needs the whole log needs an escape — which is what\n * `onDownload` is for. A log viewer with no way out of the virtual window is\n * not accessible however good its ARIA is.\n */\n\nconst LEVEL_STYLES: Record<LogLevel, string> = {\n trace: \"text-muted-foreground\",\n debug: \"text-muted-foreground\",\n info: \"text-foreground\",\n warn: \"text-warning\",\n error: \"text-destructive\",\n fatal: \"text-destructive font-semibold\",\n};\n\n/** Distance from the bottom, in pixels, that still counts as \"at the end\". */\nconst FOLLOW_THRESHOLD = 24;\n\nexport interface LogViewerProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n lines: VisibleLine[];\n /** Names the region. */\n label: string;\n /** Row height in pixels. Rows are one line; expansion is measured. */\n rowHeight?: number;\n height?: number | string;\n /**\n * Read new lines aloud. Off by default — a console that announces every line\n * makes a screen reader useless for anything else.\n */\n announce?: boolean;\n /** The way out of the virtual window, for anyone who needs the whole log. */\n onDownload?: () => void;\n children?: ReactNode;\n}\n\nexport function LogViewer({\n className,\n lines,\n label,\n rowHeight = 22,\n height = 360,\n announce = false,\n onDownload,\n children,\n ...props\n}: LogViewerProps) {\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const [following, setFollowing] = useState(true);\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n const labelId = useId();\n\n const virtualizer = useVirtualizer({\n count: lines.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 12,\n // Without a starting rect the virtualizer renders nothing until a\n // ResizeObserver fires, so the first paint is an empty box. Seeding it from\n // the declared height means rows are there immediately, and it is measured\n // properly a moment later.\n initialRect: { width: 0, height: typeof height === \"number\" ? height : 360 },\n });\n\n // Follow mode, hand-written rather than delegated. A spring-anchoring library\n // wants to own the scroll container and so does the virtualizer, and the two\n // fight; this is scrollToIndex on append plus a proximity check.\n useEffect(() => {\n if (!following || lines.length === 0) return;\n virtualizer.scrollToIndex(lines.length - 1, { align: \"end\" });\n }, [following, lines.length, virtualizer]);\n\n function handleScroll() {\n const element = scrollRef.current;\n if (!element) return;\n\n const distance = element.scrollHeight - element.scrollTop - element.clientHeight;\n // Scrolling up detaches, because the reader is looking at something and\n // yanking them back to the tail would lose it. Returning to the bottom\n // re-attaches, which is the gesture people already expect.\n setFollowing(distance <= FOLLOW_THRESHOLD);\n }\n\n function toggleExpanded(id: string) {\n setExpanded((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n }\n\n const items = virtualizer.getVirtualItems();\n\n return (\n <div data-slot=\"log-viewer\" className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span id={labelId} className=\"text-sm font-medium\">\n {label}\n </span>\n <div className=\"flex items-center gap-2\">\n {!following ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-jump\"\n onClick={() => {\n setFollowing(true);\n }}\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n >\n Jump to latest\n </button>\n ) : (\n <span className=\"text-xs text-muted-foreground\">Following</span>\n )}\n {onDownload ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-download\"\n onClick={onDownload}\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n >\n Download full log\n </button>\n ) : null}\n </div>\n </div>\n\n {children}\n\n <div\n ref={scrollRef}\n onScroll={handleScroll}\n role=\"log\"\n aria-labelledby={labelId}\n // Explicitly off unless asked for: role=\"log\" implies polite, and a\n // console that reads every line aloud drowns out everything else.\n aria-live={announce ? \"polite\" : \"off\"}\n // A scrollable region must be focusable, or its content is unreachable\n // without a pointer — WCAG 2.1.1, and axe's scrollable-region-focusable\n // requires exactly this. jsx-a11y's heuristic cannot see that this\n // element scrolls, so here the requirement outranks the rule.\n // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n tabIndex={0}\n style={{ height }}\n className={cn(\n \"overflow-auto rounded-lg border border-border bg-muted/30 font-mono text-xs\",\n focusRing,\n )}\n >\n {lines.length === 0 ? (\n <p className=\"p-3 text-muted-foreground\">No lines match the current filter.</p>\n ) : (\n <div style={{ height: virtualizer.getTotalSize(), position: \"relative\" }}>\n {items.map((item) => {\n const line = lines[item.index];\n if (!line) return null;\n\n return (\n <div\n key={line.id}\n ref={virtualizer.measureElement}\n data-index={item.index}\n data-slot=\"log-viewer-row\"\n data-level={line.level}\n className=\"absolute start-0 top-0 w-full\"\n style={{ transform: `translateY(${String(item.start)}px)` }}\n >\n <LogViewerRow\n line={line}\n expanded={expanded.has(line.id)}\n onToggle={() => {\n toggleExpanded(line.id);\n }}\n />\n </div>\n );\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport interface LogViewerRowProps {\n line: VisibleLine;\n expanded?: boolean;\n onToggle?: () => void;\n}\n\n/**\n * One line.\n *\n * Exported because the virtualized container cannot be measured outside a real\n * browser — jsdom reports every box as zero — so the row's behaviour would\n * otherwise be untestable. It is also genuinely useful on its own for anyone\n * rendering a short log without virtualization.\n */\nexport function LogViewerRow({ line, expanded = false, onToggle }: LogViewerRowProps) {\n const hasFields = line.fields !== undefined && Object.keys(line.fields).length > 0;\n const parts = segment(line.message, line.matches);\n\n return (\n <div className=\"px-3 py-0.5 hover:bg-accent/40\">\n <div className=\"flex items-baseline gap-2\">\n {line.timestamp ? (\n <time className=\"shrink-0 text-muted-foreground tabular-nums\">{line.timestamp}</time>\n ) : null}\n\n {line.level ? (\n // The level in text, not a coloured bar. Colour alone cannot\n // distinguish warn from error for everyone reading.\n <span className={cn(\"w-11 shrink-0 uppercase\", LEVEL_STYLES[line.level])}>\n {line.level}\n </span>\n ) : null}\n\n <span\n className={cn(\n \"min-w-0 flex-1 break-all whitespace-pre-wrap\",\n line.level && LEVEL_STYLES[line.level],\n )}\n >\n {parts.map((part, index) =>\n part.match ? (\n // A mark element, so the match is conveyed structurally rather\n // than only as a background colour.\n <mark key={index} className=\"rounded-[2px] bg-warning/35 text-inherit\">\n {part.text}\n </mark>\n ) : (\n <span key={index}>{part.text}</span>\n ),\n )}\n </span>\n\n {hasFields ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-expand\"\n aria-expanded={expanded}\n onClick={onToggle}\n className={cn(\n \"shrink-0 rounded px-1 text-2xs text-muted-foreground\",\n \"transition-colors hover:text-foreground\",\n focusRing,\n disabledStyles,\n )}\n >\n {expanded ? \"Hide fields\" : \"Fields\"}\n </button>\n ) : null}\n </div>\n\n {expanded && line.fields ? (\n <dl\n data-slot=\"log-viewer-fields\"\n className=\"ms-4 mt-1 mb-1 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 border-s border-border ps-3\"\n >\n {Object.entries(line.fields).map(([key, value]) => (\n <div key={key} className=\"contents\">\n <dt className=\"text-muted-foreground\">{key}</dt>\n <dd className=\"m-0 break-all\">\n {typeof value === \"string\" ? value : JSON.stringify(value)}\n </dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n );\n}\n\nexport interface LogViewerToolbarProps extends Omit<ComponentPropsWithRef<\"div\">, \"onChange\"> {\n query: string;\n onQueryChange: (query: string) => void;\n regex: boolean;\n onRegexChange: (regex: boolean) => void;\n levels: Set<LogLevel>;\n onToggleLevel: (level: LogLevel) => void;\n counts: Map<LogLevel, number>;\n invalidPattern?: boolean;\n /** Shown as \"N of M lines\". */\n showing: number;\n total: number;\n}\n\nexport function LogViewerToolbar({\n className,\n query,\n onQueryChange,\n regex,\n onRegexChange,\n levels,\n onToggleLevel,\n counts,\n invalidPattern = false,\n showing,\n total,\n ...props\n}: LogViewerToolbarProps) {\n const filterId = useId();\n const errorId = useId();\n\n return (\n <div\n data-slot=\"log-viewer-toolbar\"\n className={cn(\"flex flex-wrap items-center gap-2\", className)}\n {...props}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <label htmlFor={filterId} className=\"sr-only\">\n Filter log\n </label>\n <input\n id={filterId}\n type=\"search\"\n value={query}\n placeholder={regex ? \"Pattern…\" : \"Filter…\"}\n aria-invalid={invalidPattern || undefined}\n aria-describedby={invalidPattern ? errorId : undefined}\n onChange={(event) => {\n onQueryChange(event.target.value);\n }}\n className={cn(\n \"min-w-32 flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm\",\n invalidPattern && \"border-destructive\",\n focusRing,\n )}\n />\n <label className=\"flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <input\n type=\"checkbox\"\n checked={regex}\n onChange={(event) => {\n onRegexChange(event.target.checked);\n }}\n className={cn(\"size-3.5 rounded border-input\", focusRing)}\n />\n Regex\n </label>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-1\">\n {LOG_LEVELS.filter((level) => counts.has(level)).map((level) => {\n const on = levels.size === 0 || levels.has(level);\n return (\n <button\n key={level}\n type=\"button\"\n data-slot=\"log-viewer-facet\"\n aria-pressed={on}\n onClick={() => {\n onToggleLevel(level);\n }}\n className={cn(\n \"rounded-md border px-1.5 py-0.5 font-mono text-2xs uppercase transition-colors\",\n on\n ? \"border-transparent bg-secondary text-secondary-foreground\"\n : \"border-input bg-background text-muted-foreground line-through\",\n focusRing,\n )}\n >\n {level} {counts.get(level) ?? 0}\n </button>\n );\n })}\n </div>\n\n <p className=\"w-full text-xs text-muted-foreground tabular-nums\">\n {invalidPattern ? (\n // Said out loud rather than shown as an empty log, which reads as\n // \"nothing matched\" and sends the reader looking for the wrong thing.\n <span id={errorId} className=\"text-destructive\">\n Incomplete pattern — showing everything until it is valid\n </span>\n ) : (\n `${String(showing)} of ${String(total)} lines`\n )}\n </p>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -78,7 +78,7 @@ function Meter({ className, segments, max, label, warnAt = .9, threshold, format
78
78
  return /* @__PURE__ */ jsx("div", {
79
79
  "data-slot": "meter-segment",
80
80
  "data-tone": segment.tone ?? "primary",
81
- className: cn("h-full first:rounded-l-full last:rounded-r-full", "transition-[width] duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]", TONES[segment.tone ?? "primary"]),
81
+ className: cn("h-full first:rounded-s-full last:rounded-e-full", "transition-[width] duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]", TONES[segment.tone ?? "primary"]),
82
82
  style: { width: `${String(value / denominator * 100)}%` }
83
83
  }, segment.id);
84
84
  })
@@ -1 +1 @@
1
- {"version":3,"file":"meter.js","names":[],"sources":["../../../src/components/meter/meter.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useMemo, type ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A measurement against a known capacity: storage against a quota, seats\n * against a plan, spend against a budget.\n *\n * Not a Progress. Progress reports how far along a task is and is expected to\n * reach the end; a meter reports a level that has no reason to move at all, and\n * where being near the top is the thing worth noticing. They carry different\n * ARIA roles for exactly that reason, and screen readers phrase them\n * differently.\n *\n * The bar is a single `role=\"meter\"` and the segments inside it are decoration.\n * Making each segment its own widget would put five unlabelled meters in the\n * tab order to describe one quantity; the legend carries the per-segment detail\n * as text instead, where it can actually be read.\n */\n\nconst TONES = {\n primary: \"bg-primary\",\n info: \"bg-info\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n destructive: \"bg-destructive\",\n neutral: \"bg-border-strong\",\n} as const;\n\nexport type MeterTone = keyof typeof TONES;\n\nexport interface MeterSegment {\n /** Stable identity, so a re-order does not re-animate every segment. */\n id: string;\n /** Named in the legend and in the accessible summary. */\n label: string;\n value: number;\n tone?: MeterTone;\n}\n\ninterface MeterContextValue {\n segments: MeterSegment[];\n total: number;\n max: number;\n format: (value: number) => string;\n over: boolean;\n}\n\nconst MeterContext = createContext<MeterContextValue | null>(null);\n\nfunction useMeterContext(component: string): MeterContextValue {\n const context = useContext(MeterContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <Meter>.`);\n }\n return context;\n}\n\nfunction defaultFormat(value: number): string {\n return new Intl.NumberFormat().format(value);\n}\n\nexport interface MeterProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** Parts making up the measurement. One segment is the ordinary case. */\n segments: MeterSegment[];\n /** The capacity being measured against. */\n max: number;\n /** Names the meter. Required — an unnamed measurement is unreadable. */\n label: string;\n /**\n * Fraction of `max` past which the meter reports strain, between 0 and 1.\n * Presentation only: it never changes the reported value.\n */\n warnAt?: number;\n /** Optional marker on the track, for a soft limit or an alert threshold. */\n threshold?: { value: number; label: string };\n /** Formats every number that reaches the reader. */\n format?: (value: number) => string;\n /** Shown after the bar. Compose `MeterLegend` here, or your own summary. */\n children?: React.ReactNode;\n}\n\nexport function Meter({\n className,\n segments,\n max,\n label,\n warnAt = 0.9,\n threshold,\n format = defaultFormat,\n children,\n ...props\n}: MeterProps) {\n const total = segments.reduce((sum, segment) => sum + Math.max(0, segment.value), 0);\n const over = total > max;\n const strained = max > 0 && total / max >= warnAt;\n\n // Widths are a share of capacity, not of the total, so a half-full meter\n // renders half full. Dividing by the total instead would make every meter\n // look completely full, which is the usual bug in hand-rolled versions.\n const denominator = over ? total : max;\n\n const context = useMemo<MeterContextValue>(\n () => ({ segments, total, max, format, over }),\n [segments, total, max, format, over],\n );\n\n return (\n <MeterContext.Provider value={context}>\n <div\n data-slot=\"meter\"\n data-over={over || undefined}\n data-strained={strained || undefined}\n className={cn(\"flex w-full flex-col gap-1.5\", className)}\n {...props}\n >\n <div\n role=\"meter\"\n aria-label={label}\n aria-valuenow={total}\n aria-valuemin={0}\n aria-valuemax={max}\n // Without this a screen reader announces a bare number with no unit\n // and no capacity — \"4\" tells the reader nothing about 4 of what.\n aria-valuetext={`${format(total)} of ${format(max)}${over ? \", over capacity\" : \"\"}`}\n data-slot=\"meter-track\"\n className=\"relative h-2 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div className=\"flex h-full w-full\">\n {segments.map((segment) => {\n const value = Math.max(0, segment.value);\n // A zero segment renders nothing at all. A one-pixel sliver reads\n // as a small amount rather than as none.\n if (value === 0 || denominator === 0) return null;\n\n return (\n <div\n key={segment.id}\n data-slot=\"meter-segment\"\n data-tone={segment.tone ?? \"primary\"}\n className={cn(\n \"h-full first:rounded-l-full last:rounded-r-full\",\n \"transition-[width] duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]\",\n TONES[segment.tone ?? \"primary\"],\n )}\n style={{ width: `${String((value / denominator) * 100)}%` }}\n />\n );\n })}\n </div>\n\n {threshold && max > 0 && threshold.value > 0 && threshold.value < max ? (\n <div\n data-slot=\"meter-threshold\"\n // The marker repeats what the legend states in words, so it is\n // decoration. Announcing it would interrupt the value.\n aria-hidden=\"true\"\n className=\"absolute inset-y-0 w-px bg-foreground/45\"\n style={{ left: `${String((threshold.value / denominator) * 100)}%` }}\n />\n ) : null}\n </div>\n\n {children}\n </div>\n </MeterContext.Provider>\n );\n}\n\n/**\n * The per-segment breakdown, as text.\n *\n * This is where the detail lives. The bar communicates one number; anyone who\n * needs to know that images account for most of it reads it here.\n */\nexport function MeterLegend({ className, ...props }: ComponentPropsWithRef<\"ul\">) {\n const { segments, format } = useMeterContext(\"MeterLegend\");\n\n return (\n <ul\n data-slot=\"meter-legend\"\n className={cn(\"flex flex-wrap gap-x-4 gap-y-1 text-xs\", className)}\n {...props}\n >\n {segments.map((segment) => (\n <li key={segment.id} className=\"flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className={cn(\"size-2 shrink-0 rounded-full\", TONES[segment.tone ?? \"primary\"])}\n />\n <span className=\"text-muted-foreground\">{segment.label}</span>\n <span className=\"tabular-nums\">{format(segment.value)}</span>\n </li>\n ))}\n </ul>\n );\n}\n\n/** The headline \"4.2 GB of 10 GB\" line, with an over-capacity state. */\nexport function MeterValue({ className, ...props }: ComponentPropsWithRef<\"p\">) {\n const { total, max, format, over } = useMeterContext(\"MeterValue\");\n\n return (\n <p\n data-slot=\"meter-value\"\n className={cn(\n \"text-xs tabular-nums\",\n over ? \"text-destructive\" : \"text-muted-foreground\",\n className,\n )}\n {...props}\n >\n {format(total)} of {format(max)}\n {over ? \" — over capacity\" : null}\n </p>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"meter.js","names":[],"sources":["../../../src/components/meter/meter.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useMemo, type ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A measurement against a known capacity: storage against a quota, seats\n * against a plan, spend against a budget.\n *\n * Not a Progress. Progress reports how far along a task is and is expected to\n * reach the end; a meter reports a level that has no reason to move at all, and\n * where being near the top is the thing worth noticing. They carry different\n * ARIA roles for exactly that reason, and screen readers phrase them\n * differently.\n *\n * The bar is a single `role=\"meter\"` and the segments inside it are decoration.\n * Making each segment its own widget would put five unlabelled meters in the\n * tab order to describe one quantity; the legend carries the per-segment detail\n * as text instead, where it can actually be read.\n */\n\nconst TONES = {\n primary: \"bg-primary\",\n info: \"bg-info\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n destructive: \"bg-destructive\",\n neutral: \"bg-border-strong\",\n} as const;\n\nexport type MeterTone = keyof typeof TONES;\n\nexport interface MeterSegment {\n /** Stable identity, so a re-order does not re-animate every segment. */\n id: string;\n /** Named in the legend and in the accessible summary. */\n label: string;\n value: number;\n tone?: MeterTone;\n}\n\ninterface MeterContextValue {\n segments: MeterSegment[];\n total: number;\n max: number;\n format: (value: number) => string;\n over: boolean;\n}\n\nconst MeterContext = createContext<MeterContextValue | null>(null);\n\nfunction useMeterContext(component: string): MeterContextValue {\n const context = useContext(MeterContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <Meter>.`);\n }\n return context;\n}\n\nfunction defaultFormat(value: number): string {\n return new Intl.NumberFormat().format(value);\n}\n\nexport interface MeterProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** Parts making up the measurement. One segment is the ordinary case. */\n segments: MeterSegment[];\n /** The capacity being measured against. */\n max: number;\n /** Names the meter. Required — an unnamed measurement is unreadable. */\n label: string;\n /**\n * Fraction of `max` past which the meter reports strain, between 0 and 1.\n * Presentation only: it never changes the reported value.\n */\n warnAt?: number;\n /** Optional marker on the track, for a soft limit or an alert threshold. */\n threshold?: { value: number; label: string };\n /** Formats every number that reaches the reader. */\n format?: (value: number) => string;\n /** Shown after the bar. Compose `MeterLegend` here, or your own summary. */\n children?: React.ReactNode;\n}\n\nexport function Meter({\n className,\n segments,\n max,\n label,\n warnAt = 0.9,\n threshold,\n format = defaultFormat,\n children,\n ...props\n}: MeterProps) {\n const total = segments.reduce((sum, segment) => sum + Math.max(0, segment.value), 0);\n const over = total > max;\n const strained = max > 0 && total / max >= warnAt;\n\n // Widths are a share of capacity, not of the total, so a half-full meter\n // renders half full. Dividing by the total instead would make every meter\n // look completely full, which is the usual bug in hand-rolled versions.\n const denominator = over ? total : max;\n\n const context = useMemo<MeterContextValue>(\n () => ({ segments, total, max, format, over }),\n [segments, total, max, format, over],\n );\n\n return (\n <MeterContext.Provider value={context}>\n <div\n data-slot=\"meter\"\n data-over={over || undefined}\n data-strained={strained || undefined}\n className={cn(\"flex w-full flex-col gap-1.5\", className)}\n {...props}\n >\n <div\n role=\"meter\"\n aria-label={label}\n aria-valuenow={total}\n aria-valuemin={0}\n aria-valuemax={max}\n // Without this a screen reader announces a bare number with no unit\n // and no capacity — \"4\" tells the reader nothing about 4 of what.\n aria-valuetext={`${format(total)} of ${format(max)}${over ? \", over capacity\" : \"\"}`}\n data-slot=\"meter-track\"\n className=\"relative h-2 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div className=\"flex h-full w-full\">\n {segments.map((segment) => {\n const value = Math.max(0, segment.value);\n // A zero segment renders nothing at all. A one-pixel sliver reads\n // as a small amount rather than as none.\n if (value === 0 || denominator === 0) return null;\n\n return (\n <div\n key={segment.id}\n data-slot=\"meter-segment\"\n data-tone={segment.tone ?? \"primary\"}\n className={cn(\n \"h-full first:rounded-s-full last:rounded-e-full\",\n \"transition-[width] duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]\",\n TONES[segment.tone ?? \"primary\"],\n )}\n style={{ width: `${String((value / denominator) * 100)}%` }}\n />\n );\n })}\n </div>\n\n {threshold && max > 0 && threshold.value > 0 && threshold.value < max ? (\n <div\n data-slot=\"meter-threshold\"\n // The marker repeats what the legend states in words, so it is\n // decoration. Announcing it would interrupt the value.\n aria-hidden=\"true\"\n className=\"absolute inset-y-0 w-px bg-foreground/45\"\n style={{ left: `${String((threshold.value / denominator) * 100)}%` }}\n />\n ) : null}\n </div>\n\n {children}\n </div>\n </MeterContext.Provider>\n );\n}\n\n/**\n * The per-segment breakdown, as text.\n *\n * This is where the detail lives. The bar communicates one number; anyone who\n * needs to know that images account for most of it reads it here.\n */\nexport function MeterLegend({ className, ...props }: ComponentPropsWithRef<\"ul\">) {\n const { segments, format } = useMeterContext(\"MeterLegend\");\n\n return (\n <ul\n data-slot=\"meter-legend\"\n className={cn(\"flex flex-wrap gap-x-4 gap-y-1 text-xs\", className)}\n {...props}\n >\n {segments.map((segment) => (\n <li key={segment.id} className=\"flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className={cn(\"size-2 shrink-0 rounded-full\", TONES[segment.tone ?? \"primary\"])}\n />\n <span className=\"text-muted-foreground\">{segment.label}</span>\n <span className=\"tabular-nums\">{format(segment.value)}</span>\n </li>\n ))}\n </ul>\n );\n}\n\n/** The headline \"4.2 GB of 10 GB\" line, with an over-capacity state. */\nexport function MeterValue({ className, ...props }: ComponentPropsWithRef<\"p\">) {\n const { total, max, format, over } = useMeterContext(\"MeterValue\");\n\n return (\n <p\n data-slot=\"meter-value\"\n className={cn(\n \"text-xs tabular-nums\",\n over ? \"text-destructive\" : \"text-muted-foreground\",\n className,\n )}\n {...props}\n >\n {format(total)} of {format(max)}\n {over ? \" — over capacity\" : null}\n </p>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsBA,MAAM,QAAQ;CACZ,SAAS;CACT,MAAM;CACN,SAAS;CACT,SAAS;CACT,aAAa;CACb,SAAS;AACX;AAqBA,MAAM,eAAe,cAAwC,IAAI;AAEjE,SAAS,gBAAgB,WAAsC;CAC7D,MAAM,UAAU,WAAW,YAAY;CACvC,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,GAAG,UAAU,kCAAkC;CAEjE,OAAO;AACT;AAEA,SAAS,cAAc,OAAuB;CAC5C,OAAO,IAAI,KAAK,aAAa,CAAC,CAAC,OAAO,KAAK;AAC7C;AAsBA,SAAgB,MAAM,EACpB,WACA,UACA,KACA,OACA,SAAS,IACT,WACA,SAAS,eACT,UACA,GAAG,SACU;CACb,MAAM,QAAQ,SAAS,QAAQ,KAAK,YAAY,MAAM,KAAK,IAAI,GAAG,QAAQ,KAAK,GAAG,CAAC;CACnF,MAAM,OAAO,QAAQ;CACrB,MAAM,WAAW,MAAM,KAAK,QAAQ,OAAO;CAK3C,MAAM,cAAc,OAAO,QAAQ;CAEnC,MAAM,UAAU,eACP;EAAE;EAAU;EAAO;EAAK;EAAQ;CAAK,IAC5C;EAAC;EAAU;EAAO;EAAK;EAAQ;CAAI,CACrC;CAEA,OACE,oBAAC,aAAa,UAAd;EAAuB,OAAO;EAC5B,UAAA,qBAAC,OAAD;GACE,aAAU;GACV,aAAW,QAAQ,KAAA;GACnB,iBAAe,YAAY,KAAA;GAC3B,WAAW,GAAG,gCAAgC,SAAS;GACvD,GAAI;GALN,UAAA,CAOE,qBAAC,OAAD;IACE,MAAK;IACL,cAAY;IACZ,iBAAe;IACf,iBAAe;IACf,iBAAe;IAGf,kBAAgB,GAAG,OAAO,KAAK,EAAE,MAAM,OAAO,GAAG,IAAI,OAAO,oBAAoB;IAChF,aAAU;IACV,WAAU;IAVZ,UAAA,CAYE,oBAAC,OAAD;KAAK,WAAU;KACZ,UAAA,SAAS,KAAK,YAAY;MACzB,MAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,KAAK;MAGvC,IAAI,UAAU,KAAK,gBAAgB,GAAG,OAAO;MAE7C,OACE,oBAAC,OAAD;OAEE,aAAU;OACV,aAAW,QAAQ,QAAQ;OAC3B,WAAW,GACT,mDACA,mFACA,MAAM,QAAQ,QAAQ,UACxB;OACA,OAAO,EAAE,OAAO,GAAG,OAAQ,QAAQ,cAAe,GAAG,EAAE,GAAG;MAC3D,GATM,QAAQ,EASd;KAEL,CAAC;IACE,CAAA,GAEJ,aAAa,MAAM,KAAK,UAAU,QAAQ,KAAK,UAAU,QAAQ,MAChE,oBAAC,OAAD;KACE,aAAU;KAGV,eAAY;KACZ,WAAU;KACV,OAAO,EAAE,MAAM,GAAG,OAAQ,UAAU,QAAQ,cAAe,GAAG,EAAE,GAAG;IACpE,CAAA,IACC,IACD;GAEJ,CAAA,GAAA,QACE;;CACgB,CAAA;AAE3B;;;;;;;AAQA,SAAgB,YAAY,EAAE,WAAW,GAAG,SAAsC;CAChF,MAAM,EAAE,UAAU,WAAW,gBAAgB,aAAa;CAE1D,OACE,oBAAC,MAAD;EACE,aAAU;EACV,WAAW,GAAG,0CAA0C,SAAS;EACjE,GAAI;EAEH,UAAA,SAAS,KAAK,YACb,qBAAC,MAAD;GAAqB,WAAU;GAA/B,UAAA;IACE,oBAAC,QAAD;KACE,eAAY;KACZ,WAAW,GAAG,gCAAgC,MAAM,QAAQ,QAAQ,UAAU;IAC/E,CAAA;IACD,oBAAC,QAAD;KAAM,WAAU;KAAyB,UAAA,QAAQ;IAAY,CAAA;IAC7D,oBAAC,QAAD;KAAM,WAAU;KAAgB,UAAA,OAAO,QAAQ,KAAK;IAAQ,CAAA;GAC1D;EAPK,GAAA,QAAQ,EAOb,CACL;CACC,CAAA;AAER;;AAGA,SAAgB,WAAW,EAAE,WAAW,GAAG,SAAqC;CAC9E,MAAM,EAAE,OAAO,KAAK,QAAQ,SAAS,gBAAgB,YAAY;CAEjE,OACE,qBAAC,KAAD;EACE,aAAU;EACV,WAAW,GACT,wBACA,OAAO,qBAAqB,yBAC5B,SACF;EACA,GAAI;EAPN,UAAA;GASG,OAAO,KAAK;GAAE;GAAK,OAAO,GAAG;GAC7B,OAAO,qBAAqB;EAC5B;;AAEP"}
@@ -1 +1 @@
1
- {"version":3,"file":"pagination.d.ts","names":[],"sources":["../../../src/components/pagination/pagination.tsx"],"mappings":";;;;;;;;;;iBAcgB,aAAa,cAAc,SAAS,+CAA4B,IAAA;iBAahE,oBAAoB,cAAc,SAAS,8CAA2B,IAAA;iBAUtE,iBAAiB,cAAc,SAAS,8CAA2B,IAAA;UAIlE,4BAA4B;;EAE3C;EACA;EACA;;iBAGc,iBACd,WACA,UACA,MACA,YACG,SACF,sCAAmB,IAAA;iBAoBN,qBAAqB,cAAc,SAAS,sCAAmB,IAAA;iBAsB/D,iBAAiB,cAAc,SAAS,sCAAmB,IAAA;;;;;;;;iBA6B3D,qBAAqB,cAAc,SAAS,gDAA6B,IAAA"}
1
+ {"version":3,"file":"pagination.d.ts","names":[],"sources":["../../../src/components/pagination/pagination.tsx"],"mappings":";;;;;;;;;;iBAegB,aAAa,cAAc,SAAS,+CAA4B,IAAA;iBAahE,oBAAoB,cAAc,SAAS,8CAA2B,IAAA;iBAUtE,iBAAiB,cAAc,SAAS,8CAA2B,IAAA;UAIlE,4BAA4B;;EAE3C;EACA;EACA;;iBAGc,iBACd,WACA,UACA,MACA,YACG,SACF,sCAAmB,IAAA;iBAoBN,qBAAqB,cAAc,SAAS,sCAAmB,IAAA;iBA2B/D,iBAAiB,cAAc,SAAS,sCAAmB,IAAA;;;;;;;;iBAkC3D,qBAAqB,cAAc,SAAS,gDAA6B,IAAA"}
@@ -1,3 +1,4 @@
1
+ import { mirrorForDirection } from "../../lib/styles.js";
1
2
  import { cn } from "../../lib/utils.js";
2
3
  import { buttonVariants } from "../button/button.js";
3
4
  import { Slot } from "radix-ui";
@@ -56,7 +57,7 @@ function PaginationPrevious({ className, ...props }) {
56
57
  viewBox: "0 0 24 24",
57
58
  fill: "none",
58
59
  "aria-hidden": "true",
59
- className: "size-4",
60
+ className: cn("size-4", mirrorForDirection),
60
61
  children: /* @__PURE__ */ jsx("path", {
61
62
  d: "m15 18-6-6 6-6",
62
63
  stroke: "currentColor",
@@ -83,7 +84,7 @@ function PaginationNext({ className, ...props }) {
83
84
  viewBox: "0 0 24 24",
84
85
  fill: "none",
85
86
  "aria-hidden": "true",
86
- className: "size-4",
87
+ className: cn("size-4", mirrorForDirection),
87
88
  children: /* @__PURE__ */ jsx("path", {
88
89
  d: "m9 18 6-6-6-6",
89
90
  stroke: "currentColor",
@@ -1 +1 @@
1
- {"version":3,"file":"pagination.js","names":[],"sources":["../../../src/components/pagination/pagination.tsx"],"sourcesContent":["import { Slot } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { buttonVariants } from \"@/components/button\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Navigation between pages of a list.\n *\n * Rendered as links by default, not buttons: a page of results is a place, so\n * it should be shareable, openable in a new tab and part of history. Use\n * `asChild` to hand off to your router's link. For pagination that only changes\n * client state, render buttons instead — `PaginationLink` accepts either.\n */\nexport function Pagination({ className, ...props }: ComponentPropsWithRef<\"nav\">) {\n return (\n <nav\n // A named landmark, so it can be jumped to and is distinguishable from\n // the page's main navigation.\n aria-label=\"Pagination\"\n data-slot=\"pagination\"\n className={cn(\"mx-auto flex w-full justify-center\", className)}\n {...props}\n />\n );\n}\n\nexport function PaginationContent({ className, ...props }: ComponentPropsWithRef<\"ul\">) {\n return (\n <ul\n data-slot=\"pagination-content\"\n className={cn(\"flex flex-row items-center gap-1\", className)}\n {...props}\n />\n );\n}\n\nexport function PaginationItem({ className, ...props }: ComponentPropsWithRef<\"li\">) {\n return <li data-slot=\"pagination-item\" className={cn(className)} {...props} />;\n}\n\nexport interface PaginationLinkProps extends ComponentPropsWithRef<\"a\"> {\n /** Marks the page the user is on. Sets aria-current=\"page\". */\n isActive?: boolean;\n size?: \"sm\" | \"md\" | \"icon\" | \"icon-sm\";\n asChild?: boolean;\n}\n\nexport function PaginationLink({\n className,\n isActive,\n size = \"icon\",\n asChild,\n ...props\n}: PaginationLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n\n return (\n <Comp\n data-slot=\"pagination-link\"\n // aria-current is what tells a screen reader user where they are; styling\n // the active page differently does nothing on its own.\n aria-current={isActive ? \"page\" : undefined}\n data-active={isActive || undefined}\n className={cn(\n buttonVariants({ variant: isActive ? \"outline\" : \"ghost\", size }),\n isActive && \"border-border-strong font-medium\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function PaginationPrevious({ className, ...props }: PaginationLinkProps) {\n return (\n <PaginationLink\n aria-label=\"Go to previous page\"\n size=\"sm\"\n className={cn(\"gap-1 px-2.5\", className)}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-4\">\n <path\n d=\"m15 18-6-6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n <span className=\"hidden sm:inline\">Previous</span>\n </PaginationLink>\n );\n}\n\nexport function PaginationNext({ className, ...props }: PaginationLinkProps) {\n return (\n <PaginationLink\n aria-label=\"Go to next page\"\n size=\"sm\"\n className={cn(\"gap-1 px-2.5\", className)}\n {...props}\n >\n <span className=\"hidden sm:inline\">Next</span>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-4\">\n <path\n d=\"m9 18 6-6-6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </PaginationLink>\n );\n}\n\n/**\n * Stands in for a run of skipped pages.\n *\n * Hidden from assistive technology and given screen-reader text, because \"…\"\n * is announced as nothing useful and the gap is already implied by the page\n * numbers either side.\n */\nexport function PaginationEllipsis({ className, ...props }: ComponentPropsWithRef<\"span\">) {\n return (\n <span\n data-slot=\"pagination-ellipsis\"\n aria-hidden=\"true\"\n className={cn(\"grid size-9 place-items-center text-muted-foreground\", className)}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"size-4\">\n <circle cx=\"5\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n <circle cx=\"12\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n <circle cx=\"19\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n </svg>\n <span className=\"sr-only\">More pages</span>\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAgB,WAAW,EAAE,WAAW,GAAG,SAAuC;CAChF,OACE,oBAAC,OAAD;EAGE,cAAW;EACX,aAAU;EACV,WAAW,GAAG,sCAAsC,SAAS;EAC7D,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,kBAAkB,EAAE,WAAW,GAAG,SAAsC;CACtF,OACE,oBAAC,MAAD;EACE,aAAU;EACV,WAAW,GAAG,oCAAoC,SAAS;EAC3D,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,eAAe,EAAE,WAAW,GAAG,SAAsC;CACnF,OAAO,oBAAC,MAAD;EAAI,aAAU;EAAkB,WAAW,GAAG,SAAS;EAAG,GAAI;CAAQ,CAAA;AAC/E;AASA,SAAgB,eAAe,EAC7B,WACA,UACA,OAAO,QACP,SACA,GAAG,SACmB;CACtB,MAAM,OAAO,UAAU,KAAK,OAAO;CAEnC,OACE,oBAAC,MAAD;EACE,aAAU;EAGV,gBAAc,WAAW,SAAS,KAAA;EAClC,eAAa,YAAY,KAAA;EACzB,WAAW,GACT,eAAe;GAAE,SAAS,WAAW,YAAY;GAAS;EAAK,CAAC,GAChE,YAAY,oCACZ,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,mBAAmB,EAAE,WAAW,GAAG,SAA8B;CAC/E,OACE,qBAAC,gBAAD;EACE,cAAW;EACX,MAAK;EACL,WAAW,GAAG,gBAAgB,SAAS;EACvC,GAAI;EAJN,UAAA,CAME,oBAAC,OAAD;GAAK,SAAQ;GAAY,MAAK;GAAO,eAAY;GAAO,WAAU;GAChE,UAAA,oBAAC,QAAD;IACE,GAAE;IACF,QAAO;IACP,aAAY;IACZ,eAAc;IACd,gBAAe;GAChB,CAAA;EACE,CAAA,GACL,oBAAC,QAAD;GAAM,WAAU;GAAmB,UAAA;EAAc,CAAA,CACnC;;AAEpB;AAEA,SAAgB,eAAe,EAAE,WAAW,GAAG,SAA8B;CAC3E,OACE,qBAAC,gBAAD;EACE,cAAW;EACX,MAAK;EACL,WAAW,GAAG,gBAAgB,SAAS;EACvC,GAAI;EAJN,UAAA,CAME,oBAAC,QAAD;GAAM,WAAU;GAAmB,UAAA;EAAU,CAAA,GAC7C,oBAAC,OAAD;GAAK,SAAQ;GAAY,MAAK;GAAO,eAAY;GAAO,WAAU;GAChE,UAAA,oBAAC,QAAD;IACE,GAAE;IACF,QAAO;IACP,aAAY;IACZ,eAAc;IACd,gBAAe;GAChB,CAAA;EACE,CAAA,CACS;;AAEpB;;;;;;;;AASA,SAAgB,mBAAmB,EAAE,WAAW,GAAG,SAAwC;CACzF,OACE,qBAAC,QAAD;EACE,aAAU;EACV,eAAY;EACZ,WAAW,GAAG,wDAAwD,SAAS;EAC/E,GAAI;EAJN,UAAA,CAME,qBAAC,OAAD;GAAK,SAAQ;GAAY,MAAK;GAAO,WAAU;GAA/C,UAAA;IACE,oBAAC,UAAD;KAAQ,IAAG;KAAI,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;IACpD,oBAAC,UAAD;KAAQ,IAAG;KAAK,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;IACrD,oBAAC,UAAD;KAAQ,IAAG;KAAK,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;GAClD;EACL,CAAA,GAAA,oBAAC,QAAD;GAAM,WAAU;GAAU,UAAA;EAAgB,CAAA,CACtC;;AAEV"}
1
+ {"version":3,"file":"pagination.js","names":[],"sources":["../../../src/components/pagination/pagination.tsx"],"sourcesContent":["import { Slot } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { buttonVariants } from \"@/components/button\";\nimport { mirrorForDirection } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Navigation between pages of a list.\n *\n * Rendered as links by default, not buttons: a page of results is a place, so\n * it should be shareable, openable in a new tab and part of history. Use\n * `asChild` to hand off to your router's link. For pagination that only changes\n * client state, render buttons instead — `PaginationLink` accepts either.\n */\nexport function Pagination({ className, ...props }: ComponentPropsWithRef<\"nav\">) {\n return (\n <nav\n // A named landmark, so it can be jumped to and is distinguishable from\n // the page's main navigation.\n aria-label=\"Pagination\"\n data-slot=\"pagination\"\n className={cn(\"mx-auto flex w-full justify-center\", className)}\n {...props}\n />\n );\n}\n\nexport function PaginationContent({ className, ...props }: ComponentPropsWithRef<\"ul\">) {\n return (\n <ul\n data-slot=\"pagination-content\"\n className={cn(\"flex flex-row items-center gap-1\", className)}\n {...props}\n />\n );\n}\n\nexport function PaginationItem({ className, ...props }: ComponentPropsWithRef<\"li\">) {\n return <li data-slot=\"pagination-item\" className={cn(className)} {...props} />;\n}\n\nexport interface PaginationLinkProps extends ComponentPropsWithRef<\"a\"> {\n /** Marks the page the user is on. Sets aria-current=\"page\". */\n isActive?: boolean;\n size?: \"sm\" | \"md\" | \"icon\" | \"icon-sm\";\n asChild?: boolean;\n}\n\nexport function PaginationLink({\n className,\n isActive,\n size = \"icon\",\n asChild,\n ...props\n}: PaginationLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n\n return (\n <Comp\n data-slot=\"pagination-link\"\n // aria-current is what tells a screen reader user where they are; styling\n // the active page differently does nothing on its own.\n aria-current={isActive ? \"page\" : undefined}\n data-active={isActive || undefined}\n className={cn(\n buttonVariants({ variant: isActive ? \"outline\" : \"ghost\", size }),\n isActive && \"border-border-strong font-medium\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function PaginationPrevious({ className, ...props }: PaginationLinkProps) {\n return (\n <PaginationLink\n aria-label=\"Go to previous page\"\n size=\"sm\"\n className={cn(\"gap-1 px-2.5\", className)}\n {...props}\n >\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={cn(\"size-4\", mirrorForDirection)}\n >\n <path\n d=\"m15 18-6-6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n <span className=\"hidden sm:inline\">Previous</span>\n </PaginationLink>\n );\n}\n\nexport function PaginationNext({ className, ...props }: PaginationLinkProps) {\n return (\n <PaginationLink\n aria-label=\"Go to next page\"\n size=\"sm\"\n className={cn(\"gap-1 px-2.5\", className)}\n {...props}\n >\n <span className=\"hidden sm:inline\">Next</span>\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={cn(\"size-4\", mirrorForDirection)}\n >\n <path\n d=\"m9 18 6-6-6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </PaginationLink>\n );\n}\n\n/**\n * Stands in for a run of skipped pages.\n *\n * Hidden from assistive technology and given screen-reader text, because \"…\"\n * is announced as nothing useful and the gap is already implied by the page\n * numbers either side.\n */\nexport function PaginationEllipsis({ className, ...props }: ComponentPropsWithRef<\"span\">) {\n return (\n <span\n data-slot=\"pagination-ellipsis\"\n aria-hidden=\"true\"\n className={cn(\"grid size-9 place-items-center text-muted-foreground\", className)}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"size-4\">\n <circle cx=\"5\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n <circle cx=\"12\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n <circle cx=\"19\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n </svg>\n <span className=\"sr-only\">More pages</span>\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,SAAgB,WAAW,EAAE,WAAW,GAAG,SAAuC;CAChF,OACE,oBAAC,OAAD;EAGE,cAAW;EACX,aAAU;EACV,WAAW,GAAG,sCAAsC,SAAS;EAC7D,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,kBAAkB,EAAE,WAAW,GAAG,SAAsC;CACtF,OACE,oBAAC,MAAD;EACE,aAAU;EACV,WAAW,GAAG,oCAAoC,SAAS;EAC3D,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,eAAe,EAAE,WAAW,GAAG,SAAsC;CACnF,OAAO,oBAAC,MAAD;EAAI,aAAU;EAAkB,WAAW,GAAG,SAAS;EAAG,GAAI;CAAQ,CAAA;AAC/E;AASA,SAAgB,eAAe,EAC7B,WACA,UACA,OAAO,QACP,SACA,GAAG,SACmB;CACtB,MAAM,OAAO,UAAU,KAAK,OAAO;CAEnC,OACE,oBAAC,MAAD;EACE,aAAU;EAGV,gBAAc,WAAW,SAAS,KAAA;EAClC,eAAa,YAAY,KAAA;EACzB,WAAW,GACT,eAAe;GAAE,SAAS,WAAW,YAAY;GAAS;EAAK,CAAC,GAChE,YAAY,oCACZ,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,mBAAmB,EAAE,WAAW,GAAG,SAA8B;CAC/E,OACE,qBAAC,gBAAD;EACE,cAAW;EACX,MAAK;EACL,WAAW,GAAG,gBAAgB,SAAS;EACvC,GAAI;EAJN,UAAA,CAME,oBAAC,OAAD;GACE,SAAQ;GACR,MAAK;GACL,eAAY;GACZ,WAAW,GAAG,UAAU,kBAAkB;GAE1C,UAAA,oBAAC,QAAD;IACE,GAAE;IACF,QAAO;IACP,aAAY;IACZ,eAAc;IACd,gBAAe;GAChB,CAAA;EACE,CAAA,GACL,oBAAC,QAAD;GAAM,WAAU;GAAmB,UAAA;EAAc,CAAA,CACnC;;AAEpB;AAEA,SAAgB,eAAe,EAAE,WAAW,GAAG,SAA8B;CAC3E,OACE,qBAAC,gBAAD;EACE,cAAW;EACX,MAAK;EACL,WAAW,GAAG,gBAAgB,SAAS;EACvC,GAAI;EAJN,UAAA,CAME,oBAAC,QAAD;GAAM,WAAU;GAAmB,UAAA;EAAU,CAAA,GAC7C,oBAAC,OAAD;GACE,SAAQ;GACR,MAAK;GACL,eAAY;GACZ,WAAW,GAAG,UAAU,kBAAkB;GAE1C,UAAA,oBAAC,QAAD;IACE,GAAE;IACF,QAAO;IACP,aAAY;IACZ,eAAc;IACd,gBAAe;GAChB,CAAA;EACE,CAAA,CACS;;AAEpB;;;;;;;;AASA,SAAgB,mBAAmB,EAAE,WAAW,GAAG,SAAwC;CACzF,OACE,qBAAC,QAAD;EACE,aAAU;EACV,eAAY;EACZ,WAAW,GAAG,wDAAwD,SAAS;EAC/E,GAAI;EAJN,UAAA,CAME,qBAAC,OAAD;GAAK,SAAQ;GAAY,MAAK;GAAO,WAAU;GAA/C,UAAA;IACE,oBAAC,UAAD;KAAQ,IAAG;KAAI,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;IACpD,oBAAC,UAAD;KAAQ,IAAG;KAAK,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;IACrD,oBAAC,UAAD;KAAQ,IAAG;KAAK,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;GAClD;EACL,CAAA,GAAA,oBAAC,QAAD;GAAM,WAAU;GAAU,UAAA;EAAgB,CAAA,CACtC;;AAEV"}
@@ -57,7 +57,7 @@ function PermissionMatrix({ className, label, roles, permissions, grants, onChan
57
57
  children: [/* @__PURE__ */ jsx("th", {
58
58
  role: "columnheader",
59
59
  scope: "col",
60
- className: "px-3 py-2 text-left text-xs font-medium",
60
+ className: "px-3 py-2 text-start text-xs font-medium",
61
61
  children: "Permission"
62
62
  }), roles.map((role) => {
63
63
  const granted = countGranted(roles, grants, role, permissions);
@@ -85,7 +85,7 @@ function PermissionMatrix({ className, label, roles, permissions, grants, onChan
85
85
  children: [/* @__PURE__ */ jsxs("th", {
86
86
  role: "rowheader",
87
87
  scope: "row",
88
- className: "px-3 py-1.5 text-left text-xs font-medium",
88
+ className: "px-3 py-1.5 text-start text-xs font-medium",
89
89
  children: [row.label, /* @__PURE__ */ jsx("span", {
90
90
  className: "sr-only",
91
91
  children: ", all"
@@ -107,7 +107,7 @@ function PermissionMatrix({ className, label, roles, permissions, grants, onChan
107
107
  children: [/* @__PURE__ */ jsxs("th", {
108
108
  role: "rowheader",
109
109
  scope: "row",
110
- className: cn("px-3 py-2 text-left font-normal", row.permission.group !== void 0 && "pl-6"),
110
+ className: cn("px-3 py-2 text-start font-normal", row.permission.group !== void 0 && "ps-6"),
111
111
  children: [row.permission.label, row.permission.description ? /* @__PURE__ */ jsx("span", {
112
112
  className: "block text-xs text-muted-foreground",
113
113
  children: row.permission.description
@@ -1 +1 @@
1
- {"version":3,"file":"permission-matrix.js","names":[],"sources":["../../../src/components/permission-matrix/permission-matrix.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useId,\n useMemo,\n useState,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n} from \"react\";\n\nimport { Checkbox } from \"@/components/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\nimport {\n cellState,\n countGranted,\n groupPermissions,\n groupState,\n type Grants,\n type Permission,\n type Role,\n} from \"./permission-model\";\n\n/**\n * Roles across, permissions down, a checkbox at every crossing.\n *\n * Every admin panel has one and every admin panel builds it, because the\n * hard part is not the checkboxes. It is that a role can inherit from\n * another, so a box can be ticked without anyone having ticked it; that an\n * Owner has everything and none of it can be unticked; that a section of\n * eight permissions wants one control for all of them; and that sixty\n * checkboxes are sixty tab stops unless something is done about it.\n *\n * Something is done about it. This is a grid in the WAI-ARIA sense — one tab\n * stop, arrow keys between cells, Home and End along a row — which is the\n * right call here and was the wrong one for the diff viewer: a diff is read,\n * a matrix is operated. Every checkbox is named by both coordinates, \"Delete\n * projects for Editor\", so a reader arriving by arrow key knows where they\n * are without re-reading the headers.\n *\n * A grant that arrived by inheritance is shown as a checked box that cannot\n * be unchecked here, with the role it came from said beside it and in the\n * box's description. A disabled control would be the obvious rendering, and\n * it would take the box out of the tab order and the arrow-key path, so a\n * keyboard user would step over the one cell whose state needs explaining.\n *\n * Changes are reported, never applied. A group toggle reports every\n * permission it touched in one call, so an application can save it as one\n * change rather than eight.\n */\n\nexport interface PermissionMatrixProps extends Omit<\n ComponentPropsWithRef<\"table\">,\n \"onChange\"\n> {\n /** Names the matrix: \"Permissions for Acme\". */\n label: string;\n roles: Role[];\n permissions: Permission[];\n grants: Grants;\n /**\n * A change to make. Several ids arrive together when a group toggle was\n * used. Omit to render the matrix as a record.\n */\n onChange?: (roleId: string, permissionIds: string[], granted: boolean) => void;\n}\n\ntype MatrixRow =\n | { kind: \"group\"; label: string; permissions: Permission[] }\n | { kind: \"permission\"; permission: Permission };\n\nconst ARROWS: Record<string, [number, number]> = {\n ArrowUp: [-1, 0],\n ArrowDown: [1, 0],\n ArrowLeft: [0, -1],\n ArrowRight: [0, 1],\n};\n\nexport function PermissionMatrix({\n className,\n label,\n roles,\n permissions,\n grants,\n onChange,\n ...props\n}: PermissionMatrixProps) {\n const uid = useId();\n\n const rows = useMemo<MatrixRow[]>(\n () =>\n groupPermissions(permissions).flatMap((group): MatrixRow[] => [\n ...(group.group !== undefined\n ? [{ kind: \"group\" as const, label: group.group, permissions: group.permissions }]\n : []),\n ...group.permissions.map((permission) => ({ kind: \"permission\" as const, permission })),\n ]),\n [permissions],\n );\n\n // One tab stop. The cell that last had focus is the one Tab returns to.\n const [active, setActive] = useState<[number, number]>([0, 0]);\n\n const onKeyDown = (event: KeyboardEvent<HTMLTableElement>) => {\n const target = event.target as HTMLElement;\n const row = Number(target.dataset.row);\n const col = Number(target.dataset.col);\n if (Number.isNaN(row) || Number.isNaN(col)) return;\n\n let next: [number, number] | null = null;\n const arrow = ARROWS[event.key];\n if (arrow) {\n next = [row + arrow[0], col + arrow[1]];\n } else if (event.key === \"Home\") {\n next = event.ctrlKey ? [0, 0] : [row, 0];\n } else if (event.key === \"End\") {\n next = event.ctrlKey ? [rows.length - 1, roles.length - 1] : [row, roles.length - 1];\n }\n if (!next) return;\n\n const [nextRow, nextCol] = next;\n if (nextRow < 0 || nextRow >= rows.length || nextCol < 0 || nextCol >= roles.length) {\n event.preventDefault();\n return;\n }\n event.preventDefault();\n const cell = event.currentTarget.querySelector<HTMLElement>(\n `[data-row=\"${String(nextRow)}\"][data-col=\"${String(nextCol)}\"]`,\n );\n cell?.focus();\n };\n\n return (\n <div className=\"relative w-full overflow-x-auto\">\n <table\n data-slot=\"permission-matrix\"\n role=\"grid\"\n aria-label={label}\n aria-readonly={onChange ? undefined : true}\n onKeyDown={onKeyDown}\n className={cn(\"w-full border-collapse text-sm\", className)}\n {...props}\n >\n <thead>\n <tr role=\"row\" className=\"border-b border-border\">\n <th\n role=\"columnheader\"\n scope=\"col\"\n className=\"px-3 py-2 text-left text-xs font-medium\"\n >\n Permission\n </th>\n {roles.map((role) => {\n const granted = countGranted(roles, grants, role, permissions);\n return (\n <th\n key={role.id}\n role=\"columnheader\"\n scope=\"col\"\n className=\"px-3 py-2 text-center text-xs font-medium\"\n >\n {role.label}\n <span className=\"block font-normal text-muted-foreground\">\n {role.locked ? (\n \"all, locked\"\n ) : (\n <>\n {String(granted)}/{String(permissions.length)}\n <span className=\"sr-only\"> granted</span>\n </>\n )}\n </span>\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, rowIndex) =>\n row.kind === \"group\" ? (\n <tr\n key={`group-${row.label}`}\n role=\"row\"\n className=\"border-b border-border bg-muted/40\"\n >\n <th\n role=\"rowheader\"\n scope=\"row\"\n className=\"px-3 py-1.5 text-left text-xs font-medium\"\n >\n {row.label}\n <span className=\"sr-only\">, all</span>\n </th>\n {roles.map((role, colIndex) => (\n <GroupCell\n key={role.id}\n role={role}\n roles={roles}\n grants={grants}\n group={row.label}\n permissions={row.permissions}\n position={[rowIndex, colIndex]}\n active={active[0] === rowIndex && active[1] === colIndex}\n onActivate={setActive}\n onChange={onChange}\n />\n ))}\n </tr>\n ) : (\n <tr\n key={row.permission.id}\n role=\"row\"\n className=\"border-b border-border last:border-0 hover:bg-muted/30\"\n >\n <th\n role=\"rowheader\"\n scope=\"row\"\n className={cn(\n \"px-3 py-2 text-left font-normal\",\n row.permission.group !== undefined && \"pl-6\",\n )}\n >\n {row.permission.label}\n {row.permission.description ? (\n <span className=\"block text-xs text-muted-foreground\">\n {row.permission.description}\n </span>\n ) : null}\n </th>\n {roles.map((role, colIndex) => (\n <PermissionCell\n key={role.id}\n id={`${uid}-${role.id}-${row.permission.id}`}\n role={role}\n roles={roles}\n grants={grants}\n permission={row.permission}\n position={[rowIndex, colIndex]}\n active={active[0] === rowIndex && active[1] === colIndex}\n onActivate={setActive}\n onChange={onChange}\n />\n ))}\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n );\n}\n\ninterface CellProps {\n role: Role;\n roles: Role[];\n grants: Grants;\n position: [number, number];\n active: boolean;\n onActivate: (position: [number, number]) => void;\n onChange?: (roleId: string, permissionIds: string[], granted: boolean) => void;\n}\n\nfunction PermissionCell({\n id,\n role,\n roles,\n grants,\n permission,\n position,\n active,\n onActivate,\n onChange,\n}: CellProps & { id: string; permission: Permission }) {\n const state = cellState(roles, grants, role, permission.id);\n const fixed = state.fixed || !onChange;\n const noteId = `${id}-note`;\n\n const note = role.locked\n ? `${role.label} has every permission; this cannot be changed.`\n : state.inheritedFrom\n ? `Inherited from ${state.inheritedFrom.label}.`\n : null;\n\n return (\n <td\n role=\"gridcell\"\n data-slot=\"permission-cell\"\n data-granted={state.granted || undefined}\n data-inherited={state.inheritedFrom ? \"\" : undefined}\n className=\"px-3 py-2 text-center align-middle\"\n >\n <Checkbox\n id={id}\n checked={state.granted}\n aria-label={`${permission.label} for ${role.label}`}\n aria-describedby={note ? noteId : undefined}\n // Fixed cells stay focusable, so the arrow-key path and the tab order\n // do not step over the one cell whose state needs explaining.\n aria-disabled={fixed || undefined}\n tabIndex={active ? 0 : -1}\n data-row={position[0]}\n data-col={position[1]}\n className={cn(fixed && \"opacity-55\")}\n onFocus={() => {\n onActivate(position);\n }}\n onCheckedChange={(next) => {\n if (fixed) return;\n onChange?.(role.id, [permission.id], next === true);\n }}\n />\n {state.inheritedFrom ? (\n <span id={noteId} className=\"mt-0.5 block text-2xs text-muted-foreground\">\n via {state.inheritedFrom.label}\n </span>\n ) : note ? (\n <span id={noteId} className=\"sr-only\">\n {note}\n </span>\n ) : null}\n </td>\n );\n}\n\nfunction GroupCell({\n role,\n roles,\n grants,\n group,\n permissions,\n position,\n active,\n onActivate,\n onChange,\n}: CellProps & { group: string; permissions: Permission[] }) {\n const ids = permissions.map((permission) => permission.id);\n const state = groupState(roles, grants, role, ids);\n // Only what can actually change here: inherited grants stay either way.\n const changeable = permissions\n .filter((permission) => !cellState(roles, grants, role, permission.id).fixed)\n .map((permission) => permission.id);\n const fixed = role.locked || !onChange || changeable.length === 0;\n\n return (\n <td\n role=\"gridcell\"\n data-slot=\"permission-group-cell\"\n className=\"px-3 py-1.5 text-center align-middle\"\n >\n <Checkbox\n checked={state === \"all\" ? true : state === \"some\" ? \"indeterminate\" : false}\n aria-label={`All ${group} for ${role.label}`}\n aria-disabled={fixed || undefined}\n tabIndex={active ? 0 : -1}\n data-row={position[0]}\n data-col={position[1]}\n className={cn(fixed && \"opacity-55\")}\n onFocus={() => {\n onActivate(position);\n }}\n onCheckedChange={(next) => {\n if (fixed) return;\n onChange?.(role.id, changeable, next === true);\n }}\n />\n </td>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"permission-matrix.js","names":[],"sources":["../../../src/components/permission-matrix/permission-matrix.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useId,\n useMemo,\n useState,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n} from \"react\";\n\nimport { Checkbox } from \"@/components/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\nimport {\n cellState,\n countGranted,\n groupPermissions,\n groupState,\n type Grants,\n type Permission,\n type Role,\n} from \"./permission-model\";\n\n/**\n * Roles across, permissions down, a checkbox at every crossing.\n *\n * Every admin panel has one and every admin panel builds it, because the\n * hard part is not the checkboxes. It is that a role can inherit from\n * another, so a box can be ticked without anyone having ticked it; that an\n * Owner has everything and none of it can be unticked; that a section of\n * eight permissions wants one control for all of them; and that sixty\n * checkboxes are sixty tab stops unless something is done about it.\n *\n * Something is done about it. This is a grid in the WAI-ARIA sense — one tab\n * stop, arrow keys between cells, Home and End along a row — which is the\n * right call here and was the wrong one for the diff viewer: a diff is read,\n * a matrix is operated. Every checkbox is named by both coordinates, \"Delete\n * projects for Editor\", so a reader arriving by arrow key knows where they\n * are without re-reading the headers.\n *\n * A grant that arrived by inheritance is shown as a checked box that cannot\n * be unchecked here, with the role it came from said beside it and in the\n * box's description. A disabled control would be the obvious rendering, and\n * it would take the box out of the tab order and the arrow-key path, so a\n * keyboard user would step over the one cell whose state needs explaining.\n *\n * Changes are reported, never applied. A group toggle reports every\n * permission it touched in one call, so an application can save it as one\n * change rather than eight.\n */\n\nexport interface PermissionMatrixProps extends Omit<\n ComponentPropsWithRef<\"table\">,\n \"onChange\"\n> {\n /** Names the matrix: \"Permissions for Acme\". */\n label: string;\n roles: Role[];\n permissions: Permission[];\n grants: Grants;\n /**\n * A change to make. Several ids arrive together when a group toggle was\n * used. Omit to render the matrix as a record.\n */\n onChange?: (roleId: string, permissionIds: string[], granted: boolean) => void;\n}\n\ntype MatrixRow =\n | { kind: \"group\"; label: string; permissions: Permission[] }\n | { kind: \"permission\"; permission: Permission };\n\nconst ARROWS: Record<string, [number, number]> = {\n ArrowUp: [-1, 0],\n ArrowDown: [1, 0],\n ArrowLeft: [0, -1],\n ArrowRight: [0, 1],\n};\n\nexport function PermissionMatrix({\n className,\n label,\n roles,\n permissions,\n grants,\n onChange,\n ...props\n}: PermissionMatrixProps) {\n const uid = useId();\n\n const rows = useMemo<MatrixRow[]>(\n () =>\n groupPermissions(permissions).flatMap((group): MatrixRow[] => [\n ...(group.group !== undefined\n ? [{ kind: \"group\" as const, label: group.group, permissions: group.permissions }]\n : []),\n ...group.permissions.map((permission) => ({ kind: \"permission\" as const, permission })),\n ]),\n [permissions],\n );\n\n // One tab stop. The cell that last had focus is the one Tab returns to.\n const [active, setActive] = useState<[number, number]>([0, 0]);\n\n const onKeyDown = (event: KeyboardEvent<HTMLTableElement>) => {\n const target = event.target as HTMLElement;\n const row = Number(target.dataset.row);\n const col = Number(target.dataset.col);\n if (Number.isNaN(row) || Number.isNaN(col)) return;\n\n let next: [number, number] | null = null;\n const arrow = ARROWS[event.key];\n if (arrow) {\n next = [row + arrow[0], col + arrow[1]];\n } else if (event.key === \"Home\") {\n next = event.ctrlKey ? [0, 0] : [row, 0];\n } else if (event.key === \"End\") {\n next = event.ctrlKey ? [rows.length - 1, roles.length - 1] : [row, roles.length - 1];\n }\n if (!next) return;\n\n const [nextRow, nextCol] = next;\n if (nextRow < 0 || nextRow >= rows.length || nextCol < 0 || nextCol >= roles.length) {\n event.preventDefault();\n return;\n }\n event.preventDefault();\n const cell = event.currentTarget.querySelector<HTMLElement>(\n `[data-row=\"${String(nextRow)}\"][data-col=\"${String(nextCol)}\"]`,\n );\n cell?.focus();\n };\n\n return (\n <div className=\"relative w-full overflow-x-auto\">\n <table\n data-slot=\"permission-matrix\"\n role=\"grid\"\n aria-label={label}\n aria-readonly={onChange ? undefined : true}\n onKeyDown={onKeyDown}\n className={cn(\"w-full border-collapse text-sm\", className)}\n {...props}\n >\n <thead>\n <tr role=\"row\" className=\"border-b border-border\">\n <th\n role=\"columnheader\"\n scope=\"col\"\n className=\"px-3 py-2 text-start text-xs font-medium\"\n >\n Permission\n </th>\n {roles.map((role) => {\n const granted = countGranted(roles, grants, role, permissions);\n return (\n <th\n key={role.id}\n role=\"columnheader\"\n scope=\"col\"\n className=\"px-3 py-2 text-center text-xs font-medium\"\n >\n {role.label}\n <span className=\"block font-normal text-muted-foreground\">\n {role.locked ? (\n \"all, locked\"\n ) : (\n <>\n {String(granted)}/{String(permissions.length)}\n <span className=\"sr-only\"> granted</span>\n </>\n )}\n </span>\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, rowIndex) =>\n row.kind === \"group\" ? (\n <tr\n key={`group-${row.label}`}\n role=\"row\"\n className=\"border-b border-border bg-muted/40\"\n >\n <th\n role=\"rowheader\"\n scope=\"row\"\n className=\"px-3 py-1.5 text-start text-xs font-medium\"\n >\n {row.label}\n <span className=\"sr-only\">, all</span>\n </th>\n {roles.map((role, colIndex) => (\n <GroupCell\n key={role.id}\n role={role}\n roles={roles}\n grants={grants}\n group={row.label}\n permissions={row.permissions}\n position={[rowIndex, colIndex]}\n active={active[0] === rowIndex && active[1] === colIndex}\n onActivate={setActive}\n onChange={onChange}\n />\n ))}\n </tr>\n ) : (\n <tr\n key={row.permission.id}\n role=\"row\"\n className=\"border-b border-border last:border-0 hover:bg-muted/30\"\n >\n <th\n role=\"rowheader\"\n scope=\"row\"\n className={cn(\n \"px-3 py-2 text-start font-normal\",\n row.permission.group !== undefined && \"ps-6\",\n )}\n >\n {row.permission.label}\n {row.permission.description ? (\n <span className=\"block text-xs text-muted-foreground\">\n {row.permission.description}\n </span>\n ) : null}\n </th>\n {roles.map((role, colIndex) => (\n <PermissionCell\n key={role.id}\n id={`${uid}-${role.id}-${row.permission.id}`}\n role={role}\n roles={roles}\n grants={grants}\n permission={row.permission}\n position={[rowIndex, colIndex]}\n active={active[0] === rowIndex && active[1] === colIndex}\n onActivate={setActive}\n onChange={onChange}\n />\n ))}\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n );\n}\n\ninterface CellProps {\n role: Role;\n roles: Role[];\n grants: Grants;\n position: [number, number];\n active: boolean;\n onActivate: (position: [number, number]) => void;\n onChange?: (roleId: string, permissionIds: string[], granted: boolean) => void;\n}\n\nfunction PermissionCell({\n id,\n role,\n roles,\n grants,\n permission,\n position,\n active,\n onActivate,\n onChange,\n}: CellProps & { id: string; permission: Permission }) {\n const state = cellState(roles, grants, role, permission.id);\n const fixed = state.fixed || !onChange;\n const noteId = `${id}-note`;\n\n const note = role.locked\n ? `${role.label} has every permission; this cannot be changed.`\n : state.inheritedFrom\n ? `Inherited from ${state.inheritedFrom.label}.`\n : null;\n\n return (\n <td\n role=\"gridcell\"\n data-slot=\"permission-cell\"\n data-granted={state.granted || undefined}\n data-inherited={state.inheritedFrom ? \"\" : undefined}\n className=\"px-3 py-2 text-center align-middle\"\n >\n <Checkbox\n id={id}\n checked={state.granted}\n aria-label={`${permission.label} for ${role.label}`}\n aria-describedby={note ? noteId : undefined}\n // Fixed cells stay focusable, so the arrow-key path and the tab order\n // do not step over the one cell whose state needs explaining.\n aria-disabled={fixed || undefined}\n tabIndex={active ? 0 : -1}\n data-row={position[0]}\n data-col={position[1]}\n className={cn(fixed && \"opacity-55\")}\n onFocus={() => {\n onActivate(position);\n }}\n onCheckedChange={(next) => {\n if (fixed) return;\n onChange?.(role.id, [permission.id], next === true);\n }}\n />\n {state.inheritedFrom ? (\n <span id={noteId} className=\"mt-0.5 block text-2xs text-muted-foreground\">\n via {state.inheritedFrom.label}\n </span>\n ) : note ? (\n <span id={noteId} className=\"sr-only\">\n {note}\n </span>\n ) : null}\n </td>\n );\n}\n\nfunction GroupCell({\n role,\n roles,\n grants,\n group,\n permissions,\n position,\n active,\n onActivate,\n onChange,\n}: CellProps & { group: string; permissions: Permission[] }) {\n const ids = permissions.map((permission) => permission.id);\n const state = groupState(roles, grants, role, ids);\n // Only what can actually change here: inherited grants stay either way.\n const changeable = permissions\n .filter((permission) => !cellState(roles, grants, role, permission.id).fixed)\n .map((permission) => permission.id);\n const fixed = role.locked || !onChange || changeable.length === 0;\n\n return (\n <td\n role=\"gridcell\"\n data-slot=\"permission-group-cell\"\n className=\"px-3 py-1.5 text-center align-middle\"\n >\n <Checkbox\n checked={state === \"all\" ? true : state === \"some\" ? \"indeterminate\" : false}\n aria-label={`All ${group} for ${role.label}`}\n aria-disabled={fixed || undefined}\n tabIndex={active ? 0 : -1}\n data-row={position[0]}\n data-col={position[1]}\n className={cn(fixed && \"opacity-55\")}\n onFocus={() => {\n onActivate(position);\n }}\n onCheckedChange={(next) => {\n if (fixed) return;\n onChange?.(role.id, changeable, next === true);\n }}\n />\n </td>\n );\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ import { ComponentPropsWithRef } from "react";
4
4
  //#region src/components/progress/progress.d.ts
5
5
  declare const progressVariants: (props?: ({
6
6
  size?: "sm" | "md" | "lg" | null | undefined;
7
- tone?: "primary" | "success" | "warning" | "destructive" | null | undefined;
7
+ tone?: "destructive" | "success" | "warning" | "primary" | null | undefined;
8
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
9
9
  interface ProgressProps extends ComponentPropsWithRef<typeof Progress.Root>, VariantProps<typeof progressVariants> {}
10
10
  /**
@@ -121,17 +121,17 @@ function RecordDiffTable({ className, collapseUnchanged = true, ...props }) {
121
121
  children: [
122
122
  /* @__PURE__ */ jsx("th", {
123
123
  scope: "col",
124
- className: "px-3 py-2 text-left text-xs font-medium text-muted-foreground",
124
+ className: "px-3 py-2 text-start text-xs font-medium text-muted-foreground",
125
125
  children: "Field"
126
126
  }),
127
127
  /* @__PURE__ */ jsx("th", {
128
128
  scope: "col",
129
- className: "px-3 py-2 text-left text-xs font-medium text-muted-foreground",
129
+ className: "px-3 py-2 text-start text-xs font-medium text-muted-foreground",
130
130
  children: "Before"
131
131
  }),
132
132
  /* @__PURE__ */ jsx("th", {
133
133
  scope: "col",
134
- className: "px-3 py-2 text-left text-xs font-medium text-muted-foreground",
134
+ className: "px-3 py-2 text-start text-xs font-medium text-muted-foreground",
135
135
  children: "After"
136
136
  })
137
137
  ]
@@ -142,7 +142,7 @@ function RecordDiffTable({ className, collapseUnchanged = true, ...props }) {
142
142
  children: [
143
143
  /* @__PURE__ */ jsxs("th", {
144
144
  scope: "row",
145
- className: "px-3 py-2 text-left align-top font-medium whitespace-nowrap",
145
+ className: "px-3 py-2 text-start align-top font-medium whitespace-nowrap",
146
146
  children: [field.label, /* @__PURE__ */ jsx("span", {
147
147
  className: "sr-only",
148
148
  children: `, ${field.change}`
@@ -1 +1 @@
1
- {"version":3,"file":"record-diff.js","names":[],"sources":["../../../src/components/record-diff/record-diff.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useMemo,\n useState,\n type ComponentPropsWithRef,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * What changed about a record, field by field.\n *\n * Every diff package on npm compares lines of text. That is the wrong shape for\n * an audit entry, a settings revision or a config history, where the change is\n * \"role went from viewer to admin\" and rendering it as two lines of JSON makes\n * the reader do the comparison themselves.\n *\n * Scope is deliberately flat records. Nested objects and arrays are formatted as\n * values rather than recursed into, because a recursive differ is a different\n * and much larger component, and pretending otherwise would produce a shallow\n * one that quietly lies about deep changes. `formatValue` is the escape hatch.\n *\n * Redaction is by key pattern and applied before formatting, so a secret is\n * never handed to a formatter that might log it.\n */\n\nexport type FieldChange = \"added\" | \"removed\" | \"changed\" | \"unchanged\";\n\nexport interface RecordField {\n key: string;\n label: string;\n change: FieldChange;\n before: unknown;\n after: unknown;\n redacted: boolean;\n}\n\n/** Values that read as absent, so \"\" → null is not reported as a change. */\nfunction isEmpty(value: unknown): boolean {\n return value === null || value === undefined || value === \"\";\n}\n\nfunction classify(before: unknown, after: unknown): FieldChange {\n const hadBefore = !isEmpty(before);\n const hasAfter = !isEmpty(after);\n\n if (!hadBefore && hasAfter) return \"added\";\n if (hadBefore && !hasAfter) return \"removed\";\n // Both absent. Falling through would compare \"\" against null, which\n // serialise differently and would report a change that did not happen.\n if (!hadBefore && !hasAfter) return \"unchanged\";\n // Structural comparison, so {a:1} and {a:1} are equal. Key order differences\n // would report a false change, so keys are sorted before comparing.\n return stableStringify(before) === stableStringify(after) ? \"unchanged\" : \"changed\";\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value) ?? \"\";\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) =>\n a.localeCompare(b),\n );\n return `{${entries.map(([k, v]) => `${k}:${stableStringify(v)}`).join(\",\")}}`;\n}\n\nfunction defaultFormatValue(value: unknown): string {\n if (value === null || value === undefined) return \"—\";\n if (value === \"\") return \"empty\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"boolean\") return value ? \"yes\" : \"no\";\n if (typeof value === \"number\" || typeof value === \"bigint\") return String(value);\n if (Array.isArray(value)) {\n return value.length === 0 ? \"none\" : value.map(defaultFormatValue).join(\", \");\n }\n if (typeof value === \"object\") return JSON.stringify(value);\n // Symbols and functions have no sensible representation as a field value, and\n // String() would yield \"[object Object]\" or a function body. Say what it is.\n return `[${typeof value}]`;\n}\n\n/** Keys whose values must never be rendered, however the record was built. */\nconst DEFAULT_REDACT = /password|secret|token|api[-_]?key|authorization|credential/i;\n\nexport interface RecordDiffProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n before: Record<string, unknown>;\n after: Record<string, unknown>;\n /** Overrides the humanised key. */\n labels?: Record<string, string>;\n /** Keys to render as redacted. Defaults to common secret-shaped names. */\n redact?: RegExp;\n /** Only these keys, in this order. Anything else is ignored. */\n fields?: string[];\n /** Hides unchanged fields behind a toggle. */\n collapseUnchanged?: boolean;\n formatValue?: (value: unknown, key: string) => string;\n children?: React.ReactNode;\n}\n\ninterface RecordDiffContextValue {\n fields: RecordField[];\n changedCount: number;\n formatValue: (value: unknown, key: string) => string;\n}\n\nconst RecordDiffContext = createContext<RecordDiffContextValue | null>(null);\n\nfunction useRecordDiffContext(component: string): RecordDiffContextValue {\n const context = useContext(RecordDiffContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <RecordDiff>.`);\n }\n return context;\n}\n\n/** \"billingEmail\" and \"billing_email\" both read as \"Billing email\". */\nfunction humanise(key: string): string {\n const spaced = key\n .replace(/[_-]+/g, \" \")\n .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n .trim();\n return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();\n}\n\nexport function diffRecords(\n before: Record<string, unknown>,\n after: Record<string, unknown>,\n options: { labels?: Record<string, string>; redact?: RegExp; fields?: string[] } = {},\n): RecordField[] {\n const { labels = {}, redact = DEFAULT_REDACT, fields } = options;\n\n // The union of both sides, so a removed key is still reported. Iterating only\n // `after` is how removals go missing from audit logs.\n const keys = fields ?? [...new Set([...Object.keys(before), ...Object.keys(after)])];\n\n return keys.map((key) => ({\n key,\n label: labels[key] ?? humanise(key),\n change: classify(before[key], after[key]),\n before: before[key],\n after: after[key],\n redacted: redact.test(key),\n }));\n}\n\nconst CHANGE_STYLES: Record<FieldChange, string> = {\n added: \"text-success\",\n removed: \"text-destructive\",\n changed: \"text-foreground\",\n unchanged: \"text-muted-foreground\",\n};\n\nexport function RecordDiff({\n className,\n before,\n after,\n labels,\n redact = DEFAULT_REDACT,\n fields: fieldKeys,\n collapseUnchanged = true,\n formatValue = defaultFormatValue,\n children,\n ...props\n}: RecordDiffProps) {\n const fields = useMemo(\n () => diffRecords(before, after, { labels, redact, fields: fieldKeys }),\n [before, after, labels, redact, fieldKeys],\n );\n\n const changedCount = fields.filter((field) => field.change !== \"unchanged\").length;\n\n const context = useMemo<RecordDiffContextValue>(\n () => ({ fields, changedCount, formatValue }),\n [fields, changedCount, formatValue],\n );\n\n return (\n <RecordDiffContext.Provider value={context}>\n <div\n data-slot=\"record-diff\"\n data-changed={changedCount}\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n >\n {children ?? (\n <>\n <RecordDiffSummary />\n <RecordDiffTable collapseUnchanged={collapseUnchanged} />\n </>\n )}\n </div>\n </RecordDiffContext.Provider>\n );\n}\n\n/** \"3 fields changed\" — the thing a reviewer scanning a log needs first. */\nexport function RecordDiffSummary({ className, ...props }: ComponentPropsWithRef<\"p\">) {\n const { changedCount, fields } = useRecordDiffContext(\"RecordDiffSummary\");\n\n return (\n <p\n data-slot=\"record-diff-summary\"\n className={cn(\"text-xs text-muted-foreground\", className)}\n {...props}\n >\n {changedCount === 0\n ? \"No fields changed\"\n : `${String(changedCount)} of ${String(fields.length)} field${fields.length === 1 ? \"\" : \"s\"} changed`}\n </p>\n );\n}\n\nexport interface RecordDiffTableProps extends ComponentPropsWithRef<\"table\"> {\n collapseUnchanged?: boolean;\n}\n\nexport function RecordDiffTable({\n className,\n collapseUnchanged = true,\n ...props\n}: RecordDiffTableProps) {\n const { fields, formatValue, changedCount } = useRecordDiffContext(\"RecordDiffTable\");\n const [showUnchanged, setShowUnchanged] = useState(!collapseUnchanged);\n\n const unchangedCount = fields.length - changedCount;\n const visible = showUnchanged\n ? fields\n : fields.filter((field) => field.change !== \"unchanged\");\n\n return (\n <div className=\"flex flex-col gap-2\">\n <div className=\"overflow-x-auto\">\n <table\n data-slot=\"record-diff-table\"\n className={cn(\"w-full border-collapse text-sm\", className)}\n {...props}\n >\n <thead>\n <tr className=\"border-b border-border\">\n <th\n scope=\"col\"\n className=\"px-3 py-2 text-left text-xs font-medium text-muted-foreground\"\n >\n Field\n </th>\n <th\n scope=\"col\"\n className=\"px-3 py-2 text-left text-xs font-medium text-muted-foreground\"\n >\n Before\n </th>\n <th\n scope=\"col\"\n className=\"px-3 py-2 text-left text-xs font-medium text-muted-foreground\"\n >\n After\n </th>\n </tr>\n </thead>\n <tbody>\n {visible.map((field) => (\n <tr\n key={field.key}\n data-slot=\"record-diff-row\"\n data-change={field.change}\n className=\"border-b border-border last:border-0\"\n >\n {/* scope=\"row\" so each value is announced with its field name\n rather than as a loose cell in a wall of table. */}\n <th\n scope=\"row\"\n className=\"px-3 py-2 text-left align-top font-medium whitespace-nowrap\"\n >\n {field.label}\n {/* The change kind must not be carried by row colour alone. */}\n <span className=\"sr-only\">{`, ${field.change}`}</span>\n </th>\n <td className=\"px-3 py-2 align-top text-muted-foreground\">\n {field.redacted ? (\n <RedactedValue />\n ) : field.change === \"added\" ? (\n <span className=\"text-muted-foreground\">—</span>\n ) : (\n <span className=\"line-through decoration-muted-foreground/50\">\n {formatValue(field.before, field.key)}\n </span>\n )}\n </td>\n <td className={cn(\"px-3 py-2 align-top\", CHANGE_STYLES[field.change])}>\n {field.redacted ? (\n <RedactedValue />\n ) : field.change === \"removed\" ? (\n <span className=\"text-muted-foreground\">—</span>\n ) : (\n formatValue(field.after, field.key)\n )}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n\n {collapseUnchanged && unchangedCount > 0 ? (\n <button\n type=\"button\"\n data-slot=\"record-diff-toggle\"\n aria-expanded={showUnchanged}\n onClick={() => {\n setShowUnchanged((shown) => !shown);\n }}\n className={cn(\n \"self-start rounded-md px-1.5 py-0.5 text-xs text-muted-foreground\",\n \"transition-colors hover:text-foreground\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring/55\",\n )}\n >\n {showUnchanged\n ? `Hide ${String(unchangedCount)} unchanged`\n : `Show ${String(unchangedCount)} unchanged`}\n </button>\n ) : null}\n </div>\n );\n}\n\n/**\n * A value that is never rendered.\n *\n * Redaction happens before formatting, so a secret is not passed to a formatter\n * that might log or transform it — the value simply does not reach the DOM.\n */\nfunction RedactedValue() {\n return (\n <span data-slot=\"record-diff-redacted\" className=\"text-muted-foreground italic\">\n redacted\n </span>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"record-diff.js","names":[],"sources":["../../../src/components/record-diff/record-diff.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useMemo,\n useState,\n type ComponentPropsWithRef,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * What changed about a record, field by field.\n *\n * Every diff package on npm compares lines of text. That is the wrong shape for\n * an audit entry, a settings revision or a config history, where the change is\n * \"role went from viewer to admin\" and rendering it as two lines of JSON makes\n * the reader do the comparison themselves.\n *\n * Scope is deliberately flat records. Nested objects and arrays are formatted as\n * values rather than recursed into, because a recursive differ is a different\n * and much larger component, and pretending otherwise would produce a shallow\n * one that quietly lies about deep changes. `formatValue` is the escape hatch.\n *\n * Redaction is by key pattern and applied before formatting, so a secret is\n * never handed to a formatter that might log it.\n */\n\nexport type FieldChange = \"added\" | \"removed\" | \"changed\" | \"unchanged\";\n\nexport interface RecordField {\n key: string;\n label: string;\n change: FieldChange;\n before: unknown;\n after: unknown;\n redacted: boolean;\n}\n\n/** Values that read as absent, so \"\" → null is not reported as a change. */\nfunction isEmpty(value: unknown): boolean {\n return value === null || value === undefined || value === \"\";\n}\n\nfunction classify(before: unknown, after: unknown): FieldChange {\n const hadBefore = !isEmpty(before);\n const hasAfter = !isEmpty(after);\n\n if (!hadBefore && hasAfter) return \"added\";\n if (hadBefore && !hasAfter) return \"removed\";\n // Both absent. Falling through would compare \"\" against null, which\n // serialise differently and would report a change that did not happen.\n if (!hadBefore && !hasAfter) return \"unchanged\";\n // Structural comparison, so {a:1} and {a:1} are equal. Key order differences\n // would report a false change, so keys are sorted before comparing.\n return stableStringify(before) === stableStringify(after) ? \"unchanged\" : \"changed\";\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value) ?? \"\";\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) =>\n a.localeCompare(b),\n );\n return `{${entries.map(([k, v]) => `${k}:${stableStringify(v)}`).join(\",\")}}`;\n}\n\nfunction defaultFormatValue(value: unknown): string {\n if (value === null || value === undefined) return \"—\";\n if (value === \"\") return \"empty\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"boolean\") return value ? \"yes\" : \"no\";\n if (typeof value === \"number\" || typeof value === \"bigint\") return String(value);\n if (Array.isArray(value)) {\n return value.length === 0 ? \"none\" : value.map(defaultFormatValue).join(\", \");\n }\n if (typeof value === \"object\") return JSON.stringify(value);\n // Symbols and functions have no sensible representation as a field value, and\n // String() would yield \"[object Object]\" or a function body. Say what it is.\n return `[${typeof value}]`;\n}\n\n/** Keys whose values must never be rendered, however the record was built. */\nconst DEFAULT_REDACT = /password|secret|token|api[-_]?key|authorization|credential/i;\n\nexport interface RecordDiffProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n before: Record<string, unknown>;\n after: Record<string, unknown>;\n /** Overrides the humanised key. */\n labels?: Record<string, string>;\n /** Keys to render as redacted. Defaults to common secret-shaped names. */\n redact?: RegExp;\n /** Only these keys, in this order. Anything else is ignored. */\n fields?: string[];\n /** Hides unchanged fields behind a toggle. */\n collapseUnchanged?: boolean;\n formatValue?: (value: unknown, key: string) => string;\n children?: React.ReactNode;\n}\n\ninterface RecordDiffContextValue {\n fields: RecordField[];\n changedCount: number;\n formatValue: (value: unknown, key: string) => string;\n}\n\nconst RecordDiffContext = createContext<RecordDiffContextValue | null>(null);\n\nfunction useRecordDiffContext(component: string): RecordDiffContextValue {\n const context = useContext(RecordDiffContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <RecordDiff>.`);\n }\n return context;\n}\n\n/** \"billingEmail\" and \"billing_email\" both read as \"Billing email\". */\nfunction humanise(key: string): string {\n const spaced = key\n .replace(/[_-]+/g, \" \")\n .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n .trim();\n return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();\n}\n\nexport function diffRecords(\n before: Record<string, unknown>,\n after: Record<string, unknown>,\n options: { labels?: Record<string, string>; redact?: RegExp; fields?: string[] } = {},\n): RecordField[] {\n const { labels = {}, redact = DEFAULT_REDACT, fields } = options;\n\n // The union of both sides, so a removed key is still reported. Iterating only\n // `after` is how removals go missing from audit logs.\n const keys = fields ?? [...new Set([...Object.keys(before), ...Object.keys(after)])];\n\n return keys.map((key) => ({\n key,\n label: labels[key] ?? humanise(key),\n change: classify(before[key], after[key]),\n before: before[key],\n after: after[key],\n redacted: redact.test(key),\n }));\n}\n\nconst CHANGE_STYLES: Record<FieldChange, string> = {\n added: \"text-success\",\n removed: \"text-destructive\",\n changed: \"text-foreground\",\n unchanged: \"text-muted-foreground\",\n};\n\nexport function RecordDiff({\n className,\n before,\n after,\n labels,\n redact = DEFAULT_REDACT,\n fields: fieldKeys,\n collapseUnchanged = true,\n formatValue = defaultFormatValue,\n children,\n ...props\n}: RecordDiffProps) {\n const fields = useMemo(\n () => diffRecords(before, after, { labels, redact, fields: fieldKeys }),\n [before, after, labels, redact, fieldKeys],\n );\n\n const changedCount = fields.filter((field) => field.change !== \"unchanged\").length;\n\n const context = useMemo<RecordDiffContextValue>(\n () => ({ fields, changedCount, formatValue }),\n [fields, changedCount, formatValue],\n );\n\n return (\n <RecordDiffContext.Provider value={context}>\n <div\n data-slot=\"record-diff\"\n data-changed={changedCount}\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n >\n {children ?? (\n <>\n <RecordDiffSummary />\n <RecordDiffTable collapseUnchanged={collapseUnchanged} />\n </>\n )}\n </div>\n </RecordDiffContext.Provider>\n );\n}\n\n/** \"3 fields changed\" — the thing a reviewer scanning a log needs first. */\nexport function RecordDiffSummary({ className, ...props }: ComponentPropsWithRef<\"p\">) {\n const { changedCount, fields } = useRecordDiffContext(\"RecordDiffSummary\");\n\n return (\n <p\n data-slot=\"record-diff-summary\"\n className={cn(\"text-xs text-muted-foreground\", className)}\n {...props}\n >\n {changedCount === 0\n ? \"No fields changed\"\n : `${String(changedCount)} of ${String(fields.length)} field${fields.length === 1 ? \"\" : \"s\"} changed`}\n </p>\n );\n}\n\nexport interface RecordDiffTableProps extends ComponentPropsWithRef<\"table\"> {\n collapseUnchanged?: boolean;\n}\n\nexport function RecordDiffTable({\n className,\n collapseUnchanged = true,\n ...props\n}: RecordDiffTableProps) {\n const { fields, formatValue, changedCount } = useRecordDiffContext(\"RecordDiffTable\");\n const [showUnchanged, setShowUnchanged] = useState(!collapseUnchanged);\n\n const unchangedCount = fields.length - changedCount;\n const visible = showUnchanged\n ? fields\n : fields.filter((field) => field.change !== \"unchanged\");\n\n return (\n <div className=\"flex flex-col gap-2\">\n <div className=\"overflow-x-auto\">\n <table\n data-slot=\"record-diff-table\"\n className={cn(\"w-full border-collapse text-sm\", className)}\n {...props}\n >\n <thead>\n <tr className=\"border-b border-border\">\n <th\n scope=\"col\"\n className=\"px-3 py-2 text-start text-xs font-medium text-muted-foreground\"\n >\n Field\n </th>\n <th\n scope=\"col\"\n className=\"px-3 py-2 text-start text-xs font-medium text-muted-foreground\"\n >\n Before\n </th>\n <th\n scope=\"col\"\n className=\"px-3 py-2 text-start text-xs font-medium text-muted-foreground\"\n >\n After\n </th>\n </tr>\n </thead>\n <tbody>\n {visible.map((field) => (\n <tr\n key={field.key}\n data-slot=\"record-diff-row\"\n data-change={field.change}\n className=\"border-b border-border last:border-0\"\n >\n {/* scope=\"row\" so each value is announced with its field name\n rather than as a loose cell in a wall of table. */}\n <th\n scope=\"row\"\n className=\"px-3 py-2 text-start align-top font-medium whitespace-nowrap\"\n >\n {field.label}\n {/* The change kind must not be carried by row colour alone. */}\n <span className=\"sr-only\">{`, ${field.change}`}</span>\n </th>\n <td className=\"px-3 py-2 align-top text-muted-foreground\">\n {field.redacted ? (\n <RedactedValue />\n ) : field.change === \"added\" ? (\n <span className=\"text-muted-foreground\">—</span>\n ) : (\n <span className=\"line-through decoration-muted-foreground/50\">\n {formatValue(field.before, field.key)}\n </span>\n )}\n </td>\n <td className={cn(\"px-3 py-2 align-top\", CHANGE_STYLES[field.change])}>\n {field.redacted ? (\n <RedactedValue />\n ) : field.change === \"removed\" ? (\n <span className=\"text-muted-foreground\">—</span>\n ) : (\n formatValue(field.after, field.key)\n )}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n\n {collapseUnchanged && unchangedCount > 0 ? (\n <button\n type=\"button\"\n data-slot=\"record-diff-toggle\"\n aria-expanded={showUnchanged}\n onClick={() => {\n setShowUnchanged((shown) => !shown);\n }}\n className={cn(\n \"self-start rounded-md px-1.5 py-0.5 text-xs text-muted-foreground\",\n \"transition-colors hover:text-foreground\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring/55\",\n )}\n >\n {showUnchanged\n ? `Hide ${String(unchangedCount)} unchanged`\n : `Show ${String(unchangedCount)} unchanged`}\n </button>\n ) : null}\n </div>\n );\n}\n\n/**\n * A value that is never rendered.\n *\n * Redaction happens before formatting, so a secret is not passed to a formatter\n * that might log or transform it — the value simply does not reach the DOM.\n */\nfunction RedactedValue() {\n return (\n <span data-slot=\"record-diff-redacted\" className=\"text-muted-foreground italic\">\n redacted\n </span>\n );\n}\n"],"mappings":"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"}
@@ -88,10 +88,10 @@ function SelectContent({ className, children, position = "popper", sideOffset =
88
88
  function SelectItem({ className, label, children, ...props }) {
89
89
  return /* @__PURE__ */ jsxs(Select.Item, {
90
90
  "data-slot": "select-item",
91
- className: cn("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", "transition-colors duration-[var(--duration-instant)]", "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground", "data-[disabled]:pointer-events-none data-[disabled]:opacity-55", "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
91
+ className: cn("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", "transition-colors duration-[var(--duration-instant)]", "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground", "data-[disabled]:pointer-events-none data-[disabled]:opacity-55", "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className),
92
92
  ...props,
93
93
  children: [label === void 0 ? /* @__PURE__ */ jsx(Select.ItemText, { children }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Select.ItemText, { children: label }), children] }), /* @__PURE__ */ jsx("span", {
94
- className: "absolute right-2 grid size-4 place-items-center",
94
+ className: "absolute end-2 grid size-4 place-items-center",
95
95
  children: /* @__PURE__ */ jsx(Select.ItemIndicator, { children: /* @__PURE__ */ jsx("svg", {
96
96
  viewBox: "0 0 24 24",
97
97
  fill: "none",
@@ -1 +1 @@
1
- {"version":3,"file":"select.js","names":["Select","SelectPrimitive"],"sources":["../../../src/components/select/select.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Select as SelectPrimitive } from \"radix-ui\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\n\nimport { focusRing, invalidStyles } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Picks one value from a list.\n *\n * For a short, known list. Past roughly a dozen options people want to type\n * rather than scroll — reach for Combobox there. Unlike a native `<select>`,\n * the options are real elements, so they can carry icons and descriptions.\n */\nexport const Select = SelectPrimitive.Root;\nexport const SelectGroup = SelectPrimitive.Group;\nexport const SelectValue = SelectPrimitive.Value;\n\nconst selectTriggerVariants = cva(\n cn(\n \"flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background\",\n \"whitespace-nowrap text-foreground shadow-xs\",\n \"transition-[border-color,box-shadow] duration-[var(--duration-fast)]\",\n \"data-[placeholder]:text-muted-foreground\",\n \"focus-visible:border-ring\",\n \"disabled:cursor-not-allowed disabled:opacity-55\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"*:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2\",\n focusRing,\n invalidStyles,\n ),\n {\n variants: {\n triggerSize: {\n sm: \"h-8 px-2.5 text-sm\",\n md: \"h-9 px-3 text-sm\",\n lg: \"h-10 px-3.5 text-base\",\n },\n },\n defaultVariants: {\n triggerSize: \"md\",\n },\n },\n);\n\nexport interface SelectTriggerProps\n extends\n ComponentPropsWithRef<typeof SelectPrimitive.Trigger>,\n VariantProps<typeof selectTriggerVariants> {}\n\nexport function SelectTrigger({\n className,\n triggerSize,\n children,\n ...props\n}: SelectTriggerProps) {\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n className={cn(selectTriggerVariants({ triggerSize }), className)}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-4 opacity-60\">\n <path\n d=\"m7 10 5 5 5-5\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n}\n\n/** Scroll affordance shown only when the list overflows. Decorative. */\nfunction ScrollButton({\n direction,\n className,\n ...props\n}: {\n direction: \"up\" | \"down\";\n} & ComponentPropsWithRef<typeof SelectPrimitive.ScrollUpButton>) {\n const Comp =\n direction === \"up\" ? SelectPrimitive.ScrollUpButton : SelectPrimitive.ScrollDownButton;\n\n return (\n <Comp\n data-slot={`select-scroll-${direction}`}\n className={cn(\"flex cursor-default items-center justify-center py-1\", className)}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-4 opacity-60\">\n <path\n d={direction === \"up\" ? \"m7 14 5-5 5 5\" : \"m7 10 5 5 5-5\"}\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </Comp>\n );\n}\n\nexport type SelectContentProps = ComponentPropsWithRef<typeof SelectPrimitive.Content>;\n\nexport function SelectContent({\n className,\n children,\n position = \"popper\",\n sideOffset = 6,\n ...props\n}: SelectContentProps) {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n data-slot=\"select-content\"\n position={position}\n sideOffset={position === \"popper\" ? sideOffset : undefined}\n className={cn(\n \"relative z-[var(--z-popover)] max-h-[var(--radix-select-content-available-height)]\",\n \"min-w-32 overflow-x-hidden overflow-y-auto rounded-lg border border-border bg-popover\",\n \"p-1 text-popover-foreground shadow-lg\",\n \"origin-[var(--radix-select-content-transform-origin)]\",\n \"data-[state=closed]:animate-float-out data-[state=open]:animate-float-in\",\n position === \"popper\" && \"w-full min-w-[var(--radix-select-trigger-width)]\",\n className,\n )}\n {...props}\n >\n <ScrollButton direction=\"up\" />\n <SelectPrimitive.Viewport className=\"p-0\">{children}</SelectPrimitive.Viewport>\n <ScrollButton direction=\"down\" />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n}\n\nexport interface SelectItemProps extends ComponentPropsWithRef<typeof SelectPrimitive.Item> {\n /**\n * What the trigger shows once this option is chosen.\n *\n * The primitive clones the item's text into the trigger, so a rich option —\n * a name with a description under it — would otherwise drag all of that into\n * a one-line trigger. Give the label here and the rest as children.\n */\n label?: ReactNode;\n}\n\nexport function SelectItem({ className, label, children, ...props }: SelectItemProps) {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(\n \"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\",\n \"transition-colors duration-[var(--duration-instant)]\",\n \"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground\",\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-55\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className,\n )}\n {...props}\n >\n {label === undefined ? (\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n ) : (\n <>\n <SelectPrimitive.ItemText>{label}</SelectPrimitive.ItemText>\n {children}\n </>\n )}\n {/* Decorative: selection is already announced through aria-selected. */}\n <span className=\"absolute right-2 grid size-4 place-items-center\">\n <SelectPrimitive.ItemIndicator>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5\">\n <path\n d=\"m5 13 4 4L19 7\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </SelectPrimitive.ItemIndicator>\n </span>\n </SelectPrimitive.Item>\n );\n}\n\nexport function SelectLabel({\n className,\n ...props\n}: ComponentPropsWithRef<typeof SelectPrimitive.Label>) {\n return (\n <SelectPrimitive.Label\n data-slot=\"select-label\"\n className={cn(\"px-2 py-1.5 text-xs font-medium text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport function SelectSeparator({\n className,\n ...props\n}: ComponentPropsWithRef<typeof SelectPrimitive.Separator>) {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n {...props}\n />\n );\n}\n\nexport { selectTriggerVariants };\n"],"mappings":"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"}
1
+ {"version":3,"file":"select.js","names":["Select","SelectPrimitive"],"sources":["../../../src/components/select/select.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Select as SelectPrimitive } from \"radix-ui\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\n\nimport { focusRing, invalidStyles } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Picks one value from a list.\n *\n * For a short, known list. Past roughly a dozen options people want to type\n * rather than scroll — reach for Combobox there. Unlike a native `<select>`,\n * the options are real elements, so they can carry icons and descriptions.\n */\nexport const Select = SelectPrimitive.Root;\nexport const SelectGroup = SelectPrimitive.Group;\nexport const SelectValue = SelectPrimitive.Value;\n\nconst selectTriggerVariants = cva(\n cn(\n \"flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background\",\n \"whitespace-nowrap text-foreground shadow-xs\",\n \"transition-[border-color,box-shadow] duration-[var(--duration-fast)]\",\n \"data-[placeholder]:text-muted-foreground\",\n \"focus-visible:border-ring\",\n \"disabled:cursor-not-allowed disabled:opacity-55\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0\",\n \"*:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2\",\n focusRing,\n invalidStyles,\n ),\n {\n variants: {\n triggerSize: {\n sm: \"h-8 px-2.5 text-sm\",\n md: \"h-9 px-3 text-sm\",\n lg: \"h-10 px-3.5 text-base\",\n },\n },\n defaultVariants: {\n triggerSize: \"md\",\n },\n },\n);\n\nexport interface SelectTriggerProps\n extends\n ComponentPropsWithRef<typeof SelectPrimitive.Trigger>,\n VariantProps<typeof selectTriggerVariants> {}\n\nexport function SelectTrigger({\n className,\n triggerSize,\n children,\n ...props\n}: SelectTriggerProps) {\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n className={cn(selectTriggerVariants({ triggerSize }), className)}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-4 opacity-60\">\n <path\n d=\"m7 10 5 5 5-5\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n}\n\n/** Scroll affordance shown only when the list overflows. Decorative. */\nfunction ScrollButton({\n direction,\n className,\n ...props\n}: {\n direction: \"up\" | \"down\";\n} & ComponentPropsWithRef<typeof SelectPrimitive.ScrollUpButton>) {\n const Comp =\n direction === \"up\" ? SelectPrimitive.ScrollUpButton : SelectPrimitive.ScrollDownButton;\n\n return (\n <Comp\n data-slot={`select-scroll-${direction}`}\n className={cn(\"flex cursor-default items-center justify-center py-1\", className)}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-4 opacity-60\">\n <path\n d={direction === \"up\" ? \"m7 14 5-5 5 5\" : \"m7 10 5 5 5-5\"}\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </Comp>\n );\n}\n\nexport type SelectContentProps = ComponentPropsWithRef<typeof SelectPrimitive.Content>;\n\nexport function SelectContent({\n className,\n children,\n position = \"popper\",\n sideOffset = 6,\n ...props\n}: SelectContentProps) {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n data-slot=\"select-content\"\n position={position}\n sideOffset={position === \"popper\" ? sideOffset : undefined}\n className={cn(\n \"relative z-[var(--z-popover)] max-h-[var(--radix-select-content-available-height)]\",\n \"min-w-32 overflow-x-hidden overflow-y-auto rounded-lg border border-border bg-popover\",\n \"p-1 text-popover-foreground shadow-lg\",\n \"origin-[var(--radix-select-content-transform-origin)]\",\n \"data-[state=closed]:animate-float-out data-[state=open]:animate-float-in\",\n position === \"popper\" && \"w-full min-w-[var(--radix-select-trigger-width)]\",\n className,\n )}\n {...props}\n >\n <ScrollButton direction=\"up\" />\n <SelectPrimitive.Viewport className=\"p-0\">{children}</SelectPrimitive.Viewport>\n <ScrollButton direction=\"down\" />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n}\n\nexport interface SelectItemProps extends ComponentPropsWithRef<typeof SelectPrimitive.Item> {\n /**\n * What the trigger shows once this option is chosen.\n *\n * The primitive clones the item's text into the trigger, so a rich option —\n * a name with a description under it — would otherwise drag all of that into\n * a one-line trigger. Give the label here and the rest as children.\n */\n label?: ReactNode;\n}\n\nexport function SelectItem({ className, label, children, ...props }: SelectItemProps) {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(\n \"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\",\n \"transition-colors duration-[var(--duration-instant)]\",\n \"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground\",\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-55\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className,\n )}\n {...props}\n >\n {label === undefined ? (\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n ) : (\n <>\n <SelectPrimitive.ItemText>{label}</SelectPrimitive.ItemText>\n {children}\n </>\n )}\n {/* Decorative: selection is already announced through aria-selected. */}\n <span className=\"absolute end-2 grid size-4 place-items-center\">\n <SelectPrimitive.ItemIndicator>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5\">\n <path\n d=\"m5 13 4 4L19 7\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </SelectPrimitive.ItemIndicator>\n </span>\n </SelectPrimitive.Item>\n );\n}\n\nexport function SelectLabel({\n className,\n ...props\n}: ComponentPropsWithRef<typeof SelectPrimitive.Label>) {\n return (\n <SelectPrimitive.Label\n data-slot=\"select-label\"\n className={cn(\"px-2 py-1.5 text-xs font-medium text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport function SelectSeparator({\n className,\n ...props\n}: ComponentPropsWithRef<typeof SelectPrimitive.Separator>) {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n {...props}\n />\n );\n}\n\nexport { selectTriggerVariants };\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sheet.d.ts","names":[],"sources":["../../../src/components/sheet/sheet.tsx"],"mappings":";;;;;;;;;;;;cAiBa,uBAAK,GAAA,OAAA;cACL,8BAAY,0BAAA,OAAA,qCAAA,cAAA;cACZ,6BAAW,GAAA,OAAA;cACX,4BAAU,0BAAA,OAAA,mCAAA,cAAA;iBAEP,eACd,cACG,SACF,6BAA6B,OAAe,2BAAQ,IAAA;cAcjD,gBAAa;;6CAoBlB;UAEgB,0BAEb,6BAA6B,OAAe,UAC5C,oBAAoB;EACtB;;iBAGc,eACd,WACA,UACA,MACA,oBACG,SACF,oCAAiB,IAAA;iBAoCJ,cAAc,cAAc,SAAS,+CAA4B,IAAA;iBAUjE,cAAc,cAAc,SAAS,+CAA4B,IAAA;iBAajE,aACd,cACG,SACF,6BAA6B,OAAe,yBAAM,IAAA;iBAUrC,mBACd,cACG,SACF,6BAA6B,OAAe,+BAAY,IAAA"}
1
+ {"version":3,"file":"sheet.d.ts","names":[],"sources":["../../../src/components/sheet/sheet.tsx"],"mappings":";;;;;;;;;;;;cAiBa,uBAAK,GAAA,OAAA;cACL,8BAAY,0BAAA,OAAA,qCAAA,cAAA;cACZ,6BAAW,GAAA,OAAA;cACX,4BAAU,0BAAA,OAAA,mCAAA,cAAA;iBAEP,eACd,cACG,SACF,6BAA6B,OAAe,2BAAQ,IAAA;cAcjD,gBAAa;;6CAwBlB;UAEgB,0BAEb,6BAA6B,OAAe,UAC5C,oBAAoB;EACtB;;iBAGc,eACd,WACA,UACA,MACA,oBACG,SACF,oCAAiB,IAAA;iBAoCJ,cAAc,cAAc,SAAS,+CAA4B,IAAA;iBAUjE,cAAc,cAAc,SAAS,+CAA4B,IAAA;iBAajE,aACd,cACG,SACF,6BAA6B,OAAe,yBAAM,IAAA;iBAUrC,mBACd,cACG,SACF,6BAA6B,OAAe,+BAAY,IAAA"}