@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,9 @@
1
+ export interface ConfirmDialogProps {
2
+ open: boolean;
3
+ onOpenChange(open: boolean): void;
4
+ title: string;
5
+ description: string;
6
+ confirmLabel: string;
7
+ onConfirm(): Promise<void>;
8
+ }
9
+ export declare function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, onConfirm }: ConfirmDialogProps): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PromptDialog } from '../PromptDialog.js';
3
+ /* One destructive step, asked before it happens, over whichever dialog it was opened from. */
4
+ export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, onConfirm }) {
5
+ return (_jsx(PromptDialog, { open: open, nested: true, danger: true, title: title, description: description, confirmLabel: confirmLabel, onConfirm: async () => {
6
+ await onConfirm();
7
+ onOpenChange(false);
8
+ }, onOpenChange: onOpenChange }));
9
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReactNode, Ref } from 'react';
2
+ export interface DetailHeaderProps {
3
+ mark: ReactNode;
4
+ title: string;
5
+ subtitle: ReactNode;
6
+ actions?: ReactNode;
7
+ className?: string;
8
+ ref?: Ref<HTMLElement>;
9
+ }
10
+ export declare function DetailHeader({ mark, title, subtitle, actions, className, ref }: DetailHeaderProps): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ /* The head of a detail: a mark, the name, a line under it, and the actions of the thing. */
4
+ export function DetailHeader({ mark, title, subtitle, actions, className, ref }) {
5
+ return (_jsxs("header", { ref: ref, className: clsx('flex min-w-0 flex-wrap items-start gap-3', className), children: [mark, _jsxs("div", { className: "min-w-0 grow basis-48", children: [_jsx("h3", { className: "truncate text-lg font-semibold text-text", children: title }), _jsx("div", { className: "flex min-w-0 items-center gap-1.5 text-xs break-words text-text-muted", children: subtitle })] }), actions] }));
6
+ }
@@ -0,0 +1,24 @@
1
+ import type { ReactNode, Ref } from 'react';
2
+ declare const LIST_WIDTHS: {
3
+ readonly 280: 'w-70';
4
+ readonly 320: 'w-80';
5
+ readonly 340: 'w-85';
6
+ };
7
+ export interface MasterDetailProps {
8
+ list: ReactNode;
9
+ listWidth: keyof typeof LIST_WIDTHS;
10
+ listLabel: string;
11
+ detail: ReactNode;
12
+ className?: string;
13
+ ref?: Ref<HTMLDivElement>;
14
+ }
15
+ export declare function MasterDetail({ list, listWidth, listLabel, detail, className, ref }: MasterDetailProps): import("react").JSX.Element;
16
+ export interface MasterItemProps {
17
+ selected: boolean;
18
+ onSelect(): void;
19
+ children: ReactNode;
20
+ className?: string;
21
+ ref?: Ref<HTMLButtonElement>;
22
+ }
23
+ export declare function MasterItem({ selected, onSelect, children, className, ref }: MasterItemProps): import("react").JSX.Element;
24
+ export {};
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ const LIST_WIDTHS = { 280: 'w-70', 320: 'w-80', 340: 'w-85' };
4
+ /*
5
+ * A list beside the detail of what is picked in it, for a pane of many things of one kind (accounts,
6
+ * devices, shortcut categories). Each side scrolls on its own, and the detail takes the rest of the
7
+ * width, like a single-column pane. Its section must be `split`, so the dialog hands the pane its whole height
8
+ * instead of a padded scrolling column.
9
+ */
10
+ export function MasterDetail({ list, listWidth, listLabel, detail, className, ref }) {
11
+ return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 min-w-0 grow border-t border-border max-[640px]:flex-col', className), children: [_jsx("nav", { "aria-label": listLabel, className: clsx('flex shrink-0 flex-col gap-0.5 overflow-y-auto border-r border-border p-2 max-[640px]:max-h-60 max-[640px]:w-auto max-[640px]:border-r-0 max-[640px]:border-b', LIST_WIDTHS[listWidth]), children: list }), _jsx("div", { className: "min-h-0 min-w-0 grow overflow-y-auto", children: _jsx("div", { className: "flex min-w-0 flex-col gap-7 px-8 pt-6 pb-10 max-[960px]:px-4", children: detail }) })] }));
12
+ }
13
+ /* One row of the list. The picked one is lifted a shade. */
14
+ export function MasterItem({ selected, onSelect, children, className, ref }) {
15
+ return (_jsx("button", { ref: ref, type: "button", "aria-current": selected ? 'true' : undefined, className: clsx('flex w-full min-w-0 shrink-0 items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-text focus-visible:-outline-offset-2', selected ? 'bg-text/5' : 'hover:bg-surface-hover', className), onClick: onSelect, children: children }));
16
+ }
@@ -0,0 +1,46 @@
1
+ import { type ComponentType, type ReactNode, type Ref } from 'react';
2
+ import { type LucideIcon } from 'lucide-react';
3
+ export interface SettingsSectionEntry {
4
+ id: string;
5
+ icon: LucideIcon;
6
+ label: string;
7
+ description: string;
8
+ pane: ComponentType;
9
+ split?: boolean;
10
+ hero?: ComponentType;
11
+ }
12
+ export interface SettingsGroupEntry {
13
+ label: string | null;
14
+ sections: readonly SettingsSectionEntry[];
15
+ }
16
+ export interface SettingsSearchResult {
17
+ section: string;
18
+ id: string | null;
19
+ label: string;
20
+ }
21
+ export interface SettingsSearch {
22
+ find(query: string): readonly SettingsSearchResult[];
23
+ hint?: string;
24
+ focusAt?: number;
25
+ }
26
+ export interface SettingsDialogProps {
27
+ open: boolean;
28
+ onOpenChange(open: boolean): void;
29
+ section: string;
30
+ onNavigate(next: {
31
+ section: string;
32
+ target?: string | null;
33
+ }): void;
34
+ groups: readonly SettingsGroupEntry[];
35
+ footer?: readonly SettingsSectionEntry[];
36
+ account?: {
37
+ section: SettingsSectionEntry;
38
+ tab: ReactNode;
39
+ };
40
+ search?: SettingsSearch;
41
+ target?: string | null;
42
+ onTargetShown?(): void;
43
+ className?: string;
44
+ ref?: Ref<HTMLDivElement>;
45
+ }
46
+ export declare function SettingsDialog({ open, onOpenChange, section, onNavigate, groups, footer, account, search, target, onTargetShown, className, ref }: SettingsDialogProps): import("react").JSX.Element;
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
3
+ import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Tabs } from '@base-ui-components/react/tabs';
6
+ import { Search, X } from 'lucide-react';
7
+ import { CloseButton } from '../CloseButton.js';
8
+ import { DialogDescription, DialogPopup, DialogRoot, DialogTitle } from '../dialog/parts.js';
9
+ import { ErrorBoundary } from '../ErrorBoundary.js';
10
+ import { Icon } from '../Icon.js';
11
+ import { IconButton } from '../IconButton.js';
12
+ import { Input } from '../Input.js';
13
+ import { Select } from '../Select.js';
14
+ import { SettingsTargetContext } from './target.js';
15
+ /* How far a pane with a hero scrolls before the title bar over it is filled in. */
16
+ const BAR_FILL_DISTANCE = 70;
17
+ const NAV_ITEM = 'flex h-8 min-w-0 shrink-0 items-center gap-2.5 rounded-md px-2.5 text-sm focus-visible:-outline-offset-2 text-text-muted hover:bg-surface-hover hover:text-text data-active:bg-surface-active data-active:text-text';
18
+ function NavTab({ section }) {
19
+ return (_jsxs(Tabs.Tab, { value: section.id, className: NAV_ITEM, children: [_jsx(Icon, { icon: section.icon, size: 16, className: "shrink-0 text-text-faint" }), _jsx("span", { className: "truncate", children: section.label })] }));
20
+ }
21
+ function SettingsNav({ groups, footer, account }) {
22
+ const { t } = useTranslation('ui');
23
+ return (_jsxs(Tabs.List, { "aria-label": t('settings.sections'), activateOnFocus: true, className: "flex min-h-0 grow flex-col", children: [_jsx("div", { className: "flex min-h-0 flex-col gap-3.5 overflow-y-auto", children: groups.map((group) => (_jsxs("div", { className: "flex flex-col gap-0.5", children: [group.label !== null && _jsx("span", { className: "px-2.5 pt-2.5 pb-1 text-xs font-medium text-text-faint", children: group.label }), group.sections.map((section) => (_jsx(NavTab, { section: section }, section.id)))] }, group.sections[0].id))) }), (footer?.length || account) && (_jsxs("div", { className: "mt-auto flex flex-col gap-3 pt-3.5", children: [footer?.map((section) => (_jsx(NavTab, { section: section }, section.id))), account && _jsx("div", { className: "-mx-3 -mb-1 border-t border-border px-3 pt-3", children: account.tab })] }))] }));
24
+ }
25
+ function SearchField({ search, query, onQuery, onPick }) {
26
+ const { t } = useTranslation('ui');
27
+ const input = useRef(null);
28
+ const focusAt = search.focusAt ?? 0;
29
+ useEffect(() => {
30
+ if (focusAt > 0) {
31
+ input.current?.focus();
32
+ input.current?.select();
33
+ }
34
+ }, [focusAt]);
35
+ return (_jsxs("div", { className: "relative shrink-0", children: [_jsx(Icon, { icon: Search, size: 14, className: "pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-text-faint" }), _jsx(Input, { ref: input, className: "bg-surface-sunken pr-12 pl-8 text-xs", placeholder: t('settings.search.placeholder'), "aria-label": t('settings.search.placeholder'), value: query, spellCheck: false, autoComplete: "off", onChange: (event) => onQuery(event.target.value), onKeyDown: (event) => {
36
+ // Escape clears the search first; the dialog only closes on an empty field.
37
+ if (event.key === 'Escape' && query) {
38
+ event.stopPropagation();
39
+ onQuery('');
40
+ }
41
+ if (event.key === 'Enter') {
42
+ const first = search.find(query)[0];
43
+ if (first) {
44
+ onPick(first);
45
+ }
46
+ }
47
+ } }), query ? (_jsx(IconButton, { icon: X, size: "sm", label: t('settings.search.clear'), tooltip: false, className: "absolute top-1/2 right-1 -translate-y-1/2", onClick: () => onQuery('') })) : (search.hint && (_jsx("span", { className: "pointer-events-none absolute top-1/2 right-2.5 -translate-y-1/2 text-xs text-text-faint", "aria-hidden": true, children: search.hint })))] }));
48
+ }
49
+ /* In place of the navigation while a query is typed: the panes and rows that match, each a jump to where it lives. */
50
+ function SearchResults({ search, query, labelOf, onPick }) {
51
+ const { t, i18n } = useTranslation('ui');
52
+ // The language is a dependency the linter cannot see; every result reads its words off i18next.
53
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
54
+ const results = useMemo(() => search.find(query), [search.find, query, i18n.language]);
55
+ if (results.length === 0) {
56
+ return _jsx("p", { className: "px-2.5 py-1 text-xs break-words text-text-muted", children: t('settings.search.none', { query: query.trim() }) });
57
+ }
58
+ return (_jsx("ul", { className: "flex min-h-0 flex-col gap-0.5 overflow-y-auto", "aria-label": t('settings.search.results'), children: results.map((result) => (_jsx("li", { children: _jsxs("button", { type: "button", className: "flex w-full min-w-0 flex-col rounded-md px-2.5 py-1.5 text-left hover:bg-surface-hover", onClick: () => onPick(result), children: [_jsx("span", { className: "truncate text-sm text-text", children: result.label }), _jsx("span", { className: "truncate text-xs text-text-faint", children: result.id === null ? t('settings.search.pane') : labelOf(result.section) })] }) }, `${result.section}:${result.id ?? ''}`))) }));
59
+ }
60
+ /* Sections on the left, one pane on the right. A pane mounts only while its section is open. */
61
+ export function SettingsDialog({ open, onOpenChange, section, onNavigate, groups, footer = [], account, search, target = null, onTargetShown, className, ref }) {
62
+ const { t } = useTranslation('ui');
63
+ const [query, setQuery] = useState('');
64
+ const sections = [...groups.flatMap((group) => group.sections), ...footer, ...(account ? [account.section] : [])];
65
+ const meta = sections.find((entry) => entry.id === section) ?? sections[0];
66
+ const items = sections.map((entry) => ({ value: entry.id, label: entry.label }));
67
+ const searching = search !== undefined && query.trim() !== '';
68
+ const floating = meta.hero !== undefined && !meta.split;
69
+ const barFill = useRef(null);
70
+ // The latest callback behind a stable one, so a lit row's timer does not start over on every render.
71
+ const latestShown = useRef(onTargetShown);
72
+ useEffect(() => {
73
+ latestShown.current = onTargetShown;
74
+ });
75
+ const targetValue = useMemo(() => ({ target, shown: () => latestShown.current?.() }), [target]);
76
+ const labelOf = (id) => sections.find((entry) => entry.id === id)?.label ?? id;
77
+ const pick = (result) => onNavigate({ section: result.section, target: result.id });
78
+ return (_jsx(DialogRoot, { open: open, onOpenChange: (next) => {
79
+ onOpenChange(next);
80
+ if (!next) {
81
+ setQuery('');
82
+ }
83
+ }, children: _jsxs(DialogPopup, { ref: ref, className: clsx('flex h-[760px] w-[1200px]', className), children: [_jsxs(Tabs.Root, { value: meta.id, onValueChange: (value) => onNavigate({ section: value }), orientation: "vertical", className: "flex min-h-0 min-w-0 grow max-[640px]:flex-col", children: [_jsxs("div", { className: "flex w-58 shrink-0 flex-col gap-3.5 border-r border-border bg-surface bg-clip-padding px-3 py-4 max-[960px]:w-48 max-[640px]:w-auto max-[640px]:flex-row max-[640px]:items-center max-[640px]:border-r-0 max-[640px]:border-b", children: [_jsx(DialogTitle, { className: "px-2.5 pt-1 max-[640px]:pt-0", children: t('settings.title') }), _jsxs("div", { className: "contents max-[640px]:hidden", children: [search && _jsx(SearchField, { search: search, query: query, onQuery: setQuery, onPick: pick }), searching ? (_jsx(SearchResults, { search: search, query: query, labelOf: labelOf, onPick: pick })) : (_jsx(SettingsNav, { groups: groups, footer: footer, account: account }))] }), _jsx("div", { className: "min-w-0 grow min-[641px]:hidden", children: _jsx(Select, { value: meta.id, items: items, onValueChange: (value) => onNavigate({ section: value }), label: t('settings.section') }) })] }), _jsxs("div", { className: "relative flex min-h-0 min-w-0 grow flex-col", children: [_jsxs("div", { className: clsx('flex min-w-0 items-start gap-4 px-8 pt-5.5 pb-4.5 max-[960px]:px-4', floating && 'absolute inset-x-0 top-0 z-10'), children: [floating && (_jsx("div", { ref: barFill, className: "absolute inset-0 -z-10 border-b border-border bg-surface-raised opacity-0", "aria-hidden": true }, meta.id)), _jsxs("div", { className: "min-w-0 grow", children: [_jsx("h2", { className: "text-lg font-semibold text-text", children: meta.label }), _jsx("p", { className: "mt-0.5 text-xs break-words text-text-muted", children: meta.description })] }), _jsx(CloseButton, { label: t('settings.close'), dialog: true })] }), _jsx(SettingsTargetContext, { value: targetValue, children: sections.map((entry) => {
84
+ const Pane = entry.pane;
85
+ const Hero = entry.split ? undefined : entry.hero;
86
+ return (_jsx(Tabs.Panel, { value: entry.id, keepMounted: false, className: clsx('flex min-h-0 min-w-0 grow flex-col outline-none',
87
+ // A split pane scrolls each of its sides itself.
88
+ !entry.split && 'gap-7 overflow-y-auto px-8 pb-10 max-[960px]:px-4',
89
+ // Under a hero the bar's fill marks the edge; a fade would wipe the hero out behind the clear bar.
90
+ !entry.split && !Hero && 'scroll-fade-top pt-1'), onScroll: (event) => {
91
+ const top = event.currentTarget.scrollTop;
92
+ event.currentTarget.toggleAttribute('data-fade-start', top > 0);
93
+ barFill.current?.style.setProperty('opacity', String(Math.min(top / BAR_FILL_DISTANCE, 1)));
94
+ }, children: _jsx(ErrorBoundary, { label: t('settings.failed'), className: "min-h-0 grow", children: _jsxs(Suspense, { fallback: null, children: [Hero && (_jsx("div", { className: "-mx-8 shrink-0 max-[960px]:-mx-4", children: _jsx(Hero, {}) })), _jsx(Pane, {})] }) }) }, entry.id));
95
+ }) })] })] }), _jsx(DialogDescription, { className: "sr-only", children: meta.description })] }) }));
96
+ }
@@ -0,0 +1,22 @@
1
+ import { type ReactNode, type Ref } from 'react';
2
+ import type { LucideIcon } from 'lucide-react';
3
+ export interface SettingsRowProps {
4
+ label: ReactNode;
5
+ description?: ReactNode;
6
+ control?: ReactNode;
7
+ children?: ReactNode;
8
+ muted?: boolean;
9
+ leading?: ReactNode;
10
+ indent?: boolean;
11
+ searchId?: string;
12
+ className?: string;
13
+ ref?: Ref<HTMLDivElement>;
14
+ }
15
+ export interface TopIconProps {
16
+ icon: LucideIcon;
17
+ size?: number;
18
+ className?: string;
19
+ ref?: Ref<HTMLSpanElement>;
20
+ }
21
+ export declare function TopIcon({ icon, size, className, ref }: TopIconProps): import("react").JSX.Element;
22
+ export declare function SettingsRow({ label, description, control, children, muted, leading, indent, searchId, className, ref }: SettingsRowProps): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import clsx from 'clsx';
4
+ import { Icon } from '../Icon.js';
5
+ import { mergeRefs } from '../merge-refs.js';
6
+ import { useSettingsTarget } from './target.js';
7
+ /* How long a row a search result jumped to stays lit. */
8
+ const TARGET_MS = 1600;
9
+ /* An icon beside text that may wrap: the box is one line of `text-sm` high, so the icon sits on the first line. */
10
+ export function TopIcon({ icon, size = 16, className, ref }) {
11
+ return (_jsx("span", { ref: ref, className: "flex h-5.5 shrink-0 items-center", children: _jsx(Icon, { icon: icon, size: size, className: className }) }));
12
+ }
13
+ /*
14
+ * One setting: what it is on the left, the control on the right. Where the two do not fit side by
15
+ * side (a tablet, a narrow window) the control wraps under the label instead of widening the dialog.
16
+ */
17
+ export function SettingsRow({ label, description, control, children, muted = false, leading, indent = false, searchId, className, ref }) {
18
+ const row = useRef(null);
19
+ const { target, shown } = useSettingsTarget();
20
+ const targeted = searchId !== undefined && target === searchId;
21
+ useEffect(() => {
22
+ if (!targeted) {
23
+ return;
24
+ }
25
+ row.current?.scrollIntoView({ block: 'center' });
26
+ const timer = window.setTimeout(shown, TARGET_MS);
27
+ return () => window.clearTimeout(timer);
28
+ }, [targeted, shown]);
29
+ return (_jsxs("div", { ref: mergeRefs(row, ref), "data-settings-row": searchId, className: clsx('flex min-w-0 flex-col gap-3 pr-4.5 transition-colors duration-500 first:rounded-t-xl last:rounded-b-xl', indent ? 'pl-14.5' : 'pl-4.5', description || children || leading ? 'py-3.5' : 'py-3', targeted && 'bg-accent-soft duration-0', className), children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-5 gap-y-2", children: [_jsxs("div", { className: "flex min-w-0 grow basis-60 items-start gap-3", children: [leading, _jsxs("div", { className: "min-w-0 grow", children: [_jsx("div", { className: clsx('text-sm break-words', muted ? 'text-text-muted' : 'text-text'), children: label }), description && _jsx("div", { className: "mt-0.5 text-xs break-words text-text-muted", children: description })] })] }), control && _jsx("div", { className: "ml-auto flex max-w-full min-w-0 flex-wrap items-center justify-end gap-2", children: control })] }), children] }));
30
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode, Ref } from 'react';
2
+ import type { LucideIcon } from 'lucide-react';
3
+ export interface SettingsSectionProps {
4
+ title?: string;
5
+ description?: ReactNode;
6
+ icon?: LucideIcon;
7
+ tag?: ReactNode;
8
+ action?: ReactNode;
9
+ footer?: ReactNode;
10
+ children: ReactNode;
11
+ className?: string;
12
+ ref?: Ref<HTMLElement>;
13
+ }
14
+ export declare function SettingsSection({ title, description, icon, tag, action, footer, children, className, ref }: SettingsSectionProps): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { Icon } from '../Icon.js';
4
+ /* A titled group of rows in one card; rows divide themselves with a hairline. Without a title it is the card alone. */
5
+ export function SettingsSection({ title, description, icon, tag, action, footer, children, className, ref }) {
6
+ const hasHeader = title !== undefined || description !== undefined || tag !== undefined || action !== undefined;
7
+ return (_jsxs("section", { ref: ref, className: clsx('flex min-w-0 flex-col gap-2.5', className), "aria-label": title, children: [hasHeader && (_jsxs("div", { className: "flex min-w-0 flex-wrap items-end gap-x-3 gap-y-2", children: [_jsxs("div", { className: "min-w-0 grow basis-48", children: [title !== undefined && (_jsxs("h3", { className: "flex items-center gap-2 text-sm font-medium text-text", children: [icon && _jsx(Icon, { icon: icon, size: 14, className: "shrink-0 text-text-faint" }), title] })), description !== undefined && _jsx("p", { className: "mt-0.5 text-xs break-words text-text-muted", children: description })] }), action, tag] })), _jsx("div", { className: "min-w-0 divide-y divide-border rounded-xl border border-border bg-surface bg-clip-padding", children: children }), footer && _jsx("p", { className: "text-xs text-text-faint", children: footer })] }));
8
+ }
@@ -0,0 +1,7 @@
1
+ export { ConfirmDialog, type ConfirmDialogProps } from './ConfirmDialog.tsx';
2
+ export { DetailHeader, type DetailHeaderProps } from './DetailHeader.tsx';
3
+ export { MasterDetail, MasterItem, type MasterDetailProps, type MasterItemProps } from './MasterDetail.tsx';
4
+ export { SettingsDialog, type SettingsDialogProps, type SettingsGroupEntry, type SettingsSearch, type SettingsSearchResult, type SettingsSectionEntry } from './SettingsDialog.tsx';
5
+ export { SettingsRow, TopIcon, type SettingsRowProps, type TopIconProps } from './SettingsRow.tsx';
6
+ export { SettingsSection, type SettingsSectionProps } from './SettingsSection.tsx';
7
+ export { useSettingsTarget } from './target.ts';
@@ -0,0 +1,8 @@
1
+ /* The settings dialog and the parts a pane is built from. */
2
+ export { ConfirmDialog } from './ConfirmDialog.js';
3
+ export { DetailHeader } from './DetailHeader.js';
4
+ export { MasterDetail, MasterItem } from './MasterDetail.js';
5
+ export { SettingsDialog } from './SettingsDialog.js';
6
+ export { SettingsRow, TopIcon } from './SettingsRow.js';
7
+ export { SettingsSection } from './SettingsSection.js';
8
+ export { useSettingsTarget } from './target.js';
@@ -0,0 +1,7 @@
1
+ interface SettingsTarget {
2
+ target: string | null;
3
+ shown(): void;
4
+ }
5
+ export declare const SettingsTargetContext: import("react").Context<SettingsTarget>;
6
+ export declare const useSettingsTarget: () => SettingsTarget;
7
+ export {};
@@ -0,0 +1,3 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const SettingsTargetContext = createContext({ target: null, shown: () => { } });
3
+ export const useSettingsTarget = () => useContext(SettingsTargetContext);
@@ -0,0 +1,38 @@
1
+ import type { Shortcut } from './shortcut.ts';
2
+ export declare const HINT_DELAY_MS = 500;
3
+ export interface HintTarget {
4
+ id: number;
5
+ shortcut: Shortcut;
6
+ }
7
+ export declare const registerShortcutHint: (element: Element, shortcut: Shortcut) => (() => void);
8
+ export declare const shortcutHintTargets: () => ReadonlyMap<Element, HintTarget>;
9
+ export type HoldKey = Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey' | 'repeat'>;
10
+ export declare class ModifierHold {
11
+ private readonly apple;
12
+ private readonly onChange;
13
+ private timer;
14
+ private shown;
15
+ constructor(apple: boolean, onChange: (shown: boolean) => void);
16
+ keyDown(event: HoldKey): void;
17
+ keyUp(event: Pick<KeyboardEvent, 'key'>): void;
18
+ cancel(): void;
19
+ private modifierKey;
20
+ private otherModifierHeld;
21
+ }
22
+ export interface HintRect {
23
+ left: number;
24
+ right: number;
25
+ top: number;
26
+ bottom: number;
27
+ }
28
+ export interface HintPlacement {
29
+ left?: number;
30
+ right?: number;
31
+ top: number;
32
+ translateX: '0' | '-50%';
33
+ translateY: '0' | '-100%';
34
+ }
35
+ export declare const placeHint: (rect: HintRect, viewport: {
36
+ width: number;
37
+ height: number;
38
+ }) => HintPlacement;
@@ -0,0 +1,81 @@
1
+ /* How long the modifier has to be held on its own before every shortcut shows. */
2
+ export const HINT_DELAY_MS = 500;
3
+ const targets = new Map();
4
+ let nextId = 0;
5
+ /* Called by every `Tooltip` whose `kbd` is a real shortcut; a phrase about a key is no shortcut to print. */
6
+ export const registerShortcutHint = (element, shortcut) => {
7
+ nextId += 1;
8
+ targets.set(element, { id: nextId, shortcut });
9
+ return () => {
10
+ targets.delete(element);
11
+ };
12
+ };
13
+ export const shortcutHintTargets = () => targets;
14
+ /*
15
+ * Whether the hints are up: the platform's modifier (Cmd on macOS, Ctrl elsewhere) held on its own for
16
+ * `HINT_DELAY_MS`. Any other key, the modifier coming up or the caller cancelling takes them down, so
17
+ * a shortcut pressed in passing never flashes them.
18
+ */
19
+ export class ModifierHold {
20
+ apple;
21
+ onChange;
22
+ timer = null;
23
+ shown = false;
24
+ constructor(apple, onChange) {
25
+ this.apple = apple;
26
+ this.onChange = onChange;
27
+ }
28
+ keyDown(event) {
29
+ // A repeat is the key still held; only a fresh press starts the wait.
30
+ if (event.repeat && event.key === this.modifierKey()) {
31
+ return;
32
+ }
33
+ this.cancel();
34
+ if (event.key === this.modifierKey() && !this.otherModifierHeld(event)) {
35
+ this.timer = setTimeout(() => {
36
+ this.timer = null;
37
+ this.shown = true;
38
+ this.onChange(true);
39
+ }, HINT_DELAY_MS);
40
+ }
41
+ }
42
+ keyUp(event) {
43
+ if (event.key === this.modifierKey()) {
44
+ this.cancel();
45
+ }
46
+ }
47
+ cancel() {
48
+ if (this.timer !== null) {
49
+ clearTimeout(this.timer);
50
+ this.timer = null;
51
+ }
52
+ if (this.shown) {
53
+ this.shown = false;
54
+ this.onChange(false);
55
+ }
56
+ }
57
+ modifierKey() {
58
+ return this.apple ? 'Meta' : 'Control';
59
+ }
60
+ otherModifierHeld(event) {
61
+ return event.altKey || event.shiftKey || (this.apple ? event.ctrlKey : event.metaKey);
62
+ }
63
+ }
64
+ const HINT_GAP_PX = 2;
65
+ /* Room a hint needs to be centered under its button; nearer a window edge it lines up with that edge instead. */
66
+ const EDGE_ROOM_PX = 48;
67
+ const HINT_HEIGHT_PX = 24;
68
+ /* Under the button, centered, and above it where the window ends first. */
69
+ export const placeHint = (rect, viewport) => {
70
+ const below = rect.bottom + HINT_GAP_PX + HINT_HEIGHT_PX <= viewport.height;
71
+ const top = Math.round(below ? rect.bottom + HINT_GAP_PX : rect.top - HINT_GAP_PX);
72
+ const translateY = below ? '0' : '-100%';
73
+ const center = (rect.left + rect.right) / 2;
74
+ if (center < EDGE_ROOM_PX) {
75
+ return { left: Math.round(rect.left), top, translateX: '0', translateY };
76
+ }
77
+ if (center > viewport.width - EDGE_ROOM_PX) {
78
+ return { right: Math.round(viewport.width - rect.right), top, translateX: '0', translateY };
79
+ }
80
+ return { left: Math.round(center), top, translateX: '-50%', translateY };
81
+ };
@@ -0,0 +1,32 @@
1
+ export interface Shortcut {
2
+ mod: boolean;
3
+ ctrl: boolean;
4
+ meta: boolean;
5
+ alt: boolean;
6
+ shift: boolean;
7
+ key: string;
8
+ }
9
+ export type KeyLike = Pick<KeyboardEvent, 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey' | 'key' | 'code'>;
10
+ /**
11
+ * Parses `Mod+Shift+K`. Throws on a word it does not know or on two keys, so a typo in a table fails
12
+ * the moment the module loads instead of leaving a shortcut that never fires.
13
+ */
14
+ export declare const shortcut: (text: string) => Shortcut;
15
+ export declare const matchesShortcut: (target: Shortcut, event: KeyLike, apple: boolean) => boolean;
16
+ export declare const shortcutParts: (target: Shortcut, apple: boolean) => string[];
17
+ export declare const formatShortcut: (target: Shortcut, apple: boolean) => string;
18
+ export declare const EDIT_SHORTCUTS: {
19
+ readonly copy: Shortcut;
20
+ readonly cut: Shortcut;
21
+ readonly paste: Shortcut;
22
+ readonly selectAll: Shortcut;
23
+ };
24
+ export declare const KEY_SHORTCUTS: {
25
+ readonly enter: Shortcut;
26
+ readonly modEnter: Shortcut;
27
+ readonly backspace: Shortcut;
28
+ readonly escape: Shortcut;
29
+ readonly shift: Shortcut;
30
+ readonly rename: Shortcut;
31
+ };
32
+ export declare const isModHeld: (event: Pick<KeyLike, 'metaKey' | 'ctrlKey'>, apple: boolean) => boolean;
@@ -0,0 +1,137 @@
1
+ /* Matched on the physical key: Shift turns `[` into `{` and Option turns most keys into a dead key. */
2
+ const PUNCTUATION_CODES = {
3
+ ',': 'Comma',
4
+ '.': 'Period',
5
+ '/': 'Slash',
6
+ ';': 'Semicolon',
7
+ "'": 'Quote',
8
+ '`': 'Backquote',
9
+ '\\': 'Backslash',
10
+ '[': 'BracketLeft',
11
+ ']': 'BracketRight'
12
+ };
13
+ /* Matched on the character, because `+` sits on a different key per layout. */
14
+ const CHARACTER_KEYS = new Set(['+', '-', '=']);
15
+ const NAMED_KEYS = new Set(['Enter', 'Escape', 'Backspace', 'Delete', 'Tab', 'Space', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']);
16
+ const FUNCTION_KEY = /^F([1-9]|1[0-2])$/;
17
+ const MODIFIER_WORDS = ['Mod', 'Ctrl', 'Meta', 'Alt', 'Shift'];
18
+ const isKey = (word) => /^[A-Za-z0-9]$/.test(word) || word in PUNCTUATION_CODES || CHARACTER_KEYS.has(word) || NAMED_KEYS.has(word) || FUNCTION_KEY.test(word);
19
+ /**
20
+ * Parses `Mod+Shift+K`. Throws on a word it does not know or on two keys, so a typo in a table fails
21
+ * the moment the module loads instead of leaving a shortcut that never fires.
22
+ */
23
+ export const shortcut = (text) => {
24
+ // A trailing `++` is the plus key itself, which a plain split would read as two empty words.
25
+ const words = text === '+' ? ['+'] : text.endsWith('++') ? [...text.slice(0, -2).split('+'), '+'] : text.split('+');
26
+ const result = { mod: false, ctrl: false, meta: false, alt: false, shift: false, key: '' };
27
+ for (const word of words) {
28
+ const modifier = MODIFIER_WORDS.find((candidate) => candidate === word);
29
+ if (modifier) {
30
+ result[modifier === 'Mod' ? 'mod' : modifier === 'Ctrl' ? 'ctrl' : modifier === 'Meta' ? 'meta' : modifier === 'Alt' ? 'alt' : 'shift'] = true;
31
+ continue;
32
+ }
33
+ if (!isKey(word)) {
34
+ throw new Error(`Unknown key "${word}" in shortcut "${text}"`);
35
+ }
36
+ if (result.key !== '') {
37
+ throw new Error(`Shortcut "${text}" names two keys`);
38
+ }
39
+ result.key = word.length === 1 ? word.toUpperCase() : word;
40
+ }
41
+ return result;
42
+ };
43
+ // Letters and digits match on `code`: with Shift held `key` says `!` for 1, and Option's dead keys change it on macOS.
44
+ const matchesKey = (key, event) => {
45
+ if (/^[A-Z]$/.test(key)) {
46
+ return event.code === `Key${key}`;
47
+ }
48
+ if (/^[0-9]$/.test(key)) {
49
+ return event.code === `Digit${key}`;
50
+ }
51
+ const code = PUNCTUATION_CODES[key];
52
+ if (code) {
53
+ return event.code === code;
54
+ }
55
+ if (key === 'Space') {
56
+ return event.code === 'Space';
57
+ }
58
+ return event.key === key;
59
+ };
60
+ /* Strict: every modifier the shortcut does not name has to be up, so Ctrl+K on macOS is not Cmd+K. */
61
+ export const matchesShortcut = (target, event, apple) => {
62
+ const meta = target.meta || (target.mod && apple);
63
+ const ctrl = target.ctrl || (target.mod && !apple);
64
+ if (target.key === '' || event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== target.alt || event.shiftKey !== target.shift) {
65
+ return false;
66
+ }
67
+ return matchesKey(target.key, event);
68
+ };
69
+ const KEY_LABELS = {
70
+ Enter: { apple: '↩', other: 'Enter' },
71
+ Backspace: { apple: '⌫', other: 'Backspace' },
72
+ Delete: { apple: '⌦', other: 'Delete' },
73
+ Escape: { apple: 'Esc', other: 'Esc' },
74
+ Tab: { apple: '⇥', other: 'Tab' },
75
+ Space: { apple: 'Space', other: 'Space' },
76
+ ArrowLeft: { apple: '←', other: '←' },
77
+ ArrowRight: { apple: '→', other: '→' },
78
+ ArrowUp: { apple: '↑', other: '↑' },
79
+ ArrowDown: { apple: '↓', other: '↓' }
80
+ };
81
+ /* The modifiers and the key as separate caps, in the order the platform prints them. */
82
+ export const shortcutParts = (target, apple) => {
83
+ const parts = [];
84
+ if (apple) {
85
+ if (target.ctrl) {
86
+ parts.push('⌃');
87
+ }
88
+ if (target.alt) {
89
+ parts.push('⌥');
90
+ }
91
+ if (target.shift) {
92
+ parts.push('⇧');
93
+ }
94
+ if (target.meta || target.mod) {
95
+ parts.push('⌘');
96
+ }
97
+ }
98
+ else {
99
+ if (target.ctrl || target.mod) {
100
+ parts.push('Ctrl');
101
+ }
102
+ if (target.alt) {
103
+ parts.push('Alt');
104
+ }
105
+ if (target.shift) {
106
+ parts.push('Shift');
107
+ }
108
+ if (target.meta) {
109
+ parts.push('Meta');
110
+ }
111
+ }
112
+ if (target.key !== '') {
113
+ const label = KEY_LABELS[target.key];
114
+ parts.push(label ? label[apple ? 'apple' : 'other'] : target.key);
115
+ }
116
+ return parts;
117
+ };
118
+ /* `⌥⇧⌘K` on macOS, `Ctrl+Alt+Shift+K` elsewhere. */
119
+ export const formatShortcut = (target, apple) => shortcutParts(target, apple).join(apple ? '' : '+');
120
+ /* The browser's own editing shortcuts, which the app only ever prints in a menu. */
121
+ export const EDIT_SHORTCUTS = {
122
+ copy: /* @__PURE__ */ shortcut('Mod+C'),
123
+ cut: /* @__PURE__ */ shortcut('Mod+X'),
124
+ paste: /* @__PURE__ */ shortcut('Mod+V'),
125
+ selectAll: /* @__PURE__ */ shortcut('Mod+A')
126
+ };
127
+ /* Keys that are not a shortcut of their own but are printed as one in a hint. */
128
+ export const KEY_SHORTCUTS = {
129
+ enter: /* @__PURE__ */ shortcut('Enter'),
130
+ modEnter: /* @__PURE__ */ shortcut('Mod+Enter'),
131
+ backspace: /* @__PURE__ */ shortcut('Backspace'),
132
+ escape: /* @__PURE__ */ shortcut('Escape'),
133
+ shift: /* @__PURE__ */ shortcut('Shift'),
134
+ rename: /* @__PURE__ */ shortcut('F2')
135
+ };
136
+ /* Whether the platform's own modifier is down during a pointer gesture: Cmd on macOS, Ctrl elsewhere. */
137
+ export const isModHeld = (event, apple) => (apple ? event.metaKey : event.ctrlKey);
@@ -0,0 +1 @@
1
+ export {};