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.
@@ -59,7 +59,7 @@
59
59
  role="combobox"
60
60
  aria-expanded={open}
61
61
  {@attach shortcut({
62
- keys: ['meta', 'k'],
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
- keys: ['meta', 'j'],
101
+ hotkey: 'Mod+J',
102
102
  description: 'Focus framework combobox'
103
103
  })}
104
104
  >
@@ -146,7 +146,7 @@
146
146
  href="https://github.com/mastermakrela/svelte-incant"
147
147
  target="_blank"
148
148
  {@attach shortcut({
149
- keys: 'G',
149
+ hotkey: 'G',
150
150
  description: 'Open GitHub'
151
151
  })}
152
152
  >
@@ -1,10 +1,12 @@
1
+ import { type RegisterableHotkey } from '@tanstack/svelte-hotkeys';
1
2
  import type { Attachment } from 'svelte/attachments';
2
- import { type Shortcut } from './palette.svelte.js';
3
- type ShortcutInput = Omit<Shortcut, 'action' | 'keys'> & {
4
- keys: string | string[] | string[][];
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(shortcut: ShortcutInput): Attachment<HTMLElement>;
11
+ export declare function shortcut(input: ShortcutInput): Attachment<HTMLElement>;
10
12
  export {};
@@ -1,8 +1,8 @@
1
- import { PressedKeys, watch } from 'runed';
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, remove_shortcut, shortcuts, slugify } from './palette.svelte.js';
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, keys) {
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: { keys }
45
+ props: { hotkey }
46
46
  });
47
47
  targetNode.appendChild(anchor);
48
48
  return { anchor, instance };
49
49
  }
50
- function setupOutline(element, keys) {
50
+ function setupOutline(element, hotkey) {
51
51
  element.style.transition = 'outline 0s, outline-offset 0s';
52
- const slug = slugify(keys);
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(shortcut) {
64
+ export function shortcut(input) {
70
65
  return (element) => {
71
- const action = () => {
72
- element.focus();
73
- if (shortcut.click !== false) {
74
- element.click();
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
- shortcut.action?.();
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(shortcut.keys);
86
+ remove_shortcut(input.hotkey);
91
87
  return () => { };
92
88
  }
93
- const { anchor, instance } = setupAnchor(element, targetNode, isVoidElement, shortcut.keys);
94
- setupOutline(element, shortcut.keys);
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(shortcut.keys);
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
- steps,
7
+ sequence,
8
8
  description,
9
9
  action
10
10
  }: {
11
- steps: string[][];
11
+ sequence: HotkeySequence;
12
12
  description?: string;
13
13
  action: () => void;
14
14
  } = $props();
15
15
 
16
- watch([() => steps, () => description, () => action], () => {
16
+ watch([() => sequence, () => description, () => action], () => {
17
17
  add_chord({
18
- steps,
18
+ sequence,
19
19
  description,
20
20
  action
21
21
  });
22
22
 
23
23
  return () => {
24
- remove_chord(steps);
24
+ remove_chord(sequence);
25
25
  };
26
26
  });
27
27
  </script>
@@ -1,5 +1,6 @@
1
+ import type { HotkeySequence } from '@tanstack/svelte-hotkeys';
1
2
  type $$ComponentProps = {
2
- steps: string[][];
3
+ sequence: HotkeySequence;
3
4
  description?: string;
4
5
  action: () => void;
5
6
  };
@@ -1,257 +1,48 @@
1
- import { PressedKeys } from 'runed';
2
- import { on } from 'svelte/events';
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
- export function isChordInput(keys) {
6
- return (typeof keys === 'object' &&
7
- keys !== null &&
8
- 'isChord' in keys &&
9
- keys.isChord === true);
10
- }
11
- function sortCombo(combo) {
12
- return [...combo].sort((a, b) => a.localeCompare(b));
13
- }
14
- function comboToString(combo) {
15
- return combo.join('-');
16
- }
17
- export function normalizeChordSteps(steps) {
18
- return steps.map((step) => sortCombo(step));
19
- }
20
- export function slugifyChord(steps) {
21
- const normalized = normalizeChordSteps(steps);
22
- return normalized.map((step) => comboToString(step).toLowerCase().replace(/\s+/g, '-')).join('>');
23
- }
24
- export class ChordRegistry {
25
- chords = $state({});
26
- chordOrder = [];
27
- chordPrefixes = $state(new SvelteSet());
28
- currentProgress = $state(null);
29
- pressedKeys = new PressedKeys();
30
- chordTimeout = null;
31
- isListening = false;
32
- cleanupCallbacks = [];
33
- constructor() { }
34
- startListening() {
35
- if (this.isListening)
36
- return;
37
- if (typeof window === 'undefined')
38
- return;
39
- this.isListening = true;
40
- // Set up escape key handler using svelte/events
41
- const escapeCleanup = on(window, 'keydown', (event) => {
42
- if (event.key === 'Escape') {
43
- this.resetChord();
44
- }
45
- });
46
- this.cleanupCallbacks.push(escapeCleanup);
47
- // Use $effect to sync chord listeners when chords change
48
- $effect(() => {
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
- import { CHORD_TIMEOUT_MS } from '../chord.svelte.js';
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 / CHORD_TIMEOUT_MS;
10
+ progress = duration > 0 ? remaining / duration : 0;
13
11
 
14
12
  if (remaining > 0) {
15
13
  frame = requestAnimationFrame(update);
@@ -1,5 +1,6 @@
1
1
  type $$ComponentProps = {
2
2
  expiresAt: number;
3
+ duration: number;
3
4
  };
4
5
  declare const CircularProgress: import("svelte").Component<$$ComponentProps, {}, "">;
5
6
  type CircularProgress = ReturnType<typeof CircularProgress>;
@@ -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
- keys,
7
- isChord = false,
7
+ hotkey,
8
+ sequence,
8
9
  formatShortcut
9
10
  }: {
10
- keys: string | string[] | string[][];
11
- isChord?: boolean;
12
- formatShortcut?: (keys: string[][], isChord: boolean, isMac: boolean) => string;
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
- type KeyCombination = string[];
18
-
19
- const MODIFIER_KEYS = new Set([
20
- 'control',
21
- 'ctrl',
22
- 'alt',
23
- 'option',
24
- 'shift',
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 formattedParts as part (part)}
86
- {#if part.type === 'element'}
87
- <Kbd.Root>{part.value}</Kbd.Root>
88
- {:else if isChordMode}
89
- <span class="incant-kbds-chord-separator">→</span>
90
- {:else}
91
- <span class="incant-kbds-separator">{part.value}</span>
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
- keys: string | string[] | string[][];
3
- isChord?: boolean;
4
- formatShortcut?: (keys: string[][], isChord: boolean, isMac: boolean) => string;
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>;