@milktop/react-ui 0.0.0-stage → 0.1.2

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 (250) hide show
  1. package/README.md +173 -2
  2. package/dist/accordion/Accordion.d.ts +65 -0
  3. package/dist/accordion/Accordion.js +61 -0
  4. package/dist/accordion/accordion.css +17 -0
  5. package/dist/action-bar/ActionBar.d.ts +33 -0
  6. package/dist/action-bar/ActionBar.js +48 -0
  7. package/dist/action-bar/action-bar.css +22 -0
  8. package/dist/alert/Alert.d.ts +28 -0
  9. package/dist/alert/Alert.js +12 -0
  10. package/dist/alert/alert.css +32 -0
  11. package/dist/app-shell/AppShell.d.ts +38 -0
  12. package/dist/app-shell/AppShell.js +92 -0
  13. package/dist/app-shell/Topbar.d.ts +20 -0
  14. package/dist/app-shell/Topbar.js +13 -0
  15. package/dist/app-shell/app-shell.css +41 -0
  16. package/dist/app-shell/shell.d.ts +9 -0
  17. package/dist/app-shell/shell.js +3 -0
  18. package/dist/attachments/Attachments.d.ts +84 -0
  19. package/dist/attachments/Attachments.js +235 -0
  20. package/dist/attachments/attachments.css +86 -0
  21. package/dist/avatar/Avatar.d.ts +31 -0
  22. package/dist/avatar/Avatar.js +41 -0
  23. package/dist/avatar/AvatarGroup.d.ts +41 -0
  24. package/dist/avatar/AvatarGroup.js +17 -0
  25. package/dist/avatar/avatar.css +73 -0
  26. package/dist/badge/Badge.d.ts +26 -0
  27. package/dist/badge/Badge.js +10 -0
  28. package/dist/badge/badge.css +24 -0
  29. package/dist/banner/Banner.d.ts +33 -0
  30. package/dist/banner/Banner.js +14 -0
  31. package/dist/banner/banner.css +25 -0
  32. package/dist/breadcrumbs/Breadcrumbs.d.ts +39 -0
  33. package/dist/breadcrumbs/Breadcrumbs.js +28 -0
  34. package/dist/breadcrumbs/breadcrumbs.css +18 -0
  35. package/dist/button/Button.d.ts +40 -0
  36. package/dist/button/Button.js +19 -0
  37. package/dist/button/button.css +63 -0
  38. package/dist/card/Card.d.ts +355 -0
  39. package/dist/card/Card.js +56 -0
  40. package/dist/card/card.css +43 -0
  41. package/dist/chart/AreaChart.d.ts +19 -0
  42. package/dist/chart/AreaChart.js +61 -0
  43. package/dist/chart/BarChart.d.ts +3 -0
  44. package/dist/chart/BarChart.js +60 -0
  45. package/dist/chart/ChartFrame.d.ts +25 -0
  46. package/dist/chart/ChartFrame.js +22 -0
  47. package/dist/chart/DonutChart.d.ts +20 -0
  48. package/dist/chart/DonutChart.js +41 -0
  49. package/dist/chart/Sparkline.d.ts +26 -0
  50. package/dist/chart/Sparkline.js +36 -0
  51. package/dist/chart/chart.css +51 -0
  52. package/dist/chart/chart.d.ts +23 -0
  53. package/dist/chart/chart.js +19 -0
  54. package/dist/checkbox/Checkbox.d.ts +50 -0
  55. package/dist/checkbox/Checkbox.js +19 -0
  56. package/dist/checkbox/CheckboxGroup.d.ts +44 -0
  57. package/dist/checkbox/CheckboxGroup.js +80 -0
  58. package/dist/checkbox/checkbox.css +49 -0
  59. package/dist/collapsible/Collapsible.d.ts +41 -0
  60. package/dist/collapsible/Collapsible.js +15 -0
  61. package/dist/collapsible/collapsible.css +20 -0
  62. package/dist/color-scheme.d.ts +18 -0
  63. package/dist/color-scheme.js +84 -0
  64. package/dist/command/Command.d.ts +64 -0
  65. package/dist/command/Command.js +162 -0
  66. package/dist/command/command.css +47 -0
  67. package/dist/confirm/confirm.css +5 -0
  68. package/dist/confirm/confirm.d.ts +42 -0
  69. package/dist/confirm/confirm.js +115 -0
  70. package/dist/copy-button/CopyButton.d.ts +30 -0
  71. package/dist/copy-button/CopyButton.js +23 -0
  72. package/dist/copy-button/copy-button.css +12 -0
  73. package/dist/css.d.ts +2 -0
  74. package/dist/data-list/DataList.d.ts +55 -0
  75. package/dist/data-list/DataList.js +43 -0
  76. package/dist/data-list/data-list.css +19 -0
  77. package/dist/date-picker/DatePicker.d.ts +95 -0
  78. package/dist/date-picker/DatePicker.js +237 -0
  79. package/dist/date-picker/date-picker.css +53 -0
  80. package/dist/dialog/Dialog.d.ts +98 -0
  81. package/dist/dialog/Dialog.js +92 -0
  82. package/dist/dialog/dialog.css +31 -0
  83. package/dist/divider/Divider.d.ts +27 -0
  84. package/dist/divider/Divider.js +8 -0
  85. package/dist/divider/divider.css +12 -0
  86. package/dist/editable/Editable.d.ts +47 -0
  87. package/dist/editable/Editable.js +51 -0
  88. package/dist/editable/editable.css +25 -0
  89. package/dist/editor/Editor.d.ts +59 -0
  90. package/dist/editor/Editor.js +220 -0
  91. package/dist/editor/editor.css +42 -0
  92. package/dist/editor/image.d.ts +1 -0
  93. package/dist/editor/image.js +23 -0
  94. package/dist/empty-state/EmptyState.d.ts +22 -0
  95. package/dist/empty-state/EmptyState.js +9 -0
  96. package/dist/empty-state/empty-state.css +14 -0
  97. package/dist/event-calendar/EventCalendar.d.ts +213 -0
  98. package/dist/event-calendar/EventCalendar.js +686 -0
  99. package/dist/event-calendar/dates.d.ts +8 -0
  100. package/dist/event-calendar/dates.js +21 -0
  101. package/dist/event-calendar/event-calendar.css +157 -0
  102. package/dist/field/Field.d.ts +66 -0
  103. package/dist/field/Field.js +23 -0
  104. package/dist/field/field.css +26 -0
  105. package/dist/file-upload/FileUpload.d.ts +60 -0
  106. package/dist/file-upload/FileUpload.js +102 -0
  107. package/dist/file-upload/file-upload.css +32 -0
  108. package/dist/format/FormatNumber.d.ts +42 -0
  109. package/dist/format/FormatNumber.js +59 -0
  110. package/dist/format/LocaleProvider.d.ts +33 -0
  111. package/dist/format/LocaleProvider.js +48 -0
  112. package/dist/format/format.d.ts +37 -0
  113. package/dist/format/format.js +74 -0
  114. package/dist/format/messages.d.ts +211 -0
  115. package/dist/format/messages.js +202 -0
  116. package/dist/highlight/Highlight.d.ts +11 -0
  117. package/dist/highlight/Highlight.js +14 -0
  118. package/dist/highlight/highlight.css +17 -0
  119. package/dist/hover-card/HoverCard.d.ts +47 -0
  120. package/dist/hover-card/HoverCard.js +18 -0
  121. package/dist/hover-card/hover-card.css +19 -0
  122. package/dist/index.d.ts +84 -0
  123. package/dist/index.js +85 -0
  124. package/dist/input/Input.d.ts +41 -0
  125. package/dist/input/Input.js +48 -0
  126. package/dist/input/input.css +25 -0
  127. package/dist/kbd/Kbd.d.ts +19 -0
  128. package/dist/kbd/Kbd.js +20 -0
  129. package/dist/kbd/kbd.css +8 -0
  130. package/dist/menu/Menu.d.ts +223 -0
  131. package/dist/menu/Menu.js +275 -0
  132. package/dist/menu/menu.css +38 -0
  133. package/dist/native-select/NativeSelect.d.ts +36 -0
  134. package/dist/native-select/NativeSelect.js +51 -0
  135. package/dist/native-select/native-select.css +16 -0
  136. package/dist/number-input/NumberInput.d.ts +56 -0
  137. package/dist/number-input/NumberInput.js +67 -0
  138. package/dist/number-input/number-input.css +21 -0
  139. package/dist/page/Page.d.ts +30 -0
  140. package/dist/page/Page.js +9 -0
  141. package/dist/page/page.css +17 -0
  142. package/dist/pagination/Pagination.d.ts +31 -0
  143. package/dist/pagination/Pagination.js +58 -0
  144. package/dist/pagination/pagination.css +15 -0
  145. package/dist/password-input/PasswordInput.d.ts +47 -0
  146. package/dist/password-input/PasswordInput.js +35 -0
  147. package/dist/password-input/password-input.css +18 -0
  148. package/dist/pin-input/PinInput.d.ts +45 -0
  149. package/dist/pin-input/PinInput.js +22 -0
  150. package/dist/pin-input/pin-input.css +13 -0
  151. package/dist/popover/Popover.d.ts +65 -0
  152. package/dist/popover/Popover.js +26 -0
  153. package/dist/popover/popover.css +16 -0
  154. package/dist/progress/Progress.d.ts +56 -0
  155. package/dist/progress/Progress.js +28 -0
  156. package/dist/progress/progress.css +24 -0
  157. package/dist/prose/Prose.d.ts +23 -0
  158. package/dist/prose/Prose.js +10 -0
  159. package/dist/prose/prose.css +29 -0
  160. package/dist/radio-group/RadioGroup.d.ts +59 -0
  161. package/dist/radio-group/RadioGroup.js +37 -0
  162. package/dist/radio-group/radio-group.css +30 -0
  163. package/dist/rolling-number/RollingNumber.d.ts +27 -0
  164. package/dist/rolling-number/RollingNumber.js +18 -0
  165. package/dist/rolling-number/rolling-number.css +10 -0
  166. package/dist/segment-group/Segmented.d.ts +68 -0
  167. package/dist/segment-group/Segmented.js +79 -0
  168. package/dist/segment-group/segment-group.css +25 -0
  169. package/dist/select/ListSelect.d.ts +5 -0
  170. package/dist/select/ListSelect.js +82 -0
  171. package/dist/select/SearchSelect.d.ts +43 -0
  172. package/dist/select/SearchSelect.js +219 -0
  173. package/dist/select/Select.d.ts +147 -0
  174. package/dist/select/Select.js +32 -0
  175. package/dist/select/items.d.ts +17 -0
  176. package/dist/select/items.js +14 -0
  177. package/dist/select/search-select.css +45 -0
  178. package/dist/select/select.css +52 -0
  179. package/dist/sheet/Sheet.d.ts +88 -0
  180. package/dist/sheet/Sheet.js +66 -0
  181. package/dist/sheet/sheet.css +44 -0
  182. package/dist/sidebar/NavGroup.d.ts +29 -0
  183. package/dist/sidebar/NavGroup.js +78 -0
  184. package/dist/sidebar/NavItem.d.ts +33 -0
  185. package/dist/sidebar/NavItem.js +50 -0
  186. package/dist/sidebar/NavSection.d.ts +20 -0
  187. package/dist/sidebar/NavSection.js +44 -0
  188. package/dist/sidebar/Sidebar.d.ts +65 -0
  189. package/dist/sidebar/Sidebar.js +59 -0
  190. package/dist/sidebar/SidebarUser.d.ts +31 -0
  191. package/dist/sidebar/SidebarUser.js +14 -0
  192. package/dist/sidebar/context.d.ts +13 -0
  193. package/dist/sidebar/context.js +6 -0
  194. package/dist/sidebar/sidebar.css +109 -0
  195. package/dist/skeleton/Skeleton.d.ts +25 -0
  196. package/dist/skeleton/Skeleton.js +8 -0
  197. package/dist/skeleton/skeleton.css +13 -0
  198. package/dist/slider/Slider.d.ts +70 -0
  199. package/dist/slider/Slider.js +65 -0
  200. package/dist/slider/slider.css +17 -0
  201. package/dist/sortable-list/SortableList.d.ts +46 -0
  202. package/dist/sortable-list/SortableList.js +85 -0
  203. package/dist/sortable-list/sortable-list.css +25 -0
  204. package/dist/spinner/Spinner.d.ts +18 -0
  205. package/dist/spinner/Spinner.js +11 -0
  206. package/dist/spinner/spinner.css +9 -0
  207. package/dist/stat/Stat.d.ts +42 -0
  208. package/dist/stat/Stat.js +21 -0
  209. package/dist/stat/Stats.d.ts +23 -0
  210. package/dist/stat/Stats.js +8 -0
  211. package/dist/stat/stat.css +34 -0
  212. package/dist/swatch-picker/SwatchPicker.d.ts +45 -0
  213. package/dist/swatch-picker/SwatchPicker.js +54 -0
  214. package/dist/swatch-picker/swatch-picker.css +38 -0
  215. package/dist/switch/Switch.d.ts +39 -0
  216. package/dist/switch/Switch.js +17 -0
  217. package/dist/switch/switch.css +13 -0
  218. package/dist/table/Table.d.ts +90 -0
  219. package/dist/table/Table.js +145 -0
  220. package/dist/table/table.css +44 -0
  221. package/dist/tabs/Tabs.d.ts +87 -0
  222. package/dist/tabs/Tabs.js +90 -0
  223. package/dist/tabs/tabs.css +53 -0
  224. package/dist/tags-input/TagsInput.d.ts +69 -0
  225. package/dist/tags-input/TagsInput.js +95 -0
  226. package/dist/tags-input/tags-input.css +37 -0
  227. package/dist/textarea/Textarea.d.ts +24 -0
  228. package/dist/textarea/Textarea.js +51 -0
  229. package/dist/textarea/textarea.css +14 -0
  230. package/dist/theme-toggle/ThemeToggle.d.ts +49 -0
  231. package/dist/theme-toggle/ThemeToggle.js +71 -0
  232. package/dist/theme-toggle/theme-toggle.css +23 -0
  233. package/dist/theme.css +121 -0
  234. package/dist/time-input/TimeInput.d.ts +25 -0
  235. package/dist/time-input/TimeInput.js +20 -0
  236. package/dist/timeline/Timeline.d.ts +50 -0
  237. package/dist/timeline/Timeline.js +15 -0
  238. package/dist/timeline/timeline.css +29 -0
  239. package/dist/toast/Toaster.d.ts +36 -0
  240. package/dist/toast/Toaster.js +25 -0
  241. package/dist/toast/toast.css +33 -0
  242. package/dist/tooltip/Tooltip.d.ts +55 -0
  243. package/dist/tooltip/Tooltip.js +48 -0
  244. package/dist/tooltip/tooltip.css +18 -0
  245. package/dist/tree-view/TreeView.d.ts +86 -0
  246. package/dist/tree-view/TreeView.js +74 -0
  247. package/dist/tree-view/tree-view.css +29 -0
  248. package/dist/utils.d.ts +19 -0
  249. package/dist/utils.js +59 -0
  250. package/package.json +99 -4
@@ -0,0 +1,18 @@
1
+ export type Scheme = 'light' | 'dark' | 'system';
2
+ declare class ColorScheme {
3
+ #private;
4
+ constructor();
5
+ get storageKey(): string;
6
+ set storageKey(key: string);
7
+ get value(): Scheme;
8
+ set value(scheme: Scheme);
9
+ get dark(): boolean;
10
+ subscribe: (listener: () => void) => () => void;
11
+ }
12
+ export declare const colorScheme: ColorScheme;
13
+ export declare function useColorScheme(): {
14
+ scheme: Scheme;
15
+ dark: boolean;
16
+ setScheme: (next: Scheme) => void;
17
+ };
18
+ export {};
@@ -0,0 +1,84 @@
1
+ // Light, dark or system colour scheme for the whole page:
2
+ //
3
+ // import { colorScheme, useColorScheme } from '@milktop/react-ui'
4
+ // colorScheme.value = 'dark' // 'light', 'dark' or 'system'
5
+ // const { scheme, dark, setScheme } = useColorScheme() // re-renders on change
6
+ //
7
+ // It puts `dark` on <html> (the theme's dark tokens apply under it) and sets
8
+ // `color-scheme` there for native controls and scrollbars. 'system' follows
9
+ // the OS setting live. The choice is saved in localStorage (`storageKey`)
10
+ // and kept in step across tabs.
11
+ import { useSyncExternalStore } from 'react';
12
+ const schemes = ['light', 'dark', 'system'];
13
+ const parse = (scheme) => (schemes.includes(scheme) ? scheme : 'system');
14
+ class ColorScheme {
15
+ #key = 'ui-color-scheme';
16
+ #value = 'system';
17
+ #systemDark = false;
18
+ #listeners = new Set();
19
+ constructor() {
20
+ if (typeof document === 'undefined')
21
+ return;
22
+ const media = matchMedia('(prefers-color-scheme: dark)');
23
+ this.#systemDark = media.matches;
24
+ media.addEventListener('change', (e) => {
25
+ this.#systemDark = e.matches;
26
+ this.#apply();
27
+ });
28
+ addEventListener('storage', (e) => {
29
+ if (e.key !== this.#key)
30
+ return;
31
+ this.#value = parse(e.newValue);
32
+ this.#apply();
33
+ });
34
+ this.#value = this.#load();
35
+ this.#apply();
36
+ }
37
+ get storageKey() { return this.#key; }
38
+ // Set it before the page reads a value (it reloads the saved choice).
39
+ set storageKey(key) {
40
+ this.#key = key;
41
+ this.#value = this.#load();
42
+ this.#apply();
43
+ }
44
+ get value() { return this.#value; }
45
+ set value(scheme) {
46
+ scheme = parse(scheme);
47
+ if (scheme === this.#value)
48
+ return;
49
+ this.#value = scheme;
50
+ try {
51
+ localStorage.setItem(this.#key, scheme);
52
+ }
53
+ catch { /* private mode */ }
54
+ this.#apply();
55
+ }
56
+ get dark() { return this.#value === 'dark' || (this.#value === 'system' && this.#systemDark); }
57
+ subscribe = (listener) => {
58
+ this.#listeners.add(listener);
59
+ return () => { this.#listeners.delete(listener); };
60
+ };
61
+ #load() {
62
+ try {
63
+ return parse(localStorage.getItem(this.#key));
64
+ }
65
+ catch {
66
+ return 'system';
67
+ }
68
+ }
69
+ #apply() {
70
+ if (typeof document === 'undefined')
71
+ return;
72
+ const root = document.documentElement;
73
+ root.classList.toggle('dark', this.dark);
74
+ root.style.colorScheme = this.dark ? 'dark' : 'light';
75
+ this.#listeners.forEach((listener) => listener());
76
+ }
77
+ }
78
+ export const colorScheme = new ColorScheme();
79
+ // The scheme and what's showing, re-rendering when either changes.
80
+ export function useColorScheme() {
81
+ const scheme = useSyncExternalStore(colorScheme.subscribe, () => colorScheme.value, () => 'system');
82
+ const dark = useSyncExternalStore(colorScheme.subscribe, () => colorScheme.dark, () => false);
83
+ return { scheme, dark, setScheme: (next) => { colorScheme.value = next; } };
84
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A command menu (⌘K): a dialog with a search box over a list of commands,
3
+ * filtered as you type.
4
+ *
5
+ * <Command items={commands} onSelect={run} trigger={<Button icon="lucide:search">Search</Button>} />
6
+ *
7
+ * ⌘K (Ctrl+K elsewhere) opens and closes it; ↑/↓ move through the results,
8
+ * Enter picks one, Escape closes it.
9
+ *
10
+ * - `items`: objects with `label`, plus optional `value`, `group` (a heading
11
+ * the item is listed under), `icon`, `description`, `shortcut` text,
12
+ * `keywords` (extra words to match) and `disabled`. An item with `href`
13
+ * goes there when picked
14
+ * - `load`: optional async function(query) returning items, for server
15
+ * search (debounced by `debounce` ms, `loadingText` meanwhile)
16
+ * - `onSelect`: called with the item's `value` (or the item, if it has none)
17
+ * - `trigger`: the element that opens it (it gets the trigger's props)
18
+ * - `open`, `defaultOpen`; `onOpenChange`: called with the new open state
19
+ * - `hotkey`: the key used with ⌘/Ctrl ('k'); null turns the shortcut off
20
+ * - `navigate`: function(href) for items with `href`, e.g. your router's
21
+ * `navigate`; by default a link is clicked
22
+ * - `placeholder`, `emptyText`, `label`; `hints`: false hides the keyboard
23
+ * hints along the bottom
24
+ *
25
+ * `formatHotkey('k')` gives how the shortcut reads here ('⌘K' or 'Ctrl K'),
26
+ * for a trigger's hint.
27
+ */
28
+ import { type ReactElement } from 'react';
29
+ import { Dialog } from '@ark-ui/react/dialog';
30
+ import 'iconify-icon';
31
+ import { type Classes } from '../utils.js';
32
+ import '../theme.css';
33
+ import './command.css';
34
+ export type CommandItem = {
35
+ label: string;
36
+ value?: unknown;
37
+ href?: string;
38
+ group?: string;
39
+ icon?: string;
40
+ description?: string;
41
+ shortcut?: string;
42
+ keywords?: string[];
43
+ disabled?: boolean;
44
+ };
45
+ export type CommandProps = Omit<Dialog.ContentProps, 'onSelect'> & {
46
+ items?: CommandItem[];
47
+ load?: ((query: string) => Promise<CommandItem[] | null | undefined>) | null;
48
+ debounce?: number;
49
+ onSelect?: (value: unknown) => void;
50
+ trigger?: ReactElement | null;
51
+ open?: boolean;
52
+ defaultOpen?: boolean;
53
+ onOpenChange?: (open: boolean) => void;
54
+ hotkey?: string | null;
55
+ navigate?: ((href: string) => void) | null;
56
+ label?: string;
57
+ placeholder?: string;
58
+ emptyText?: string;
59
+ loadingText?: string;
60
+ hints?: boolean;
61
+ classes?: Classes<'backdrop' | 'positioner' | 'content' | 'search' | 'searchIcon' | 'input' | 'list' | 'status' | 'group' | 'groupLabel' | 'item' | 'icon' | 'text' | 'label' | 'description' | 'shortcut' | 'hints' | 'key'>;
62
+ };
63
+ export declare function formatHotkey(key?: string): string;
64
+ export declare function Command({ items, load, debounce, onSelect, trigger, open: openProp, defaultOpen, onOpenChange, hotkey, navigate, label: labelProp, placeholder: placeholderProp, emptyText: emptyTextProp, loadingText: loadingTextProp, hints, className, classes, ...rest }: CommandProps): import("react").JSX.Element;
@@ -0,0 +1,162 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A command menu (⌘K): a dialog with a search box over a list of commands,
4
+ * filtered as you type.
5
+ *
6
+ * <Command items={commands} onSelect={run} trigger={<Button icon="lucide:search">Search</Button>} />
7
+ *
8
+ * ⌘K (Ctrl+K elsewhere) opens and closes it; ↑/↓ move through the results,
9
+ * Enter picks one, Escape closes it.
10
+ *
11
+ * - `items`: objects with `label`, plus optional `value`, `group` (a heading
12
+ * the item is listed under), `icon`, `description`, `shortcut` text,
13
+ * `keywords` (extra words to match) and `disabled`. An item with `href`
14
+ * goes there when picked
15
+ * - `load`: optional async function(query) returning items, for server
16
+ * search (debounced by `debounce` ms, `loadingText` meanwhile)
17
+ * - `onSelect`: called with the item's `value` (or the item, if it has none)
18
+ * - `trigger`: the element that opens it (it gets the trigger's props)
19
+ * - `open`, `defaultOpen`; `onOpenChange`: called with the new open state
20
+ * - `hotkey`: the key used with ⌘/Ctrl ('k'); null turns the shortcut off
21
+ * - `navigate`: function(href) for items with `href`, e.g. your router's
22
+ * `navigate`; by default a link is clicked
23
+ * - `placeholder`, `emptyText`, `label`; `hints`: false hides the keyboard
24
+ * hints along the bottom
25
+ *
26
+ * `formatHotkey('k')` gives how the shortcut reads here ('⌘K' or 'Ctrl K'),
27
+ * for a trigger's hint.
28
+ */
29
+ import { useEffect, useRef, useState } from 'react';
30
+ import { Dialog } from '@ark-ui/react/dialog';
31
+ import { Listbox, createListCollection, useListbox } from '@ark-ui/react/listbox';
32
+ import { Portal } from '@ark-ui/react/portal';
33
+ import 'iconify-icon';
34
+ import { cx, partsOf, useControllable } from '../utils.js';
35
+ import { useMessages } from '../format/LocaleProvider.js';
36
+ import '../theme.css';
37
+ import './command.css';
38
+ // How the shortcut reads on this platform: '⌘K' or 'Ctrl K'.
39
+ export function formatHotkey(key = 'k') {
40
+ const mac = /Mac|iPhone|iPad/.test(globalThis.navigator?.platform ?? '');
41
+ return mac ? `⌘${key.toUpperCase()}` : `Ctrl ${key.toUpperCase()}`;
42
+ }
43
+ const keyOf = (item) => String(item.value ?? item.href ?? item.label);
44
+ // Every word typed must appear in the label, description, group or keywords.
45
+ function matches(item, words) {
46
+ const text = [item.label, item.description, item.group, ...(item.keywords ?? [])].join(' ').toLowerCase();
47
+ return words.every((word) => text.includes(word));
48
+ }
49
+ function go(href) {
50
+ const link = document.createElement('a');
51
+ link.href = href;
52
+ document.body.appendChild(link);
53
+ link.click();
54
+ link.remove();
55
+ }
56
+ export function Command({ items = [], load = null, debounce = 200, onSelect, trigger = null, open: openProp, defaultOpen = false, onOpenChange, hotkey = 'k', navigate = null, label: labelProp, placeholder: placeholderProp, emptyText: emptyTextProp, loadingText: loadingTextProp, hints = true, className, classes, ...rest }) {
57
+ const t = useMessages();
58
+ const label = labelProp ?? t.command.label;
59
+ const placeholder = placeholderProp ?? t.command.placeholder;
60
+ const emptyText = emptyTextProp ?? t.command.empty;
61
+ const loadingText = loadingTextProp ?? t.command.loading;
62
+ const part = partsOf('command', classes);
63
+ const [open, setOpen] = useControllable(openProp, defaultOpen, onOpenChange);
64
+ const [query, setQuery] = useState('');
65
+ const [loaded, setLoaded] = useState([]);
66
+ const [loading, setLoading] = useState(false);
67
+ const input = useRef(null);
68
+ const listEl = useRef(null);
69
+ const ticket = useRef(0);
70
+ const timer = useRef(undefined);
71
+ const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
72
+ const found = load ? loaded : words.length ? items.filter((item) => matches(item, words)) : items;
73
+ // Results in group order (by first appearance), so the list reads and the
74
+ // arrow keys move the same way.
75
+ const groups = [...new Set(found.map((item) => item.group ?? ''))]
76
+ .map((name) => ({ name, items: found.filter((item) => (item.group ?? '') === name) }));
77
+ const results = groups.flatMap((group) => group.items);
78
+ const collection = createListCollection({
79
+ items: results, itemToString: (item) => item.label, itemToValue: keyOf, isItemDisabled: (item) => !!item.disabled,
80
+ });
81
+ function search(text) {
82
+ setQuery(text);
83
+ if (!load)
84
+ return;
85
+ const mine = ++ticket.current;
86
+ setLoading(true);
87
+ clearTimeout(timer.current);
88
+ timer.current = setTimeout(async () => {
89
+ const list = await load(text);
90
+ if (mine !== ticket.current)
91
+ return;
92
+ setLoading(false);
93
+ setLoaded(list ?? []);
94
+ }, debounce);
95
+ }
96
+ function choose(key) {
97
+ const item = results.find((item) => keyOf(item) === key);
98
+ if (!item)
99
+ return;
100
+ setOpen(false);
101
+ onSelect?.(item.value === undefined ? item : item.value);
102
+ if (item.href)
103
+ (navigate ?? go)(item.href);
104
+ }
105
+ // Keeps the highlighted item in view, scrolling only the list, and showing
106
+ // a group's heading along with its first item.
107
+ function scrollToItem({ getElement }) {
108
+ const el = getElement();
109
+ const box = listEl.current;
110
+ if (!el || !box)
111
+ return;
112
+ const top0 = box.getBoundingClientRect().top;
113
+ const offset = (node) => node.getBoundingClientRect().top - top0 + box.scrollTop;
114
+ const group = el.closest('[data-part=item-group]');
115
+ const first = group && group.querySelector('[data-part=item]') === el;
116
+ const top = offset(first ? group : el) - 4;
117
+ const bottom = offset(el) + el.offsetHeight + 4;
118
+ if (top < box.scrollTop)
119
+ box.scrollTop = top;
120
+ else if (bottom > box.scrollTop + box.clientHeight)
121
+ box.scrollTop = bottom - box.clientHeight;
122
+ }
123
+ // The value stays empty, so picking the same command again still counts.
124
+ const list = useListbox({
125
+ collection, value: [], loopFocus: true, scrollToIndexFn: scrollToItem, onSelect: (details) => choose(details.value),
126
+ });
127
+ const api = useRef(list);
128
+ useEffect(() => { api.current = list; });
129
+ // Each time it opens (from the trigger, the hotkey or `open`): an empty
130
+ // search over all the commands.
131
+ const latestSearch = useRef(search);
132
+ useEffect(() => { latestSearch.current = search; });
133
+ useEffect(() => {
134
+ if (!open)
135
+ return;
136
+ setLoaded([]);
137
+ latestSearch.current('');
138
+ }, [open]);
139
+ // The first result stays highlighted as the results change, so Enter picks it.
140
+ const resultKeys = results.map(keyOf).join('\u0000');
141
+ useEffect(() => {
142
+ if (!open)
143
+ return;
144
+ const id = setTimeout(() => api.current.highlightFirst(), 0);
145
+ return () => clearTimeout(id);
146
+ }, [open, resultKeys]);
147
+ useEffect(() => {
148
+ if (!hotkey)
149
+ return;
150
+ function keydown(e) {
151
+ if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== hotkey)
152
+ return;
153
+ e.preventDefault();
154
+ setOpen(!open);
155
+ }
156
+ addEventListener('keydown', keydown);
157
+ return () => removeEventListener('keydown', keydown);
158
+ }, [hotkey, open, setOpen]);
159
+ return (_jsxs(Dialog.Root, { open: open, onOpenChange: (details) => setOpen(details.open), lazyMount: true, unmountOnExit: true, initialFocusEl: () => input.current, children: [trigger && _jsx(Dialog.Trigger, { asChild: true, children: trigger }), _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, { ...part('backdrop') }), _jsx(Dialog.Positioner, { ...part('positioner'), children: _jsxs(Dialog.Content, { ...rest, ...part('content'), className: cx(className, classes?.content), children: [_jsx(Dialog.Title, { ...part('title'), hidden: true, children: label }), _jsxs(Listbox.RootProvider, { value: list, children: [_jsx(Listbox.Label, { hidden: true, children: label }), _jsxs("div", { ...part('search'), children: [_jsx("iconify-icon", { ...part('search-icon'), icon: "lucide:search", "aria-hidden": "true" }), _jsx(Listbox.Input, { ref: input, className: classes?.input, autoHighlight: true, type: "text", placeholder: placeholder, value: query, onChange: (e) => search(e.target.value) }), _jsx("kbd", { ...part('key'), "data-esc": "", children: t.command.esc })] }), _jsx(Listbox.Content, { ref: listEl, className: classes?.list, children: !results.length
160
+ ? _jsx("div", { ...part('status'), children: loading ? loadingText : emptyText })
161
+ : groups.map((group) => (_jsxs(Listbox.ItemGroup, { className: classes?.group, children: [group.name && _jsx(Listbox.ItemGroupLabel, { className: classes?.groupLabel, children: group.name }), group.items.map((item) => (_jsxs(Listbox.Item, { item: item, highlightOnHover: true, className: classes?.item, children: [item.icon && _jsx("iconify-icon", { ...part('icon'), icon: item.icon, "aria-hidden": "true" }), _jsxs("span", { ...part('text'), children: [_jsx("span", { ...part('label'), children: item.label }), item.description && _jsx("span", { ...part('description'), children: item.description })] }), item.shortcut && _jsx("kbd", { ...part('shortcut'), children: item.shortcut })] }, keyOf(item))))] }, group.name))) }), hints && (_jsxs("div", { ...part('hints'), "aria-hidden": "true", children: [_jsxs("span", { children: [_jsx("kbd", { ...part('key'), children: "\u2191" }), _jsx("kbd", { ...part('key'), children: "\u2193" }), " ", t.command.toNavigate] }), _jsxs("span", { children: [_jsx("kbd", { ...part('key'), children: "\u21B5" }), " ", t.command.toSelect] })] }))] })] }) })] })] }));
162
+ }
@@ -0,0 +1,47 @@
1
+ /* The listbox parts keep Zag's names (root, input, content, item,
2
+ item-group…), so they're styled inside the command's own content. */
3
+ @layer ui {
4
+ [data-scope=command][data-part=backdrop] { position: fixed; inset: 0; z-index: 100; background: rgba(0, 0, 0, 0.45);
5
+ &[data-state=open] { animation: ui-fade-in 120ms ease-out; }
6
+ &[data-state=closed] { animation: ui-fade-out 100ms ease-in forwards; } }
7
+ /* Near the top, so the panel stays put as results come and go. Clicks pass through to the backdrop. */
8
+ [data-scope=command][data-part=positioner] { position: fixed; inset: 0; z-index: 100; display: flex; justify-content: center; align-items: flex-start;
9
+ padding: 1rem; box-sizing: border-box; pointer-events: none;
10
+ @media (min-width: 640px) { padding-top: 12vh; } }
11
+ [data-scope=command][data-part=content] {
12
+ display: flex; flex-direction: column; width: 100%; max-width: 34rem; max-height: min(28rem, calc(100dvh - 32px)); box-sizing: border-box; overflow: hidden;
13
+ background: var(--ui-surface); color: var(--ui-text); font-family: var(--ui-font); font-size: 14px;
14
+ border: 1px solid var(--ui-border); border-radius: calc(var(--ui-radius) + 4px); box-shadow: var(--ui-shadow); outline: none; pointer-events: auto;
15
+ &[data-state=open] { animation: ui-command-in 140ms ease-out; }
16
+ &[data-state=closed] { animation: ui-command-out 100ms ease-in forwards; } }
17
+ [data-scope=command][data-part=content] [data-scope=listbox][data-part=root] { display: flex; flex-direction: column; min-height: 0; flex-grow: 1; }
18
+ [data-scope=command][data-part=search] { display: flex; align-items: center; gap: 0.5rem; padding: 0 0.875rem; height: 2.75rem; flex-shrink: 0; border-bottom: 1px solid var(--ui-border); }
19
+ [data-scope=command][data-part=search-icon] { color: var(--ui-muted); flex-shrink: 0; font-size: 16px; }
20
+ [data-scope=command][data-part=content] [data-scope=listbox][data-part=input] { flex-grow: 1; min-width: 0; height: 100%; padding: 0; border: none; background: transparent; color: inherit;
21
+ font: inherit; font-size: 14px; outline: none;
22
+ &::placeholder { color: var(--ui-muted); } }
23
+ [data-scope=command][data-part=key] { display: inline-flex; align-items: center; justify-content: center; min-width: 1.125rem; height: 1.125rem; padding: 0 0.25rem;
24
+ box-sizing: border-box; border: 1px solid var(--ui-border); border-radius: 4px; background: var(--ui-hover); color: var(--ui-muted); font-family: inherit; font-size: 11px; line-height: 1;
25
+ &[data-esc] { display: none; @media (min-width: 640px) { display: inline-flex; } } }
26
+ [data-scope=command][data-part=content] [data-scope=listbox][data-part=content] { flex-grow: 1; min-height: 0; overflow-y: auto; padding: 0.375rem; outline: none; }
27
+ [data-scope=command][data-part=status] { padding: 2.5rem 0; text-align: center; color: var(--ui-muted); }
28
+ [data-scope=command][data-part=content] [data-scope=listbox][data-part=item-group] + [data-part=item-group] { margin-top: 0.25rem; }
29
+ [data-scope=command][data-part=content] [data-scope=listbox][data-part=item-group-label] { padding: 0.375rem 0.5rem 0.25rem; font-size: 12px; font-weight: 500; color: var(--ui-muted); }
30
+ [data-scope=command][data-part=content] [data-scope=listbox][data-part=item] { display: flex; align-items: center; gap: 0.625rem; padding: 0.375rem 0.5rem; min-height: 2rem; box-sizing: border-box;
31
+ border-radius: var(--ui-radius); font-size: 13px; cursor: pointer; user-select: none;
32
+ &[data-highlighted] { background: var(--ui-hover); }
33
+ &[data-disabled] { opacity: 0.4; cursor: not-allowed; } }
34
+ [data-scope=command][data-part=icon] { font-size: 15px; flex-shrink: 0; color: var(--ui-muted); }
35
+ /* The description follows the label on the same line, cut short first. */
36
+ [data-scope=command][data-part=text] { display: flex; align-items: baseline; gap: 0.5rem; flex-grow: 1; min-width: 0; }
37
+ [data-scope=command][data-part=label] { flex-shrink: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
38
+ [data-scope=command][data-part=description] { min-width: 0; font-size: 12px; color: var(--ui-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
39
+ [data-scope=command][data-part=shortcut] { flex-shrink: 0; color: var(--ui-muted); font-family: inherit; font-size: 12px; }
40
+ [data-scope=command][data-part=hints] { display: none; gap: 1rem; padding: 0.5rem 0.875rem; flex-shrink: 0; border-top: 1px solid var(--ui-border); font-size: 12px; color: var(--ui-muted);
41
+ @media (min-width: 640px) { display: flex; }
42
+ & > span { display: flex; align-items: center; gap: 0.25rem; } }
43
+ @keyframes ui-command-in { from { opacity: 0; transform: scale(0.98); } }
44
+ @keyframes ui-command-out { to { opacity: 0; transform: scale(0.98); } }
45
+ @keyframes ui-fade-in { from { opacity: 0; } }
46
+ @keyframes ui-fade-out { to { opacity: 0; } }
47
+ }
@@ -0,0 +1,5 @@
1
+ @layer ui {
2
+ [data-scope=confirm][data-part=body] { display: flex; flex-direction: column; gap: 0.75rem; font-size: 14px; }
3
+ [data-scope=confirm][data-part=type] { display: flex; flex-direction: column; gap: 0.375rem; }
4
+ [data-scope=confirm][data-part=type-label] { color: var(--ui-muted); }
5
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Asks before doing something, from anywhere: a dialog you `await` for a yes
3
+ * or no, with no state to keep and nothing to put in the layout.
4
+ *
5
+ * if (await confirm({ heading: 'Archive Ada?', confirmLabel: 'Archive' })) archive(ada)
6
+ *
7
+ * await confirm({
8
+ * heading: 'Delete Ada Lovelace?', description: 'Her lessons and invoices go too.',
9
+ * danger: true, confirmLabel: 'Delete', typeToConfirm: 'Ada Lovelace',
10
+ * onConfirm: () => api.delete(ada.id), // the button spins until it's done
11
+ * })
12
+ *
13
+ * Resolves true when confirmed, false when cancelled (Cancel, Escape or the
14
+ * close button). Calls made while one is open wait their turn.
15
+ *
16
+ * - `heading`, `description` (text or markup); `content`: more under them
17
+ * - `confirmLabel` ('Confirm'), `cancelLabel` ('Cancel'; null for none, an
18
+ * acknowledgement)
19
+ * - `danger`: a red confirm button, for things that can't be undone
20
+ * - `typeToConfirm`: text to type before the button enables
21
+ * - `onConfirm`: runs before it closes, with the button loading; if it
22
+ * throws, the dialog stays open and shows the error's message
23
+ * - `size`: the dialog's size ('sm')
24
+ *
25
+ * It renders into its own root at the end of <body> the first time it's
26
+ * used (the library's CSS and tokens apply there too).
27
+ */
28
+ import { type ReactNode } from 'react';
29
+ import '../theme.css';
30
+ import './confirm.css';
31
+ export type ConfirmOptions = {
32
+ heading: ReactNode;
33
+ description?: ReactNode;
34
+ content?: ReactNode;
35
+ confirmLabel?: ReactNode;
36
+ cancelLabel?: ReactNode | null;
37
+ danger?: boolean;
38
+ typeToConfirm?: string | null;
39
+ onConfirm?: (() => unknown) | null;
40
+ size?: 'sm' | 'md' | 'lg';
41
+ };
42
+ export declare function confirm(options: ConfirmOptions): Promise<boolean>;
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Asks before doing something, from anywhere: a dialog you `await` for a yes
4
+ * or no, with no state to keep and nothing to put in the layout.
5
+ *
6
+ * if (await confirm({ heading: 'Archive Ada?', confirmLabel: 'Archive' })) archive(ada)
7
+ *
8
+ * await confirm({
9
+ * heading: 'Delete Ada Lovelace?', description: 'Her lessons and invoices go too.',
10
+ * danger: true, confirmLabel: 'Delete', typeToConfirm: 'Ada Lovelace',
11
+ * onConfirm: () => api.delete(ada.id), // the button spins until it's done
12
+ * })
13
+ *
14
+ * Resolves true when confirmed, false when cancelled (Cancel, Escape or the
15
+ * close button). Calls made while one is open wait their turn.
16
+ *
17
+ * - `heading`, `description` (text or markup); `content`: more under them
18
+ * - `confirmLabel` ('Confirm'), `cancelLabel` ('Cancel'; null for none, an
19
+ * acknowledgement)
20
+ * - `danger`: a red confirm button, for things that can't be undone
21
+ * - `typeToConfirm`: text to type before the button enables
22
+ * - `onConfirm`: runs before it closes, with the button loading; if it
23
+ * throws, the dialog stays open and shows the error's message
24
+ * - `size`: the dialog's size ('sm')
25
+ *
26
+ * It renders into its own root at the end of <body> the first time it's
27
+ * used (the library's CSS and tokens apply there too).
28
+ */
29
+ import { useState, useSyncExternalStore } from 'react';
30
+ import { createRoot } from 'react-dom/client';
31
+ import { Dialog } from '../dialog/Dialog.js';
32
+ import { Button } from '../button/Button.js';
33
+ import { Input } from '../input/Input.js';
34
+ import { Alert } from '../alert/Alert.js';
35
+ import { partsOf } from '../utils.js';
36
+ import { AmbientLocale, useMessages } from '../format/LocaleProvider.js';
37
+ import '../theme.css';
38
+ import './confirm.css';
39
+ // The open request first, then the ones waiting.
40
+ let queue = [];
41
+ let nextId = 1;
42
+ const listeners = new Set();
43
+ const emit = () => listeners.forEach((listener) => listener());
44
+ const subscribe = (listener) => {
45
+ listeners.add(listener);
46
+ return () => { listeners.delete(listener); };
47
+ };
48
+ let mounted = false;
49
+ function mount() {
50
+ if (mounted || typeof document === 'undefined')
51
+ return;
52
+ mounted = true;
53
+ const host = document.createElement('div');
54
+ host.setAttribute('data-confirm-host', '');
55
+ document.body.appendChild(host);
56
+ createRoot(host).render(_jsx(ConfirmHost, {}));
57
+ }
58
+ export function confirm(options) {
59
+ mount();
60
+ return new Promise((resolve) => {
61
+ queue = [...queue, { ...options, id: nextId++, resolve }];
62
+ emit();
63
+ });
64
+ }
65
+ // Gone from the queue once its dialog has animated out.
66
+ function remove(request) {
67
+ queue = queue.filter((other) => other !== request);
68
+ emit();
69
+ }
70
+ function ConfirmHost() {
71
+ const current = useSyncExternalStore(subscribe, () => queue[0] ?? null, () => null);
72
+ // A new dialog per request, so each starts with its own state.
73
+ return current ? _jsx(AmbientLocale, { children: _jsx(ConfirmDialog, { request: current }, current.id) }) : null;
74
+ }
75
+ function ConfirmDialog({ request }) {
76
+ const { heading, description = null, content = null, confirmLabel: confirmLabelProp, cancelLabel: cancelLabelProp, danger = false, typeToConfirm = null, onConfirm = null, size = 'sm', } = request;
77
+ const t = useMessages();
78
+ const confirmLabel = confirmLabelProp ?? t.common.confirm;
79
+ const cancelLabel = cancelLabelProp === undefined ? t.common.cancel : cancelLabelProp;
80
+ const part = partsOf('confirm');
81
+ const [typed, setTyped] = useState('');
82
+ const [busy, setBusy] = useState(false);
83
+ const [error, setError] = useState(null);
84
+ const [open, setOpen] = useState(true);
85
+ // Answers once: resolves the promise, then closes.
86
+ function settle(confirmed) {
87
+ if (!open)
88
+ return;
89
+ request.resolve(confirmed);
90
+ setOpen(false);
91
+ }
92
+ const ready = !typeToConfirm || typed.trim() === typeToConfirm;
93
+ async function yes() {
94
+ if (!ready || busy)
95
+ return;
96
+ if (onConfirm) {
97
+ setBusy(true);
98
+ setError(null);
99
+ try {
100
+ await onConfirm();
101
+ }
102
+ catch (e) {
103
+ setBusy(false);
104
+ setError(e instanceof Error ? e.message : String(e));
105
+ return;
106
+ }
107
+ }
108
+ settle(true);
109
+ }
110
+ return (_jsx(Dialog, { alert: true, open: open, onExitComplete: () => remove(request), size: size, heading: heading, description: description,
111
+ // Closing any other way (Escape, the close button) is a no, unless it's still working;
112
+ // it stays open (it's controlled) until it settles.
113
+ onOpenChange: (open) => { if (!open && !busy)
114
+ settle(false); }, footer: _jsxs(_Fragment, { children: [cancelLabel !== null && _jsx(Button, { disabled: busy, onClick: () => settle(false), children: cancelLabel }), _jsx(Button, { variant: danger ? 'danger' : 'primary', disabled: !ready, loading: busy, onClick: yes, children: confirmLabel })] }), children: (content || typeToConfirm || error) && (_jsxs("div", { ...part('body'), children: [content, typeToConfirm && (_jsxs("form", { ...part('type'), onSubmit: (e) => { e.preventDefault(); yes(); }, children: [_jsxs("span", { ...part('type-label'), children: [t.confirm.typePrefix, " ", _jsx("strong", { children: typeToConfirm }), " ", t.confirm.typeSuffix] }), _jsx(Input, { value: typed, onValueChange: setTyped, autoFocus: true, autoComplete: "off", "aria-label": t.confirm.typeToConfirm(typeToConfirm) })] })), error && _jsx(Alert, { variant: "danger", children: error })] })) }));
115
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copies `value` to the clipboard, then shows a tick and "Copied" for a moment.
3
+ *
4
+ * <CopyButton value={bookingLink} />
5
+ * <Input value={bookingLink} readOnly end={<CopyButton value={bookingLink} iconOnly />} />
6
+ *
7
+ * - `value`: the text it copies
8
+ * - `label`, `copiedLabel`: the text ('Copy' and 'Copied'); `iconOnly` hides
9
+ * it (it stays the accessible name)
10
+ * - `timeout`: how long it shows "Copied" (2000 ms)
11
+ * - `onCopy`: called with the value once it is copied
12
+ *
13
+ * Other attributes go on the <button>. By hand, Ark's Clipboard parts
14
+ * (Clipboard.Root, Trigger, Indicator…) from '@ark-ui/react/clipboard'.
15
+ */
16
+ import type { ComponentProps } from 'react';
17
+ import 'iconify-icon';
18
+ import { type Classes } from '../utils.js';
19
+ import '../theme.css';
20
+ import './copy-button.css';
21
+ export type CopyButtonProps = Omit<ComponentProps<'button'>, 'value' | 'onCopy'> & {
22
+ value?: string | number | null;
23
+ label?: string;
24
+ copiedLabel?: string;
25
+ iconOnly?: boolean;
26
+ timeout?: number;
27
+ onCopy?: ((value: string) => void) | null;
28
+ classes?: Classes<'trigger' | 'icon' | 'text'>;
29
+ };
30
+ export declare function CopyButton({ value, label: labelProp, copiedLabel: copiedLabelProp, iconOnly, timeout, onCopy, className, classes, ...rest }: CopyButtonProps): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Clipboard, useClipboard } from '@ark-ui/react/clipboard';
3
+ import 'iconify-icon';
4
+ import { cx, partsOf } from '../utils.js';
5
+ import { useMessages } from '../format/LocaleProvider.js';
6
+ import '../theme.css';
7
+ import './copy-button.css';
8
+ export function CopyButton({ value = '', label: labelProp, copiedLabel: copiedLabelProp, iconOnly = false, timeout = 2000, onCopy = null, className, classes, ...rest }) {
9
+ const t = useMessages();
10
+ const label = labelProp ?? t.copyButton.copy;
11
+ const copiedLabel = copiedLabelProp ?? t.copyButton.copied;
12
+ // Ark's clipboard stamps data-scope="clipboard"; ours goes last, for copy-button.css.
13
+ const part = partsOf('copy-button', classes);
14
+ const text = String(value ?? '');
15
+ const clipboard = useClipboard({
16
+ value: text, timeout,
17
+ ...(t.zag.clipboard && { translations: t.zag.clipboard }),
18
+ onStatusChange: (details) => { if (details.copied)
19
+ onCopy?.(text); },
20
+ });
21
+ const shown = clipboard.copied ? copiedLabel : label;
22
+ return (_jsx(Clipboard.RootProvider, { value: clipboard, asChild: true, children: _jsx("span", { style: { display: 'contents' }, children: _jsxs(Clipboard.Trigger, { ...rest, ...part('trigger'), className: cx(className, classes?.trigger), "data-icon-only": iconOnly || undefined, "aria-label": iconOnly ? shown : undefined, children: [_jsx("iconify-icon", { ...part('icon'), icon: clipboard.copied ? 'lucide:check' : 'lucide:copy', "aria-hidden": "true" }), !iconOnly && _jsx("span", { ...part('text'), "aria-live": "polite", children: shown })] }) }) }));
23
+ }
@@ -0,0 +1,12 @@
1
+ @layer ui {
2
+ [data-scope=copy-button][data-part=trigger] {
3
+ display: inline-flex; align-items: center; gap: 0.375rem; height: 1.75rem; padding: 0 0.5rem; box-sizing: border-box;
4
+ border: 1px solid var(--ui-border); background: var(--ui-surface); color: var(--ui-text); border-radius: calc(var(--ui-radius) - 2px);
5
+ font-family: var(--ui-font); font-size: 12px; font-weight: 500; white-space: nowrap; cursor: pointer; outline: none;
6
+ &:hover { background: var(--ui-hover); }
7
+ &:focus-visible { outline: 2px solid var(--ui-ring-soft); border-color: var(--ui-ring); }
8
+ &[data-icon-only] { width: 1.75rem; padding: 0; justify-content: center; }
9
+ }
10
+ [data-scope=copy-button][data-part=icon] { font-size: 14px; color: var(--ui-muted);
11
+ [data-copied] > & { color: #16a34a; } }
12
+ }
package/dist/css.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Components import their own CSS (shipped in dist, referenced from index.d.ts).
2
+ declare module '*.css'