@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,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button } from './Button.js';
5
+ import { DialogDescription, DialogFooter, DialogPopup, DialogRoot, DialogTitle } from './dialog/parts.js';
6
+ import { Field, FormError } from './Field.js';
7
+ import { Icon } from './Icon.js';
8
+ import { Input, TextArea } from './Input.js';
9
+ import { useAsyncAction } from './useAsyncAction.js';
10
+ /*
11
+ * The one dialog a question is asked in: a name to type, or a warning to agree with. Every confirm
12
+ * and every rename shares it, so none of them grows a layout of its own. It never closes itself,
13
+ * because what a confirm leads to is the caller's business: an action that asks a second question
14
+ * would be shut out by a close of its own.
15
+ */
16
+ export function PromptDialog({ open, title, titleIcon, description, field, area, confirmLabel, confirmBusyLabel, confirmIcon, danger = false, busy = false, allowEmpty = false, confirmDisabled = false, secondary, nested = false, children, onConfirm, fallbackMessage, onOpenChange, className, ref }) {
17
+ const { t } = useTranslation('ui');
18
+ /* The dialog stays mounted between questions, so every opening starts from what it was handed.
19
+ The token is what it was handed, so a new question resets the fields in the same render that
20
+ shows it and not in a second one after. */
21
+ const token = open ? `${field?.initial ?? ''}\u0000${area?.initial ?? ''}` : '';
22
+ const [draft, setDraft] = useState({ token, value: field?.initial ?? '', body: area?.initial ?? '' });
23
+ const step = useAsyncAction(fallbackMessage);
24
+ if (draft.token !== token) {
25
+ setDraft({ token, value: field?.initial ?? '', body: area?.initial ?? '' });
26
+ step.clear();
27
+ }
28
+ const value = draft.value;
29
+ const body = draft.body;
30
+ const working = busy || step.busy;
31
+ const setValue = (next) => setDraft({ ...draft, value: next });
32
+ const setBody = (next) => setDraft({ ...draft, body: next });
33
+ const submit = () => {
34
+ if (!working && !confirmDisabled && (field === undefined || allowEmpty || value.trim() !== '')) {
35
+ void step.run(async () => await onConfirm(value.trim(), body.trim()));
36
+ }
37
+ };
38
+ const close = () => onOpenChange(false);
39
+ return (_jsx(DialogRoot, { open: open, onOpenChange: (next) => !next && close(), children: _jsxs(DialogPopup, { ref: ref, size: "sm", nested: nested, className: className, children: [_jsxs(DialogTitle, { className: "flex items-center gap-2 break-words", children: [titleIcon && _jsx(Icon, { icon: titleIcon, size: 16 }), title] }), description !== undefined && _jsx(DialogDescription, { className: "mt-1 break-words", children: description }), field !== undefined && (_jsx(Field, { label: field.label, className: "mt-4", children: _jsx(Input, { autoFocus: true, mono: field.mono === true, "aria-label": field.ariaLabel, spellCheck: false, placeholder: field.placeholder, maxLength: field.maxLength, value: value, onChange: (event) => setValue(event.target.value), onKeyDown: (event) => {
40
+ // Whatever listens on the window has keys of its own, and a name may hold any of them.
41
+ event.stopPropagation();
42
+ if (event.key === 'Enter') {
43
+ event.preventDefault();
44
+ submit();
45
+ }
46
+ } }) })), area !== undefined && (_jsx(Field, { label: area.label, className: "mt-3", children: _jsx(TextArea, { rows: 5, spellCheck: false, placeholder: area.placeholder, value: body, onChange: (event) => setBody(event.target.value) }) })), children, step.failure !== null && _jsx(FormError, { className: "mt-2 break-words", children: step.failure }), _jsxs(DialogFooter, { children: [_jsx(Button, { onClick: close, children: t('action.cancel') }), secondary !== undefined && (_jsx(Button, { disabled: working, onClick: secondary.onClick, children: secondary.label })), _jsxs(Button, { variant: danger ? 'danger' : 'primary', disabled: working || confirmDisabled || (field !== undefined && !allowEmpty && value.trim() === ''), onClick: submit, children: [confirmIcon && _jsx(Icon, { icon: confirmIcon, size: 12 }), working && confirmBusyLabel !== undefined ? confirmBusyLabel : confirmLabel] })] })] }) }));
47
+ }
@@ -0,0 +1,3 @@
1
+ import { useRender } from '@base-ui-components/react/use-render';
2
+ export type SectionLabelProps = useRender.ComponentProps<'span'>;
3
+ export declare function SectionLabel({ render, className, ref, ...props }: SectionLabelProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,16 @@
1
+ import clsx from 'clsx';
2
+ import { useRender } from '@base-ui-components/react/use-render';
3
+ /* A label takes the line height of the row it sits in, which is why the size carries `/[inherit]`. */
4
+ const SECTION_LABEL = 'text-xs/[inherit] font-medium text-text-faint';
5
+ /*
6
+ * The small label above a group of rows outside a popup: a sidebar's groups, a palette's sections,
7
+ * the name of a field. Always sentence case. A `<span>` unless `render` makes it a heading or a label.
8
+ */
9
+ export function SectionLabel({ render, className, ref, ...props }) {
10
+ return useRender({
11
+ render,
12
+ ref,
13
+ defaultTagName: 'span',
14
+ props: { ...props, className: clsx(SECTION_LABEL, className) }
15
+ });
16
+ }
@@ -0,0 +1,18 @@
1
+ import { type Ref } from 'react';
2
+ export interface SegmentBarPart {
3
+ value: number;
4
+ start?: number;
5
+ label?: string;
6
+ color?: string;
7
+ current?: boolean;
8
+ }
9
+ export interface SegmentBarProps {
10
+ parts: readonly SegmentBarPart[];
11
+ range?: readonly [from: number, to: number];
12
+ size?: 'sm' | 'md';
13
+ onSelect?(index: number): void;
14
+ label?: string;
15
+ className?: string;
16
+ ref?: Ref<HTMLOListElement>;
17
+ }
18
+ export declare function SegmentBar({ parts, range, size, onSelect, label, className, ref }: SegmentBarProps): import("react").JSX.Element;
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import clsx from 'clsx';
4
+ import { mergeRefs } from './merge-refs.js';
5
+ import { Tooltip } from './Tooltip.js';
6
+ import { useMeasuredWidth } from './useMeasuredWidth.js';
7
+ /* A whole split into parts, each as wide as it lasts, so they compare at a glance. A part starts exactly at its
8
+ place; the gap between two parts that touch comes out of the one before. */
9
+ export function SegmentBar({ parts, range, size = 'md', onSelect, label, className, ref }) {
10
+ const [measure, width] = useMeasuredWidth();
11
+ const rootRef = useMemo(() => mergeRefs(ref, measure), [ref, measure]);
12
+ const labels = useRef([]);
13
+ const [cut, setCut] = useState([]);
14
+ let end = 0;
15
+ const placed = parts.map((part, index) => {
16
+ const from = part.start ?? end;
17
+ end = from + Math.max(0, part.value);
18
+ return { part, index, from, to: end };
19
+ });
20
+ const [from, to] = range ?? [0, end];
21
+ const span = to - from;
22
+ const visible = span > 0 ? placed.filter((placement) => placement.to > from && placement.from < to) : [];
23
+ // Parts without a name say nothing a screen reader could use; the element around the bar names it, as a meter does.
24
+ // Parts a person can press stay reachable, since a hidden list must not hold focusable buttons.
25
+ const hidden = onSelect === undefined && !parts.some((part) => part.label !== undefined);
26
+ // A label that stops fitting after a resize or a new range gets its tooltip.
27
+ useEffect(() => {
28
+ const next = labels.current.slice(0, visible.length).map((element) => element !== null && element.scrollWidth > element.clientWidth);
29
+ setCut((current) => (current.length === next.length && current.every((value, i) => value === next[i]) ? current : next));
30
+ }, [width, parts, from, to, size, visible.length]);
31
+ return (_jsx("ol", { ref: rootRef, "aria-label": hidden ? undefined : label, "aria-hidden": hidden ? true : undefined, className: clsx('relative min-w-0', size === 'sm' ? 'h-1.5' : 'h-7', className), children: visible.map(({ part, index, from: partFrom, to: partTo }, i) => {
32
+ const left = (Math.max(partFrom, from) - from) / span;
33
+ const right = (Math.min(partTo, to) - from) / span;
34
+ const next = visible[i + 1];
35
+ // Less than a pixel apart they touch, whatever rounding the caller's starts carry.
36
+ const touches = next !== undefined && ((next.from - partTo) / span) * width < 1;
37
+ const fill = clsx('flex size-full min-w-0 items-center', size === 'sm' ? 'rounded-full' : 'rounded-sm px-1.5 text-left text-xs', part.current && (size === 'sm' ? 'bg-accent' : 'bg-accent-soft text-text'), !part.current && part.color === undefined && 'bg-surface-active', !part.current && size === 'md' && 'text-text-muted', onSelect !== undefined && 'cursor-pointer hover:opacity-80');
38
+ const text = (_jsx("span", { ref: (element) => {
39
+ labels.current[i] = element;
40
+ }, className: size === 'sm' ? 'sr-only' : 'min-w-0 truncate', children: part.label }));
41
+ const body = onSelect === undefined ? (_jsx("span", { className: fill, style: { backgroundColor: part.color }, "aria-current": part.current ? true : undefined, children: text })) : (_jsx("button", { type: "button", className: fill, style: { backgroundColor: part.color }, "aria-current": part.current ? true : undefined, onClick: () => onSelect(index), children: text }));
42
+ return (_jsx("li", { className: clsx('absolute inset-y-0 focus-within:z-10', touches && 'pr-0.5'), style: { left: `${left * 100}%`, width: `${(right - left) * 100}%` }, children: part.label === undefined ? (body) : (_jsx(Tooltip, { label: part.label, disabled: size === 'md' && !cut[i], children: body })) }, index));
43
+ }) }));
44
+ }
@@ -0,0 +1,17 @@
1
+ import type { Ref } from 'react';
2
+ import type { LucideIcon } from 'lucide-react';
3
+ export interface SegmentedOption<T extends string> {
4
+ id: T;
5
+ label: string;
6
+ icon?: LucideIcon;
7
+ }
8
+ export interface SegmentedProps<T extends string> {
9
+ value: T;
10
+ onValueChange(id: T): void;
11
+ options: readonly SegmentedOption<T>[];
12
+ label: string;
13
+ disabled?: boolean;
14
+ className?: string;
15
+ ref?: Ref<HTMLDivElement>;
16
+ }
17
+ export declare function Segmented<T extends string>({ value, onValueChange, options, label, disabled, className, ref }: SegmentedProps<T>): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { Icon } from './Icon.js';
4
+ /* One of a few options side by side in one sunken track, the picked one lifted out of it. */
5
+ export function Segmented({ value, onValueChange, options, label, disabled, className, ref }) {
6
+ return (_jsx("div", { ref: ref, className: clsx('flex h-8 w-fit items-center rounded-lg bg-surface-sunken p-0.5 text-xs font-medium', className), role: "radiogroup", "aria-label": label, children: options.map((option) => (_jsxs("button", { type: "button", role: "radio", "aria-checked": value === option.id, disabled: disabled, className: clsx('flex h-7 items-center gap-1.5 rounded-md px-3 transition-colors disabled:opacity-50', value === option.id ? 'bg-surface-raised text-text shadow-raised' : 'text-text-muted hover:text-text'), onClick: () => onValueChange(option.id), children: [option.icon && _jsx(Icon, { icon: option.icon, size: 14 }), option.label] }, option.id))) }));
7
+ }
@@ -0,0 +1,27 @@
1
+ import type { ReactNode, Ref } from 'react';
2
+ export interface SelectItem<T extends string> {
3
+ value: T;
4
+ label: string;
5
+ description?: string;
6
+ icon?: ReactNode;
7
+ disabled?: boolean;
8
+ }
9
+ export interface SelectGroup<T extends string> {
10
+ label: string;
11
+ items: SelectItem<T>[];
12
+ }
13
+ export interface SelectProps<T extends string> {
14
+ value: T | null;
15
+ onValueChange(value: T): void;
16
+ items: SelectItem<T>[] | SelectGroup<T>[];
17
+ label: string;
18
+ placeholder?: string;
19
+ size?: 'sm' | 'md';
20
+ variant?: 'outlined' | 'ghost';
21
+ disabled?: boolean;
22
+ align?: 'start' | 'end';
23
+ truncateValue?: boolean;
24
+ className?: string;
25
+ ref?: Ref<HTMLButtonElement>;
26
+ }
27
+ export declare function Select<T extends string>({ value, onValueChange, items, label, placeholder, size, variant, disabled, align, truncateValue, className, ref }: SelectProps<T>): import("react").JSX.Element;
package/dist/Select.js ADDED
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Select as BaseSelect } from '@base-ui-components/react/select';
3
+ import clsx from 'clsx';
4
+ import { ChevronDown } from 'lucide-react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Icon } from './Icon.js';
7
+ import { MENU_LABEL, MenuCheck } from './menu/parts.js';
8
+ const isGroup = (entry) => Array.isArray(entry.items);
9
+ const TRIGGER_SIZE = {
10
+ sm: 'h-7 gap-1 px-2',
11
+ md: 'h-8 gap-1.5 px-2'
12
+ };
13
+ const TRIGGER_VARIANT = {
14
+ outlined: 'max-w-56 rounded-lg border border-border bg-surface-raised text-text',
15
+ ghost: 'rounded-md text-text-muted hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text data-disabled:hover:bg-transparent data-disabled:hover:text-text-muted'
16
+ };
17
+ /* The dimmed look `Switch` and `Button` wear when disabled, so a select that cannot open reads as such. */
18
+ const TRIGGER_DISABLED = 'data-disabled:cursor-default data-disabled:opacity-50';
19
+ /* A description makes the row two lines high; the box keeps the icon on the label's line. */
20
+ function Row({ item, picked }) {
21
+ return (_jsxs(BaseSelect.Item, { className: clsx('menu-item', item.description && 'items-start'), value: item.value, label: item.label, disabled: item.disabled, children: [_jsx(MenuCheck, { kind: "radio", checked: picked }), item.icon && _jsx("span", { className: "flex h-lh shrink-0 items-center", children: item.icon }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx(BaseSelect.ItemText, { children: item.label }), item.description && _jsx("span", { className: "text-xs text-text-faint", children: item.description })] })] }));
22
+ }
23
+ /* The one select. A Base UI `Select` in the popup style the menus use, so a settings field and a
24
+ picker in a toolbar read the same. Base UI brings the keyboard along (arrows,
25
+ Home and End, typeahead, Enter, Escape), which a hand-rolled listbox would have to repeat. */
26
+ export function Select({ value, onValueChange, items, label, placeholder, size = 'md', variant = 'outlined', disabled, align = 'start', truncateValue = true, className, ref }) {
27
+ const { t } = useTranslation('ui');
28
+ const groups = items.length > 0 && isGroup(items[0]) ? items : null;
29
+ const flat = groups ? groups.flatMap((group) => group.items) : items;
30
+ const selected = flat.find((item) => item.value === value) ?? null;
31
+ return (_jsxs(BaseSelect.Root, { items: flat.map((item) => ({ value: item.value, label: item.label })), value: value, disabled: disabled, onValueChange: (next) => onValueChange(next), children: [_jsxs(BaseSelect.Trigger, { ref: ref, "aria-label": label, className: clsx('flex shrink-0 items-center whitespace-nowrap text-xs', TRIGGER_SIZE[size], TRIGGER_VARIANT[variant], TRIGGER_DISABLED, className), children: [selected?.icon, _jsx(BaseSelect.Value, { className: clsx(truncateValue && 'truncate'), children: () => selected?.label ?? placeholder ?? t('action.select') }), _jsx(BaseSelect.Icon, { className: "ml-auto flex shrink-0 pl-1", children: _jsx(Icon, { icon: ChevronDown, size: 14 }) })] }), _jsx(BaseSelect.Portal, { children: _jsx(BaseSelect.Positioner, { className: "popup-positioner", side: "bottom", align: align, sideOffset: 6, alignItemWithTrigger: false, children: _jsx(BaseSelect.Popup, { className: "menu-popup", children: _jsx(BaseSelect.List, { className: "max-h-80 overflow-auto", children: groups
32
+ ? groups.map((group) => (_jsxs(BaseSelect.Group, { children: [_jsx(BaseSelect.GroupLabel, { className: MENU_LABEL, children: group.label }), group.items.map((item) => (_jsx(Row, { item: item, picked: item.value === value }, item.value)))] }, group.label)))
33
+ : flat.map((item) => _jsx(Row, { item: item, picked: item.value === value }, item.value)) }) }) }) })] }));
34
+ }
@@ -0,0 +1,7 @@
1
+ import type { Ref } from 'react';
2
+ export interface SeparatorProps {
3
+ orientation?: 'vertical' | 'horizontal';
4
+ className?: string;
5
+ ref?: Ref<HTMLSpanElement>;
6
+ }
7
+ export declare function Separator({ orientation, className, ref }: SeparatorProps): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ /* A hairline between two groups of controls in a toolbar-like row. It carries no margin of its own.
4
+ The row's gap puts 8px on either side of it, which is what makes the line read as a divider and
5
+ not as a group of its own. Between two groups of menu rows it is `Menu.Separator`. */
6
+ export function Separator({ orientation = 'vertical', className, ref }) {
7
+ return _jsx("span", { ref: ref, "aria-hidden": true, className: clsx(orientation === 'vertical' ? 'h-4 w-px' : 'h-px w-full', 'shrink-0 bg-border', className) });
8
+ }
@@ -0,0 +1 @@
1
+ export declare function ShortcutHints(): import("react").ReactPortal | null;
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { isApplePlatform } from './platform.js';
5
+ import { formatShortcut } from './shortcut.js';
6
+ import { ModifierHold, placeHint, shortcutHintTargets } from './shortcut-hints.js';
7
+ /* Whether a person can see the button now: on screen, not faded out, and not under a dialog or another node. */
8
+ const isSeen = (element, rect) => {
9
+ if (rect.width === 0 || rect.height === 0 || rect.bottom <= 0 || rect.right <= 0 || rect.top >= window.innerHeight || rect.left >= window.innerWidth) {
10
+ return false;
11
+ }
12
+ if (!element.checkVisibility({ opacityProperty: true, visibilityProperty: true })) {
13
+ return false;
14
+ }
15
+ const hit = document.elementFromPoint((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2);
16
+ return hit !== null && element.contains(hit);
17
+ };
18
+ const measureHints = (apple) => {
19
+ const viewport = { width: window.innerWidth, height: window.innerHeight };
20
+ const hints = [];
21
+ for (const [element, target] of shortcutHintTargets()) {
22
+ const rect = element.getBoundingClientRect();
23
+ if (!isSeen(element, rect)) {
24
+ continue;
25
+ }
26
+ const placement = placeHint(rect, viewport);
27
+ hints.push({
28
+ id: target.id,
29
+ text: formatShortcut(target.shortcut, apple),
30
+ style: { left: placement.left, right: placement.right, top: placement.top, translate: `${placement.translateX} ${placement.translateY}` }
31
+ });
32
+ }
33
+ return hints;
34
+ };
35
+ /*
36
+ * Every visible button's shortcut, under the button, for as long as the modifier is held on its own.
37
+ * Mounted once; the listener sits on the window in the capture phase so a terminal that keeps a key to
38
+ * itself still takes the hints down. An embedded page (`<webview>`) has the keyboard to itself and shows none.
39
+ */
40
+ export function ShortcutHints() {
41
+ const [shown, setShown] = useState(false);
42
+ const [hints, setHints] = useState([]);
43
+ const holdRef = useRef(null);
44
+ useEffect(() => {
45
+ const hold = new ModifierHold(isApplePlatform(), setShown);
46
+ holdRef.current = hold;
47
+ const onKeyDown = (e) => hold.keyDown(e);
48
+ const onKeyUp = (e) => hold.keyUp(e);
49
+ // A click or a zoom with the modifier down is a gesture, not someone looking for a shortcut.
50
+ const cancel = () => hold.cancel();
51
+ window.addEventListener('keydown', onKeyDown, true);
52
+ window.addEventListener('keyup', onKeyUp, true);
53
+ window.addEventListener('pointerdown', cancel, true);
54
+ window.addEventListener('wheel', cancel, { capture: true, passive: true });
55
+ window.addEventListener('blur', cancel);
56
+ return () => {
57
+ window.removeEventListener('keydown', onKeyDown, true);
58
+ window.removeEventListener('keyup', onKeyUp, true);
59
+ window.removeEventListener('pointerdown', cancel, true);
60
+ window.removeEventListener('wheel', cancel, true);
61
+ window.removeEventListener('blur', cancel);
62
+ hold.cancel();
63
+ };
64
+ }, []);
65
+ // Measured before paint and then every frame, so a hint never shows where its button was.
66
+ useLayoutEffect(() => {
67
+ if (!shown) {
68
+ return;
69
+ }
70
+ const apple = isApplePlatform();
71
+ let frame = 0;
72
+ let last = '';
73
+ const measure = () => {
74
+ // Focus that moved into a page takes the key-up with it, so the embedder never hears the modifier let go.
75
+ if (document.activeElement?.tagName === 'WEBVIEW') {
76
+ holdRef.current?.cancel();
77
+ return;
78
+ }
79
+ const next = measureHints(apple);
80
+ const signature = JSON.stringify(next);
81
+ if (signature !== last) {
82
+ last = signature;
83
+ setHints(next);
84
+ }
85
+ frame = requestAnimationFrame(measure);
86
+ };
87
+ measure();
88
+ return () => cancelAnimationFrame(frame);
89
+ }, [shown]);
90
+ if (!shown) {
91
+ return null;
92
+ }
93
+ return createPortal(_jsx("div", { "aria-hidden": true, className: "pointer-events-none fixed inset-0 z-(--z-shortcut-hints)", children: hints.map((hint) => (_jsx("kbd", { className: "absolute rounded-md bg-text px-1.5 font-sans text-xs/tight font-medium whitespace-nowrap text-surface shadow-float", style: hint.style, children: hint.text }, hint.id))) }), document.body);
94
+ }
@@ -0,0 +1,6 @@
1
+ import type { Ref } from 'react';
2
+ export interface SkeletonProps {
3
+ className?: string;
4
+ ref?: Ref<HTMLSpanElement>;
5
+ }
6
+ export declare function Skeleton({ className, ref }: SkeletonProps): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ /* A bar where a value will be, while the answer is still on its way. */
4
+ export function Skeleton({ className, ref }) {
5
+ return _jsx("span", { ref: ref, className: clsx('block h-4 animate-pulse rounded bg-surface-sunken', className), "aria-hidden": true });
6
+ }
@@ -0,0 +1,19 @@
1
+ import { type KeyboardEvent as ReactKeyboardEvent, type ReactNode, type Ref, type RefObject } from 'react';
2
+ export interface SlidingColumnProps {
3
+ open: boolean;
4
+ width: number;
5
+ bounds: {
6
+ min: number;
7
+ max(): number;
8
+ };
9
+ onWidthChange(width: number): void;
10
+ instant?: boolean;
11
+ body?: {
12
+ ref: RefObject<HTMLDivElement | null>;
13
+ onKeyDown(event: ReactKeyboardEvent<HTMLDivElement>): void;
14
+ };
15
+ className?: string;
16
+ ref?: Ref<HTMLElement>;
17
+ children?: ReactNode;
18
+ }
19
+ export declare function SlidingColumn({ open, width, bounds, onWidthChange, instant, body, className, ref, children }: SlidingColumnProps): import("react").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import clsx from 'clsx';
4
+ import { ColumnResizeHandle } from './ColumnResizeHandle.js';
5
+ import { mergeRefs } from './merge-refs.js';
6
+ import { useColumnResize } from './useColumnResize.js';
7
+ // How long the open and close motion takes; the same number as `.sliding-column` in `theme.css`.
8
+ const TRANSITION_MS = 200;
9
+ /*
10
+ * A panel that slides in and out along the right edge. The outer column is 0 wide and inert while
11
+ * closed; the contents sit in an inner column of the stored width, so they do not reflow while it
12
+ * moves, and they stay mounted until the slide is over, so a close plays out. The left edge is the
13
+ * drag handle.
14
+ */
15
+ export function SlidingColumn({ open, width, bounds, onWidthChange, instant = false, body, className, ref, children }) {
16
+ /* Closed and done animating. Until then the contents stay mounted, so a close plays out. */
17
+ const [settled, setSettled] = useState(!open);
18
+ /* A width that lands without a transition fires no `transitionend`, so the motion it would have
19
+ ended is over in the same commit that starts it. */
20
+ if (instant && settled !== !open) {
21
+ setSettled(!open);
22
+ }
23
+ const present = open || !settled;
24
+ const column = useRef(null);
25
+ const { startResize } = useColumnResize(column, { ...bounds, size: width, from: 'right', onSize: onWidthChange });
26
+ useEffect(() => {
27
+ if (open || settled) {
28
+ return;
29
+ }
30
+ // Reduced motion and a hidden tab paint no width change, so no `transitionend` arrives.
31
+ const timer = window.setTimeout(() => setSettled(true), TRANSITION_MS + 50);
32
+ return () => {
33
+ window.clearTimeout(timer);
34
+ };
35
+ }, [open, settled]);
36
+ return (_jsx("aside", { ref: mergeRefs(column, ref), inert: !open, "data-instant": instant ? '' : undefined, className: clsx('sliding-column flex h-full shrink-0 justify-end overflow-hidden', className), style: { width: open ? width : 0 }, onTransitionEnd: (event) => {
37
+ if (event.propertyName === 'width' && event.target === event.currentTarget) {
38
+ setSettled(!open);
39
+ }
40
+ }, children: present && (_jsxs("div", { ref: body?.ref, tabIndex: body ? -1 : undefined, className: "relative flex h-full shrink-0 flex-col border-l border-border bg-surface outline-none", style: { width }, onKeyDown: body?.onKeyDown, children: [open && _jsx(ColumnResizeHandle, { from: "right", onPointerDown: startResize }), children] })) }));
41
+ }
@@ -0,0 +1,6 @@
1
+ import type { ComponentProps } from 'react';
2
+ export interface SpinnerProps extends Omit<ComponentProps<'span'>, 'children'> {
3
+ size?: number;
4
+ label?: string;
5
+ }
6
+ export declare function Spinner({ size, label, className, style, ref, ...props }: SpinnerProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ const DOTS = [0, 1, 2];
4
+ /* Work in progress: three dots that leap over each other in the color of the text around them. With reduced motion they stand still. */
5
+ export function Spinner({ size = 16, label, className, style, ref, ...props }) {
6
+ return (_jsx("span", { ref: ref, role: label === undefined ? undefined : 'img', "aria-label": label, "aria-hidden": label === undefined ? true : undefined, ...props, className: clsx('spinner', className), style: { ...style, '--spinner-size': `${size}px` }, children: DOTS.map((dot) => (_jsx("span", { className: "spinner-hop", style: { '--spinner-step': dot }, children: _jsx("span", { className: "spinner-dot" }) }, dot))) }));
7
+ }
@@ -0,0 +1,14 @@
1
+ import type { Ref } from 'react';
2
+ export interface StepperProps {
3
+ value: number;
4
+ onValueChange(value: number): void;
5
+ min: number;
6
+ max: number;
7
+ step: number;
8
+ decimals?: number;
9
+ unit?: string;
10
+ label: string;
11
+ className?: string;
12
+ ref?: Ref<HTMLDivElement>;
13
+ }
14
+ export declare function Stepper({ value, onValueChange, min, max, step, decimals, unit, label, className, ref }: StepperProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { Minus, Plus } from 'lucide-react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { ButtonGroup } from './ButtonGroup.js';
6
+ import { formatFixed } from './format/number.js';
7
+ import { IconButton } from './IconButton.js';
8
+ const placesOf = (step) => String(step).split('.')[1]?.length ?? 0;
9
+ /* A number between minus and plus, in one sunken group so the three read as a single control. */
10
+ export function Stepper({ value, onValueChange, min, max, step, decimals = placesOf(step), unit, label, className, ref }) {
11
+ const { t } = useTranslation('ui');
12
+ const reading = (amount) => (_jsxs(_Fragment, { children: [formatFixed(amount, decimals), unit && _jsx("span", { className: "text-text-muted", children: unit })] }));
13
+ const nudge = (direction) => {
14
+ const next = Math.round((value + direction * step) * 100) / 100;
15
+ onValueChange(Math.min(max, Math.max(min, next)));
16
+ };
17
+ return (_jsxs(ButtonGroup, { ref: ref, className: clsx('shrink-0 rounded-lg bg-surface-sunken p-0.5', className), role: "group", "aria-label": label, children: [_jsx(IconButton, { icon: Minus, size: "sm", label: t('controls.stepper.smallerFor', { label }), tooltip: t('controls.stepper.smaller'), disabled: value <= min, onClick: () => nudge(-1) }), _jsxs("span", { className: "grid min-w-10 px-1 text-center text-xs text-text tabular-nums", children: [_jsx("span", { className: "invisible col-start-1 row-start-1", children: reading(min) }), _jsx("span", { className: "invisible col-start-1 row-start-1", children: reading(max) }), _jsx("span", { className: "col-start-1 row-start-1", "aria-live": "polite", children: reading(value) })] }), _jsx(IconButton, { icon: Plus, size: "sm", label: t('controls.stepper.largerFor', { label }), tooltip: t('controls.stepper.larger'), disabled: value >= max, onClick: () => nudge(1) })] }));
18
+ }
@@ -0,0 +1,3 @@
1
+ import { useRender } from '@base-ui-components/react/use-render';
2
+ export type SurfaceProps = useRender.ComponentProps<'div'>;
3
+ export declare function Surface({ render, className, ref, ...props }: SurfaceProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,16 @@
1
+ import clsx from 'clsx';
2
+ import { useRender } from '@base-ui-components/react/use-render';
3
+ const FLOAT = 'border border-border bg-[color-mix(in_srgb,var(--surface-raised)_88%,transparent)] shadow-float backdrop-blur-[14px]';
4
+ /*
5
+ * The glass card that floats over the content under it: a dock, a banner, a toast, a find bar, a
6
+ * chip over a scrolling thread. It brings the ground, the border and the shadow; the caller brings
7
+ * the shape (radius, padding, height) and the position.
8
+ */
9
+ export function Surface({ render, className, ref, ...props }) {
10
+ return useRender({
11
+ render,
12
+ ref,
13
+ defaultTagName: 'div',
14
+ props: { ...props, className: clsx(FLOAT, className) }
15
+ });
16
+ }
@@ -0,0 +1,10 @@
1
+ import type { Ref } from 'react';
2
+ export interface SwitchProps {
3
+ checked: boolean;
4
+ onCheckedChange(checked: boolean): void;
5
+ label: string;
6
+ disabled?: boolean;
7
+ className?: string;
8
+ ref?: Ref<HTMLButtonElement>;
9
+ }
10
+ export declare function Switch({ checked, onCheckedChange, label, disabled, className, ref }: SwitchProps): import("react").JSX.Element;
package/dist/Switch.js ADDED
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { Switch as BaseSwitch } from '@base-ui-components/react/switch';
4
+ /* On or off, as a switch rather than a checkbox: a setting that takes effect the moment it flips. */
5
+ export function Switch({ checked, onCheckedChange, label, disabled, className, ref }) {
6
+ return (_jsx(BaseSwitch.Root, { ref: ref, checked: checked, onCheckedChange: (next) => onCheckedChange(next), "aria-label": label, disabled: disabled, className: clsx('relative h-5 w-9 shrink-0 rounded-full bg-border-strong p-0.5 transition-colors data-checked:bg-accent data-disabled:opacity-50', className), children: _jsx(BaseSwitch.Thumb, { className: "block h-4 w-4 rounded-full bg-surface-raised shadow-raised transition-transform data-checked:translate-x-4" }) }));
7
+ }
@@ -0,0 +1,5 @@
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
+ export type TextMenuProps = ComponentProps<'div'> & {
3
+ items?: ReactNode;
4
+ };
5
+ export declare function TextMenu({ children, items, ref, ...rest }: TextMenuProps): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from 'react';
3
+ import { Copy, Scan } from 'lucide-react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { copyText } from './clipboard.js';
6
+ import { Icon } from './Icon.js';
7
+ import { Kbd } from './Kbd.js';
8
+ import { mergeRefs } from './merge-refs.js';
9
+ import { ContextMenuRoot, ContextMenuTrigger, MenuItem, MenuPopup, MenuSeparator } from './menu/parts.js';
10
+ import { isApplePlatform } from './platform.js';
11
+ import { selectAllWithin, selectionWithin } from './selection.js';
12
+ import { EDIT_SHORTCUTS, matchesShortcut } from './shortcut.js';
13
+ /*
14
+ * A block of text and the menu that belongs to it: Copy for what is selected inside it, Select all
15
+ * for the whole of it, on Cmd+A as well. Anywhere text can be selected a right-click has to offer
16
+ * to copy it, and an app that draws every menu itself offers nothing at all without one of these.
17
+ */
18
+ export function TextMenu({ children, items, ref, ...rest }) {
19
+ const { t } = useTranslation('ui');
20
+ const host = useRef(null);
21
+ // Read when the menu opens: a selection made after that is not the one the click was about.
22
+ const [selection, setSelection] = useState('');
23
+ const onKeyDown = (event) => {
24
+ if (!matchesShortcut(EDIT_SHORTCUTS.selectAll, event, isApplePlatform())) {
25
+ return;
26
+ }
27
+ event.preventDefault();
28
+ selectAllWithin(host.current);
29
+ };
30
+ return (_jsxs(ContextMenuRoot, { onOpenChange: (open) => setSelection(open ? selectionWithin(host.current) : ''), children: [_jsx(ContextMenuTrigger, { ...rest, ref: mergeRefs(host, ref), onKeyDown: onKeyDown, children: children }), _jsxs(MenuPopup, { children: [_jsxs(MenuItem, { disabled: selection === '', onClick: () => copyText(selection), children: [_jsx(Icon, { icon: Copy, size: 14 }), " ", t('action.copy'), " ", _jsx(Kbd, { shortcut: EDIT_SHORTCUTS.copy })] }), _jsx(MenuSeparator, {}), _jsxs(MenuItem, { onClick: () => selectAllWithin(host.current), children: [_jsx(Icon, { icon: Scan, size: 14 }), " ", t('action.selectAll'), " ", _jsx(Kbd, { shortcut: EDIT_SHORTCUTS.selectAll })] }), items !== undefined && (_jsxs(_Fragment, { children: [_jsx(MenuSeparator, {}), items] }))] })] }));
31
+ }
package/dist/Tile.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ import type { Shortcut } from './shortcut.ts';
3
+ export interface TileProps extends Omit<ComponentProps<'button'>, 'title'> {
4
+ icon: ReactNode;
5
+ title: string;
6
+ description?: ReactNode;
7
+ shortcut?: Shortcut;
8
+ primary?: boolean;
9
+ size?: 'md' | 'sm';
10
+ }
11
+ export declare function Tile({ icon, title, description, shortcut, primary, size, className, type, ...rest }: TileProps): import("react").JSX.Element;
package/dist/Tile.js ADDED
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { Kbd } from './Kbd.js';
4
+ /*
5
+ * One thing to start with, as a card that is a button: a start screen and the empty places of an app
6
+ * use it, so a place to begin looks the same wherever it is offered.
7
+ */
8
+ export function Tile({ icon, title, description, shortcut, primary = false, size = 'md', className, type = 'button', ...rest }) {
9
+ return (_jsxs("button", { type: type, className: clsx('flex min-w-0 items-center border bg-surface text-left hover:bg-surface-hover disabled:opacity-50 disabled:hover:bg-surface', size === 'md' ? 'gap-3 rounded-xl p-3' : 'gap-2 rounded-lg px-2 py-1.5', primary ? 'border-accent' : 'border-border', className), ...rest, children: [_jsx("span", { className: clsx('grid size-8 shrink-0 place-items-center rounded-lg', primary ? 'bg-accent text-accent-text' : 'bg-surface-sunken text-text-muted'), children: icon }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "truncate text-sm font-medium text-text", children: title }), description && _jsx("span", { className: "truncate text-xs text-text-muted", children: description })] }), shortcut && _jsx(Kbd, { shortcut: shortcut, variant: "inline", className: "shrink-0" })] }));
10
+ }
@@ -0,0 +1,9 @@
1
+ import { type ReactNode, type Ref } from 'react';
2
+ import { type Toast, type ToastStoreHook } from './toast-store.ts';
3
+ export interface ToastsProps<T extends Toast> {
4
+ store: ToastStoreHook<T>;
5
+ footer?: (toast: T) => ReactNode;
6
+ className?: string;
7
+ ref?: Ref<HTMLDivElement>;
8
+ }
9
+ export declare function Toasts<T extends Toast>({ store, footer, className, ref }: ToastsProps<T>): import("react").JSX.Element | null;