@nibapp/ui 0.0.0-stage → 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 (292) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +124 -2
  4. package/TRADEMARKS.md +22 -0
  5. package/dist/_bundled/money/index.d.ts +98 -0
  6. package/dist/_bundled/money/index.js +152 -0
  7. package/dist/_bundled/tokens/index.d.ts +2 -0
  8. package/dist/_bundled/tokens/index.js +2 -0
  9. package/dist/_bundled/tokens/palette.gen.d.ts +51 -0
  10. package/dist/_bundled/tokens/palette.gen.js +206 -0
  11. package/dist/_bundled/tokens/spec.d.ts +291 -0
  12. package/dist/_bundled/tokens/spec.js +221 -0
  13. package/dist/components/accordion.d.ts +27 -0
  14. package/dist/components/accordion.js +66 -0
  15. package/dist/components/alert-dialog.d.ts +59 -0
  16. package/dist/components/alert-dialog.js +82 -0
  17. package/dist/components/alert.d.ts +141 -0
  18. package/dist/components/alert.js +55 -0
  19. package/dist/components/appear.d.ts +21 -0
  20. package/dist/components/appear.js +42 -0
  21. package/dist/components/avatar.d.ts +196 -0
  22. package/dist/components/avatar.js +135 -0
  23. package/dist/components/badge.d.ts +183 -0
  24. package/dist/components/badge.js +68 -0
  25. package/dist/components/block-stack.d.ts +33 -0
  26. package/dist/components/block-stack.js +197 -0
  27. package/dist/components/button.d.ts +327 -0
  28. package/dist/components/button.js +101 -0
  29. package/dist/components/card.d.ts +131 -0
  30. package/dist/components/card.js +100 -0
  31. package/dist/components/checkbox.d.ts +34 -0
  32. package/dist/components/checkbox.js +37 -0
  33. package/dist/components/choice-row.d.ts +104 -0
  34. package/dist/components/choice-row.js +56 -0
  35. package/dist/components/clamped-text.d.ts +17 -0
  36. package/dist/components/clamped-text.js +47 -0
  37. package/dist/components/code-block.d.ts +48 -0
  38. package/dist/components/code-block.js +124 -0
  39. package/dist/components/code-input.d.ts +49 -0
  40. package/dist/components/code-input.js +59 -0
  41. package/dist/components/combobox.d.ts +194 -0
  42. package/dist/components/combobox.js +176 -0
  43. package/dist/components/command-palette.d.ts +57 -0
  44. package/dist/components/command-palette.js +83 -0
  45. package/dist/components/context-menu.d.ts +24 -0
  46. package/dist/components/context-menu.js +30 -0
  47. package/dist/components/copy-button.d.ts +9 -0
  48. package/dist/components/copy-button.js +20 -0
  49. package/dist/components/copy-command.d.ts +18 -0
  50. package/dist/components/copy-command.js +19 -0
  51. package/dist/components/data-table.d.ts +240 -0
  52. package/dist/components/data-table.js +63 -0
  53. package/dist/components/dialog.d.ts +80 -0
  54. package/dist/components/dialog.js +85 -0
  55. package/dist/components/diff-lines.d.ts +20 -0
  56. package/dist/components/diff-lines.js +10 -0
  57. package/dist/components/diff-view.d.ts +128 -0
  58. package/dist/components/diff-view.js +675 -0
  59. package/dist/components/disclosure.d.ts +33 -0
  60. package/dist/components/disclosure.js +51 -0
  61. package/dist/components/drawn-check.d.ts +8 -0
  62. package/dist/components/drawn-check.js +16 -0
  63. package/dist/components/edge-resizer.d.ts +26 -0
  64. package/dist/components/edge-resizer.js +53 -0
  65. package/dist/components/empty-state.d.ts +112 -0
  66. package/dist/components/empty-state.js +56 -0
  67. package/dist/components/expand-toggle.d.ts +19 -0
  68. package/dist/components/expand-toggle.js +10 -0
  69. package/dist/components/field.d.ts +48 -0
  70. package/dist/components/field.js +60 -0
  71. package/dist/components/filter-chip.d.ts +47 -0
  72. package/dist/components/filter-chip.js +33 -0
  73. package/dist/components/group.d.ts +10 -0
  74. package/dist/components/group.js +9 -0
  75. package/dist/components/icon-text.d.ts +11 -0
  76. package/dist/components/icon-text.js +10 -0
  77. package/dist/components/image-sources.d.ts +57 -0
  78. package/dist/components/image-sources.js +58 -0
  79. package/dist/components/image-thumbnail.d.ts +34 -0
  80. package/dist/components/image-thumbnail.js +58 -0
  81. package/dist/components/image-viewer.d.ts +60 -0
  82. package/dist/components/image-viewer.js +411 -0
  83. package/dist/components/inline-edit.d.ts +22 -0
  84. package/dist/components/inline-edit.js +51 -0
  85. package/dist/components/input.d.ts +155 -0
  86. package/dist/components/input.js +109 -0
  87. package/dist/components/item-label.d.ts +11 -0
  88. package/dist/components/item-label.js +5 -0
  89. package/dist/components/item-row.d.ts +23 -0
  90. package/dist/components/item-row.js +19 -0
  91. package/dist/components/kbd.d.ts +55 -0
  92. package/dist/components/kbd.js +86 -0
  93. package/dist/components/link-policy.d.ts +24 -0
  94. package/dist/components/link-policy.js +32 -0
  95. package/dist/components/linked-text.d.ts +15 -0
  96. package/dist/components/linked-text.js +48 -0
  97. package/dist/components/loading-region.d.ts +18 -0
  98. package/dist/components/loading-region.js +17 -0
  99. package/dist/components/markdown.d.ts +34 -0
  100. package/dist/components/markdown.js +307 -0
  101. package/dist/components/menu.d.ts +132 -0
  102. package/dist/components/menu.js +102 -0
  103. package/dist/components/message-bubble.d.ts +57 -0
  104. package/dist/components/message-bubble.js +28 -0
  105. package/dist/components/meta-line.d.ts +28 -0
  106. package/dist/components/meta-line.js +36 -0
  107. package/dist/components/money-input.d.ts +23 -0
  108. package/dist/components/money-input.js +34 -0
  109. package/dist/components/native-select.d.ts +120 -0
  110. package/dist/components/native-select.js +85 -0
  111. package/dist/components/page-trail.d.ts +30 -0
  112. package/dist/components/page-trail.js +43 -0
  113. package/dist/components/page.d.ts +80 -0
  114. package/dist/components/page.js +60 -0
  115. package/dist/components/path-text.d.ts +8 -0
  116. package/dist/components/path-text.js +11 -0
  117. package/dist/components/percent-input.d.ts +20 -0
  118. package/dist/components/percent-input.js +32 -0
  119. package/dist/components/popover.d.ts +52 -0
  120. package/dist/components/popover.js +36 -0
  121. package/dist/components/progress.d.ts +389 -0
  122. package/dist/components/progress.js +142 -0
  123. package/dist/components/radio.d.ts +65 -0
  124. package/dist/components/radio.js +54 -0
  125. package/dist/components/remote-image-gate.d.ts +34 -0
  126. package/dist/components/remote-image-gate.js +60 -0
  127. package/dist/components/resizable.d.ts +23 -0
  128. package/dist/components/resizable.js +30 -0
  129. package/dist/components/rolling-number.d.ts +13 -0
  130. package/dist/components/rolling-number.js +69 -0
  131. package/dist/components/scroll-area.d.ts +17 -0
  132. package/dist/components/scroll-area.js +25 -0
  133. package/dist/components/section-label.d.ts +10 -0
  134. package/dist/components/section-label.js +21 -0
  135. package/dist/components/section-nav.d.ts +22 -0
  136. package/dist/components/section-nav.js +80 -0
  137. package/dist/components/segmented-control.d.ts +125 -0
  138. package/dist/components/segmented-control.js +101 -0
  139. package/dist/components/select.d.ts +120 -0
  140. package/dist/components/select.js +89 -0
  141. package/dist/components/separator.d.ts +10 -0
  142. package/dist/components/separator.js +12 -0
  143. package/dist/components/sheet.d.ts +101 -0
  144. package/dist/components/sheet.js +129 -0
  145. package/dist/components/shortcut-recorder.d.ts +31 -0
  146. package/dist/components/shortcut-recorder.js +67 -0
  147. package/dist/components/skeleton.d.ts +31 -0
  148. package/dist/components/skeleton.js +33 -0
  149. package/dist/components/slider.d.ts +138 -0
  150. package/dist/components/slider.js +98 -0
  151. package/dist/components/spinner.d.ts +37 -0
  152. package/dist/components/spinner.js +16 -0
  153. package/dist/components/switch.d.ts +59 -0
  154. package/dist/components/switch.js +56 -0
  155. package/dist/components/tab-strip.d.ts +29 -0
  156. package/dist/components/tab-strip.js +42 -0
  157. package/dist/components/tabs.d.ts +13 -0
  158. package/dist/components/tabs.js +68 -0
  159. package/dist/components/text-link.d.ts +54 -0
  160. package/dist/components/text-link.js +38 -0
  161. package/dist/components/textarea.d.ts +75 -0
  162. package/dist/components/textarea.js +90 -0
  163. package/dist/components/toast.d.ts +74 -0
  164. package/dist/components/toast.js +128 -0
  165. package/dist/components/tooltip.d.ts +50 -0
  166. package/dist/components/tooltip.js +46 -0
  167. package/dist/components/truncated-text.d.ts +15 -0
  168. package/dist/components/truncated-text.js +19 -0
  169. package/dist/i18n/defaults.d.ts +7 -0
  170. package/dist/i18n/defaults.js +7 -0
  171. package/dist/i18n/format.d.ts +84 -0
  172. package/dist/i18n/format.js +169 -0
  173. package/dist/i18n/index.d.ts +6 -0
  174. package/dist/i18n/index.js +8 -0
  175. package/dist/i18n/locales/ru-formatters.d.ts +3 -0
  176. package/dist/i18n/locales/ru-formatters.js +22 -0
  177. package/dist/i18n/locales/ru.d.ts +5 -0
  178. package/dist/i18n/locales/ru.js +185 -0
  179. package/dist/i18n/plural.d.ts +20 -0
  180. package/dist/i18n/plural.js +26 -0
  181. package/dist/i18n/provider.d.ts +36 -0
  182. package/dist/i18n/provider.js +39 -0
  183. package/dist/i18n/rich.d.ts +14 -0
  184. package/dist/i18n/rich.js +44 -0
  185. package/dist/i18n/ru-plural.d.ts +3 -0
  186. package/dist/i18n/ru-plural.js +4 -0
  187. package/dist/i18n/strings/alert-dialog.d.ts +5 -0
  188. package/dist/i18n/strings/alert-dialog.js +5 -0
  189. package/dist/i18n/strings/block-stack.d.ts +17 -0
  190. package/dist/i18n/strings/block-stack.js +11 -0
  191. package/dist/i18n/strings/code-block.d.ts +18 -0
  192. package/dist/i18n/strings/code-block.js +13 -0
  193. package/dist/i18n/strings/combobox.d.ts +12 -0
  194. package/dist/i18n/strings/combobox.js +10 -0
  195. package/dist/i18n/strings/command-palette.d.ts +17 -0
  196. package/dist/i18n/strings/command-palette.js +11 -0
  197. package/dist/i18n/strings/diff-view.d.ts +92 -0
  198. package/dist/i18n/strings/diff-view.js +68 -0
  199. package/dist/i18n/strings/image-viewer.d.ts +43 -0
  200. package/dist/i18n/strings/image-viewer.js +32 -0
  201. package/dist/i18n/strings/index.d.ts +314 -0
  202. package/dist/i18n/strings/index.js +43 -0
  203. package/dist/i18n/strings/kbd.d.ts +42 -0
  204. package/dist/i18n/strings/kbd.js +39 -0
  205. package/dist/i18n/strings/markdown.d.ts +23 -0
  206. package/dist/i18n/strings/markdown.js +18 -0
  207. package/dist/i18n/strings/money.d.ts +7 -0
  208. package/dist/i18n/strings/money.js +5 -0
  209. package/dist/i18n/strings/primitives.d.ts +25 -0
  210. package/dist/i18n/strings/primitives.js +18 -0
  211. package/dist/i18n/strings/progress.d.ts +4 -0
  212. package/dist/i18n/strings/progress.js +4 -0
  213. package/dist/i18n/strings/shortcut-recorder.d.ts +15 -0
  214. package/dist/i18n/strings/shortcut-recorder.js +11 -0
  215. package/dist/i18n/strings/time.d.ts +41 -0
  216. package/dist/i18n/strings/time.js +20 -0
  217. package/dist/i18n/strings/toast.d.ts +6 -0
  218. package/dist/i18n/strings/toast.js +6 -0
  219. package/dist/i18n/time-zones.d.ts +20 -0
  220. package/dist/i18n/time-zones.js +64 -0
  221. package/dist/index.d.ts +84 -0
  222. package/dist/index.js +88 -0
  223. package/dist/lib/ansi.d.ts +55 -0
  224. package/dist/lib/ansi.js +438 -0
  225. package/dist/lib/clock.d.ts +32 -0
  226. package/dist/lib/clock.js +106 -0
  227. package/dist/lib/cn.d.ts +12 -0
  228. package/dist/lib/cn.js +28 -0
  229. package/dist/lib/control-props.d.ts +21 -0
  230. package/dist/lib/control-props.js +14 -0
  231. package/dist/lib/direction.d.ts +13 -0
  232. package/dist/lib/direction.js +25 -0
  233. package/dist/lib/image-size.d.ts +12 -0
  234. package/dist/lib/image-size.js +66 -0
  235. package/dist/lib/keycaps.d.ts +21 -0
  236. package/dist/lib/keycaps.js +62 -0
  237. package/dist/lib/keyed.d.ts +9 -0
  238. package/dist/lib/keyed.js +14 -0
  239. package/dist/lib/keys.d.ts +49 -0
  240. package/dist/lib/keys.js +159 -0
  241. package/dist/lib/kit-motion.d.ts +9 -0
  242. package/dist/lib/kit-motion.js +12 -0
  243. package/dist/lib/link-check-dialog.d.ts +11 -0
  244. package/dist/lib/link-check-dialog.js +18 -0
  245. package/dist/lib/motion-features.d.ts +1 -0
  246. package/dist/lib/motion-features.js +4 -0
  247. package/dist/lib/motion-provider.d.ts +5 -0
  248. package/dist/lib/motion-provider.js +8 -0
  249. package/dist/lib/motion.d.ts +39 -0
  250. package/dist/lib/motion.js +49 -0
  251. package/dist/lib/number-format.d.ts +114 -0
  252. package/dist/lib/number-format.js +313 -0
  253. package/dist/lib/patch.d.ts +72 -0
  254. package/dist/lib/patch.js +344 -0
  255. package/dist/lib/path.d.ts +34 -0
  256. package/dist/lib/path.js +85 -0
  257. package/dist/lib/search.d.ts +10 -0
  258. package/dist/lib/search.js +20 -0
  259. package/dist/lib/shiki-themes.d.ts +7 -0
  260. package/dist/lib/shiki-themes.js +115 -0
  261. package/dist/lib/shiki.d.ts +101 -0
  262. package/dist/lib/shiki.js +353 -0
  263. package/dist/lib/shortcut-context.d.ts +23 -0
  264. package/dist/lib/shortcut-context.js +29 -0
  265. package/dist/lib/text.d.ts +56 -0
  266. package/dist/lib/text.js +149 -0
  267. package/dist/lib/theme-scope.d.ts +18 -0
  268. package/dist/lib/theme-scope.js +24 -0
  269. package/dist/lib/theme.d.ts +40 -0
  270. package/dist/lib/theme.js +96 -0
  271. package/dist/lib/tree-keys.d.ts +72 -0
  272. package/dist/lib/tree-keys.js +126 -0
  273. package/dist/lib/url.d.ts +17 -0
  274. package/dist/lib/url.js +38 -0
  275. package/dist/lib/use-copy-to-clipboard.d.ts +13 -0
  276. package/dist/lib/use-copy-to-clipboard.js +40 -0
  277. package/dist/lib/use-delayed-loading.d.ts +11 -0
  278. package/dist/lib/use-delayed-loading.js +31 -0
  279. package/dist/lib/use-file-picker.d.ts +26 -0
  280. package/dist/lib/use-file-picker.js +43 -0
  281. package/dist/lib/use-hydrated.d.ts +6 -0
  282. package/dist/lib/use-hydrated.js +11 -0
  283. package/dist/lib/use-latest.d.ts +12 -0
  284. package/dist/lib/use-latest.js +22 -0
  285. package/dist/lib/use-resolved-theme.d.ts +12 -0
  286. package/dist/lib/use-resolved-theme.js +25 -0
  287. package/dist/styles/base.css +393 -0
  288. package/dist/styles/code.css +35 -0
  289. package/dist/styles/fonts.css +12 -0
  290. package/dist/styles/index.css +13 -0
  291. package/dist/styles/tokens.gen.css +494 -0
  292. package/package.json +208 -4
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Radio as BaseRadio } from "@base-ui/react/radio";
4
+ import { RadioGroup as BaseRadioGroup } from "@base-ui/react/radio-group";
5
+ import { cn, resolveClassName, tv } from "../lib/cn.js";
6
+ /** Single choice from a set of <Radio>/<RadioCard>. Arrow keys move the selection. */
7
+ export function RadioGroup({ orientation = "vertical", className, ...props }) {
8
+ return (_jsx(BaseRadioGroup, { "data-slot": "radio-group", className: (state) => cn("flex", orientation === "vertical" ? "flex-col gap-2.5" : "flex-row flex-wrap gap-x-5 gap-y-2.5", resolveClassName(className, state)), ...props }));
9
+ }
10
+ const radioStyles = tv({
11
+ base: [
12
+ "group/radio relative inline-flex shrink-0 items-center justify-center rounded-full outline-none touch-target",
13
+ "border border-border-strong bg-surface shadow-xs",
14
+ "transition-[border-color,box-shadow,scale] duration-(--duration-fast) ease-(--ease-standard)",
15
+ "hover:border-fg-subtle active:scale-90 motion-reduce:active:scale-100",
16
+ "focus-visible:ring-focus",
17
+ "data-checked:border-primary data-checked:hover:border-primary-hover",
18
+ "data-invalid:data-unchecked:border-danger-border",
19
+ "data-disabled:pointer-events-none data-disabled:opacity-45 data-readonly:pointer-events-none",
20
+ ],
21
+ variants: {
22
+ size: {
23
+ sm: "size-3.5",
24
+ md: "size-4",
25
+ },
26
+ },
27
+ defaultVariants: { size: "md" },
28
+ });
29
+ /** The ink dot; springs in from nothing when checked. */
30
+ const dotClass = cn("block rounded-full bg-primary", "transition-[scale,opacity] duration-(--duration-spring-snappy) ease-(--ease-spring-bouncy)", "data-unchecked:scale-0 data-unchecked:opacity-0 data-unchecked:duration-(--duration-fast) data-unchecked:ease-(--ease-in)", "motion-reduce:transition-none");
31
+ export function Radio({ size, label, description, className, ...props }) {
32
+ const control = (_jsx(BaseRadio.Root, { "data-slot": "radio", className: (state) => cn(radioStyles({ size }), (label !== undefined || description !== undefined) && "mt-[3px]", resolveClassName(className, state)), ...props, children: _jsx(BaseRadio.Indicator, { keepMounted: true, className: cn(dotClass, size === "sm" ? "size-1.5" : "size-2") }) }));
33
+ if (label === undefined && description === undefined)
34
+ return control;
35
+ return (_jsxs("label", { "data-slot": "radio-label", className: "inline-flex max-w-full cursor-default select-none items-start gap-2 has-data-disabled:opacity-45", children: [control, _jsxs("span", { className: "flex min-w-0 flex-col", children: [label !== undefined && _jsx("span", { className: "text-base text-fg", children: label }), description !== undefined && _jsx("span", { className: "text-sm text-fg-subtle", children: description })] })] }));
36
+ }
37
+ /** A whole card as one radio — for weighty choices like permission modes, or ones best shown (a theme). */
38
+ export function RadioCard({ label, description, icon, trailing, preview, previewSide = "top", className, ...props }) {
39
+ const content = (_jsxs(_Fragment, { children: [icon !== undefined && (_jsx("span", { "aria-hidden": true, className: "mt-0.5 flex size-4 shrink-0 items-center justify-center text-fg-subtle transition-colors group-data-checked/radio-card:text-fg [&_svg]:size-4", children: icon })), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("span", { className: "text-base font-medium text-pretty break-words text-fg", children: label }), description !== undefined && (_jsx("span", { className: "text-sm text-pretty break-words text-fg-subtle", children: description }))] }), trailing, _jsx("span", { "aria-hidden": true, className: cn(radioStyles({ size: "md" }), "pointer-events-none mt-[3px] group-hover/radio-card:border-fg-subtle", "group-data-checked/radio-card:border-primary"), children: _jsx(BaseRadio.Indicator, { keepMounted: true, className: cn(dotClass, "size-2") }) })] }));
40
+ return (_jsx(BaseRadio.Root, { "data-slot": "radio-card", className: (state) => cn("group/radio-card relative flex w-full cursor-default select-none rounded-xl text-start outline-none", preview === undefined
41
+ ? "items-start gap-3 p-3"
42
+ : previewSide === "auto"
43
+ ? "@container/radio-card overflow-hidden"
44
+ : "flex-col overflow-hidden", "border border-border bg-surface shadow-xs", "transition-[border-color,background-color,box-shadow] duration-(--duration-fast) ease-(--ease-standard)", "hover:border-border-strong hover:bg-bg-subtle", "focus-visible:ring-focus", "data-checked:border-primary data-checked:bg-surface data-checked:[box-shadow:var(--shadow-sm),inset_0_0_0_0.5px_var(--primary)]", "data-disabled:pointer-events-none data-disabled:opacity-45 data-readonly:pointer-events-none", resolveClassName(className, state)), ...props, children: preview === undefined ? (content) : previewSide === "auto" ? (_jsxs("span", { className: "flex w-full flex-col @sm/radio-card:flex-row", children: [_jsx("span", { "aria-hidden": true, "data-slot": "radio-card-preview", className: "block border-b border-border-subtle @sm/radio-card:w-32 @sm/radio-card:shrink-0 @sm/radio-card:border-e @sm/radio-card:border-b-0", children: preview }), _jsx("span", { className: "flex min-w-0 flex-1 items-start gap-3 px-3 py-2.5", children: content })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, "data-slot": "radio-card-preview", className: "block border-b border-border-subtle", children: preview }), _jsx("span", { className: "flex items-start gap-3 px-3 py-2.5", children: content })] })) }));
45
+ }
46
+ /**
47
+ * One choice shown as what it looks like — a colour or a picture — in a `RadioGroup`: a real radio
48
+ * (arrow keys move the choice, one tab stop), the chosen one outlined. Name each by `label`.
49
+ */
50
+ export function RadioSwatch({ label, color, children, size = "md", className, ...props }) {
51
+ return (_jsxs(BaseRadio.Root, { "data-slot": "radio-swatch", "aria-label": label, className: (state) => cn("relative inline-flex shrink-0 items-center justify-center rounded-full touch-target", "transition-[scale,box-shadow] duration-(--duration-fast) ease-(--ease-standard)", "hover:scale-110 motion-reduce:hover:scale-100",
52
+ // The chosen one is ringed in ink; focus is the outline beyond it, so both show at once.
53
+ "data-checked:ring-2 data-checked:ring-primary", "outline-none focus-visible:ring-focus", "data-disabled:pointer-events-none data-disabled:opacity-45 data-readonly:pointer-events-none", resolveClassName(className, state)), ...props, children: [color !== undefined && (_jsx("span", { "aria-hidden": true, className: cn("block rounded-full inset-ring inset-ring-border", size === "sm" ? "size-4" : "size-5"), style: { background: color } })), children] }));
54
+ }
@@ -0,0 +1,34 @@
1
+ import { type ReactNode } from "react";
2
+ /**
3
+ * The host of a remote http(s) image, or undefined when it loads without leaving this page.
4
+ * `data:image/svg+xml` counts as local: safe only because it is rendered as `<img>` (no scripts, no
5
+ * external refs). Rendering SVG inline, via `<object>` or CSS, would need its own guard.
6
+ */
7
+ export declare function remoteImageHost(src: string, base?: string): string | undefined;
8
+ export interface RemoteImageGateProps {
9
+ /** The image's `src`. */
10
+ src: string;
11
+ /** The image's alt text; names the placeholder when present. */
12
+ alt: string;
13
+ /** Remote hosts trusted to load on render. */
14
+ allowHosts?: ReadonlySet<string> | undefined;
15
+ /** The <img> to render once allowed. */
16
+ children: ReactNode;
17
+ }
18
+ /**
19
+ * A remote image waits for the reader: untrusted content (generated or pasted markdown,
20
+ * a url-kind image part) must not fetch — and leak — the moment it renders, on any surface, CSP or
21
+ * not. Same-origin, relative, data: and blob: images render at once; a cross-origin http(s) one shows
22
+ * what it is and where from, and "Load image" fetches it on purpose.
23
+ */
24
+ export declare function RemoteImageGate({ src, alt, allowHosts, children }: RemoteImageGateProps): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null | undefined;
25
+ /**
26
+ * A remote image where the page may not load one (a page whose CSP keeps other sites' images out,
27
+ * so a message can't fetch, and leak, anything by rendering): what it is and where it lives, and
28
+ * the link to open it in the browser.
29
+ */
30
+ export declare function RemoteImageCard({ src, alt, host }: {
31
+ src: string;
32
+ alt: string;
33
+ host: string;
34
+ }): import("react").JSX.Element;
@@ -0,0 +1,60 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ImageOff } from "lucide-react";
4
+ import { useState } from "react";
5
+ import { useUiStrings } from "../i18n/provider.js";
6
+ import { cn } from "../lib/cn.js";
7
+ import { cardStyles } from "./card.js";
8
+ import { ExternalLinkCheck, useLinkPolicy } from "./link-policy.js";
9
+ import { TextLink } from "./text-link.js";
10
+ /**
11
+ * The host of a remote http(s) image, or undefined when it loads without leaving this page.
12
+ * `data:image/svg+xml` counts as local: safe only because it is rendered as `<img>` (no scripts, no
13
+ * external refs). Rendering SVG inline, via `<object>` or CSS, would need its own guard.
14
+ */
15
+ export function remoteImageHost(src, base) {
16
+ if (/^(?:data:image\/|blob:)/iu.test(src))
17
+ return undefined;
18
+ let url;
19
+ try {
20
+ url = new URL(src, base ?? globalThis.location?.href ?? "about:blank");
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ if (url.protocol !== "http:" && url.protocol !== "https:")
26
+ return undefined;
27
+ const origin = globalThis.location?.origin;
28
+ return origin !== undefined && url.origin === origin ? undefined : url.host;
29
+ }
30
+ /**
31
+ * A remote image waits for the reader: untrusted content (generated or pasted markdown,
32
+ * a url-kind image part) must not fetch — and leak — the moment it renders, on any surface, CSP or
33
+ * not. Same-origin, relative, data: and blob: images render at once; a cross-origin http(s) one shows
34
+ * what it is and where from, and "Load image" fetches it on purpose.
35
+ */
36
+ export function RemoteImageGate({ src, alt, allowHosts, children }) {
37
+ const t = useUiStrings().markdown;
38
+ const [loaded, setLoaded] = useState(false);
39
+ const host = remoteImageHost(src);
40
+ if (host === undefined || loaded || allowHosts?.has(host) === true)
41
+ return children;
42
+ return (_jsxs("span", { "data-slot": "remote-image", className: cn(cardStyles({ variant: "inset", size: "flush" }), "inline-flex max-w-full flex-row items-center gap-2 px-3 py-2 align-middle text-sm"), children: [_jsx(ImageOff, { "aria-hidden": true, className: "size-4 shrink-0 text-fg-subtle" }), _jsx("span", { className: "min-w-0 truncate text-fg-muted", children: alt === "" ? t.remoteImage({ host }) : t.remoteImageNamed({ alt, host }) }), _jsx(TextLink, { tone: "plain", className: "shrink-0 px-1 font-medium text-fg", render: _jsx("button", { type: "button" }), onClick: () => setLoaded(true), children: t.loadImage })] }));
43
+ }
44
+ /**
45
+ * A remote image where the page may not load one (a page whose CSP keeps other sites' images out,
46
+ * so a message can't fetch, and leak, anything by rendering): what it is and where it lives, and
47
+ * the link to open it in the browser.
48
+ */
49
+ export function RemoteImageCard({ src, alt, host }) {
50
+ const t = useUiStrings().imageViewer;
51
+ // Under `check`, the image's whole address shows before it opens: its words are never the address.
52
+ const policy = useLinkPolicy();
53
+ const open = (onCheck) => (_jsx(TextLink, { tone: "plain", href: src, target: "_blank", rel: "noopener noreferrer", className: "shrink-0 px-1 font-medium text-fg", ...(onCheck !== undefined && {
54
+ onClick: (event) => {
55
+ event.preventDefault();
56
+ onCheck();
57
+ },
58
+ }), children: t.openInBrowser }));
59
+ return (_jsxs("span", { "data-slot": "remote-image", className: cn(cardStyles({ variant: "inset", size: "flush" }), "inline-flex max-w-full flex-row items-center gap-2 px-3 py-2 align-middle text-sm"), children: [_jsx(ImageOff, { "aria-hidden": true, className: "size-4 shrink-0 text-fg-subtle" }), alt === "" ? (_jsx("span", { className: "shrink-0 text-fg-muted", children: t.remote({ host }) })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 truncate text-fg-muted", children: alt }), _jsx("span", { className: "shrink-0 text-fg-muted", children: t.remoteHost({ host }) })] })), policy === "check" ? _jsx(ExternalLinkCheck, { href: src, trigger: open }) : open()] }));
60
+ }
@@ -0,0 +1,23 @@
1
+ import type { ComponentProps } from "react";
2
+ import { Group, Panel, Separator } from "react-resizable-panels";
3
+ export type { GroupImperativeHandle as ResizableGroupHandle, Layout as ResizableLayout, PanelImperativeHandle as ResizablePanelHandle, PanelSize as ResizablePanelSize, } from "react-resizable-panels";
4
+ export {
5
+ /** Persist a group's layout (e.g. to localStorage): spread the result onto ResizablePanelGroup. */
6
+ useDefaultLayout as useResizableLayout, useGroupRef as useResizableGroupRef, usePanelRef as useResizablePanelRef, } from "react-resizable-panels";
7
+ /** Lays out panels side by side (`horizontal`) or stacked (`vertical`). */
8
+ export declare function ResizablePanelGroup({ orientation, className, ...props }: ComponentProps<typeof Group>): import("react").JSX.Element;
9
+ /**
10
+ * One resizable region. Sizes: numbers are px, unitless strings are %, or use explicit units
11
+ * (`"240px"`, `"30%"`, `"16rem"`).
12
+ */
13
+ export declare function ResizablePanel({ className, ...props }: ComponentProps<typeof Panel>): import("react").JSX.Element;
14
+ export interface ResizableHandleProps extends ComponentProps<typeof Separator> {
15
+ /** Show the grip pill at rest, not only on hover/drag. */
16
+ withGrip?: boolean;
17
+ }
18
+ /**
19
+ * Hairline divider that thickens and reveals a grip on hover, focus and drag. Keyboard: focus and
20
+ * use arrow keys (Home/End jump to min/max). Double-click resets the neighbouring panel to its
21
+ * default size.
22
+ */
23
+ export declare function ResizableHandle({ withGrip, className, ...props }: ResizableHandleProps): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Group, Panel, Separator } from "react-resizable-panels";
4
+ import { cn } from "../lib/cn.js";
5
+ export {
6
+ /** Persist a group's layout (e.g. to localStorage): spread the result onto ResizablePanelGroup. */
7
+ useDefaultLayout as useResizableLayout, useGroupRef as useResizableGroupRef, usePanelRef as useResizablePanelRef, } from "react-resizable-panels";
8
+ /** Lays out panels side by side (`horizontal`) or stacked (`vertical`). */
9
+ export function ResizablePanelGroup({ orientation = "horizontal", className, ...props }) {
10
+ return (_jsx(Group, { "data-slot": "resizable-group", orientation: orientation, resizeTargetMinimumSize: { fine: 9, coarse: 28 }, className: cn("size-full", className), ...props }));
11
+ }
12
+ /**
13
+ * One resizable region. Sizes: numbers are px, unitless strings are %, or use explicit units
14
+ * (`"240px"`, `"30%"`, `"16rem"`).
15
+ */
16
+ export function ResizablePanel({ className, ...props }) {
17
+ return _jsx(Panel, { "data-slot": "resizable-panel", className: cn("min-h-0 min-w-0", className), ...props });
18
+ }
19
+ /**
20
+ * Hairline divider that thickens and reveals a grip on hover, focus and drag. Keyboard: focus and
21
+ * use arrow keys (Home/End jump to min/max). Double-click resets the neighbouring panel to its
22
+ * default size.
23
+ */
24
+ export function ResizableHandle({ withGrip = false, className, ...props }) {
25
+ return (_jsx(Separator, { "data-slot": "resizable-handle", className: cn("group/handle relative flex shrink-0 items-center justify-center bg-border-subtle outline-none",
26
+ // Hairline along the split axis. aria-orientation is the separator's own orientation.
27
+ "aria-[orientation=vertical]:w-px aria-[orientation=horizontal]:h-px",
28
+ // The visible bar: grows from the hairline, sits above neighbours while active.
29
+ "before:pointer-events-none before:absolute before:z-10 before:rounded-full before:bg-transparent before:content-['']", "before:transition-[background-color,scale] before:duration-(--duration-spring-snappy) before:ease-(--ease-spring-snappy)", "aria-[orientation=vertical]:before:inset-y-0 aria-[orientation=vertical]:before:start-[-1px] aria-[orientation=vertical]:before:w-[3px] aria-[orientation=vertical]:before:scale-x-[0.34]", "aria-[orientation=horizontal]:before:inset-x-0 aria-[orientation=horizontal]:before:top-[-1px] aria-[orientation=horizontal]:before:h-[3px] aria-[orientation=horizontal]:before:scale-y-[0.34]", "data-[separator=hover]:before:scale-100 data-[separator=hover]:before:bg-border-strong", "data-[separator=active]:before:scale-100 data-[separator=active]:before:bg-fg-subtle", "data-[separator=focus]:before:scale-100 data-[separator=focus]:before:bg-focus", "data-[separator=disabled]:opacity-50", "motion-reduce:before:transition-none", className), ...props, children: _jsx("span", { "aria-hidden": true, className: cn("pointer-events-none z-20 flex items-center justify-center rounded-full border border-border bg-surface-raised shadow-xs", "transition-[opacity,scale] duration-(--duration-spring-snappy) ease-(--ease-spring-snappy) motion-reduce:transition-none", "group-aria-[orientation=vertical]/handle:h-6 group-aria-[orientation=vertical]/handle:w-1.5", "group-aria-[orientation=horizontal]/handle:h-1.5 group-aria-[orientation=horizontal]/handle:w-6", withGrip ? "opacity-100" : "scale-75 opacity-0", "group-data-[separator=hover]/handle:scale-100 group-data-[separator=hover]/handle:opacity-100", "group-data-[separator=active]/handle:scale-100 group-data-[separator=active]/handle:opacity-100", "group-data-[separator=focus]/handle:scale-100 group-data-[separator=focus]/handle:opacity-100") }) }));
30
+ }
@@ -0,0 +1,13 @@
1
+ import { type ComponentProps } from "react";
2
+ export interface RollingTextProps extends Omit<ComponentProps<"span">, "children"> {
3
+ /** The formatted value ("3", "$2.07", "4 need you"); only the digits that change roll. */
4
+ readonly value: string;
5
+ }
6
+ /**
7
+ * A count or an amount whose changed digits roll in place: down when they're new, never moving the
8
+ * text around them (tabular figures). The words around a number stay one run of text, so letters
9
+ * keep their shapes and the sentence keeps its order. A number reads left to right in every
10
+ * language and is isolated as such: in a right-to-left sentence "12" stays "12". Screen readers get
11
+ * the plain value; under Reduce Motion the digits cross-fade.
12
+ */
13
+ export declare function RollingText({ value, className, ...props }: RollingTextProps): import("react").JSX.Element;
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { AnimatePresence, useReducedMotion } from "motion/react";
4
+ import * as m from "motion/react-m";
5
+ import { useMemo } from "react";
6
+ import { cn } from "../lib/cn.js";
7
+ import { KitMotion } from "../lib/kit-motion.js";
8
+ import { spring } from "../lib/motion.js";
9
+ import { graphemesOf } from "../lib/text.js";
10
+ const DIGIT = /^\p{Nd}$/u;
11
+ /**
12
+ * One mark between two digits keeps them one number: a group or decimal mark ("1,234.5", "1 234,5",
13
+ * "١٬٢٣٤٫٥") or the punctuation of a time, a date or a range ("10:30", "2026-10-28", "3–5").
14
+ */
15
+ const MARK = /^[\p{P}\p{Zs}]$/u;
16
+ /** Written against the digits and part of the number: a sign, a currency symbol, a percent sign. */
17
+ const AFFIX = /^[\p{Sc}\p{Pd}%‰‱+−]$/u;
18
+ const isDigit = (grapheme) => grapheme !== undefined && DIGIT.test(grapheme);
19
+ const isAffix = (grapheme) => grapheme !== undefined && AFFIX.test(grapheme);
20
+ /**
21
+ * The value as numbers and the text between them, by grapheme, so a letter that joins its
22
+ * neighbours or a character outside the BMP stays whole. A number takes the marks between its
23
+ * digits and the sign or symbol written against it.
24
+ */
25
+ function runsOf(value) {
26
+ const graphemes = graphemesOf(value);
27
+ const runs = [];
28
+ let text = [];
29
+ let index = 0;
30
+ while (index < graphemes.length) {
31
+ const grapheme = graphemes[index] ?? "";
32
+ if (!isDigit(grapheme)) {
33
+ text.push(grapheme);
34
+ index += 1;
35
+ continue;
36
+ }
37
+ const number = [];
38
+ while (isAffix(text.at(-1)))
39
+ number.unshift(text.pop() ?? "");
40
+ if (text.length > 0)
41
+ runs.push({ number: false, graphemes: text });
42
+ text = [];
43
+ for (; index < graphemes.length; index += 1) {
44
+ const next = graphemes[index] ?? "";
45
+ const joins = MARK.test(next) && isDigit(graphemes[index + 1]);
46
+ if (!isDigit(next) && !joins)
47
+ break;
48
+ number.push(next);
49
+ }
50
+ for (; isAffix(graphemes[index]); index += 1)
51
+ number.push(graphemes[index] ?? "");
52
+ runs.push({ number: true, graphemes: number });
53
+ }
54
+ if (text.length > 0)
55
+ runs.push({ number: false, graphemes: text });
56
+ return runs;
57
+ }
58
+ /**
59
+ * A count or an amount whose changed digits roll in place: down when they're new, never moving the
60
+ * text around them (tabular figures). The words around a number stay one run of text, so letters
61
+ * keep their shapes and the sentence keeps its order. A number reads left to right in every
62
+ * language and is isolated as such: in a right-to-left sentence "12" stays "12". Screen readers get
63
+ * the plain value; under Reduce Motion the digits cross-fade.
64
+ */
65
+ export function RollingText({ value, className, ...props }) {
66
+ const reduced = useReducedMotion() === true;
67
+ const runs = useMemo(() => runsOf(value), [value]);
68
+ return (_jsxs("span", { "data-slot": "rolling-text", className: cn("whitespace-pre tabular", className), ...props, children: [_jsx("span", { className: "sr-only", children: value }), runs.map((run, at) => run.number ? (_jsx("span", { "aria-hidden": true, dir: "ltr", className: "inline-flex", children: run.graphemes.map((grapheme, i) => (_jsx("span", { className: "relative inline-grid overflow-hidden", children: _jsx(KitMotion, { children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(m.span, { initial: reduced ? { opacity: 0 } : { y: "-90%", opacity: 0 }, animate: { y: 0, opacity: 1, transition: spring.snappy }, exit: reduced ? { opacity: 0 } : { y: "90%", opacity: 0, transition: { duration: 0.12 } }, children: grapheme }, grapheme) }) }) }, i))) }, at)) : (_jsx("span", { "aria-hidden": true, children: run.graphemes.join("") }, at)))] }));
69
+ }
@@ -0,0 +1,17 @@
1
+ import { ScrollArea as BaseScrollArea } from "@base-ui/react/scroll-area";
2
+ import type { ComponentProps, ReactNode, Ref } from "react";
3
+ export declare function ScrollAreaViewport({ className, ...props }: ComponentProps<typeof BaseScrollArea.Viewport>): import("react").JSX.Element;
4
+ /** Thin overlay bar: invisible at rest, fades in while hovering or scrolling. */
5
+ export declare function ScrollAreaScrollbar({ orientation, className, ...props }: ComponentProps<typeof BaseScrollArea.Scrollbar>): import("react").JSX.Element;
6
+ export interface ScrollAreaProps extends Omit<ComponentProps<typeof BaseScrollArea.Root>, "children"> {
7
+ children?: ReactNode;
8
+ /** Which axes scroll. */
9
+ orientation?: "vertical" | "horizontal" | "both";
10
+ /** Soft mask at edges that have more content beyond them. */
11
+ fade?: boolean;
12
+ /** Classes for the scrolling viewport (e.g. padding). */
13
+ viewportClassName?: string;
14
+ viewportRef?: Ref<HTMLDivElement>;
15
+ }
16
+ /** Native scrolling with calm, overlay scrollbars. Give it a bounded height (or flex-1 min-h-0). */
17
+ export declare function ScrollArea({ orientation, fade, viewportClassName, viewportRef, className, children, ...props }: ScrollAreaProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ScrollArea as BaseScrollArea } from "@base-ui/react/scroll-area";
4
+ import { cn, withBase } from "../lib/cn.js";
5
+ /*
6
+ * Edge fades grow with the distance scrolled from each edge (Base UI exposes it as CSS vars), so
7
+ * content never looks clipped at rest and the fade never hides the first/last line.
8
+ */
9
+ const fadeY = "[mask-image:linear-gradient(to_bottom,transparent_0,black_min(12px,var(--scroll-area-overflow-y-start,0px)),black_calc(100%_-_min(12px,var(--scroll-area-overflow-y-end,0px))),transparent_100%)]";
10
+ const fadeX = "[mask-image:linear-gradient(to_right,transparent_0,black_min(12px,var(--scroll-area-overflow-x-start,0px)),black_calc(100%_-_min(12px,var(--scroll-area-overflow-x-end,0px))),transparent_100%)]";
11
+ export function ScrollAreaViewport({ className, ...props }) {
12
+ return (_jsx(BaseScrollArea.Viewport, { "data-slot": "scroll-area-viewport", className: withBase("size-full overscroll-contain rounded-[inherit] outline-none focus-visible:ring-focus focus-visible:outline-offset-[-2px]", className), ...props }));
13
+ }
14
+ /** Thin overlay bar: invisible at rest, fades in while hovering or scrolling. */
15
+ export function ScrollAreaScrollbar({ orientation = "vertical", className, ...props }) {
16
+ return (_jsx(BaseScrollArea.Scrollbar, { "data-slot": "scroll-area-scrollbar", orientation: orientation, className: withBase(cn("pointer-events-none z-10 flex touch-none select-none p-0.5 opacity-0", "transition-opacity delay-300 duration-(--duration-slow) ease-(--ease-out)", "data-hovering:pointer-events-auto data-hovering:opacity-100 data-hovering:delay-0 data-hovering:duration-(--duration-fast)", "data-scrolling:pointer-events-auto data-scrolling:opacity-100 data-scrolling:delay-0 data-scrolling:duration-0", "data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:h-2.5 data-[orientation=horizontal]:flex-col"), className), ...props, children: _jsx(BaseScrollArea.Thumb, { "data-slot": "scroll-area-thumb", className: cn("relative rounded-full bg-fg/20 transition-colors duration-(--duration-fast) hover:bg-fg/35 active:bg-fg/45", "data-[orientation=vertical]:w-full data-[orientation=horizontal]:h-full",
17
+ // Generous invisible hit area around the slim thumb.
18
+ "before:absolute before:-inset-1 before:content-['']") }) }));
19
+ }
20
+ /** Native scrolling with calm, overlay scrollbars. Give it a bounded height (or flex-1 min-h-0). */
21
+ export function ScrollArea({ orientation = "vertical", fade = false, viewportClassName, viewportRef, className, children, ...props }) {
22
+ const y = orientation !== "horizontal";
23
+ const x = orientation !== "vertical";
24
+ return (_jsxs(BaseScrollArea.Root, { "data-slot": "scroll-area", className: withBase("relative min-h-0 overflow-hidden", className), ...props, children: [_jsx(ScrollAreaViewport, { ...(viewportRef && { ref: viewportRef }), className: cn(fade && (y ? fadeY : fadeX), viewportClassName), children: children }), y && _jsx(ScrollAreaScrollbar, { orientation: "vertical" }), x && _jsx(ScrollAreaScrollbar, { orientation: "horizontal" }), x && y && _jsx(BaseScrollArea.Corner, {})] }));
25
+ }
@@ -0,0 +1,10 @@
1
+ import { useRender } from "@base-ui/react/use-render";
2
+ export interface SectionLabelProps extends useRender.ComponentProps<"span"> {
3
+ }
4
+ /**
5
+ * The small uppercase eyebrow over a group ("INCLUDED", "PERMISSIONS"): one size, weight, tracking
6
+ * and colour for the whole app. A `<span>` by default; `render={(props) => <h3 {...props} />}` when
7
+ * it heads a section (the function form: biome's useHeadingContent reads `<h3 />` as an empty
8
+ * heading), `render={<figcaption />}` and the like for other elements.
9
+ */
10
+ export declare function SectionLabel({ className, render, ref, ...props }: SectionLabelProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ import { cn } from "../lib/cn.js";
4
+ /**
5
+ * The small uppercase eyebrow over a group ("INCLUDED", "PERMISSIONS"): one size, weight, tracking
6
+ * and colour for the whole app. A `<span>` by default; `render={(props) => <h3 {...props} />}` when
7
+ * it heads a section (the function form: biome's useHeadingContent reads `<h3 />` as an empty
8
+ * heading), `render={<figcaption />}` and the like for other elements.
9
+ */
10
+ export function SectionLabel({ className, render, ref, ...props }) {
11
+ return useRender({
12
+ defaultTagName: "span",
13
+ render,
14
+ ref,
15
+ props: {
16
+ "data-slot": "section-label",
17
+ className: cn("text-xs font-medium tracking-wide text-fg-subtle uppercase", className),
18
+ ...props,
19
+ },
20
+ });
21
+ }
@@ -0,0 +1,22 @@
1
+ import { useRender } from "@base-ui/react/use-render";
2
+ import { type ComponentProps, type ReactNode } from "react";
3
+ /**
4
+ * A page's own sections (a settings page's): put it in the page's `<nav>`, beside whatever else the
5
+ * nav holds (a search field, a group's heading, a hint). Several lists may share one nav, one per group.
6
+ */
7
+ export declare function SectionNavList({ className, ...props }: ComponentProps<"ul">): import("react").JSX.Element;
8
+ export interface SectionNavItemProps extends useRender.ComponentProps<"button"> {
9
+ /** The section's glyph, sized and toned for you. */
10
+ readonly icon?: ReactNode;
11
+ /** The section on screen: it takes the selection and `aria-current="page"`. */
12
+ readonly active?: boolean | undefined;
13
+ /** At the row's end: a count or a status (`CountBadge`). */
14
+ readonly trailing?: ReactNode;
15
+ }
16
+ /**
17
+ * One section: a button by default; `render={<Link to="…" />}` when the section has an address of its
18
+ * own (copying the link works, and hovering can start its reads).
19
+ */
20
+ export declare function SectionNavItem({ icon, active, trailing, render, ref, className, children, ...props }: SectionNavItemProps): import("react").JSX.Element;
21
+ /** A hairline between groups of sections in one list: across the column, or upright between rows' items. */
22
+ export declare function SectionNavSeparator({ className, ...props }: Omit<ComponentProps<"li">, "children">): import("react").JSX.Element;
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRender } from "@base-ui/react/use-render";
4
+ import { useState } from "react";
5
+ import { cn, tv } from "../lib/cn.js";
6
+ /*
7
+ * The list takes its layout from its own width (`@3xs/section-nav`), not the page's: in a rail beside
8
+ * the page, narrower than 16rem, the sections stand in a column; across the top of a narrow page they
9
+ * wrap onto more rows, so none hides in an unmarked scroller. The page's breakpoint, where the nav
10
+ * moves beside it, can't drift from the list's.
11
+ */
12
+ const sectionNavItem = tv({
13
+ slots: {
14
+ // `min-w-0`: a flex item is otherwise as wide as its unbroken name, past the column.
15
+ item: "min-w-0 shrink-0 basis-full @3xs/section-nav:basis-auto",
16
+ control: [
17
+ // Touch screens: a 44 px row, drawn, not an invisible layer that would overlap the next one.
18
+ "flex h-8 w-full min-w-0 items-center gap-2 rounded-md px-2 text-start text-base whitespace-nowrap outline-none focus-visible:ring-focus pointer-coarse:h-11",
19
+ "transition-[color,background-color,opacity] duration-(--duration-fast) ease-(--ease-standard)",
20
+ ],
21
+ icon: "flex shrink-0 [&_svg]:size-4",
22
+ // A name longer than the rail ends in an ellipsis, never in the next column.
23
+ label: "min-w-0 truncate",
24
+ trailing: "ms-auto flex shrink-0 items-center",
25
+ },
26
+ variants: {
27
+ // The selection moves at once, never glides.
28
+ active: {
29
+ true: { control: "bg-selected text-fg", icon: "text-fg" },
30
+ false: { control: "text-fg-muted hover:bg-hover hover:text-fg", icon: "text-fg-subtle" },
31
+ },
32
+ },
33
+ defaultVariants: { active: false },
34
+ });
35
+ /**
36
+ * A page's own sections (a settings page's): put it in the page's `<nav>`, beside whatever else the
37
+ * nav holds (a search field, a group's heading, a hint). Several lists may share one nav, one per group.
38
+ */
39
+ export function SectionNavList({ className, ...props }) {
40
+ return (_jsx("ul", { "data-slot": "section-nav", className: cn("@container/section-nav flex flex-wrap gap-0.5", className), ...props }));
41
+ }
42
+ /**
43
+ * One section: a button by default; `render={<Link to="…" />}` when the section has an address of its
44
+ * own (copying the link works, and hovering can start its reads).
45
+ */
46
+ export function SectionNavItem({ icon, active = false, trailing, render, ref, className, children, ...props }) {
47
+ const styles = sectionNavItem({ active });
48
+ const control = useRender({
49
+ defaultTagName: "button",
50
+ render,
51
+ ref,
52
+ props: {
53
+ ...(render === undefined && { type: "button" }),
54
+ "data-slot": "section-nav-item",
55
+ "aria-current": active ? "page" : undefined,
56
+ className: styles.control({ className }),
57
+ children: (_jsxs(_Fragment, { children: [icon !== undefined && (_jsx("span", { "aria-hidden": true, className: styles.icon(), children: icon })), _jsx(TruncatedLabel, { className: styles.label(), children: children }), trailing !== undefined && _jsx("span", { className: styles.trailing(), children: trailing })] })),
58
+ ...props,
59
+ },
60
+ });
61
+ return _jsx("li", { className: styles.item(), children: control });
62
+ }
63
+ /**
64
+ * A label that may end in an ellipsis: while it's cut short, pointing at it shows the whole name (the
65
+ * native title, the way a macOS sidebar shows a clipped name); the row's accessible name is whole.
66
+ */
67
+ function TruncatedLabel({ className, children, }) {
68
+ const [whole, setWhole] = useState(undefined);
69
+ const measure = (element) => {
70
+ const clipped = element.scrollWidth > element.clientWidth;
71
+ setWhole(clipped ? (element.textContent ?? undefined) : undefined);
72
+ };
73
+ return (_jsx("span", { className: className, title: whole, onPointerEnter: (event) => measure(event.currentTarget), children: children }));
74
+ }
75
+ /** A hairline between groups of sections in one list: across the column, or upright between rows' items. */
76
+ export function SectionNavSeparator({ className, ...props }) {
77
+ return (_jsx("li", { "aria-hidden": true, "data-slot": "section-nav-separator", className: cn(
78
+ // Across the column it shrinks by its margins; upright it keeps its hairline.
79
+ "mx-1 my-2 h-px basis-full bg-border-subtle", "@3xs/section-nav:my-0 @3xs/section-nav:h-auto @3xs/section-nav:w-px @3xs/section-nav:shrink-0 @3xs/section-nav:basis-auto", className), ...props }));
80
+ }
@@ -0,0 +1,125 @@
1
+ import { Toggle as BaseToggle } from "@base-ui/react/toggle";
2
+ import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group";
3
+ import { type ComponentProps, type ReactNode } from "react";
4
+ import { type VariantProps } from "../lib/cn.js";
5
+ declare const segmentedControlStyles: import("tailwind-variants").TVReturnType<{
6
+ size: {
7
+ sm: {
8
+ root: string;
9
+ item: string;
10
+ pill: string;
11
+ };
12
+ md: {
13
+ root: string;
14
+ item: string;
15
+ pill: string;
16
+ };
17
+ };
18
+ /** Stretch to the container; segments share the width equally. */
19
+ block: {
20
+ true: {
21
+ root: string;
22
+ item: string;
23
+ text: string;
24
+ };
25
+ };
26
+ /** Icon-only segments: square buttons. */
27
+ iconOnly: {
28
+ true: {};
29
+ };
30
+ }, {
31
+ root: string[];
32
+ item: string[];
33
+ pill: string[];
34
+ text: string;
35
+ label: string[];
36
+ }, undefined, {
37
+ size: {
38
+ sm: {
39
+ root: string;
40
+ item: string;
41
+ pill: string;
42
+ };
43
+ md: {
44
+ root: string;
45
+ item: string;
46
+ pill: string;
47
+ };
48
+ };
49
+ /** Stretch to the container; segments share the width equally. */
50
+ block: {
51
+ true: {
52
+ root: string;
53
+ item: string;
54
+ text: string;
55
+ };
56
+ };
57
+ /** Icon-only segments: square buttons. */
58
+ iconOnly: {
59
+ true: {};
60
+ };
61
+ }, {
62
+ root: string[];
63
+ item: string[];
64
+ pill: string[];
65
+ text: string;
66
+ label: string[];
67
+ }, import("tailwind-variants").TVReturnTypeLike<{
68
+ size: {
69
+ sm: {
70
+ root: string;
71
+ item: string;
72
+ pill: string;
73
+ };
74
+ md: {
75
+ root: string;
76
+ item: string;
77
+ pill: string;
78
+ };
79
+ };
80
+ /** Stretch to the container; segments share the width equally. */
81
+ block: {
82
+ true: {
83
+ root: string;
84
+ item: string;
85
+ text: string;
86
+ };
87
+ };
88
+ /** Icon-only segments: square buttons. */
89
+ iconOnly: {
90
+ true: {};
91
+ };
92
+ }, {
93
+ root: string[];
94
+ item: string[];
95
+ pill: string[];
96
+ text: string;
97
+ label: string[];
98
+ }>>;
99
+ type SegmentedControlVariants = VariantProps<typeof segmentedControlStyles>;
100
+ export interface SegmentedControlProps extends Omit<ComponentProps<typeof BaseToggleGroup>, "value" | "defaultValue" | "onValueChange" | "multiple">, Omit<SegmentedControlVariants, "iconOnly"> {
101
+ /** The selected segment (controlled). A segmented control always has exactly one selection. */
102
+ value?: string;
103
+ defaultValue?: string;
104
+ onValueChange?: (value: string) => void;
105
+ /** Accessible name for the group, e.g. "Permission mode". */
106
+ "aria-label"?: string;
107
+ children: ReactNode;
108
+ }
109
+ /**
110
+ * Single-select segmented control. The selected segment takes the pill at once: nothing slides.
111
+ * Arrow keys move focus; Space/Enter select.
112
+ */
113
+ export declare function SegmentedControl({ value: valueProp, defaultValue, onValueChange, size, block, className, children, ...props }: SegmentedControlProps): import("react").JSX.Element;
114
+ export interface SegmentedControlItemProps extends Omit<ComponentProps<typeof BaseToggle>, "value" | "pressed" | "defaultPressed" | "onPressedChange"> {
115
+ value: string;
116
+ /** Leading icon. With no children the segment becomes a square icon button (needs `aria-label`). */
117
+ icon?: ReactNode;
118
+ /**
119
+ * Disables the segment and says why: the reason is its tooltip and its accessible description, and
120
+ * the segment stays in the arrow keys' path (`aria-disabled`) so a keyboard reaches it.
121
+ */
122
+ disabledReason?: string | undefined;
123
+ }
124
+ export declare function SegmentedControlItem({ value, icon, disabledReason, className, children, ...props }: SegmentedControlItemProps): import("react").JSX.Element;
125
+ export {};