astralyx-ui 0.1.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 (279) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/bin/index.d.ts +2 -0
  4. package/bin/index.js +404 -0
  5. package/bin/registry.d.ts +37 -0
  6. package/bin/registry.js +69 -0
  7. package/package.json +89 -0
  8. package/registry/index.json +3914 -0
  9. package/registry/items/accordion.json +22 -0
  10. package/registry/items/address-input.json +21 -0
  11. package/registry/items/agent-steps.json +23 -0
  12. package/registry/items/alert-dialog.json +22 -0
  13. package/registry/items/alert-triage.json +25 -0
  14. package/registry/items/alert.json +21 -0
  15. package/registry/items/anomaly-chart.json +18 -0
  16. package/registry/items/api-keys.json +25 -0
  17. package/registry/items/aspect-ratio.json +18 -0
  18. package/registry/items/attachment-preview.json +23 -0
  19. package/registry/items/audit-log.json +26 -0
  20. package/registry/items/avatar.json +21 -0
  21. package/registry/items/badge.json +22 -0
  22. package/registry/items/banner.json +22 -0
  23. package/registry/items/benchmark-table.json +20 -0
  24. package/registry/items/bet-input.json +23 -0
  25. package/registry/items/blame-view.json +22 -0
  26. package/registry/items/branch-select.json +21 -0
  27. package/registry/items/breadcrumb.json +22 -0
  28. package/registry/items/bridge-status.json +24 -0
  29. package/registry/items/build-log.json +23 -0
  30. package/registry/items/bulk-action-bar.json +21 -0
  31. package/registry/items/bundle-treemap.json +20 -0
  32. package/registry/items/button-group.json +21 -0
  33. package/registry/items/button.json +22 -0
  34. package/registry/items/cache-stats.json +21 -0
  35. package/registry/items/calendar.json +25 -0
  36. package/registry/items/campaign-card.json +22 -0
  37. package/registry/items/candlestick-chart.json +18 -0
  38. package/registry/items/card-input.json +22 -0
  39. package/registry/items/card.json +21 -0
  40. package/registry/items/carousel.json +22 -0
  41. package/registry/items/cart.json +23 -0
  42. package/registry/items/chain-select.json +24 -0
  43. package/registry/items/changelog-entry.json +21 -0
  44. package/registry/items/chart.json +19 -0
  45. package/registry/items/checkbox.json +22 -0
  46. package/registry/items/checkout-summary.json +20 -0
  47. package/registry/items/citations.json +22 -0
  48. package/registry/items/code-block.json +23 -0
  49. package/registry/items/code-search.json +22 -0
  50. package/registry/items/cohort-table.json +19 -0
  51. package/registry/items/collapsible.json +23 -0
  52. package/registry/items/color-picker.json +24 -0
  53. package/registry/items/combobox.json +23 -0
  54. package/registry/items/command.json +23 -0
  55. package/registry/items/commit-graph.json +20 -0
  56. package/registry/items/commit-list.json +26 -0
  57. package/registry/items/composer.json +30 -0
  58. package/registry/items/confidence-meter.json +18 -0
  59. package/registry/items/connection-string.json +24 -0
  60. package/registry/items/container-list.json +24 -0
  61. package/registry/items/context-menu.json +20 -0
  62. package/registry/items/context-picker.json +25 -0
  63. package/registry/items/coordinate-input.json +21 -0
  64. package/registry/items/copy-button.json +22 -0
  65. package/registry/items/cost-breakdown.json +20 -0
  66. package/registry/items/coverage-report.json +22 -0
  67. package/registry/items/cron-schedule.json +24 -0
  68. package/registry/items/curl-command.json +18 -0
  69. package/registry/items/data-grid.json +24 -0
  70. package/registry/items/date-picker.json +24 -0
  71. package/registry/items/date-range-compare.json +24 -0
  72. package/registry/items/dependency-list.json +23 -0
  73. package/registry/items/deploy-list.json +27 -0
  74. package/registry/items/description-list.json +18 -0
  75. package/registry/items/device-list.json +24 -0
  76. package/registry/items/dialog.json +24 -0
  77. package/registry/items/diff-proposal.json +23 -0
  78. package/registry/items/diff-stat.json +18 -0
  79. package/registry/items/diff-view.json +23 -0
  80. package/registry/items/donut.json +19 -0
  81. package/registry/items/drawer.json +19 -0
  82. package/registry/items/dropdown-menu.json +24 -0
  83. package/registry/items/dropzone.json +22 -0
  84. package/registry/items/empty.json +19 -0
  85. package/registry/items/endpoint-list.json +24 -0
  86. package/registry/items/env-diff.json +23 -0
  87. package/registry/items/env-vars.json +24 -0
  88. package/registry/items/error-boundary.json +22 -0
  89. package/registry/items/eval-results.json +21 -0
  90. package/registry/items/feature-flag.json +23 -0
  91. package/registry/items/feedback.json +23 -0
  92. package/registry/items/field.json +19 -0
  93. package/registry/items/file-tree.json +21 -0
  94. package/registry/items/fingerprint-diff.json +23 -0
  95. package/registry/items/flame-graph.json +19 -0
  96. package/registry/items/fmt.json +16 -0
  97. package/registry/items/form.json +21 -0
  98. package/registry/items/fraud-verdict.json +24 -0
  99. package/registry/items/funnel.json +18 -0
  100. package/registry/items/gas-tracker.json +21 -0
  101. package/registry/items/gauge.json +18 -0
  102. package/registry/items/governance-proposal.json +24 -0
  103. package/registry/items/group.json +21 -0
  104. package/registry/items/health-checks.json +23 -0
  105. package/registry/items/heatmap-grid.json +19 -0
  106. package/registry/items/hover-card.json +20 -0
  107. package/registry/items/http-status.json +19 -0
  108. package/registry/items/incident-card.json +24 -0
  109. package/registry/items/input-file.json +22 -0
  110. package/registry/items/input-otp.json +19 -0
  111. package/registry/items/input.json +23 -0
  112. package/registry/items/invoice-list.json +24 -0
  113. package/registry/items/ip-cluster.json +22 -0
  114. package/registry/items/json-viewer.json +21 -0
  115. package/registry/items/jwt-inspector.json +25 -0
  116. package/registry/items/kanban.json +20 -0
  117. package/registry/items/kbd.json +18 -0
  118. package/registry/items/label-picker.json +24 -0
  119. package/registry/items/label.json +18 -0
  120. package/registry/items/leaderboard.json +23 -0
  121. package/registry/items/ledger-table.json +22 -0
  122. package/registry/items/lib-date.json +14 -0
  123. package/registry/items/lib-highlighter.json +16 -0
  124. package/registry/items/lib-styles.json +14 -0
  125. package/registry/items/lib-use-clipboard.json +14 -0
  126. package/registry/items/lib-utils.json +16 -0
  127. package/registry/items/lighthouse-score.json +19 -0
  128. package/registry/items/liquidity-position.json +23 -0
  129. package/registry/items/live-announcer.json +16 -0
  130. package/registry/items/location-picker.json +26 -0
  131. package/registry/items/log-viewer.json +24 -0
  132. package/registry/items/login-form.json +26 -0
  133. package/registry/items/map-embed.json +22 -0
  134. package/registry/items/market-table.json +23 -0
  135. package/registry/items/mask-input.json +19 -0
  136. package/registry/items/media-gallery.json +25 -0
  137. package/registry/items/mention-input.json +21 -0
  138. package/registry/items/menubar.json +21 -0
  139. package/registry/items/merge-conflict.json +20 -0
  140. package/registry/items/message.json +25 -0
  141. package/registry/items/migration-list.json +23 -0
  142. package/registry/items/mint-panel.json +23 -0
  143. package/registry/items/model-comparison.json +24 -0
  144. package/registry/items/model-select.json +20 -0
  145. package/registry/items/moderation-queue.json +26 -0
  146. package/registry/items/money-input.json +19 -0
  147. package/registry/items/multi-select.json +23 -0
  148. package/registry/items/multiplier-chart.json +18 -0
  149. package/registry/items/navigation-menu.json +23 -0
  150. package/registry/items/network-status.json +23 -0
  151. package/registry/items/nft-card.json +23 -0
  152. package/registry/items/notification-inbox.json +25 -0
  153. package/registry/items/number-input.json +21 -0
  154. package/registry/items/odds-display.json +21 -0
  155. package/registry/items/on-call-schedule.json +25 -0
  156. package/registry/items/order-book.json +19 -0
  157. package/registry/items/page-header.json +18 -0
  158. package/registry/items/pagination.json +21 -0
  159. package/registry/items/password-input.json +21 -0
  160. package/registry/items/payment-method.json +23 -0
  161. package/registry/items/payout-status.json +24 -0
  162. package/registry/items/permission-matrix.json +22 -0
  163. package/registry/items/pipeline.json +23 -0
  164. package/registry/items/popover.json +22 -0
  165. package/registry/items/port-table.json +22 -0
  166. package/registry/items/portfolio-balance.json +22 -0
  167. package/registry/items/postback-config.json +26 -0
  168. package/registry/items/presence.json +20 -0
  169. package/registry/items/price-ticker.json +21 -0
  170. package/registry/items/pricing-table.json +24 -0
  171. package/registry/items/primitive-collapsible.json +16 -0
  172. package/registry/items/primitive-dialog.json +16 -0
  173. package/registry/items/primitive-dismissable.json +14 -0
  174. package/registry/items/primitive-field.json +16 -0
  175. package/registry/items/primitive-focus-trap.json +14 -0
  176. package/registry/items/primitive-media-query.json +14 -0
  177. package/registry/items/primitive-popper.json +14 -0
  178. package/registry/items/primitive-portal.json +14 -0
  179. package/registry/items/primitive-radio-group.json +16 -0
  180. package/registry/items/primitive-router.json +14 -0
  181. package/registry/items/primitive-slot.json +16 -0
  182. package/registry/items/primitive-tabs.json +16 -0
  183. package/registry/items/progress.json +21 -0
  184. package/registry/items/prompt-input.json +25 -0
  185. package/registry/items/prompt-template.json +23 -0
  186. package/registry/items/pull-request-card.json +25 -0
  187. package/registry/items/query-editor.json +25 -0
  188. package/registry/items/query-plan.json +23 -0
  189. package/registry/items/queue-monitor.json +22 -0
  190. package/registry/items/radio-group.json +22 -0
  191. package/registry/items/range-slider.json +19 -0
  192. package/registry/items/rate-limit-meter.json +20 -0
  193. package/registry/items/rating.json +21 -0
  194. package/registry/items/reasoning-block.json +23 -0
  195. package/registry/items/regex-tester.json +25 -0
  196. package/registry/items/release-list.json +22 -0
  197. package/registry/items/request-builder.json +28 -0
  198. package/registry/items/resizable.json +22 -0
  199. package/registry/items/resource-meter.json +19 -0
  200. package/registry/items/response-viewer.json +26 -0
  201. package/registry/items/retrieval-results.json +23 -0
  202. package/registry/items/revenue-share-table.json +20 -0
  203. package/registry/items/review-thread.json +25 -0
  204. package/registry/items/risk-score.json +18 -0
  205. package/registry/items/root-cause-tree.json +20 -0
  206. package/registry/items/round-history.json +21 -0
  207. package/registry/items/runbook-steps.json +24 -0
  208. package/registry/items/schema-table.json +22 -0
  209. package/registry/items/schema-viewer.json +22 -0
  210. package/registry/items/scroll-area.json +18 -0
  211. package/registry/items/seed-phrase.json +22 -0
  212. package/registry/items/select.json +24 -0
  213. package/registry/items/separator.json +18 -0
  214. package/registry/items/service-status.json +21 -0
  215. package/registry/items/session-list.json +24 -0
  216. package/registry/items/sheet.json +24 -0
  217. package/registry/items/shortcut-sheet.json +19 -0
  218. package/registry/items/sidebar.json +24 -0
  219. package/registry/items/skeleton.json +21 -0
  220. package/registry/items/skip-link.json +19 -0
  221. package/registry/items/slider.json +21 -0
  222. package/registry/items/slo-budget.json +19 -0
  223. package/registry/items/sparkline.json +18 -0
  224. package/registry/items/spinner.json +20 -0
  225. package/registry/items/stack-trace.json +23 -0
  226. package/registry/items/staking-panel.json +24 -0
  227. package/registry/items/stat.json +21 -0
  228. package/registry/items/status-checks.json +23 -0
  229. package/registry/items/stepper.json +22 -0
  230. package/registry/items/streaming-text.json +19 -0
  231. package/registry/items/subscription-state.json +22 -0
  232. package/registry/items/suggestions.json +19 -0
  233. package/registry/items/swap-panel.json +22 -0
  234. package/registry/items/switch.json +21 -0
  235. package/registry/items/symbol-outline.json +22 -0
  236. package/registry/items/table-of-contents.json +19 -0
  237. package/registry/items/table.json +19 -0
  238. package/registry/items/tabs.json +22 -0
  239. package/registry/items/tag-input.json +21 -0
  240. package/registry/items/terminal.json +20 -0
  241. package/registry/items/test-results.json +24 -0
  242. package/registry/items/textarea.json +21 -0
  243. package/registry/items/theme-toggle.json +20 -0
  244. package/registry/items/theme.json +18 -0
  245. package/registry/items/thread-list.json +24 -0
  246. package/registry/items/ticket-card.json +24 -0
  247. package/registry/items/time-picker.json +23 -0
  248. package/registry/items/timeline.json +19 -0
  249. package/registry/items/timezone-select.json +18 -0
  250. package/registry/items/toast.json +22 -0
  251. package/registry/items/toggle.json +21 -0
  252. package/registry/items/token-amount.json +18 -0
  253. package/registry/items/token-approvals.json +25 -0
  254. package/registry/items/token-select.json +25 -0
  255. package/registry/items/token-usage.json +20 -0
  256. package/registry/items/tool-call.json +24 -0
  257. package/registry/items/toolbar.json +19 -0
  258. package/registry/items/tooltip.json +19 -0
  259. package/registry/items/topology-map.json +20 -0
  260. package/registry/items/tour.json +25 -0
  261. package/registry/items/traffic-split.json +20 -0
  262. package/registry/items/transaction-list.json +23 -0
  263. package/registry/items/transaction-status.json +25 -0
  264. package/registry/items/tree.json +21 -0
  265. package/registry/items/two-factor-setup.json +25 -0
  266. package/registry/items/typing-indicator.json +19 -0
  267. package/registry/items/typography.json +18 -0
  268. package/registry/items/upload-list.json +24 -0
  269. package/registry/items/uptime-strip.json +19 -0
  270. package/registry/items/user-menu.json +24 -0
  271. package/registry/items/utm-builder.json +24 -0
  272. package/registry/items/validator-list.json +23 -0
  273. package/registry/items/video-player.json +23 -0
  274. package/registry/items/virtual-list.json +18 -0
  275. package/registry/items/visually-hidden.json +19 -0
  276. package/registry/items/wallet-address.json +22 -0
  277. package/registry/items/wallet-connect.json +25 -0
  278. package/registry/items/webhook-inspector.json +26 -0
  279. package/registry/items/websocket-frames.json +24 -0
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "reasoning-block",
3
+ "type": "registry:ui",
4
+ "title": "Reasoning Block",
5
+ "description": "A model's intermediate reasoning, folded away by default — it is supporting evidence, not the answer, and a panel that opens itself pushes the answer below the fold.",
6
+ "category": "AI",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "fmt",
12
+ "lib-styles",
13
+ "lib-utils",
14
+ "spinner"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "components/ui/reasoning-block.tsx",
19
+ "type": "registry:ui",
20
+ "content": "import { useState, type ComponentProps, type ReactNode } from 'react'\nimport { Brain, ChevronDown, ChevronRight } from 'lucide-react'\nimport { Fmt } from '@/components/ui/fmt'\nimport { Spinner } from '@/components/ui/spinner'\nimport { focusRing, interactive, radius } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A model's intermediate reasoning, folded away by default.\n *\n * Collapsed by default on purpose: reasoning is supporting evidence, not the\n * answer, and a panel that opens itself pushes the thing the reader asked for\n * below the fold.\n *\n * While streaming it opens itself and shows a spinner, because during that time\n * it is the only thing happening — then collapses once the answer starts. That\n * transition is the whole interaction, so `streaming` is a prop rather than\n * something inferred from children changing.\n */\nfunction ReasoningBlock({\n children,\n streaming = false,\n duration,\n defaultOpen,\n label = 'Reasoning',\n className,\n ...props\n}: ComponentProps<'div'> & {\n streaming?: boolean\n /** Seconds spent thinking. Shown once finished. */\n duration?: number\n defaultOpen?: boolean\n label?: ReactNode\n}) {\n const [open, setOpen] = useState(defaultOpen ?? streaming)\n const Chevron = open ? ChevronDown : ChevronRight\n\n return (\n <div\n data-slot=\"reasoning-block\"\n data-streaming={streaming || undefined}\n className={cn('border-border overflow-hidden border', radius.surface, className)}\n {...props}\n >\n <button\n type=\"button\"\n aria-expanded={open}\n onClick={() => setOpen((value) => !value)}\n className={cn(\n 'text-muted-foreground hover:text-foreground hover:bg-accent/40 flex w-full items-center gap-2 px-3 py-2',\n interactive,\n focusRing,\n )}\n >\n <Chevron className=\"size-4 shrink-0\" aria-hidden=\"true\" />\n {streaming ? (\n <Spinner size=\"xs\" className=\"shrink-0\" label=\"Thinking\" />\n ) : (\n <Brain className=\"size-4 shrink-0\" aria-hidden=\"true\" />\n )}\n <span className=\"min-w-0 flex-1 truncate text-start text-xs font-medium\">\n {streaming ? 'Thinking…' : label}\n </span>\n {!streaming && duration !== undefined && (\n <span className=\"shrink-0 text-xs tabular-nums\">\n <Fmt type=\"duration\" value={duration} />\n </span>\n )}\n </button>\n\n {open && (\n <div className=\"border-border text-muted-foreground border-t p-3 text-sm\">\n {children}\n </div>\n )}\n </div>\n )\n}\n\nexport { ReasoningBlock }\n"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "regex-tester",
3
+ "type": "registry:ui",
4
+ "title": "Regex Tester",
5
+ "description": "A pattern against a test string, with matches highlighted and capture groups listed. Zero-length matches are guarded — `/a*/g` matches empty at every position and hangs a naive exec loop.",
6
+ "category": "Code Tools",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "badge",
12
+ "input",
13
+ "label",
14
+ "lib-styles",
15
+ "lib-utils",
16
+ "textarea"
17
+ ],
18
+ "files": [
19
+ {
20
+ "path": "components/ui/regex-tester.tsx",
21
+ "type": "registry:ui",
22
+ "content": "import { useMemo, useState, type ComponentProps, type ReactNode } from 'react'\nimport { TriangleAlert } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Textarea } from '@/components/ui/textarea'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A regular expression against a test string, with matches highlighted.\n *\n * The pattern is compiled in a try/catch and the error is shown as you type. An\n * unfinished pattern is the normal state while typing, so a syntax error is a\n * hint, not a failure — the previous good match set is not thrown away.\n *\n * Zero-length matches are guarded. `/a*\\/g` matches empty at every position and\n * a naive `exec` loop over it never terminates; the index is advanced manually\n * when a match consumes nothing. This is the bug in most hand-rolled regex\n * testers.\n *\n * Named and numbered capture groups are listed per match. A pattern that\n * matches but captures the wrong thing is the failure people actually hit, and\n * a highlight alone cannot show it.\n */\nexport type RegexMatch = {\n index: number\n text: string\n groups: { name: string; value: string | undefined }[]\n}\n\nconst FLAG_SET = ['g', 'i', 'm', 's', 'u', 'y'] as const\n\nfunction RegexTester({\n pattern: patternProp,\n onPatternChange,\n flags: flagsProp,\n onFlagsChange,\n input: inputProp,\n onInputChange,\n patternLabel = 'Pattern',\n flagsLabel = 'Flags',\n inputLabel = 'Test string',\n matchesLabel = 'Matches',\n noMatchLabel = 'No matches',\n groupsLabel = 'Groups',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'onChange' | 'children'> & {\n pattern?: string\n onPatternChange?: (pattern: string) => void\n flags?: string\n onFlagsChange?: (flags: string) => void\n input?: string\n onInputChange?: (input: string) => void\n patternLabel?: ReactNode\n flagsLabel?: ReactNode\n inputLabel?: ReactNode\n matchesLabel?: ReactNode\n noMatchLabel?: ReactNode\n groupsLabel?: ReactNode\n}) {\n const [ownPattern, setOwnPattern] = useState(patternProp ?? '')\n const [ownFlags, setOwnFlags] = useState(flagsProp ?? 'g')\n const [ownInput, setOwnInput] = useState(inputProp ?? '')\n\n const pattern = patternProp ?? ownPattern\n const flags = flagsProp ?? ownFlags\n const input = inputProp ?? ownInput\n\n const { matches, error } = useMemo(() => {\n if (!pattern) return { matches: [] as RegexMatch[], error: undefined }\n let regex: RegExp\n try {\n regex = new RegExp(pattern, flags.includes('g') ? flags : `${flags}g`)\n } catch (exception) {\n return { matches: [] as RegexMatch[], error: (exception as Error).message }\n }\n\n const found: RegexMatch[] = []\n let hit: RegExpExecArray | null\n let guard = 0\n while ((hit = regex.exec(input)) && guard++ < 5000) {\n found.push({\n index: hit.index,\n text: hit[0],\n groups: [\n ...hit.slice(1).map((value, i) => ({ name: String(i + 1), value })),\n ...Object.entries(hit.groups ?? {}).map(([name, value]) => ({ name, value })),\n ],\n })\n // A zero-length match leaves lastIndex where it was; without this the\n // loop never terminates.\n if (hit[0] === '') regex.lastIndex++\n }\n return { matches: found, error: undefined }\n }, [pattern, flags, input])\n\n /** Splits the input into plain and matched runs for highlighting. */\n const segments = useMemo(() => {\n const out: { text: string; hit: boolean }[] = []\n let cursor = 0\n for (const match of matches) {\n if (match.index > cursor) out.push({ text: input.slice(cursor, match.index), hit: false })\n out.push({ text: match.text, hit: true })\n cursor = match.index + match.text.length\n }\n if (cursor < input.length) out.push({ text: input.slice(cursor), hit: false })\n return out\n }, [matches, input])\n\n const setPattern = (next: string) => {\n if (patternProp === undefined) setOwnPattern(next)\n onPatternChange?.(next)\n }\n const setFlags = (next: string) => {\n if (flagsProp === undefined) setOwnFlags(next)\n onFlagsChange?.(next)\n }\n const setInput = (next: string) => {\n if (inputProp === undefined) setOwnInput(next)\n onInputChange?.(next)\n }\n\n return (\n <div\n data-slot=\"regex-tester\"\n className={cn(surface, radius.surface, 'flex flex-col gap-3 p-4', className)}\n {...props}\n >\n <div className=\"flex flex-wrap items-end gap-2\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n <Label htmlFor=\"regex-pattern\">{patternLabel}</Label>\n <Input\n id=\"regex-pattern\"\n value={pattern}\n error={Boolean(error)}\n placeholder=\"(\\w+)@(\\w+)\\.com\"\n className=\"font-mono text-xs\"\n onChange={(event) => setPattern(event.target.value)}\n />\n </div>\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"regex-flags\">{flagsLabel}</Label>\n <Input\n id=\"regex-flags\"\n value={flags}\n placeholder=\"gi\"\n containerClassName=\"w-24\"\n className=\"font-mono text-xs\"\n onChange={(event) =>\n setFlags([...new Set(event.target.value.split(''))]\n .filter((f) => (FLAG_SET as readonly string[]).includes(f))\n .join(''))\n }\n />\n </div>\n </div>\n\n {error && (\n <p className=\"flex items-start gap-1.5 text-xs text-[var(--destructive-soft-foreground)]\">\n <TriangleAlert className=\"mt-0.5 size-3.5 shrink-0\" aria-hidden=\"true\" />\n {error}\n </p>\n )}\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"regex-input\">{inputLabel}</Label>\n <Textarea\n id=\"regex-input\"\n rows={4}\n value={input}\n className=\"font-mono text-xs\"\n onChange={(event) => setInput(event.target.value)}\n />\n </div>\n\n <div className={cn('bg-secondary/60 p-3 font-mono text-xs whitespace-pre-wrap', radius.control)}>\n {segments.length === 0 ? (\n <span className=\"text-muted-foreground/60\">{noMatchLabel}</span>\n ) : (\n segments.map((segment, index) =>\n segment.hit ? (\n <mark\n key={index}\n className=\"bg-[color-mix(in_oklab,var(--amber)_35%,transparent)] text-foreground\"\n >\n {segment.text}\n </mark>\n ) : (\n <span key={index}>{segment.text}</span>\n ),\n )\n )}\n </div>\n\n <div className=\"flex flex-col gap-2\">\n <span className=\"text-muted-foreground flex items-center gap-2 text-xs font-medium\">\n {matchesLabel}\n <Badge size=\"sm\">{matches.length}</Badge>\n </span>\n\n {matches.length > 0 && (\n <ul className=\"flex list-none flex-col gap-1.5\">\n {matches.slice(0, 20).map((match, index) => (\n <li key={index} className=\"flex flex-wrap items-baseline gap-x-3 gap-y-1 text-xs\">\n <span className=\"text-muted-foreground/60 w-8 shrink-0 text-end tabular-nums\">\n {match.index}\n </span>\n <code className=\"font-mono\">{match.text || '∅'}</code>\n {/* A pattern that matches but captures the wrong thing is the\n failure people actually hit; a highlight cannot show it. */}\n {match.groups.length > 0 && (\n <span className=\"text-muted-foreground flex flex-wrap gap-x-2\">\n <span className=\"opacity-70\">{groupsLabel}:</span>\n {match.groups.map((group) => (\n <span key={group.name} className=\"font-mono\">\n {group.name}={group.value === undefined ? '—' : `\"${group.value}\"`}\n </span>\n ))}\n </span>\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n )\n}\n\nexport { RegexTester }\n"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "release-list",
3
+ "type": "registry:ui",
4
+ "title": "Release List",
5
+ "description": "A changelog as a document — each version an article with a real heading, so it can be linked to and navigated by heading. Never sorts, since \"newest first\" cannot be derived from semver once pre-releases exist.",
6
+ "category": "Development",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "badge",
12
+ "lib-styles",
13
+ "lib-utils"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/ui/release-list.tsx",
18
+ "type": "registry:ui",
19
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { Tag } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A changelog: versions newest first, each with its notes.\n *\n * The version is an `<h3>` inside an article rather than a styled div, because\n * a changelog is a document — someone lands on it from a link and navigates it\n * by heading. Order is the caller's; this never sorts, since \"newest first\"\n * cannot be derived from a semver string alone once pre-releases are involved.\n */\nexport type Release = {\n version: string\n date?: Date\n /** Highlighted as the current or newest release. */\n current?: boolean\n tag?: ReactNode\n notes?: ReactNode\n /** Grouped bullets — Added, Fixed, Changed. */\n sections?: { label: string; items: ReactNode[] }[]\n}\n\nfunction ReleaseList({\n releases,\n locale = 'en-GB',\n latestLabel = 'Latest',\n className,\n ...props\n}: ComponentProps<'div'> & {\n releases: Release[]\n locale?: string\n /** Badge on the newest release. */\n latestLabel?: ReactNode\n}) {\n const date = new Intl.DateTimeFormat(locale, {\n day: 'numeric',\n month: 'long',\n year: 'numeric',\n })\n\n return (\n <div\n data-slot=\"release-list\"\n className={cn('flex flex-col gap-4', className)}\n {...props}\n >\n {releases.map((release) => (\n <article\n key={release.version}\n className={cn(surface, radius.surface, 'p-4')}\n >\n <header className=\"mb-3 flex flex-wrap items-center gap-2\">\n <Tag className=\"text-muted-foreground size-4 shrink-0\" aria-hidden=\"true\" />\n <h3 className=\"font-mono text-sm font-semibold\">{release.version}</h3>\n {release.current && (\n <Badge size=\"sm\" color=\"green\">\n {latestLabel}\n </Badge>\n )}\n {release.tag}\n {release.date && (\n <time\n dateTime={release.date.toISOString()}\n className=\"text-muted-foreground ms-auto text-xs\"\n >\n {date.format(release.date)}\n </time>\n )}\n </header>\n\n {release.notes && (\n <div className=\"text-muted-foreground mb-3 text-sm\">{release.notes}</div>\n )}\n\n {release.sections?.map((section) => (\n <section key={section.label} className=\"mb-3 last:mb-0\">\n <h4 className=\"mb-1 text-xs font-medium tracking-wide uppercase\">\n {section.label}\n </h4>\n <ul className=\"text-muted-foreground ms-4 flex list-disc flex-col gap-0.5 text-sm\">\n {section.items.map((item, index) => (\n <li key={index}>{item}</li>\n ))}\n </ul>\n </section>\n ))}\n </article>\n ))}\n </div>\n )\n}\n\nexport { ReleaseList }\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "request-builder",
3
+ "type": "registry:ui",
4
+ "title": "Request Builder",
5
+ "description": "Method, URL, headers, query and body. Query parameters are edited as rows and the assembled URL is shown beneath — hand-editing a query string is where encoding bugs come from.",
6
+ "category": "API",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "badge",
12
+ "button",
13
+ "collapsible",
14
+ "input",
15
+ "lib-styles",
16
+ "lib-utils",
17
+ "select",
18
+ "switch",
19
+ "textarea"
20
+ ],
21
+ "files": [
22
+ {
23
+ "path": "components/ui/request-builder.tsx",
24
+ "type": "registry:ui",
25
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { Plus, Send, Trash2 } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Select } from '@/components/ui/select'\nimport { Switch } from '@/components/ui/switch'\nimport { Badge } from '@/components/ui/badge'\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'\nimport { Textarea } from '@/components/ui/textarea'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Compose an HTTP request: method, URL, headers, query and body.\n *\n * Query parameters are edited as rows, not as a query string. Hand-editing a\n * URL is where encoding bugs come from — a `&` inside a value silently becomes\n * a parameter boundary — and the assembled URL is shown read-only underneath so\n * you can still see exactly what will be sent.\n *\n * Query, headers and body are stacked disclosures rather than tabs. Their\n * heights differ a lot, so a shared tab panel resizes the card on every switch\n * and the strip moves out from under the pointer. Stacking also lets you see\n * the parameters and the body at once, which is what composing a request takes.\n *\n * Rows can be disabled rather than only deleted. Toggling a header off to test\n * something and back on again is the actual workflow; deleting it means\n * retyping it.\n *\n * The body tab is disabled for methods that do not carry one. GET with a body\n * is legal in the RFC, widely unsupported in practice, and offering it invites\n * an afternoon debugging a proxy that stripped it.\n */\nexport type RequestRow = { id: string; name: string; value: string; enabled?: boolean }\n\nconst METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']\nconst BODYLESS = new Set(['GET', 'HEAD'])\n\nfunction RequestBuilder({\n method = 'GET',\n onMethodChange,\n url = '',\n onUrlChange,\n headers = [],\n onHeadersChange,\n params = [],\n onParamsChange,\n body = '',\n onBodyChange,\n onSend,\n sending = false,\n urlPlaceholder = 'https://api.example.com/v1/users',\n urlLabel = 'Request URL',\n sendLabel = 'Send',\n headersLabel = 'Headers',\n paramsLabel = 'Query',\n bodyLabel = 'Body',\n addLabel = 'Add row',\n removeLabel = 'Remove row',\n enableLabel = 'Include this row',\n nameePlaceholder = 'name',\n valuePlaceholder = 'value',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'onChange' | 'children'> & {\n method?: string\n onMethodChange?: (method: string) => void\n url?: string\n onUrlChange?: (url: string) => void\n headers?: RequestRow[]\n onHeadersChange?: (rows: RequestRow[]) => void\n params?: RequestRow[]\n onParamsChange?: (rows: RequestRow[]) => void\n body?: string\n onBodyChange?: (body: string) => void\n onSend?: () => void\n sending?: boolean\n urlPlaceholder?: string\n urlLabel?: string\n sendLabel?: ReactNode\n headersLabel?: ReactNode\n paramsLabel?: ReactNode\n bodyLabel?: ReactNode\n addLabel?: ReactNode\n removeLabel?: string\n enableLabel?: string\n nameePlaceholder?: string\n valuePlaceholder?: string\n}) {\n const bodyless = BODYLESS.has(method.toUpperCase())\n\n // Built with URL so values are encoded properly, not concatenated.\n const assembled = (() => {\n const active = params.filter((row) => row.enabled !== false && row.name)\n if (!url) return ''\n try {\n const parsed = new URL(url.includes('://') ? url : `https://${url}`)\n for (const row of active) parsed.searchParams.set(row.name, row.value)\n return parsed.toString()\n } catch {\n return url\n }\n })()\n\n const rowEditor = (\n rows: RequestRow[],\n onChange: ((rows: RequestRow[]) => void) | undefined,\n ) => (\n <div className=\"flex flex-col gap-2\">\n {rows.map((row, index) => (\n <div key={row.id} className=\"flex items-center gap-2\">\n {/* Disable, don't delete — toggling off and on is the real workflow. */}\n <Switch\n size=\"sm\"\n checked={row.enabled !== false}\n aria-label={enableLabel}\n onChange={(event) =>\n onChange?.(\n rows.map((r, i) => (i === index ? { ...r, enabled: event.target.checked } : r)),\n )\n }\n />\n <Input\n size=\"sm\"\n value={row.name}\n placeholder={nameePlaceholder}\n containerClassName=\"flex-1\"\n className=\"font-mono text-xs\"\n onChange={(event) =>\n onChange?.(rows.map((r, i) => (i === index ? { ...r, name: event.target.value } : r)))\n }\n />\n <Input\n size=\"sm\"\n value={row.value}\n placeholder={valuePlaceholder}\n containerClassName=\"flex-1\"\n className=\"font-mono text-xs\"\n onChange={(event) =>\n onChange?.(rows.map((r, i) => (i === index ? { ...r, value: event.target.value } : r)))\n }\n />\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={removeLabel}\n onClick={() => onChange?.(rows.filter((_, i) => i !== index))}\n >\n <Trash2 />\n </Button>\n </div>\n ))}\n <Button\n variant=\"ghost\"\n size=\"xs\"\n className=\"self-start\"\n onClick={() =>\n onChange?.([...rows, { id: `row-${Date.now()}`, name: '', value: '', enabled: true }])\n }\n >\n <Plus />\n {addLabel}\n </Button>\n </div>\n )\n\n return (\n <div\n data-slot=\"request-builder\"\n className={cn(surface, radius.surface, 'flex flex-col overflow-hidden', className)}\n {...props}\n >\n <div className=\"border-border flex flex-wrap items-center gap-2 border-b p-3\">\n <Select\n size=\"sm\"\n className=\"w-28 shrink-0\"\n aria-label=\"Method\"\n value={method}\n onValueChange={(next) => onMethodChange?.(next)}\n options={METHODS.map((m) => ({ value: m, label: m }))}\n />\n <Input\n size=\"sm\"\n value={url}\n placeholder={urlPlaceholder}\n aria-label={urlLabel}\n containerClassName=\"min-w-40 flex-1\"\n className=\"font-mono text-xs\"\n onChange={(event) => onUrlChange?.(event.target.value)}\n />\n {onSend && (\n <Button size=\"sm\" disabled={sending || !url} onClick={onSend}>\n <Send />\n {sendLabel}\n </Button>\n )}\n </div>\n\n <Section title={paramsLabel} count={params.length} defaultOpen>\n {rowEditor(params, onParamsChange)}\n </Section>\n\n <Section title={headersLabel} count={headers.length}>\n {rowEditor(headers, onHeadersChange)}\n </Section>\n\n {/* A GET body is legal in the RFC and widely stripped by proxies.\n Offering it invites an afternoon of debugging. */}\n {!bodyless && (\n <Section title={bodyLabel}>\n <Textarea\n rows={6}\n value={body}\n className=\"font-mono text-xs\"\n aria-label={typeof bodyLabel === 'string' ? bodyLabel : 'Body'}\n onChange={(event) => onBodyChange?.(event.target.value)}\n />\n </Section>\n )}\n\n {assembled && (\n <p className=\"border-border text-muted-foreground border-t p-3 font-mono text-xs break-all\">\n {assembled}\n </p>\n )}\n </div>\n )\n}\n\n/** One disclosure row, at module scope so it is a stable component type. */\nfunction Section({\n title,\n count,\n defaultOpen = false,\n children,\n}: {\n title: ReactNode\n count?: number\n defaultOpen?: boolean\n children: ReactNode\n}) {\n return (\n <Collapsible defaultOpen={defaultOpen} className=\"border-border border-t\">\n <CollapsibleTrigger className=\"hover:bg-accent/40 w-full p-3 text-start text-xs font-medium\">\n <span className=\"flex-1\">{title}</span>\n {count !== undefined && count > 0 && (\n <Badge size=\"sm\" color=\"neutral\">\n {count}\n </Badge>\n )}\n </CollapsibleTrigger>\n {/* Padding goes on the inner content, not on CollapsibleContent itself:\n that element's height is animated from its child's measured height,\n so padding on the wrapper is excluded from the measurement and the\n panel opens to the padding alone. */}\n <CollapsibleContent>\n <div className=\"p-3 pt-0\">{children}</div>\n </CollapsibleContent>\n </Collapsible>\n )\n}\n\nexport { RequestBuilder }\n"
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "resizable",
3
+ "type": "registry:ui",
4
+ "title": "Resizable",
5
+ "description": "Two panels with a draggable divider. Sizes are percentages, so the split survives a container resize — and the handle is a real slider, so arrow keys resize it too.",
6
+ "category": "Navigation",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "lib-styles",
12
+ "lib-utils",
13
+ "primitive-media-query"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/ui/resizable.tsx",
18
+ "type": "registry:ui",
19
+ "content": "import {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ComponentProps,\n type ReactNode,\n} from 'react'\nimport { GripVertical } from 'lucide-react'\nimport { useBreakpoint } from '@/components/primitives/media-query'\nimport { focusRing, type Responsive } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Two panels with a draggable divider between them.\n *\n * Sizes are percentages, so the split survives a container resize. Pointer\n * capture is what makes the drag reliable — without it, moving faster than the\n * browser repaints drops the pointer outside the handle and the drag stops.\n *\n * The handle is also a real slider: arrow keys move it, which is the only way a\n * keyboard user can resize at all.\n */\nfunction Resizable({\n handleLabel = 'Resize panels',\n className,\n orientation = 'horizontal',\n defaultSize = 50,\n minSize = 15,\n maxSize = 85,\n onResize,\n responsive = 'md',\n children,\n ...props\n}: Omit<ComponentProps<'div'>, 'children' | 'onResize'> & {\n orientation?: 'horizontal' | 'vertical'\n /** Percentage taken by the first panel. */\n defaultSize?: number\n minSize?: number\n maxSize?: number\n onResize?: (size: number) => void\n /**\n * Breakpoint a horizontal split stays side by side at. Below it the panels\n * stack, because two columns on a phone give each about 180px.\n *\n * Unlike the other responsive components this cannot be done in CSS: the\n * pointer maths, the arrow keys and the ARIA orientation all depend on which\n * axis is being dragged, so the orientation has to genuinely change.\n */\n responsive?: Responsive\n /** Accessible name for the drag handle. */\n handleLabel?: string\n children: [ReactNode, ReactNode]\n}) {\n const wide = useBreakpoint(responsive === false ? 'sm' : responsive)\n const vertical =\n orientation === 'vertical' || (responsive !== false && !wide)\n\n const [size, setSize] = useState(defaultSize)\n const containerRef = useRef<HTMLDivElement>(null)\n const dragging = useRef(false)\n\n const apply = useCallback(\n (next: number) => {\n const clamped = Math.min(maxSize, Math.max(minSize, next))\n setSize(clamped)\n onResize?.(clamped)\n },\n [minSize, maxSize, onResize],\n )\n\n const onPointerMove = useCallback(\n (event: PointerEvent) => {\n if (!dragging.current || !containerRef.current) return\n const rect = containerRef.current.getBoundingClientRect()\n const ratio = vertical\n ? (event.clientY - rect.top) / rect.height\n : (event.clientX - rect.left) / rect.width\n apply(ratio * 100)\n },\n [vertical, apply],\n )\n\n useEffect(() => {\n const stop = () => {\n dragging.current = false\n document.body.style.userSelect = ''\n }\n document.addEventListener('pointermove', onPointerMove)\n document.addEventListener('pointerup', stop)\n return () => {\n document.removeEventListener('pointermove', onPointerMove)\n document.removeEventListener('pointerup', stop)\n stop()\n }\n }, [onPointerMove])\n\n const step = (delta: number) => apply(size + delta)\n\n return (\n <div\n ref={containerRef}\n data-slot=\"resizable\"\n className={cn('flex w-full', vertical ? 'flex-col' : 'flex-row', className)}\n {...props}\n >\n {/* Each panel is a flex column, so a single child can take `flex-1` and\n fill it. `height: 100%` inside a panel would depend on the parent's\n height being definite, which it is not while the split is being\n dragged. */}\n <div\n style={{ flexBasis: `${size}%` }}\n className=\"flex min-h-0 min-w-0 flex-col overflow-hidden\"\n >\n {children[0]}\n </div>\n\n <div\n role=\"separator\"\n tabIndex={0}\n aria-orientation={vertical ? 'horizontal' : 'vertical'}\n aria-valuenow={Math.round(size)}\n aria-valuemin={minSize}\n aria-valuemax={maxSize}\n aria-label={handleLabel}\n onPointerDown={(event) => {\n event.preventDefault()\n dragging.current = true\n // Keeps events coming to this element even when the pointer outruns\n // it, which is what makes a fast drag not fall off the handle.\n event.currentTarget.setPointerCapture(event.pointerId)\n document.body.style.userSelect = 'none'\n }}\n onKeyDown={(event) => {\n const back = vertical ? 'ArrowUp' : 'ArrowLeft'\n const forward = vertical ? 'ArrowDown' : 'ArrowRight'\n if (event.key === back) {\n event.preventDefault()\n step(event.shiftKey ? -10 : -2)\n } else if (event.key === forward) {\n event.preventDefault()\n step(event.shiftKey ? 10 : 2)\n } else if (event.key === 'Home') {\n event.preventDefault()\n apply(minSize)\n } else if (event.key === 'End') {\n event.preventDefault()\n apply(maxSize)\n }\n }}\n className={cn(\n 'bg-border hover:bg-[var(--border-active)] group relative flex shrink-0 items-center justify-center transition-colors duration-150 ease-out motion-reduce:transition-none',\n focusRing,\n // `self-stretch` rather than `h-full`: the handle is a flex child, so\n // stretching fills the cross axis whether or not the container's\n // height is definite. `h-full` collapses the line to nothing when it\n // is not.\n vertical\n ? 'h-px w-full cursor-row-resize'\n : 'w-px cursor-col-resize self-stretch',\n )}\n >\n <span\n className={cn(\n 'bg-border group-hover:bg-[var(--border-active)] text-muted-foreground absolute grid place-items-center rounded-sm transition-colors duration-150 ease-out motion-reduce:transition-none',\n vertical ? 'h-3 w-6' : 'h-6 w-3',\n )}\n >\n <GripVertical className={cn('size-3', vertical && 'rotate-90')} />\n </span>\n </div>\n\n <div className=\"flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden\">\n {children[1]}\n </div>\n </div>\n )\n}\n\nexport { Resizable }\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "resource-meter",
3
+ "type": "registry:ui",
4
+ "title": "Resource Meter",
5
+ "description": "Used against a cap: build minutes, storage, seats, rate limits. Unlike Progress it can exceed 100%, because a quota can be 130% used and hiding that is how a bill becomes a surprise.",
6
+ "category": "Operations",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "fmt",
10
+ "lib-utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/resource-meter.tsx",
15
+ "type": "registry:ui",
16
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { Fmt } from '@/components/ui/fmt'\nimport { cn } from '@/lib/utils'\n\n/**\n * A used-against-cap bar: build minutes, storage, seats, rate limits.\n *\n * Not `Progress`, which reports how far along a task is. This reports\n * consumption of a fixed budget, so it warns as it fills and can overflow past\n * 100% — a task cannot be 130% done, but a quota can be 130% used, and hiding\n * that by clamping is how a bill becomes a surprise.\n */\nfunction ResourceMeter({\n label,\n used,\n cap,\n unit = 'number',\n tone,\n hint,\n size = 'default',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n label: ReactNode\n used: number\n cap: number\n /** How the numbers are written. */\n unit?: 'number' | 'bytes' | 'duration'\n /** Any CSS colour. Defaults to a threshold ramp. */\n tone?: string\n hint?: ReactNode\n size?: 'sm' | 'default'\n}) {\n const ratio = cap > 0 ? used / cap : 0\n const over = ratio > 1\n const percent = Math.round(ratio * 100)\n\n const colour =\n tone ??\n (over || ratio >= 0.95\n ? 'var(--destructive)'\n : ratio >= 0.8\n ? 'var(--amber)'\n : 'var(--blue)')\n\n return (\n <div\n data-slot=\"resource-meter\"\n className={cn('flex min-w-0 flex-col gap-1.5', className)}\n {...props}\n >\n <div className=\"flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5\">\n <span className={cn('font-medium', size === 'sm' ? 'text-xs' : 'text-sm')}>\n {label}\n </span>\n <span className=\"text-muted-foreground text-xs tabular-nums\">\n <Fmt type={unit} value={used} /> / <Fmt type={unit} value={cap} />\n <span className={cn('ms-1.5', over && 'text-[var(--destructive-soft-foreground)] font-medium')}>\n ({percent}%)\n </span>\n </span>\n </div>\n\n <div\n role=\"progressbar\"\n aria-valuenow={used}\n aria-valuemin={0}\n aria-valuemax={cap}\n aria-label={typeof label === 'string' ? label : undefined}\n className={cn(\n 'bg-secondary w-full overflow-hidden rounded-full [corner-shape:round]',\n size === 'sm' ? 'h-1.5' : 'h-2',\n )}\n >\n <div\n className=\"h-full rounded-full transition-[width,background-color] duration-300 ease-out [corner-shape:round] motion-reduce:transition-none\"\n style={{\n // Clamped for the bar only — the number above still says 130%.\n width: `${Math.min(ratio, 1) * 100}%`,\n backgroundColor: colour,\n }}\n />\n </div>\n\n {hint && <p className=\"text-muted-foreground/70 text-xs\">{hint}</p>}\n {over && !hint && (\n <p className=\"text-[var(--destructive-soft-foreground)] text-xs\">\n Over budget by <Fmt type={unit} value={used - cap} />\n </p>\n )}\n </div>\n )\n}\n\nexport { ResourceMeter }\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "response-viewer",
3
+ "type": "registry:ui",
4
+ "title": "Response Viewer",
5
+ "description": "Status, timing, size, headers and body. The three headline figures share one line because they are read together — a 200 that took 4 seconds is a different result from one that took 40ms.",
6
+ "category": "API",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "badge",
10
+ "code-block",
11
+ "collapsible",
12
+ "fmt",
13
+ "http-status",
14
+ "json-viewer",
15
+ "lib-highlighter",
16
+ "lib-styles",
17
+ "lib-utils"
18
+ ],
19
+ "files": [
20
+ {
21
+ "path": "components/ui/response-viewer.tsx",
22
+ "type": "registry:ui",
23
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { CodeBlock } from '@/components/ui/code-block'\nimport type { Language } from '@/lib/highlighter'\nimport { HttpStatus } from '@/components/ui/http-status'\nimport { JsonViewer, type Json } from '@/components/ui/json-viewer'\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'\nimport { Badge } from '@/components/ui/badge'\nimport { Fmt } from '@/components/ui/fmt'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * An HTTP response: status, timing, size, headers and body.\n *\n * Status, time and size sit together on one line because they are read\n * together — a 200 that took 4 seconds and returned 8MB is a different result\n * from a 200 that took 40ms, and separating them into panels makes that\n * comparison impossible.\n *\n * Body, headers and raw are stacked disclosures rather than tabs. Their heights\n * differ by an order of magnitude, so a shared tab panel resizes the card on\n * every switch — the strip moves out from under the pointer and the next click\n * lands on nothing. Stacking also lets headers and body be read together, which\n * is what diagnosing a response actually takes.\n *\n * A JSON body gets the tree view, anything else gets syntax-highlighted text.\n * Rendering JSON as a wall of text is technically honest and practically\n * useless; rendering HTML as a tree is nonsense.\n *\n * Header names are lower-cased for display. HTTP header names are\n * case-insensitive and servers are inconsistent about them, so preserving the\n * original casing means `Content-Type` and `content-type` sort apart in a list\n * someone is scanning alphabetically.\n */\nexport type ResponseHeader = { name: string; value: string }\n\nfunction ResponseViewer({\n status,\n statusText,\n durationMs,\n sizeBytes,\n headers = [],\n body,\n contentType,\n bodyLabel = 'Body',\n headersLabel = 'Headers',\n rawLabel = 'Raw',\n emptyBodyLabel = 'No body',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n status: number\n statusText?: string\n durationMs?: number\n sizeBytes?: number\n headers?: ResponseHeader[]\n /** A string, or a parsed object for the tree view. */\n body?: string | object\n contentType?: string\n bodyLabel?: ReactNode\n headersLabel?: ReactNode\n rawLabel?: ReactNode\n emptyBodyLabel?: ReactNode\n}) {\n const json =\n typeof body === 'object' && body !== null\n ? (body as Json)\n : typeof body === 'string' && /json/i.test(contentType ?? '')\n ? (() => {\n try {\n return JSON.parse(body) as Json\n } catch {\n return undefined\n }\n })()\n : undefined\n\n const raw = typeof body === 'string' ? body : body ? JSON.stringify(body, null, 2) : ''\n // The highlighter has no plain-text grammar; JSON is the closest neutral one.\n const language: Language = /html|xml/i.test(contentType ?? '')\n ? 'html'\n : /json/i.test(contentType ?? '')\n ? 'json'\n : 'bash'\n\n return (\n <div\n data-slot=\"response-viewer\"\n className={cn(surface, radius.surface, 'flex flex-col overflow-hidden', className)}\n {...props}\n >\n {/* Status, time and size on one line: they are only meaningful together. */}\n <div className=\"border-border flex flex-wrap items-center gap-3 border-b p-3\">\n <HttpStatus status={status} phrase={statusText} />\n {durationMs !== undefined && (\n <span\n className={cn(\n 'text-xs tabular-nums',\n durationMs > 1000 ? 'text-[var(--amber-soft-foreground)]' : 'text-muted-foreground',\n )}\n >\n {durationMs < 1000 ? `${Math.round(durationMs)}ms` : `${(durationMs / 1000).toFixed(2)}s`}\n </span>\n )}\n {sizeBytes !== undefined && (\n <span className=\"text-muted-foreground text-xs tabular-nums\">\n <Fmt type=\"bytes\" value={sizeBytes} />\n </span>\n )}\n {contentType && (\n <span className=\"text-muted-foreground/70 ms-auto truncate font-mono text-xs\">\n {contentType}\n </span>\n )}\n </div>\n\n <Section title={bodyLabel} defaultOpen>\n {json !== undefined ? (\n <JsonViewer value={json} defaultExpandedDepth={2} />\n ) : raw ? (\n <CodeBlock code={raw} language={language} maxLines={20} header={false} />\n ) : (\n <p className=\"text-muted-foreground p-4 text-center text-sm\">{emptyBodyLabel}</p>\n )}\n </Section>\n\n <Section title={headersLabel} count={headers.length}>\n <dl className=\"grid grid-cols-[minmax(0,14rem)_minmax(0,1fr)] gap-x-3 gap-y-1.5 font-mono text-xs\">\n {[...headers]\n // Lower-cased and sorted: HTTP header names are case-insensitive\n // and servers are inconsistent, which breaks an A–Z scan.\n .map((h) => ({ name: h.name.toLowerCase(), value: h.value }))\n .sort((a, b) => a.name.localeCompare(b.name))\n .map((header) => (\n <div key={header.name} className=\"contents\">\n <dt className=\"text-muted-foreground truncate\">{header.name}</dt>\n <dd className=\"min-w-0 break-all\">{header.value}</dd>\n </div>\n ))}\n </dl>\n </Section>\n\n <Section title={rawLabel}>\n <CodeBlock code={raw || '(empty)'} language={language} maxLines={24} header={false} />\n </Section>\n\n </div>\n )\n}\n\n/**\n * One disclosure row. At module scope so it is not a new component type on\n * every render of the viewer.\n */\nfunction Section({\n title,\n count,\n defaultOpen = false,\n children,\n}: {\n title: ReactNode\n count?: number\n defaultOpen?: boolean\n children: ReactNode\n}) {\n return (\n <Collapsible defaultOpen={defaultOpen} className=\"border-border border-t first:border-t-0\">\n <CollapsibleTrigger className=\"hover:bg-accent/40 w-full p-3 text-start text-xs font-medium\">\n <span className=\"flex-1\">{title}</span>\n {count !== undefined && (\n <Badge size=\"sm\" color=\"neutral\">\n {count}\n </Badge>\n )}\n </CollapsibleTrigger>\n {/* Padding goes on the inner content, not on CollapsibleContent itself:\n that element's height is animated from its child's measured height,\n so padding on the wrapper is excluded from the measurement and the\n panel opens to the padding alone. */}\n <CollapsibleContent>\n <div className=\"p-3 pt-0\">{children}</div>\n </CollapsibleContent>\n </Collapsible>\n )\n}\n\nexport { ResponseViewer }\n"
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "retrieval-results",
3
+ "type": "registry:ui",
4
+ "title": "Retrieval Results",
5
+ "description": "The chunks a retrieval step pulled, with relevance. Near-misses are dimmed rather than dropped — when an answer is wrong, \"what did it nearly retrieve\" is the useful question.",
6
+ "category": "AI",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "badge",
12
+ "confidence-meter",
13
+ "lib-styles",
14
+ "lib-utils"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "components/ui/retrieval-results.tsx",
19
+ "type": "registry:ui",
20
+ "content": "import { useState, type ComponentProps, type ReactNode } from 'react'\nimport { ChevronDown, ChevronRight, FileText } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { ConfidenceMeter } from '@/components/ui/confidence-meter'\nimport { focusRing, interactive, radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * The chunks a retrieval step pulled, with their relevance.\n *\n * Chunks below `threshold` are still listed, dimmed, rather than dropped. When\n * an answer is wrong the useful question is usually \"what did it nearly\n * retrieve\" — a list that silently hides the near-misses cannot answer it.\n *\n * Scores use `ConfidenceMeter`'s banding rather than a raw number, for the same\n * reason: a cosine similarity of 0.82 is not a calibrated probability and\n * printing two decimals implies a precision it does not have.\n */\nexport type RetrievedChunk = {\n id: string\n /** 0–1. */\n score: number\n content: ReactNode\n source: string\n page?: number\n metadata?: Record<string, string>\n}\n\nfunction RetrievalResults({\n chunks,\n query,\n threshold = 0.5,\n defaultExpanded,\n title = 'Retrieved context',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n chunks: RetrievedChunk[]\n query?: ReactNode\n /** Below this a chunk is dimmed, not hidden. */\n threshold?: number\n defaultExpanded?: string[]\n title?: ReactNode\n}) {\n const [expanded, setExpanded] = useState<string[]>(\n // The top hit opens: it is the one that shaped the answer.\n defaultExpanded ?? (chunks[0] ? [chunks[0].id] : []),\n )\n\n const used = chunks.filter((chunk) => chunk.score >= threshold).length\n\n return (\n <div\n data-slot=\"retrieval-results\"\n className={cn(surface, radius.surface, 'overflow-hidden', className)}\n {...props}\n >\n <div className=\"border-border flex flex-wrap items-center gap-2 border-b p-3\">\n <span className=\"text-sm font-medium\">{title}</span>\n <Badge size=\"sm\">{used} of {chunks.length} above threshold</Badge>\n {query && (\n <span className=\"text-muted-foreground w-full truncate text-xs\">\n for “{query}”\n </span>\n )}\n </div>\n\n <ul className=\"list-none divide-y divide-[var(--border)]\">\n {chunks.map((chunk) => {\n const open = expanded.includes(chunk.id)\n const weak = chunk.score < threshold\n\n return (\n <li key={chunk.id} className={cn(weak && 'opacity-55')}>\n <button\n type=\"button\"\n aria-expanded={open}\n onClick={() =>\n setExpanded((current) =>\n open\n ? current.filter((id) => id !== chunk.id)\n : [...current, chunk.id],\n )\n }\n className={cn(\n 'hover:bg-accent/40 flex w-full items-center gap-2 p-3 text-start',\n interactive,\n focusRing,\n )}\n >\n {open ? (\n <ChevronDown className=\"text-muted-foreground size-3.5 shrink-0\" />\n ) : (\n <ChevronRight className=\"text-muted-foreground size-3.5 shrink-0\" />\n )}\n <FileText className=\"text-muted-foreground size-4 shrink-0\" aria-hidden=\"true\" />\n\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm\">{chunk.source}</span>\n {chunk.page !== undefined && (\n <span className=\"text-muted-foreground block text-xs\">\n page {chunk.page}\n </span>\n )}\n </span>\n\n <ConfidenceMeter\n value={chunk.score}\n showLabel={false}\n size=\"sm\"\n label=\"Relevance\"\n className=\"w-16 shrink-0\"\n />\n </button>\n\n {open && (\n <div className=\"bg-muted/30 border-border/60 border-t p-3\">\n <p className=\"text-muted-foreground text-sm\">{chunk.content}</p>\n\n {chunk.metadata && (\n <dl className=\"mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs\">\n {Object.entries(chunk.metadata).map(([key, value]) => (\n <div key={key} className=\"flex gap-1.5\">\n <dt className=\"text-muted-foreground/70\">{key}</dt>\n <dd className=\"font-medium\">{value}</dd>\n </div>\n ))}\n </dl>\n )}\n </div>\n )}\n </li>\n )\n })}\n </ul>\n </div>\n )\n}\n\nexport { RetrievalResults }\n"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "revenue-share-table",
3
+ "type": "registry:ui",
4
+ "title": "Revenue Share Table",
5
+ "description": "How a payout splits between parties. The unallocated remainder gets a named row instead of leaving the reader to subtract, and a split exceeding 100% is called out rather than quietly rendered.",
6
+ "category": "Affiliate",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "fmt",
10
+ "lib-utils",
11
+ "table"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/revenue-share-table.tsx",
16
+ "type": "registry:ui",
17
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { Fmt } from '@/components/ui/fmt'\nimport {\n Table,\n TableBody,\n TableCell,\n TableFooter,\n TableHead,\n TableHeader,\n TableRow,\n} from '@/components/ui/table'\nimport { cn } from '@/lib/utils'\n\n/**\n * How a payout splits between parties.\n *\n * The rows are shares of a gross figure, so the table adds them up and shows the\n * remainder as a named row rather than leaving the reader to subtract. A split\n * that does not reach 100% is the normal case — the house keeps the rest — and a\n * split that exceeds it is a configuration error worth seeing immediately.\n *\n * Each party's amount is computed from the gross rather than passed in, because\n * a percentage beside an amount that contradicts it is worse than either alone.\n */\nexport type RevenueShareRow = {\n id: string\n party: ReactNode\n /** Share of gross, 0–1. */\n share: number\n note?: ReactNode\n}\n\nfunction RevenueShareTable({\n rows,\n gross,\n currency = 'USD',\n locale = 'en-GB',\n remainderLabel = 'Retained',\n partyHeader = 'Party',\n shareHeader = 'Share',\n amountHeader = 'Amount',\n grossLabel = 'Gross',\n overAllocatedLabel = 'Over-allocated',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n rows: RevenueShareRow[]\n gross: number\n currency?: string\n locale?: string\n remainderLabel?: ReactNode\n partyHeader?: ReactNode\n shareHeader?: ReactNode\n amountHeader?: ReactNode\n /** Label on the total row. */\n grossLabel?: ReactNode\n /** Replaces `remainderLabel` when the shares exceed 100%. */\n overAllocatedLabel?: ReactNode\n}) {\n const allocated = rows.reduce((total, row) => total + row.share, 0)\n const remainder = 1 - allocated\n // Floating-point shares never sum exactly; a hair either side is not an error.\n const over = remainder < -0.0001\n\n return (\n <div data-slot=\"revenue-share-table\" className={cn('flex flex-col gap-2', className)} {...props}>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{partyHeader}</TableHead>\n <TableHead className=\"w-24 text-end\">{shareHeader}</TableHead>\n <TableHead className=\"w-32 text-end\">{amountHeader}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {rows.map((row) => (\n <TableRow key={row.id}>\n <TableCell>\n <span className=\"font-medium\">{row.party}</span>\n {row.note && (\n <span className=\"text-muted-foreground block text-xs\">{row.note}</span>\n )}\n </TableCell>\n <TableCell className=\"text-end tabular-nums\">\n <Fmt type=\"percent\" value={row.share} decimals={row.share * 100 % 1 ? 1 : 0} locale={locale} />\n </TableCell>\n <TableCell className=\"text-end tabular-nums\">\n <Fmt\n type=\"currency\"\n value={gross * row.share}\n currency={currency}\n locale={locale}\n decimals={2}\n />\n </TableCell>\n </TableRow>\n ))}\n\n {/* Named, not left as an exercise in subtraction. */}\n {Math.abs(remainder) > 0.0001 && (\n <TableRow className={over ? 'text-[var(--destructive-soft-foreground)]' : undefined}>\n <TableCell className=\"text-muted-foreground\">\n {over ? overAllocatedLabel : remainderLabel}\n </TableCell>\n <TableCell className=\"text-end tabular-nums\">\n <Fmt type=\"percent\" value={Math.abs(remainder)} decimals={1} locale={locale} />\n </TableCell>\n <TableCell className=\"text-end tabular-nums\">\n <Fmt\n type=\"currency\"\n value={Math.abs(gross * remainder)}\n currency={currency}\n locale={locale}\n decimals={2}\n />\n </TableCell>\n </TableRow>\n )}\n </TableBody>\n <TableFooter>\n <TableRow>\n <TableCell className=\"font-medium\">{grossLabel}</TableCell>\n <TableCell className=\"text-muted-foreground text-end tabular-nums\">\n 100%\n </TableCell>\n <TableCell className=\"text-end font-semibold tabular-nums\">\n <Fmt type=\"currency\" value={gross} currency={currency} locale={locale} decimals={2} />\n </TableCell>\n </TableRow>\n </TableFooter>\n </Table>\n </div>\n )\n}\n\nexport { RevenueShareTable }\n"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "review-thread",
3
+ "type": "registry:ui",
4
+ "title": "Review Thread",
5
+ "description": "A review conversation anchored to a line of code, with the line shown as context. Resolving is always a callback — a resolved thread is server truth other reviewers see.",
6
+ "category": "Development",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "avatar",
12
+ "badge",
13
+ "button",
14
+ "lib-styles",
15
+ "lib-utils",
16
+ "textarea"
17
+ ],
18
+ "files": [
19
+ {
20
+ "path": "components/ui/review-thread.tsx",
21
+ "type": "registry:ui",
22
+ "content": "import { useState, type ComponentProps, type ReactNode } from 'react'\nimport { Check, MessageSquare } from 'lucide-react'\nimport { Avatar } from '@/components/ui/avatar'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Textarea } from '@/components/ui/textarea'\nimport { radius } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A review conversation anchored to a line of code.\n *\n * The anchor is rendered as context above the comments — a thread detached\n * from the line it is about is unreadable, and re-finding that line is the\n * main cost of reviewing in a browser.\n *\n * Resolving is a callback, never local state. A resolved thread is server\n * truth that other reviewers see; a component that hid it locally would show\n * two people different threads on the same diff.\n */\nexport type ReviewComment = {\n id: string\n author: string\n body: ReactNode\n time?: ReactNode\n /** Marks the review that requested changes. */\n pending?: boolean\n}\n\nfunction ReviewThread({\n path,\n line,\n snippet,\n comments,\n resolved = false,\n onResolve,\n onReply,\n resolvedLabel = 'Resolved',\n pendingLabel = 'Pending',\n replyLabel = 'Reply',\n replyPlaceholder = 'Reply…',\n replyFieldLabel = 'Reply to thread',\n resolveLabel = 'Resolve',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'onSubmit'> & {\n path?: string\n line?: number\n /** The anchored source line. */\n snippet?: string\n comments: ReviewComment[]\n resolved?: boolean\n onResolve?: () => void\n onReply?: (body: string) => void\n resolvedLabel?: ReactNode\n /** Badge on a comment awaiting a reply. */\n pendingLabel?: ReactNode\n replyLabel?: ReactNode\n replyPlaceholder?: string\n /** Accessible name for the reply field. */\n replyFieldLabel?: string\n resolveLabel?: ReactNode\n}) {\n const [draft, setDraft] = useState('')\n\n return (\n <div\n data-slot=\"review-thread\"\n data-resolved={resolved || undefined}\n className={cn(\n 'border-border overflow-hidden border',\n radius.surface,\n resolved && 'opacity-70',\n className,\n )}\n {...props}\n >\n {(path || snippet) && (\n <div className=\"border-border bg-muted/40 border-b p-3\">\n <div className=\"flex items-center gap-2\">\n <MessageSquare className=\"text-muted-foreground size-3.5 shrink-0\" />\n <span className=\"text-muted-foreground min-w-0 flex-1 truncate font-mono text-xs\">\n {path}\n {line !== undefined && `:${line}`}\n </span>\n {resolved && (\n <Badge size=\"sm\" color=\"green\">\n <Check />\n {resolvedLabel}\n </Badge>\n )}\n </div>\n\n {snippet && (\n <pre className=\"text-muted-foreground mt-2 overflow-x-auto font-mono text-xs\">\n <code>{snippet}</code>\n </pre>\n )}\n </div>\n )}\n\n <ul className=\"divide-border/60 flex list-none flex-col divide-y\">\n {comments.map((comment) => (\n <li key={comment.id} className=\"flex gap-2.5 px-3 py-2.5\">\n <Avatar size=\"xs\" name={comment.author} className=\"mt-0.5 shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-baseline gap-x-2\">\n <span className=\"text-sm font-medium\">{comment.author}</span>\n {comment.time && (\n <span className=\"text-muted-foreground text-xs\">{comment.time}</span>\n )}\n {comment.pending && (\n <Badge size=\"sm\" color=\"amber\">\n {pendingLabel}\n </Badge>\n )}\n </div>\n <div className=\"text-muted-foreground mt-0.5 text-sm\">\n {comment.body}\n </div>\n </div>\n </li>\n ))}\n </ul>\n\n {(onReply || onResolve) && !resolved && (\n <div className=\"border-border flex flex-col gap-2 border-t p-3\">\n {onReply && (\n <Textarea\n rows={2}\n autoResize\n aria-label={replyFieldLabel}\n placeholder={replyPlaceholder}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n />\n )}\n <div className=\"flex flex-wrap items-center gap-2\">\n {onReply && (\n <Button\n size=\"sm\"\n disabled={!draft.trim()}\n onClick={() => {\n onReply(draft)\n setDraft('')\n }}\n >\n {replyLabel}\n </Button>\n )}\n {onResolve && (\n <Button variant=\"secondary\" size=\"sm\" onClick={onResolve}>\n <Check />\n {resolveLabel}\n </Button>\n )}\n </div>\n </div>\n )}\n </div>\n )\n}\n\nexport { ReviewThread }\n"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "risk-score",
3
+ "type": "registry:ui",
4
+ "title": "Risk Score",
5
+ "description": "A score with the signals that produced it. The factors are the component, not a detail beside it — an analyst cannot approve or decline on \"82\", and an unexplained score is the one thing a regulator will ask you to justify.",
6
+ "category": "Security",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "lib-utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/risk-score.tsx",
14
+ "type": "registry:ui",
15
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\n\n/**\n * A 0–100 risk score with the signals that produced it.\n *\n * The contributing factors are the component, not a detail beside it. A bare\n * number is unactionable — an analyst cannot approve or decline on \"82\" — and an\n * unexplained score is also the one thing a regulator will ask you to justify.\n *\n * Bands are named, not just coloured. \"High\" survives being printed, screenshot\n * into a ticket, or read by someone who cannot distinguish amber from green;\n * the colour is reinforcement.\n *\n * Weights are drawn relative to the largest contributor rather than to 100, so\n * a set of small signals still shows its shape instead of five invisible bars.\n */\nexport type RiskFactor = {\n label: ReactNode\n /** Points contributed. Negative values lower the score. */\n weight: number\n detail?: ReactNode\n}\n\nexport type RiskBand = { min: number; label: string; color: string; ink: string }\n\nconst BANDS: RiskBand[] = [\n { min: 0, label: 'Low', color: 'var(--green)', ink: 'var(--green-soft-foreground)' },\n { min: 34, label: 'Elevated', color: 'var(--amber)', ink: 'var(--amber-soft-foreground)' },\n { min: 67, label: 'High', color: 'var(--destructive)', ink: 'var(--destructive-soft-foreground)' },\n]\n\nfunction bandFor(score: number, bands: RiskBand[]) {\n return [...bands].reverse().find((band) => score >= band.min) ?? bands[0]\n}\n\nfunction RiskScore({\n score,\n factors,\n bands = BANDS,\n max = 100,\n label = 'Risk score',\n size = 'md',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n score: number\n factors?: RiskFactor[]\n bands?: RiskBand[]\n max?: number\n label?: ReactNode\n size?: 'sm' | 'md'\n}) {\n const clamped = Math.max(0, Math.min(max, score))\n const band = bandFor((clamped / max) * 100, bands)\n // Relative to the biggest signal, so small ones still read.\n const peak = factors?.reduce((top, factor) => Math.max(top, Math.abs(factor.weight)), 0) ?? 0\n\n return (\n <div\n data-slot=\"risk-score\"\n data-band={band.label.toLowerCase()}\n className={cn('flex flex-col gap-3', className)}\n {...props}\n >\n <div className=\"flex items-baseline gap-2\">\n <span\n className={cn('font-semibold tabular-nums', size === 'sm' ? 'text-2xl' : 'text-3xl')}\n style={{ color: band.ink }}\n >\n {Math.round(clamped)}\n </span>\n <span className=\"text-muted-foreground text-xs\">/ {max}</span>\n {/* Named as well as coloured — this gets screenshotted into tickets. */}\n <span className=\"ms-auto text-xs font-medium\" style={{ color: band.ink }}>\n {band.label}\n </span>\n </div>\n\n <div\n role=\"meter\"\n aria-valuenow={Math.round(clamped)}\n aria-valuemin={0}\n aria-valuemax={max}\n aria-valuetext={`${Math.round(clamped)} of ${max}, ${band.label}`}\n aria-label={typeof label === 'string' ? label : undefined}\n className={cn('bg-secondary relative h-1.5 w-full overflow-hidden', 'rounded-full')}\n >\n <div\n className=\"h-full transition-[width] duration-300\"\n style={{ width: `${(clamped / max) * 100}%`, background: band.color }}\n />\n </div>\n\n {factors && factors.length > 0 && (\n <ul className=\"flex list-none flex-col gap-1.5\">\n {factors.map((factor, index) => {\n const share = peak > 0 ? Math.abs(factor.weight) / peak : 0\n const negative = factor.weight < 0\n return (\n <li key={index} className=\"flex flex-col gap-1\">\n <div className=\"flex items-baseline gap-2 text-xs\">\n <span className=\"min-w-0 flex-1 truncate\">{factor.label}</span>\n <span\n className=\"shrink-0 font-medium tabular-nums\"\n style={{ color: negative ? 'var(--green-soft-foreground)' : undefined }}\n >\n {negative ? '' : '+'}\n {factor.weight}\n </span>\n </div>\n <div className={cn('bg-secondary h-1 w-full overflow-hidden', 'rounded-full')}>\n <div\n className=\"h-full\"\n style={{\n width: `${share * 100}%`,\n background: negative ? 'var(--green)' : band.color,\n opacity: negative ? 1 : 0.65,\n }}\n />\n </div>\n {factor.detail && (\n <p className=\"text-muted-foreground text-xs\">{factor.detail}</p>\n )}\n </li>\n )\n })}\n </ul>\n )}\n </div>\n )\n}\n\nexport { RiskScore, BANDS as riskBands, bandFor as riskBandFor }\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "root-cause-tree",
3
+ "type": "registry:ui",
4
+ "title": "Root Cause Tree",
5
+ "description": "A causal chain from symptom to candidate causes, each with a confidence band. Built on Tree, so it inherits the keyboard model rather than growing a second one.",
6
+ "category": "AIOps",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "confidence-meter",
10
+ "lib-utils",
11
+ "tree"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/root-cause-tree.tsx",
16
+ "type": "registry:ui",
17
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { ConfidenceMeter } from '@/components/ui/confidence-meter'\nimport { Tree, type TreeNode } from '@/components/ui/tree'\nimport { cn } from '@/lib/utils'\n\n/**\n * A causal chain: symptom at the root, candidate causes beneath it.\n *\n * Built on `Tree`, so it inherits the keyboard model rather than growing a\n * second one. What it adds is confidence — each branch carries a score, and the\n * label renders it inline.\n *\n * Confidence is shown as a band, never a bare percentage. These numbers come\n * out of a correlation engine and are not calibrated probabilities; printing\n * \"87%\" beside a guess lends it a precision it has not earned.\n */\nexport type CauseNode = {\n id: string\n label: ReactNode\n /** 0 to 1. */\n confidence?: number\n detail?: ReactNode\n children?: CauseNode[]\n}\n\nfunction toTreeNodes(causes: CauseNode[]): TreeNode[] {\n return causes.map((cause) => ({\n id: cause.id,\n label: (\n <span className=\"flex min-w-0 flex-1 items-center gap-2\">\n <span className=\"min-w-0 flex-1 truncate\">{cause.label}</span>\n {cause.confidence !== undefined && (\n <ConfidenceMeter\n value={cause.confidence}\n showLabel={false}\n size=\"sm\"\n className=\"w-16 shrink-0\"\n label=\"Confidence\"\n />\n )}\n </span>\n ),\n meta: cause.detail,\n children: cause.children ? toTreeNodes(cause.children) : undefined,\n }))\n}\n\nfunction RootCauseTree({\n causes,\n defaultExpanded,\n label = 'Root cause analysis',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n causes: CauseNode[]\n defaultExpanded?: string[]\n /** Accessible name for the tree. */\n label?: string\n}) {\n const nodes = toTreeNodes(causes)\n\n return (\n <div data-slot=\"root-cause-tree\" className={cn('min-w-0', className)} {...props}>\n <Tree\n nodes={nodes}\n defaultExpanded={defaultExpanded ?? causes.map((cause) => cause.id)}\n aria-label={label}\n />\n </div>\n )\n}\n\nexport { RootCauseTree }\n"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "round-history",
3
+ "type": "registry:ui",
4
+ "title": "Round History",
5
+ "description": "Recent results as a strip. It carries an explicit note that past rounds do not predict the next one, because a row of results is read as a pattern whether or not one exists.",
6
+ "category": "Gaming",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "fmt",
10
+ "lib-styles",
11
+ "lib-utils",
12
+ "tooltip"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/ui/round-history.tsx",
17
+ "type": "registry:ui",
18
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { Tooltip } from '@/components/ui/tooltip'\nimport { Fmt } from '@/components/ui/fmt'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Recent round outcomes as a strip of chips.\n *\n * Newest first, left to right. Game histories are read the way a ticker is —\n * the most recent result is the one being reacted to — and appending to the end\n * buries it off the right edge on a phone.\n *\n * The thresholds that colour a chip are props, because \"high\" means 2× in one\n * game and 100× in another. Hard-coding them produces a strip that is all one\n * colour on half the games it is used for.\n *\n * A note about what this cannot tell you is included deliberately: past rounds\n * carry no information about the next one, and a history strip is the single\n * most common surface for the gambler's fallacy.\n */\nexport type Round = {\n id: string\n /** The result — a multiplier, a score, a number. */\n value: number\n at?: Date\n /** Marks a round the player took part in. */\n played?: boolean\n detail?: ReactNode\n}\n\n/**\n * Default label formatters, hoisted out of the parameter list.\n *\n * An arrow function written inline as a default parameter is a value the\n * React Compiler cannot safely reorder, so it bails on the whole component\n * and none of it gets auto-memoised. At module scope it is a stable\n * reference and the component compiles.\n */\nconst DEFAULT_FORMAT: (value: number) => ReactNode = (value) => `${value.toFixed(2)}×`\n\nfunction RoundHistory({\n rounds,\n lowBelow = 2,\n highAbove = 10,\n format = DEFAULT_FORMAT,\n now,\n locale = 'en-GB',\n note = 'Past rounds do not affect future ones.',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n rounds: Round[]\n /** Below this reads as a loss. Game-specific, hence a prop. */\n lowBelow?: number\n highAbove?: number\n format?: (value: number) => ReactNode\n now?: Date\n locale?: string\n note?: ReactNode\n}) {\n return (\n <div\n data-slot=\"round-history\"\n className={cn(surface, radius.surface, 'flex flex-col gap-2 p-3', className)}\n {...props}\n >\n {/* Newest first: the last result is the one being reacted to. */}\n <div className=\"flex flex-wrap gap-1.5\">\n {rounds.map((round) => {\n const low = round.value < lowBelow\n const high = round.value >= highAbove\n\n return (\n <Tooltip\n key={round.id}\n content={\n <span className=\"flex flex-col\">\n <span>{format(round.value)}</span>\n {round.at && (\n <span className=\"opacity-70\">\n <Fmt type=\"relative\" value={round.at} now={now} locale={locale} />\n </span>\n )}\n {round.detail && <span className=\"opacity-70\">{round.detail}</span>}\n </span>\n }\n >\n <span\n tabIndex={0}\n className={cn(\n 'inline-flex h-6 items-center px-2 text-xs font-medium tabular-nums outline-none',\n radius.xs,\n 'focus-visible:ring-ring/50 focus-visible:ring-2',\n low && 'bg-[color-mix(in_oklab,var(--destructive),transparent_86%)] text-[var(--destructive-soft-foreground)]',\n high && 'bg-[color-mix(in_oklab,var(--green),transparent_82%)] text-[var(--green-soft-foreground)]',\n !low && !high && 'bg-secondary text-muted-foreground',\n round.played && 'ring-primary/40 ring-2',\n )}\n >\n {format(round.value)}\n </span>\n </Tooltip>\n )\n })}\n </div>\n\n {/* Stated on purpose — this strip is where the gambler's fallacy lives. */}\n {note && <p className=\"text-muted-foreground/70 text-xs\">{note}</p>}\n </div>\n )\n}\n\nexport { RoundHistory }\n"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "runbook-steps",
3
+ "type": "registry:ui",
4
+ "title": "Runbook Steps",
5
+ "description": "Remediation steps, run one at a time and only in order. Offering \"run\" on step four while step two is untried invites exactly the mistake the runbook exists to prevent.",
6
+ "category": "AIOps",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "badge",
12
+ "button",
13
+ "lib-styles",
14
+ "lib-utils",
15
+ "spinner"
16
+ ],
17
+ "files": [
18
+ {
19
+ "path": "components/ui/runbook-steps.tsx",
20
+ "type": "registry:ui",
21
+ "content": "import { useState, type ComponentProps, type ReactNode } from 'react'\nimport { Check, Play, SkipForward, X } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Spinner } from '@/components/ui/spinner'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A runbook: remediation steps, each runnable on its own.\n *\n * Steps run one at a time and only in order — a runbook is a procedure, and\n * offering \"run\" on step four while step two has not been attempted invites\n * exactly the mistake the runbook exists to prevent. The next actionable step is\n * computed rather than tracked, so it stays correct when a step is skipped.\n *\n * Destructive steps must be confirmed. `confirm` is a per-step flag, because the\n * ones that restart a database sit right beside the ones that read a log.\n */\nexport type RunbookStatus = 'pending' | 'running' | 'done' | 'failed' | 'skipped'\n\nexport type RunbookStep = {\n id: string\n title: ReactNode\n description?: ReactNode\n status: RunbookStatus\n /** Ask before running. For anything destructive. */\n confirm?: boolean\n output?: ReactNode\n}\n\nconst STATUS_BADGE = {\n pending: null,\n running: { label: 'Running', color: 'blue' },\n done: { label: 'Done', color: 'green' },\n failed: { label: 'Failed', color: 'destructive' },\n skipped: { label: 'Skipped', color: 'neutral' },\n} as const\n\nfunction RunbookSteps({\n steps,\n onRun,\n onSkip,\n title,\n destructiveLabel = 'Destructive',\n confirmNote = 'This cannot be undone. Run it?',\n confirmLabel = 'Confirm',\n cancelLabel = 'Cancel',\n runLabel = 'Run',\n skipLabel = 'Skip',\n runningLabel = 'Running',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'title'> & {\n steps: RunbookStep[]\n onRun?: (id: string) => void\n onSkip?: (id: string) => void\n title?: ReactNode\n /** Badge on a step that asks before it runs. */\n destructiveLabel?: ReactNode\n confirmNote?: ReactNode\n confirmLabel?: ReactNode\n cancelLabel?: ReactNode\n runLabel?: ReactNode\n skipLabel?: ReactNode\n /** Accessible name for the in-progress spinner. */\n runningLabel?: string\n}) {\n const [confirming, setConfirming] = useState<string | null>(null)\n\n // The first step that has not been resolved is the only one that can run.\n const nextIndex = steps.findIndex((step) => step.status === 'pending')\n const busy = steps.some((step) => step.status === 'running')\n\n return (\n <div\n data-slot=\"runbook-steps\"\n className={cn(surface, radius.surface, 'overflow-hidden', className)}\n {...props}\n >\n {title && (\n <div className=\"border-border bg-muted/40 border-b p-3\">\n <h3 className=\"text-sm font-medium\">{title}</h3>\n </div>\n )}\n\n <ol className=\"list-none divide-y divide-[var(--border)]\">\n {steps.map((step, index) => {\n const badge = STATUS_BADGE[step.status]\n const actionable = index === nextIndex && !busy\n const isConfirming = confirming === step.id\n\n return (\n <li key={step.id} data-status={step.status} className=\"flex flex-col gap-2 p-3\">\n <div className=\"flex items-start gap-2.5\">\n <span\n className={cn(\n 'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border text-[10px] font-semibold [corner-shape:round]',\n step.status === 'done' &&\n 'border-[var(--green)] bg-[var(--green)] text-[var(--green-foreground)]',\n step.status === 'failed' &&\n 'border-[var(--destructive)] bg-[var(--destructive)] text-[var(--destructive-foreground)]',\n step.status === 'skipped' && 'border-border text-muted-foreground/60',\n (step.status === 'pending' || step.status === 'running') &&\n 'border-border text-muted-foreground',\n )}\n >\n {step.status === 'done' ? (\n <Check className=\"size-3\" />\n ) : step.status === 'failed' ? (\n <X className=\"size-3\" />\n ) : step.status === 'running' ? (\n <Spinner size=\"xs\" label={runningLabel} />\n ) : (\n index + 1\n )}\n </span>\n\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span\n className={cn(\n 'text-sm font-medium',\n step.status === 'skipped' && 'text-muted-foreground/60 line-through',\n )}\n >\n {step.title}\n </span>\n {badge && (\n <Badge size=\"sm\" color={badge.color}>\n {badge.label}\n </Badge>\n )}\n {step.confirm && step.status === 'pending' && (\n <Badge size=\"sm\" color=\"amber\">\n {destructiveLabel}\n </Badge>\n )}\n </div>\n\n {step.description && (\n <p className=\"text-muted-foreground mt-0.5 text-sm\">\n {step.description}\n </p>\n )}\n </div>\n </div>\n\n {step.output && (\n <div className=\"ms-7 min-w-0\">{step.output}</div>\n )}\n\n {actionable && (onRun || onSkip) && (\n <div className=\"ms-7 flex flex-wrap items-center gap-2\">\n {isConfirming ? (\n <>\n <span className=\"text-muted-foreground text-xs\">\n {confirmNote}\n </span>\n <Button\n size=\"xs\"\n color=\"destructive\"\n onClick={() => {\n setConfirming(null)\n onRun?.(step.id)\n }}\n >\n {confirmLabel}\n </Button>\n <Button size=\"xs\" variant=\"secondary\" onClick={() => setConfirming(null)}>\n {cancelLabel}\n </Button>\n </>\n ) : (\n <>\n {onRun && (\n <Button\n size=\"xs\"\n onClick={() =>\n step.confirm ? setConfirming(step.id) : onRun(step.id)\n }\n >\n <Play />\n {runLabel}\n </Button>\n )}\n {onSkip && (\n <Button size=\"xs\" variant=\"secondary\" onClick={() => onSkip(step.id)}>\n <SkipForward />\n {skipLabel}\n </Button>\n )}\n </>\n )}\n </div>\n )}\n </li>\n )\n })}\n </ol>\n </div>\n )\n}\n\nexport { RunbookSteps }\n"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "schema-table",
3
+ "type": "registry:ui",
4
+ "title": "Schema Table",
5
+ "description": "Columns, keys and indexes. Foreign keys name their target inline, because `owner_id` tells you nothing about whether it points at users or accounts — which is exactly what you opened the schema to find out.",
6
+ "category": "Database",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "badge",
12
+ "lib-styles",
13
+ "lib-utils"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/ui/schema-table.tsx",
18
+ "type": "registry:ui",
19
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { KeyRound, Link2 } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A table's columns, keys and indexes.\n *\n * Nullability is stated as \"not null\" rather than left implied. A blank in a\n * nullable column reads as missing information; the constraint is the fact, and\n * it is the thing that decides whether your code needs a null check.\n *\n * Foreign keys name their target inline. A column called `owner_id` tells you\n * nothing about whether it points at `users` or `accounts`, and that is\n * precisely what someone reading a schema is trying to find out.\n *\n * Indexes are listed separately from columns, with their columns in order.\n * Index column order is not cosmetic — an index on `(a, b)` cannot serve a\n * query filtering only on `b`, and a set-like display hides that completely.\n */\nexport type SchemaColumn = {\n name: string\n type: string\n nullable?: boolean\n primaryKey?: boolean\n /** e.g. \"users.id\" */\n references?: string\n default?: string\n comment?: ReactNode\n}\n\nexport type SchemaIndex = {\n name: string\n /** In order — order determines which queries it can serve. */\n columns: string[]\n unique?: boolean\n method?: string\n}\n\nfunction SchemaTable({\n name,\n columns,\n indexes = [],\n rowCount,\n columnsLabel = 'Columns',\n indexesLabel = 'Indexes',\n notNullLabel = 'not null',\n primaryKeyLabel = 'primary key',\n uniqueLabel = 'unique',\n referencesLabel = 'references',\n defaultLabel = 'default',\n rowsLabel = 'rows',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n name: ReactNode\n columns: SchemaColumn[]\n indexes?: SchemaIndex[]\n rowCount?: number\n columnsLabel?: ReactNode\n indexesLabel?: ReactNode\n notNullLabel?: ReactNode\n /** Accessible name for the key icon. */\n primaryKeyLabel?: string\n uniqueLabel?: ReactNode\n /** Accessible name for the foreign-key icon. */\n referencesLabel?: string\n defaultLabel?: ReactNode\n rowsLabel?: ReactNode\n}) {\n return (\n <div\n data-slot=\"schema-table\"\n className={cn(surface, radius.surface, 'flex flex-col overflow-hidden', className)}\n {...props}\n >\n <div className=\"border-border flex flex-wrap items-baseline gap-2 border-b p-3\">\n <code className=\"font-mono text-sm font-medium\">{name}</code>\n {rowCount !== undefined && (\n <span className=\"text-muted-foreground ms-auto text-xs tabular-nums\">\n {rowCount.toLocaleString()} {rowsLabel}\n </span>\n )}\n </div>\n\n <div className=\"p-3\">\n <p className=\"text-muted-foreground mb-2 text-xs font-medium\">{columnsLabel}</p>\n <ul className=\"flex list-none flex-col gap-1.5\">\n {columns.map((column) => (\n <li key={column.name} className=\"flex flex-wrap items-baseline gap-x-2 gap-y-1 text-xs\">\n {column.primaryKey && (\n <KeyRound\n className=\"size-3 shrink-0 self-center text-[var(--amber-soft-foreground)]\"\n aria-label={primaryKeyLabel}\n />\n )}\n {column.references && (\n <Link2\n className=\"text-muted-foreground/60 size-3 shrink-0 self-center\"\n aria-label={referencesLabel}\n />\n )}\n\n <code className=\"font-mono font-medium\">{column.name}</code>\n <code className=\"text-muted-foreground font-mono\">{column.type}</code>\n\n {/* The constraint is the fact; a blank reads as missing data. */}\n {column.nullable === false && (\n <span className=\"text-muted-foreground/70\">{notNullLabel}</span>\n )}\n\n {/* Names the target: `owner_id` alone answers nothing. */}\n {column.references && (\n <code className=\"text-muted-foreground/70 font-mono\">→ {column.references}</code>\n )}\n\n {column.default !== undefined && (\n <span className=\"text-muted-foreground/70\">\n {defaultLabel} <code className=\"font-mono\">{column.default}</code>\n </span>\n )}\n\n {column.comment && (\n <span className=\"text-muted-foreground/60 w-full\">{column.comment}</span>\n )}\n </li>\n ))}\n </ul>\n </div>\n\n {indexes.length > 0 && (\n <div className=\"border-border border-t p-3\">\n <p className=\"text-muted-foreground mb-2 text-xs font-medium\">{indexesLabel}</p>\n <ul className=\"flex list-none flex-col gap-1.5\">\n {indexes.map((index) => (\n <li key={index.name} className=\"flex flex-wrap items-baseline gap-x-2 text-xs\">\n <code className=\"font-mono\">{index.name}</code>\n {/* Order matters: (a, b) cannot serve a query on b alone. */}\n <code className=\"text-muted-foreground font-mono\">\n ({index.columns.join(', ')})\n </code>\n {index.unique && (\n <Badge size=\"sm\" color=\"neutral\">\n {uniqueLabel}\n </Badge>\n )}\n {index.method && (\n <span className=\"text-muted-foreground/60 font-mono\">{index.method}</span>\n )}\n </li>\n ))}\n </ul>\n </div>\n )}\n </div>\n )\n}\n\nexport { SchemaTable }\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "schema-viewer",
3
+ "type": "registry:ui",
4
+ "title": "Schema Viewer",
5
+ "description": "JSON Schema as a tree. Required fields are marked on the field itself rather than left in the parent's `required` array — cross-referencing that list is the one job a renderer should do for you.",
6
+ "category": "API",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "badge",
12
+ "lib-styles",
13
+ "lib-utils"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/ui/schema-viewer.tsx",
18
+ "type": "registry:ui",
19
+ "content": "import { useState, type ComponentProps, type ReactNode } from 'react'\nimport { ChevronDown, ChevronRight } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { focusRing, interactive, radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A JSON Schema rendered as a navigable tree.\n *\n * Required fields are marked on the field, not listed separately at the parent.\n * JSON Schema puts `required` on the object as an array of names, which means\n * reading a schema involves cross-referencing a list against the properties\n * below it — the one job a renderer should do for you.\n *\n * Constraints are shown inline: `minLength`, `enum`, `pattern`, `format`. A type\n * of \"string\" is rarely the interesting part; that it must match a UUID pattern\n * is.\n *\n * `oneOf` and `anyOf` render as labelled alternatives rather than being\n * flattened. A flattened union looks like an object with contradictory fields.\n */\nexport type JsonSchema = {\n type?: string | string[]\n title?: string\n description?: string\n properties?: Record<string, JsonSchema>\n items?: JsonSchema\n required?: string[]\n enum?: unknown[]\n format?: string\n pattern?: string\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n default?: unknown\n oneOf?: JsonSchema[]\n anyOf?: JsonSchema[]\n deprecated?: boolean\n}\n\n/** The short constraint list shown beside a field's type. */\nfunction constraints(schema: JsonSchema): string[] {\n const out: string[] = []\n if (schema.format) out.push(schema.format)\n if (schema.enum) out.push(schema.enum.map((v) => JSON.stringify(v)).join(' | '))\n if (schema.pattern) out.push(`/${schema.pattern}/`)\n if (schema.minLength !== undefined || schema.maxLength !== undefined) {\n out.push(`${schema.minLength ?? 0}–${schema.maxLength ?? '∞'} chars`)\n }\n if (schema.minimum !== undefined || schema.maximum !== undefined) {\n out.push(`${schema.minimum ?? '−∞'}–${schema.maximum ?? '∞'}`)\n }\n if (schema.default !== undefined) out.push(`default ${JSON.stringify(schema.default)}`)\n return out\n}\n\nfunction typeOf(schema: JsonSchema): string {\n if (Array.isArray(schema.type)) return schema.type.join(' | ')\n if (schema.type === 'array' && schema.items) return `${typeOf(schema.items)}[]`\n return schema.type ?? (schema.oneOf ? 'oneOf' : schema.anyOf ? 'anyOf' : 'any')\n}\n\nfunction SchemaViewer({\n schema,\n name = 'root',\n defaultOpenDepth = 2,\n requiredLabel = 'required',\n deprecatedLabel = 'deprecated',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n schema: JsonSchema\n name?: string\n defaultOpenDepth?: number\n requiredLabel?: ReactNode\n deprecatedLabel?: ReactNode\n}) {\n return (\n <div\n data-slot=\"schema-viewer\"\n className={cn(surface, radius.surface, 'overflow-hidden p-2', className)}\n {...props}\n >\n <Node\n schema={schema}\n name={name}\n depth={0}\n required={false}\n defaultOpenDepth={defaultOpenDepth}\n requiredLabel={requiredLabel}\n deprecatedLabel={deprecatedLabel}\n />\n </div>\n )\n}\n\n/** At module scope: a component declared in render remounts every keystroke. */\nfunction Node({\n schema,\n name,\n depth,\n required,\n defaultOpenDepth,\n requiredLabel,\n deprecatedLabel,\n}: {\n schema: JsonSchema\n name: string\n depth: number\n required: boolean\n defaultOpenDepth: number\n requiredLabel: ReactNode\n deprecatedLabel: ReactNode\n}) {\n const [open, setOpen] = useState(depth < defaultOpenDepth)\n\n const properties = schema.properties ?? schema.items?.properties\n const requiredSet = new Set(schema.required ?? schema.items?.required ?? [])\n const alternatives = schema.oneOf ?? schema.anyOf\n const branching = Boolean(properties || alternatives)\n const notes = constraints(schema)\n\n return (\n <div>\n <button\n type=\"button\"\n aria-expanded={branching ? open : undefined}\n disabled={!branching}\n onClick={() => setOpen((v) => !v)}\n style={{ paddingInlineStart: depth * 14 + 6 }}\n className={cn(\n 'flex w-full items-baseline gap-2 py-1 pe-2 text-start',\n radius.xs,\n branching && interactive,\n focusRing,\n )}\n >\n {branching ? (\n open ? (\n <ChevronDown className=\"text-muted-foreground size-3.5 shrink-0 self-center\" aria-hidden=\"true\" />\n ) : (\n <ChevronRight className=\"text-muted-foreground size-3.5 shrink-0 self-center\" aria-hidden=\"true\" />\n )\n ) : (\n <span className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n )}\n\n <code className={cn('font-mono text-xs font-medium', schema.deprecated && 'line-through')}>\n {name}\n </code>\n\n {/* Marked on the field, not cross-referenced from the parent's array. */}\n {required && (\n <span className=\"text-[var(--destructive-soft-foreground)] text-xs\" title={String(requiredLabel)}>\n *\n </span>\n )}\n\n <code className=\"text-muted-foreground font-mono text-xs\">{typeOf(schema)}</code>\n\n {schema.deprecated && (\n <Badge size=\"sm\" color=\"amber\">\n {deprecatedLabel}\n </Badge>\n )}\n\n {/* The constraint, not the type, is usually the interesting part. */}\n {notes.length > 0 && (\n <span className=\"text-muted-foreground/70 min-w-0 truncate font-mono text-xs\">\n {notes.join(' · ')}\n </span>\n )}\n </button>\n\n {schema.description && (\n <p\n className=\"text-muted-foreground/80 pe-2 text-xs\"\n style={{ paddingInlineStart: depth * 14 + 26 }}\n >\n {schema.description}\n </p>\n )}\n\n {open && properties && (\n <div>\n {Object.entries(properties).map(([key, child]) => (\n <Node\n key={key}\n schema={child}\n name={key}\n depth={depth + 1}\n required={requiredSet.has(key)}\n defaultOpenDepth={defaultOpenDepth}\n requiredLabel={requiredLabel}\n deprecatedLabel={deprecatedLabel}\n />\n ))}\n </div>\n )}\n\n {/* Labelled alternatives — a flattened union looks self-contradictory. */}\n {open && alternatives && (\n <div>\n {alternatives.map((child, index) => (\n <Node\n key={index}\n schema={child}\n name={`${schema.oneOf ? 'oneOf' : 'anyOf'}[${index}]`}\n depth={depth + 1}\n required={false}\n defaultOpenDepth={defaultOpenDepth}\n requiredLabel={requiredLabel}\n deprecatedLabel={deprecatedLabel}\n />\n ))}\n </div>\n )}\n </div>\n )\n}\n\nexport { SchemaViewer, constraints as schemaConstraints }\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "scroll-area",
3
+ "type": "registry:ui",
4
+ "title": "Scroll Area",
5
+ "description": "A scrollable region with a themed scrollbar, styled through the standard scrollbar properties rather than a JavaScript overlay — so native momentum and keyboard scrolling stay intact.",
6
+ "category": "Navigation",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "lib-utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/scroll-area.tsx",
14
+ "type": "registry:ui",
15
+ "content": "import type { ComponentProps } from 'react'\nimport { cn } from '@/lib/utils'\n\n/**\n * A bounded scrollable region.\n *\n * The scrollbar itself is styled globally in `index.css`, so this only decides\n * which axis scrolls — it exists to make that intent explicit at the call site\n * rather than to restyle anything.\n */\nfunction ScrollArea({\n className,\n orientation = 'vertical',\n ...props\n}: ComponentProps<'div'> & {\n orientation?: 'vertical' | 'horizontal' | 'both'\n}) {\n return (\n <div\n data-slot=\"scroll-area\"\n data-orientation={orientation}\n className={cn(\n 'relative',\n orientation === 'vertical' && 'overflow-y-auto',\n orientation === 'horizontal' && 'overflow-x-auto',\n orientation === 'both' && 'overflow-auto',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport { ScrollArea }\n"
16
+ }
17
+ ]
18
+ }