@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.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/AccentSwatches.d.ts +15 -0
- package/dist/AccentSwatches.js +21 -0
- package/dist/Banner.d.ts +12 -0
- package/dist/Banner.js +18 -0
- package/dist/Button.d.ts +9 -0
- package/dist/Button.js +28 -0
- package/dist/ButtonGroup.d.ts +3 -0
- package/dist/ButtonGroup.js +11 -0
- package/dist/Checkbox.d.ts +11 -0
- package/dist/Checkbox.js +9 -0
- package/dist/ChoiceCards.d.ts +23 -0
- package/dist/ChoiceCards.js +21 -0
- package/dist/CloseButton.d.ts +15 -0
- package/dist/CloseButton.js +11 -0
- package/dist/ColorSwatch.d.ts +14 -0
- package/dist/ColorSwatch.js +28 -0
- package/dist/ColumnResizeHandle.d.ts +9 -0
- package/dist/ColumnResizeHandle.js +12 -0
- package/dist/DisabledReason.d.ts +6 -0
- package/dist/DisabledReason.js +8 -0
- package/dist/DockShell.d.ts +9 -0
- package/dist/DockShell.js +49 -0
- package/dist/EmptyState.d.ts +16 -0
- package/dist/EmptyState.js +11 -0
- package/dist/ErrorBoundary.d.ts +24 -0
- package/dist/ErrorBoundary.js +47 -0
- package/dist/Field.d.ts +17 -0
- package/dist/Field.js +48 -0
- package/dist/FileIcon.d.ts +8 -0
- package/dist/FileIcon.js +15 -0
- package/dist/Icon.d.ts +9 -0
- package/dist/Icon.js +12 -0
- package/dist/IconButton.d.ts +23 -0
- package/dist/IconButton.js +25 -0
- package/dist/IconPicker.d.ts +17 -0
- package/dist/IconPicker.js +93 -0
- package/dist/Input.d.ts +14 -0
- package/dist/Input.js +25 -0
- package/dist/Kbd.d.ts +23 -0
- package/dist/Kbd.js +20 -0
- package/dist/ListRow.d.ts +10 -0
- package/dist/ListRow.js +20 -0
- package/dist/Meter.d.ts +12 -0
- package/dist/Meter.js +7 -0
- package/dist/PanelEmpty.d.ts +16 -0
- package/dist/PanelEmpty.js +7 -0
- package/dist/PanelHeader.d.ts +6 -0
- package/dist/PanelHeader.js +20 -0
- package/dist/Pill.d.ts +27 -0
- package/dist/Pill.js +25 -0
- package/dist/ProjectSwitcher.d.ts +30 -0
- package/dist/ProjectSwitcher.js +18 -0
- package/dist/PromptDialog.d.ts +40 -0
- package/dist/PromptDialog.js +47 -0
- package/dist/SectionLabel.d.ts +3 -0
- package/dist/SectionLabel.js +16 -0
- package/dist/SegmentBar.d.ts +18 -0
- package/dist/SegmentBar.js +44 -0
- package/dist/Segmented.d.ts +17 -0
- package/dist/Segmented.js +7 -0
- package/dist/Select.d.ts +27 -0
- package/dist/Select.js +34 -0
- package/dist/Separator.d.ts +7 -0
- package/dist/Separator.js +8 -0
- package/dist/ShortcutHints.d.ts +1 -0
- package/dist/ShortcutHints.js +94 -0
- package/dist/Skeleton.d.ts +6 -0
- package/dist/Skeleton.js +6 -0
- package/dist/SlidingColumn.d.ts +19 -0
- package/dist/SlidingColumn.js +41 -0
- package/dist/Spinner.d.ts +6 -0
- package/dist/Spinner.js +7 -0
- package/dist/Stepper.d.ts +14 -0
- package/dist/Stepper.js +18 -0
- package/dist/Surface.d.ts +3 -0
- package/dist/Surface.js +16 -0
- package/dist/Switch.d.ts +10 -0
- package/dist/Switch.js +7 -0
- package/dist/TextMenu.d.ts +5 -0
- package/dist/TextMenu.js +31 -0
- package/dist/Tile.d.ts +11 -0
- package/dist/Tile.js +10 -0
- package/dist/Toasts.d.ts +9 -0
- package/dist/Toasts.js +58 -0
- package/dist/Tooltip.d.ts +16 -0
- package/dist/Tooltip.js +21 -0
- package/dist/UIProvider.d.ts +9 -0
- package/dist/UIProvider.js +26 -0
- package/dist/Waveform.d.ts +16 -0
- package/dist/Waveform.js +123 -0
- package/dist/Wipe.d.ts +12 -0
- package/dist/Wipe.js +47 -0
- package/dist/ZoomControls.d.ts +30 -0
- package/dist/ZoomControls.js +16 -0
- package/dist/class-name.d.ts +2 -0
- package/dist/class-name.js +3 -0
- package/dist/clipboard.d.ts +2 -0
- package/dist/clipboard.js +16 -0
- package/dist/context-menu/index.parts.d.ts +1 -0
- package/dist/context-menu/index.parts.js +2 -0
- package/dist/dialog/index.parts.d.ts +1 -0
- package/dist/dialog/index.parts.js +1 -0
- package/dist/dialog/parts.d.ts +36 -0
- package/dist/dialog/parts.js +55 -0
- package/dist/dialog-layer.d.ts +1 -0
- package/dist/dialog-layer.js +27 -0
- package/dist/error-boundary.d.ts +4 -0
- package/dist/error-boundary.js +34 -0
- package/dist/error-message.d.ts +1 -0
- package/dist/error-message.js +4 -0
- package/dist/field-context.d.ts +7 -0
- package/dist/field-context.js +4 -0
- package/dist/file-icon.d.ts +4 -0
- package/dist/file-icon.js +46 -0
- package/dist/floating.d.ts +5 -0
- package/dist/floating.js +6 -0
- package/dist/format/datetime.d.ts +11 -0
- package/dist/format/datetime.js +50 -0
- package/dist/format/duration.d.ts +5 -0
- package/dist/format/duration.js +76 -0
- package/dist/format/index.d.ts +6 -0
- package/dist/format/index.js +7 -0
- package/dist/format/locale.d.ts +17 -0
- package/dist/format/locale.js +105 -0
- package/dist/format/number.d.ts +8 -0
- package/dist/format/number.js +72 -0
- package/dist/format/regions.d.ts +6 -0
- package/dist/format/regions.js +15 -0
- package/dist/format/time-zone.d.ts +1 -0
- package/dist/format/time-zone.js +2 -0
- package/dist/icon-button-size.d.ts +3 -0
- package/dist/icon-button-size.js +3 -0
- package/dist/icon-picker.d.ts +23 -0
- package/dist/icon-picker.js +80 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +74 -0
- package/dist/lazy.d.ts +12 -0
- package/dist/lazy.js +104 -0
- package/dist/locales/en.json +59 -0
- package/dist/locales/nl.json +59 -0
- package/dist/locales.d.ts +4 -0
- package/dist/locales.js +17 -0
- package/dist/menu/index.parts.d.ts +1 -0
- package/dist/menu/index.parts.js +1 -0
- package/dist/menu/parts.d.ts +67 -0
- package/dist/menu/parts.js +113 -0
- package/dist/merge-refs.d.ts +2 -0
- package/dist/merge-refs.js +11 -0
- package/dist/modality.d.ts +1 -0
- package/dist/modality.js +29 -0
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +9 -0
- package/dist/popover/index.parts.d.ts +1 -0
- package/dist/popover/index.parts.js +1 -0
- package/dist/popover/parts.d.ts +26 -0
- package/dist/popover/parts.js +28 -0
- package/dist/prefetch.d.ts +21 -0
- package/dist/prefetch.js +99 -0
- package/dist/preview-card/index.parts.d.ts +1 -0
- package/dist/preview-card/index.parts.js +1 -0
- package/dist/project-switcher-row.d.ts +6 -0
- package/dist/project-switcher-row.js +16 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +22 -0
- package/dist/seti-icons.d.ts +5 -0
- package/dist/seti-icons.js +445 -0
- package/dist/settings/ConfirmDialog.d.ts +9 -0
- package/dist/settings/ConfirmDialog.js +9 -0
- package/dist/settings/DetailHeader.d.ts +10 -0
- package/dist/settings/DetailHeader.js +6 -0
- package/dist/settings/MasterDetail.d.ts +24 -0
- package/dist/settings/MasterDetail.js +16 -0
- package/dist/settings/SettingsDialog.d.ts +46 -0
- package/dist/settings/SettingsDialog.js +96 -0
- package/dist/settings/SettingsRow.d.ts +22 -0
- package/dist/settings/SettingsRow.js +30 -0
- package/dist/settings/SettingsSection.d.ts +14 -0
- package/dist/settings/SettingsSection.js +8 -0
- package/dist/settings/index.d.ts +7 -0
- package/dist/settings/index.js +8 -0
- package/dist/settings/target.d.ts +7 -0
- package/dist/settings/target.js +3 -0
- package/dist/shortcut-hints.d.ts +38 -0
- package/dist/shortcut-hints.js +81 -0
- package/dist/shortcut.d.ts +32 -0
- package/dist/shortcut.js +137 -0
- package/dist/testing/dedupe.d.ts +1 -0
- package/dist/testing/dedupe.js +73 -0
- package/dist/testing/fake-source.d.ts +8 -0
- package/dist/testing/fake-source.js +15 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/theme.css +562 -0
- package/dist/toast-store.d.ts +38 -0
- package/dist/toast-store.js +61 -0
- package/dist/useAsyncAction.d.ts +8 -0
- package/dist/useAsyncAction.js +23 -0
- package/dist/useColumnResize.d.ts +14 -0
- package/dist/useColumnResize.js +49 -0
- package/dist/useContentSize.d.ts +5 -0
- package/dist/useContentSize.js +21 -0
- package/dist/useMeasuredWidth.d.ts +1 -0
- package/dist/useMeasuredWidth.js +6 -0
- package/dist/useNow.d.ts +3 -0
- package/dist/useNow.js +41 -0
- package/dist/wipe-split.d.ts +7 -0
- package/dist/wipe-split.js +20 -0
- package/dist/zoom.d.ts +3 -0
- package/dist/zoom.js +6 -0
- package/package.json +92 -0
- package/src/AccentSwatches.tsx +73 -0
- package/src/Banner.tsx +43 -0
- package/src/Button.tsx +55 -0
- package/src/ButtonGroup.tsx +14 -0
- package/src/Checkbox.tsx +44 -0
- package/src/ChoiceCards.tsx +108 -0
- package/src/CloseButton.tsx +21 -0
- package/src/ColorSwatch.tsx +41 -0
- package/src/ColumnResizeHandle.tsx +23 -0
- package/src/DisabledReason.tsx +23 -0
- package/src/DockShell.tsx +76 -0
- package/src/EmptyState.tsx +48 -0
- package/src/ErrorBoundary.tsx +116 -0
- package/src/Field.tsx +116 -0
- package/src/FileIcon.tsx +27 -0
- package/src/Icon.tsx +22 -0
- package/src/IconButton.tsx +84 -0
- package/src/IconPicker.tsx +257 -0
- package/src/Input.tsx +78 -0
- package/src/Kbd.tsx +67 -0
- package/src/ListRow.tsx +26 -0
- package/src/Meter.tsx +41 -0
- package/src/PanelEmpty.tsx +37 -0
- package/src/PanelHeader.tsx +31 -0
- package/src/Pill.tsx +61 -0
- package/src/ProjectSwitcher.tsx +127 -0
- package/src/PromptDialog.tsx +168 -0
- package/src/SectionLabel.tsx +20 -0
- package/src/SegmentBar.tsx +128 -0
- package/src/Segmented.tsx +51 -0
- package/src/Select.tsx +135 -0
- package/src/Separator.tsx +15 -0
- package/src/ShortcutHints.tsx +118 -0
- package/src/Skeleton.tsx +13 -0
- package/src/SlidingColumn.tsx +91 -0
- package/src/Spinner.tsx +32 -0
- package/src/Stepper.tsx +67 -0
- package/src/Surface.tsx +20 -0
- package/src/Switch.tsx +32 -0
- package/src/TextMenu.tsx +59 -0
- package/src/Tile.tsx +49 -0
- package/src/Toasts.tsx +151 -0
- package/src/Tooltip.tsx +70 -0
- package/src/UIProvider.tsx +41 -0
- package/src/Waveform.tsx +199 -0
- package/src/Wipe.tsx +99 -0
- package/src/ZoomControls.tsx +99 -0
- package/src/class-name.ts +8 -0
- package/src/clipboard.ts +17 -0
- package/src/context-menu/index.parts.ts +21 -0
- package/src/dialog/index.parts.ts +10 -0
- package/src/dialog/parts.tsx +112 -0
- package/src/dialog-layer.ts +31 -0
- package/src/error-boundary.ts +40 -0
- package/src/error-message.ts +5 -0
- package/src/field-context.ts +12 -0
- package/src/file-icon.ts +52 -0
- package/src/floating.ts +9 -0
- package/src/format/datetime.ts +63 -0
- package/src/format/duration.ts +83 -0
- package/src/format/index.ts +20 -0
- package/src/format/locale.ts +153 -0
- package/src/format/number.ts +87 -0
- package/src/format/regions.ts +20 -0
- package/src/format/time-zone.ts +2 -0
- package/src/icon-button-size.ts +6 -0
- package/src/icon-picker.ts +112 -0
- package/src/index.ts +103 -0
- package/src/lazy.tsx +124 -0
- package/src/locales/en.json +59 -0
- package/src/locales/nl.json +59 -0
- package/src/locales.ts +21 -0
- package/src/menu/index.parts.ts +20 -0
- package/src/menu/parts.tsx +296 -0
- package/src/merge-refs.ts +14 -0
- package/src/modality.ts +37 -0
- package/src/platform.ts +10 -0
- package/src/popover/index.parts.ts +8 -0
- package/src/popover/parts.tsx +119 -0
- package/src/prefetch.ts +113 -0
- package/src/preview-card/index.parts.ts +1 -0
- package/src/project-switcher-row.tsx +51 -0
- package/src/selection.ts +24 -0
- package/src/seti-icons.ts +451 -0
- package/src/settings/ConfirmDialog.tsx +30 -0
- package/src/settings/DetailHeader.tsx +25 -0
- package/src/settings/MasterDetail.tsx +67 -0
- package/src/settings/SettingsDialog.tsx +342 -0
- package/src/settings/SettingsRow.tsx +89 -0
- package/src/settings/SettingsSection.tsx +46 -0
- package/src/settings/index.ts +16 -0
- package/src/settings/target.ts +12 -0
- package/src/shortcut-hints.ts +117 -0
- package/src/shortcut.ts +170 -0
- package/src/testing/dedupe.ts +93 -0
- package/src/testing/fake-source.ts +21 -0
- package/src/testing/index.ts +3 -0
- package/src/theme.css +562 -0
- package/src/toast-store.ts +120 -0
- package/src/useAsyncAction.ts +32 -0
- package/src/useColumnResize.ts +75 -0
- package/src/useContentSize.ts +27 -0
- package/src/useMeasuredWidth.ts +7 -0
- package/src/useNow.ts +49 -0
- package/src/wipe-split.ts +24 -0
- package/src/zoom.ts +9 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Shortcut } from './shortcut.ts';
|
|
2
|
+
|
|
3
|
+
/* How long the modifier has to be held on its own before every shortcut shows. */
|
|
4
|
+
export const HINT_DELAY_MS = 500;
|
|
5
|
+
|
|
6
|
+
export interface HintTarget {
|
|
7
|
+
id: number;
|
|
8
|
+
shortcut: Shortcut;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const targets = new Map<Element, HintTarget>();
|
|
12
|
+
let nextId = 0;
|
|
13
|
+
|
|
14
|
+
/* Called by every `Tooltip` whose `kbd` is a real shortcut; a phrase about a key is no shortcut to print. */
|
|
15
|
+
export const registerShortcutHint = (element: Element, shortcut: Shortcut): (() => void) => {
|
|
16
|
+
nextId += 1;
|
|
17
|
+
targets.set(element, { id: nextId, shortcut });
|
|
18
|
+
return () => {
|
|
19
|
+
targets.delete(element);
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const shortcutHintTargets = (): ReadonlyMap<Element, HintTarget> => targets;
|
|
24
|
+
|
|
25
|
+
export type HoldKey = Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey' | 'repeat'>;
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Whether the hints are up: the platform's modifier (Cmd on macOS, Ctrl elsewhere) held on its own for
|
|
29
|
+
* `HINT_DELAY_MS`. Any other key, the modifier coming up or the caller cancelling takes them down, so
|
|
30
|
+
* a shortcut pressed in passing never flashes them.
|
|
31
|
+
*/
|
|
32
|
+
export class ModifierHold {
|
|
33
|
+
private readonly apple: boolean;
|
|
34
|
+
private readonly onChange: (shown: boolean) => void;
|
|
35
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
private shown = false;
|
|
37
|
+
|
|
38
|
+
constructor(apple: boolean, onChange: (shown: boolean) => void) {
|
|
39
|
+
this.apple = apple;
|
|
40
|
+
this.onChange = onChange;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
keyDown(event: HoldKey): void {
|
|
44
|
+
// A repeat is the key still held; only a fresh press starts the wait.
|
|
45
|
+
if (event.repeat && event.key === this.modifierKey()) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.cancel();
|
|
49
|
+
if (event.key === this.modifierKey() && !this.otherModifierHeld(event)) {
|
|
50
|
+
this.timer = setTimeout(() => {
|
|
51
|
+
this.timer = null;
|
|
52
|
+
this.shown = true;
|
|
53
|
+
this.onChange(true);
|
|
54
|
+
}, HINT_DELAY_MS);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
keyUp(event: Pick<KeyboardEvent, 'key'>): void {
|
|
59
|
+
if (event.key === this.modifierKey()) {
|
|
60
|
+
this.cancel();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
cancel(): void {
|
|
65
|
+
if (this.timer !== null) {
|
|
66
|
+
clearTimeout(this.timer);
|
|
67
|
+
this.timer = null;
|
|
68
|
+
}
|
|
69
|
+
if (this.shown) {
|
|
70
|
+
this.shown = false;
|
|
71
|
+
this.onChange(false);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private modifierKey(): string {
|
|
76
|
+
return this.apple ? 'Meta' : 'Control';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private otherModifierHeld(event: HoldKey): boolean {
|
|
80
|
+
return event.altKey || event.shiftKey || (this.apple ? event.ctrlKey : event.metaKey);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface HintRect {
|
|
85
|
+
left: number;
|
|
86
|
+
right: number;
|
|
87
|
+
top: number;
|
|
88
|
+
bottom: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface HintPlacement {
|
|
92
|
+
left?: number;
|
|
93
|
+
right?: number;
|
|
94
|
+
top: number;
|
|
95
|
+
translateX: '0' | '-50%';
|
|
96
|
+
translateY: '0' | '-100%';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const HINT_GAP_PX = 2;
|
|
100
|
+
/* Room a hint needs to be centered under its button; nearer a window edge it lines up with that edge instead. */
|
|
101
|
+
const EDGE_ROOM_PX = 48;
|
|
102
|
+
const HINT_HEIGHT_PX = 24;
|
|
103
|
+
|
|
104
|
+
/* Under the button, centered, and above it where the window ends first. */
|
|
105
|
+
export const placeHint = (rect: HintRect, viewport: { width: number; height: number }): HintPlacement => {
|
|
106
|
+
const below = rect.bottom + HINT_GAP_PX + HINT_HEIGHT_PX <= viewport.height;
|
|
107
|
+
const top = Math.round(below ? rect.bottom + HINT_GAP_PX : rect.top - HINT_GAP_PX);
|
|
108
|
+
const translateY = below ? '0' : '-100%';
|
|
109
|
+
const center = (rect.left + rect.right) / 2;
|
|
110
|
+
if (center < EDGE_ROOM_PX) {
|
|
111
|
+
return { left: Math.round(rect.left), top, translateX: '0', translateY };
|
|
112
|
+
}
|
|
113
|
+
if (center > viewport.width - EDGE_ROOM_PX) {
|
|
114
|
+
return { right: Math.round(viewport.width - rect.right), top, translateX: '0', translateY };
|
|
115
|
+
}
|
|
116
|
+
return { left: Math.round(center), top, translateX: '-50%', translateY };
|
|
117
|
+
};
|
package/src/shortcut.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One shortcut, written once. The same value decides whether a key event is the shortcut and how the
|
|
3
|
+
* shortcut is printed. `mod` is Cmd on macOS and Ctrl everywhere else; `ctrl` and `meta` name the
|
|
4
|
+
* physical key and exist for the few shortcuts that differ per platform on purpose.
|
|
5
|
+
*/
|
|
6
|
+
export interface Shortcut {
|
|
7
|
+
mod: boolean;
|
|
8
|
+
ctrl: boolean;
|
|
9
|
+
meta: boolean;
|
|
10
|
+
alt: boolean;
|
|
11
|
+
shift: boolean;
|
|
12
|
+
/* An uppercase letter, a digit, a punctuation mark or a named key (`Enter`, `ArrowLeft`, `F2`).
|
|
13
|
+
Empty for modifiers held during a pointer gesture (`Mod` then "drag"), which never match a key. */
|
|
14
|
+
key: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* What a shortcut is matched against. A real `KeyboardEvent` satisfies it, a test writes one by hand. */
|
|
18
|
+
export type KeyLike = Pick<KeyboardEvent, 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey' | 'key' | 'code'>;
|
|
19
|
+
|
|
20
|
+
/* Matched on the physical key: Shift turns `[` into `{` and Option turns most keys into a dead key. */
|
|
21
|
+
const PUNCTUATION_CODES: Record<string, string> = {
|
|
22
|
+
',': 'Comma',
|
|
23
|
+
'.': 'Period',
|
|
24
|
+
'/': 'Slash',
|
|
25
|
+
';': 'Semicolon',
|
|
26
|
+
"'": 'Quote',
|
|
27
|
+
'`': 'Backquote',
|
|
28
|
+
'\\': 'Backslash',
|
|
29
|
+
'[': 'BracketLeft',
|
|
30
|
+
']': 'BracketRight'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/* Matched on the character, because `+` sits on a different key per layout. */
|
|
34
|
+
const CHARACTER_KEYS = new Set(['+', '-', '=']);
|
|
35
|
+
|
|
36
|
+
const NAMED_KEYS = new Set(['Enter', 'Escape', 'Backspace', 'Delete', 'Tab', 'Space', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']);
|
|
37
|
+
|
|
38
|
+
const FUNCTION_KEY = /^F([1-9]|1[0-2])$/;
|
|
39
|
+
|
|
40
|
+
const MODIFIER_WORDS = ['Mod', 'Ctrl', 'Meta', 'Alt', 'Shift'] as const;
|
|
41
|
+
|
|
42
|
+
const isKey = (word: string): boolean =>
|
|
43
|
+
/^[A-Za-z0-9]$/.test(word) || word in PUNCTUATION_CODES || CHARACTER_KEYS.has(word) || NAMED_KEYS.has(word) || FUNCTION_KEY.test(word);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parses `Mod+Shift+K`. Throws on a word it does not know or on two keys, so a typo in a table fails
|
|
47
|
+
* the moment the module loads instead of leaving a shortcut that never fires.
|
|
48
|
+
*/
|
|
49
|
+
export const shortcut = (text: string): Shortcut => {
|
|
50
|
+
// A trailing `++` is the plus key itself, which a plain split would read as two empty words.
|
|
51
|
+
const words = text === '+' ? ['+'] : text.endsWith('++') ? [...text.slice(0, -2).split('+'), '+'] : text.split('+');
|
|
52
|
+
const result: Shortcut = { mod: false, ctrl: false, meta: false, alt: false, shift: false, key: '' };
|
|
53
|
+
for (const word of words) {
|
|
54
|
+
const modifier = MODIFIER_WORDS.find((candidate) => candidate === word);
|
|
55
|
+
if (modifier) {
|
|
56
|
+
result[modifier === 'Mod' ? 'mod' : modifier === 'Ctrl' ? 'ctrl' : modifier === 'Meta' ? 'meta' : modifier === 'Alt' ? 'alt' : 'shift'] = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!isKey(word)) {
|
|
60
|
+
throw new Error(`Unknown key "${word}" in shortcut "${text}"`);
|
|
61
|
+
}
|
|
62
|
+
if (result.key !== '') {
|
|
63
|
+
throw new Error(`Shortcut "${text}" names two keys`);
|
|
64
|
+
}
|
|
65
|
+
result.key = word.length === 1 ? word.toUpperCase() : word;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Letters and digits match on `code`: with Shift held `key` says `!` for 1, and Option's dead keys change it on macOS.
|
|
71
|
+
const matchesKey = (key: string, event: KeyLike): boolean => {
|
|
72
|
+
if (/^[A-Z]$/.test(key)) {
|
|
73
|
+
return event.code === `Key${key}`;
|
|
74
|
+
}
|
|
75
|
+
if (/^[0-9]$/.test(key)) {
|
|
76
|
+
return event.code === `Digit${key}`;
|
|
77
|
+
}
|
|
78
|
+
const code = PUNCTUATION_CODES[key];
|
|
79
|
+
if (code) {
|
|
80
|
+
return event.code === code;
|
|
81
|
+
}
|
|
82
|
+
if (key === 'Space') {
|
|
83
|
+
return event.code === 'Space';
|
|
84
|
+
}
|
|
85
|
+
return event.key === key;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/* Strict: every modifier the shortcut does not name has to be up, so Ctrl+K on macOS is not Cmd+K. */
|
|
89
|
+
export const matchesShortcut = (target: Shortcut, event: KeyLike, apple: boolean): boolean => {
|
|
90
|
+
const meta = target.meta || (target.mod && apple);
|
|
91
|
+
const ctrl = target.ctrl || (target.mod && !apple);
|
|
92
|
+
if (target.key === '' || event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== target.alt || event.shiftKey !== target.shift) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return matchesKey(target.key, event);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const KEY_LABELS: Record<string, { apple: string; other: string }> = {
|
|
99
|
+
Enter: { apple: '↩', other: 'Enter' },
|
|
100
|
+
Backspace: { apple: '⌫', other: 'Backspace' },
|
|
101
|
+
Delete: { apple: '⌦', other: 'Delete' },
|
|
102
|
+
Escape: { apple: 'Esc', other: 'Esc' },
|
|
103
|
+
Tab: { apple: '⇥', other: 'Tab' },
|
|
104
|
+
Space: { apple: 'Space', other: 'Space' },
|
|
105
|
+
ArrowLeft: { apple: '←', other: '←' },
|
|
106
|
+
ArrowRight: { apple: '→', other: '→' },
|
|
107
|
+
ArrowUp: { apple: '↑', other: '↑' },
|
|
108
|
+
ArrowDown: { apple: '↓', other: '↓' }
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/* The modifiers and the key as separate caps, in the order the platform prints them. */
|
|
112
|
+
export const shortcutParts = (target: Shortcut, apple: boolean): string[] => {
|
|
113
|
+
const parts: string[] = [];
|
|
114
|
+
if (apple) {
|
|
115
|
+
if (target.ctrl) {
|
|
116
|
+
parts.push('⌃');
|
|
117
|
+
}
|
|
118
|
+
if (target.alt) {
|
|
119
|
+
parts.push('⌥');
|
|
120
|
+
}
|
|
121
|
+
if (target.shift) {
|
|
122
|
+
parts.push('⇧');
|
|
123
|
+
}
|
|
124
|
+
if (target.meta || target.mod) {
|
|
125
|
+
parts.push('⌘');
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
if (target.ctrl || target.mod) {
|
|
129
|
+
parts.push('Ctrl');
|
|
130
|
+
}
|
|
131
|
+
if (target.alt) {
|
|
132
|
+
parts.push('Alt');
|
|
133
|
+
}
|
|
134
|
+
if (target.shift) {
|
|
135
|
+
parts.push('Shift');
|
|
136
|
+
}
|
|
137
|
+
if (target.meta) {
|
|
138
|
+
parts.push('Meta');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (target.key !== '') {
|
|
142
|
+
const label = KEY_LABELS[target.key];
|
|
143
|
+
parts.push(label ? label[apple ? 'apple' : 'other'] : target.key);
|
|
144
|
+
}
|
|
145
|
+
return parts;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/* `⌥⇧⌘K` on macOS, `Ctrl+Alt+Shift+K` elsewhere. */
|
|
149
|
+
export const formatShortcut = (target: Shortcut, apple: boolean): string => shortcutParts(target, apple).join(apple ? '' : '+');
|
|
150
|
+
|
|
151
|
+
/* The browser's own editing shortcuts, which the app only ever prints in a menu. */
|
|
152
|
+
export const EDIT_SHORTCUTS = {
|
|
153
|
+
copy: /* @__PURE__ */ shortcut('Mod+C'),
|
|
154
|
+
cut: /* @__PURE__ */ shortcut('Mod+X'),
|
|
155
|
+
paste: /* @__PURE__ */ shortcut('Mod+V'),
|
|
156
|
+
selectAll: /* @__PURE__ */ shortcut('Mod+A')
|
|
157
|
+
} as const;
|
|
158
|
+
|
|
159
|
+
/* Keys that are not a shortcut of their own but are printed as one in a hint. */
|
|
160
|
+
export const KEY_SHORTCUTS = {
|
|
161
|
+
enter: /* @__PURE__ */ shortcut('Enter'),
|
|
162
|
+
modEnter: /* @__PURE__ */ shortcut('Mod+Enter'),
|
|
163
|
+
backspace: /* @__PURE__ */ shortcut('Backspace'),
|
|
164
|
+
escape: /* @__PURE__ */ shortcut('Escape'),
|
|
165
|
+
shift: /* @__PURE__ */ shortcut('Shift'),
|
|
166
|
+
rename: /* @__PURE__ */ shortcut('F2')
|
|
167
|
+
} as const;
|
|
168
|
+
|
|
169
|
+
/* Whether the platform's own modifier is down during a pointer gesture: Cmd on macOS, Ctrl elsewhere. */
|
|
170
|
+
export const isModHeld = (event: Pick<KeyLike, 'metaKey' | 'ctrlKey'>, apple: boolean): boolean => (apple ? event.metaKey : event.ctrlKey);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { Glob, plugin } from 'bun';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* A preload for `bun test` in an app that links a checkout of this library: what `resolve.dedupe`
|
|
7
|
+
* does for Vite. The checkout has node_modules of its own, and a second React, i18next or Base UI
|
|
8
|
+
* breaks every hook, every word and every compound part. Bun has no dedupe and its resolve hook never
|
|
9
|
+
* sees a package import, so every file of these packages in the checkout loads as a stand-in for the
|
|
10
|
+
* same file in the app's copy. Does nothing for a package the checkout has no copy of, which is every
|
|
11
|
+
* package once the library is installed from the registry.
|
|
12
|
+
*/
|
|
13
|
+
const SHARED = ['react', 'react-dom', 'i18next', 'react-i18next', '@base-ui-components/react'];
|
|
14
|
+
|
|
15
|
+
const NAME = '@adecore/ui';
|
|
16
|
+
|
|
17
|
+
/* The package root, from `src/testing` or `dist/testing` alike. */
|
|
18
|
+
const library = realpathSync(join(import.meta.dir, '..', '..'));
|
|
19
|
+
|
|
20
|
+
interface Manifest {
|
|
21
|
+
workspaces?: string[] | { packages?: string[] };
|
|
22
|
+
dependencies?: Record<string, string>;
|
|
23
|
+
devDependencies?: Record<string, string>;
|
|
24
|
+
peerDependencies?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const manifestOf = (folder: string): Manifest | null => {
|
|
28
|
+
const path = join(folder, 'package.json');
|
|
29
|
+
return existsSync(path) ? (JSON.parse(readFileSync(path, 'utf8')) as Manifest) : null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const dependsOnLibrary = (manifest: Manifest | null): boolean =>
|
|
33
|
+
manifest !== null && [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].some((deps) => deps !== undefined && NAME in deps);
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* Where the app's copies are found: the folder the tests run in, then each workspace of it that
|
|
37
|
+
* depends on the library. With an isolated install the root of a monorepo has no React of its own.
|
|
38
|
+
*/
|
|
39
|
+
const appFolders = (): string[] => {
|
|
40
|
+
const root = process.cwd();
|
|
41
|
+
const workspaces = manifestOf(root)?.workspaces;
|
|
42
|
+
const patterns = (Array.isArray(workspaces) ? workspaces : workspaces?.packages) ?? [];
|
|
43
|
+
const members = patterns
|
|
44
|
+
.flatMap((pattern) => [...new Glob(`${pattern}/package.json`).scanSync({ cwd: root, onlyFiles: true })])
|
|
45
|
+
.map((path) => join(root, dirname(path)))
|
|
46
|
+
.filter((folder) => dependsOnLibrary(manifestOf(folder)));
|
|
47
|
+
return [root, ...members];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/* The copy of a package that `from` resolves to, walking up the node_modules folders as Node does. */
|
|
51
|
+
const copyFrom = (from: string, name: string): string | null => {
|
|
52
|
+
for (let folder = from; ; folder = dirname(folder)) {
|
|
53
|
+
const candidate = join(folder, 'node_modules', name);
|
|
54
|
+
if (existsSync(candidate)) {
|
|
55
|
+
return realpathSync(candidate);
|
|
56
|
+
}
|
|
57
|
+
if (dirname(folder) === folder) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const folders = appFolders();
|
|
64
|
+
|
|
65
|
+
const copies = SHARED.flatMap((name) => {
|
|
66
|
+
const theirs = join(library, 'node_modules', name);
|
|
67
|
+
if (!existsSync(theirs)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const real = realpathSync(theirs);
|
|
71
|
+
const ours = folders.map((folder) => copyFrom(folder, name)).find((copy): copy is string => copy !== null && !copy.startsWith(`${library}/`));
|
|
72
|
+
return ours === undefined || ours === real ? [] : [{ theirs: `${real}/`, ours: `${ours}/` }];
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const escape = (text: string): string => text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
76
|
+
|
|
77
|
+
// Bun refuses `export default` beside `export *`, so the stand-in names every export of the real file.
|
|
78
|
+
const standIn = (path: string): string => {
|
|
79
|
+
const names = Object.keys(require(path) as object).filter((name) => name !== 'default' && /^[A-Za-z_$][\w$]*$/.test(name));
|
|
80
|
+
return [`import * as real from ${JSON.stringify(path)};`, 'export default real.default;', `export const { ${names.join(', ')} } = real;`].join('\n');
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (copies.length > 0) {
|
|
84
|
+
plugin({
|
|
85
|
+
name: 'adecore-ui-dedupe',
|
|
86
|
+
setup(build) {
|
|
87
|
+
build.onLoad({ filter: new RegExp(`^(${copies.map((copy) => escape(copy.theirs)).join('|')})`) }, (args) => {
|
|
88
|
+
const copy = copies.find((candidate) => args.path.startsWith(candidate.theirs))!;
|
|
89
|
+
return { contents: standIn(copy.ours + args.path.slice(copy.theirs.length)), loader: 'js' };
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FormatSource } from '../format/locale.ts';
|
|
2
|
+
import { FORMAT_LANGUAGE } from '../format/regions.ts';
|
|
3
|
+
|
|
4
|
+
export interface FakeFormatSource extends FormatSource {
|
|
5
|
+
set(settings: { language?: string; region?: string }): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* A source a test sets by hand: English, the region of the language, and no system locale of a shell. */
|
|
9
|
+
export const fakeFormatSource = (): FakeFormatSource => {
|
|
10
|
+
let language = 'en';
|
|
11
|
+
let region: string = FORMAT_LANGUAGE;
|
|
12
|
+
return {
|
|
13
|
+
language: () => language,
|
|
14
|
+
region: () => region,
|
|
15
|
+
subscribe: () => () => {},
|
|
16
|
+
set: (settings) => {
|
|
17
|
+
language = settings.language ?? language;
|
|
18
|
+
region = settings.region ?? region;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
};
|