@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,197 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { GripVertical } from "lucide-react";
4
+ import { Children, createContext, isValidElement, use, useCallback, useEffect, useId, useMemo, useRef, useState, } from "react";
5
+ import { useUiStrings } from "../i18n/provider.js";
6
+ import { cn } from "../lib/cn.js";
7
+ import { useLatest } from "../lib/use-latest.js";
8
+ import { DisclosureChevron } from "./disclosure.js";
9
+ const Context = createContext(null);
10
+ export function BlockStack({ layout, onLayoutChange, className, children, ...props }) {
11
+ const t = useUiStrings().blockStack;
12
+ const blocks = useMemo(() => {
13
+ const byId = new Map();
14
+ for (const child of Children.toArray(children)) {
15
+ if (isValidElement(child) && typeof child.props.id === "string")
16
+ byId.set(child.props.id, child);
17
+ }
18
+ const ordered = layout.order.flatMap((id) => {
19
+ const block = byId.get(id);
20
+ return block === undefined ? [] : [block];
21
+ });
22
+ for (const [id, block] of byId)
23
+ if (!layout.order.includes(id))
24
+ ordered.push(block);
25
+ return ordered;
26
+ }, [children, layout.order]);
27
+ const ids = useMemo(() => blocks.map((block) => block.props.id), [blocks]);
28
+ const titles = useMemo(() => new Map(blocks.map((block) => [block.props.id, block.props.title])), [blocks]);
29
+ const elements = useRef(new Map()).current;
30
+ const [drag, setDrag] = useState(undefined);
31
+ const [announcement, setAnnouncement] = useState("");
32
+ // The moves below are stable callbacks for the blocks; they read the committed props.
33
+ const latest = useLatest({ layout, ids, onLayoutChange });
34
+ const move = useCallback((id, to) => {
35
+ const { layout: current, ids: order, onLayoutChange: change } = latest.current;
36
+ const from = order.indexOf(id);
37
+ if (from === -1)
38
+ return;
39
+ const target = Math.max(0, Math.min(order.length - 1, to));
40
+ if (target === from)
41
+ return;
42
+ const next = order.filter((other) => other !== id);
43
+ next.splice(target, 0, id);
44
+ change({ ...current, order: next });
45
+ setAnnouncement(t.moved({ title: titles.get(id) ?? id, position: target + 1, count: order.length }));
46
+ }, [t, titles]);
47
+ const toggle = useCallback((id) => {
48
+ const { layout: current, ids: order, onLayoutChange: change } = latest.current;
49
+ const collapsed = current.collapsed.includes(id)
50
+ ? current.collapsed.filter((other) => other !== id)
51
+ : [...current.collapsed, id];
52
+ change({ order, collapsed });
53
+ }, []);
54
+ const startDrag = useCallback((id, event) => {
55
+ if (event.button !== 0)
56
+ return;
57
+ event.preventDefault();
58
+ const grip = event.currentTarget;
59
+ grip.setPointerCapture(event.pointerId);
60
+ const startY = event.clientY;
61
+ const from = latest.current.ids.indexOf(id);
62
+ const scroller = scrollParent(grip);
63
+ let state = { id, to: from, dy: 0 };
64
+ setDrag(state);
65
+ // Where the pointer would drop the block: before the first block whose middle is below it.
66
+ const landing = (y) => {
67
+ const order = latest.current.ids;
68
+ for (let index = 0; index < order.length; index++) {
69
+ const element = elements.get(order[index] ?? "");
70
+ if (element === undefined || order[index] === id)
71
+ continue;
72
+ const rect = element.getBoundingClientRect();
73
+ if (y < rect.top + rect.height / 2)
74
+ return index > from ? index - 1 : index;
75
+ }
76
+ return order.length - 1;
77
+ };
78
+ const onMove = (moved) => {
79
+ if (scroller !== undefined) {
80
+ const box = scroller.getBoundingClientRect();
81
+ if (moved.clientY < box.top + 32)
82
+ scroller.scrollBy(0, -12);
83
+ else if (moved.clientY > box.bottom - 32)
84
+ scroller.scrollBy(0, 12);
85
+ }
86
+ state = { id, to: landing(moved.clientY), dy: moved.clientY - startY };
87
+ setDrag(state);
88
+ };
89
+ const finish = (drop) => {
90
+ grip.removeEventListener("pointermove", onMove);
91
+ grip.removeEventListener("pointerup", onUp);
92
+ grip.removeEventListener("pointercancel", onCancel);
93
+ window.removeEventListener("keydown", onKey, true);
94
+ setDrag(undefined);
95
+ if (drop)
96
+ move(id, state.to);
97
+ };
98
+ const onUp = () => finish(true);
99
+ const onCancel = () => finish(false);
100
+ const onKey = (key) => {
101
+ if (key.key !== "Escape")
102
+ return;
103
+ key.preventDefault();
104
+ key.stopPropagation();
105
+ finish(false);
106
+ };
107
+ grip.addEventListener("pointermove", onMove);
108
+ grip.addEventListener("pointerup", onUp);
109
+ grip.addEventListener("pointercancel", onCancel);
110
+ window.addEventListener("keydown", onKey, true);
111
+ }, [elements, move]);
112
+ const register = useCallback((id, element) => {
113
+ if (element === null)
114
+ elements.delete(id);
115
+ else
116
+ elements.set(id, element);
117
+ }, [elements]);
118
+ const context = useMemo(() => ({
119
+ collapsed: (id) => layout.collapsed.includes(id),
120
+ toggle,
121
+ move,
122
+ position: (id) => ids.indexOf(id),
123
+ count: ids.length,
124
+ drag,
125
+ startDrag,
126
+ register,
127
+ }), [layout.collapsed, toggle, move, ids, drag, startDrag, register]);
128
+ return (_jsxs(Context, { value: context, children: [_jsx("div", { "data-slot": "block-stack", className: cn("flex flex-col gap-1", className), ...props, children: blocks }), _jsx("p", { "aria-live": "polite", className: "sr-only", children: announcement })] }));
129
+ }
130
+ /** One block of a `BlockStack`: the shared header (title and caret, meta, grip), then its body. */
131
+ export function Block({ id, title, meta, className, children, ...props }) {
132
+ const stack = use(Context);
133
+ if (stack === null)
134
+ throw new Error("<Block> must be inside <BlockStack>");
135
+ const t = useUiStrings().blockStack;
136
+ const headingId = useId();
137
+ const bodyId = useId();
138
+ const hintId = useId();
139
+ const collapsed = stack.collapsed(id);
140
+ const position = stack.position(id);
141
+ const dragging = stack.drag?.id === id;
142
+ // The drop line: above the block the dragged one would take the place of.
143
+ const drop = stack.drag !== undefined && !dragging && stack.drag.to !== stack.position(stack.drag.id)
144
+ ? stack.drag.to === position
145
+ ? stack.drag.to < stack.position(stack.drag.id)
146
+ ? "before"
147
+ : "after"
148
+ : undefined
149
+ : undefined;
150
+ const gripRef = useRef(null);
151
+ // A moved block's grip keeps the focus it had (moving a node in the DOM drops it).
152
+ const hadFocus = useRef(false);
153
+ const placedAt = useRef(position);
154
+ useEffect(() => {
155
+ if (placedAt.current === position)
156
+ return;
157
+ placedAt.current = position;
158
+ if (hadFocus.current) {
159
+ gripRef.current?.focus();
160
+ hadFocus.current = false;
161
+ }
162
+ }, [position]);
163
+ const onGripKey = (event) => {
164
+ const to = event.key === "ArrowUp"
165
+ ? position - 1
166
+ : event.key === "ArrowDown"
167
+ ? position + 1
168
+ : event.key === "Home"
169
+ ? 0
170
+ : event.key === "End"
171
+ ? stack.count - 1
172
+ : undefined;
173
+ if (to === undefined)
174
+ return;
175
+ event.preventDefault();
176
+ // Only a move that changes the position brings the focus back; one at either end moves nothing,
177
+ // and a stale flag would pull the focus to this grip when another block moves past it later.
178
+ if (to < 0 || to >= stack.count || to === position)
179
+ return;
180
+ hadFocus.current = true;
181
+ stack.move(id, to);
182
+ };
183
+ return (_jsxs("section", { ref: (element) => stack.register(id, element), "data-slot": "block", "data-collapsed": collapsed || undefined, "data-dragging": dragging || undefined, "data-drop": drop, "aria-labelledby": headingId, className: cn("group/block relative flex min-w-0 flex-col rounded-lg",
184
+ // The drop line sits in the gap above or below the block it would displace.
185
+ "data-drop:before:pointer-events-none data-drop:before:absolute data-drop:before:inset-x-0 data-drop:before:h-0.5 data-drop:before:rounded-full data-drop:before:bg-focus", "data-[drop=before]:before:-top-1 data-[drop=after]:before:-bottom-1",
186
+ // Lifted while dragged: a card with room around its content (content stays where it was).
187
+ "data-dragging:z-10 data-dragging:-mx-2 data-dragging:bg-surface data-dragging:px-2 data-dragging:shadow-lg", className), style: dragging && stack.drag !== undefined ? { transform: `translateY(${stack.drag.dy}px)` } : undefined, ...props, children: [_jsxs("div", { className: "flex min-h-8 min-w-0 items-center gap-2", children: [_jsx("h3", { id: headingId, className: "min-w-0", children: _jsxs("button", { type: "button", "aria-expanded": !collapsed, "aria-controls": collapsed ? undefined : bodyId, onClick: () => stack.toggle(id), className: "-ms-1 flex h-7 min-w-0 items-center gap-1 rounded-md px-1 text-sm font-medium text-fg outline-none transition-colors hover:bg-hover focus-visible:ring-focus", children: [_jsx("span", { className: "truncate", children: title }), _jsx(DisclosureChevron, { open: !collapsed, className: "text-fg-subtle" })] }) }), meta !== undefined && (_jsx("span", { className: "min-w-0 truncate text-xs text-fg-subtle tabular", children: meta })), _jsx("span", { className: "flex-1" }), _jsx("button", { ref: gripRef, type: "button", "aria-label": t.reorder({ title }), "aria-describedby": hintId, "aria-roledescription": t.grip, onPointerDown: (event) => stack.startDrag(id, event), onKeyDown: onGripKey, className: cn("-me-1 grid size-6 shrink-0 cursor-grab touch-none place-items-center rounded-sm text-fg-subtle outline-none", "opacity-0 transition-opacity duration-(--duration-fast) group-hover/block:opacity-100 focus-visible:opacity-100 focus-visible:ring-focus pointer-coarse:opacity-100", "hover:bg-hover hover:text-fg-muted active:cursor-grabbing group-data-dragging/block:opacity-100"), children: _jsx(GripVertical, { className: "size-3.5" }) }), _jsx("span", { id: hintId, className: "sr-only", children: t.reorderHint })] }), !collapsed && (_jsx("div", { id: bodyId, className: "flex min-w-0 flex-col gap-2 pb-3", children: children }))] }));
188
+ }
189
+ /** The nearest ancestor that scrolls vertically (to scroll while a block is dragged near an edge). */
190
+ function scrollParent(element) {
191
+ for (let node = element.parentElement; node !== null; node = node.parentElement) {
192
+ const { overflowY } = getComputedStyle(node);
193
+ if ((overflowY === "auto" || overflowY === "scroll") && node.scrollHeight > node.clientHeight)
194
+ return node;
195
+ }
196
+ return undefined;
197
+ }
@@ -0,0 +1,327 @@
1
+ import { Button as BaseButton } from "@base-ui/react/button";
2
+ import type { ComponentProps, ReactNode } from "react";
3
+ import { type VariantProps } from "../lib/cn.js";
4
+ export declare const buttonStyles: import("tailwind-variants").TVReturnType<{
5
+ variant: {
6
+ /** Neutral ink: the main call to action. Actions are ink, never the accent. */
7
+ primary: "bg-primary text-primary-fg shadow-sm hover:bg-primary-hover [box-shadow:var(--shadow-sm),inset_0_1px_0_0_oklch(100%_0_0/0.12)]";
8
+ /** Quiet surface with a hairline — the default for most actions. */
9
+ secondary: "border border-border bg-surface text-fg shadow-xs hover:border-border-strong hover:bg-bg-subtle data-popup-open:bg-bg-subtle";
10
+ /** No chrome until hovered — toolbars, inline actions. */
11
+ ghost: "text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
12
+ /**
13
+ * Words that are also an action, in the text's own weight and laid out from the start, so they
14
+ * line up with the text around them: `xs` in a line (a header's crumbs, a title that renames),
15
+ * `sm` a toolbar's picker, `md` a row in a list or a popover ("Open settings", a sidebar's link).
16
+ */
17
+ quiet: "justify-start font-normal text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
18
+ /** Destructive confirmation. */
19
+ danger: "bg-danger text-danger-on-solid shadow-sm hover:bg-danger-hover";
20
+ /** Destructive but low emphasis. */
21
+ "danger-ghost": "text-danger-fg hover:bg-danger-subtle active:bg-danger-subtle-hover";
22
+ /** Inline text action. */
23
+ link: "h-auto px-0 text-fg underline decoration-border-strong underline-offset-4 hover:decoration-fg active:scale-100";
24
+ };
25
+ size: {
26
+ xs: "h-6 rounded-sm px-2 text-xs [&_svg:not([class*='size-'])]:size-3.5";
27
+ sm: "h-7 rounded-md px-2.5 text-sm";
28
+ md: "h-8 rounded-lg px-3 text-base";
29
+ lg: "h-10 rounded-xl px-4 text-md";
30
+ };
31
+ block: {
32
+ true: "w-full";
33
+ };
34
+ /** The button may shrink below its label's width; the label then truncates with an ellipsis. */
35
+ truncate: {
36
+ true: "min-w-0 shrink";
37
+ };
38
+ /**
39
+ * The label wraps onto more lines and the button grows, its `leading` icon on the first line: for
40
+ * words that are an action and may run long in another language (a tip). One line keeps the size.
41
+ */
42
+ wrap: {
43
+ true: "h-auto min-w-0 shrink items-start whitespace-normal text-start";
44
+ };
45
+ }, undefined, string[], {
46
+ variant: {
47
+ /** Neutral ink: the main call to action. Actions are ink, never the accent. */
48
+ primary: "bg-primary text-primary-fg shadow-sm hover:bg-primary-hover [box-shadow:var(--shadow-sm),inset_0_1px_0_0_oklch(100%_0_0/0.12)]";
49
+ /** Quiet surface with a hairline — the default for most actions. */
50
+ secondary: "border border-border bg-surface text-fg shadow-xs hover:border-border-strong hover:bg-bg-subtle data-popup-open:bg-bg-subtle";
51
+ /** No chrome until hovered — toolbars, inline actions. */
52
+ ghost: "text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
53
+ /**
54
+ * Words that are also an action, in the text's own weight and laid out from the start, so they
55
+ * line up with the text around them: `xs` in a line (a header's crumbs, a title that renames),
56
+ * `sm` a toolbar's picker, `md` a row in a list or a popover ("Open settings", a sidebar's link).
57
+ */
58
+ quiet: "justify-start font-normal text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
59
+ /** Destructive confirmation. */
60
+ danger: "bg-danger text-danger-on-solid shadow-sm hover:bg-danger-hover";
61
+ /** Destructive but low emphasis. */
62
+ "danger-ghost": "text-danger-fg hover:bg-danger-subtle active:bg-danger-subtle-hover";
63
+ /** Inline text action. */
64
+ link: "h-auto px-0 text-fg underline decoration-border-strong underline-offset-4 hover:decoration-fg active:scale-100";
65
+ };
66
+ size: {
67
+ xs: "h-6 rounded-sm px-2 text-xs [&_svg:not([class*='size-'])]:size-3.5";
68
+ sm: "h-7 rounded-md px-2.5 text-sm";
69
+ md: "h-8 rounded-lg px-3 text-base";
70
+ lg: "h-10 rounded-xl px-4 text-md";
71
+ };
72
+ block: {
73
+ true: "w-full";
74
+ };
75
+ /** The button may shrink below its label's width; the label then truncates with an ellipsis. */
76
+ truncate: {
77
+ true: "min-w-0 shrink";
78
+ };
79
+ /**
80
+ * The label wraps onto more lines and the button grows, its `leading` icon on the first line: for
81
+ * words that are an action and may run long in another language (a tip). One line keeps the size.
82
+ */
83
+ wrap: {
84
+ true: "h-auto min-w-0 shrink items-start whitespace-normal text-start";
85
+ };
86
+ }, undefined, import("tailwind-variants").TVReturnTypeLike<{
87
+ variant: {
88
+ /** Neutral ink: the main call to action. Actions are ink, never the accent. */
89
+ primary: "bg-primary text-primary-fg shadow-sm hover:bg-primary-hover [box-shadow:var(--shadow-sm),inset_0_1px_0_0_oklch(100%_0_0/0.12)]";
90
+ /** Quiet surface with a hairline — the default for most actions. */
91
+ secondary: "border border-border bg-surface text-fg shadow-xs hover:border-border-strong hover:bg-bg-subtle data-popup-open:bg-bg-subtle";
92
+ /** No chrome until hovered — toolbars, inline actions. */
93
+ ghost: "text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
94
+ /**
95
+ * Words that are also an action, in the text's own weight and laid out from the start, so they
96
+ * line up with the text around them: `xs` in a line (a header's crumbs, a title that renames),
97
+ * `sm` a toolbar's picker, `md` a row in a list or a popover ("Open settings", a sidebar's link).
98
+ */
99
+ quiet: "justify-start font-normal text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
100
+ /** Destructive confirmation. */
101
+ danger: "bg-danger text-danger-on-solid shadow-sm hover:bg-danger-hover";
102
+ /** Destructive but low emphasis. */
103
+ "danger-ghost": "text-danger-fg hover:bg-danger-subtle active:bg-danger-subtle-hover";
104
+ /** Inline text action. */
105
+ link: "h-auto px-0 text-fg underline decoration-border-strong underline-offset-4 hover:decoration-fg active:scale-100";
106
+ };
107
+ size: {
108
+ xs: "h-6 rounded-sm px-2 text-xs [&_svg:not([class*='size-'])]:size-3.5";
109
+ sm: "h-7 rounded-md px-2.5 text-sm";
110
+ md: "h-8 rounded-lg px-3 text-base";
111
+ lg: "h-10 rounded-xl px-4 text-md";
112
+ };
113
+ block: {
114
+ true: "w-full";
115
+ };
116
+ /** The button may shrink below its label's width; the label then truncates with an ellipsis. */
117
+ truncate: {
118
+ true: "min-w-0 shrink";
119
+ };
120
+ /**
121
+ * The label wraps onto more lines and the button grows, its `leading` icon on the first line: for
122
+ * words that are an action and may run long in another language (a tip). One line keeps the size.
123
+ */
124
+ wrap: {
125
+ true: "h-auto min-w-0 shrink items-start whitespace-normal text-start";
126
+ };
127
+ }, undefined>>;
128
+ export type ButtonVariants = VariantProps<typeof buttonStyles>;
129
+ export interface ButtonProps extends ComponentProps<typeof BaseButton>, ButtonVariants {
130
+ /** Shows a spinner, keeps the button's width, and blocks interaction. */
131
+ loading?: boolean;
132
+ /** Icon rendered before the label. */
133
+ leading?: ReactNode;
134
+ /** Icon or hint (e.g. <Kbd>) rendered after the label. */
135
+ trailing?: ReactNode;
136
+ /**
137
+ * Disables the button and says why: the reason is its tooltip and its accessible description, and
138
+ * the button stays focusable (`aria-disabled`) so a keyboard reaches it.
139
+ */
140
+ disabledReason?: string | undefined;
141
+ }
142
+ export declare function Button({ variant, size, block, truncate, wrap, loading, leading, trailing, disabled, disabledReason, className, children, ...props }: ButtonProps): import("react").JSX.Element;
143
+ export declare const iconButtonStyles: import("tailwind-variants").TVReturnType<{
144
+ size: {
145
+ xs: "size-6 rounded-sm [&_svg:not([class*='size-'])]:size-3.5";
146
+ sm: "size-7 rounded-md [&_svg:not([class*='size-'])]:size-4";
147
+ md: "size-8 rounded-lg [&_svg:not([class*='size-'])]:size-4";
148
+ lg: "size-10 rounded-xl [&_svg:not([class*='size-'])]:size-5";
149
+ };
150
+ }, undefined, "px-0", {
151
+ variant: {
152
+ /** Neutral ink: the main call to action. Actions are ink, never the accent. */
153
+ primary: "bg-primary text-primary-fg shadow-sm hover:bg-primary-hover [box-shadow:var(--shadow-sm),inset_0_1px_0_0_oklch(100%_0_0/0.12)]";
154
+ /** Quiet surface with a hairline — the default for most actions. */
155
+ secondary: "border border-border bg-surface text-fg shadow-xs hover:border-border-strong hover:bg-bg-subtle data-popup-open:bg-bg-subtle";
156
+ /** No chrome until hovered — toolbars, inline actions. */
157
+ ghost: "text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
158
+ /**
159
+ * Words that are also an action, in the text's own weight and laid out from the start, so they
160
+ * line up with the text around them: `xs` in a line (a header's crumbs, a title that renames),
161
+ * `sm` a toolbar's picker, `md` a row in a list or a popover ("Open settings", a sidebar's link).
162
+ */
163
+ quiet: "justify-start font-normal text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
164
+ /** Destructive confirmation. */
165
+ danger: "bg-danger text-danger-on-solid shadow-sm hover:bg-danger-hover";
166
+ /** Destructive but low emphasis. */
167
+ "danger-ghost": "text-danger-fg hover:bg-danger-subtle active:bg-danger-subtle-hover";
168
+ /** Inline text action. */
169
+ link: "h-auto px-0 text-fg underline decoration-border-strong underline-offset-4 hover:decoration-fg active:scale-100";
170
+ };
171
+ size: {
172
+ xs: "h-6 rounded-sm px-2 text-xs [&_svg:not([class*='size-'])]:size-3.5";
173
+ sm: "h-7 rounded-md px-2.5 text-sm";
174
+ md: "h-8 rounded-lg px-3 text-base";
175
+ lg: "h-10 rounded-xl px-4 text-md";
176
+ };
177
+ block: {
178
+ true: "w-full";
179
+ };
180
+ /** The button may shrink below its label's width; the label then truncates with an ellipsis. */
181
+ truncate: {
182
+ true: "min-w-0 shrink";
183
+ };
184
+ /**
185
+ * The label wraps onto more lines and the button grows, its `leading` icon on the first line: for
186
+ * words that are an action and may run long in another language (a tip). One line keeps the size.
187
+ */
188
+ wrap: {
189
+ true: "h-auto min-w-0 shrink items-start whitespace-normal text-start";
190
+ };
191
+ }, undefined, import("tailwind-variants").TVReturnType<{
192
+ variant: {
193
+ /** Neutral ink: the main call to action. Actions are ink, never the accent. */
194
+ primary: "bg-primary text-primary-fg shadow-sm hover:bg-primary-hover [box-shadow:var(--shadow-sm),inset_0_1px_0_0_oklch(100%_0_0/0.12)]";
195
+ /** Quiet surface with a hairline — the default for most actions. */
196
+ secondary: "border border-border bg-surface text-fg shadow-xs hover:border-border-strong hover:bg-bg-subtle data-popup-open:bg-bg-subtle";
197
+ /** No chrome until hovered — toolbars, inline actions. */
198
+ ghost: "text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
199
+ /**
200
+ * Words that are also an action, in the text's own weight and laid out from the start, so they
201
+ * line up with the text around them: `xs` in a line (a header's crumbs, a title that renames),
202
+ * `sm` a toolbar's picker, `md` a row in a list or a popover ("Open settings", a sidebar's link).
203
+ */
204
+ quiet: "justify-start font-normal text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
205
+ /** Destructive confirmation. */
206
+ danger: "bg-danger text-danger-on-solid shadow-sm hover:bg-danger-hover";
207
+ /** Destructive but low emphasis. */
208
+ "danger-ghost": "text-danger-fg hover:bg-danger-subtle active:bg-danger-subtle-hover";
209
+ /** Inline text action. */
210
+ link: "h-auto px-0 text-fg underline decoration-border-strong underline-offset-4 hover:decoration-fg active:scale-100";
211
+ };
212
+ size: {
213
+ xs: "h-6 rounded-sm px-2 text-xs [&_svg:not([class*='size-'])]:size-3.5";
214
+ sm: "h-7 rounded-md px-2.5 text-sm";
215
+ md: "h-8 rounded-lg px-3 text-base";
216
+ lg: "h-10 rounded-xl px-4 text-md";
217
+ };
218
+ block: {
219
+ true: "w-full";
220
+ };
221
+ /** The button may shrink below its label's width; the label then truncates with an ellipsis. */
222
+ truncate: {
223
+ true: "min-w-0 shrink";
224
+ };
225
+ /**
226
+ * The label wraps onto more lines and the button grows, its `leading` icon on the first line: for
227
+ * words that are an action and may run long in another language (a tip). One line keeps the size.
228
+ */
229
+ wrap: {
230
+ true: "h-auto min-w-0 shrink items-start whitespace-normal text-start";
231
+ };
232
+ }, undefined, string[], {
233
+ variant: {
234
+ /** Neutral ink: the main call to action. Actions are ink, never the accent. */
235
+ primary: "bg-primary text-primary-fg shadow-sm hover:bg-primary-hover [box-shadow:var(--shadow-sm),inset_0_1px_0_0_oklch(100%_0_0/0.12)]";
236
+ /** Quiet surface with a hairline — the default for most actions. */
237
+ secondary: "border border-border bg-surface text-fg shadow-xs hover:border-border-strong hover:bg-bg-subtle data-popup-open:bg-bg-subtle";
238
+ /** No chrome until hovered — toolbars, inline actions. */
239
+ ghost: "text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
240
+ /**
241
+ * Words that are also an action, in the text's own weight and laid out from the start, so they
242
+ * line up with the text around them: `xs` in a line (a header's crumbs, a title that renames),
243
+ * `sm` a toolbar's picker, `md` a row in a list or a popover ("Open settings", a sidebar's link).
244
+ */
245
+ quiet: "justify-start font-normal text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
246
+ /** Destructive confirmation. */
247
+ danger: "bg-danger text-danger-on-solid shadow-sm hover:bg-danger-hover";
248
+ /** Destructive but low emphasis. */
249
+ "danger-ghost": "text-danger-fg hover:bg-danger-subtle active:bg-danger-subtle-hover";
250
+ /** Inline text action. */
251
+ link: "h-auto px-0 text-fg underline decoration-border-strong underline-offset-4 hover:decoration-fg active:scale-100";
252
+ };
253
+ size: {
254
+ xs: "h-6 rounded-sm px-2 text-xs [&_svg:not([class*='size-'])]:size-3.5";
255
+ sm: "h-7 rounded-md px-2.5 text-sm";
256
+ md: "h-8 rounded-lg px-3 text-base";
257
+ lg: "h-10 rounded-xl px-4 text-md";
258
+ };
259
+ block: {
260
+ true: "w-full";
261
+ };
262
+ /** The button may shrink below its label's width; the label then truncates with an ellipsis. */
263
+ truncate: {
264
+ true: "min-w-0 shrink";
265
+ };
266
+ /**
267
+ * The label wraps onto more lines and the button grows, its `leading` icon on the first line: for
268
+ * words that are an action and may run long in another language (a tip). One line keeps the size.
269
+ */
270
+ wrap: {
271
+ true: "h-auto min-w-0 shrink items-start whitespace-normal text-start";
272
+ };
273
+ }, undefined, import("tailwind-variants").TVReturnTypeLike<{
274
+ variant: {
275
+ /** Neutral ink: the main call to action. Actions are ink, never the accent. */
276
+ primary: "bg-primary text-primary-fg shadow-sm hover:bg-primary-hover [box-shadow:var(--shadow-sm),inset_0_1px_0_0_oklch(100%_0_0/0.12)]";
277
+ /** Quiet surface with a hairline — the default for most actions. */
278
+ secondary: "border border-border bg-surface text-fg shadow-xs hover:border-border-strong hover:bg-bg-subtle data-popup-open:bg-bg-subtle";
279
+ /** No chrome until hovered — toolbars, inline actions. */
280
+ ghost: "text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
281
+ /**
282
+ * Words that are also an action, in the text's own weight and laid out from the start, so they
283
+ * line up with the text around them: `xs` in a line (a header's crumbs, a title that renames),
284
+ * `sm` a toolbar's picker, `md` a row in a list or a popover ("Open settings", a sidebar's link).
285
+ */
286
+ quiet: "justify-start font-normal text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg";
287
+ /** Destructive confirmation. */
288
+ danger: "bg-danger text-danger-on-solid shadow-sm hover:bg-danger-hover";
289
+ /** Destructive but low emphasis. */
290
+ "danger-ghost": "text-danger-fg hover:bg-danger-subtle active:bg-danger-subtle-hover";
291
+ /** Inline text action. */
292
+ link: "h-auto px-0 text-fg underline decoration-border-strong underline-offset-4 hover:decoration-fg active:scale-100";
293
+ };
294
+ size: {
295
+ xs: "h-6 rounded-sm px-2 text-xs [&_svg:not([class*='size-'])]:size-3.5";
296
+ sm: "h-7 rounded-md px-2.5 text-sm";
297
+ md: "h-8 rounded-lg px-3 text-base";
298
+ lg: "h-10 rounded-xl px-4 text-md";
299
+ };
300
+ block: {
301
+ true: "w-full";
302
+ };
303
+ /** The button may shrink below its label's width; the label then truncates with an ellipsis. */
304
+ truncate: {
305
+ true: "min-w-0 shrink";
306
+ };
307
+ /**
308
+ * The label wraps onto more lines and the button grows, its `leading` icon on the first line: for
309
+ * words that are an action and may run long in another language (a tip). One line keeps the size.
310
+ */
311
+ wrap: {
312
+ true: "h-auto min-w-0 shrink items-start whitespace-normal text-start";
313
+ };
314
+ }, undefined>>>;
315
+ export interface IconButtonProps extends Omit<ComponentProps<typeof BaseButton>, "aria-label" | "children">, Omit<VariantProps<typeof iconButtonStyles>, "block" | "truncate" | "wrap"> {
316
+ /** Required: icon-only controls must be named for assistive tech. */
317
+ "aria-label": string;
318
+ children: ReactNode;
319
+ loading?: boolean;
320
+ /** Disables the button and says why, as Button's `disabledReason`. */
321
+ disabledReason?: string | undefined;
322
+ }
323
+ /**
324
+ * Square, icon-only button. Pair with <Tooltip> to surface the label visually (a `disabledReason`
325
+ * brings its own).
326
+ */
327
+ export declare function IconButton({ variant, size, loading, disabled, disabledReason, className, children, ...props }: IconButtonProps): import("react").JSX.Element;
@@ -0,0 +1,101 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button as BaseButton } from "@base-ui/react/button";
4
+ import { cn, resolveClassName, tv } from "../lib/cn.js";
5
+ import { useDelayedLoading } from "../lib/use-delayed-loading.js";
6
+ import { Spinner } from "./spinner.js";
7
+ import { DisabledReasonTooltip, disabledReasonClass } from "./tooltip.js";
8
+ export const buttonStyles = tv({
9
+ base: [
10
+ "relative inline-flex shrink-0 select-none items-center justify-center gap-1.5 whitespace-nowrap font-medium touch-target",
11
+ "outline-none control-motion",
12
+ "focus-visible:ring-focus active:scale-[0.97] motion-reduce:active:scale-100",
13
+ "data-disabled:pointer-events-none data-disabled:opacity-45",
14
+ "[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0",
15
+ ],
16
+ variants: {
17
+ variant: {
18
+ /** Neutral ink: the main call to action. Actions are ink, never the accent. */
19
+ primary: "bg-primary text-primary-fg shadow-sm hover:bg-primary-hover [box-shadow:var(--shadow-sm),inset_0_1px_0_0_oklch(100%_0_0/0.12)]",
20
+ /** Quiet surface with a hairline — the default for most actions. */
21
+ secondary: "border border-border bg-surface text-fg shadow-xs hover:border-border-strong hover:bg-bg-subtle data-popup-open:bg-bg-subtle",
22
+ /** No chrome until hovered — toolbars, inline actions. */
23
+ ghost: "text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg",
24
+ /**
25
+ * Words that are also an action, in the text's own weight and laid out from the start, so they
26
+ * line up with the text around them: `xs` in a line (a header's crumbs, a title that renames),
27
+ * `sm` a toolbar's picker, `md` a row in a list or a popover ("Open settings", a sidebar's link).
28
+ */
29
+ quiet: "justify-start font-normal text-fg-muted hover:bg-hover hover:text-fg active:bg-active data-popup-open:bg-hover data-popup-open:text-fg",
30
+ /** Destructive confirmation. */
31
+ danger: "bg-danger text-danger-on-solid shadow-sm hover:bg-danger-hover",
32
+ /** Destructive but low emphasis. */
33
+ "danger-ghost": "text-danger-fg hover:bg-danger-subtle active:bg-danger-subtle-hover",
34
+ /** Inline text action. */
35
+ link: "h-auto px-0 text-fg underline decoration-border-strong underline-offset-4 hover:decoration-fg active:scale-100",
36
+ },
37
+ size: {
38
+ xs: "h-6 rounded-sm px-2 text-xs [&_svg:not([class*='size-'])]:size-3.5",
39
+ sm: "h-7 rounded-md px-2.5 text-sm",
40
+ md: "h-8 rounded-lg px-3 text-base",
41
+ lg: "h-10 rounded-xl px-4 text-md",
42
+ },
43
+ block: { true: "w-full" },
44
+ /** The button may shrink below its label's width; the label then truncates with an ellipsis. */
45
+ truncate: { true: "min-w-0 shrink" },
46
+ /**
47
+ * The label wraps onto more lines and the button grows, its `leading` icon on the first line: for
48
+ * words that are an action and may run long in another language (a tip). One line keeps the size.
49
+ */
50
+ wrap: { true: "h-auto min-w-0 shrink items-start whitespace-normal text-start" },
51
+ },
52
+ compoundVariants: [
53
+ { variant: "link", class: "h-auto px-0" },
54
+ // Quiet words keep the dense text size and a tight inset at every size.
55
+ { variant: "quiet", size: "xs", class: "rounded-md px-1 text-sm" },
56
+ { variant: "quiet", size: "sm", class: "px-2" },
57
+ { variant: "quiet", size: "md", class: "px-2 text-sm" },
58
+ // Wrapping, the padding makes one line as tall as the size's height (its text's line height).
59
+ { wrap: true, size: "xs", class: "py-0.75" },
60
+ { wrap: true, size: "sm", class: "py-1" },
61
+ { wrap: true, size: "md", class: "py-1.25" },
62
+ { wrap: true, size: "lg", class: "py-2" },
63
+ { wrap: true, variant: "quiet", size: "xs", class: "py-0.5" },
64
+ { wrap: true, variant: "quiet", size: "md", class: "py-1.5" },
65
+ ],
66
+ defaultVariants: { variant: "secondary", size: "md" },
67
+ });
68
+ export function Button({ variant, size, block, truncate, wrap, loading = false, leading, trailing, disabled, disabledReason, className, children, ...props }) {
69
+ // Blocked and busy at once; the spinner only once the wait is long enough to notice (a quick
70
+ // save never flashes one).
71
+ const spinning = useDelayedLoading(loading);
72
+ const reasoned = disabledReason !== undefined;
73
+ const label = truncate === true ? (_jsx("span", { className: "min-w-0 truncate", children: children })) : wrap === true ? (_jsx("span", { className: "min-w-0", children: children })) : (children);
74
+ // Wrapping, the icon's box is the first line's height, so it stays beside the words' first line.
75
+ const lead = wrap === true && leading !== undefined ? (_jsx("span", { className: "flex h-lh shrink-0 items-center", children: leading })) : (leading);
76
+ const button = (_jsxs(BaseButton, { "data-slot": "button", disabled: disabled === true || loading || reasoned, focusableWhenDisabled: loading || reasoned, "aria-busy": loading || undefined, className: (state) => cn(buttonStyles({ variant, size, block, truncate, wrap }), reasoned && disabledReasonClass, resolveClassName(className, state)), ...props, children: [spinning ? (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-[inherit] opacity-0", children: [lead, label, trailing] })) : (_jsxs(_Fragment, { children: [lead, label, trailing] })), spinning && (_jsx("span", { className: "absolute inset-0 grid place-items-center", children: _jsx(Spinner, { size: size === "lg" ? "lg" : size === "xs" ? "xs" : "md" }) }))] }));
77
+ return reasoned ? _jsx(DisabledReasonTooltip, { reason: disabledReason, children: button }) : button;
78
+ }
79
+ export const iconButtonStyles = tv({
80
+ extend: buttonStyles,
81
+ base: "px-0",
82
+ variants: {
83
+ size: {
84
+ xs: "size-6 rounded-sm [&_svg:not([class*='size-'])]:size-3.5",
85
+ sm: "size-7 rounded-md [&_svg:not([class*='size-'])]:size-4",
86
+ md: "size-8 rounded-lg [&_svg:not([class*='size-'])]:size-4",
87
+ lg: "size-10 rounded-xl [&_svg:not([class*='size-'])]:size-5",
88
+ },
89
+ },
90
+ defaultVariants: { variant: "ghost", size: "md" },
91
+ });
92
+ /**
93
+ * Square, icon-only button. Pair with <Tooltip> to surface the label visually (a `disabledReason`
94
+ * brings its own).
95
+ */
96
+ export function IconButton({ variant, size, loading = false, disabled, disabledReason, className, children, ...props }) {
97
+ const spinning = useDelayedLoading(loading);
98
+ const reasoned = disabledReason !== undefined;
99
+ const button = (_jsx(BaseButton, { "data-slot": "icon-button", disabled: disabled === true || loading || reasoned, focusableWhenDisabled: loading || reasoned, "aria-busy": loading || undefined, className: (state) => cn(iconButtonStyles({ variant, size }), reasoned && disabledReasonClass, resolveClassName(className, state)), ...props, children: spinning ? _jsx(Spinner, { size: size === "lg" ? "lg" : "sm" }) : children }));
100
+ return reasoned ? _jsx(DisabledReasonTooltip, { reason: disabledReason, children: button }) : button;
101
+ }