@adecore/ui 0.14.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 (318) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/AccentSwatches.d.ts +15 -0
  4. package/dist/AccentSwatches.js +21 -0
  5. package/dist/Banner.d.ts +12 -0
  6. package/dist/Banner.js +18 -0
  7. package/dist/Button.d.ts +9 -0
  8. package/dist/Button.js +28 -0
  9. package/dist/ButtonGroup.d.ts +3 -0
  10. package/dist/ButtonGroup.js +11 -0
  11. package/dist/Checkbox.d.ts +11 -0
  12. package/dist/Checkbox.js +9 -0
  13. package/dist/ChoiceCards.d.ts +23 -0
  14. package/dist/ChoiceCards.js +21 -0
  15. package/dist/CloseButton.d.ts +15 -0
  16. package/dist/CloseButton.js +11 -0
  17. package/dist/ColorSwatch.d.ts +14 -0
  18. package/dist/ColorSwatch.js +28 -0
  19. package/dist/ColumnResizeHandle.d.ts +9 -0
  20. package/dist/ColumnResizeHandle.js +12 -0
  21. package/dist/DisabledReason.d.ts +6 -0
  22. package/dist/DisabledReason.js +8 -0
  23. package/dist/DockShell.d.ts +9 -0
  24. package/dist/DockShell.js +49 -0
  25. package/dist/EmptyState.d.ts +16 -0
  26. package/dist/EmptyState.js +11 -0
  27. package/dist/ErrorBoundary.d.ts +24 -0
  28. package/dist/ErrorBoundary.js +47 -0
  29. package/dist/Field.d.ts +17 -0
  30. package/dist/Field.js +48 -0
  31. package/dist/FileIcon.d.ts +8 -0
  32. package/dist/FileIcon.js +15 -0
  33. package/dist/Icon.d.ts +9 -0
  34. package/dist/Icon.js +12 -0
  35. package/dist/IconButton.d.ts +23 -0
  36. package/dist/IconButton.js +25 -0
  37. package/dist/IconPicker.d.ts +17 -0
  38. package/dist/IconPicker.js +93 -0
  39. package/dist/Input.d.ts +14 -0
  40. package/dist/Input.js +25 -0
  41. package/dist/Kbd.d.ts +23 -0
  42. package/dist/Kbd.js +20 -0
  43. package/dist/ListRow.d.ts +10 -0
  44. package/dist/ListRow.js +20 -0
  45. package/dist/Meter.d.ts +12 -0
  46. package/dist/Meter.js +7 -0
  47. package/dist/PanelEmpty.d.ts +16 -0
  48. package/dist/PanelEmpty.js +7 -0
  49. package/dist/PanelHeader.d.ts +6 -0
  50. package/dist/PanelHeader.js +20 -0
  51. package/dist/Pill.d.ts +27 -0
  52. package/dist/Pill.js +25 -0
  53. package/dist/ProjectSwitcher.d.ts +30 -0
  54. package/dist/ProjectSwitcher.js +18 -0
  55. package/dist/PromptDialog.d.ts +40 -0
  56. package/dist/PromptDialog.js +47 -0
  57. package/dist/SectionLabel.d.ts +3 -0
  58. package/dist/SectionLabel.js +16 -0
  59. package/dist/SegmentBar.d.ts +18 -0
  60. package/dist/SegmentBar.js +44 -0
  61. package/dist/Segmented.d.ts +17 -0
  62. package/dist/Segmented.js +7 -0
  63. package/dist/Select.d.ts +27 -0
  64. package/dist/Select.js +34 -0
  65. package/dist/Separator.d.ts +7 -0
  66. package/dist/Separator.js +8 -0
  67. package/dist/ShortcutHints.d.ts +1 -0
  68. package/dist/ShortcutHints.js +94 -0
  69. package/dist/Skeleton.d.ts +6 -0
  70. package/dist/Skeleton.js +6 -0
  71. package/dist/SlidingColumn.d.ts +19 -0
  72. package/dist/SlidingColumn.js +41 -0
  73. package/dist/Spinner.d.ts +6 -0
  74. package/dist/Spinner.js +7 -0
  75. package/dist/Stepper.d.ts +14 -0
  76. package/dist/Stepper.js +18 -0
  77. package/dist/Surface.d.ts +3 -0
  78. package/dist/Surface.js +16 -0
  79. package/dist/Switch.d.ts +10 -0
  80. package/dist/Switch.js +7 -0
  81. package/dist/TextMenu.d.ts +5 -0
  82. package/dist/TextMenu.js +31 -0
  83. package/dist/Tile.d.ts +11 -0
  84. package/dist/Tile.js +10 -0
  85. package/dist/Toasts.d.ts +9 -0
  86. package/dist/Toasts.js +58 -0
  87. package/dist/Tooltip.d.ts +16 -0
  88. package/dist/Tooltip.js +21 -0
  89. package/dist/UIProvider.d.ts +9 -0
  90. package/dist/UIProvider.js +26 -0
  91. package/dist/Waveform.d.ts +16 -0
  92. package/dist/Waveform.js +123 -0
  93. package/dist/Wipe.d.ts +12 -0
  94. package/dist/Wipe.js +47 -0
  95. package/dist/ZoomControls.d.ts +30 -0
  96. package/dist/ZoomControls.js +16 -0
  97. package/dist/class-name.d.ts +2 -0
  98. package/dist/class-name.js +3 -0
  99. package/dist/clipboard.d.ts +2 -0
  100. package/dist/clipboard.js +16 -0
  101. package/dist/context-menu/index.parts.d.ts +1 -0
  102. package/dist/context-menu/index.parts.js +2 -0
  103. package/dist/dialog/index.parts.d.ts +1 -0
  104. package/dist/dialog/index.parts.js +1 -0
  105. package/dist/dialog/parts.d.ts +36 -0
  106. package/dist/dialog/parts.js +55 -0
  107. package/dist/dialog-layer.d.ts +1 -0
  108. package/dist/dialog-layer.js +27 -0
  109. package/dist/error-boundary.d.ts +4 -0
  110. package/dist/error-boundary.js +34 -0
  111. package/dist/error-message.d.ts +1 -0
  112. package/dist/error-message.js +4 -0
  113. package/dist/field-context.d.ts +7 -0
  114. package/dist/field-context.js +4 -0
  115. package/dist/file-icon.d.ts +4 -0
  116. package/dist/file-icon.js +46 -0
  117. package/dist/floating.d.ts +5 -0
  118. package/dist/floating.js +6 -0
  119. package/dist/format/datetime.d.ts +11 -0
  120. package/dist/format/datetime.js +50 -0
  121. package/dist/format/duration.d.ts +5 -0
  122. package/dist/format/duration.js +76 -0
  123. package/dist/format/index.d.ts +6 -0
  124. package/dist/format/index.js +7 -0
  125. package/dist/format/locale.d.ts +17 -0
  126. package/dist/format/locale.js +105 -0
  127. package/dist/format/number.d.ts +8 -0
  128. package/dist/format/number.js +72 -0
  129. package/dist/format/regions.d.ts +6 -0
  130. package/dist/format/regions.js +15 -0
  131. package/dist/format/time-zone.d.ts +1 -0
  132. package/dist/format/time-zone.js +2 -0
  133. package/dist/icon-button-size.d.ts +3 -0
  134. package/dist/icon-button-size.js +3 -0
  135. package/dist/icon-picker.d.ts +23 -0
  136. package/dist/icon-picker.js +80 -0
  137. package/dist/index.d.ts +74 -0
  138. package/dist/index.js +74 -0
  139. package/dist/lazy.d.ts +12 -0
  140. package/dist/lazy.js +104 -0
  141. package/dist/locales/en.json +59 -0
  142. package/dist/locales/nl.json +59 -0
  143. package/dist/locales.d.ts +4 -0
  144. package/dist/locales.js +17 -0
  145. package/dist/menu/index.parts.d.ts +1 -0
  146. package/dist/menu/index.parts.js +1 -0
  147. package/dist/menu/parts.d.ts +67 -0
  148. package/dist/menu/parts.js +113 -0
  149. package/dist/merge-refs.d.ts +2 -0
  150. package/dist/merge-refs.js +11 -0
  151. package/dist/modality.d.ts +1 -0
  152. package/dist/modality.js +29 -0
  153. package/dist/platform.d.ts +2 -0
  154. package/dist/platform.js +9 -0
  155. package/dist/popover/index.parts.d.ts +1 -0
  156. package/dist/popover/index.parts.js +1 -0
  157. package/dist/popover/parts.d.ts +26 -0
  158. package/dist/popover/parts.js +28 -0
  159. package/dist/prefetch.d.ts +21 -0
  160. package/dist/prefetch.js +99 -0
  161. package/dist/preview-card/index.parts.d.ts +1 -0
  162. package/dist/preview-card/index.parts.js +1 -0
  163. package/dist/project-switcher-row.d.ts +6 -0
  164. package/dist/project-switcher-row.js +16 -0
  165. package/dist/selection.d.ts +2 -0
  166. package/dist/selection.js +22 -0
  167. package/dist/seti-icons.d.ts +5 -0
  168. package/dist/seti-icons.js +445 -0
  169. package/dist/settings/ConfirmDialog.d.ts +9 -0
  170. package/dist/settings/ConfirmDialog.js +9 -0
  171. package/dist/settings/DetailHeader.d.ts +10 -0
  172. package/dist/settings/DetailHeader.js +6 -0
  173. package/dist/settings/MasterDetail.d.ts +24 -0
  174. package/dist/settings/MasterDetail.js +16 -0
  175. package/dist/settings/SettingsDialog.d.ts +46 -0
  176. package/dist/settings/SettingsDialog.js +96 -0
  177. package/dist/settings/SettingsRow.d.ts +22 -0
  178. package/dist/settings/SettingsRow.js +30 -0
  179. package/dist/settings/SettingsSection.d.ts +14 -0
  180. package/dist/settings/SettingsSection.js +8 -0
  181. package/dist/settings/index.d.ts +7 -0
  182. package/dist/settings/index.js +8 -0
  183. package/dist/settings/target.d.ts +7 -0
  184. package/dist/settings/target.js +3 -0
  185. package/dist/shortcut-hints.d.ts +38 -0
  186. package/dist/shortcut-hints.js +81 -0
  187. package/dist/shortcut.d.ts +32 -0
  188. package/dist/shortcut.js +137 -0
  189. package/dist/testing/dedupe.d.ts +1 -0
  190. package/dist/testing/dedupe.js +73 -0
  191. package/dist/testing/fake-source.d.ts +8 -0
  192. package/dist/testing/fake-source.js +15 -0
  193. package/dist/testing/index.d.ts +1 -0
  194. package/dist/testing/index.js +2 -0
  195. package/dist/theme.css +562 -0
  196. package/dist/toast-store.d.ts +38 -0
  197. package/dist/toast-store.js +61 -0
  198. package/dist/useAsyncAction.d.ts +8 -0
  199. package/dist/useAsyncAction.js +23 -0
  200. package/dist/useColumnResize.d.ts +14 -0
  201. package/dist/useColumnResize.js +49 -0
  202. package/dist/useContentSize.d.ts +5 -0
  203. package/dist/useContentSize.js +21 -0
  204. package/dist/useMeasuredWidth.d.ts +1 -0
  205. package/dist/useMeasuredWidth.js +6 -0
  206. package/dist/useNow.d.ts +3 -0
  207. package/dist/useNow.js +41 -0
  208. package/dist/wipe-split.d.ts +7 -0
  209. package/dist/wipe-split.js +20 -0
  210. package/dist/zoom.d.ts +3 -0
  211. package/dist/zoom.js +6 -0
  212. package/package.json +92 -0
  213. package/src/AccentSwatches.tsx +73 -0
  214. package/src/Banner.tsx +43 -0
  215. package/src/Button.tsx +55 -0
  216. package/src/ButtonGroup.tsx +14 -0
  217. package/src/Checkbox.tsx +44 -0
  218. package/src/ChoiceCards.tsx +108 -0
  219. package/src/CloseButton.tsx +21 -0
  220. package/src/ColorSwatch.tsx +41 -0
  221. package/src/ColumnResizeHandle.tsx +23 -0
  222. package/src/DisabledReason.tsx +23 -0
  223. package/src/DockShell.tsx +76 -0
  224. package/src/EmptyState.tsx +48 -0
  225. package/src/ErrorBoundary.tsx +116 -0
  226. package/src/Field.tsx +116 -0
  227. package/src/FileIcon.tsx +27 -0
  228. package/src/Icon.tsx +22 -0
  229. package/src/IconButton.tsx +84 -0
  230. package/src/IconPicker.tsx +257 -0
  231. package/src/Input.tsx +78 -0
  232. package/src/Kbd.tsx +67 -0
  233. package/src/ListRow.tsx +26 -0
  234. package/src/Meter.tsx +41 -0
  235. package/src/PanelEmpty.tsx +37 -0
  236. package/src/PanelHeader.tsx +31 -0
  237. package/src/Pill.tsx +61 -0
  238. package/src/ProjectSwitcher.tsx +127 -0
  239. package/src/PromptDialog.tsx +168 -0
  240. package/src/SectionLabel.tsx +20 -0
  241. package/src/SegmentBar.tsx +128 -0
  242. package/src/Segmented.tsx +51 -0
  243. package/src/Select.tsx +135 -0
  244. package/src/Separator.tsx +15 -0
  245. package/src/ShortcutHints.tsx +118 -0
  246. package/src/Skeleton.tsx +13 -0
  247. package/src/SlidingColumn.tsx +91 -0
  248. package/src/Spinner.tsx +32 -0
  249. package/src/Stepper.tsx +67 -0
  250. package/src/Surface.tsx +20 -0
  251. package/src/Switch.tsx +32 -0
  252. package/src/TextMenu.tsx +59 -0
  253. package/src/Tile.tsx +49 -0
  254. package/src/Toasts.tsx +151 -0
  255. package/src/Tooltip.tsx +70 -0
  256. package/src/UIProvider.tsx +41 -0
  257. package/src/Waveform.tsx +199 -0
  258. package/src/Wipe.tsx +99 -0
  259. package/src/ZoomControls.tsx +99 -0
  260. package/src/class-name.ts +8 -0
  261. package/src/clipboard.ts +17 -0
  262. package/src/context-menu/index.parts.ts +21 -0
  263. package/src/dialog/index.parts.ts +10 -0
  264. package/src/dialog/parts.tsx +112 -0
  265. package/src/dialog-layer.ts +31 -0
  266. package/src/error-boundary.ts +40 -0
  267. package/src/error-message.ts +5 -0
  268. package/src/field-context.ts +12 -0
  269. package/src/file-icon.ts +52 -0
  270. package/src/floating.ts +9 -0
  271. package/src/format/datetime.ts +63 -0
  272. package/src/format/duration.ts +83 -0
  273. package/src/format/index.ts +20 -0
  274. package/src/format/locale.ts +153 -0
  275. package/src/format/number.ts +87 -0
  276. package/src/format/regions.ts +20 -0
  277. package/src/format/time-zone.ts +2 -0
  278. package/src/icon-button-size.ts +6 -0
  279. package/src/icon-picker.ts +112 -0
  280. package/src/index.ts +103 -0
  281. package/src/lazy.tsx +124 -0
  282. package/src/locales/en.json +59 -0
  283. package/src/locales/nl.json +59 -0
  284. package/src/locales.ts +21 -0
  285. package/src/menu/index.parts.ts +20 -0
  286. package/src/menu/parts.tsx +296 -0
  287. package/src/merge-refs.ts +14 -0
  288. package/src/modality.ts +37 -0
  289. package/src/platform.ts +10 -0
  290. package/src/popover/index.parts.ts +8 -0
  291. package/src/popover/parts.tsx +119 -0
  292. package/src/prefetch.ts +113 -0
  293. package/src/preview-card/index.parts.ts +1 -0
  294. package/src/project-switcher-row.tsx +51 -0
  295. package/src/selection.ts +24 -0
  296. package/src/seti-icons.ts +451 -0
  297. package/src/settings/ConfirmDialog.tsx +30 -0
  298. package/src/settings/DetailHeader.tsx +25 -0
  299. package/src/settings/MasterDetail.tsx +67 -0
  300. package/src/settings/SettingsDialog.tsx +342 -0
  301. package/src/settings/SettingsRow.tsx +89 -0
  302. package/src/settings/SettingsSection.tsx +46 -0
  303. package/src/settings/index.ts +16 -0
  304. package/src/settings/target.ts +12 -0
  305. package/src/shortcut-hints.ts +117 -0
  306. package/src/shortcut.ts +170 -0
  307. package/src/testing/dedupe.ts +93 -0
  308. package/src/testing/fake-source.ts +21 -0
  309. package/src/testing/index.ts +3 -0
  310. package/src/theme.css +562 -0
  311. package/src/toast-store.ts +120 -0
  312. package/src/useAsyncAction.ts +32 -0
  313. package/src/useColumnResize.ts +75 -0
  314. package/src/useContentSize.ts +27 -0
  315. package/src/useMeasuredWidth.ts +7 -0
  316. package/src/useNow.ts +49 -0
  317. package/src/wipe-split.ts +24 -0
  318. package/src/zoom.ts +9 -0
@@ -0,0 +1,38 @@
1
+ import { type StoreApi, type UseBoundStore } from 'zustand';
2
+ import type { Shortcut } from './shortcut.ts';
3
+ export declare const SUCCESS_MS = 4000;
4
+ export declare const UNDO_MS = 8000;
5
+ export type ToastKind = 'progress' | 'success' | 'error' | 'deleted';
6
+ export interface ToastAction {
7
+ label: string;
8
+ run(): void;
9
+ shortcut?: Shortcut;
10
+ }
11
+ export interface ToastDeadline {
12
+ start: number;
13
+ end: number;
14
+ }
15
+ export declare const elapsedOf: (deadline: ToastDeadline, now: number) => number;
16
+ export interface Toast {
17
+ id: string;
18
+ title: string;
19
+ description?: string;
20
+ kind: ToastKind;
21
+ action?: ToastAction;
22
+ actions?: ToastAction[];
23
+ persist?: boolean;
24
+ onClose?: () => void;
25
+ deadline?: ToastDeadline;
26
+ }
27
+ export type ToastInput<T extends Toast = Toast> = Omit<T, 'id' | 'deadline'> & {
28
+ id?: string;
29
+ };
30
+ export type ToastPatch<T extends Toast = Toast> = Partial<Omit<T, 'id' | 'deadline'>>;
31
+ export interface ToastStore<T extends Toast = Toast> {
32
+ toasts: T[];
33
+ show(toast: ToastInput<T>): string;
34
+ update(id: string, patch: ToastPatch<T>): void;
35
+ dismiss(id: string): void;
36
+ }
37
+ export type ToastStoreHook<T extends Toast = Toast> = UseBoundStore<StoreApi<ToastStore<T>>>;
38
+ export declare const createToastStore: <T extends Toast = Toast>() => ToastStoreHook<T>;
@@ -0,0 +1,61 @@
1
+ import { create } from 'zustand';
2
+ // How long a toast that went well stays up; a failure waits for the person instead.
3
+ export const SUCCESS_MS = 4000;
4
+ // How long a deletion can still be taken back.
5
+ export const UNDO_MS = 8000;
6
+ /* How far into its lifetime a toast is at `now`, held inside that lifetime. */
7
+ export const elapsedOf = (deadline, now) => Math.min(Math.max(now - deadline.start, 0), deadline.end - deadline.start);
8
+ /*
9
+ * The one place an app says how something went that no surface on screen shows. A long action keeps
10
+ * one id from progress to its outcome, so a person never watches two cards for one thing.
11
+ */
12
+ export const createToastStore = () => {
13
+ const timers = new Map();
14
+ let counter = 0;
15
+ return create((set, get) => {
16
+ const stop = (id) => {
17
+ clearTimeout(timers.get(id));
18
+ timers.delete(id);
19
+ };
20
+ /* A toast that went well takes itself away, and so does the offer to undo; a failure and a running action stay. */
21
+ const schedule = (id, kind, persist) => {
22
+ stop(id);
23
+ const lifetime = kind === 'deleted' ? UNDO_MS : kind === 'success' && !persist ? SUCCESS_MS : null;
24
+ if (lifetime === null) {
25
+ return undefined;
26
+ }
27
+ timers.set(id, setTimeout(() => {
28
+ timers.delete(id);
29
+ get().dismiss(id);
30
+ }, lifetime));
31
+ const start = Date.now();
32
+ return { start, end: start + lifetime };
33
+ };
34
+ return {
35
+ toasts: [],
36
+ show(toast) {
37
+ counter += 1;
38
+ const id = toast.id ?? `toast-${counter}`;
39
+ const next = { ...toast, id, deadline: schedule(id, toast.kind, toast.persist === true) };
40
+ const toasts = get().toasts;
41
+ set({ toasts: toasts.some((entry) => entry.id === id) ? toasts.map((entry) => (entry.id === id ? next : entry)) : [...toasts, next] });
42
+ return id;
43
+ },
44
+ update(id, patch) {
45
+ const current = get().toasts.find((entry) => entry.id === id);
46
+ if (current === undefined) {
47
+ return;
48
+ }
49
+ const merged = { ...current, ...patch };
50
+ const next = { ...merged, deadline: schedule(id, merged.kind, merged.persist === true) };
51
+ set({ toasts: get().toasts.map((entry) => (entry.id === id ? next : entry)) });
52
+ },
53
+ dismiss(id) {
54
+ stop(id);
55
+ const gone = get().toasts.find((entry) => entry.id === id);
56
+ set({ toasts: get().toasts.filter((entry) => entry.id !== id) });
57
+ gone?.onClose?.();
58
+ }
59
+ };
60
+ });
61
+ };
@@ -0,0 +1,8 @@
1
+ export interface AsyncAction {
2
+ busy: boolean;
3
+ failure: string | null;
4
+ run(work: () => Promise<unknown>): Promise<boolean>;
5
+ fail(message: string): void;
6
+ clear(): void;
7
+ }
8
+ export declare function useAsyncAction(fallback?: string): AsyncAction;
@@ -0,0 +1,23 @@
1
+ import { useState } from 'react';
2
+ import { messageOf } from './error-message.js';
3
+ /* One step a surface waits on: the button goes quiet while it runs and the reason stays on screen when it does not. */
4
+ export function useAsyncAction(fallback) {
5
+ const [busy, setBusy] = useState(false);
6
+ const [failure, setFailure] = useState(null);
7
+ const run = async (work) => {
8
+ setBusy(true);
9
+ setFailure(null);
10
+ try {
11
+ await work();
12
+ return true;
13
+ }
14
+ catch (e) {
15
+ setFailure(messageOf(e, fallback));
16
+ return false;
17
+ }
18
+ finally {
19
+ setBusy(false);
20
+ }
21
+ };
22
+ return { busy, failure, run, fail: setFailure, clear: () => setFailure(null) };
23
+ }
@@ -0,0 +1,14 @@
1
+ import type { PointerEvent as ReactPointerEvent, RefObject } from 'react';
2
+ export type ColumnEdge = 'left' | 'right' | 'top' | 'bottom';
3
+ export interface ColumnResizeOptions {
4
+ size: number;
5
+ min: number;
6
+ from: ColumnEdge;
7
+ max?(): number;
8
+ onSize(size: number): void;
9
+ }
10
+ export interface ColumnResize {
11
+ startResize(event: ReactPointerEvent<HTMLElement>): void;
12
+ }
13
+ export declare const clampColumnSize: (options: Pick<ColumnResizeOptions, 'min' | 'max'>, size: number) => number;
14
+ export declare const useColumnResize: (ref: RefObject<HTMLElement | null>, options: ColumnResizeOptions) => ColumnResize;
@@ -0,0 +1,49 @@
1
+ export const clampColumnSize = (options, size) => {
2
+ const max = Math.max(options.min, options.max?.() ?? Number.MAX_SAFE_INTEGER);
3
+ return Math.max(options.min, Math.min(Math.round(size), max));
4
+ };
5
+ /* Where the pointer sits on the axis this edge belongs to. */
6
+ const along = (from, event) => (from === 'left' || from === 'right' ? event.clientX : event.clientY);
7
+ /* The edge the column is pinned to, in page coordinates, or the window's own when there is no box yet. */
8
+ const anchorOf = (from, rect) => {
9
+ switch (from) {
10
+ case 'left':
11
+ return rect?.left ?? 0;
12
+ case 'right':
13
+ return rect?.right ?? window.innerWidth;
14
+ case 'top':
15
+ return rect?.top ?? 0;
16
+ case 'bottom':
17
+ return rect?.bottom ?? window.innerHeight;
18
+ }
19
+ };
20
+ /*
21
+ * One resizable column or row: a handle that drags the size its owner keeps. The handle takes the
22
+ * pointer capture, so the drag survives leaving the few pixels it is wide, and `[data-resizing]` on
23
+ * the column turns off any transition on that size for as long as it lasts.
24
+ */
25
+ export const useColumnResize = (ref, options) => {
26
+ const startResize = (event) => {
27
+ event.preventDefault();
28
+ const handle = event.currentTarget;
29
+ const column = ref.current;
30
+ const anchor = anchorOf(options.from, column?.getBoundingClientRect());
31
+ /* The far edge grows as the pointer comes towards it; the near edge grows as it goes away. */
32
+ const growsTowardsAnchor = options.from === 'right' || options.from === 'bottom';
33
+ column?.setAttribute('data-resizing', 'true');
34
+ const onMove = (move) => {
35
+ const reach = along(options.from, move);
36
+ options.onSize(clampColumnSize(options, growsTowardsAnchor ? anchor - reach : reach - anchor));
37
+ };
38
+ const onUp = () => {
39
+ handle.removeEventListener('pointermove', onMove);
40
+ handle.removeEventListener('pointerup', onUp);
41
+ handle.releasePointerCapture(event.pointerId);
42
+ column?.removeAttribute('data-resizing');
43
+ };
44
+ handle.setPointerCapture(event.pointerId);
45
+ handle.addEventListener('pointermove', onMove);
46
+ handle.addEventListener('pointerup', onUp);
47
+ };
48
+ return { startResize };
49
+ };
@@ -0,0 +1,5 @@
1
+ export interface ContentSize {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ export declare function useContentSize(): [(node: HTMLElement | null) => void, ContentSize];
@@ -0,0 +1,21 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ /* Whole pixels, so a frame fitted into the space lands on them too. */
3
+ export function useContentSize() {
4
+ const [size, setSize] = useState({ width: 0, height: 0 });
5
+ const observer = useRef(null);
6
+ useEffect(() => () => observer.current?.disconnect(), []);
7
+ const measure = useCallback((node) => {
8
+ observer.current?.disconnect();
9
+ if (node === null) {
10
+ return;
11
+ }
12
+ const update = (width, height) => {
13
+ const next = { width: Math.round(width), height: Math.round(height) };
14
+ setSize((current) => (current.width === next.width && current.height === next.height ? current : next));
15
+ };
16
+ update(node.clientWidth, node.clientHeight);
17
+ observer.current = new ResizeObserver(([entry]) => update(entry.contentRect.width, entry.contentRect.height));
18
+ observer.current.observe(node);
19
+ }, []);
20
+ return [measure, size];
21
+ }
@@ -0,0 +1 @@
1
+ export declare function useMeasuredWidth(): [(node: HTMLElement | null) => void, number];
@@ -0,0 +1,6 @@
1
+ import { useContentSize } from './useContentSize.js';
2
+ /* A plot draws in real pixels rather than a stretched view box, so what it draws lands on whole ones. */
3
+ export function useMeasuredWidth() {
4
+ const [measure, size] = useContentSize();
5
+ return [measure, size.width];
6
+ }
@@ -0,0 +1,3 @@
1
+ import { type RefObject } from 'react';
2
+ export declare const useNow: (intervalMs: number, ticking?: boolean) => number;
3
+ export declare const useTickingText: (render: () => string, intervalMs?: number) => RefObject<HTMLSpanElement | null>;
package/dist/useNow.js ADDED
@@ -0,0 +1,41 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ /*
3
+ * The clock a surface reads to say how long something has been running. One timer for the whole
4
+ * surface, ticking only while there is something to count, and starting at the time it was mounted
5
+ * so a panel that comes back mid-session does not read zero for a second first.
6
+ */
7
+ export const useNow = (intervalMs, ticking = true) => {
8
+ const [now, setNow] = useState(Date.now);
9
+ useEffect(() => {
10
+ if (!ticking) {
11
+ return;
12
+ }
13
+ const timer = window.setInterval(() => setNow(Date.now()), intervalMs);
14
+ return () => window.clearInterval(timer);
15
+ }, [intervalMs, ticking]);
16
+ return now;
17
+ };
18
+ /*
19
+ * A timer on one line of a long thread. The text is written straight into the node, because a chat
20
+ * that re-renders every second while an agent works re-renders every row it is drawing with it.
21
+ */
22
+ export const useTickingText = (render, intervalMs = 1000) => {
23
+ const ref = useRef(null);
24
+ const latest = useRef(render);
25
+ const tick = useCallback(() => {
26
+ if (ref.current !== null) {
27
+ ref.current.textContent = latest.current();
28
+ }
29
+ }, []);
30
+ useEffect(() => {
31
+ const timer = window.setInterval(tick, intervalMs);
32
+ return () => window.clearInterval(timer);
33
+ }, [intervalMs, tick]);
34
+ /* After every render as well, so the first one writes the line and a language or region change
35
+ lands on it without waiting for the next tick. */
36
+ useEffect(() => {
37
+ latest.current = render;
38
+ tick();
39
+ });
40
+ return ref;
41
+ };
@@ -0,0 +1,7 @@
1
+ export declare const WIPE_KEY_STEP = 0.02;
2
+ export declare const clampSplit: (split: number) => number;
3
+ export declare const splitAt: (clientX: number, frame: {
4
+ left: number;
5
+ width: number;
6
+ }) => number | null;
7
+ export declare const splitForKey: (split: number, key: string) => number | null;
@@ -0,0 +1,20 @@
1
+ /* One press of an arrow moves the handle this share of the frame. */
2
+ export const WIPE_KEY_STEP = 0.02;
3
+ export const clampSplit = (split) => Math.min(1, Math.max(0, split));
4
+ /* A frame without a width has no place to point at, so the split stays where it is. */
5
+ export const splitAt = (clientX, frame) => frame.width > 0 ? clampSplit((clientX - frame.left) / frame.width) : null;
6
+ /* Null for a key the handle does not answer, so it goes on to whatever is around it. */
7
+ export const splitForKey = (split, key) => {
8
+ switch (key) {
9
+ case 'ArrowLeft':
10
+ return clampSplit(split - WIPE_KEY_STEP);
11
+ case 'ArrowRight':
12
+ return clampSplit(split + WIPE_KEY_STEP);
13
+ case 'Home':
14
+ return 0;
15
+ case 'End':
16
+ return 1;
17
+ default:
18
+ return null;
19
+ }
20
+ };
package/dist/zoom.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare const ZOOM_PRESETS: readonly number[];
2
+ export declare const activeZoomPreset: (zoom: number, presets: readonly number[]) => number | null;
3
+ export declare const steppedZoom: (zoom: number, by: number) => number;
package/dist/zoom.js ADDED
@@ -0,0 +1,6 @@
1
+ /* The percentages a zoom menu offers when the app names none. */
2
+ export const ZOOM_PRESETS = [25, 50, 75, 100, 150, 200];
3
+ /* Compared on the rounded percent the readout shows, so 0.999 still ticks 100%. */
4
+ export const activeZoomPreset = (zoom, presets) => presets.find((preset) => preset === Math.round(zoom * 100)) ?? null;
5
+ /* Ten percent a click, on the readout's own rounded percent, so + and - come back where they were. */
6
+ export const steppedZoom = (zoom, by) => Math.round(zoom * 100 + by) / 100;
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@adecore/ui",
3
+ "version": "0.14.0",
4
+ "description": "React components, a theme, formatters and a settings dialog for desktop-grade apps on React 19, Base UI and Tailwind 4.",
5
+ "license": "MIT",
6
+ "author": "Bas Milius",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/basmilius/adecore.git",
10
+ "directory": "packages/ui"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/basmilius/adecore/issues"
14
+ },
15
+ "homepage": "https://adecore.dev/ui/",
16
+ "type": "module",
17
+ "sideEffects": [
18
+ "*.css",
19
+ "./src/testing/dedupe.ts",
20
+ "./dist/testing/dedupe.js"
21
+ ],
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "!src/**/*.test.ts",
26
+ "!src/**/*.test.tsx",
27
+ "!src/**/__snapshots__"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "source": "./src/index.ts",
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./settings": {
36
+ "source": "./src/settings/index.ts",
37
+ "types": "./dist/settings/index.d.ts",
38
+ "default": "./dist/settings/index.js"
39
+ },
40
+ "./format": {
41
+ "source": "./src/format/index.ts",
42
+ "types": "./dist/format/index.d.ts",
43
+ "default": "./dist/format/index.js"
44
+ },
45
+ "./testing": {
46
+ "source": "./src/testing/index.ts",
47
+ "types": "./dist/testing/index.d.ts",
48
+ "default": "./dist/testing/index.js"
49
+ },
50
+ "./testing/dedupe": {
51
+ "source": "./src/testing/dedupe.ts",
52
+ "types": "./dist/testing/dedupe.d.ts",
53
+ "default": "./dist/testing/dedupe.js"
54
+ },
55
+ "./theme.css": {
56
+ "source": "./src/theme.css",
57
+ "style": "./src/theme.css",
58
+ "default": "./dist/theme.css"
59
+ }
60
+ },
61
+ "scripts": {
62
+ "build": "bun scripts/build.ts",
63
+ "typecheck": "tsc --noEmit",
64
+ "test": "bun test"
65
+ },
66
+ "dependencies": {
67
+ "@base-ui-components/react": "^1.0.0-rc.0",
68
+ "@pierre/trees": "1.0.0-beta.6",
69
+ "clsx": "^2.1.1",
70
+ "lucide-react": "^1.47.0",
71
+ "zustand": "^5.0.15"
72
+ },
73
+ "peerDependencies": {
74
+ "i18next": "^26.4.2",
75
+ "react": "^19.3.0",
76
+ "react-dom": "^19.3.0",
77
+ "react-i18next": "^17.0.15"
78
+ },
79
+ "devDependencies": {
80
+ "@types/bun": "^1.4.2",
81
+ "@types/opentype.js": "^1.3.10",
82
+ "@types/react": "^19.3.0",
83
+ "@types/react-dom": "^19.3.0",
84
+ "i18next": "^26.4.2",
85
+ "opentype.js": "2.0.0",
86
+ "oxc-parser": "^0.151.0",
87
+ "react": "^19.3.0",
88
+ "react-dom": "^19.3.0",
89
+ "react-i18next": "^17.0.15",
90
+ "typescript": "^7.0.2"
91
+ }
92
+ }
@@ -0,0 +1,73 @@
1
+ import type { Ref } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Ellipsis } from 'lucide-react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { ColorSwatch } from './ColorSwatch.tsx';
6
+ import { Icon } from './Icon.tsx';
7
+ import { MenuPopup, MenuRadioGroup, MenuRadioItem, MenuRoot, MenuTrigger } from './menu/parts.tsx';
8
+ import { Tooltip } from './Tooltip.tsx';
9
+
10
+ export interface AccentSwatchesProps<Id extends string> {
11
+ value: string | undefined;
12
+ onValueChange(id: Id): void;
13
+ label: string;
14
+ /* Every color on offer, in the order of the list behind the featured ones. */
15
+ accents: readonly { id: Id; color: string }[];
16
+ /* The few drawn in the open, in the order they stand. */
17
+ featured: readonly Id[];
18
+ labelOf(id: Id): string;
19
+ className?: string;
20
+ ref?: Ref<HTMLDivElement>;
21
+ }
22
+
23
+ /*
24
+ * An accent, a few colors at a time. A palette is more than a settings row can carry, so the list
25
+ * behind them holds all of them, the featured ones included; the trigger wears the chosen color
26
+ * itself whenever that color is not one of the few. A value that is no accent at all picks nothing.
27
+ */
28
+ export function AccentSwatches<Id extends string>({ value, onValueChange, label, accents, featured, labelOf, className, ref }: AccentSwatchesProps<Id>) {
29
+ const { t } = useTranslation('ui');
30
+ const isFeatured = (id: string): boolean => featured.some((entry) => entry === id);
31
+ const colorOf = (id: string): string | undefined => accents.find((entry) => entry.id === id)?.color;
32
+ const shown = accents.filter((entry) => isFeatured(entry.id)).sort((a, b) => featured.indexOf(a.id) - featured.indexOf(b.id));
33
+ const hidden = value !== undefined && !isFeatured(value) && colorOf(value) !== undefined ? value : null;
34
+ return (
35
+ <div ref={ref} className={clsx('flex items-center gap-2', className)} role="radiogroup" aria-label={label}>
36
+ {shown.map((entry) => (
37
+ <Tooltip key={entry.id} label={labelOf(entry.id)}>
38
+ <ColorSwatch
39
+ role="radio"
40
+ aria-checked={value === entry.id}
41
+ aria-label={labelOf(entry.id)}
42
+ color={entry.color}
43
+ picked={value === entry.id}
44
+ onClick={() => onValueChange(entry.id)}
45
+ />
46
+ </Tooltip>
47
+ ))}
48
+ <MenuRoot>
49
+ <Tooltip label={t('accent.more')}>
50
+ <ColorSwatch
51
+ render={<MenuTrigger />}
52
+ aria-label={t('accent.more')}
53
+ color={hidden === null ? undefined : colorOf(hidden)}
54
+ picked={hidden !== null}
55
+ >
56
+ {/* Undefined leaves the tick of the picked color; otherwise this says there is more behind the swatch. */}
57
+ {hidden === null ? <Icon icon={Ellipsis} size={12} /> : undefined}
58
+ </ColorSwatch>
59
+ </Tooltip>
60
+ <MenuPopup align="end" className="max-h-96 overflow-y-auto">
61
+ <MenuRadioGroup value={value ?? null} onValueChange={(id: Id) => onValueChange(id)}>
62
+ {accents.map((entry) => (
63
+ <MenuRadioItem key={entry.id} value={entry.id}>
64
+ <span className="h-3 w-3 shrink-0 rounded-full" style={{ background: entry.color }} aria-hidden />
65
+ <span className="grow">{labelOf(entry.id)}</span>
66
+ </MenuRadioItem>
67
+ ))}
68
+ </MenuRadioGroup>
69
+ </MenuPopup>
70
+ </MenuRoot>
71
+ </div>
72
+ );
73
+ }
package/src/Banner.tsx ADDED
@@ -0,0 +1,43 @@
1
+ import type { ReactNode, Ref } from 'react';
2
+ import clsx from 'clsx';
3
+ import type { LucideIcon } from 'lucide-react';
4
+ import { Icon } from './Icon.tsx';
5
+ import { Surface } from './Surface.tsx';
6
+
7
+ /* The mark in front of the line: a decision to make, a failure to read, or something that happened. */
8
+ export type BannerTone = 'attention' | 'error' | 'neutral';
9
+
10
+ const TONE: Record<BannerTone, string> = {
11
+ attention: 'text-status-needs-you',
12
+ error: 'text-status-error',
13
+ neutral: 'text-text-muted'
14
+ };
15
+
16
+ export interface BannerProps {
17
+ icon: LucideIcon;
18
+ tone: BannerTone;
19
+ message: ReactNode;
20
+ /* The buttons that answer the line. */
21
+ children?: ReactNode;
22
+ /* Where the strip stands; by default centered along the top of the positioned parent. */
23
+ className?: string;
24
+ ref?: Ref<HTMLDivElement>;
25
+ }
26
+
27
+ /*
28
+ * The strip over a view: one line and the buttons that answer it. It is where anything that waits
29
+ * for a person goes (a conflict, a failure, a request), because a corner toast is read after the fact
30
+ * and a decision has to be where the eyes already are. One at a time: the slot is one row and the
31
+ * caller picks what stands in it.
32
+ */
33
+ export function Banner({ icon, tone, message, children, className = 'absolute inset-x-0 top-3 z-20', ref }: BannerProps) {
34
+ return (
35
+ <div ref={ref} className={clsx('pointer-events-auto flex justify-center px-4', className)} role="status" aria-live="polite">
36
+ <Surface className="flex max-w-[640px] items-center gap-3 rounded-lg px-3 py-2 text-sm text-text">
37
+ <Icon icon={icon} size={16} className={clsx('shrink-0', TONE[tone])} />
38
+ <span className="grow">{message}</span>
39
+ {children}
40
+ </Surface>
41
+ </div>
42
+ );
43
+ }
package/src/Button.tsx ADDED
@@ -0,0 +1,55 @@
1
+ import type { AnchorHTMLAttributes, ComponentProps, Ref } from 'react';
2
+ import clsx from 'clsx';
3
+
4
+ export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'danger-outline' | 'positive' | 'inverse';
5
+ export type ButtonSize = 'xs' | 'sm' | 'md';
6
+
7
+ const VARIANT: Record<ButtonVariant, string> = {
8
+ // A filled button darkens under the pointer. Its color is the meaning, so no token of its own for hover.
9
+ primary: 'bg-accent text-accent-text hover:brightness-90 disabled:hover:brightness-100',
10
+ secondary: 'border border-border bg-surface-raised text-text hover:bg-surface-hover disabled:hover:bg-surface-raised',
11
+ ghost: 'text-text-muted hover:bg-surface-hover hover:text-text',
12
+ danger: 'bg-status-error text-accent-text hover:brightness-90 disabled:hover:brightness-100',
13
+ // A step that forgets something rather than destroys it: softer than the filled button of a deletion that cannot come back.
14
+ 'danger-outline': 'border border-status-error/35 text-status-error hover:bg-status-error/10 disabled:hover:bg-transparent',
15
+ positive: 'bg-positive text-positive-text hover:brightness-90 disabled:hover:brightness-100',
16
+ // Dark on a light theme, light on a dark one, matching what Apple's and GitHub's sign-in buttons ask for.
17
+ inverse: 'bg-text text-bg hover:opacity-90 disabled:hover:opacity-100'
18
+ };
19
+
20
+ /* 28 and 32 pixels are the compact and the normal control; 24 fits a header of 32. */
21
+ const SIZE: Record<ButtonSize, string> = {
22
+ xs: 'h-6 px-2',
23
+ sm: 'h-7 px-2.5',
24
+ md: 'h-8 px-3'
25
+ };
26
+
27
+ export interface ButtonProps extends ComponentProps<'button'> {
28
+ variant?: ButtonVariant;
29
+ size?: ButtonSize;
30
+ /* Makes it a link that looks like a button, a real anchor, so it opens the way links open. */
31
+ href?: string;
32
+ }
33
+
34
+ /* Every button with a word in it. A button that is only an icon is an `IconButton`, which is a square, not a label. */
35
+ export function Button({ variant = 'ghost', size = 'md', className, href, type = 'button', ref, ...rest }: ButtonProps) {
36
+ const classes = clsx(
37
+ 'inline-flex shrink-0 items-center justify-center gap-1.5 rounded-md text-xs font-medium disabled:opacity-50',
38
+ VARIANT[variant],
39
+ SIZE[size],
40
+ className
41
+ );
42
+ if (href) {
43
+ return (
44
+ <a
45
+ ref={ref as Ref<HTMLAnchorElement>}
46
+ className={classes}
47
+ href={href}
48
+ target="_blank"
49
+ rel="noreferrer"
50
+ {...(rest as AnchorHTMLAttributes<HTMLAnchorElement>)}
51
+ />
52
+ );
53
+ }
54
+ return <button ref={ref} type={type} className={classes} {...rest} />;
55
+ }
@@ -0,0 +1,14 @@
1
+ import clsx from 'clsx';
2
+ import { useRender } from '@base-ui-components/react/use-render';
3
+
4
+ export type ButtonGroupProps = useRender.ComponentProps<'div'>;
5
+
6
+ /* Icon buttons that belong together sit 1px apart; groups keep the wider gap of their container. */
7
+ export function ButtonGroup({ render, className, ref, ...props }: ButtonGroupProps) {
8
+ return useRender({
9
+ render,
10
+ ref,
11
+ defaultTagName: 'div',
12
+ props: { ...props, className: clsx('inline-flex items-center gap-px', className) }
13
+ });
14
+ }
@@ -0,0 +1,44 @@
1
+ import type { Ref } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Check, Minus } from 'lucide-react';
4
+ import { Checkbox as BaseCheckbox } from '@base-ui-components/react/checkbox';
5
+ import { Icon } from './Icon.tsx';
6
+
7
+ export interface CheckboxProps {
8
+ checked: boolean;
9
+ onCheckedChange(checked: boolean): void;
10
+ /* The accessible name; a `<label>` around the box and its text usually carries the visible one. */
11
+ label: string;
12
+ /* Some of the items it stands for are picked and some are not, as on a box that picks every row of a list. */
13
+ indeterminate?: boolean;
14
+ disabled?: boolean;
15
+ className?: string;
16
+ ref?: Ref<HTMLButtonElement>;
17
+ }
18
+
19
+ /* Picked or not, as one item out of a list or an option that waits for a Save button. */
20
+ export function Checkbox({ checked, onCheckedChange, label, indeterminate = false, disabled, className, ref }: CheckboxProps) {
21
+ return (
22
+ <BaseCheckbox.Root
23
+ ref={ref}
24
+ checked={checked}
25
+ indeterminate={indeterminate}
26
+ onCheckedChange={(next) => onCheckedChange(next)}
27
+ aria-label={label}
28
+ disabled={disabled}
29
+ className={clsx(
30
+ 'flex size-4 shrink-0 items-center justify-center rounded-sm border border-border-strong bg-surface text-accent-text transition-colors',
31
+ 'data-checked:border-accent data-checked:bg-accent data-indeterminate:border-accent data-indeterminate:bg-accent data-disabled:opacity-50',
32
+ className
33
+ )}
34
+ >
35
+ <BaseCheckbox.Indicator
36
+ render={(props, state) => (
37
+ <span {...props}>
38
+ <Icon icon={state.indeterminate ? Minus : Check} size={12} />
39
+ </span>
40
+ )}
41
+ />
42
+ </BaseCheckbox.Root>
43
+ );
44
+ }