@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,31 @@
1
+ "use client";
2
+ import { DURATIONS } from "../_bundled/tokens/index.js";
3
+ import { useEffect, useRef, useState } from "react";
4
+ /**
5
+ * Turns a raw `loading` flag into a flicker-free one: fast operations never flash a loader, and a
6
+ * loader that does appear stays long enough to read as intentional (Vercel's 150–300 / 300–500 ms rule).
7
+ */
8
+ export function useDelayedLoading(loading, { delay = DURATIONS.loaderDelay, minVisible = DURATIONS.loaderMin } = {}) {
9
+ const [visible, setVisible] = useState(false);
10
+ const shownAt = useRef(null);
11
+ useEffect(() => {
12
+ if (loading) {
13
+ if (visible)
14
+ return undefined;
15
+ const id = setTimeout(() => {
16
+ shownAt.current = Date.now();
17
+ setVisible(true);
18
+ }, delay);
19
+ return () => clearTimeout(id);
20
+ }
21
+ if (!visible)
22
+ return undefined;
23
+ const elapsed = Date.now() - (shownAt.current ?? 0);
24
+ const id = setTimeout(() => {
25
+ shownAt.current = null;
26
+ setVisible(false);
27
+ }, Math.max(0, minVisible - elapsed));
28
+ return () => clearTimeout(id);
29
+ }, [loading, visible, delay, minVisible]);
30
+ return visible;
31
+ }
@@ -0,0 +1,26 @@
1
+ import { type ReactElement } from "react";
2
+ declare module "react" {
3
+ interface InputHTMLAttributes<T> {
4
+ /** The folder picker every browser engine has, missing from React's types. */
5
+ webkitdirectory?: string;
6
+ }
7
+ }
8
+ export interface FilePickerOptions {
9
+ /** File types the picker offers (`"image/png,image/jpeg"`); every type when absent. */
10
+ readonly accept?: string | undefined;
11
+ /**
12
+ * What the person picks: one file (the default), several, or a folder — every file under it, each
13
+ * with its `webkitRelativePath`.
14
+ */
15
+ readonly select?: "one" | "many" | "folder" | undefined;
16
+ }
17
+ /**
18
+ * The system's file picker on demand: render `input` once anywhere in the component and call `pick()`
19
+ * from a button or menu item (the press is the activation a browser asks for). `pick()` resolves with
20
+ * what the person picked, or with no files when they cancel. The input is cleared after each pick, so
21
+ * picking the same file or folder again is a new pick.
22
+ */
23
+ export declare function useFilePicker({ accept, select }?: FilePickerOptions): {
24
+ readonly pick: () => Promise<readonly File[]>;
25
+ readonly input: ReactElement;
26
+ };
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useRef } from "react";
4
+ /** The input's answer to one pick: the files, or none when the person cancels. */
5
+ function answerOf(element, signal) {
6
+ return new Promise((resolve) => {
7
+ signal.addEventListener("abort", () => resolve([]), { once: true });
8
+ // On the element itself, so the answer arrives even when the component is gone by then.
9
+ element.addEventListener("change", () => {
10
+ const files = [...(element.files ?? [])];
11
+ element.value = "";
12
+ resolve(files);
13
+ }, { signal });
14
+ element.addEventListener("cancel", () => resolve([]), { signal });
15
+ });
16
+ }
17
+ /**
18
+ * The system's file picker on demand: render `input` once anywhere in the component and call `pick()`
19
+ * from a button or menu item (the press is the activation a browser asks for). `pick()` resolves with
20
+ * what the person picked, or with no files when they cancel. The input is cleared after each pick, so
21
+ * picking the same file or folder again is a new pick.
22
+ */
23
+ export function useFilePicker({ accept, select = "one" } = {}) {
24
+ const ref = useRef(null);
25
+ const waiting = useRef(null);
26
+ const input = (_jsx("input", { ref: ref, type: "file", hidden: true, accept: accept, multiple: select !== "one", ...(select === "folder" ? { webkitdirectory: "" } : {}) }));
27
+ const pick = async () => {
28
+ const element = ref.current;
29
+ if (element === null)
30
+ return [];
31
+ // A pick still waiting (an engine that never says `cancel`) ends as a cancel, so each answer goes
32
+ // to the latest pick alone.
33
+ waiting.current?.abort();
34
+ const controller = new AbortController();
35
+ waiting.current = controller;
36
+ const answer = answerOf(element, controller.signal);
37
+ element.click();
38
+ const files = await answer;
39
+ controller.abort();
40
+ return files;
41
+ };
42
+ return { pick, input };
43
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * False on the server and during hydration, true once the client has taken over. Gate any render
3
+ * difference that comes from client-only state (e.g. `prefers-reduced-motion`) behind it so the
4
+ * first client render matches the server HTML.
5
+ */
6
+ export declare function useHydrated(): boolean;
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { useSyncExternalStore } from "react";
3
+ const subscribe = () => () => { };
4
+ /**
5
+ * False on the server and during hydration, true once the client has taken over. Gate any render
6
+ * difference that comes from client-only state (e.g. `prefers-reduced-motion`) behind it so the
7
+ * first client render matches the server HTML.
8
+ */
9
+ export function useHydrated() {
10
+ return useSyncExternalStore(subscribe, () => true, () => false);
11
+ }
@@ -0,0 +1,12 @@
1
+ import { type RefObject } from "react";
2
+ /**
3
+ * A ref that holds `value` as of the last commit, for effects, timers and stable callbacks that must
4
+ * read the newest props without re-running. It's written after commit (a layout effect), never during
5
+ * render: a render React throws away can't leak into it. Don't read it while rendering.
6
+ */
7
+ export declare function useLatest<T>(value: T): RefObject<T>;
8
+ /**
9
+ * A callback with a stable identity that always runs the latest `fn` (for memoized children). Like
10
+ * `useLatest`, it reads what the last commit saw: call it from events and effects, never while rendering.
11
+ */
12
+ export declare function useStableCallback<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R;
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ import { useCallback, useLayoutEffect, useRef } from "react";
3
+ /**
4
+ * A ref that holds `value` as of the last commit, for effects, timers and stable callbacks that must
5
+ * read the newest props without re-running. It's written after commit (a layout effect), never during
6
+ * render: a render React throws away can't leak into it. Don't read it while rendering.
7
+ */
8
+ export function useLatest(value) {
9
+ const ref = useRef(value);
10
+ useLayoutEffect(() => {
11
+ ref.current = value;
12
+ });
13
+ return ref;
14
+ }
15
+ /**
16
+ * A callback with a stable identity that always runs the latest `fn` (for memoized children). Like
17
+ * `useLatest`, it reads what the last commit saw: call it from events and effects, never while rendering.
18
+ */
19
+ export function useStableCallback(fn) {
20
+ const latest = useLatest(fn);
21
+ return useCallback((...args) => latest.current(...args), []);
22
+ }
@@ -0,0 +1,12 @@
1
+ import { type RefObject } from "react";
2
+ import type { ResolvedTheme } from "./theme.js";
3
+ /**
4
+ * The theme an element is painted in: its nearest `data-theme` (written on <html> by
5
+ * <ThemeProvider> and `themeInitScript`, or on any subtree that forces one), without requiring the
6
+ * provider. For art with a separate file per theme, so only that file loads.
7
+ *
8
+ * `undefined` until the element is mounted: the server can't know the theme, and asset URLs from
9
+ * `new URL(…, import.meta.url)` only resolve in the client bundle. It's set in a layout effect, so
10
+ * client-rendered art never paints without it.
11
+ */
12
+ export declare function useResolvedTheme(ref: RefObject<Element | null>): ResolvedTheme | undefined;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { useLayoutEffect, useState } from "react";
3
+ const SELECTOR = "[data-theme]";
4
+ const resolve = (el) => el?.getAttribute("data-theme") === "dark" ? "dark" : "light";
5
+ /**
6
+ * The theme an element is painted in: its nearest `data-theme` (written on <html> by
7
+ * <ThemeProvider> and `themeInitScript`, or on any subtree that forces one), without requiring the
8
+ * provider. For art with a separate file per theme, so only that file loads.
9
+ *
10
+ * `undefined` until the element is mounted: the server can't know the theme, and asset URLs from
11
+ * `new URL(…, import.meta.url)` only resolve in the client bundle. It's set in a layout effect, so
12
+ * client-rendered art never paints without it.
13
+ */
14
+ export function useResolvedTheme(ref) {
15
+ const [theme, setTheme] = useState();
16
+ useLayoutEffect(() => {
17
+ const host = ref.current?.closest(SELECTOR) ?? document.documentElement;
18
+ const update = () => setTheme(resolve(host));
19
+ update();
20
+ const observer = new MutationObserver(update);
21
+ observer.observe(host, { attributes: true, attributeFilter: ["data-theme"] });
22
+ return () => observer.disconnect();
23
+ }, [ref]);
24
+ return theme;
25
+ }
@@ -0,0 +1,393 @@
1
+ /* Base layer, variants, keyframes and bespoke utilities for @nibapp/ui. */
2
+
3
+ /* `dark:` follows the resolved theme attribute set by <ThemeProvider> (or any subtree). */
4
+ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
5
+
6
+ @theme {
7
+ /* Room for macOS's traffic lights in an overlay title bar (inset at x 18, 70 px wide): what follows
8
+ them starts 18 px after, as they start 18 px in (`pl-traffic-lights`). */
9
+ --spacing-traffic-lights: 88px;
10
+ --animate-spin: spin 0.9s linear infinite;
11
+ --animate-shimmer: nib-shimmer 2.2s var(--ease-in-out) infinite;
12
+ --animate-sheen: nib-sheen 1.8s linear infinite;
13
+ --animate-pulse-soft: nib-pulse-soft 1.6s var(--ease-in-out) infinite;
14
+ --animate-caret: nib-caret 1.1s steps(1, end) infinite;
15
+ /* Server-rendered entrances: CSS, so the content paints with the HTML instead of waiting for
16
+ hydration to fade it in (a JS-driven `initial={{ opacity: 0 }}` hides it until then). */
17
+ --animate-rise-in: nib-rise-in var(--duration-spring-smooth) var(--ease-spring-smooth) both;
18
+
19
+ @keyframes spin {
20
+ to {
21
+ transform: rotate(360deg);
22
+ }
23
+ }
24
+ @keyframes nib-shimmer {
25
+ from {
26
+ background-position: 150% 0;
27
+ }
28
+ to {
29
+ background-position: -50% 0;
30
+ }
31
+ }
32
+ @keyframes nib-sheen {
33
+ from {
34
+ transform: translateX(-100%);
35
+ }
36
+ to {
37
+ transform: translateX(100%);
38
+ }
39
+ }
40
+ @keyframes nib-pulse-soft {
41
+ 0%,
42
+ 100% {
43
+ opacity: 1;
44
+ }
45
+ 50% {
46
+ opacity: 0.45;
47
+ }
48
+ }
49
+ @keyframes nib-rise-in {
50
+ from {
51
+ opacity: 0;
52
+ translate: 0 8px;
53
+ }
54
+ }
55
+ @keyframes nib-caret {
56
+ 0%,
57
+ 100% {
58
+ opacity: 1;
59
+ }
60
+ 50% {
61
+ opacity: 0;
62
+ }
63
+ }
64
+ }
65
+
66
+ @layer base {
67
+ html {
68
+ font-family: var(--font-sans);
69
+ font-optical-sizing: auto;
70
+ /* Inter 4: tailed l (cv05), single-storey a (cv11) — softer, friendlier rhythm. */
71
+ font-feature-settings: "cv05", "cv11", "calt";
72
+ -webkit-font-smoothing: antialiased;
73
+ -moz-osx-font-smoothing: grayscale;
74
+ text-rendering: optimizeLegibility;
75
+ -webkit-tap-highlight-color: transparent;
76
+ background-color: var(--bg);
77
+ color: var(--fg);
78
+ /* Native checkboxes, radios and ranges are ink like ours; the text caret is a blue micro-moment. */
79
+ accent-color: var(--primary);
80
+ caret-color: var(--accent);
81
+ }
82
+
83
+ body {
84
+ font-size: var(--text-base);
85
+ line-height: var(--text-base--line-height);
86
+ letter-spacing: var(--text-base--letter-spacing);
87
+ }
88
+
89
+ ::selection {
90
+ background-color: var(--selection);
91
+ }
92
+
93
+ :where(code, kbd, samp, pre) {
94
+ font-family: var(--font-mono);
95
+ font-feature-settings: normal;
96
+ font-variant-ligatures: none;
97
+ }
98
+
99
+ /* Code, keys and machine output read left to right in every language, isolated from the text
100
+ around them, so a right-to-left sentence never reorders their punctuation. A `dir` on the
101
+ element (`auto` for text a person wrote) still decides. */
102
+ :where(code, kbd, samp, pre):not([dir]) {
103
+ direction: ltr;
104
+ unicode-bidi: isolate;
105
+ }
106
+
107
+ /* A field that takes its direction from what a person types (`dir="auto"`) is left to right
108
+ while empty; its placeholder is our copy, so it keeps the page's direction until then. */
109
+ :where(input, textarea)[dir="auto"]:placeholder-shown {
110
+ direction: inherit;
111
+ }
112
+
113
+ :where(:focus-visible) {
114
+ outline: 2px solid var(--focus);
115
+ outline-offset: 2px;
116
+ }
117
+
118
+ /* Calm scrollbars everywhere; ScrollArea renders its own overlay bars. */
119
+ * {
120
+ scrollbar-width: thin;
121
+ scrollbar-color: color-mix(in oklab, var(--gray-12) 18%, transparent) transparent;
122
+ }
123
+
124
+ :where(button, [role="button"]) {
125
+ cursor: default;
126
+ }
127
+
128
+ [hidden]:where(:not([hidden="until-found"])) {
129
+ display: none !important;
130
+ }
131
+
132
+ /*
133
+ * Icons that point along the reading direction mirror in a right-to-left language (`dir` comes
134
+ * from the language). The mirror applies before a component's own `rotate`, so a mirrored icon that
135
+ * turns (an open chevron) turns the other way right to left (`rtl:-rotate-90`).
136
+ */
137
+ [dir="rtl"]
138
+ :is(
139
+ .lucide-chevron-right,
140
+ .lucide-chevron-left,
141
+ .lucide-chevrons-right,
142
+ .lucide-chevrons-left,
143
+ .lucide-arrow-right,
144
+ .lucide-arrow-left,
145
+ .lucide-arrow-up-right,
146
+ .lucide-arrow-up-left,
147
+ .lucide-corner-down-right,
148
+ .lucide-corner-down-left,
149
+ .lucide-external-link,
150
+ .lucide-square-arrow-out-up-right,
151
+ .lucide-log-in,
152
+ .lucide-log-out,
153
+ .lucide-panel-left,
154
+ .lucide-panel-right,
155
+ .lucide-reply,
156
+ .lucide-forward,
157
+ .lucide-undo-2,
158
+ .lucide-redo-2
159
+ ) {
160
+ scale: -1 1;
161
+ }
162
+ }
163
+
164
+ /* ------------------------------------------------------------------------------------------ */
165
+ /* Utilities */
166
+ /* ------------------------------------------------------------------------------------------ */
167
+
168
+ /* Focus ring for components that manage their own focus treatment. */
169
+ @utility ring-focus {
170
+ outline: 2px solid var(--focus);
171
+ outline-offset: 2px;
172
+ }
173
+
174
+ /* Animated light sweep over text — the "working" voice of the thinking indicator. */
175
+ @utility text-shimmer {
176
+ color: transparent;
177
+ background-image: linear-gradient(
178
+ 100deg,
179
+ var(--fg-subtle) 0%,
180
+ var(--fg-subtle) 35%,
181
+ var(--fg) 50%,
182
+ var(--fg-subtle) 65%,
183
+ var(--fg-subtle) 100%
184
+ );
185
+ background-size: 250% 100%;
186
+ background-clip: text;
187
+ -webkit-background-clip: text;
188
+ animation: var(--animate-shimmer);
189
+
190
+ @media (prefers-reduced-motion: reduce) {
191
+ animation: none;
192
+ color: var(--fg-muted);
193
+ background: none;
194
+ }
195
+ }
196
+
197
+ /* Touch screens: the control's hit area reaches at least 44 × 44 px (WCAG 2.5.5, Apple's HIG) without
198
+ changing how it looks — an invisible layer centred on it, grown only along the sides shorter than
199
+ 44 px. The control needs `position: relative` and no clipping; its own ::after stays free elsewhere. */
200
+ @utility touch-target {
201
+ @media (pointer: coarse) {
202
+ &::after {
203
+ content: "";
204
+ position: absolute;
205
+ inset-block: min(0px, calc((100% - 44px) / 2));
206
+ inset-inline: min(0px, calc((100% - 44px) / 2));
207
+ }
208
+ }
209
+ }
210
+
211
+ /* Any pointer: a control drawn smaller than 24 × 24 px (a 14 px checkbox) still takes presses across
212
+ 24 × 24 (WCAG 2.5.8), on an invisible ::before layer centred on it. `touch-target` grows it to
213
+ 44 px on touch screens on ::after; the two coexist. The control needs `position: relative`. */
214
+ @utility hit-target {
215
+ &::before {
216
+ content: "";
217
+ position: absolute;
218
+ inset-block: min(0px, calc((100% - 24px) / 2));
219
+ inset-inline: min(0px, calc((100% - 24px) / 2));
220
+ }
221
+ }
222
+
223
+ /*
224
+ * The caret where text is arriving (a streamed reply, live dictation): accent ink, 2 px wide, blinking
225
+ * in steps and still under reduced motion. One mark wherever text streams in; callers add its gap.
226
+ */
227
+ @utility stream-caret {
228
+ display: inline-block;
229
+ height: 1.05em;
230
+ width: 2px;
231
+ translate: 0 0.18em;
232
+ border-radius: calc(infinity * 1px);
233
+ background-color: var(--color-accent);
234
+ animation: var(--animate-caret);
235
+
236
+ @media (prefers-reduced-motion: reduce) {
237
+ animation: none;
238
+ }
239
+ }
240
+
241
+ /* Raised surface: background + border + layered shadow + top edge highlight. */
242
+ @utility surface-raised {
243
+ background-color: var(--surface-raised);
244
+ border: 1px solid var(--border);
245
+ box-shadow: var(--shadow-lg), var(--shadow-inset-edge);
246
+ }
247
+
248
+ @utility tabular {
249
+ font-variant-numeric: tabular-nums;
250
+ }
251
+
252
+ /*
253
+ * Something whose moment has passed or is held back (an expired request, an archived session, a
254
+ * resolved comment, a question waiting its turn, a list while offline): it reads inactive at a glance
255
+ * and its words still read at 4.5:1. Ink text steps down to fg-muted (the quieter tiers stay as they
256
+ * are); icons, avatars and status glyphs lose their colour and fade, which decorative imagery may.
257
+ * Never opacity on text: faded words fall under 4.5:1.
258
+ */
259
+ @utility inactive {
260
+ --fg: var(--fg-muted);
261
+
262
+ &
263
+ :where(svg, img, [data-slot="avatar"], [data-slot="crew-avatar"], [data-inactive-media]):not(
264
+ :where([data-slot="avatar"], [data-slot="crew-avatar"], [data-inactive-media]) *
265
+ ) {
266
+ filter: grayscale(1);
267
+ opacity: 0.55;
268
+ }
269
+ }
270
+
271
+ @utility scrollbar-none {
272
+ scrollbar-width: none;
273
+ &::-webkit-scrollbar {
274
+ display: none;
275
+ }
276
+ }
277
+
278
+ /*
279
+ * A scroller whose edges fade only where content goes on past them: the top once it's scrolled, the
280
+ * bottom while there's more below, neither when it all fits. Scroll-driven (no script); an engine
281
+ * without scroll timelines keeps a 24 px top fade only. Used by the sidebar and the transcript.
282
+ */
283
+ @property --scroll-fade-top {
284
+ syntax: "<length>";
285
+ inherits: false;
286
+ initial-value: 0px;
287
+ }
288
+ @property --scroll-fade-bottom {
289
+ syntax: "<length>";
290
+ inherits: false;
291
+ initial-value: 0px;
292
+ }
293
+ @keyframes scroll-fade-top-in {
294
+ from {
295
+ --scroll-fade-top: 0px;
296
+ }
297
+ to {
298
+ --scroll-fade-top: 24px;
299
+ }
300
+ }
301
+ @keyframes scroll-fade-bottom-out {
302
+ from {
303
+ --scroll-fade-bottom: 24px;
304
+ }
305
+ to {
306
+ --scroll-fade-bottom: 0px;
307
+ }
308
+ }
309
+ @utility scroll-fade-y {
310
+ mask-image: linear-gradient(
311
+ to bottom,
312
+ transparent 0,
313
+ black var(--scroll-fade-top),
314
+ black calc(100% - var(--scroll-fade-bottom)),
315
+ transparent 100%
316
+ );
317
+ animation:
318
+ scroll-fade-top-in linear both,
319
+ scroll-fade-bottom-out linear both;
320
+ animation-timeline: scroll(self), scroll(self);
321
+ animation-range:
322
+ 0 24px,
323
+ calc(100% - 24px) 100%;
324
+ }
325
+
326
+ /* Base UI popups expose data-starting-style / data-ending-style — one shared enter/exit. Functional
327
+ UI: a 120 ms fade and scale from the trigger, no blur; exits are quicker still. */
328
+ @utility popup-motion {
329
+ transform-origin: var(--transform-origin);
330
+ transition-property: opacity, scale;
331
+ transition-duration: var(--duration-fast);
332
+ transition-timing-function: var(--ease-out);
333
+
334
+ &[data-starting-style],
335
+ &[data-ending-style] {
336
+ opacity: 0;
337
+ scale: 0.97;
338
+ }
339
+ &[data-ending-style] {
340
+ /* Exits are quicker than entrances and never ease-in (UI shouldn't accelerate away). */
341
+ transition-duration: var(--duration-instant);
342
+ }
343
+ @media (prefers-reduced-motion: reduce) {
344
+ &[data-starting-style],
345
+ &[data-ending-style] {
346
+ scale: 1;
347
+ }
348
+ }
349
+ }
350
+
351
+ /* Controls (buttons, tiles): colour feedback is a quick tween that keeps up with the pointer; only
352
+ the press scale uses the snappy spring. */
353
+ @utility control-motion {
354
+ transition-property: background-color, border-color, color, box-shadow, opacity, scale;
355
+ transition-duration:
356
+ var(--duration-fast), var(--duration-fast), var(--duration-fast), var(--duration-fast),
357
+ var(--duration-fast), var(--duration-spring-snappy);
358
+ transition-timing-function:
359
+ var(--ease-standard), var(--ease-standard), var(--ease-standard), var(--ease-standard),
360
+ var(--ease-standard), var(--ease-spring-snappy);
361
+ }
362
+
363
+ /* Rows and cards that open something answer the press like buttons do: a 1.5 % press on the
364
+ snappy spring (docs/design/system.md §7). No movement under Reduce Motion. */
365
+ @utility press-motion {
366
+ transition: scale var(--duration-spring-snappy) var(--ease-spring-snappy);
367
+ &:active {
368
+ scale: 0.985;
369
+ }
370
+ @media (prefers-reduced-motion: reduce) {
371
+ &:active {
372
+ scale: 1;
373
+ }
374
+ }
375
+ }
376
+
377
+ @utility backdrop-motion {
378
+ transition: opacity var(--duration-moderate) var(--ease-out);
379
+ &[data-starting-style],
380
+ &[data-ending-style] {
381
+ opacity: 0;
382
+ }
383
+ /* A closing backdrop is already gone for the person: a click lands on what's under it, as the closing popup lets it. */
384
+ &[data-ending-style] {
385
+ pointer-events: none;
386
+ }
387
+ }
388
+
389
+ /* Theme swaps flip every surface in one frame instead of cross-fading at different speeds. */
390
+ [data-theme-switching],
391
+ [data-theme-switching] * {
392
+ transition: none !important;
393
+ }
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Syntax highlighting for <CodeBlock> (and anything else rendering Shiki tokens from
3
+ * `src/lib/shiki.ts`).
4
+ *
5
+ * Tokens are emitted with `defaultColor: false`, i.e. each span carries both palettes as custom
6
+ * properties (`--shiki-light`, `--shiki-dark`, `--shiki-light-font-style`, …). `light-dark()` picks
7
+ * the right one from the element's used `color-scheme`, which `[data-theme]` (ThemeProvider) and
8
+ * `:root { color-scheme: light dark }` (system) already drive — so nested theme scopes work and a
9
+ * theme switch never re-tokenizes. kit-light/kit-dark share font styles, so the light variables
10
+ * are authoritative for italic/bold.
11
+ */
12
+
13
+ [data-shiki] span[style*="--shiki-"] {
14
+ color: light-dark(var(--shiki-light), var(--shiki-dark));
15
+ font-style: var(--shiki-light-font-style, inherit);
16
+ font-weight: var(--shiki-light-font-weight, inherit);
17
+ text-decoration: var(--shiki-light-text-decoration, inherit);
18
+ }
19
+
20
+ /* Streaming: freshly appended lines fade in instead of popping (opacity only). */
21
+ @keyframes nib-code-line-in {
22
+ from {
23
+ opacity: 0;
24
+ }
25
+ }
26
+
27
+ [data-shiki][data-streaming] [data-line] {
28
+ animation: nib-code-line-in var(--duration-base) var(--ease-out) both;
29
+ }
30
+
31
+ @media (prefers-reduced-motion: reduce) {
32
+ [data-shiki][data-streaming] [data-line] {
33
+ animation: none;
34
+ }
35
+ }
@@ -0,0 +1,12 @@
1
+ /*
2
+ * The kit's typefaces (self-hosted via Fontsource, OFL-licensed):
3
+ * - Inter 4 with optical sizing → UI + prose
4
+ * - Geist Mono → code, terminals, numerals in data
5
+ * - Source Serif 4 (opsz) → rare editorial moments (greetings, landing headlines)
6
+ * Import once at the app root: `import "@nibapp/ui/fonts.css"`.
7
+ */
8
+ @import "@fontsource-variable/inter/opsz.css";
9
+ @import "@fontsource-variable/inter/opsz-italic.css";
10
+ @import "@fontsource-variable/geist-mono/wght.css";
11
+ @import "@fontsource-variable/source-serif-4/opsz.css";
12
+ @import "@fontsource-variable/source-serif-4/opsz-italic.css";
@@ -0,0 +1,13 @@
1
+ /*
2
+ * @nibapp/ui stylesheet. In an app's global CSS:
3
+ *
4
+ * @import "tailwindcss";
5
+ * @import "@nibapp/ui/styles.css";
6
+ *
7
+ * The @source below makes Tailwind scan @nibapp/ui's components wherever the package lives.
8
+ */
9
+ @import "./tokens.gen.css";
10
+ @import "./base.css";
11
+ @import "./code.css";
12
+
13
+ @source "../";