@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,68 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Tabs as BaseTabs } from "@base-ui/react/tabs";
4
+ import { createContext, use } from "react";
5
+ import { cn, tv, withBase } from "../lib/cn.js";
6
+ const TabsVariantContext = createContext("underline");
7
+ const slide = "transition-[translate] duration-(--duration-spring-snappy) ease-(--ease-spring-snappy) motion-reduce:transition-none";
8
+ const tabsStyles = tv({
9
+ slots: {
10
+ list: "relative flex shrink-0 select-none",
11
+ tab: [
12
+ "relative z-[1] inline-flex shrink-0 cursor-default items-center justify-center gap-1.5 whitespace-nowrap font-medium outline-none",
13
+ "text-fg-muted transition-colors duration-(--duration-fast) ease-(--ease-standard)",
14
+ "hover:text-fg data-active:text-fg focus-visible:ring-focus",
15
+ "data-disabled:pointer-events-none data-disabled:opacity-45",
16
+ "[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
17
+ ],
18
+ indicator: ["pointer-events-none absolute", slide],
19
+ },
20
+ variants: {
21
+ variant: {
22
+ /** Ink line under the active tab — page-level navigation. */
23
+ underline: {
24
+ list: [
25
+ "gap-1 data-[orientation=horizontal]:border-b data-[orientation=horizontal]:border-border-subtle",
26
+ "data-[orientation=vertical]:flex-col data-[orientation=vertical]:border-s data-[orientation=vertical]:border-border-subtle",
27
+ ],
28
+ tab: "h-9 rounded-md px-2 text-base focus-visible:outline-offset-[-2px] data-[orientation=vertical]:h-8 data-[orientation=vertical]:justify-start",
29
+ indicator: [
30
+ "rounded-full bg-fg",
31
+ "data-[orientation=horizontal]:bottom-[-1px] data-[orientation=horizontal]:left-0 data-[orientation=horizontal]:h-0.5",
32
+ "data-[orientation=horizontal]:w-(--active-tab-width) data-[orientation=horizontal]:translate-x-(--active-tab-left)",
33
+ "data-[orientation=vertical]:top-0 data-[orientation=vertical]:start-[-1px] data-[orientation=vertical]:w-0.5",
34
+ "data-[orientation=vertical]:h-(--active-tab-height) data-[orientation=vertical]:translate-y-(--active-tab-top)",
35
+ ],
36
+ },
37
+ /** Sliding raised pill on a sunken track — segmented control. */
38
+ pill: {
39
+ list: "inline-flex w-fit items-center gap-0.5 rounded-lg bg-bg-muted p-0.5 data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch",
40
+ tab: "h-7 rounded-md px-3 text-sm focus-visible:outline-offset-0",
41
+ // Base UI measures the active tab from the list's left edge in either direction.
42
+ indicator: [
43
+ "top-0 left-0 rounded-md border border-border-subtle bg-surface shadow-xs",
44
+ "h-(--active-tab-height) w-(--active-tab-width) translate-x-(--active-tab-left) translate-y-(--active-tab-top)",
45
+ ],
46
+ },
47
+ },
48
+ },
49
+ defaultVariants: { variant: "underline" },
50
+ });
51
+ /** Root. `variant` styles every List/Tab inside it. */
52
+ export function Tabs({ variant = "underline", className, ...props }) {
53
+ return (_jsx(TabsVariantContext, { value: variant, children: _jsx(BaseTabs.Root, { "data-slot": "tabs", "data-variant": variant, className: withBase("flex flex-col gap-3 data-[orientation=vertical]:flex-row", className), ...props }) }));
54
+ }
55
+ /** The strip of tabs. Renders the sliding indicator automatically. */
56
+ export function TabsList({ className, children, ...props }) {
57
+ const variant = use(TabsVariantContext);
58
+ const styles = tabsStyles({ variant });
59
+ return (_jsxs(BaseTabs.List, { "data-slot": "tabs-list", className: withBase(styles.list(), className), ...props, children: [children, _jsx(BaseTabs.Indicator, { "data-slot": "tabs-indicator", className: styles.indicator() })] }));
60
+ }
61
+ export function TabsTab({ className, ...props }) {
62
+ const variant = use(TabsVariantContext);
63
+ return (_jsx(BaseTabs.Tab, { "data-slot": "tabs-tab", className: withBase(tabsStyles({ variant }).tab(), className), ...props }));
64
+ }
65
+ /** Content for one tab; fades and rises in when it becomes active. */
66
+ export function TabsPanel({ className, ...props }) {
67
+ return (_jsx(BaseTabs.Panel, { "data-slot": "tabs-panel", className: withBase(cn("min-w-0 flex-1 rounded-lg outline-none focus-visible:ring-focus", "transition-[opacity,translate] duration-(--duration-base) ease-(--ease-out)", "starting:translate-y-1 starting:opacity-0 motion-reduce:starting:translate-y-0"), className), ...props }));
68
+ }
@@ -0,0 +1,54 @@
1
+ import { useRender } from "@base-ui/react/use-render";
2
+ import { type VariantProps } from "../lib/cn.js";
3
+ declare const textLink: import("tailwind-variants").TVReturnType<{
4
+ tone: {
5
+ /** In running text: ink, always underlined. */
6
+ default: "text-fg underline decoration-border-strong hover:decoration-fg";
7
+ /** In captions and footers: muted, underlined on hover. */
8
+ quiet: "text-fg-muted hover:text-fg hover:underline";
9
+ /** A name or title that is also a link: its own colour, underlined on hover. */
10
+ plain: "hover:underline";
11
+ /**
12
+ * A header's navigation: muted, padded to a target, never underlined; ink on hover and for the
13
+ * current page (`aria-current="page"`).
14
+ */
15
+ nav: "rounded-md px-3 py-1.5 text-sm text-fg-muted hover:text-fg aria-[current=page]:text-fg";
16
+ };
17
+ }, undefined, string[], {
18
+ tone: {
19
+ /** In running text: ink, always underlined. */
20
+ default: "text-fg underline decoration-border-strong hover:decoration-fg";
21
+ /** In captions and footers: muted, underlined on hover. */
22
+ quiet: "text-fg-muted hover:text-fg hover:underline";
23
+ /** A name or title that is also a link: its own colour, underlined on hover. */
24
+ plain: "hover:underline";
25
+ /**
26
+ * A header's navigation: muted, padded to a target, never underlined; ink on hover and for the
27
+ * current page (`aria-current="page"`).
28
+ */
29
+ nav: "rounded-md px-3 py-1.5 text-sm text-fg-muted hover:text-fg aria-[current=page]:text-fg";
30
+ };
31
+ }, undefined, import("tailwind-variants").TVReturnTypeLike<{
32
+ tone: {
33
+ /** In running text: ink, always underlined. */
34
+ default: "text-fg underline decoration-border-strong hover:decoration-fg";
35
+ /** In captions and footers: muted, underlined on hover. */
36
+ quiet: "text-fg-muted hover:text-fg hover:underline";
37
+ /** A name or title that is also a link: its own colour, underlined on hover. */
38
+ plain: "hover:underline";
39
+ /**
40
+ * A header's navigation: muted, padded to a target, never underlined; ink on hover and for the
41
+ * current page (`aria-current="page"`).
42
+ */
43
+ nav: "rounded-md px-3 py-1.5 text-sm text-fg-muted hover:text-fg aria-[current=page]:text-fg";
44
+ };
45
+ }, undefined>>;
46
+ export interface TextLinkProps extends useRender.ComponentProps<"a">, VariantProps<typeof textLink> {
47
+ }
48
+ /**
49
+ * A link inside text: inline, wraps with the sentence, one underline and focus ring for the whole
50
+ * app. An `<a>` by default; `render={<Link to="…" />}` for a route, `render={<button type="button" />}`
51
+ * for an action.
52
+ */
53
+ export declare function TextLink({ tone, className, render, ref, ...props }: TextLinkProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
54
+ export {};
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ import { cn, tv } from "../lib/cn.js";
4
+ const textLink = tv({
5
+ base: [
6
+ "rounded-xs underline-offset-4 outline-none focus-visible:ring-focus",
7
+ "transition-[color,text-decoration-color] duration-(--duration-fast) ease-(--ease-standard)",
8
+ ],
9
+ variants: {
10
+ tone: {
11
+ /** In running text: ink, always underlined. */
12
+ default: "text-fg underline decoration-border-strong hover:decoration-fg",
13
+ /** In captions and footers: muted, underlined on hover. */
14
+ quiet: "text-fg-muted hover:text-fg hover:underline",
15
+ /** A name or title that is also a link: its own colour, underlined on hover. */
16
+ plain: "hover:underline",
17
+ /**
18
+ * A header's navigation: muted, padded to a target, never underlined; ink on hover and for the
19
+ * current page (`aria-current="page"`).
20
+ */
21
+ nav: "rounded-md px-3 py-1.5 text-sm text-fg-muted hover:text-fg aria-[current=page]:text-fg",
22
+ },
23
+ },
24
+ defaultVariants: { tone: "default" },
25
+ });
26
+ /**
27
+ * A link inside text: inline, wraps with the sentence, one underline and focus ring for the whole
28
+ * app. An `<a>` by default; `render={<Link to="…" />}` for a route, `render={<button type="button" />}`
29
+ * for an action.
30
+ */
31
+ export function TextLink({ tone, className, render, ref, ...props }) {
32
+ return useRender({
33
+ defaultTagName: "a",
34
+ render,
35
+ ref,
36
+ props: { "data-slot": "text-link", className: cn(textLink({ tone }), className), ...props },
37
+ });
38
+ }
@@ -0,0 +1,75 @@
1
+ import { type ComponentProps, type Ref } from "react";
2
+ import { type VariantProps } from "../lib/cn.js";
3
+ declare const textareaStyles: import("tailwind-variants").TVReturnType<{
4
+ variant: {
5
+ /** Framed field, same chrome as Input. */
6
+ default: string[];
7
+ /** No chrome: embed inside a custom container, such as a message field with its own toolbar. */
8
+ bare: "rounded-none border-0 p-0 shadow-none";
9
+ };
10
+ size: {
11
+ sm: "rounded-md px-2 py-[3px] text-sm";
12
+ md: "rounded-lg px-2.5 py-1 text-base";
13
+ lg: "rounded-xl px-3 py-[7px] text-md";
14
+ };
15
+ autoGrow: {
16
+ true: "field-sizing-content";
17
+ false: "resize-y";
18
+ };
19
+ }, undefined, string[], {
20
+ variant: {
21
+ /** Framed field, same chrome as Input. */
22
+ default: string[];
23
+ /** No chrome: embed inside a custom container, such as a message field with its own toolbar. */
24
+ bare: "rounded-none border-0 p-0 shadow-none";
25
+ };
26
+ size: {
27
+ sm: "rounded-md px-2 py-[3px] text-sm";
28
+ md: "rounded-lg px-2.5 py-1 text-base";
29
+ lg: "rounded-xl px-3 py-[7px] text-md";
30
+ };
31
+ autoGrow: {
32
+ true: "field-sizing-content";
33
+ false: "resize-y";
34
+ };
35
+ }, undefined, import("tailwind-variants").TVReturnTypeLike<{
36
+ variant: {
37
+ /** Framed field, same chrome as Input. */
38
+ default: string[];
39
+ /** No chrome: embed inside a custom container, such as a message field with its own toolbar. */
40
+ bare: "rounded-none border-0 p-0 shadow-none";
41
+ };
42
+ size: {
43
+ sm: "rounded-md px-2 py-[3px] text-sm";
44
+ md: "rounded-lg px-2.5 py-1 text-base";
45
+ lg: "rounded-xl px-3 py-[7px] text-md";
46
+ };
47
+ autoGrow: {
48
+ true: "field-sizing-content";
49
+ false: "resize-y";
50
+ };
51
+ }, undefined>>;
52
+ type TextareaVariants = VariantProps<typeof textareaStyles>;
53
+ export interface TextareaProps extends Omit<ComponentProps<"textarea">, "ref" | "rows" | "value" | "defaultValue">, Omit<TextareaVariants, "autoGrow"> {
54
+ ref?: Ref<HTMLTextAreaElement> | undefined;
55
+ value?: string;
56
+ defaultValue?: string;
57
+ /** Fires with the new text on every edit. */
58
+ onValueChange?: (value: string) => void;
59
+ /** Grow with content between `minRows` and `maxRows`. Default `true`. */
60
+ autoGrow?: boolean;
61
+ /** Minimum visible lines. Default 3 (1 for `bare`). */
62
+ minRows?: number;
63
+ /** Maximum visible lines before scrolling. Default 12. */
64
+ maxRows?: number;
65
+ /** Marks the field invalid outside of a <Field>. */
66
+ invalid?: boolean;
67
+ }
68
+ /**
69
+ * Multi-line text field. Auto-grows with CSS `field-sizing: content` and falls back to measuring
70
+ * `scrollHeight` where unsupported. Works inside <Field> for labels and validation. What a person
71
+ * writes takes its direction from its first letter (`dir="auto"`), so an English note reads left to
72
+ * right in a right-to-left interface; pass `dir` to fix it.
73
+ */
74
+ export declare function Textarea({ ref, variant, size, autoGrow, minRows, maxRows, invalid, value, defaultValue, onValueChange, disabled, id, name, autoFocus, className, style, ...rest }: TextareaProps): import("react").JSX.Element;
75
+ export {};
@@ -0,0 +1,90 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Field as BaseField } from "@base-ui/react/field";
4
+ import { useCallback, useLayoutEffect, useRef, } from "react";
5
+ import { cn, tv } from "../lib/cn.js";
6
+ import { givenControlProps } from "../lib/control-props.js";
7
+ import { fieldChrome } from "./input.js";
8
+ const textareaStyles = tv({
9
+ base: [
10
+ "block w-full min-w-0 resize-none bg-transparent text-fg outline-none placeholder:text-fg-subtle",
11
+ // iOS zooms into a field whose text is under 16 px; touch screens get 16 px.
12
+ "pointer-coarse:text-[1rem]",
13
+ "focus-visible:outline-none disabled:pointer-events-none disabled:opacity-45",
14
+ "overflow-y-auto overscroll-contain",
15
+ ],
16
+ variants: {
17
+ variant: {
18
+ /** Framed field, same chrome as Input. */
19
+ default: [
20
+ fieldChrome,
21
+ "focus:border-border-strong focus:ring-3",
22
+ "read-only:bg-bg-subtle read-only:shadow-none",
23
+ ],
24
+ /** No chrome: embed inside a custom container, such as a message field with its own toolbar. */
25
+ bare: "rounded-none border-0 p-0 shadow-none",
26
+ },
27
+ size: {
28
+ sm: "rounded-md px-2 py-[3px] text-sm",
29
+ md: "rounded-lg px-2.5 py-1 text-base",
30
+ lg: "rounded-xl px-3 py-[7px] text-md",
31
+ },
32
+ autoGrow: { true: "field-sizing-content", false: "resize-y" },
33
+ },
34
+ compoundVariants: [{ variant: "bare", class: "rounded-none p-0" }],
35
+ defaultVariants: { variant: "default", size: "md", autoGrow: true },
36
+ });
37
+ /** Vertical padding + borders per size, so `minRows`/`maxRows` map to exact pixel heights. */
38
+ const CHROME_PX = { sm: 8, md: 10, lg: 16 };
39
+ const supportsFieldSizing = () => typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("field-sizing", "content");
40
+ function assignRef(ref, value) {
41
+ if (typeof ref === "function")
42
+ ref(value);
43
+ else if (ref)
44
+ ref.current = value;
45
+ }
46
+ /**
47
+ * Multi-line text field. Auto-grows with CSS `field-sizing: content` and falls back to measuring
48
+ * `scrollHeight` where unsupported. Works inside <Field> for labels and validation. What a person
49
+ * writes takes its direction from its first letter (`dir="auto"`), so an English note reads left to
50
+ * right in a right-to-left interface; pass `dir` to fix it.
51
+ */
52
+ export function Textarea({ ref, variant = "default", size = "md", autoGrow = true, minRows, maxRows = 12, invalid, value, defaultValue, onValueChange, disabled, id, name, autoFocus, className, style, ...rest }) {
53
+ const innerRef = useRef(null);
54
+ const resolvedSize = size ?? "md";
55
+ const chrome = variant === "bare" ? 0 : CHROME_PX[resolvedSize];
56
+ const min = Math.max(1, minRows ?? (variant === "bare" ? 1 : 3));
57
+ const max = Math.max(min, maxRows);
58
+ const resize = useCallback(() => {
59
+ const el = innerRef.current;
60
+ if (!el || !autoGrow || supportsFieldSizing())
61
+ return;
62
+ el.style.height = "auto";
63
+ const borders = el.offsetHeight - el.clientHeight;
64
+ el.style.height = `${el.scrollHeight + borders}px`;
65
+ }, [autoGrow]);
66
+ // Fallback path only: re-measure when a controlled value changes from the outside.
67
+ useLayoutEffect(() => {
68
+ if (value !== undefined)
69
+ resize();
70
+ }, [value, resize]);
71
+ useLayoutEffect(() => {
72
+ resize();
73
+ }, [resize]);
74
+ const setRef = useCallback((node) => {
75
+ const el = node instanceof HTMLTextAreaElement ? node : null;
76
+ innerRef.current = el;
77
+ assignRef(ref, el);
78
+ }, [ref]);
79
+ const sizing = autoGrow
80
+ ? { minHeight: `calc(${min}lh + ${chrome}px)`, maxHeight: `calc(${max}lh + ${chrome}px)` }
81
+ : {};
82
+ return (_jsx(BaseField.Control, { ref: setRef, ...givenControlProps({ value, defaultValue, disabled, id, name, autoFocus }), onValueChange: (next) => {
83
+ onValueChange?.(next);
84
+ resize();
85
+ },
86
+ // Element form: Base UI merges its control props (and chains handlers) into the textarea.
87
+ // The callback form would round-trip `rest` through mergeProps' mapped type, which breaks
88
+ // consumers with exactOptionalPropertyTypes plus JSX augmentations (e.g. Next's `tw`).
89
+ render: _jsx("textarea", { dir: "auto", ...rest, "data-slot": "textarea", ...(invalid === true ? { "aria-invalid": true } : {}), rows: autoGrow ? 1 : min, style: { ...sizing, ...style }, className: cn(textareaStyles({ variant, size, autoGrow }), className) }) }));
90
+ }
@@ -0,0 +1,74 @@
1
+ import type { MouseEvent, ReactNode } from "react";
2
+ export type ToastKind = "default" | "success" | "error" | "warning" | "info" | "loading";
3
+ export type ToastId = string | number;
4
+ export interface ToastButton {
5
+ label: ReactNode;
6
+ onClick: (event: MouseEvent<HTMLButtonElement>) => void;
7
+ }
8
+ /** Options accepted by every `toast.*` call. */
9
+ export interface ToastOptions {
10
+ /** Re-using an id replaces that toast in place (and restarts its timer) instead of stacking. */
11
+ id?: ToastId | undefined;
12
+ description?: ReactNode;
13
+ /** Primary ink button. The toast closes after it runs unless the handler calls `preventDefault()`. */
14
+ action?: ToastButton | undefined;
15
+ /** Quiet secondary button; the toast closes after it runs. */
16
+ cancel?: ToastButton | undefined;
17
+ /** Milliseconds before it dismisses itself; `0` or `Infinity` keeps it until dismissed. */
18
+ duration?: number | undefined;
19
+ /** Announce assertively (interrupts the screen reader). Default: polite. */
20
+ important?: boolean | undefined;
21
+ }
22
+ type PromiseMessage<T> = ReactNode | ((value: T) => ReactNode);
23
+ export interface ToastPromiseMessages<T> {
24
+ loading: ReactNode;
25
+ success: PromiseMessage<T>;
26
+ error: PromiseMessage<unknown>;
27
+ }
28
+ /**
29
+ * `toast("Changes saved")`, `toast.success`, `.error`, `.warning`, `.info`, `.loading`,
30
+ * `.promise(fetchThing(), { loading, success, error })`, `toast.dismiss(id)`. Needs one mounted
31
+ * `<Toaster />`.
32
+ */
33
+ export declare const toast: ((title: ReactNode, options?: ToastOptions) => ToastId) & {
34
+ success: (title: ReactNode, options?: ToastOptions) => ToastId;
35
+ error: (title: ReactNode, options?: ToastOptions) => ToastId;
36
+ warning: (title: ReactNode, options?: ToastOptions) => ToastId;
37
+ info: (title: ReactNode, options?: ToastOptions) => ToastId;
38
+ loading: (title: ReactNode, options?: ToastOptions) => ToastId;
39
+ /**
40
+ * One toast that follows the promise: loading, then success or error. Fire and forget: the
41
+ * caller still owns `promise` and its rejection.
42
+ */
43
+ promise: <T>(promise: Promise<T>, messages: ToastPromiseMessages<T>, options?: ToastOptions) => void;
44
+ /** Dismisses one toast, or every toast without an id. */
45
+ dismiss: (id?: ToastId) => void;
46
+ };
47
+ /** Bottom corners only: a toast at the top covers pane headers and tab bars. */
48
+ export type ToastPosition = "bottom-end" | "bottom-start" | "bottom-center";
49
+ type Inset = number | string;
50
+ /** Insets from the window edges. Numbers are px. */
51
+ export type ToastOffset = Inset | {
52
+ readonly bottom?: Inset | undefined;
53
+ readonly end?: Inset | undefined;
54
+ };
55
+ export interface ToasterProps {
56
+ /** Default `bottom-end`. */
57
+ position?: ToastPosition | undefined;
58
+ /**
59
+ * Distance from the window edges (default 16 px). Same as setting `--toast-inset-bottom` /
60
+ * `--toast-inset-x` in CSS; raise the bottom one to clear a docked toolbar or status bar.
61
+ */
62
+ offset?: ToastOffset | undefined;
63
+ /** Default auto-dismiss in ms (default 4000); `toast(…, { duration })` overrides per toast. */
64
+ duration?: number | undefined;
65
+ className?: string | undefined;
66
+ }
67
+ /**
68
+ * Mount once near the app root. Bottom-end by default, raised surfaces, semantic icons, swipe
69
+ * or ✕ to dismiss, F6 to reach the stack from the keyboard; announced politely. The viewport sits on
70
+ * the `toast` layer, under `overlay` (dialogs, popovers), so a modal's backdrop covers the stack and
71
+ * its close button always stays clickable. Fire toasts with `toast(...)`.
72
+ */
73
+ export declare function Toaster({ position, offset, duration, className }: ToasterProps): import("react").JSX.Element;
74
+ export {};
@@ -0,0 +1,128 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Toast as BaseToast } from "@base-ui/react/toast";
4
+ import { CircleAlert, CircleCheck, Info, TriangleAlert, X } from "lucide-react";
5
+ import { useUiStrings } from "../i18n/provider.js";
6
+ import { cn } from "../lib/cn.js";
7
+ import { usePortalContainer } from "../lib/theme-scope.js";
8
+ import { buttonStyles } from "./button.js";
9
+ import { Spinner } from "./spinner.js";
10
+ // Not locale-bound: it only queues what callers pass. The <Toaster> renders it in its locale.
11
+ const manager = BaseToast.createToastManager();
12
+ function addOptions(kind, title, options = {}) {
13
+ const { id, description, action, cancel, duration, important } = options;
14
+ const timeout = duration === undefined ? (kind === "loading" ? 0 : undefined) : duration;
15
+ return {
16
+ ...(id !== undefined && { id: String(id) }),
17
+ title,
18
+ type: kind,
19
+ ...(description !== undefined && { description }),
20
+ ...(timeout !== undefined && { timeout: Number.isFinite(timeout) ? timeout : 0 }),
21
+ priority: important === true ? "high" : "low",
22
+ data: { action, cancel },
23
+ };
24
+ }
25
+ const show = (kind) => (title, options) => manager.add(addOptions(kind, title, options));
26
+ const resolveMessage = (message, value) => typeof message === "function" ? message(value) : message;
27
+ /**
28
+ * `toast("Changes saved")`, `toast.success`, `.error`, `.warning`, `.info`, `.loading`,
29
+ * `.promise(fetchThing(), { loading, success, error })`, `toast.dismiss(id)`. Needs one mounted
30
+ * `<Toaster />`.
31
+ */
32
+ export const toast = Object.assign(show("default"), {
33
+ success: show("success"),
34
+ error: show("error"),
35
+ warning: show("warning"),
36
+ info: show("info"),
37
+ loading: show("loading"),
38
+ /**
39
+ * One toast that follows the promise: loading, then success or error. Fire and forget: the
40
+ * caller still owns `promise` and its rejection.
41
+ */
42
+ promise: (promise, messages, options) => {
43
+ // The promise owns one toast, so per-state options can't carry an id.
44
+ const { id: _id, ...rest } = options ?? {};
45
+ const state = (kind, title) => {
46
+ const { id: _ignored, ...update } = addOptions(kind, title, rest);
47
+ return update;
48
+ };
49
+ manager
50
+ .promise(promise, {
51
+ loading: state("loading", messages.loading),
52
+ success: (value) => state("success", resolveMessage(messages.success, value)),
53
+ error: (error) => state("error", resolveMessage(messages.error, error)),
54
+ })
55
+ .catch(() => undefined);
56
+ },
57
+ /** Dismisses one toast, or every toast without an id. */
58
+ dismiss: (id) => manager.close(id === undefined ? undefined : String(id)),
59
+ });
60
+ /* ------------------------------------------------------------------------------------------ */
61
+ /* Toaster */
62
+ /* ------------------------------------------------------------------------------------------ */
63
+ const ICONS = {
64
+ default: null,
65
+ success: _jsx(CircleCheck, { "aria-hidden": true, className: "size-4 text-success-fg" }),
66
+ error: _jsx(CircleAlert, { "aria-hidden": true, className: "size-4 text-danger-fg" }),
67
+ warning: _jsx(TriangleAlert, { "aria-hidden": true, className: "size-4 text-warning-fg" }),
68
+ info: _jsx(Info, { "aria-hidden": true, className: "size-4 text-info-fg" }),
69
+ loading: _jsx(Spinner, { size: "md", className: "text-fg-muted" }),
70
+ };
71
+ const isKind = (type) => type !== undefined && Object.hasOwn(ICONS, type);
72
+ /** Stack geometry from Base UI's variables: the front toast sits at the bottom, the rest peek above. */
73
+ const ROOT_CLASS = cn("group/toast absolute inset-x-0 bottom-0 origin-bottom select-none", "flex items-start gap-2.5 rounded-xl p-3 pe-3.5 text-base text-fg surface-raised", "[--gap:0.5rem] [--peek:0.5rem] [--scale:calc(max(0,1-(var(--toast-index)*0.05)))] [--shrink:calc(1-var(--scale))]", "[--height:var(--toast-frontmost-height,var(--toast-height))]", "[--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))]", "z-[calc(100-var(--toast-index))] h-(--height) data-expanded:h-(--toast-height)", "[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))]", "data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]",
74
+ // The hover bridge between stacked toasts, so the stack doesn't collapse while moving across gaps.
75
+ "after:absolute after:top-full after:start-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']", "data-starting-style:[transform:translateY(150%)] data-limited:opacity-0 data-ending-style:opacity-0", "[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]", "data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]", "data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]", "[transition:transform_var(--duration-spring-smooth)_var(--ease-spring-smooth),opacity_var(--duration-base)]", "motion-reduce:[transition:opacity_var(--duration-fast)]", "outline-none focus-visible:ring-focus");
76
+ function ToastItem({ toast: item }) {
77
+ const t = useUiStrings().toast;
78
+ const kind = isKind(item.type) ? item.type : "default";
79
+ const { action, cancel } = item.data ?? {};
80
+ const run = (button) => (event) => {
81
+ button.onClick(event);
82
+ if (!event.defaultPrevented)
83
+ manager.close(item.id);
84
+ };
85
+ return (_jsx(BaseToast.Root, { toast: item, "data-slot": "toast", "data-kind": kind, className: ROOT_CLASS, children: _jsxs(BaseToast.Content, { className: "flex min-w-0 flex-1 items-start gap-2.5 overflow-hidden transition-opacity duration-(--duration-base) data-behind:opacity-0 data-expanded:opacity-100", children: [ICONS[kind] !== null && (_jsx("span", { className: "relative mt-0.5 flex size-4 shrink-0 items-center justify-center", children: ICONS[kind] })), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx(BaseToast.Title, { className: "font-medium leading-5 text-fg" }), _jsx(BaseToast.Description, { render: _jsx("div", {}), className: "text-sm text-fg-muted text-pretty" })] }), (cancel !== undefined || action !== undefined) && (_jsxs("div", { className: "flex shrink-0 flex-wrap items-center justify-end gap-1.5 self-center", children: [cancel !== undefined && (_jsx("button", { type: "button", className: buttonStyles({ variant: "ghost", size: "xs" }), onClick: run(cancel), children: cancel.label })), action !== undefined && (_jsx(BaseToast.Action, { className: buttonStyles({ variant: "primary", size: "xs" }), onClick: run(action), children: action.label }))] })), _jsx(BaseToast.Close, { "aria-label": t.close, className: cn("absolute -top-1.5 -start-1.5 z-10 grid size-5 place-items-center rounded-full", "border border-border bg-surface-raised text-fg-muted shadow-xs outline-none", "opacity-0 transition-opacity duration-(--duration-fast) group-hover/toast:opacity-100 focus-visible:opacity-100", "hover:text-fg focus-visible:ring-focus"), children: _jsx(X, { "aria-hidden": true, className: "size-3" }) })] }) }));
86
+ }
87
+ function ToastList() {
88
+ const { toasts } = BaseToast.useToastManager();
89
+ return toasts.map((item) => _jsx(ToastItem, { toast: item }, item.id));
90
+ }
91
+ const length = (value) => (typeof value === "number" ? `${value}px` : value);
92
+ function insetVars(offset) {
93
+ if (offset === undefined)
94
+ return {};
95
+ if (typeof offset === "number" || typeof offset === "string") {
96
+ return { "--toast-inset-bottom": length(offset), "--toast-inset-x": length(offset) };
97
+ }
98
+ const x = offset.end;
99
+ return {
100
+ ...(offset.bottom !== undefined && { "--toast-inset-bottom": length(offset.bottom) }),
101
+ ...(x !== undefined && { "--toast-inset-x": length(x) }),
102
+ };
103
+ }
104
+ function placement(position) {
105
+ switch (position) {
106
+ case "bottom-start":
107
+ return "start-(--toast-inset-x)";
108
+ case "bottom-center":
109
+ return "left-1/2 -translate-x-1/2";
110
+ case "bottom-end":
111
+ return "end-(--toast-inset-x)";
112
+ default: {
113
+ const never = position;
114
+ return never;
115
+ }
116
+ }
117
+ }
118
+ /**
119
+ * Mount once near the app root. Bottom-end by default, raised surfaces, semantic icons, swipe
120
+ * or ✕ to dismiss, F6 to reach the stack from the keyboard; announced politely. The viewport sits on
121
+ * the `toast` layer, under `overlay` (dialogs, popovers), so a modal's backdrop covers the stack and
122
+ * its close button always stays clickable. Fire toasts with `toast(...)`.
123
+ */
124
+ export function Toaster({ position = "bottom-end", offset, duration = 4000, className }) {
125
+ const container = usePortalContainer();
126
+ const t = useUiStrings().toast;
127
+ return (_jsx(BaseToast.Provider, { toastManager: manager, limit: 4, timeout: duration, children: _jsx(BaseToast.Portal, { container: container, children: _jsx(BaseToast.Viewport, { "data-slot": "toaster", "data-position": position, "aria-label": t.notifications, className: cn("fixed z-(--z-toast) font-sans outline-none [--toast-inset-bottom:1rem] [--toast-inset-x:1rem]", "bottom-[calc(var(--toast-inset-bottom)+env(safe-area-inset-bottom))]", "w-[min(22.5rem,calc(100vw-2*var(--toast-inset-x)))]", placement(position), className), style: insetVars(offset), children: _jsx(ToastList, {}) }) }) }));
128
+ }
@@ -0,0 +1,50 @@
1
+ import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
2
+ import { type ComponentProps, type ReactElement, type ReactNode } from "react";
3
+ /** Wrap the app once: tooltips opened in quick succession skip their delay. */
4
+ export declare function TooltipProvider({ delay, closeDelay, ...props }: ComponentProps<typeof BaseTooltip.Provider>): import("react").JSX.Element;
5
+ export interface TooltipContentProps extends ComponentProps<typeof BaseTooltip.Popup> {
6
+ side?: ComponentProps<typeof BaseTooltip.Positioner>["side"];
7
+ align?: ComponentProps<typeof BaseTooltip.Positioner>["align"];
8
+ sideOffset?: number;
9
+ }
10
+ export declare function TooltipContent({ side, align, sideOffset, className, ...props }: TooltipContentProps): import("react").JSX.Element;
11
+ export interface TooltipProps {
12
+ /** Tooltip text. */
13
+ content: ReactNode;
14
+ /**
15
+ * The action this control triggers: its keys, from the host's keymap (`ShortcutProvider`), show
16
+ * after the text, and nothing shows when it has no binding. Prefer it to `shortcut`.
17
+ */
18
+ action?: string;
19
+ /** Keys shown after the text when there is no `action` (keys the caller already read from the keymap). */
20
+ shortcut?: readonly string[];
21
+ /** The trigger element; must accept a ref and DOM props (e.g. <IconButton>). */
22
+ children: ReactElement;
23
+ side?: TooltipContentProps["side"];
24
+ align?: TooltipContentProps["align"];
25
+ disabled?: boolean;
26
+ }
27
+ /** Keycaps styled for the tooltip's dark popup (a tooltip that lists several actions uses one per row). */
28
+ export declare function TooltipKeys({ keys }: {
29
+ readonly keys: readonly string[];
30
+ }): import("react").JSX.Element;
31
+ /**
32
+ * What a control with a `disabledReason` adds to the disabled look: it still takes the pointer, so its
33
+ * tooltip opens on hover, and it doesn't press.
34
+ */
35
+ export declare const disabledReasonClass = "data-disabled:pointer-events-auto data-disabled:active:scale-100";
36
+ /**
37
+ * A disabled control that says why (`disabledReason` on Button, IconButton and SegmentedControlItem):
38
+ * the reason is its tooltip and its accessible description. The control stays focusable
39
+ * (`aria-disabled`), so a keyboard reaches the reason too.
40
+ */
41
+ export declare function DisabledReasonTooltip({ reason, children, }: {
42
+ readonly reason: string;
43
+ /** The disabled control; must accept a ref and DOM props. */
44
+ readonly children: ReactElement;
45
+ }): import("react").JSX.Element;
46
+ /**
47
+ * Convenience wrapper: `<Tooltip content="Copy" shortcut={["mod","c"]}><IconButton …/></Tooltip>`.
48
+ * A `shortcut` also sets `aria-keyshortcuts` on the trigger; the keycaps in the popup are hidden.
49
+ */
50
+ export declare function Tooltip({ content, action, shortcut: given, children, side, align, disabled }: TooltipProps): import("react").JSX.Element;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
4
+ import { useId } from "react";
5
+ import { cn, resolveClassName } from "../lib/cn.js";
6
+ import { keyShortcutsProps } from "../lib/keys.js";
7
+ import { useShortcut } from "../lib/shortcut-context.js";
8
+ import { usePortalContainer } from "../lib/theme-scope.js";
9
+ import { Shortcut } from "./kbd.js";
10
+ /** Wrap the app once: tooltips opened in quick succession skip their delay. */
11
+ export function TooltipProvider({ delay = 450, closeDelay = 0, ...props }) {
12
+ return _jsx(BaseTooltip.Provider, { delay: delay, closeDelay: closeDelay, ...props });
13
+ }
14
+ export function TooltipContent({ side = "top", align = "center", sideOffset = 6, className, ...props }) {
15
+ const container = usePortalContainer();
16
+ return (_jsx(BaseTooltip.Portal, { container: container, children: _jsx(BaseTooltip.Positioner, { side: side, align: align, sideOffset: sideOffset, className: "z-(--z-tooltip)", children: _jsx(BaseTooltip.Popup, { "data-slot": "tooltip", className: (state) => cn("popup-motion flex max-w-72 items-center gap-2 rounded-md px-2 py-1 text-xs font-medium", "bg-gray-12 text-gray-1 shadow-md data-instant:transition-none", resolveClassName(className, state)), ...props }) }) }));
17
+ }
18
+ /** Keycaps styled for the tooltip's dark popup (a tooltip that lists several actions uses one per row). */
19
+ export function TooltipKeys({ keys }) {
20
+ return (_jsx(Shortcut, { keys: keys, size: "sm", className: "[&_kbd]:border-transparent [&_kbd]:bg-gray-11/30 [&_kbd]:text-gray-1 [&_kbd]:shadow-none" }));
21
+ }
22
+ /**
23
+ * What a control with a `disabledReason` adds to the disabled look: it still takes the pointer, so its
24
+ * tooltip opens on hover, and it doesn't press.
25
+ */
26
+ export const disabledReasonClass = "data-disabled:pointer-events-auto data-disabled:active:scale-100";
27
+ /**
28
+ * A disabled control that says why (`disabledReason` on Button, IconButton and SegmentedControlItem):
29
+ * the reason is its tooltip and its accessible description. The control stays focusable
30
+ * (`aria-disabled`), so a keyboard reaches the reason too.
31
+ */
32
+ export function DisabledReasonTooltip({ reason, children, }) {
33
+ const descriptionId = useId();
34
+ return (_jsxs(_Fragment, { children: [_jsxs(BaseTooltip.Root, { children: [_jsx(BaseTooltip.Trigger, { render: children, "aria-describedby": descriptionId }), _jsx(TooltipContent, { children: _jsx("span", { children: reason }) })] }), _jsx("span", { id: descriptionId, hidden: true, children: reason })] }));
35
+ }
36
+ /**
37
+ * Convenience wrapper: `<Tooltip content="Copy" shortcut={["mod","c"]}><IconButton …/></Tooltip>`.
38
+ * A `shortcut` also sets `aria-keyshortcuts` on the trigger; the keycaps in the popup are hidden.
39
+ */
40
+ export function Tooltip({ content, action, shortcut: given, children, side, align, disabled }) {
41
+ const bound = useShortcut(action);
42
+ const shortcut = bound ?? given;
43
+ if (disabled)
44
+ return children;
45
+ return (_jsxs(BaseTooltip.Root, { children: [_jsx(BaseTooltip.Trigger, { render: children, ...keyShortcutsProps(shortcut) }), _jsxs(TooltipContent, { ...(side && { side }), ...(align && { align }), children: [_jsx("span", { children: content }), shortcut && _jsx(TooltipKeys, { keys: shortcut })] })] }));
46
+ }
@@ -0,0 +1,15 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ import { type TooltipContentProps } from "./tooltip.js";
3
+ export interface TruncatedTextProps extends Omit<ComponentProps<"span">, "children"> {
4
+ /** The text; cut to one line with an ellipsis when it doesn't fit. */
5
+ readonly children: ReactNode;
6
+ /** What the tooltip shows (default: the text itself). */
7
+ readonly full?: ReactNode;
8
+ readonly side?: TooltipContentProps["side"];
9
+ }
10
+ /**
11
+ * One line of text that truncates with an ellipsis when its box is too narrow, and then shows the
12
+ * whole of it in a tooltip on hover. Text that fits has no tooltip. Screen readers always get the
13
+ * full text (the cut is visual). Use it for names and titles that must stay on one line.
14
+ */
15
+ export declare function TruncatedText({ children, full, side, className, ...props }: TruncatedTextProps): import("react").JSX.Element;