@cloudvoyant/helical-svelte 0.20.1 → 0.21.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/Kbd.svelte +86 -0
- package/dist/Kbd.svelte.d.ts +10 -0
- package/dist/PageContent.svelte +33 -4
- package/dist/PageContent.svelte.d.ts +2 -1
- package/dist/PageGutter.svelte +34 -3
- package/dist/PageGutter.svelte.d.ts +2 -1
- package/dist/PageSection.svelte +33 -4
- package/dist/PageSection.svelte.d.ts +2 -1
- package/dist/SplitterPanel.svelte +29 -3
- package/dist/SplitterPanel.svelte.d.ts +3 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/listbox/ListboxItemGroup.svelte +17 -0
- package/dist/listbox/ListboxItemGroup.svelte.d.ts +9 -0
- package/dist/listbox/ListboxItemGroupLabel.svelte +17 -0
- package/dist/listbox/ListboxItemGroupLabel.svelte.d.ts +9 -0
- package/dist/listbox/index.d.ts +2 -0
- package/dist/listbox/index.js +2 -0
- package/dist/sidebar/Root.svelte +34 -9
- package/dist/sidebar/Root.svelte.d.ts +2 -1
- package/dist/which-key/Pane.svelte +49 -0
- package/dist/which-key/Pane.svelte.d.ts +10 -0
- package/dist/which-key/WhichKey.svelte +197 -0
- package/dist/which-key/WhichKey.svelte.d.ts +8 -0
- package/dist/which-key/WhichKeyExplorer.svelte +269 -0
- package/dist/which-key/WhichKeyExplorer.svelte.d.ts +8 -0
- package/dist/which-key/WhichKeyMode.svelte +44 -0
- package/dist/which-key/WhichKeyMode.svelte.d.ts +9 -0
- package/dist/which-key/WhichKeyProvider.svelte +136 -0
- package/dist/which-key/WhichKeyProvider.svelte.d.ts +9 -0
- package/dist/which-key/context.svelte.d.ts +57 -0
- package/dist/which-key/context.svelte.js +177 -0
- package/dist/which-key/index.d.ts +7 -0
- package/dist/which-key/index.js +7 -0
- package/dist/window/Window.svelte +30 -4
- package/dist/window/Window.svelte.d.ts +2 -1
- package/package.json +5 -2
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<!-- libs/helical-svelte/src/which-key/WhichKey.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { Portal } from '@ark-ui/svelte/portal';
|
|
4
|
+
import { Presence } from '@ark-ui/svelte/presence';
|
|
5
|
+
import {
|
|
6
|
+
cn,
|
|
7
|
+
jumpBadgeMatchedPrefixBase,
|
|
8
|
+
jumpBadgeVariants,
|
|
9
|
+
jumpOverlayRootBase,
|
|
10
|
+
whichKeyBodyVariants,
|
|
11
|
+
whichKeyBreadcrumbBase,
|
|
12
|
+
whichKeyFooterBase,
|
|
13
|
+
whichKeyHeaderBase,
|
|
14
|
+
whichKeyItemBase,
|
|
15
|
+
whichKeyItemLabelBase,
|
|
16
|
+
whichKeyTitleBase,
|
|
17
|
+
whichKeyVariants,
|
|
18
|
+
type WhichKeyProps as CoreWhichKeyProps,
|
|
19
|
+
} from '@cloudvoyant/helical-ui';
|
|
20
|
+
import Kbd from '../Kbd.svelte';
|
|
21
|
+
import Row from '../Row.svelte';
|
|
22
|
+
import Scroll from '../Scroll.svelte';
|
|
23
|
+
import { getOptionalWhichKeyContext } from './context.svelte';
|
|
24
|
+
import WhichKeyExplorer from './WhichKeyExplorer.svelte';
|
|
25
|
+
import WhichKeyMode from './WhichKeyMode.svelte';
|
|
26
|
+
|
|
27
|
+
type Props = Omit<CoreWhichKeyProps, 'className'> & {
|
|
28
|
+
class?: string;
|
|
29
|
+
className?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
variant = 'helix',
|
|
34
|
+
showModeBadge = true,
|
|
35
|
+
showExplorer = true,
|
|
36
|
+
class: className = '',
|
|
37
|
+
className: reactClassName = '',
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
const ctx = getOptionalWhichKeyContext();
|
|
41
|
+
|
|
42
|
+
let panelRef: HTMLDivElement | null = $state(null);
|
|
43
|
+
let jumpOverlayRef: HTMLDivElement | null = $state(null);
|
|
44
|
+
|
|
45
|
+
$effect(() => {
|
|
46
|
+
if (!ctx?.state.isOpen) return;
|
|
47
|
+
return ctx.controller.trackBranchElement(() => panelRef);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
$effect(() => {
|
|
51
|
+
if (ctx?.state.mode !== 'jump') return;
|
|
52
|
+
return ctx.controller.trackBranchElement(() => jumpOverlayRef);
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
{#if ctx}
|
|
57
|
+
<Portal>
|
|
58
|
+
<Presence present={ctx.state.isOpen} lazyMount unmountOnExit>
|
|
59
|
+
<div
|
|
60
|
+
bind:this={panelRef}
|
|
61
|
+
role="region"
|
|
62
|
+
aria-label={ctx.state.title || 'WhichKey commands'}
|
|
63
|
+
data-which-key-root="true"
|
|
64
|
+
data-testid="which-key-panel"
|
|
65
|
+
data-variant={variant}
|
|
66
|
+
data-preset={ctx.state.preset}
|
|
67
|
+
data-mode={ctx.state.mode}
|
|
68
|
+
data-sticky={ctx.state.sticky ? 'true' : 'false'}
|
|
69
|
+
class={cn(whichKeyVariants({ variant }), className, reactClassName)}
|
|
70
|
+
>
|
|
71
|
+
<div class={whichKeyHeaderBase}>
|
|
72
|
+
<div class={whichKeyTitleBase}>
|
|
73
|
+
{#if showModeBadge}
|
|
74
|
+
<WhichKeyMode mode={ctx.state.mode} sticky={ctx.state.sticky} />
|
|
75
|
+
{/if}
|
|
76
|
+
<span data-testid="which-key-title">
|
|
77
|
+
{ctx.state.title === 'Space' ? 'Leader' : ctx.state.title}
|
|
78
|
+
</span>
|
|
79
|
+
{#if ctx.state.countBuffer}
|
|
80
|
+
<span
|
|
81
|
+
data-testid="which-key-count"
|
|
82
|
+
class="rounded bg-muted px-1 font-mono text-[10px]"
|
|
83
|
+
>
|
|
84
|
+
{ctx.state.countBuffer}×
|
|
85
|
+
</span>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
<div
|
|
89
|
+
data-testid="which-key-breadcrumb"
|
|
90
|
+
class={whichKeyBreadcrumbBase}
|
|
91
|
+
>
|
|
92
|
+
<span>{ctx.state.breadcrumb.join(' » ')}</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<Scroll class="max-h-72" viewportClass="max-h-72">
|
|
97
|
+
<div
|
|
98
|
+
data-testid="which-key-items"
|
|
99
|
+
class={whichKeyBodyVariants({ variant })}
|
|
100
|
+
>
|
|
101
|
+
{#each ctx.state.items as node (node.key)}
|
|
102
|
+
{@const isGroup = Boolean(
|
|
103
|
+
node.children || node.proxy || node.expand || node.label.startsWith('+'),
|
|
104
|
+
)}
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
data-testid="which-key-item"
|
|
108
|
+
data-key={node.key}
|
|
109
|
+
data-group={isGroup ? 'true' : 'false'}
|
|
110
|
+
onclick={(e: MouseEvent) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
e.stopPropagation();
|
|
113
|
+
ctx.controller.triggerKey(node.key);
|
|
114
|
+
}}
|
|
115
|
+
class={whichKeyItemBase}
|
|
116
|
+
>
|
|
117
|
+
<span
|
|
118
|
+
data-group={isGroup ? 'true' : 'false'}
|
|
119
|
+
class={whichKeyItemLabelBase}
|
|
120
|
+
>
|
|
121
|
+
{node.label}
|
|
122
|
+
</span>
|
|
123
|
+
<Kbd
|
|
124
|
+
size="sm"
|
|
125
|
+
variant={isGroup ? 'subtle' : 'outline'}
|
|
126
|
+
hotkey={node.key}
|
|
127
|
+
/>
|
|
128
|
+
</button>
|
|
129
|
+
{/each}
|
|
130
|
+
</div>
|
|
131
|
+
</Scroll>
|
|
132
|
+
|
|
133
|
+
<div class={whichKeyFooterBase}>
|
|
134
|
+
<Row class="items-center gap-2">
|
|
135
|
+
{#if ctx.state.breadcrumb.length > 1}
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onclick={() => ctx.controller.stepBackPrefix()}
|
|
139
|
+
class="inline-flex items-center gap-1 hover:text-foreground"
|
|
140
|
+
>
|
|
141
|
+
<Kbd size="sm">⌫</Kbd>
|
|
142
|
+
<span>Back</span>
|
|
143
|
+
</button>
|
|
144
|
+
{/if}
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onclick={() => ctx.controller.closePrefix()}
|
|
148
|
+
class="inline-flex items-center gap-1 hover:text-foreground"
|
|
149
|
+
>
|
|
150
|
+
<Kbd size="sm">Esc</Kbd>
|
|
151
|
+
<span>Close</span>
|
|
152
|
+
</button>
|
|
153
|
+
</Row>
|
|
154
|
+
<span class="font-mono uppercase opacity-75">
|
|
155
|
+
{ctx.state.preset}
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</Presence>
|
|
160
|
+
</Portal>
|
|
161
|
+
|
|
162
|
+
<Portal>
|
|
163
|
+
<Presence
|
|
164
|
+
present={ctx.state.mode === 'jump' && ctx.state.jumpTargets.length > 0}
|
|
165
|
+
lazyMount
|
|
166
|
+
unmountOnExit
|
|
167
|
+
>
|
|
168
|
+
<div
|
|
169
|
+
bind:this={jumpOverlayRef}
|
|
170
|
+
data-which-key-jump-overlay="true"
|
|
171
|
+
data-testid="which-key-jump-overlay"
|
|
172
|
+
data-jump-action={ctx.state.jumpAction}
|
|
173
|
+
class={jumpOverlayRootBase}
|
|
174
|
+
>
|
|
175
|
+
{#each ctx.state.jumpTargets as target (target.id)}
|
|
176
|
+
<span
|
|
177
|
+
data-testid="which-key-jump-badge"
|
|
178
|
+
data-jump-label={target.label}
|
|
179
|
+
style={`top: ${Math.max(4, target.rect.top - 4)}px; left: ${Math.max(4, target.rect.left - 4)}px;`}
|
|
180
|
+
class={jumpBadgeVariants({ action: ctx.state.jumpAction })}
|
|
181
|
+
>
|
|
182
|
+
{#if target.matchedPrefix}
|
|
183
|
+
<span class={jumpBadgeMatchedPrefixBase}>
|
|
184
|
+
{target.matchedPrefix}
|
|
185
|
+
</span>
|
|
186
|
+
{/if}
|
|
187
|
+
<span>{target.remainingLabel}</span>
|
|
188
|
+
</span>
|
|
189
|
+
{/each}
|
|
190
|
+
</div>
|
|
191
|
+
</Presence>
|
|
192
|
+
</Portal>
|
|
193
|
+
|
|
194
|
+
{#if showExplorer}
|
|
195
|
+
<WhichKeyExplorer />
|
|
196
|
+
{/if}
|
|
197
|
+
{/if}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type WhichKeyProps as CoreWhichKeyProps } from '@cloudvoyant/helical-ui';
|
|
2
|
+
type Props = Omit<CoreWhichKeyProps, 'className'> & {
|
|
3
|
+
class?: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const WhichKey: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type WhichKey = ReturnType<typeof WhichKey>;
|
|
8
|
+
export default WhichKey;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
<!-- libs/helical-svelte/src/which-key/WhichKeyExplorer.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { Highlight } from '@ark-ui/svelte/highlight';
|
|
4
|
+
import { createListCollection } from '@ark-ui/svelte/listbox';
|
|
5
|
+
import {
|
|
6
|
+
cn,
|
|
7
|
+
whichKeyExplorerContentBase,
|
|
8
|
+
whichKeyExplorerHeaderBase,
|
|
9
|
+
whichKeyExplorerHighlightMarkBase,
|
|
10
|
+
whichKeyExplorerInputBase,
|
|
11
|
+
whichKeyExplorerItemBase,
|
|
12
|
+
whichKeyExplorerListboxBase,
|
|
13
|
+
whichKeyExplorerListContentBase,
|
|
14
|
+
whichKeyModeBadgeVariants,
|
|
15
|
+
type KeymapExplorerEntry,
|
|
16
|
+
type WhichKeyExplorerProps as CoreWhichKeyExplorerProps,
|
|
17
|
+
} from '@cloudvoyant/helical-ui';
|
|
18
|
+
import {
|
|
19
|
+
Dialog,
|
|
20
|
+
DialogBackdrop,
|
|
21
|
+
DialogContent,
|
|
22
|
+
DialogDismiss,
|
|
23
|
+
DialogHeader,
|
|
24
|
+
DialogTitle,
|
|
25
|
+
} from '../dialog';
|
|
26
|
+
import Kbd from '../Kbd.svelte';
|
|
27
|
+
import {
|
|
28
|
+
Listbox,
|
|
29
|
+
ListboxContent,
|
|
30
|
+
ListboxEmpty,
|
|
31
|
+
ListboxInput,
|
|
32
|
+
ListboxItem,
|
|
33
|
+
ListboxItemGroup,
|
|
34
|
+
ListboxItemGroupLabel,
|
|
35
|
+
ListboxItemText,
|
|
36
|
+
} from '../listbox';
|
|
37
|
+
import Row from '../Row.svelte';
|
|
38
|
+
import Scroll from '../Scroll.svelte';
|
|
39
|
+
import { getOptionalWhichKeyContext } from './context.svelte';
|
|
40
|
+
|
|
41
|
+
type Props = Omit<CoreWhichKeyExplorerProps, 'className'> & {
|
|
42
|
+
class?: string;
|
|
43
|
+
className?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let {
|
|
47
|
+
open: controlledOpen,
|
|
48
|
+
onOpenChange,
|
|
49
|
+
placeholder = 'Search keybindings...',
|
|
50
|
+
class: className = '',
|
|
51
|
+
className: reactClassName = '',
|
|
52
|
+
}: Props = $props();
|
|
53
|
+
|
|
54
|
+
const ctx = getOptionalWhichKeyContext();
|
|
55
|
+
|
|
56
|
+
const isOpen = $derived(controlledOpen ?? Boolean(ctx?.state.explorerOpen));
|
|
57
|
+
const explorerEntries = $derived(ctx?.state.explorerEntries ?? []);
|
|
58
|
+
const explorerQuery = $derived(ctx?.state.explorerQuery ?? '');
|
|
59
|
+
const preset = $derived(ctx?.state.preset ?? 'helix');
|
|
60
|
+
|
|
61
|
+
let highlightedId = $state<string | null>(null);
|
|
62
|
+
|
|
63
|
+
const groupedEntries = $derived.by((): Array<[string, KeymapExplorerEntry[]]> => {
|
|
64
|
+
const groups = new Map<string, KeymapExplorerEntry[]>();
|
|
65
|
+
for (const entry of explorerEntries) {
|
|
66
|
+
const groupName = entry.group || 'General';
|
|
67
|
+
const list = groups.get(groupName) ?? [];
|
|
68
|
+
list.push(entry);
|
|
69
|
+
groups.set(groupName, list);
|
|
70
|
+
}
|
|
71
|
+
return Array.from(groups.entries());
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const orderedEntries = $derived(groupedEntries.flatMap(([, entries]) => entries));
|
|
75
|
+
|
|
76
|
+
const effectiveHighlightedId = $derived.by((): string | null => {
|
|
77
|
+
if (orderedEntries.length === 0) return null;
|
|
78
|
+
if (highlightedId && orderedEntries.some((e) => e.id === highlightedId)) {
|
|
79
|
+
return highlightedId;
|
|
80
|
+
}
|
|
81
|
+
return orderedEntries[0]!.id;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const collection = $derived(
|
|
85
|
+
createListCollection({
|
|
86
|
+
items: orderedEntries.map((entry) => ({
|
|
87
|
+
value: entry.id,
|
|
88
|
+
label: entry.label,
|
|
89
|
+
entry,
|
|
90
|
+
})),
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
$effect(() => {
|
|
95
|
+
if (!isOpen || typeof document === 'undefined') return;
|
|
96
|
+
const focusInput = () => {
|
|
97
|
+
const input = document.querySelector<HTMLInputElement>(
|
|
98
|
+
'[data-which-key-explorer] [data-testid="which-key-explorer-input"]',
|
|
99
|
+
);
|
|
100
|
+
input?.focus();
|
|
101
|
+
};
|
|
102
|
+
focusInput();
|
|
103
|
+
const raf = requestAnimationFrame(focusInput);
|
|
104
|
+
return () => cancelAnimationFrame(raf);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function scrollItemIntoView(id: string) {
|
|
108
|
+
requestAnimationFrame(() => {
|
|
109
|
+
if (typeof document === 'undefined') return;
|
|
110
|
+
const items = Array.from(
|
|
111
|
+
document.querySelectorAll<HTMLElement>(
|
|
112
|
+
'[data-which-key-explorer] [data-part="item"]',
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
for (const el of items) {
|
|
116
|
+
if (el.getAttribute('data-value') === id) {
|
|
117
|
+
el.scrollIntoView({ block: 'nearest' });
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function handleOpenChange(details: { open: boolean }) {
|
|
125
|
+
if (!ctx) return;
|
|
126
|
+
if (details.open) {
|
|
127
|
+
ctx.controller.openExplorer();
|
|
128
|
+
} else {
|
|
129
|
+
ctx.controller.closeExplorer();
|
|
130
|
+
}
|
|
131
|
+
onOpenChange?.(details.open);
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
{#if ctx && isOpen}
|
|
136
|
+
<Dialog
|
|
137
|
+
open={isOpen}
|
|
138
|
+
lazyMount
|
|
139
|
+
unmountOnExit
|
|
140
|
+
initialFocusEl={() =>
|
|
141
|
+
typeof document !== 'undefined'
|
|
142
|
+
? document.querySelector<HTMLElement>(
|
|
143
|
+
'[data-which-key-explorer] [data-testid="which-key-explorer-input"]',
|
|
144
|
+
)
|
|
145
|
+
: null}
|
|
146
|
+
onOpenChange={handleOpenChange}
|
|
147
|
+
>
|
|
148
|
+
<DialogBackdrop />
|
|
149
|
+
<DialogContent
|
|
150
|
+
data-which-key-explorer="true"
|
|
151
|
+
data-testid="which-key-explorer"
|
|
152
|
+
class={cn(whichKeyExplorerContentBase, className, reactClassName)}
|
|
153
|
+
>
|
|
154
|
+
<DialogHeader class={whichKeyExplorerHeaderBase}>
|
|
155
|
+
<Row class="items-center gap-2">
|
|
156
|
+
<DialogTitle class="text-sm font-semibold">Keybindings</DialogTitle>
|
|
157
|
+
<span class={whichKeyModeBadgeVariants({ mode: 'normal' })}>
|
|
158
|
+
{preset}
|
|
159
|
+
</span>
|
|
160
|
+
</Row>
|
|
161
|
+
<DialogDismiss
|
|
162
|
+
tabindex={-1}
|
|
163
|
+
class="static opacity-80 hover:opacity-100 focus:ring-0"
|
|
164
|
+
>
|
|
165
|
+
{#snippet children()}
|
|
166
|
+
<Kbd size="sm">Esc</Kbd>
|
|
167
|
+
{/snippet}
|
|
168
|
+
</DialogDismiss>
|
|
169
|
+
</DialogHeader>
|
|
170
|
+
|
|
171
|
+
<Listbox
|
|
172
|
+
{collection}
|
|
173
|
+
selectionMode="single"
|
|
174
|
+
highlightedValue={effectiveHighlightedId}
|
|
175
|
+
onHighlightChange={(details) => {
|
|
176
|
+
if (details.highlightedValue) {
|
|
177
|
+
highlightedId = details.highlightedValue;
|
|
178
|
+
}
|
|
179
|
+
}}
|
|
180
|
+
onSelect={(details) => {
|
|
181
|
+
ctx.controller.executeExplorerEntry(details.value);
|
|
182
|
+
}}
|
|
183
|
+
class={whichKeyExplorerListboxBase}
|
|
184
|
+
>
|
|
185
|
+
<ListboxInput
|
|
186
|
+
autofocus
|
|
187
|
+
autoHighlight
|
|
188
|
+
data-testid="which-key-explorer-input"
|
|
189
|
+
{placeholder}
|
|
190
|
+
value={explorerQuery}
|
|
191
|
+
oninput={(e: Event) => {
|
|
192
|
+
ctx.controller.setExplorerQuery((e.currentTarget as HTMLInputElement).value);
|
|
193
|
+
}}
|
|
194
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
195
|
+
if (orderedEntries.length === 0) return;
|
|
196
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
const delta = e.key === 'ArrowDown' ? 1 : -1;
|
|
200
|
+
const currentIdx = orderedEntries.findIndex(
|
|
201
|
+
(item) => item.id === effectiveHighlightedId,
|
|
202
|
+
);
|
|
203
|
+
const nextIdx =
|
|
204
|
+
currentIdx === -1
|
|
205
|
+
? 0
|
|
206
|
+
: (currentIdx + delta + orderedEntries.length) %
|
|
207
|
+
orderedEntries.length;
|
|
208
|
+
const nextId = orderedEntries[nextIdx]!.id;
|
|
209
|
+
highlightedId = nextId;
|
|
210
|
+
scrollItemIntoView(nextId);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (e.key === 'Enter') {
|
|
214
|
+
const itemVal = effectiveHighlightedId ?? orderedEntries[0]?.id;
|
|
215
|
+
if (itemVal) {
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
e.stopPropagation();
|
|
218
|
+
ctx.controller.executeExplorerEntry(itemVal);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}}
|
|
222
|
+
class={whichKeyExplorerInputBase}
|
|
223
|
+
/>
|
|
224
|
+
<Scroll class="max-h-96 min-h-0 flex-1" viewportClass="max-h-96">
|
|
225
|
+
{#snippet children()}
|
|
226
|
+
<ListboxContent
|
|
227
|
+
data-testid="which-key-explorer-list"
|
|
228
|
+
class={whichKeyExplorerListContentBase}
|
|
229
|
+
>
|
|
230
|
+
{#each groupedEntries as [groupName, entries] (groupName)}
|
|
231
|
+
<ListboxItemGroup>
|
|
232
|
+
<ListboxItemGroupLabel>{groupName}</ListboxItemGroupLabel>
|
|
233
|
+
{#each entries as entry (entry.id)}
|
|
234
|
+
<ListboxItem
|
|
235
|
+
item={{
|
|
236
|
+
value: entry.id,
|
|
237
|
+
label: entry.label,
|
|
238
|
+
}}
|
|
239
|
+
data-testid="which-key-explorer-item"
|
|
240
|
+
data-command-id={entry.commandId}
|
|
241
|
+
onclick={() => ctx.controller.executeExplorerEntry(entry)}
|
|
242
|
+
class={whichKeyExplorerItemBase}
|
|
243
|
+
>
|
|
244
|
+
<ListboxItemText class="min-w-0 flex-1 truncate font-medium text-foreground">
|
|
245
|
+
<Highlight
|
|
246
|
+
text={entry.label}
|
|
247
|
+
query={explorerQuery}
|
|
248
|
+
ignoreCase
|
|
249
|
+
matchAll
|
|
250
|
+
class={whichKeyExplorerHighlightMarkBase}
|
|
251
|
+
/>
|
|
252
|
+
</ListboxItemText>
|
|
253
|
+
<Row class="shrink-0 items-center gap-1">
|
|
254
|
+
<Kbd size="sm" hotkey={entry.chords} />
|
|
255
|
+
</Row>
|
|
256
|
+
</ListboxItem>
|
|
257
|
+
{/each}
|
|
258
|
+
</ListboxItemGroup>
|
|
259
|
+
{/each}
|
|
260
|
+
{#if groupedEntries.length === 0}
|
|
261
|
+
<ListboxEmpty>No matching keybindings found.</ListboxEmpty>
|
|
262
|
+
{/if}
|
|
263
|
+
</ListboxContent>
|
|
264
|
+
{/snippet}
|
|
265
|
+
</Scroll>
|
|
266
|
+
</Listbox>
|
|
267
|
+
</DialogContent>
|
|
268
|
+
</Dialog>
|
|
269
|
+
{/if}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type WhichKeyExplorerProps as CoreWhichKeyExplorerProps } from '@cloudvoyant/helical-ui';
|
|
2
|
+
type Props = Omit<CoreWhichKeyExplorerProps, 'className'> & {
|
|
3
|
+
class?: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const WhichKeyExplorer: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type WhichKeyExplorer = ReturnType<typeof WhichKeyExplorer>;
|
|
8
|
+
export default WhichKeyExplorer;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!-- libs/helical-svelte/src/which-key/WhichKeyMode.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import {
|
|
4
|
+
cn,
|
|
5
|
+
whichKeyModeBadgeVariants,
|
|
6
|
+
type WhichKeyModeProps as CoreWhichKeyModeProps,
|
|
7
|
+
} from '@cloudvoyant/helical-ui';
|
|
8
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
9
|
+
import { useWhichKey } from './context.svelte';
|
|
10
|
+
|
|
11
|
+
type Props = Omit<CoreWhichKeyModeProps, 'className'> &
|
|
12
|
+
Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
|
|
13
|
+
class?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
mode: modeProp,
|
|
19
|
+
sticky: stickyProp,
|
|
20
|
+
showPreset = false,
|
|
21
|
+
size = 'sm',
|
|
22
|
+
class: className = '',
|
|
23
|
+
className: reactClassName = '',
|
|
24
|
+
...rest
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
const wk = useWhichKey();
|
|
28
|
+
const resolvedMode = $derived(modeProp ?? wk.mode);
|
|
29
|
+
const resolvedSticky = $derived(stickyProp ?? wk.sticky);
|
|
30
|
+
const modeLabel = $derived(resolvedSticky ? `${resolvedMode}·lock` : resolvedMode);
|
|
31
|
+
const label = $derived(showPreset ? `${wk.preset} · ${modeLabel}` : modeLabel);
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<span
|
|
35
|
+
data-slot="which-key-mode"
|
|
36
|
+
data-testid="which-key-mode-badge"
|
|
37
|
+
data-mode={resolvedMode}
|
|
38
|
+
data-sticky={resolvedSticky ? 'true' : 'false'}
|
|
39
|
+
data-preset={wk.preset}
|
|
40
|
+
class={cn(whichKeyModeBadgeVariants({ mode: resolvedMode, size }), className, reactClassName)}
|
|
41
|
+
{...rest}
|
|
42
|
+
>
|
|
43
|
+
{label}
|
|
44
|
+
</span>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type WhichKeyModeProps as CoreWhichKeyModeProps } from '@cloudvoyant/helical-ui';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Props = Omit<CoreWhichKeyModeProps, 'className'> & Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
|
|
4
|
+
class?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const WhichKeyMode: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type WhichKeyMode = ReturnType<typeof WhichKeyMode>;
|
|
9
|
+
export default WhichKeyMode;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!-- libs/helical-svelte/src/which-key/WhichKeyProvider.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import { untrack, type Snippet } from 'svelte';
|
|
4
|
+
import { useEnvironmentContext } from '@ark-ui/svelte/environment';
|
|
5
|
+
import { HotkeysProvider } from '@tanstack/svelte-hotkeys';
|
|
6
|
+
import {
|
|
7
|
+
createWhichKeyController,
|
|
8
|
+
type WhichKeyControllerOptions,
|
|
9
|
+
type WhichKeyState,
|
|
10
|
+
} from '@cloudvoyant/helical-ui';
|
|
11
|
+
import { setWhichKeyContext } from './context.svelte';
|
|
12
|
+
|
|
13
|
+
type Props = WhichKeyControllerOptions & {
|
|
14
|
+
scoped?: boolean;
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
preset,
|
|
20
|
+
defaultMode = 'normal',
|
|
21
|
+
leaderKey = 'Space',
|
|
22
|
+
jumpAction,
|
|
23
|
+
jumpAlphabet,
|
|
24
|
+
storageKey,
|
|
25
|
+
keymap,
|
|
26
|
+
commands,
|
|
27
|
+
enabled = true,
|
|
28
|
+
disableOnMobile = true,
|
|
29
|
+
scoped = true,
|
|
30
|
+
onPresetChange,
|
|
31
|
+
onModeChange,
|
|
32
|
+
onJumpActionChange,
|
|
33
|
+
onCommand,
|
|
34
|
+
onYank,
|
|
35
|
+
children,
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
const env = useEnvironmentContext();
|
|
39
|
+
let scopeEl: HTMLDivElement | null = $state(null);
|
|
40
|
+
|
|
41
|
+
const controller = untrack(() =>
|
|
42
|
+
createWhichKeyController({
|
|
43
|
+
preset,
|
|
44
|
+
defaultMode,
|
|
45
|
+
leaderKey,
|
|
46
|
+
jumpAction,
|
|
47
|
+
jumpAlphabet,
|
|
48
|
+
storageKey,
|
|
49
|
+
keymap,
|
|
50
|
+
commands,
|
|
51
|
+
enabled,
|
|
52
|
+
disableOnMobile,
|
|
53
|
+
getDocument: () => {
|
|
54
|
+
try {
|
|
55
|
+
return env?.().getDocument?.() ?? document;
|
|
56
|
+
} catch {
|
|
57
|
+
return document;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
getScopeElement: () => (scoped ? scopeEl : null),
|
|
61
|
+
onPresetChange: (next) => onPresetChange?.(next),
|
|
62
|
+
onModeChange: (next) => onModeChange?.(next),
|
|
63
|
+
onJumpActionChange: (next) => onJumpActionChange?.(next),
|
|
64
|
+
onCommand: (id, ctx) => onCommand?.(id, ctx),
|
|
65
|
+
onYank: (text, ctx) => onYank?.(text, ctx),
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
let reactiveState: WhichKeyState = $state(controller.getState());
|
|
70
|
+
|
|
71
|
+
setWhichKeyContext({
|
|
72
|
+
controller,
|
|
73
|
+
get state() {
|
|
74
|
+
return reactiveState;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
$effect(() => {
|
|
79
|
+
const unsubscribe = controller.subscribe(() => {
|
|
80
|
+
reactiveState = controller.getState();
|
|
81
|
+
});
|
|
82
|
+
return () => {
|
|
83
|
+
unsubscribe();
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
$effect(() => {
|
|
88
|
+
if (preset !== undefined) {
|
|
89
|
+
controller.setPreset(preset);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
$effect(() => {
|
|
94
|
+
if (jumpAction !== undefined) {
|
|
95
|
+
controller.setJumpAction(jumpAction);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
$effect(() => {
|
|
100
|
+
if (!enabled) return;
|
|
101
|
+
let doc: Document | undefined;
|
|
102
|
+
try {
|
|
103
|
+
doc = env?.().getDocument?.();
|
|
104
|
+
} catch {
|
|
105
|
+
doc = typeof document !== 'undefined' ? document : undefined;
|
|
106
|
+
}
|
|
107
|
+
return controller.attach(doc);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
$effect(() => {
|
|
111
|
+
if (!keymap) return;
|
|
112
|
+
return controller.registerKeymap('__provider_keymap__', keymap, {
|
|
113
|
+
enabled: true,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
$effect(() => {
|
|
118
|
+
if (!commands) return;
|
|
119
|
+
const cleanups = Object.entries(commands).map(([id, handler]) =>
|
|
120
|
+
controller.registerCommand(id, handler),
|
|
121
|
+
);
|
|
122
|
+
return () => {
|
|
123
|
+
for (const fn of cleanups) fn();
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<HotkeysProvider>
|
|
129
|
+
{#if scoped}
|
|
130
|
+
<div bind:this={scopeEl} data-which-key-scope class="contents">
|
|
131
|
+
{@render children?.()}
|
|
132
|
+
</div>
|
|
133
|
+
{:else}
|
|
134
|
+
{@render children?.()}
|
|
135
|
+
{/if}
|
|
136
|
+
</HotkeysProvider>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import { type WhichKeyControllerOptions } from '@cloudvoyant/helical-ui';
|
|
3
|
+
type Props = WhichKeyControllerOptions & {
|
|
4
|
+
scoped?: boolean;
|
|
5
|
+
children?: Snippet;
|
|
6
|
+
};
|
|
7
|
+
declare const WhichKeyProvider: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type WhichKeyProvider = ReturnType<typeof WhichKeyProvider>;
|
|
9
|
+
export default WhichKeyProvider;
|