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,22 @@
1
+ {
2
+ "name": "accordion",
3
+ "type": "registry:ui",
4
+ "title": "Accordion",
5
+ "description": "A stack of collapsible sections. Each header is a real button inside a heading, which is what makes the set navigable by heading in a screen reader.",
6
+ "category": "Navigation",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "lib-styles",
12
+ "lib-utils",
13
+ "primitive-collapsible"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/ui/accordion.tsx",
18
+ "type": "registry:ui",
19
+ "content": "import {\n createContext,\n use,\n useId,\n useMemo,\n useRef,\n useState,\n type ComponentProps,\n} from 'react'\nimport { ChevronDown } from 'lucide-react'\nimport { useCollapsibleHeight } from '@/components/primitives/collapsible'\nimport { focusRing } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A stack of collapsible sections.\n *\n * `type=\"single\"` closes the previous section when another opens; `\"multiple\"`\n * lets any number stand open. Each header is a real `<button>` inside a heading,\n * which is what makes the set navigable by heading in a screen reader.\n */\ntype AccordionContextValue = {\n open: string[]\n toggle: (value: string) => void\n collapsible: boolean\n}\n\nconst AccordionContext = createContext<AccordionContextValue | null>(null)\n\nconst ItemContext = createContext<{\n value: string\n open: boolean\n ids: { trigger: string; content: string }\n} | null>(null)\n\nfunction useAccordion() {\n const context = use(AccordionContext)\n if (!context) throw new Error('Must be used inside <Accordion>')\n return context\n}\n\nfunction useItem() {\n const context = use(ItemContext)\n if (!context) throw new Error('Must be used inside <AccordionItem>')\n return context\n}\n\ntype AccordionProps = Omit<ComponentProps<'div'>, 'defaultValue' | 'onChange'> & {\n type?: 'single' | 'multiple'\n value?: string[]\n defaultValue?: string[]\n onValueChange?: (value: string[]) => void\n /** With `type=\"single\"`, allow closing the open section. */\n collapsible?: boolean\n}\n\nfunction Accordion({\n className,\n type = 'single',\n value: valueProp,\n defaultValue = [],\n onValueChange,\n collapsible = true,\n ...props\n}: AccordionProps) {\n const controlled = valueProp !== undefined\n const [uncontrolled, setUncontrolled] = useState(defaultValue)\n const open = controlled ? valueProp : uncontrolled\n\n const context = useMemo<AccordionContextValue>(\n () => ({\n open,\n collapsible,\n toggle: (item) => {\n const isOpen = open.includes(item)\n const next =\n type === 'single'\n ? isOpen\n ? collapsible\n ? []\n : open\n : [item]\n : isOpen\n ? open.filter((v) => v !== item)\n : [...open, item]\n\n if (!controlled) setUncontrolled(next)\n onValueChange?.(next)\n },\n }),\n [open, type, collapsible, controlled, onValueChange],\n )\n\n return (\n <AccordionContext value={context}>\n <div\n data-slot=\"accordion\"\n className={cn('border-border divide-border divide-y border-y', className)}\n {...props}\n />\n </AccordionContext>\n )\n}\n\nfunction AccordionItem({\n className,\n value,\n ...props\n}: ComponentProps<'div'> & { value: string }) {\n const { open } = useAccordion()\n const id = useId()\n\n const context = useMemo(\n () => ({\n value,\n open: open.includes(value),\n ids: { trigger: `${id}-trigger`, content: `${id}-content` },\n }),\n [value, open, id],\n )\n\n return (\n <ItemContext value={context}>\n <div\n data-slot=\"accordion-item\"\n data-state={context.open ? 'open' : 'closed'}\n className={className}\n {...props}\n />\n </ItemContext>\n )\n}\n\nfunction AccordionTrigger({\n className,\n children,\n ...props\n}: ComponentProps<'button'>) {\n const { toggle } = useAccordion()\n const { value, open, ids } = useItem()\n\n return (\n // The heading wrapper is what lets screen-reader users jump between\n // sections by heading rather than tabbing through every trigger.\n <h3 className=\"flex\">\n <button\n type=\"button\"\n id={ids.trigger}\n data-slot=\"accordion-trigger\"\n aria-expanded={open}\n aria-controls={ids.content}\n onClick={() => toggle(value)}\n className={cn(\n 'flex flex-1 cursor-pointer items-center justify-between gap-4 py-4 text-start text-sm font-medium',\n 'hover:text-muted-foreground transition-colors duration-150 ease-out motion-reduce:transition-none',\n focusRing,\n className,\n )}\n {...props}\n >\n {children}\n <ChevronDown\n className={cn(\n 'text-muted-foreground size-4 shrink-0 transition-transform duration-200 ease-out motion-reduce:transition-none',\n open && 'rotate-180',\n )}\n />\n </button>\n </h3>\n )\n}\n\nfunction AccordionContent({\n className,\n children,\n ...props\n}: ComponentProps<'div'>) {\n const { open, ids } = useItem()\n const innerRef = useRef<HTMLDivElement>(null)\n const height = useCollapsibleHeight(innerRef, open)\n\n return (\n <div\n id={ids.content}\n role=\"region\"\n aria-labelledby={ids.trigger}\n data-slot=\"accordion-content\"\n style={{ height }}\n className={cn(\n 'overflow-hidden transition-[height] duration-200 ease-out motion-reduce:transition-none',\n className,\n )}\n {...props}\n >\n <div ref={innerRef} className=\"text-muted-foreground pb-4 text-sm\">\n {children}\n </div>\n </div>\n )\n}\n\nexport { Accordion, AccordionContent, AccordionItem, AccordionTrigger }\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "address-input",
3
+ "type": "registry:ui",
4
+ "title": "Address Input",
5
+ "description": "A postal address whose shape follows the country. A US form asks for a State and a ZIP; a UK one asks for a County and a Postcode; Japan puts the postal code first. Shipping the US layout everywhere is how forms reject valid addresses.",
6
+ "category": "Geo",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "combobox",
10
+ "input",
11
+ "label",
12
+ "lib-utils"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/ui/address-input.tsx",
17
+ "type": "registry:ui",
18
+ "content": "import { useId, type ComponentProps, type ReactNode } from 'react'\nimport { Combobox } from '@/components/ui/combobox'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { cn } from '@/lib/utils'\n\n/**\n * A structured postal address whose shape follows the selected country.\n *\n * The country comes first and everything below it re-labels. A US form asks for\n * a State and a ZIP code; a UK one asks for a County and a Postcode and treats\n * the region as optional; Japan puts the postal code above the prefecture.\n * Shipping the US layout to everyone is how forms end up rejecting valid\n * addresses.\n *\n * Nothing is validated against a format table beyond requiredness. Postal code\n * regexes are famously wrong — Irish Eircodes, BFPO addresses, new UK districts\n * — and a form that refuses a real address is worse than one that accepts a\n * typo the courier will query.\n *\n * `autoComplete` tokens are set on every field. The browser can fill a whole\n * address from one tap, and it only does so when the tokens are right.\n */\nexport type AddressValue = {\n country: string\n line1: string\n line2: string\n city: string\n region: string\n postalCode: string\n}\n\ntype CountryRules = {\n regionLabel: string\n postalLabel: string\n regionRequired: boolean\n postalRequired: boolean\n /** Postal code sits above city and region. */\n postalFirst?: boolean\n}\n\nconst DEFAULT_RULES: CountryRules = {\n regionLabel: 'Region',\n postalLabel: 'Postal code',\n regionRequired: false,\n postalRequired: true,\n}\n\nconst RULES: Record<string, CountryRules> = {\n US: { regionLabel: 'State', postalLabel: 'ZIP code', regionRequired: true, postalRequired: true },\n CA: { regionLabel: 'Province', postalLabel: 'Postal code', regionRequired: true, postalRequired: true },\n GB: { regionLabel: 'County', postalLabel: 'Postcode', regionRequired: false, postalRequired: true },\n IE: { regionLabel: 'County', postalLabel: 'Eircode', regionRequired: false, postalRequired: false },\n AU: { regionLabel: 'State', postalLabel: 'Postcode', regionRequired: true, postalRequired: true },\n DE: { regionLabel: 'State', postalLabel: 'Postleitzahl', regionRequired: false, postalRequired: true },\n FR: { regionLabel: 'Region', postalLabel: 'Code postal', regionRequired: false, postalRequired: true },\n NL: { regionLabel: 'Province', postalLabel: 'Postcode', regionRequired: false, postalRequired: true },\n JP: { regionLabel: 'Prefecture', postalLabel: '郵便番号', regionRequired: true, postalRequired: true, postalFirst: true },\n // Hong Kong, the UAE and a handful of others have no postal codes at all.\n HK: { regionLabel: 'District', postalLabel: 'Postal code', regionRequired: false, postalRequired: false },\n AE: { regionLabel: 'Emirate', postalLabel: 'Postal code', regionRequired: true, postalRequired: false },\n}\n\nconst COUNTRIES = [\n 'US', 'CA', 'GB', 'IE', 'AU', 'NZ', 'DE', 'FR', 'ES', 'IT', 'NL', 'BE', 'SE',\n 'NO', 'DK', 'FI', 'PL', 'PT', 'CH', 'AT', 'JP', 'SG', 'HK', 'AE', 'IN', 'BR',\n 'MX', 'ZA',\n]\n\nfunction countryName(code: string) {\n try {\n return new Intl.DisplayNames(['en'], { type: 'region' }).of(code) ?? code\n } catch {\n return code\n }\n}\n\nconst EMPTY: AddressValue = {\n country: '',\n line1: '',\n line2: '',\n city: '',\n region: '',\n postalCode: '',\n}\n\nfunction AddressInput({\n value = EMPTY,\n onValueChange,\n countries = COUNTRIES,\n size = 'default',\n disabled,\n countryLabel = 'Country',\n countryPlaceholder = 'Select country…',\n countrySearchPlaceholder = 'Search countries…',\n addressLabel = 'Address',\n addressLine2Label = 'Apartment, suite, etc.',\n cityLabel = 'City',\n optionalLabel = '(optional)',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'onChange' | 'value'> & {\n value?: AddressValue\n onValueChange?: (value: AddressValue) => void\n countries?: string[]\n size?: 'sm' | 'default'\n disabled?: boolean\n countryLabel?: ReactNode\n countryPlaceholder?: string\n countrySearchPlaceholder?: string\n addressLabel?: string\n addressLine2Label?: string\n cityLabel?: string\n /** Suffix marking a non-required field. */\n optionalLabel?: ReactNode\n}) {\n const scope = useId()\n const rules = RULES[value.country] ?? DEFAULT_RULES\n\n const set = (patch: Partial<AddressValue>) => onValueChange?.({ ...value, ...patch })\n\n const field = (\n key: keyof AddressValue,\n label: string,\n autoComplete: string,\n required = false,\n extra?: string,\n ) => (\n <div className={cn('flex flex-col gap-1.5', extra)}>\n <Label htmlFor={`${scope}-${key}`} className=\"text-xs\">\n {label}\n {!required && key !== 'line1' && key !== 'city' && (\n <span className=\"text-muted-foreground font-normal\"> {optionalLabel}</span>\n )}\n </Label>\n <Input\n id={`${scope}-${key}`}\n size={size}\n value={value[key]}\n disabled={disabled}\n required={required}\n // One tap fills the lot — but only when these tokens are right.\n autoComplete={autoComplete}\n onChange={(event) => set({ [key]: event.target.value } as Partial<AddressValue>)}\n />\n </div>\n )\n\n const postal = rules.postalRequired || value.postalCode\n ? field('postalCode', rules.postalLabel, 'postal-code', rules.postalRequired)\n : null\n\n return (\n <div\n data-slot=\"address-input\"\n className={cn('grid gap-3 sm:grid-cols-2', className)}\n {...props}\n >\n {/* First, because everything below it re-labels. */}\n <div className=\"flex flex-col gap-1.5 sm:col-span-2\">\n <Label htmlFor={`${scope}-country`} className=\"text-xs\">\n {countryLabel}\n </Label>\n <Combobox\n size={size === 'sm' ? 'sm' : 'md'}\n value={value.country}\n disabled={disabled}\n placeholder={countryPlaceholder}\n searchPlaceholder={countrySearchPlaceholder}\n options={countries.map((code) => ({ value: code, label: countryName(code) }))}\n onValueChange={(country) => set({ country })}\n />\n </div>\n\n {field('line1', addressLabel, 'address-line1', true, 'sm:col-span-2')}\n {field('line2', addressLine2Label, 'address-line2', false, 'sm:col-span-2')}\n\n {rules.postalFirst && postal}\n {field('city', cityLabel, 'address-level2', true)}\n {field('region', rules.regionLabel, 'address-level1', rules.regionRequired)}\n {!rules.postalFirst && postal}\n </div>\n )\n}\n\nexport { AddressInput, RULES as addressRules, EMPTY as emptyAddress }\n"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "agent-steps",
3
+ "type": "registry:ui",
4
+ "title": "Agent Steps",
5
+ "description": "An agent's plan with each step's state. The whole plan is shown from the start, including steps not yet reached — revealing them one at a time hides how much is left, which is what someone watching most wants to know.",
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/agent-steps.tsx",
19
+ "type": "registry:ui",
20
+ "content": "import { useState, type ComponentProps, type ReactNode } from 'react'\nimport { Check, ChevronDown, ChevronRight, Circle, CircleSlash, X } 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 * An agent's plan, with each step's state as it runs.\n *\n * The plan is shown whole from the start, including steps not yet reached.\n * Revealing them one at a time hides how much is left, which is the single\n * thing someone watching an agent most wants to know.\n *\n * Related to `ToolCall`, which shows one invocation with its arguments and\n * result. This is the level above: the sequence those calls belong to.\n */\nexport type AgentStepStatus = 'pending' | 'running' | 'done' | 'failed' | 'skipped'\n\nexport type AgentStep = {\n id: string\n label: ReactNode\n status: AgentStepStatus\n /** Seconds. */\n duration?: number\n /** Tool calls, output, an error — anything expandable. */\n detail?: ReactNode\n}\n\nconst STATUS = {\n pending: { Icon: Circle, tone: 'text-muted-foreground/40' },\n running: { Icon: null, tone: 'text-[var(--blue-soft-foreground)]' },\n done: { Icon: Check, tone: 'text-[var(--green-soft-foreground)]' },\n failed: { Icon: X, tone: 'text-[var(--destructive-soft-foreground)]' },\n skipped: { Icon: CircleSlash, tone: 'text-muted-foreground/60' },\n} as const\n\nfunction AgentSteps({\n steps,\n defaultExpanded = [],\n title,\n className,\n ...props\n}: ComponentProps<'div'> & {\n steps: AgentStep[]\n defaultExpanded?: string[]\n title?: ReactNode\n}) {\n const [expanded, setExpanded] = useState<string[]>(defaultExpanded)\n\n const done = steps.filter((step) => step.status === 'done').length\n const running = steps.some((step) => step.status === 'running')\n\n return (\n <div\n data-slot=\"agent-steps\"\n className={cn('border-border overflow-hidden border', radius.surface, className)}\n {...props}\n >\n <div className=\"border-border bg-muted/40 flex items-center gap-2 border-b p-3\">\n <span className=\"min-w-0 flex-1 truncate text-xs font-medium\">\n {title ?? 'Plan'}\n </span>\n <span\n className=\"text-muted-foreground shrink-0 text-xs tabular-nums\"\n aria-live=\"polite\"\n >\n {done} / {steps.length}\n {running && ' · running'}\n </span>\n </div>\n\n <ol className=\"divide-border/60 flex list-none flex-col divide-y\">\n {steps.map((step) => {\n const { Icon, tone } = STATUS[step.status]\n const open = expanded.includes(step.id)\n\n return (\n <li key={step.id} data-status={step.status}>\n <div className=\"flex items-center gap-2.5 px-3 py-2\">\n {step.detail ? (\n <button\n type=\"button\"\n aria-expanded={open}\n aria-label={`${open ? 'Hide' : 'Show'} detail`}\n onClick={() =>\n setExpanded((current) =>\n open\n ? current.filter((id) => id !== step.id)\n : [...current, step.id],\n )\n }\n className={cn(\n 'text-muted-foreground hover:text-foreground -m-1 shrink-0 p-1',\n radius.xs,\n interactive,\n focusRing,\n )}\n >\n {open ? (\n <ChevronDown className=\"size-3.5\" />\n ) : (\n <ChevronRight className=\"size-3.5\" />\n )}\n </button>\n ) : (\n <span className=\"w-3.5 shrink-0\" />\n )}\n\n {Icon ? (\n <Icon className={cn('size-4 shrink-0', tone)} aria-hidden=\"true\" />\n ) : (\n <Spinner size=\"xs\" className={cn('shrink-0', tone)} label=\"Running\" />\n )}\n\n <span\n className={cn(\n 'min-w-0 flex-1 text-sm',\n step.status === 'pending' && 'text-muted-foreground',\n step.status === 'skipped' &&\n 'text-muted-foreground/60 line-through',\n )}\n >\n {step.label}\n </span>\n\n {step.duration !== undefined && (\n <span className=\"text-muted-foreground shrink-0 text-xs tabular-nums\">\n <Fmt type=\"duration\" value={step.duration} />\n </span>\n )}\n </div>\n\n {open && step.detail && (\n <div className=\"bg-muted/30 border-border/60 border-t p-3 text-sm\">\n {step.detail}\n </div>\n )}\n </li>\n )\n })}\n </ol>\n </div>\n )\n}\n\nexport { AgentSteps }\n"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "alert-dialog",
3
+ "type": "registry:ui",
4
+ "title": "Alert Dialog",
5
+ "description": "A modal that interrupts to confirm something consequential. Unlike Dialog it has no backdrop dismissal and no corner close — a destructive confirmation should take a deliberate answer.",
6
+ "category": "Overlays",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "button",
10
+ "lib-styles",
11
+ "lib-utils",
12
+ "primitive-dialog",
13
+ "primitive-slot"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/ui/alert-dialog.tsx",
18
+ "type": "registry:ui",
19
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { DialogProvider, useDialog } from '@/components/primitives/dialog'\nimport { Slot } from '@/components/primitives/slot'\nimport { Button } from '@/components/ui/button'\nimport { cardPadding, radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A modal that interrupts to confirm something consequential.\n *\n * Distinct from Dialog in three ways that matter: `role=\"alertdialog\"`, no\n * backdrop dismissal and no corner close — a destructive confirmation should\n * take a deliberate answer, not an accidental click outside. Escape still works,\n * because trapping someone in a modal is worse.\n */\nfunction AlertDialog({\n open,\n defaultOpen,\n onOpenChange,\n children,\n}: {\n open?: boolean\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n children: ReactNode\n}) {\n return (\n <DialogProvider\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n >\n {children}\n </DialogProvider>\n )\n}\n\nfunction AlertDialogTrigger({\n asChild = false,\n onClick,\n ...props\n}: ComponentProps<'button'> & { asChild?: boolean }) {\n const { setOpen } = useDialog()\n const Comp = asChild ? Slot : 'button'\n\n return (\n <Comp\n data-slot=\"alert-dialog-trigger\"\n onClick={(event: React.MouseEvent<HTMLButtonElement>) => {\n onClick?.(event)\n if (!event.defaultPrevented) setOpen(true)\n }}\n {...props}\n />\n )\n}\n\nfunction AlertDialogContent({\n className,\n children,\n ...props\n}: ComponentProps<'dialog'>) {\n const { dialogRef, ids } = useDialog()\n\n return (\n <dialog\n ref={dialogRef}\n role=\"alertdialog\"\n data-slot=\"alert-dialog\"\n aria-labelledby={ids.title}\n aria-describedby={ids.description}\n className={cn(\n surface,\n radius.panel,\n 'text-card-foreground m-auto w-[calc(100vw-2rem)] max-w-md p-0',\n 'backdrop:bg-transparent',\n className,\n )}\n {...props}\n >\n <div className=\"flex flex-col\">{children}</div>\n </dialog>\n )\n}\n\nfunction AlertDialogHeader({ className, ...props }: ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"alert-dialog-header\"\n className={cn('flex flex-col gap-2', cardPadding.lg, className)}\n {...props}\n />\n )\n}\n\nfunction AlertDialogTitle({ className, ...props }: ComponentProps<'h2'>) {\n const { ids } = useDialog()\n\n return (\n <h2\n id={ids.title}\n data-slot=\"alert-dialog-title\"\n className={cn('text-base font-semibold', className)}\n {...props}\n />\n )\n}\n\nfunction AlertDialogDescription({ className, ...props }: ComponentProps<'p'>) {\n const { ids } = useDialog()\n\n return (\n <p\n id={ids.description}\n data-slot=\"alert-dialog-description\"\n className={cn('text-muted-foreground text-sm', className)}\n {...props}\n />\n )\n}\n\nfunction AlertDialogFooter({ className, ...props }: ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"alert-dialog-footer\"\n className={cn(\n 'border-border flex flex-col-reverse gap-2 border-t sm:flex-row sm:justify-end',\n cardPadding.lg,\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction AlertDialogCancel({\n className,\n onClick,\n ...props\n}: ComponentProps<typeof Button>) {\n const { setOpen } = useDialog()\n\n return (\n <Button\n variant=\"secondary\"\n data-slot=\"alert-dialog-cancel\"\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) setOpen(false)\n }}\n className={className}\n {...props}\n />\n )\n}\n\nfunction AlertDialogAction({\n className,\n onClick,\n ...props\n}: ComponentProps<typeof Button>) {\n const { setOpen } = useDialog()\n\n return (\n <Button\n data-slot=\"alert-dialog-action\"\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) setOpen(false)\n }}\n className={className}\n {...props}\n />\n )\n}\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogTrigger,\n}\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "alert-triage",
3
+ "type": "registry:ui",
4
+ "title": "Alert Triage",
5
+ "description": "An alert queue grouped by fingerprint. A flapping check fires two hundred times, and an ungrouped list buries every other alert underneath it — which is how real pages get missed.",
6
+ "category": "AIOps",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "badge",
12
+ "button",
13
+ "checkbox",
14
+ "fmt",
15
+ "lib-styles",
16
+ "lib-utils"
17
+ ],
18
+ "files": [
19
+ {
20
+ "path": "components/ui/alert-triage.tsx",
21
+ "type": "registry:ui",
22
+ "content": "import { useMemo, useState, type ComponentProps, type ReactNode } from 'react'\nimport { AlertTriangle, BellOff, Check, ChevronDown, ChevronRight } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Checkbox } from '@/components/ui/checkbox'\nimport { Fmt } from '@/components/ui/fmt'\nimport { focusRing, interactive, radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * An alert queue, grouped by fingerprint.\n *\n * Grouping is the whole job. A flapping check fires two hundred times and the\n * useful view is \"this one alert, 200 occurrences\" — an ungrouped list buries\n * every other alert underneath it, which is how real pages get missed.\n *\n * Bulk actions report ids and never mutate the list. Acknowledging is a\n * server-side decision, and a queue that optimistically hid rows would show two\n * responders different pictures of the same incident.\n */\nexport type Alert = {\n id: string\n title: ReactNode\n severity: 'critical' | 'warning' | 'info'\n /** Alerts sharing a fingerprint collapse into one row. */\n fingerprint?: string\n count?: number\n source?: string\n firstSeen?: Date\n lastSeen?: Date\n acknowledged?: boolean\n detail?: ReactNode\n}\n\nconst SEVERITY = {\n critical: { label: 'Critical', color: 'destructive', edge: 'bg-[var(--destructive)]' },\n warning: { label: 'Warning', color: 'amber', edge: 'bg-[var(--amber)]' },\n info: { label: 'Info', color: 'blue', edge: 'bg-[var(--blue)]' },\n} as const\n\nconst RANK = { critical: 0, warning: 1, info: 2 } as const\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_SELECT_ALERT_LABEL: (fingerprint: string) => string = (key) => `Select alert ${key}`\nconst DEFAULT_TOGGLE_DETAIL_LABEL: (open: boolean) => string = (open) => `${open ? 'Hide' : 'Show'} alert detail`\nconst DEFAULT_SELECTED_LABEL: (count: number) => ReactNode = (count) => `${count} selected`\nconst DEFAULT_SUMMARY_LABEL: (total: number, critical: number) => ReactNode = (total, critical) => `${total} alerts · ${critical} critical`\n\nfunction AlertTriage({\n alerts,\n onAcknowledge,\n onSilence,\n now,\n selectAllLabel = 'Select all alerts',\n clearSelectionLabel = 'Clear selection',\n selectAlertLabel = DEFAULT_SELECT_ALERT_LABEL,\n toggleDetailLabel = DEFAULT_TOGGLE_DETAIL_LABEL,\n selectedLabel = DEFAULT_SELECTED_LABEL,\n summaryLabel = DEFAULT_SUMMARY_LABEL,\n acknowledgeLabel = 'Acknowledge',\n silenceLabel = 'Silence',\n acknowledgedLabel = 'Acked',\n lastSeenLabel = 'last',\n firstSeenLabel = 'first',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'onSelect'> & {\n alerts: Alert[]\n onAcknowledge?: (ids: string[]) => void\n onSilence?: (ids: string[]) => void\n now?: Date\n selectAllLabel?: string\n clearSelectionLabel?: string\n selectAlertLabel?: (fingerprint: string) => string\n toggleDetailLabel?: (open: boolean) => string\n selectedLabel?: (count: number) => ReactNode\n /** Roll-up shown when nothing is selected. */\n summaryLabel?: (total: number, critical: number) => ReactNode\n acknowledgeLabel?: ReactNode\n silenceLabel?: ReactNode\n acknowledgedLabel?: ReactNode\n /** Precedes the last-seen time. */\n lastSeenLabel?: ReactNode\n /** Precedes the first-seen time. */\n firstSeenLabel?: ReactNode\n}) {\n const [selected, setSelected] = useState<string[]>([])\n const [expanded, setExpanded] = useState<string[]>([])\n\n // Group by fingerprint, then sort by severity. Order within a group is the\n // caller's; only the roll-up is computed.\n const groups = useMemo(() => {\n const map = new Map<string, Alert[]>()\n for (const alert of alerts) {\n const key = alert.fingerprint ?? alert.id\n map.set(key, [...(map.get(key) ?? []), alert])\n }\n return [...map.entries()]\n .map(([key, members]) => ({\n key,\n lead: members[0],\n count: members.reduce((sum, alert) => sum + (alert.count ?? 1), 0),\n members,\n }))\n .sort((a, b) => RANK[a.lead.severity] - RANK[b.lead.severity])\n }, [alerts])\n\n const allKeys = groups.map((group) => group.key)\n const allSelected = allKeys.length > 0 && allKeys.every((key) => selected.includes(key))\n\n const idsFor = (keys: string[]) =>\n groups.filter((group) => keys.includes(group.key)).flatMap((group) => group.members.map((m) => m.id))\n\n return (\n <div\n data-slot=\"alert-triage\"\n className={cn(surface, radius.surface, 'overflow-hidden', className)}\n {...props}\n >\n <div className=\"border-border bg-muted/40 flex flex-wrap items-center gap-2 border-b p-2\">\n <Checkbox\n aria-label={allSelected ? clearSelectionLabel : selectAllLabel}\n checked={allSelected}\n indeterminate={selected.length > 0 && !allSelected}\n onChange={() => setSelected(allSelected ? [] : allKeys)}\n />\n <span className=\"text-muted-foreground text-xs\">\n {selected.length > 0\n ? selectedLabel(selected.length)\n : summaryLabel(\n groups.length,\n groups.filter((g) => g.lead.severity === 'critical').length,\n )}\n </span>\n\n {selected.length > 0 && (\n <div className=\"ms-auto flex flex-wrap gap-1.5\">\n {onAcknowledge && (\n <Button\n size=\"xs\"\n variant=\"secondary\"\n onClick={() => {\n onAcknowledge(idsFor(selected))\n setSelected([])\n }}\n >\n <Check />\n {acknowledgeLabel}\n </Button>\n )}\n {onSilence && (\n <Button\n size=\"xs\"\n variant=\"secondary\"\n onClick={() => {\n onSilence(idsFor(selected))\n setSelected([])\n }}\n >\n <BellOff />\n {silenceLabel}\n </Button>\n )}\n </div>\n )}\n </div>\n\n <ul className=\"list-none divide-y divide-[var(--border)]\">\n {groups.map((group) => {\n const alert = group.lead\n const level = SEVERITY[alert.severity]\n const open = expanded.includes(group.key)\n const isSelected = selected.includes(group.key)\n\n return (\n <li\n key={group.key}\n data-severity={alert.severity}\n className={cn('relative ps-1.5', alert.acknowledged && 'opacity-60')}\n >\n <span\n aria-hidden=\"true\"\n className={cn('absolute inset-y-0 start-0 w-1', level.edge)}\n />\n\n <div className=\"flex items-start gap-2.5 p-3\">\n <Checkbox\n aria-label={selectAlertLabel(group.key)}\n checked={isSelected}\n className=\"mt-0.5\"\n onChange={() =>\n setSelected((current) =>\n isSelected\n ? current.filter((key) => key !== group.key)\n : [...current, group.key],\n )\n }\n />\n\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <Badge size=\"sm\" color={level.color}>\n <AlertTriangle />\n {level.label}\n </Badge>\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium\">\n {alert.title}\n </span>\n {group.count > 1 && (\n <Badge size=\"sm\">×{group.count}</Badge>\n )}\n {alert.acknowledged && (\n <Badge size=\"sm\" color=\"green\">\n <Check />\n {acknowledgedLabel}\n </Badge>\n )}\n </div>\n\n <p className=\"text-muted-foreground mt-1 flex flex-wrap items-center gap-x-2 text-xs\">\n {alert.source && <span>{alert.source}</span>}\n {alert.lastSeen && (\n <span>\n {lastSeenLabel} <Fmt type=\"relative\" value={alert.lastSeen} now={now} />\n </span>\n )}\n {alert.firstSeen && (\n <span>\n {firstSeenLabel} <Fmt type=\"relative\" value={alert.firstSeen} now={now} />\n </span>\n )}\n </p>\n </div>\n\n {alert.detail && (\n <button\n type=\"button\"\n aria-expanded={open}\n aria-label={toggleDetailLabel(open)}\n onClick={() =>\n setExpanded((current) =>\n open\n ? current.filter((key) => key !== group.key)\n : [...current, group.key],\n )\n }\n className={cn(\n 'text-muted-foreground hover:text-foreground shrink-0 p-1',\n radius.xs,\n interactive,\n focusRing,\n )}\n >\n {open ? (\n <ChevronDown className=\"size-4\" />\n ) : (\n <ChevronRight className=\"size-4\" />\n )}\n </button>\n )}\n </div>\n\n {open && alert.detail && (\n <div className=\"bg-muted/30 border-border/60 border-t p-3 text-sm\">\n {alert.detail}\n </div>\n )}\n </li>\n )\n })}\n </ul>\n </div>\n )\n}\n\nexport { AlertTriage }\n"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "alert",
3
+ "type": "registry:ui",
4
+ "title": "Alert",
5
+ "description": "A prominent message that stays on the page. Keeps the colour system, and picks its ARIA role from severity — a destructive alert interrupts, the rest wait their turn.",
6
+ "category": "Feedback",
7
+ "dependencies": [
8
+ "class-variance-authority@^0.7.1"
9
+ ],
10
+ "registryDependencies": [
11
+ "lib-styles",
12
+ "lib-utils"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/ui/alert.tsx",
17
+ "type": "registry:ui",
18
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { colorSet, radius, tintStyle, type ColorSet } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A prominent message that stays on the page.\n *\n * Like Badge, an alert is meant to be noticed, so it keeps the colour system.\n * `role` follows severity: a destructive alert is announced immediately, the\n * rest are announced when the user gets to them.\n */\nconst alertVariants = cva(\n ['flex w-full gap-3', \"[&_svg:not([class*='size-'])]:size-4\"].join(' '),\n {\n variants: {\n variant: {\n /** Tinted panel — the readable default for a long message. */\n default: 'bg-[var(--ui-soft)] text-[var(--ui-soft-fg)]',\n outline:\n 'border border-[color-mix(in_oklab,var(--ui),transparent_70%)] text-[var(--ui-soft-fg)]',\n /** Solid, for a message that must not be missed. */\n solid: 'bg-[var(--ui)] text-[var(--ui-fg)]',\n },\n color: colorSet,\n size: {\n sm: 'p-3 text-xs',\n default: 'p-4 text-sm',\n },\n },\n defaultVariants: { variant: 'default', color: 'neutral', size: 'default' },\n },\n)\n\ntype AlertProps = Omit<ComponentProps<'div'>, 'color' | 'title'> &\n VariantProps<typeof alertVariants> & {\n icon?: ReactNode\n title?: ReactNode\n /** Any CSS colour, used instead of a named `color` set. */\n tint?: string\n }\n\nfunction Alert({\n className,\n variant,\n color,\n size,\n icon,\n title,\n tint,\n style,\n children,\n ...props\n}: AlertProps) {\n return (\n <div\n // A destructive message interrupts; anything else waits its turn.\n role={color === 'destructive' ? 'alert' : 'status'}\n data-slot=\"alert\"\n className={cn(alertVariants({ variant, color, size }), radius.surface, className)}\n style={tint ? { ...tintStyle(tint), ...style } : style}\n {...props}\n >\n {icon && <span className=\"mt-0.5 shrink-0\">{icon}</span>}\n <div className=\"min-w-0 flex-1 space-y-1\">\n {title && <div className=\"font-medium\">{title}</div>}\n {children && <div className=\"opacity-90\">{children}</div>}\n </div>\n </div>\n )\n}\n\nfunction AlertTitle({ className, ...props }: ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"alert-title\"\n className={cn('font-medium', className)}\n {...props}\n />\n )\n}\n\nfunction AlertDescription({ className, ...props }: ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"alert-description\"\n className={cn('opacity-90', className)}\n {...props}\n />\n )\n}\n\nexport { Alert, AlertDescription, AlertTitle, alertVariants }\nexport type { AlertProps, ColorSet }\n"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "anomaly-chart",
3
+ "type": "registry:ui",
4
+ "title": "Anomaly Chart",
5
+ "description": "A metric against its expected range, with outliers marked. The band is the point — 400ms at 03:00 may be an incident while 400ms at peak is Tuesday.",
6
+ "category": "AIOps",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "lib-utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/anomaly-chart.tsx",
14
+ "type": "registry:ui",
15
+ "content": "import { useId, useMemo, type ComponentProps, type ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\n\n/**\n * A metric plotted against its expected range, with outliers marked.\n *\n * The band is the point. A spike means nothing without knowing what normal\n * looked like at that moment — 400ms at 03:00 may be an incident while 400ms at\n * peak is Tuesday. Detection stays outside this component: it renders the\n * `expected` range you give it and marks whatever falls outside, rather than\n * inventing a threshold of its own.\n *\n * Points are drawn in a second, unstretched layer. The plot is stretched to fit\n * its container, and a circle inside that stretch would render as an ellipse.\n */\nexport type AnomalyPoint = {\n value: number\n /** Expected [low, high] at this moment. */\n expected?: [number, number]\n label?: string\n}\n\nconst PLOT = { width: 100, height: 60 } as const\n\nfunction AnomalyChart({\n points,\n height = 160,\n color = 'var(--blue)',\n bandColor = 'var(--blue)',\n anomalyColor = 'var(--destructive)',\n labels,\n observedLabel = 'Observed',\n expectedLabel = 'Expected range',\n className,\n ...props\n}: Omit<ComponentProps<'figure'>, 'height'> & {\n points: AnomalyPoint[]\n height?: number\n color?: string\n bandColor?: string\n anomalyColor?: string\n labels?: string[]\n /** Legend entries. */\n observedLabel?: ReactNode\n expectedLabel?: ReactNode\n}) {\n const titleId = useId()\n\n const { min, span } = useMemo(() => {\n const values = points.flatMap((point) => [\n point.value,\n ...(point.expected ?? []),\n ])\n const low = Math.min(...values)\n const high = Math.max(...values)\n // A little headroom, so an anomaly never sits exactly on the edge.\n const pad = (high - low || 1) * 0.1\n return { min: low - pad, span: high - low + pad * 2 || 1 }\n }, [points])\n\n const x = (index: number) =>\n points.length === 1 ? PLOT.width / 2 : (index / (points.length - 1)) * PLOT.width\n const y = (value: number) => PLOT.height - ((value - min) / span) * PLOT.height\n\n const anomalies = points\n .map((point, index) => ({ point, index }))\n .filter(\n ({ point }) =>\n point.expected &&\n (point.value < point.expected[0] || point.value > point.expected[1]),\n )\n\n const banded = points.filter((point) => point.expected)\n const bandPath =\n banded.length === points.length && points.length > 0\n ? [\n ...points.map((point, index) => `${x(index)},${y(point.expected![1])}`),\n ...[...points]\n .reverse()\n .map((point, index) => `${x(points.length - 1 - index)},${y(point.expected![0])}`),\n ].join(' ')\n : undefined\n\n return (\n <figure\n data-slot=\"anomaly-chart\"\n className={cn('flex min-w-0 flex-col gap-2', className)}\n {...props}\n >\n <div className=\"relative min-w-0\" style={{ height }}>\n <svg\n viewBox={`0 0 ${PLOT.width} ${PLOT.height}`}\n preserveAspectRatio=\"none\"\n role=\"img\"\n aria-labelledby={titleId}\n className=\"size-full\"\n >\n {/* One string: React cannot join multiple children into a <title>. */}\n <title id={titleId}>\n {`Metric against expected range${\n anomalies.length > 0 ? `, ${anomalies.length} outside the band` : ''\n }`}\n </title>\n\n {bandPath && <polygon points={bandPath} fill={bandColor} opacity={0.12} />}\n\n <polyline\n points={points.map((point, index) => `${x(index)},${y(point.value)}`).join(' ')}\n fill=\"none\"\n stroke={color}\n strokeWidth={1.75}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n />\n </svg>\n\n {/* Unstretched overlay: a circle in the layer above keeps its shape. */}\n <svg\n viewBox={`0 0 ${PLOT.width} ${PLOT.height}`}\n className=\"pointer-events-none absolute inset-0 size-full overflow-visible\"\n aria-hidden=\"true\"\n >\n {anomalies.map(({ point, index }) => (\n <circle\n key={index}\n cx={x(index)}\n cy={y(point.value)}\n r={2.5}\n fill={anomalyColor}\n stroke=\"var(--background)\"\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n />\n ))}\n </svg>\n </div>\n\n {labels && (\n <div className=\"text-muted-foreground/70 flex justify-between text-[10px]\" aria-hidden=\"true\">\n {labels.map((label, index) => (\n <span key={index}>{label}</span>\n ))}\n </div>\n )}\n\n <figcaption className=\"text-muted-foreground flex flex-wrap gap-x-4 gap-y-1 text-xs\">\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-0.5 w-3 rounded-full\" style={{ backgroundColor: color }} />\n {observedLabel}\n </span>\n <span className=\"flex items-center gap-1.5\">\n <span\n className=\"h-2.5 w-3 rounded-sm\"\n style={{ backgroundColor: bandColor, opacity: 0.25 }}\n />\n {expectedLabel}\n </span>\n {anomalies.length > 0 && (\n <span className=\"flex items-center gap-1.5\">\n <span\n className=\"size-2 rounded-full [corner-shape:round]\"\n style={{ backgroundColor: anomalyColor }}\n />\n {anomalies.length} anomal{anomalies.length === 1 ? 'y' : 'ies'}\n </span>\n )}\n </figcaption>\n </figure>\n )\n}\n\nexport { AnomalyChart }\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "api-keys",
3
+ "type": "registry:ui",
4
+ "title": "API Keys",
5
+ "description": "Credentials, listed and revocable. A key is shown in full exactly once — after that only the prefix and last four, because a list that can reveal every key means a session holds every secret.",
6
+ "category": "Auth",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "badge",
12
+ "button",
13
+ "copy-button",
14
+ "fmt",
15
+ "lib-styles",
16
+ "lib-utils"
17
+ ],
18
+ "files": [
19
+ {
20
+ "path": "components/ui/api-keys.tsx",
21
+ "type": "registry:ui",
22
+ "content": "import { useState, type ComponentProps, type ReactNode } from 'react'\nimport { Eye, EyeOff, KeyRound, Trash2 } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { CopyButton } from '@/components/ui/copy-button'\nimport { Fmt } from '@/components/ui/fmt'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * API credentials, listed and revocable.\n *\n * A key is shown in full exactly once — when it is created. After that only the\n * prefix and last four characters are available, because a list that can reveal\n * every key on demand means anyone with a session has every secret. `secret` is\n * therefore optional and expected to be absent for existing keys.\n *\n * Revoking is confirmed inline rather than in a dialog. The confirmation needs\n * to sit beside the key it affects — a modal saying \"revoke this key?\" with the\n * list hidden behind it is how the wrong one gets revoked.\n */\nexport type ApiKey = {\n id: string\n name: string\n /** Shown as `sk_live_…abcd`. */\n prefix: string\n last4: string\n created: Date\n lastUsed?: Date\n scopes?: string[]\n /** Present only for a key just created. */\n secret?: string\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_REVOKE_ACTION_LABEL: (name: string) => string = (name) => `Revoke ${name}`\n\nfunction ApiKeys({\n keys,\n onRevoke,\n now,\n emptyLabel = 'No API keys yet',\n freshLabel = 'Copy it now',\n revealLabel = 'Reveal key',\n hideLabel = 'Hide key',\n copyLabel = 'Copy key',\n revokeLabel = 'Revoke',\n revokeActionLabel = DEFAULT_REVOKE_ACTION_LABEL,\n createdLabel = 'Created',\n lastUsedLabel = 'Last used',\n neverUsedLabel = 'Never used',\n confirmNote = 'Revoking is immediate and cannot be undone.',\n cancelLabel = 'Cancel',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n keys: ApiKey[]\n onRevoke?: (id: string) => void\n now?: Date\n emptyLabel?: ReactNode\n /** Badge on a key whose secret is still visible. */\n freshLabel?: ReactNode\n revealLabel?: string\n hideLabel?: string\n copyLabel?: string\n revokeLabel?: ReactNode\n /** Accessible name for the revoke control, given the key's name. */\n revokeActionLabel?: (name: string) => string\n createdLabel?: ReactNode\n lastUsedLabel?: ReactNode\n neverUsedLabel?: ReactNode\n confirmNote?: ReactNode\n cancelLabel?: ReactNode\n}) {\n const [revealed, setRevealed] = useState<string[]>([])\n const [confirming, setConfirming] = useState<string | null>(null)\n\n return (\n <div\n data-slot=\"api-keys\"\n className={cn(surface, radius.surface, 'overflow-hidden', className)}\n {...props}\n >\n {keys.length === 0 ? (\n <p className=\"text-muted-foreground p-8 text-center text-sm\">{emptyLabel}</p>\n ) : (\n <ul className=\"list-none divide-y divide-[var(--border)]\">\n {keys.map((key) => {\n const open = revealed.includes(key.id)\n const display =\n key.secret && open\n ? key.secret\n : `${key.prefix}${'•'.repeat(8)}${key.last4}`\n\n return (\n <li key={key.id} className=\"flex flex-col gap-2 p-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <KeyRound className=\"text-muted-foreground size-4 shrink-0\" aria-hidden=\"true\" />\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium\">\n {key.name}\n </span>\n {key.secret && (\n <Badge size=\"sm\" color=\"amber\">\n {freshLabel}\n </Badge>\n )}\n {key.scopes?.map((scope) => (\n <Badge key={scope} size=\"sm\">\n {scope}\n </Badge>\n ))}\n </div>\n\n <div className=\"flex items-center gap-1\">\n <code className=\"text-muted-foreground min-w-0 flex-1 truncate font-mono text-xs\">\n {display}\n </code>\n\n {/* Only a freshly created key can be revealed at all. */}\n {key.secret && (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={open ? hideLabel : revealLabel}\n aria-pressed={open}\n onClick={() =>\n setRevealed((current) =>\n open ? current.filter((id) => id !== key.id) : [...current, key.id],\n )\n }\n >\n {open ? <EyeOff /> : <Eye />}\n </Button>\n )}\n\n {key.secret && <CopyButton value={key.secret} label={copyLabel} />}\n\n {onRevoke && (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={revokeActionLabel(String(key.name))}\n className=\"text-[var(--destructive-soft-foreground)]\"\n onClick={() => setConfirming(key.id)}\n >\n <Trash2 />\n </Button>\n )}\n </div>\n\n <p className=\"text-muted-foreground/70 flex flex-wrap gap-x-3 text-xs\">\n <span>\n {createdLabel} <Fmt type=\"relative\" value={key.created} now={now} />\n </span>\n <span>\n {key.lastUsed ? (\n <>\n {lastUsedLabel} <Fmt type=\"relative\" value={key.lastUsed} now={now} />\n </>\n ) : (\n neverUsedLabel\n )}\n </span>\n </p>\n\n {confirming === key.id && (\n <div className=\"border-border bg-muted/40 flex flex-wrap items-center gap-2 border p-2 text-xs\">\n <span className=\"text-muted-foreground min-w-0 flex-1\">\n {confirmNote}\n </span>\n <Button\n size=\"xs\"\n color=\"destructive\"\n onClick={() => {\n onRevoke?.(key.id)\n setConfirming(null)\n }}\n >\n {revokeLabel}\n </Button>\n <Button size=\"xs\" variant=\"secondary\" onClick={() => setConfirming(null)}>\n {cancelLabel}\n </Button>\n </div>\n )}\n </li>\n )\n })}\n </ul>\n )}\n </div>\n )\n}\n\nexport { ApiKeys }\n"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "aspect-ratio",
3
+ "type": "registry:ui",
4
+ "title": "Aspect Ratio",
5
+ "description": "Hold a box at a fixed ratio while its width is fluid. Modern CSS aspect-ratio, so no padding-top hack.",
6
+ "category": "Display",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "lib-utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/aspect-ratio.tsx",
14
+ "type": "registry:ui",
15
+ "content": "import type { ComponentProps } from 'react'\nimport { cn } from '@/lib/utils'\n\n/**\n * Hold a box at a fixed ratio while its width is fluid.\n *\n * Modern `aspect-ratio` makes the old padding-top trick unnecessary; this exists\n * so the ratio is a prop rather than an inline style at every call site.\n */\nfunction AspectRatio({\n className,\n ratio = 16 / 9,\n style,\n ...props\n}: ComponentProps<'div'> & { ratio?: number }) {\n return (\n <div\n data-slot=\"aspect-ratio\"\n className={cn('w-full overflow-hidden', className)}\n style={{ aspectRatio: ratio, ...style }}\n {...props}\n />\n )\n}\n\nexport { AspectRatio }\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "attachment-preview",
3
+ "type": "registry:ui",
4
+ "title": "Attachment Preview",
5
+ "description": "A file chip with an icon chosen from the MIME type. Never from the extension: a `.dat` that is really a PDF should show a PDF, and a `.pdf` that is really an executable should not.",
6
+ "category": "Messaging",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "button",
12
+ "lib-styles",
13
+ "lib-utils",
14
+ "progress"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "components/ui/attachment-preview.tsx",
19
+ "type": "registry:ui",
20
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { Download, File, FileText, Film, ImageIcon, Music, X } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { Progress } from '@/components/ui/progress'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * An attached file: thumbnail, size, progress.\n *\n * The icon is chosen from the MIME type, not the file extension. Extensions are\n * user-supplied and routinely wrong or absent, and `invoice.pdf.exe` is the\n * oldest trick there is — deriving the icon from what the file claims to be by\n * name would draw a document icon on an executable.\n *\n * An upload in progress is the same component as a finished one, so the layout\n * does not jump when it completes. Files change height on completion in most\n * implementations and the list reflows under the pointer.\n */\nexport type Attachment = {\n id: string\n name: string\n /** MIME type. Drives the icon — not the extension. */\n type?: string\n /** Bytes. */\n size?: number\n url?: string\n thumbnail?: string\n /** 0–1 while uploading; omit when complete. */\n progress?: number\n error?: ReactNode\n}\n\nfunction iconFor(type: string | undefined) {\n if (!type) return File\n if (type.startsWith('image/')) return ImageIcon\n if (type.startsWith('video/')) return Film\n if (type.startsWith('audio/')) return Music\n if (type === 'application/pdf' || type.startsWith('text/')) return FileText\n return File\n}\n\nfunction formatBytes(bytes: number) {\n if (bytes < 1024) return `${bytes} B`\n const units = ['KB', 'MB', 'GB']\n let value = bytes / 1024\n let unit = 0\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024\n unit++\n }\n return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`\n}\n\nfunction AttachmentPreview({\n attachment,\n onRemove,\n onDownload,\n compact = false,\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children'> & {\n attachment: Attachment\n onRemove?: (id: string) => void\n onDownload?: (attachment: Attachment) => void\n compact?: boolean\n}) {\n const Icon = iconFor(attachment.type)\n const uploading = attachment.progress !== undefined && attachment.progress < 1\n\n return (\n <div\n data-slot=\"attachment-preview\"\n className={cn(\n surface,\n radius.control,\n 'flex items-center gap-3 p-2',\n attachment.error && 'border-[var(--destructive)]',\n className,\n )}\n {...props}\n >\n <span\n className={cn(\n 'bg-secondary flex shrink-0 items-center justify-center overflow-hidden',\n radius.xs,\n compact ? 'size-8' : 'size-10',\n )}\n >\n {attachment.thumbnail ? (\n <img src={attachment.thumbnail} alt=\"\" className=\"size-full object-cover\" />\n ) : (\n <Icon className=\"text-muted-foreground size-4\" aria-hidden=\"true\" />\n )}\n </span>\n\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium\">{attachment.name}</p>\n\n {/* Same row whether uploading or done — no reflow on completion. */}\n <div className=\"text-muted-foreground mt-0.5 flex items-center gap-2 text-xs\">\n {attachment.size !== undefined && (\n <span className=\"tabular-nums\">{formatBytes(attachment.size)}</span>\n )}\n {uploading && (\n <>\n <Progress value={attachment.progress! * 100} className=\"h-1 min-w-16 flex-1\" />\n <span className=\"tabular-nums\">{Math.round(attachment.progress! * 100)}%</span>\n </>\n )}\n {attachment.error && (\n <span className=\"text-[var(--destructive-soft-foreground)]\">{attachment.error}</span>\n )}\n </div>\n </div>\n\n <span className=\"flex shrink-0 items-center gap-1\">\n {onDownload && !uploading && !attachment.error && (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={`Download ${attachment.name}`}\n onClick={() => onDownload(attachment)}\n >\n <Download />\n </Button>\n )}\n {onRemove && (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={`Remove ${attachment.name}`}\n onClick={() => onRemove(attachment.id)}\n >\n <X />\n </Button>\n )}\n </span>\n </div>\n )\n}\n\nexport { AttachmentPreview, iconFor as attachmentIconFor }\n"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "audit-log",
3
+ "type": "registry:ui",
4
+ "title": "Audit Log",
5
+ "description": "Who did what to which thing, when. Denied and errored attempts are kept and marked rather than filtered out — a log of successes only tells you nothing about the attack you are looking for.",
6
+ "category": "Admin",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "avatar",
12
+ "badge",
13
+ "fmt",
14
+ "input",
15
+ "json-viewer",
16
+ "lib-styles",
17
+ "lib-utils"
18
+ ],
19
+ "files": [
20
+ {
21
+ "path": "components/ui/audit-log.tsx",
22
+ "type": "registry:ui",
23
+ "content": "import { useMemo, useState, type ComponentProps, type ReactNode } from 'react'\nimport { ChevronDown, ChevronRight, Search } from 'lucide-react'\nimport { Avatar } from '@/components/ui/avatar'\nimport { Badge } from '@/components/ui/badge'\nimport { Fmt } from '@/components/ui/fmt'\nimport { Input } from '@/components/ui/input'\nimport { JsonViewer } from '@/components/ui/json-viewer'\nimport { focusRing, interactive, radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * An append-only record of who did what.\n *\n * There is no edit or delete affordance anywhere in this component, by design.\n * An audit log that can be changed from the interface that displays it is not\n * evidence, and the absence of those buttons is the feature.\n *\n * Timestamps show both the relative and the absolute form. Relative is how\n * people scan; absolute is what gets quoted in an incident review, and having\n * to hover for it wastes the reviewer's time.\n *\n * A failed action is recorded and shown. An attempt that was denied is often\n * the most interesting row on the page.\n */\nexport type AuditEvent = {\n id: string\n actor: ReactNode\n actorAvatar?: ReactNode\n /** Verb — \"updated\", \"deleted\", \"signed in\". */\n action: string\n target?: ReactNode\n at: Date\n ip?: string\n outcome?: 'success' | 'denied' | 'error'\n /** Before/after, request payload — anything structured. */\n detail?: unknown\n}\n\nconst OUTCOME = {\n success: { label: 'OK', color: 'green' },\n denied: { label: 'Denied', color: 'amber' },\n error: { label: 'Error', color: 'destructive' },\n} as const\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_TOGGLE_DETAIL_LABEL: (open: boolean) => string = (open) => `${open ? 'Hide' : 'Show'} detail`\n\nfunction AuditLog({\n events,\n searchable = true,\n now,\n locale = 'en-GB',\n filterPlaceholder = 'Filter by actor, action or IP',\n filterLabel = 'Filter audit log',\n toggleDetailLabel = DEFAULT_TOGGLE_DETAIL_LABEL,\n emptyMessage = 'No events match.',\n className,\n ...props\n}: ComponentProps<'div'> & {\n events: AuditEvent[]\n searchable?: boolean\n now?: Date\n locale?: string\n filterPlaceholder?: string\n /** Accessible name for the filter field. */\n filterLabel?: string\n /** Accessible name for a row's detail toggle. */\n toggleDetailLabel?: (open: boolean) => string\n emptyMessage?: ReactNode\n}) {\n const [query, setQuery] = useState('')\n const [expanded, setExpanded] = useState<string[]>([])\n\n const rows = useMemo(() => {\n const term = query.trim().toLowerCase()\n if (!term) return events\n return events.filter((event) =>\n [event.actor, event.action, event.target, event.ip]\n .map((part) => String(part ?? '').toLowerCase())\n .some((part) => part.includes(term)),\n )\n }, [events, query])\n\n return (\n <div\n data-slot=\"audit-log\"\n className={cn(surface, radius.surface, 'flex flex-col overflow-hidden', className)}\n {...props}\n >\n {searchable && (\n <div className=\"border-border border-b p-2\">\n <Input\n size=\"sm\"\n variant=\"secondary\"\n icon={<Search />}\n placeholder={filterPlaceholder}\n aria-label={filterLabel}\n value={query}\n clearable\n onChange={(event) => setQuery(event.target.value)}\n containerClassName=\"sm:w-72\"\n />\n </div>\n )}\n\n <ul className=\"list-none divide-y divide-[var(--border)]\">\n {rows.map((event) => {\n const open = expanded.includes(event.id)\n const outcome = event.outcome ? OUTCOME[event.outcome] : undefined\n\n return (\n <li key={event.id}>\n <div className=\"flex items-start gap-2.5 p-3\">\n {event.detail !== undefined ? (\n <button\n type=\"button\"\n aria-expanded={open}\n aria-label={toggleDetailLabel(open)}\n onClick={() =>\n setExpanded((current) =>\n open ? current.filter((id) => id !== event.id) : [...current, event.id],\n )\n }\n className={cn(\n 'text-muted-foreground hover:text-foreground -m-1 mt-0 shrink-0 p-1',\n radius.xs,\n interactive,\n focusRing,\n )}\n >\n {open ? <ChevronDown className=\"size-3.5\" /> : <ChevronRight className=\"size-3.5\" />}\n </button>\n ) : (\n <span className=\"w-3.5 shrink-0\" />\n )}\n\n {event.actorAvatar ?? <Avatar size=\"xs\" name={String(event.actor)} className=\"mt-0.5 shrink-0\" />}\n\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm\">\n <span className=\"font-medium\">{event.actor}</span>{' '}\n <span className=\"text-muted-foreground\">{event.action}</span>{' '}\n {event.target && <span className=\"font-medium\">{event.target}</span>}\n </p>\n\n <p className=\"text-muted-foreground/80 mt-0.5 flex flex-wrap gap-x-3 text-xs\">\n {/* Both forms: relative to scan, absolute to quote. */}\n <span>\n <Fmt type=\"relative\" value={event.at} now={now} locale={locale} />\n </span>\n <span className=\"tabular-nums\">\n <Fmt type=\"date\" value={event.at} format=\"DD/MM/YYYY HH:mm:ss\" locale={locale} />\n </span>\n {event.ip && <span className=\"font-mono\">{event.ip}</span>}\n </p>\n </div>\n\n {outcome && (\n <Badge size=\"sm\" color={outcome.color} className=\"shrink-0\">\n {outcome.label}\n </Badge>\n )}\n </div>\n\n {open && event.detail !== undefined && (\n <div className=\"bg-muted/30 border-border/60 border-t p-3\">\n <JsonViewer value={event.detail as never} defaultExpandedDepth={2} />\n </div>\n )}\n </li>\n )\n })}\n\n {rows.length === 0 && (\n <li className=\"text-muted-foreground p-8 text-center text-sm\">{emptyMessage}</li>\n )}\n </ul>\n </div>\n )\n}\n\nexport { AuditLog }\n"
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "avatar",
3
+ "type": "registry:ui",
4
+ "title": "Avatar",
5
+ "description": "An image standing in for a person, with a fallback that always renders. Initials are derived from the name, and a broken image falls through to them.",
6
+ "category": "Display",
7
+ "dependencies": [
8
+ "class-variance-authority@^0.7.1"
9
+ ],
10
+ "registryDependencies": [
11
+ "lib-styles",
12
+ "lib-utils"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/ui/avatar.tsx",
17
+ "type": "registry:ui",
18
+ "content": "import {\n Children,\n cloneElement,\n isValidElement,\n useState,\n type ComponentProps,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { avatarSize, radius } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * An image standing in for a person, with a fallback that always renders.\n *\n * The image is swapped out on error rather than left broken, and initials are\n * derived from `name` so a caller does not have to compute them.\n */\nconst avatarVariants = cva(\n [\n 'relative inline-flex shrink-0 items-center justify-center overflow-hidden',\n 'bg-secondary text-secondary-foreground font-medium select-none',\n ].join(' '),\n {\n variants: {\n size: {\n xs: avatarSize.xs,\n sm: avatarSize.sm,\n default: avatarSize.default,\n lg: avatarSize.lg,\n xl: avatarSize.xl,\n },\n shape: {\n circle: 'rounded-full [corner-shape:round]',\n rounded: radius.control,\n },\n },\n defaultVariants: { size: 'default', shape: 'circle' },\n },\n)\n\n/** First letters of the first and last word — \"Ada Lovelace\" becomes \"AL\". */\nfunction initialsFrom(name: string) {\n const words = name.trim().split(/\\s+/).filter(Boolean)\n if (words.length === 0) return ''\n if (words.length === 1) return words[0].slice(0, 2).toUpperCase()\n return (words[0][0] + words[words.length - 1][0]).toUpperCase()\n}\n\ntype AvatarProps = Omit<ComponentProps<'span'>, 'children'> &\n VariantProps<typeof avatarVariants> & {\n src?: string\n /** Used for the alt text and to derive fallback initials. */\n name?: string\n /** Overrides the derived initials. */\n fallback?: ReactNode\n }\n\nfunction Avatar({\n className,\n size,\n shape,\n src,\n name,\n fallback,\n ...props\n}: AvatarProps) {\n const [failed, setFailed] = useState(false)\n const showImage = Boolean(src) && !failed\n\n return (\n <span\n data-slot=\"avatar\"\n className={cn(avatarVariants({ size, shape }), className)}\n {...props}\n >\n {showImage ? (\n <img\n src={src}\n alt={name ?? ''}\n // A broken URL falls through to the initials instead of an icon.\n onError={() => setFailed(true)}\n className=\"size-full object-cover\"\n />\n ) : (\n <span aria-hidden={Boolean(name)}>\n {fallback ?? (name ? initialsFrom(name) : null)}\n </span>\n )}\n {!showImage && name && <span className=\"sr-only\">{name}</span>}\n </span>\n )\n}\n\n/**\n * Overlapping row of avatars.\n *\n * The group's `size` is pushed onto every child rather than left to each one.\n * A row where the members and the \"+N\" chip disagree on size reads as a\n * rendering fault, and per-child sizes are the easiest way to end up there.\n */\nfunction AvatarGroup({\n className,\n max,\n size = 'default',\n children,\n ...props\n}: ComponentProps<'div'> & {\n max?: number\n size?: VariantProps<typeof avatarVariants>['size']\n}) {\n const items = Children.toArray(children).filter(isValidElement)\n const shown = max ? items.slice(0, max) : items\n const hidden = items.length - shown.length\n\n return (\n <div\n data-slot=\"avatar-group\"\n className={cn(\n 'flex items-center -space-x-2 [&_[data-slot=avatar]]:ring-background [&_[data-slot=avatar]]:ring-2',\n className,\n )}\n {...props}\n >\n {shown.map((child, index) =>\n cloneElement(child as ReactElement<AvatarProps>, {\n key: (child as ReactElement).key ?? index,\n size,\n }),\n )}\n {hidden > 0 && (\n <Avatar size={size} fallback={`+${hidden}`} className=\"bg-muted\" />\n )}\n </div>\n )\n}\n\n\n/**\n * A marker pinned to a corner of an Avatar — presence, a count, a small icon.\n *\n * Positioned against an `AvatarWithBadge` wrapper rather than the Avatar\n * itself, because the Avatar is `overflow-hidden` to clip its image: anything\n * absolutely positioned inside it would be cut off at exactly the corner it is\n * meant to sit on.\n *\n * The ring is not decoration. A status dot the same colour as something behind\n * it disappears; a ring in the surface colour keeps the badge legible on any\n * background, which is why it is on by default.\n */\nexport type AvatarBadgePosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'\n\nconst BADGE_POSITION = {\n 'top-right': 'top-0 end-0',\n 'top-left': 'top-0 start-0',\n 'bottom-right': 'bottom-0 end-0',\n 'bottom-left': 'bottom-0 start-0',\n} as const\n\n/** Short aliases, since these corners are usually written as two letters. */\nconst POSITION_ALIAS = {\n tr: 'top-right',\n tl: 'top-left',\n br: 'bottom-right',\n bl: 'bottom-left',\n} as const\n\nconst BADGE_SIZE = {\n xs: 'size-2 text-[8px]',\n sm: 'size-2.5 text-[8px]',\n default: 'size-3 text-[9px]',\n lg: 'size-3.5 text-[10px]',\n} as const\n\nconst BADGE_TONE = {\n online: 'bg-[var(--green)]',\n away: 'bg-[var(--amber)]',\n busy: 'bg-[var(--destructive)]',\n offline: 'bg-muted-foreground',\n primary: 'bg-primary text-primary-foreground',\n neutral: 'bg-secondary text-secondary-foreground',\n} as const\n\nfunction AvatarBadge({\n position = 'bottom-right',\n size = 'default',\n tone = 'online',\n ring = true,\n label,\n className,\n children,\n ...props\n}: ComponentProps<'span'> & {\n /** Corner to pin to. `tr`, `tl`, `br` and `bl` are accepted too. */\n position?: AvatarBadgePosition | keyof typeof POSITION_ALIAS\n size?: keyof typeof BADGE_SIZE\n tone?: keyof typeof BADGE_TONE\n /** Ring in the surface colour, so the badge reads on any background. */\n ring?: boolean\n /** Accessible name. A bare status dot is invisible without one. */\n label?: string\n}) {\n const corner =\n position in POSITION_ALIAS\n ? POSITION_ALIAS[position as keyof typeof POSITION_ALIAS]\n : (position as AvatarBadgePosition)\n\n return (\n <span\n data-slot=\"avatar-badge\"\n data-position={corner}\n // A dot with no text needs a name, or it is announced as nothing at all.\n role={label ? 'img' : undefined}\n aria-label={label}\n aria-hidden={label ? undefined : true}\n className={cn(\n 'pointer-events-none absolute z-10 flex items-center justify-center rounded-full font-medium [corner-shape:round]',\n BADGE_POSITION[corner],\n // Children need room; a bare dot does not.\n children ? 'min-w-4 px-1' : '',\n BADGE_SIZE[size],\n BADGE_TONE[tone],\n ring && 'ring-background ring-2',\n className,\n )}\n {...props}\n >\n {children}\n </span>\n )\n}\n\n/**\n * Wrapper that gives an AvatarBadge something to anchor to.\n *\n * Avatar clips its own overflow, so the badge cannot live inside it.\n */\nfunction AvatarWithBadge({ className, ...props }: ComponentProps<'span'>) {\n return (\n <span\n data-slot=\"avatar-with-badge\"\n className={cn('relative inline-flex shrink-0', className)}\n {...props}\n />\n )\n}\n\nexport { Avatar, AvatarBadge, AvatarGroup, AvatarWithBadge, avatarVariants }\nexport type { AvatarProps }\n"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "badge",
3
+ "type": "registry:ui",
4
+ "title": "Badge",
5
+ "description": "A short status or category marker. Unlike a field, a badge exists to be noticed, so it keeps the full colour system and the tint escape hatch.",
6
+ "category": "Display",
7
+ "dependencies": [
8
+ "class-variance-authority@^0.7.1"
9
+ ],
10
+ "registryDependencies": [
11
+ "lib-styles",
12
+ "lib-utils",
13
+ "primitive-slot"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/ui/badge.tsx",
18
+ "type": "registry:ui",
19
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { Slot } from '@/components/primitives/slot'\nimport {\n badgeSize,\n colorSet,\n iconChild,\n tintStyle,\n type ColorSet,\n} from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A short status or category marker.\n *\n * Unlike a field, a badge exists to be noticed, so it keeps the full colour\n * system — the same `--ui-*` sets and `tint` escape hatch a Button uses.\n *\n * `secondary` is the default because it is the right answer nearly every time:\n * tinted enough to read as a badge, quiet enough to sit in a table row or a\n * heading without shouting. Reach for `default` only when a badge has to be the\n * loudest thing on screen, and for `outline` only on an already-tinted surface\n * where a second fill would compete. Meaning belongs in `color`, not `variant`.\n */\nconst badgeVariants = cva(\n [\n 'inline-flex w-fit shrink-0 items-center justify-center',\n 'font-medium whitespace-nowrap',\n 'transition-colors duration-150 ease-out motion-reduce:transition-none',\n iconChild,\n ].join(' '),\n {\n variants: {\n variant: {\n /** Solid fill in the current colour set. */\n default: 'bg-[var(--ui)] text-[var(--ui-fg)]',\n /** Tinted fill, saturated text — the quiet default for most labels. */\n secondary: 'bg-[var(--ui-soft)] text-[var(--ui-soft-fg)]',\n outline:\n 'border border-[var(--ui-soft-fg)] text-[var(--ui-soft-fg)] bg-transparent',\n ghost: 'text-[var(--ui-soft-fg)] bg-transparent',\n },\n color: colorSet,\n size: {\n sm: badgeSize.sm,\n default: badgeSize.default,\n lg: badgeSize.lg,\n },\n shape: {\n pill: 'rounded-full [corner-shape:round]',\n rounded: 'rounded-[var(--radius-check-lg)]',\n },\n },\n defaultVariants: {\n variant: 'secondary',\n color: 'neutral',\n size: 'default',\n shape: 'pill',\n },\n },\n)\n\ntype BadgeProps = Omit<ComponentProps<'span'>, 'color'> &\n VariantProps<typeof badgeVariants> & {\n /** Any CSS colour, used instead of a named `color` set. */\n tint?: string\n /** Rendered inside the badge, before or after the text. */\n icon?: ReactNode\n iconPosition?: 'start' | 'end'\n /** Render the child element instead of a `<span>`, keeping the styles. */\n asChild?: boolean\n }\n\nfunction Badge({\n className,\n variant,\n color,\n size,\n shape,\n tint,\n icon,\n iconPosition = 'start',\n asChild = false,\n style,\n children,\n ...props\n}: BadgeProps) {\n const Comp = asChild ? Slot : 'span'\n\n // With `asChild` the caller owns the children entirely, so an icon prop would\n // have nowhere to go without cloning their element.\n const content =\n icon && !asChild ? (\n <>\n {iconPosition === 'start' && icon}\n {children}\n {iconPosition === 'end' && icon}\n </>\n ) : (\n children\n )\n\n return (\n <Comp\n data-slot=\"badge\"\n className={cn(badgeVariants({ variant, color, size, shape }), className)}\n style={tint ? { ...tintStyle(tint), ...style } : style}\n {...props}\n >\n {content}\n </Comp>\n )\n}\n\nexport { Badge, badgeVariants }\nexport type { BadgeProps, ColorSet }\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "banner",
3
+ "type": "registry:ui",
4
+ "title": "Banner",
5
+ "description": "A page-level announcement strip. Distinct from Alert, which belongs to the content around it — a banner spans the page and belongs to the session.",
6
+ "category": "Feedback",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "button",
12
+ "lib-styles",
13
+ "lib-utils"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/ui/banner.tsx",
18
+ "type": "registry:ui",
19
+ "content": "import { useState, type ComponentProps, type ReactNode } from 'react'\nimport { Info, TriangleAlert, X, CircleCheck, CircleAlert } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { focusRing, radius } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A page-level announcement strip: maintenance windows, trial expiry, an\n * outage notice.\n *\n * Distinct from `Alert`, which sits inside a layout and belongs to the content\n * around it. A banner spans the page and belongs to the session, so it is full\n * bleed, dismissible, and does not steal focus.\n *\n * `role=\"status\"` rather than `role=\"alert\"`. An assertive alert interrupts\n * whatever a screen reader is saying, which is right for a form error and\n * wrong for \"scheduled maintenance on Sunday\".\n */\nconst TONE = {\n info: {\n Icon: Info,\n surface: 'bg-[color-mix(in_oklab,var(--blue),transparent_88%)] text-foreground',\n icon: 'text-[var(--blue-soft-foreground)]',\n },\n success: {\n Icon: CircleCheck,\n surface: 'bg-[color-mix(in_oklab,var(--green),transparent_88%)] text-foreground',\n icon: 'text-[var(--green-soft-foreground)]',\n },\n warning: {\n Icon: TriangleAlert,\n surface: 'bg-[color-mix(in_oklab,var(--amber),transparent_88%)] text-foreground',\n icon: 'text-[var(--amber-soft-foreground)]',\n },\n danger: {\n Icon: CircleAlert,\n surface: 'bg-[color-mix(in_oklab,var(--destructive),transparent_88%)] text-foreground',\n icon: 'text-[var(--destructive-soft-foreground)]',\n },\n neutral: {\n Icon: Info,\n surface: 'bg-secondary text-foreground',\n icon: 'text-muted-foreground',\n },\n} as const\n\nfunction Banner({\n tone = 'info',\n title,\n children,\n action,\n dismissible = false,\n onDismiss,\n icon,\n sticky = false,\n dismissLabel = 'Dismiss',\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'title'> & {\n tone?: keyof typeof TONE\n title?: ReactNode\n action?: ReactNode\n dismissible?: boolean\n onDismiss?: () => void\n icon?: ReactNode\n sticky?: boolean\n /** Accessible name for the dismiss button. */\n dismissLabel?: string\n}) {\n const [open, setOpen] = useState(true)\n if (!open) return null\n\n const { Icon, surface, icon: iconTone } = TONE[tone]\n\n return (\n <div\n role=\"status\"\n data-slot=\"banner\"\n data-tone={tone}\n className={cn(\n 'flex w-full items-center gap-3 p-4 text-sm',\n surface,\n sticky && 'sticky top-0 z-40',\n className,\n )}\n {...props}\n >\n <span className={cn('shrink-0', iconTone)} aria-hidden=\"true\">\n {icon ?? <Icon className=\"size-4\" />}\n </span>\n\n <div className=\"min-w-0 flex-1\">\n {title && <span className=\"font-medium\">{title} </span>}\n <span className=\"text-muted-foreground\">{children}</span>\n </div>\n\n {action && <div className=\"shrink-0\">{action}</div>}\n\n {dismissible && (\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={dismissLabel}\n className={cn('shrink-0', radius.xs, focusRing)}\n onClick={() => {\n setOpen(false)\n onDismiss?.()\n }}\n >\n <X />\n </Button>\n )}\n </div>\n )\n}\n\nexport { Banner }\n"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "benchmark-table",
3
+ "type": "registry:ui",
4
+ "title": "Benchmark Table",
5
+ "description": "Ranked results with variance. A difference inside the combined error bars is reported as \"within noise\" rather than as a win — printing \"1.03× faster\" from two noisy runs is how performance myths start.",
6
+ "category": "Build & Perf",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "badge",
10
+ "lib-styles",
11
+ "lib-utils"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/benchmark-table.tsx",
16
+ "type": "registry:ui",
17
+ "content": "import { useMemo, type ComponentProps, type ReactNode } from 'react'\nimport { Badge } from '@/components/ui/badge'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Benchmark results, ranked, with variance.\n *\n * Variance is shown alongside every result and a difference inside the combined\n * error bars is reported as inconclusive rather than as a winner. A benchmark\n * table that prints \"1.03× faster\" from two noisy runs is how performance myths\n * start, and refusing to call that a win is the entire value of this component.\n *\n * Relative speed is expressed against the fastest, and the direction is stated\n * in words. \"2.4×\" is ambiguous without knowing whether higher is better —\n * ops/sec and nanoseconds point opposite ways and both are normal here.\n *\n * The bar is logarithmic when the spread exceeds two orders of magnitude.\n * Linear bars turn a 1000× spread into one full bar and nine invisible ones.\n */\nexport type BenchmarkResult = {\n name: ReactNode\n /** The measurement. */\n value: number\n /** Standard deviation or margin of error, same unit as `value`. */\n error?: number\n samples?: number\n}\n\nfunction BenchmarkTable({\n results,\n unit = 'ops/s',\n higherIsBetter = true,\n baselineLabel = 'fastest',\n inconclusiveLabel = 'within noise',\n samplesLabel = 'n',\n className,\n ...props\n // `results` is a legacy HTML attribute on every element, so it has to be\n // omitted or TypeScript intersects it with `number`.\n}: Omit<ComponentProps<'div'>, 'children' | 'results'> & {\n results: BenchmarkResult[]\n unit?: string\n /** ops/sec: true. Nanoseconds per op: false. */\n higherIsBetter?: boolean\n baselineLabel?: ReactNode\n /** Marks a result whose error bars overlap the leader's. */\n inconclusiveLabel?: ReactNode\n samplesLabel?: ReactNode\n}) {\n const rows = useMemo(() => {\n const sorted = [...results].sort((a, b) =>\n higherIsBetter ? b.value - a.value : a.value - b.value,\n )\n const best = sorted[0]\n if (!best) return []\n\n return sorted.map((result) => {\n const ratio = higherIsBetter ? best.value / result.value : result.value / best.value\n // Overlapping error bars mean there is no measured difference.\n const spread = (best.error ?? 0) + (result.error ?? 0)\n const inconclusive =\n result !== best && Math.abs(result.value - best.value) <= spread\n return { result, ratio, inconclusive, isBest: result === best }\n })\n }, [results, higherIsBetter])\n\n const values = results.map((r) => r.value).filter((v) => v > 0)\n const spread = values.length ? Math.max(...values) / Math.min(...values) : 1\n // Linear bars turn a 1000× spread into one bar and nine slivers.\n const log = spread > 100\n\n const extent = (value: number) => {\n if (values.length === 0) return 0\n const max = Math.max(...values)\n const min = Math.min(...values)\n if (!log) return higherIsBetter ? value / max : min / value\n const scale = (v: number) => Math.log10(Math.max(v, 1e-9))\n const lo = scale(min), hi = scale(max)\n const t = hi === lo ? 1 : (scale(value) - lo) / (hi - lo)\n return higherIsBetter ? t : 1 - t\n }\n\n const format = (value: number) =>\n value >= 1000 ? value.toLocaleString(undefined, { maximumFractionDigits: 0 }) : value.toPrecision(3)\n\n return (\n <div\n data-slot=\"benchmark-table\"\n className={cn(surface, radius.surface, 'flex flex-col overflow-hidden', className)}\n {...props}\n >\n <ul className=\"divide-border/60 list-none divide-y\">\n {rows.map(({ result, ratio, inconclusive, isBest }, index) => (\n <li key={index} className=\"flex flex-wrap items-center gap-3 p-3\">\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium\">{result.name}</span>\n\n {isBest ? (\n <Badge size=\"sm\" color=\"green\">\n {baselineLabel}\n </Badge>\n ) : inconclusive ? (\n // Refusing to call this a win is the point of the component.\n <Badge size=\"sm\" color=\"neutral\">\n {inconclusiveLabel}\n </Badge>\n ) : (\n <span className=\"text-muted-foreground text-xs tabular-nums\">\n {ratio.toFixed(2)}× slower\n </span>\n )}\n\n <span className=\"hidden h-1.5 w-28 shrink-0 overflow-hidden rounded-full bg-[var(--secondary)] sm:block\">\n <span\n className=\"block h-full\"\n style={{\n width: `${Math.max(2, extent(result.value) * 100)}%`,\n background: isBest ? 'var(--green)' : inconclusive ? 'var(--muted-foreground)' : 'var(--blue)',\n }}\n />\n </span>\n\n <span className=\"w-32 shrink-0 text-end text-xs tabular-nums\">\n <span className=\"font-medium\">{format(result.value)}</span>\n {result.error !== undefined && (\n <span className=\"text-muted-foreground\"> ±{format(result.error)}</span>\n )}\n <span className=\"text-muted-foreground/60 ms-1\">{unit}</span>\n </span>\n\n {result.samples !== undefined && (\n <span className=\"text-muted-foreground/60 w-12 shrink-0 text-end text-xs tabular-nums\">\n {samplesLabel}={result.samples}\n </span>\n )}\n </li>\n ))}\n </ul>\n\n {log && (\n <p className=\"border-border text-muted-foreground border-t p-3 text-xs\">\n Bars are logarithmic — the spread exceeds two orders of magnitude.\n </p>\n )}\n </div>\n )\n}\n\nexport { BenchmarkTable }\n"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "bet-input",
3
+ "type": "registry:ui",
4
+ "title": "Bet Input",
5
+ "description": "A stake field with quick multipliers and a live return. Stakes are integer minor units and the max is clamped to the balance, so \"all in\" cannot stake money that is not there.",
6
+ "category": "Gaming",
7
+ "dependencies": [
8
+ "lucide-react@^1.39.0"
9
+ ],
10
+ "registryDependencies": [
11
+ "button",
12
+ "lib-styles",
13
+ "lib-utils",
14
+ "money-input"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "components/ui/bet-input.tsx",
19
+ "type": "registry:ui",
20
+ "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { TriangleAlert } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { MoneyInput } from '@/components/ui/money-input'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A stake field with quick multipliers and a payout preview.\n *\n * Potential payout is shown before the button, computed from the stake and the\n * odds. A stake box with no payout makes people work out the return in their\n * head at exactly the moment they are least inclined to.\n *\n * The balance ceiling is enforced on the field, not on submit. \"Insufficient\n * balance\" after a confirm dialog is a worse experience than a max that simply\n * cannot be exceeded, and half and double clamp rather than overshoot.\n *\n * All amounts are integer minor units, so repeated halving cannot accumulate a\n * float error into a stake that does not match the balance.\n */\nfunction BetInput({\n stake,\n onStakeChange,\n balance,\n odds,\n min,\n max,\n currency = 'USD',\n locale = 'en-GB',\n onPlace,\n action = 'Place bet',\n stakeLabel = 'Stake',\n maxLabel = 'Max',\n payoutLabel = 'Potential payout',\n overBalanceNote = 'Stake is above your balance.',\n disabled = false,\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'children' | 'onChange'> & {\n /** Minor units. */\n stake: number\n onStakeChange?: (stake: number) => void\n balance?: number\n /** Decimal odds — 2.5 returns 2.5× the stake. */\n odds?: number\n min?: number\n max?: number\n currency?: string\n locale?: string\n onPlace?: () => void\n action?: ReactNode\n /** Heading and accessible name for the stake field. */\n stakeLabel?: ReactNode\n /** The \"stake everything\" quick action. */\n maxLabel?: ReactNode\n payoutLabel?: ReactNode\n overBalanceNote?: ReactNode\n disabled?: boolean\n}) {\n const ceiling = Math.min(max ?? Infinity, balance ?? Infinity)\n const floor = min ?? 0\n\n const set = (next: number) =>\n onStakeChange?.(Math.max(floor, Math.min(Math.round(next), ceiling)))\n\n const overBalance = balance !== undefined && stake > balance\n const belowMin = min !== undefined && stake < min\n const payout = odds !== undefined ? Math.round(stake * odds) : undefined\n\n const money = (minor: number) =>\n new Intl.NumberFormat(locale, {\n style: 'currency',\n currency,\n minimumFractionDigits: 2,\n }).format(minor / 100)\n\n return (\n <div\n data-slot=\"bet-input\"\n className={cn(surface, radius.surface, 'flex flex-col gap-3 p-4', className)}\n {...props}\n >\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"text-sm font-medium\">{stakeLabel}</span>\n {balance !== undefined && (\n <span className=\"text-muted-foreground text-xs tabular-nums\">\n Balance {money(balance)}\n </span>\n )}\n </div>\n\n <MoneyInput\n value={stake}\n onValueChange={(next) => set(next ?? 0)}\n currency={currency}\n locale={locale}\n max={ceiling}\n min={floor}\n error={overBalance || belowMin}\n aria-label={typeof stakeLabel === 'string' ? stakeLabel : 'Stake'}\n />\n\n {/* Clamped, never overshooting the balance. */}\n <div className=\"flex flex-wrap gap-1.5\">\n <Button variant=\"secondary\" size=\"xs\" onClick={() => set(stake / 2)}>\n ½\n </Button>\n <Button variant=\"secondary\" size=\"xs\" onClick={() => set(stake * 2)}>\n 2×\n </Button>\n {balance !== undefined && (\n <Button variant=\"secondary\" size=\"xs\" onClick={() => set(balance)}>\n {maxLabel}\n </Button>\n )}\n </div>\n\n {payout !== undefined && (\n <dl className=\"flex items-baseline justify-between gap-2 text-sm\">\n <dt className=\"text-muted-foreground\">{payoutLabel}</dt>\n <dd className=\"font-semibold tabular-nums text-[var(--green-soft-foreground)]\">\n {money(payout)}\n </dd>\n </dl>\n )}\n\n {overBalance && (\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 {overBalanceNote}\n </p>\n )}\n {belowMin && !overBalance && (\n <p className=\"text-[var(--amber-soft-foreground)] text-xs\">\n Minimum stake is {money(min!)}.\n </p>\n )}\n\n <Button disabled={disabled || overBalance || belowMin || stake <= 0} onClick={onPlace}>\n {action}\n </Button>\n </div>\n )\n}\n\nexport { BetInput }\n"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "blame-view",
3
+ "type": "registry:ui",
4
+ "title": "Blame View",
5
+ "description": "Every line attributed to the commit that last touched it. Consecutive lines from one commit collapse into a single gutter entry — a blame view that repeats itself hides where one commit ends and the next begins.",
6
+ "category": "Development",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "avatar",
10
+ "fmt",
11
+ "lib-styles",
12
+ "lib-utils",
13
+ "tooltip"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/ui/blame-view.tsx",
18
+ "type": "registry:ui",
19
+ "content": "import { useMemo, type ComponentProps } from 'react'\nimport { Avatar } from '@/components/ui/avatar'\nimport { Fmt } from '@/components/ui/fmt'\nimport { Tooltip } from '@/components/ui/tooltip'\nimport { radius } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Git blame: every line attributed to the commit that last touched it.\n *\n * Consecutive lines from the same commit collapse into one gutter entry rather\n * than repeating the author on all forty. A blame view that repeats itself is\n * unreadable, and the repetition hides exactly the thing you are looking for —\n * where one commit's territory ends and the next begins.\n *\n * Age is shown as a colour ramp on the gutter edge, so \"what is old and stable\n * versus new and suspect\" is answerable without reading a single date.\n */\nexport type BlameLine = {\n line: number\n content: string\n sha: string\n author: string\n date: Date\n summary?: string\n}\n\nfunction BlameView({\n lines,\n now,\n onSelectCommit,\n className,\n ...props\n}: Omit<ComponentProps<'div'>, 'onSelect'> & {\n lines: BlameLine[]\n now?: Date\n onSelectCommit?: (sha: string) => void\n}) {\n const reference = now ?? new Date()\n\n // Group consecutive lines sharing a commit; the first of each run carries\n // the attribution and the rest are blank in the gutter.\n const runs = useMemo(() => {\n const out: { sha: string; lines: BlameLine[] }[] = []\n for (const line of lines) {\n const last = out.at(-1)\n if (last?.sha === line.sha) last.lines.push(line)\n else out.push({ sha: line.sha, lines: [line] })\n }\n return out\n }, [lines])\n\n const oldest = Math.min(...lines.map((line) => line.date.getTime()))\n const newest = Math.max(...lines.map((line) => line.date.getTime()))\n const span = newest - oldest || 1\n\n /** Newer is hotter. */\n const heat = (date: Date) => (date.getTime() - oldest) / span\n\n return (\n <div\n data-slot=\"blame-view\"\n className={cn(\n 'border-border overflow-x-auto border font-mono text-xs',\n radius.surface,\n className,\n )}\n {...props}\n >\n {runs.map((run) => {\n const first = run.lines[0]\n const intensity = heat(first.date)\n\n return (\n <div key={run.sha + first.line} className=\"flex min-w-max\">\n <div\n className=\"border-border/60 bg-muted/30 relative flex w-56 shrink-0 border-e\"\n style={{\n // The age ramp lives on the gutter's inner edge.\n boxShadow: `inset -2px 0 0 0 color-mix(in oklab, var(--blue), transparent ${Math.round(\n (1 - intensity) * 90,\n )}%)`,\n }}\n >\n <Tooltip\n content={\n <span className=\"flex flex-col gap-0.5\">\n <span className=\"font-medium\">{first.summary ?? first.sha}</span>\n <span className=\"opacity-70\">\n {first.author} · <Fmt type=\"relative\" value={first.date} now={reference} />\n </span>\n </span>\n }\n >\n <button\n type=\"button\"\n onClick={() => onSelectCommit?.(first.sha)}\n className=\"hover:bg-accent/50 flex min-w-0 flex-1 items-center gap-2 px-2 py-1 text-start transition-colors duration-150 ease-out motion-reduce:transition-none\"\n >\n <Avatar size=\"xs\" name={first.author} className=\"shrink-0\" />\n <span className=\"min-w-0 flex-1 truncate\">\n <span className=\"text-muted-foreground block truncate\">\n {first.summary ?? first.author}\n </span>\n </span>\n <span className=\"text-muted-foreground/60 shrink-0\">\n {first.sha.slice(0, 7)}\n </span>\n </button>\n </Tooltip>\n </div>\n\n <div className=\"min-w-0 flex-1\">\n {run.lines.map((line) => (\n <div key={line.line} className=\"flex\">\n <span className=\"text-muted-foreground/40 w-12 shrink-0 px-2 text-end select-none\">\n {line.line}\n </span>\n <code className=\"flex-1 pe-3 whitespace-pre\">{line.content}</code>\n </div>\n ))}\n </div>\n </div>\n )\n })}\n </div>\n )\n}\n\nexport { BlameView }\n"
20
+ }
21
+ ]
22
+ }