svelte-incant 0.7.0 → 0.8.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/dist/combobox-example.svelte +2 -2
- package/dist/components/header.svelte +1 -1
- package/dist/package/attachment.svelte.d.ts +6 -4
- package/dist/package/attachment.svelte.js +25 -29
- package/dist/package/chord.svelte +6 -6
- package/dist/package/chord.svelte.d.ts +2 -1
- package/dist/package/chord.svelte.js +46 -255
- package/dist/package/components/circular-progress.svelte +2 -4
- package/dist/package/components/circular-progress.svelte.d.ts +1 -0
- package/dist/package/components/kbds.svelte +30 -71
- package/dist/package/components/kbds.svelte.d.ts +4 -3
- package/dist/package/focus.svelte +7 -23
- package/dist/package/focus.svelte.d.ts +2 -1
- package/dist/package/index.d.ts +2 -2
- package/dist/package/index.js +2 -2
- package/dist/package/overlay-component.svelte +7 -23
- package/dist/package/overlay-component.svelte.d.ts +2 -1
- package/dist/package/palette.svelte +137 -97
- package/dist/package/palette.svelte.d.ts +2 -1
- package/dist/package/palette.svelte.js +46 -285
- package/dist/package/shortcut.svelte +6 -5
- package/dist/package/shortcut.svelte.d.ts +2 -1
- package/package.json +30 -35
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
role="combobox"
|
|
60
60
|
aria-expanded={open}
|
|
61
61
|
{@attach shortcut({
|
|
62
|
-
|
|
62
|
+
hotkey: 'Mod+K',
|
|
63
63
|
description: 'Focus framework combobox'
|
|
64
64
|
})}
|
|
65
65
|
>
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
role="combobox"
|
|
99
99
|
aria-expanded={open}
|
|
100
100
|
{@attach shortcut({
|
|
101
|
-
|
|
101
|
+
hotkey: 'Mod+J',
|
|
102
102
|
description: 'Focus framework combobox'
|
|
103
103
|
})}
|
|
104
104
|
>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { type RegisterableHotkey } from '@tanstack/svelte-hotkeys';
|
|
1
2
|
import type { Attachment } from 'svelte/attachments';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
type ShortcutInput = {
|
|
4
|
+
hotkey: RegisterableHotkey;
|
|
5
|
+
description?: string;
|
|
5
6
|
action?: () => void;
|
|
6
7
|
click?: boolean;
|
|
7
8
|
preventDefault?: boolean;
|
|
9
|
+
enabled?: boolean;
|
|
8
10
|
};
|
|
9
|
-
export declare function shortcut(
|
|
11
|
+
export declare function shortcut(input: ShortcutInput): Attachment<HTMLElement>;
|
|
10
12
|
export {};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getIsKeyHeld } from '@tanstack/svelte-hotkeys';
|
|
2
|
+
import { watch } from 'runed';
|
|
2
3
|
import { mount, unmount } from 'svelte';
|
|
3
4
|
import OverlayComponent from './overlay-component.svelte';
|
|
4
|
-
import { add_shortcut,
|
|
5
|
-
const pressed_keys = new PressedKeys();
|
|
5
|
+
import { add_shortcut, isShortcutEnabled, remove_shortcut } from './palette.svelte.js';
|
|
6
6
|
const voidElements = new Set([
|
|
7
7
|
'area',
|
|
8
8
|
'base',
|
|
@@ -19,7 +19,7 @@ const voidElements = new Set([
|
|
|
19
19
|
'track',
|
|
20
20
|
'wbr'
|
|
21
21
|
]);
|
|
22
|
-
function setupAnchor(element, targetNode, isVoidElement,
|
|
22
|
+
function setupAnchor(element, targetNode, isVoidElement, hotkey) {
|
|
23
23
|
const anchor = document.createElement('div');
|
|
24
24
|
anchor.style.pointerEvents = 'none';
|
|
25
25
|
if (isVoidElement) {
|
|
@@ -42,20 +42,15 @@ function setupAnchor(element, targetNode, isVoidElement, keys) {
|
|
|
42
42
|
}
|
|
43
43
|
const instance = mount(OverlayComponent, {
|
|
44
44
|
target: anchor,
|
|
45
|
-
props: {
|
|
45
|
+
props: { hotkey }
|
|
46
46
|
});
|
|
47
47
|
targetNode.appendChild(anchor);
|
|
48
48
|
return { anchor, instance };
|
|
49
49
|
}
|
|
50
|
-
function setupOutline(element,
|
|
50
|
+
function setupOutline(element, hotkey) {
|
|
51
51
|
element.style.transition = 'outline 0s, outline-offset 0s';
|
|
52
|
-
const
|
|
53
|
-
watch(() => {
|
|
54
|
-
const altPressed = pressed_keys.has('alt');
|
|
55
|
-
const shortcut = shortcuts[slug];
|
|
56
|
-
const isEnabled = shortcut?.enabled ?? true;
|
|
57
|
-
return altPressed && isEnabled;
|
|
58
|
-
}, (should_show_outline) => {
|
|
52
|
+
const altHeld = getIsKeyHeld('Alt');
|
|
53
|
+
watch(() => altHeld.held && isShortcutEnabled(hotkey), (should_show_outline) => {
|
|
59
54
|
if (should_show_outline) {
|
|
60
55
|
element.style.outline = '2px dotted #878787';
|
|
61
56
|
element.style.outlineOffset = '2px';
|
|
@@ -66,20 +61,21 @@ function setupOutline(element, keys) {
|
|
|
66
61
|
}
|
|
67
62
|
});
|
|
68
63
|
}
|
|
69
|
-
export function shortcut(
|
|
64
|
+
export function shortcut(input) {
|
|
70
65
|
return (element) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
add_shortcut({
|
|
67
|
+
hotkey: input.hotkey,
|
|
68
|
+
description: input.description,
|
|
69
|
+
preventDefault: input.preventDefault,
|
|
70
|
+
enabled: input.enabled,
|
|
71
|
+
action: () => {
|
|
72
|
+
element.focus();
|
|
73
|
+
if (input.click !== false) {
|
|
74
|
+
element.click();
|
|
75
|
+
}
|
|
76
|
+
input.action?.();
|
|
75
77
|
}
|
|
76
|
-
|
|
77
|
-
};
|
|
78
|
-
const shortcutData = {
|
|
79
|
-
...shortcut,
|
|
80
|
-
action
|
|
81
|
-
};
|
|
82
|
-
add_shortcut(shortcutData);
|
|
78
|
+
});
|
|
83
79
|
let targetNode = element;
|
|
84
80
|
const tagName = element.tagName.toLowerCase();
|
|
85
81
|
const isVoidElement = voidElements.has(tagName);
|
|
@@ -87,15 +83,15 @@ export function shortcut(shortcut) {
|
|
|
87
83
|
targetNode = element.parentElement;
|
|
88
84
|
}
|
|
89
85
|
if (!targetNode) {
|
|
90
|
-
remove_shortcut(
|
|
86
|
+
remove_shortcut(input.hotkey);
|
|
91
87
|
return () => { };
|
|
92
88
|
}
|
|
93
|
-
const { anchor, instance } = setupAnchor(element, targetNode, isVoidElement,
|
|
94
|
-
setupOutline(element,
|
|
89
|
+
const { anchor, instance } = setupAnchor(element, targetNode, isVoidElement, input.hotkey);
|
|
90
|
+
setupOutline(element, input.hotkey);
|
|
95
91
|
return () => {
|
|
96
92
|
unmount(instance);
|
|
97
93
|
anchor.remove();
|
|
98
|
-
remove_shortcut(
|
|
94
|
+
remove_shortcut(input.hotkey);
|
|
99
95
|
};
|
|
100
96
|
};
|
|
101
97
|
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { HotkeySequence } from '@tanstack/svelte-hotkeys';
|
|
2
3
|
import { watch } from 'runed';
|
|
3
|
-
import { onMount } from 'svelte';
|
|
4
4
|
import { add_chord, remove_chord } from './chord.svelte.js';
|
|
5
5
|
|
|
6
6
|
let {
|
|
7
|
-
|
|
7
|
+
sequence,
|
|
8
8
|
description,
|
|
9
9
|
action
|
|
10
10
|
}: {
|
|
11
|
-
|
|
11
|
+
sequence: HotkeySequence;
|
|
12
12
|
description?: string;
|
|
13
13
|
action: () => void;
|
|
14
14
|
} = $props();
|
|
15
15
|
|
|
16
|
-
watch([() =>
|
|
16
|
+
watch([() => sequence, () => description, () => action], () => {
|
|
17
17
|
add_chord({
|
|
18
|
-
|
|
18
|
+
sequence,
|
|
19
19
|
description,
|
|
20
20
|
action
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
return () => {
|
|
24
|
-
remove_chord(
|
|
24
|
+
remove_chord(sequence);
|
|
25
25
|
};
|
|
26
26
|
});
|
|
27
27
|
</script>
|
|
@@ -1,257 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { SvelteSet } from 'svelte/reactivity';
|
|
1
|
+
import { formatHotkeySequence, getSequenceManager } from '@tanstack/svelte-hotkeys';
|
|
2
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
4
3
|
export const CHORD_TIMEOUT_MS = 1500;
|
|
5
|
-
|
|
6
|
-
return (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.syncChordListeners();
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
syncChordListeners() {
|
|
53
|
-
// Clear existing chord-specific listeners (keep escape handler)
|
|
54
|
-
// Note: We can't easily remove onKeys callbacks, so we rely on the enabled check
|
|
55
|
-
// For each registered chord, set up listeners
|
|
56
|
-
for (const chord of Object.values(this.chords)) {
|
|
57
|
-
this.setupChordListener(chord);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
setupChordListener(chord) {
|
|
61
|
-
if (!chord.enabled)
|
|
62
|
-
return;
|
|
63
|
-
const firstStep = chord.steps[0];
|
|
64
|
-
if (!firstStep)
|
|
65
|
-
return;
|
|
66
|
-
// Listen for first step - triggers immediately on keydown (VS Code style)
|
|
67
|
-
this.pressedKeys.onKeys(firstStep, () => {
|
|
68
|
-
if (!chord.enabled)
|
|
69
|
-
return;
|
|
70
|
-
// If we're already in a chord progress for a different chord, ignore
|
|
71
|
-
if (this.currentProgress) {
|
|
72
|
-
const currentSlug = this.slugifyChord(this.currentProgress.steps);
|
|
73
|
-
const thisSlug = this.slugifyChord(chord.steps);
|
|
74
|
-
if (currentSlug !== thisSlug) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// Start the chord (shows progress UI immediately)
|
|
79
|
-
this.startChord(chord);
|
|
80
|
-
});
|
|
81
|
-
// Listen for second step
|
|
82
|
-
if (chord.steps.length > 1) {
|
|
83
|
-
const secondStep = chord.steps[1];
|
|
84
|
-
if (!secondStep)
|
|
85
|
-
return;
|
|
86
|
-
this.pressedKeys.onKeys(secondStep, () => {
|
|
87
|
-
if (!chord.enabled)
|
|
88
|
-
return;
|
|
89
|
-
// Only complete if we're in progress for THIS chord
|
|
90
|
-
if (this.currentProgress &&
|
|
91
|
-
this.slugifyChord(this.currentProgress.steps) === this.slugifyChord(chord.steps) &&
|
|
92
|
-
this.currentProgress.currentIndex === 0) {
|
|
93
|
-
this.completeChord();
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
normalizeChordSteps(steps) {
|
|
99
|
-
return normalizeChordSteps(steps);
|
|
100
|
-
}
|
|
101
|
-
slugifyChord(steps) {
|
|
102
|
-
return slugifyChord(steps);
|
|
103
|
-
}
|
|
104
|
-
comboToString(combo) {
|
|
105
|
-
return comboToString(combo);
|
|
106
|
-
}
|
|
107
|
-
checkCollision(steps, description) {
|
|
108
|
-
const slug = this.slugifyChord(steps);
|
|
109
|
-
if (this.chords[slug]) {
|
|
110
|
-
console.warn(`Chord collision detected: "${slug}" already registered${description ? ` (trying to register: "${description}")` : ''}`);
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
add(chord) {
|
|
116
|
-
this.startListening();
|
|
117
|
-
const normalizedSteps = this.normalizeChordSteps(chord.steps);
|
|
118
|
-
if (normalizedSteps.length === 0) {
|
|
119
|
-
console.warn('Cannot add chord with no steps');
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (normalizedSteps.length < 2) {
|
|
123
|
-
console.warn('Chords require at least 2 steps');
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
this.checkCollision(normalizedSteps, chord.description);
|
|
127
|
-
const slug = this.slugifyChord(normalizedSteps);
|
|
128
|
-
const firstStepString = this.comboToString(normalizedSteps[0]);
|
|
129
|
-
this.chordPrefixes.add(firstStepString);
|
|
130
|
-
this.chords[slug] = {
|
|
131
|
-
...chord,
|
|
132
|
-
steps: normalizedSteps,
|
|
133
|
-
enabled: chord.enabled ?? true
|
|
134
|
-
};
|
|
135
|
-
if (!this.chordOrder.includes(slug)) {
|
|
136
|
-
this.chordOrder.push(slug);
|
|
137
|
-
}
|
|
138
|
-
// Set up listener for this chord
|
|
139
|
-
this.setupChordListener(this.chords[slug]);
|
|
140
|
-
}
|
|
141
|
-
remove(steps) {
|
|
142
|
-
const slug = this.slugifyChord(steps);
|
|
143
|
-
const chord = this.chords[slug];
|
|
144
|
-
if (!chord) {
|
|
145
|
-
console.warn(`Chord not found for steps: ${JSON.stringify(steps)}`);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
const firstStepString = this.comboToString(chord.steps[0]);
|
|
149
|
-
const hasOtherChordsWithSamePrefix = Object.values(this.chords).some((c) => c !== chord && c.steps[0] && this.comboToString(c.steps[0]) === firstStepString);
|
|
150
|
-
if (!hasOtherChordsWithSamePrefix) {
|
|
151
|
-
this.chordPrefixes.delete(firstStepString);
|
|
152
|
-
}
|
|
153
|
-
delete this.chords[slug];
|
|
154
|
-
const index = this.chordOrder.indexOf(slug);
|
|
155
|
-
if (index > -1) {
|
|
156
|
-
this.chordOrder.splice(index, 1);
|
|
157
|
-
}
|
|
158
|
-
if (this.currentProgress && this.slugifyChord(this.currentProgress.steps) === slug) {
|
|
159
|
-
this.resetChord();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
toggle(steps) {
|
|
163
|
-
const slug = this.slugifyChord(steps);
|
|
164
|
-
if (this.chords[slug]) {
|
|
165
|
-
this.chords[slug].enabled = !this.chords[slug].enabled;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
getChords() {
|
|
169
|
-
this.startListening();
|
|
170
|
-
return this.chordOrder
|
|
171
|
-
.map((slug) => this.chords[slug])
|
|
172
|
-
.filter((chord) => chord !== undefined);
|
|
173
|
-
}
|
|
174
|
-
isChordPrefix(combo) {
|
|
175
|
-
const comboString = this.comboToString(sortCombo(combo));
|
|
176
|
-
return this.chordPrefixes.has(comboString);
|
|
177
|
-
}
|
|
178
|
-
findChordForPrefix(combo) {
|
|
179
|
-
const comboString = this.comboToString(sortCombo(combo));
|
|
180
|
-
return (this.getChords().find((chord) => this.comboToString(chord.steps[0]) === comboString && chord.enabled) || null);
|
|
181
|
-
}
|
|
182
|
-
startChord(chord) {
|
|
183
|
-
if (this.chordTimeout) {
|
|
184
|
-
clearTimeout(this.chordTimeout);
|
|
185
|
-
}
|
|
186
|
-
this.currentProgress = {
|
|
187
|
-
steps: chord.steps,
|
|
188
|
-
currentIndex: 0,
|
|
189
|
-
expiresAt: Date.now() + CHORD_TIMEOUT_MS
|
|
190
|
-
};
|
|
191
|
-
this.chordTimeout = setTimeout(() => {
|
|
192
|
-
this.resetChord();
|
|
193
|
-
}, CHORD_TIMEOUT_MS);
|
|
194
|
-
}
|
|
195
|
-
completeChord() {
|
|
196
|
-
if (this.currentProgress) {
|
|
197
|
-
const chord = this.chords[this.slugifyChord(this.currentProgress.steps)];
|
|
198
|
-
if (chord && chord.enabled) {
|
|
199
|
-
chord.action();
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
this.resetChord();
|
|
203
|
-
}
|
|
204
|
-
resetChord() {
|
|
205
|
-
if (this.chordTimeout) {
|
|
206
|
-
clearTimeout(this.chordTimeout);
|
|
207
|
-
this.chordTimeout = null;
|
|
208
|
-
}
|
|
209
|
-
this.currentProgress = null;
|
|
210
|
-
}
|
|
211
|
-
destroy() {
|
|
212
|
-
for (const cleanup of this.cleanupCallbacks) {
|
|
213
|
-
cleanup();
|
|
214
|
-
}
|
|
215
|
-
this.cleanupCallbacks = [];
|
|
216
|
-
this.resetChord();
|
|
217
|
-
this.isListening = false;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
let _registry = null;
|
|
221
|
-
function getRegistry() {
|
|
222
|
-
if (!_registry) {
|
|
223
|
-
_registry = new ChordRegistry();
|
|
224
|
-
}
|
|
225
|
-
return _registry;
|
|
226
|
-
}
|
|
227
|
-
// Export direct access to the registry for reactivity
|
|
228
|
-
export function getChordRegistry() {
|
|
229
|
-
return getRegistry();
|
|
230
|
-
}
|
|
231
|
-
export const chordRegistry = new Proxy({}, {
|
|
232
|
-
get(_, prop) {
|
|
233
|
-
return getRegistry()[prop];
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
export const chords = new Proxy({}, {
|
|
237
|
-
get(_, prop) {
|
|
238
|
-
const registry = getRegistry();
|
|
239
|
-
return registry.chords[prop];
|
|
240
|
-
},
|
|
241
|
-
has(_, prop) {
|
|
242
|
-
const registry = getRegistry();
|
|
243
|
-
return prop in registry.chords;
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
export function add_chord(chord) {
|
|
247
|
-
getRegistry().add(chord);
|
|
248
|
-
}
|
|
249
|
-
export function remove_chord(steps) {
|
|
250
|
-
getRegistry().remove(steps);
|
|
251
|
-
}
|
|
252
|
-
export function toggle_chord(steps) {
|
|
253
|
-
getRegistry().toggle(steps);
|
|
254
|
-
}
|
|
255
|
-
export function get_current_progress() {
|
|
256
|
-
return getRegistry().currentProgress;
|
|
4
|
+
function canonicalize(sequence) {
|
|
5
|
+
return formatHotkeySequence(sequence);
|
|
6
|
+
}
|
|
7
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- internal lookup, not consumed reactively
|
|
8
|
+
const handles = new Map();
|
|
9
|
+
const enabledState = new SvelteMap();
|
|
10
|
+
export function add_chord(input) {
|
|
11
|
+
if (input.sequence.length < 2) {
|
|
12
|
+
console.warn('Chords require at least 2 steps');
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
const key = canonicalize(input.sequence);
|
|
16
|
+
handles.get(key)?.unregister();
|
|
17
|
+
const enabled = input.enabled ?? true;
|
|
18
|
+
enabledState.set(key, enabled);
|
|
19
|
+
const meta = {};
|
|
20
|
+
if (input.description)
|
|
21
|
+
meta.description = input.description;
|
|
22
|
+
const options = {
|
|
23
|
+
enabled,
|
|
24
|
+
timeout: input.timeout ?? CHORD_TIMEOUT_MS,
|
|
25
|
+
meta
|
|
26
|
+
};
|
|
27
|
+
const handle = getSequenceManager().register(input.sequence, input.action, options);
|
|
28
|
+
handles.set(key, handle);
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
export function remove_chord(sequence) {
|
|
32
|
+
const key = canonicalize(sequence);
|
|
33
|
+
handles.get(key)?.unregister();
|
|
34
|
+
handles.delete(key);
|
|
35
|
+
enabledState.delete(key);
|
|
36
|
+
}
|
|
37
|
+
export function toggle_chord(sequence) {
|
|
38
|
+
const key = canonicalize(sequence);
|
|
39
|
+
const handle = handles.get(key);
|
|
40
|
+
if (!handle?.isActive)
|
|
41
|
+
return;
|
|
42
|
+
const next = !(enabledState.get(key) ?? true);
|
|
43
|
+
enabledState.set(key, next);
|
|
44
|
+
handle.setOptions({ enabled: next });
|
|
45
|
+
}
|
|
46
|
+
export function isChordEnabled(sequence) {
|
|
47
|
+
return enabledState.get(canonicalize(sequence)) ?? true;
|
|
257
48
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
let { expiresAt }: { expiresAt: number } = $props();
|
|
2
|
+
let { expiresAt, duration }: { expiresAt: number; duration: number } = $props();
|
|
5
3
|
|
|
6
4
|
let progress = $state(1);
|
|
7
5
|
let frame: number;
|
|
@@ -9,7 +7,7 @@
|
|
|
9
7
|
function update() {
|
|
10
8
|
const now = Date.now();
|
|
11
9
|
const remaining = Math.max(0, expiresAt - now);
|
|
12
|
-
progress = remaining /
|
|
10
|
+
progress = duration > 0 ? remaining / duration : 0;
|
|
13
11
|
|
|
14
12
|
if (remaining > 0) {
|
|
15
13
|
frame = requestAnimationFrame(update);
|
|
@@ -1,79 +1,37 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { formatForDisplay, type RegisterableHotkey } from '@tanstack/svelte-hotkeys';
|
|
3
|
+
import { getIsMac } from '../utils.js';
|
|
2
4
|
import * as Kbd from './ui/kbd/index.js';
|
|
3
|
-
import { getKeyLabel, getIsMac } from '../utils.js';
|
|
4
5
|
|
|
5
6
|
let {
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
hotkey,
|
|
8
|
+
sequence,
|
|
8
9
|
formatShortcut
|
|
9
10
|
}: {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
formatShortcut?: (
|
|
11
|
+
hotkey?: RegisterableHotkey;
|
|
12
|
+
sequence?: RegisterableHotkey[];
|
|
13
|
+
formatShortcut?: (
|
|
14
|
+
hotkey: RegisterableHotkey | undefined,
|
|
15
|
+
sequence: RegisterableHotkey[] | undefined,
|
|
16
|
+
isMac: boolean
|
|
17
|
+
) => string;
|
|
13
18
|
} = $props();
|
|
14
19
|
|
|
15
20
|
const isMac = getIsMac();
|
|
21
|
+
const platform: 'mac' | 'windows' = $derived(isMac ? 'mac' : 'windows');
|
|
22
|
+
const isChordMode = $derived(sequence !== undefined);
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'meta',
|
|
26
|
-
'command',
|
|
27
|
-
'cmd'
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
function isModifier(key: string): boolean {
|
|
31
|
-
return MODIFIER_KEYS.has(key.toLowerCase());
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function sortKeys(keys: string[]): string[] {
|
|
35
|
-
return [...keys].sort((a, b) => {
|
|
36
|
-
const aIsModifier = isModifier(a);
|
|
37
|
-
const bIsModifier = isModifier(b);
|
|
38
|
-
if (aIsModifier && !bIsModifier) return -1;
|
|
39
|
-
if (!aIsModifier && bIsModifier) return 1;
|
|
40
|
-
return 0;
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let keyGroups: KeyCombination[] = $derived(
|
|
45
|
-
typeof keys === 'string'
|
|
46
|
-
? [[keys]]
|
|
47
|
-
: Array.isArray(keys) && keys.length > 0 && typeof keys[0] === 'string'
|
|
48
|
-
? [sortKeys(keys as string[])]
|
|
49
|
-
: (keys as KeyCombination[]).map(sortKeys)
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
let isChordMode: boolean = $derived(isChord === true);
|
|
53
|
-
|
|
54
|
-
const formatter: Intl.ListFormat = $derived(
|
|
55
|
-
new Intl.ListFormat(
|
|
56
|
-
undefined,
|
|
57
|
-
isChordMode
|
|
58
|
-
? {
|
|
59
|
-
style: 'narrow',
|
|
60
|
-
type: 'unit'
|
|
61
|
-
}
|
|
62
|
-
: {
|
|
63
|
-
style: 'long',
|
|
64
|
-
type: 'disjunction'
|
|
65
|
-
}
|
|
66
|
-
)
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const formattedParts = $derived.by(() => {
|
|
70
|
-
const combos = keyGroups.map((group) => group.map((key) => getKeyLabel(key, isMac)).join(' '));
|
|
71
|
-
return formatter.formatToParts(combos);
|
|
24
|
+
const tokens: string[] = $derived.by(() => {
|
|
25
|
+
if (hotkey !== undefined) {
|
|
26
|
+
return [formatForDisplay(hotkey, { platform, useSymbols: isMac })];
|
|
27
|
+
}
|
|
28
|
+
if (sequence) {
|
|
29
|
+
return sequence.map((step) => formatForDisplay(step, { platform, useSymbols: isMac }));
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
72
32
|
});
|
|
73
33
|
|
|
74
|
-
const customFormatted = $derived(
|
|
75
|
-
formatShortcut ? formatShortcut(keyGroups, isChordMode, isMac) : null
|
|
76
|
-
);
|
|
34
|
+
const customFormatted = $derived(formatShortcut ? formatShortcut(hotkey, sequence, isMac) : null);
|
|
77
35
|
</script>
|
|
78
36
|
|
|
79
37
|
{#if customFormatted !== null}
|
|
@@ -82,14 +40,15 @@
|
|
|
82
40
|
</Kbd.Group>
|
|
83
41
|
{:else}
|
|
84
42
|
<Kbd.Group class="incant-kbds-container">
|
|
85
|
-
{#each
|
|
86
|
-
{#if
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
43
|
+
{#each tokens as token, i (i)}
|
|
44
|
+
{#if i > 0}
|
|
45
|
+
{#if isChordMode}
|
|
46
|
+
<span class="incant-kbds-chord-separator">→</span>
|
|
47
|
+
{:else}
|
|
48
|
+
<span class="incant-kbds-separator">+</span>
|
|
49
|
+
{/if}
|
|
92
50
|
{/if}
|
|
51
|
+
<Kbd.Root>{token}</Kbd.Root>
|
|
93
52
|
{/each}
|
|
94
53
|
</Kbd.Group>
|
|
95
54
|
{/if}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { type RegisterableHotkey } from '@tanstack/svelte-hotkeys';
|
|
1
2
|
type $$ComponentProps = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
formatShortcut?: (
|
|
3
|
+
hotkey?: RegisterableHotkey;
|
|
4
|
+
sequence?: RegisterableHotkey[];
|
|
5
|
+
formatShortcut?: (hotkey: RegisterableHotkey | undefined, sequence: RegisterableHotkey[] | undefined, isMac: boolean) => string;
|
|
5
6
|
};
|
|
6
7
|
declare const Kbds: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
8
|
type Kbds = ReturnType<typeof Kbds>;
|