@joewinke/jatui 0.22.0 → 0.24.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/CLAUDE.md +72 -2
- package/dist/actions/reveal.d.ts +37 -0
- package/dist/actions/reveal.js +42 -0
- package/dist/components/AudioRecorder.svelte +529 -0
- package/dist/components/AudioRecorder.svelte.d.ts +30 -0
- package/dist/components/BulkActionBar.svelte +40 -0
- package/dist/components/BulkActionBar.svelte.d.ts +9 -0
- package/dist/components/ConfirmDialog.svelte +39 -35
- package/dist/components/CopyButton.svelte +5 -2
- package/dist/components/FloatingActionBar.svelte +55 -1
- package/dist/components/FloatingActionBar.svelte.d.ts +8 -1
- package/dist/components/IconSelector.svelte +417 -0
- package/dist/components/IconSelector.svelte.d.ts +55 -0
- package/dist/components/InfiniteGrid.svelte +305 -0
- package/dist/components/InfiniteGrid.svelte.d.ts +71 -0
- package/dist/components/InfiniteSearchGrid.svelte +131 -0
- package/dist/components/InfiniteSearchGrid.svelte.d.ts +67 -0
- package/dist/components/InputDialog.svelte +49 -38
- package/dist/components/KeyboardShortcutsOverlay.svelte +94 -3
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +61 -16
- package/dist/components/MediaCaptureIcon.svelte +23 -0
- package/dist/components/MediaCaptureIcon.svelte.d.ts +1 -1
- package/dist/components/ThumbnailGrid.svelte +137 -0
- package/dist/components/ThumbnailGrid.svelte.d.ts +42 -0
- package/dist/components/batch/BatchJobTable.svelte +549 -0
- package/dist/components/batch/BatchJobTable.svelte.d.ts +114 -0
- package/dist/components/batch/QuotaMeter.svelte +78 -0
- package/dist/components/batch/QuotaMeter.svelte.d.ts +16 -0
- package/dist/components/batch/batchQueue.d.ts +56 -0
- package/dist/components/batch/batchQueue.js +81 -0
- package/dist/components/batch/elapsedEstimate.d.ts +24 -0
- package/dist/components/batch/elapsedEstimate.js +25 -0
- package/dist/components/batch/pollOperation.d.ts +45 -0
- package/dist/components/batch/pollOperation.js +80 -0
- package/dist/components/batch/useElapsedEstimate.d.ts +10 -0
- package/dist/components/batch/useElapsedEstimate.js +38 -0
- package/dist/components/bulkActionBarKeys.d.ts +40 -0
- package/dist/components/bulkActionBarKeys.js +38 -0
- package/dist/components/infiniteGridLoader.d.ts +70 -0
- package/dist/components/infiniteGridLoader.js +141 -0
- package/dist/components/linked-columns/LinkedColumns.svelte +10 -22
- package/dist/components/linked-columns/LinkedColumns.svelte.d.ts +1 -9
- package/dist/components/linked-columns/types.d.ts +9 -0
- package/dist/components/linked-columns/types.js +1 -0
- package/dist/components/replay/ChapterTimeline.svelte +43 -25
- package/dist/components/replay/ChapterTimeline.svelte.d.ts +26 -1
- package/dist/components/table/columnResize.d.ts +38 -0
- package/dist/components/table/columnResize.js +117 -0
- package/dist/components/table/index.d.ts +2 -0
- package/dist/components/table/index.js +1 -0
- package/dist/components/toast/ToastItem.svelte +47 -2
- package/dist/components/variant/VariantMatrixPicker.svelte +181 -0
- package/dist/components/variant/VariantMatrixPicker.svelte.d.ts +37 -0
- package/dist/components/variant/variantCombinations.d.ts +75 -0
- package/dist/components/variant/variantCombinations.js +100 -0
- package/dist/index.d.ts +34 -3
- package/dist/index.js +30 -1
- package/dist/stores/toast.d.ts +8 -0
- package/dist/stores/toast.js +2 -0
- package/dist/styles/motion.css +100 -0
- package/dist/utils/audioRecorder.d.ts +28 -0
- package/dist/utils/audioRecorder.js +58 -0
- package/dist/utils/audioRecorderCore.d.ts +132 -0
- package/dist/utils/audioRecorderCore.js +447 -0
- package/dist/utils/clipboard.d.ts +25 -0
- package/dist/utils/clipboard.js +59 -0
- package/dist/utils/dateFormatters.d.ts +11 -2
- package/dist/utils/dateFormatters.js +22 -2
- package/dist/utils/keyboardShortcutRegistry.d.ts +152 -0
- package/dist/utils/keyboardShortcutRegistry.js +365 -0
- package/package.json +1 -1
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
function handleSubmit() {
|
|
24
24
|
if (opts?.validate) {
|
|
25
25
|
const msg = opts.validate(value);
|
|
26
|
-
if (msg !== null) {
|
|
26
|
+
if (msg !== null) {
|
|
27
|
+
error = msg;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
27
30
|
}
|
|
28
31
|
error = null;
|
|
29
32
|
submitInput(value);
|
|
@@ -80,7 +83,6 @@
|
|
|
80
83
|
<input
|
|
81
84
|
bind:this={inputRef}
|
|
82
85
|
bind:value
|
|
83
|
-
oninput={() => { if (error) error = null; }}
|
|
84
86
|
type="text"
|
|
85
87
|
class="id-input"
|
|
86
88
|
class:id-input-error={error !== null}
|
|
@@ -88,7 +90,10 @@
|
|
|
88
90
|
maxlength={opts.maxLength}
|
|
89
91
|
autocomplete="off"
|
|
90
92
|
spellcheck="false"
|
|
93
|
+
oninput={() => { if (error) error = null; }}
|
|
91
94
|
/>
|
|
95
|
+
|
|
96
|
+
<!-- Error / char count -->
|
|
92
97
|
{#if error !== null || opts.maxLength}
|
|
93
98
|
<div class="id-field-footer">
|
|
94
99
|
{#if error !== null}
|
|
@@ -127,10 +132,15 @@
|
|
|
127
132
|
{/if}
|
|
128
133
|
|
|
129
134
|
<style>
|
|
135
|
+
/* Theme tokens (jatui-is2ow.3): every colour derives from var(--color-*) —
|
|
136
|
+
relative-color `oklch(from var(--x) l c h / a)` keeps the token's hue/chroma
|
|
137
|
+
and only swaps alpha, so light and dark themes both render correctly.
|
|
138
|
+
--n-00 (pure black) is a jat-IDE neutral; the inline fallback keeps
|
|
139
|
+
standalone consumers (no app.css) rendering the same value. */
|
|
130
140
|
.id-scrim {
|
|
131
141
|
position: fixed;
|
|
132
142
|
inset: 0;
|
|
133
|
-
background: oklch(0 0 0 / 0.45);
|
|
143
|
+
background: oklch(from var(--n-00, oklch(0 0 0)) l c h / 0.45);
|
|
134
144
|
z-index: 9991;
|
|
135
145
|
backdrop-filter: blur(2px);
|
|
136
146
|
}
|
|
@@ -151,12 +161,12 @@
|
|
|
151
161
|
flex-direction: column;
|
|
152
162
|
gap: 0.625rem;
|
|
153
163
|
padding: 1rem 1.125rem 0.625rem;
|
|
154
|
-
background: oklch(
|
|
155
|
-
border: 1px solid
|
|
164
|
+
background: oklch(from var(--color-base-200) l c h / 98%);
|
|
165
|
+
border: 1px solid var(--color-base-100);
|
|
156
166
|
border-radius: 0.875rem;
|
|
157
167
|
box-shadow:
|
|
158
|
-
0 20px 60px oklch(0 0 0 / 0.55),
|
|
159
|
-
0 0 0 1px oklch(
|
|
168
|
+
0 20px 60px oklch(from var(--n-00, oklch(0 0 0)) l c h / 0.55),
|
|
169
|
+
0 0 0 1px oklch(from var(--color-base-100) l c h / 50%);
|
|
160
170
|
backdrop-filter: blur(16px);
|
|
161
171
|
}
|
|
162
172
|
|
|
@@ -174,9 +184,9 @@
|
|
|
174
184
|
display: flex;
|
|
175
185
|
align-items: center;
|
|
176
186
|
justify-content: center;
|
|
177
|
-
background:
|
|
178
|
-
border: 1px solid
|
|
179
|
-
color:
|
|
187
|
+
background: color-mix(in oklch, var(--color-info) 20%, transparent);
|
|
188
|
+
border: 1px solid color-mix(in oklch, var(--color-info) 30%, transparent);
|
|
189
|
+
color: var(--color-info);
|
|
180
190
|
}
|
|
181
191
|
|
|
182
192
|
.id-icon svg {
|
|
@@ -196,14 +206,14 @@
|
|
|
196
206
|
margin: 0;
|
|
197
207
|
font-size: 0.9375rem;
|
|
198
208
|
font-weight: 600;
|
|
199
|
-
color:
|
|
209
|
+
color: var(--color-base-content);
|
|
200
210
|
line-height: 1.3;
|
|
201
211
|
}
|
|
202
212
|
|
|
203
213
|
.id-body {
|
|
204
214
|
margin: 0;
|
|
205
215
|
font-size: 0.8rem;
|
|
206
|
-
color: oklch(
|
|
216
|
+
color: oklch(from var(--color-base-content) l c h / 70%);
|
|
207
217
|
line-height: 1.5;
|
|
208
218
|
}
|
|
209
219
|
|
|
@@ -211,51 +221,52 @@
|
|
|
211
221
|
width: 100%;
|
|
212
222
|
padding: 0.4375rem 0.75rem;
|
|
213
223
|
font-size: 0.875rem;
|
|
214
|
-
background:
|
|
215
|
-
border: 1px solid
|
|
224
|
+
background: var(--color-base-300);
|
|
225
|
+
border: 1px solid var(--color-base-100);
|
|
216
226
|
border-radius: 0.5rem;
|
|
217
|
-
color:
|
|
227
|
+
color: var(--color-base-content);
|
|
218
228
|
outline: none;
|
|
219
229
|
transition: border-color 0.1s ease;
|
|
220
230
|
box-sizing: border-box;
|
|
221
231
|
}
|
|
222
232
|
|
|
223
233
|
.id-input::placeholder {
|
|
224
|
-
color: oklch(
|
|
234
|
+
color: oklch(from var(--color-base-content) l c h / 50%);
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
.id-input:focus {
|
|
228
|
-
border-color:
|
|
229
|
-
box-shadow: 0 0 0 2px oklch(
|
|
238
|
+
border-color: var(--color-primary);
|
|
239
|
+
box-shadow: 0 0 0 2px oklch(from var(--color-primary) l c h / 20%);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.id-input-error {
|
|
243
|
+
border-color: var(--color-error);
|
|
230
244
|
}
|
|
231
245
|
|
|
232
|
-
.id-input-error,
|
|
233
246
|
.id-input-error:focus {
|
|
234
|
-
border-color:
|
|
235
|
-
box-shadow: 0 0 0 2px oklch(
|
|
247
|
+
border-color: var(--color-error);
|
|
248
|
+
box-shadow: 0 0 0 2px oklch(from var(--color-error) l c h / 20%);
|
|
236
249
|
}
|
|
237
250
|
|
|
238
251
|
.id-field-footer {
|
|
239
252
|
display: flex;
|
|
240
|
-
align-items: center;
|
|
241
253
|
justify-content: space-between;
|
|
254
|
+
align-items: flex-start;
|
|
242
255
|
gap: 0.5rem;
|
|
243
256
|
margin-top: -0.125rem;
|
|
244
|
-
min-height: 1rem;
|
|
245
257
|
}
|
|
246
258
|
|
|
247
259
|
.id-error {
|
|
248
260
|
margin: 0;
|
|
249
261
|
font-size: 0.75rem;
|
|
250
|
-
color:
|
|
262
|
+
color: var(--color-error);
|
|
251
263
|
line-height: 1.4;
|
|
252
264
|
}
|
|
253
265
|
|
|
254
266
|
.id-char-count {
|
|
255
267
|
margin-left: auto;
|
|
256
268
|
font-size: 0.75rem;
|
|
257
|
-
color: oklch(
|
|
258
|
-
font-variant-numeric: tabular-nums;
|
|
269
|
+
color: oklch(from var(--color-base-content) l c h / 55%);
|
|
259
270
|
white-space: nowrap;
|
|
260
271
|
flex-shrink: 0;
|
|
261
272
|
}
|
|
@@ -277,34 +288,34 @@
|
|
|
277
288
|
border-radius: 0.5rem;
|
|
278
289
|
border: 1px solid transparent;
|
|
279
290
|
background: transparent;
|
|
280
|
-
color: oklch(
|
|
291
|
+
color: oklch(from var(--color-base-content) l c h / 95%);
|
|
281
292
|
cursor: pointer;
|
|
282
293
|
transition: background 0.1s ease, border-color 0.1s ease, color 0.1s ease;
|
|
283
294
|
white-space: nowrap;
|
|
284
295
|
}
|
|
285
296
|
|
|
286
297
|
.id-btn:hover {
|
|
287
|
-
background:
|
|
288
|
-
border-color:
|
|
298
|
+
background: var(--color-base-100);
|
|
299
|
+
border-color: var(--color-base-100);
|
|
289
300
|
}
|
|
290
301
|
|
|
291
302
|
.id-btn-cancel:focus-visible {
|
|
292
|
-
outline: 2px solid
|
|
303
|
+
outline: 2px solid var(--color-primary);
|
|
293
304
|
outline-offset: 2px;
|
|
294
305
|
}
|
|
295
306
|
|
|
296
307
|
.id-btn-confirm {
|
|
297
|
-
background: oklch(
|
|
298
|
-
border-color: oklch(
|
|
308
|
+
background: oklch(from var(--color-base-100) l c h / 60%);
|
|
309
|
+
border-color: oklch(from var(--color-base-content) l c h / 25%);
|
|
299
310
|
}
|
|
300
311
|
|
|
301
312
|
.id-btn-confirm:hover {
|
|
302
|
-
background: oklch(
|
|
303
|
-
border-color: oklch(
|
|
313
|
+
background: color-mix(in oklch, var(--color-primary) 20%, var(--color-base-300));
|
|
314
|
+
border-color: color-mix(in oklch, var(--color-primary) 40%, transparent);
|
|
304
315
|
}
|
|
305
316
|
|
|
306
317
|
.id-btn-confirm:focus-visible {
|
|
307
|
-
outline: 2px solid
|
|
318
|
+
outline: 2px solid var(--color-primary);
|
|
308
319
|
outline-offset: 2px;
|
|
309
320
|
}
|
|
310
321
|
|
|
@@ -312,11 +323,11 @@
|
|
|
312
323
|
font-family: ui-monospace, monospace;
|
|
313
324
|
font-size: 0.65rem;
|
|
314
325
|
padding: 0.0625rem 0.3125rem;
|
|
315
|
-
background:
|
|
316
|
-
border: 1px solid
|
|
326
|
+
background: var(--color-base-100);
|
|
327
|
+
border: 1px solid var(--color-base-100);
|
|
317
328
|
border-bottom-width: 2px;
|
|
318
329
|
border-radius: 0.25rem;
|
|
319
|
-
color: oklch(
|
|
330
|
+
color: oklch(from var(--color-base-content) l c h / 85%);
|
|
320
331
|
line-height: 1.6;
|
|
321
332
|
}
|
|
322
333
|
|
|
@@ -15,7 +15,33 @@
|
|
|
15
15
|
* { key: 'k / ↑', description: 'Focus previous item' },
|
|
16
16
|
* { key: 'Enter', description: 'Open detail' },
|
|
17
17
|
* ]} />
|
|
18
|
+
*
|
|
19
|
+
* Or feed it from a shortcut registry (createShortcutRegistry) — the
|
|
20
|
+
* overlay lists whatever can fire right now, grouped by context, and
|
|
21
|
+
* pushes a modal context while open so page shortcuts don't fire behind it:
|
|
22
|
+
*
|
|
23
|
+
* <KeyboardShortcutsOverlay {registry} />
|
|
24
|
+
*
|
|
25
|
+
* Precedence: `sections` > `registry` > `shortcuts`. The overlay owns `?` —
|
|
26
|
+
* don't register it in the registry too.
|
|
27
|
+
*
|
|
28
|
+
* App-specific sections (marks, list motions, cockpit panels…) that the
|
|
29
|
+
* overlay itself doesn't know about come in via `extraSections` — they
|
|
30
|
+
* render after the primary content, before the meta rows, in EVERY mode:
|
|
31
|
+
*
|
|
32
|
+
* <KeyboardShortcutsOverlay {registry} extraSections={[
|
|
33
|
+
* { title: 'Marks & jumps (Vim)', shortcuts: [{ key: "m{a-z}", description: 'Drop a mark' }] },
|
|
34
|
+
* ]} />
|
|
35
|
+
*
|
|
36
|
+
* For content the section renderer can't express, an `extra` snippet is
|
|
37
|
+
* rendered last (just above the meta rows):
|
|
38
|
+
*
|
|
39
|
+
* <KeyboardShortcutsOverlay {registry}>
|
|
40
|
+
* {#snippet extra()}<div>…custom footer…</div>{/snippet}
|
|
41
|
+
* </KeyboardShortcutsOverlay>
|
|
18
42
|
*/
|
|
43
|
+
import type { Snippet } from 'svelte';
|
|
44
|
+
import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
|
|
19
45
|
|
|
20
46
|
export interface KeyboardShortcut {
|
|
21
47
|
key: string;
|
|
@@ -37,6 +63,9 @@
|
|
|
37
63
|
let {
|
|
38
64
|
shortcuts = [],
|
|
39
65
|
sections,
|
|
66
|
+
registry,
|
|
67
|
+
extraSections,
|
|
68
|
+
extra,
|
|
40
69
|
title = 'Keyboard Shortcuts',
|
|
41
70
|
open = $bindable(false)
|
|
42
71
|
}: {
|
|
@@ -47,6 +76,25 @@
|
|
|
47
76
|
* precedence over the flat `shortcuts` prop.
|
|
48
77
|
*/
|
|
49
78
|
sections?: ShortcutSection[];
|
|
79
|
+
/**
|
|
80
|
+
* Shortcut registry to read sections from (active contexts, top of
|
|
81
|
+
* the stack first). Ignored when `sections` is given.
|
|
82
|
+
*/
|
|
83
|
+
registry?: ShortcutRegistry;
|
|
84
|
+
/**
|
|
85
|
+
* Extra app-provided sections (e.g. marks, list motions, cockpit
|
|
86
|
+
* panels) appended after the primary content and before the meta
|
|
87
|
+
* rows, regardless of whether the primary content came from
|
|
88
|
+
* `sections`, `registry` or the flat `shortcuts` list. Same shape
|
|
89
|
+
* as `sections`, with per-key `color` tinting supported.
|
|
90
|
+
*/
|
|
91
|
+
extraSections?: ShortcutSection[];
|
|
92
|
+
/**
|
|
93
|
+
* Fully custom footer content, rendered after `extraSections` and
|
|
94
|
+
* just above the built-in meta rows. Use when the section renderer
|
|
95
|
+
* (title + key/description rows) can't express the markup.
|
|
96
|
+
*/
|
|
97
|
+
extra?: Snippet;
|
|
50
98
|
title?: string;
|
|
51
99
|
open?: boolean;
|
|
52
100
|
} = $props();
|
|
@@ -60,6 +108,19 @@
|
|
|
60
108
|
open = isOpen;
|
|
61
109
|
});
|
|
62
110
|
|
|
111
|
+
// Registry sections are snapshotted on open — BEFORE pushing the overlay's
|
|
112
|
+
// own modal context, which would otherwise hide every page context.
|
|
113
|
+
let registrySections = $state<ShortcutSection[] | null>(null);
|
|
114
|
+
$effect(() => {
|
|
115
|
+
if (!isOpen || !registry) return;
|
|
116
|
+
registrySections = registry.toSections();
|
|
117
|
+
return registry.pushContext('keyboard-shortcuts-overlay', { modal: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const shownSections = $derived(
|
|
121
|
+
sections && sections.length > 0 ? sections : registry ? registrySections : null
|
|
122
|
+
);
|
|
123
|
+
|
|
63
124
|
// Track which key label is currently depressed
|
|
64
125
|
let depressedKey = $state<string | null>(null);
|
|
65
126
|
let depressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -104,7 +165,15 @@
|
|
|
104
165
|
if (!pressed) return false;
|
|
105
166
|
// Match single-char labels directly
|
|
106
167
|
const parts = keyLabel.split(/\s*\/\s*/);
|
|
107
|
-
|
|
168
|
+
const p = pressed.toLowerCase();
|
|
169
|
+
return parts.some((part) => {
|
|
170
|
+
const t = part.trim();
|
|
171
|
+
if (t === pressed || t.toLowerCase() === p) return true;
|
|
172
|
+
// Combo labels ("Ctrl+K", "⇧⌘K") — match on the key itself.
|
|
173
|
+
const base = (t.length > 1 && t.includes('+') ? t.slice(t.lastIndexOf('+', t.length - 2) + 1) : t)
|
|
174
|
+
.replace(/^[⌃⌥⇧⌘]+/, '');
|
|
175
|
+
return base !== t && base.toLowerCase() === p;
|
|
176
|
+
});
|
|
108
177
|
}
|
|
109
178
|
|
|
110
179
|
$effect(() => {
|
|
@@ -143,8 +212,8 @@
|
|
|
143
212
|
</button>
|
|
144
213
|
</div>
|
|
145
214
|
<div class="kso-list">
|
|
146
|
-
{#if
|
|
147
|
-
{#each
|
|
215
|
+
{#if shownSections && shownSections.length > 0}
|
|
216
|
+
{#each shownSections as section, sIdx (section.title + '::' + sIdx)}
|
|
148
217
|
<div class="kso-section-title">{section.title}</div>
|
|
149
218
|
{#each section.shortcuts as { key, description, color }, i (section.title + '::' + key + '::' + i)}
|
|
150
219
|
<div class="kso-row">
|
|
@@ -172,6 +241,28 @@
|
|
|
172
241
|
</div>
|
|
173
242
|
{/each}
|
|
174
243
|
{/if}
|
|
244
|
+
{#if extraSections}
|
|
245
|
+
{#each extraSections as section, sIdx (section.title + '::extra::' + sIdx)}
|
|
246
|
+
<div class="kso-section-title">{section.title}</div>
|
|
247
|
+
{#each section.shortcuts as { key, description, color }, i (section.title + '::extra::' + key + '::' + i)}
|
|
248
|
+
<div class="kso-row">
|
|
249
|
+
<kbd
|
|
250
|
+
class="kso-key"
|
|
251
|
+
class:kso-key-pressed={keyMatchesPressed(key, depressedKey)}
|
|
252
|
+
style={color
|
|
253
|
+
? `color:${color};border-color:color-mix(in oklch,${color} 50%,oklch(0.35 0.02 250));background:color-mix(in oklch,${color} 12%,oklch(0.25 0.02 250))`
|
|
254
|
+
: ''}
|
|
255
|
+
>
|
|
256
|
+
{key}
|
|
257
|
+
</kbd>
|
|
258
|
+
<span class="kso-description">{description}</span>
|
|
259
|
+
</div>
|
|
260
|
+
{/each}
|
|
261
|
+
{/each}
|
|
262
|
+
{/if}
|
|
263
|
+
{#if extra}
|
|
264
|
+
{@render extra()}
|
|
265
|
+
{/if}
|
|
175
266
|
<div class="kso-row kso-row-meta">
|
|
176
267
|
<kbd class="kso-key" class:kso-key-pressed={keyMatchesPressed('?', depressedKey)}>?</kbd>
|
|
177
268
|
<span class="kso-description">Toggle this overlay</span>
|
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
* KeyboardShortcutsOverlay — press `?` to toggle a modal listing the
|
|
3
|
+
* keyboard shortcuts available on the current page. Press `?` again or
|
|
4
|
+
* `Escape` to dismiss. Input/contenteditable fields and Ctrl/Meta/Alt
|
|
5
|
+
* combos are ignored so the shortcut never interferes with typing.
|
|
6
|
+
*
|
|
7
|
+
* While open, physical keydown events depress the matching on-screen keycap
|
|
8
|
+
* with a brief translateY(1px) + tint (var(--motion-instant)).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* <KeyboardShortcutsOverlay shortcuts={[
|
|
13
|
+
* { key: 'j / ↓', description: 'Focus next item' },
|
|
14
|
+
* { key: 'k / ↑', description: 'Focus previous item' },
|
|
15
|
+
* { key: 'Enter', description: 'Open detail' },
|
|
16
|
+
* ]} />
|
|
17
|
+
*
|
|
18
|
+
* Or feed it from a shortcut registry (createShortcutRegistry) — the
|
|
19
|
+
* overlay lists whatever can fire right now, grouped by context, and
|
|
20
|
+
* pushes a modal context while open so page shortcuts don't fire behind it:
|
|
21
|
+
*
|
|
22
|
+
* <KeyboardShortcutsOverlay {registry} />
|
|
23
|
+
*
|
|
24
|
+
* Precedence: `sections` > `registry` > `shortcuts`. The overlay owns `?` —
|
|
25
|
+
* don't register it in the registry too.
|
|
26
|
+
*
|
|
27
|
+
* App-specific sections (marks, list motions, cockpit panels…) that the
|
|
28
|
+
* overlay itself doesn't know about come in via `extraSections` — they
|
|
29
|
+
* render after the primary content, before the meta rows, in EVERY mode:
|
|
30
|
+
*
|
|
31
|
+
* <KeyboardShortcutsOverlay {registry} extraSections={[
|
|
32
|
+
* { title: 'Marks & jumps (Vim)', shortcuts: [{ key: "m{a-z}", description: 'Drop a mark' }] },
|
|
33
|
+
* ]} />
|
|
34
|
+
*
|
|
35
|
+
* For content the section renderer can't express, an `extra` snippet is
|
|
36
|
+
* rendered last (just above the meta rows):
|
|
37
|
+
*
|
|
38
|
+
* <KeyboardShortcutsOverlay {registry}>
|
|
39
|
+
* {#snippet extra()}<div>…custom footer…</div>{/snippet}
|
|
40
|
+
* </KeyboardShortcutsOverlay>
|
|
41
|
+
*/
|
|
42
|
+
import type { Snippet } from 'svelte';
|
|
43
|
+
import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
|
|
18
44
|
export interface KeyboardShortcut {
|
|
19
45
|
key: string;
|
|
20
46
|
description: string;
|
|
@@ -37,6 +63,25 @@ type $$ComponentProps = {
|
|
|
37
63
|
* precedence over the flat `shortcuts` prop.
|
|
38
64
|
*/
|
|
39
65
|
sections?: ShortcutSection[];
|
|
66
|
+
/**
|
|
67
|
+
* Shortcut registry to read sections from (active contexts, top of
|
|
68
|
+
* the stack first). Ignored when `sections` is given.
|
|
69
|
+
*/
|
|
70
|
+
registry?: ShortcutRegistry;
|
|
71
|
+
/**
|
|
72
|
+
* Extra app-provided sections (e.g. marks, list motions, cockpit
|
|
73
|
+
* panels) appended after the primary content and before the meta
|
|
74
|
+
* rows, regardless of whether the primary content came from
|
|
75
|
+
* `sections`, `registry` or the flat `shortcuts` list. Same shape
|
|
76
|
+
* as `sections`, with per-key `color` tinting supported.
|
|
77
|
+
*/
|
|
78
|
+
extraSections?: ShortcutSection[];
|
|
79
|
+
/**
|
|
80
|
+
* Fully custom footer content, rendered after `extraSections` and
|
|
81
|
+
* just above the built-in meta rows. Use when the section renderer
|
|
82
|
+
* (title + key/description rows) can't express the markup.
|
|
83
|
+
*/
|
|
84
|
+
extra?: Snippet;
|
|
40
85
|
title?: string;
|
|
41
86
|
open?: boolean;
|
|
42
87
|
};
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
| 'trash'
|
|
22
22
|
| 'plus'
|
|
23
23
|
| 'type'
|
|
24
|
+
| 'pause'
|
|
25
|
+
| 'play'
|
|
24
26
|
size?: number
|
|
25
27
|
class?: string
|
|
26
28
|
} = $props()
|
|
@@ -83,6 +85,27 @@
|
|
|
83
85
|
>
|
|
84
86
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
|
85
87
|
</svg>
|
|
88
|
+
{:else if name === 'pause'}
|
|
89
|
+
<svg
|
|
90
|
+
width={size}
|
|
91
|
+
height={size}
|
|
92
|
+
viewBox="0 0 24 24"
|
|
93
|
+
fill="currentColor"
|
|
94
|
+
class={className}
|
|
95
|
+
>
|
|
96
|
+
<rect x="6" y="4" width="4" height="16" rx="1" />
|
|
97
|
+
<rect x="14" y="4" width="4" height="16" rx="1" />
|
|
98
|
+
</svg>
|
|
99
|
+
{:else if name === 'play'}
|
|
100
|
+
<svg
|
|
101
|
+
width={size}
|
|
102
|
+
height={size}
|
|
103
|
+
viewBox="0 0 24 24"
|
|
104
|
+
fill="currentColor"
|
|
105
|
+
class={className}
|
|
106
|
+
>
|
|
107
|
+
<path d="M8 5.14v13.72a1 1 0 0 0 1.5.87l11-6.86a1 1 0 0 0 0-1.74l-11-6.86a1 1 0 0 0-1.5.87Z" />
|
|
108
|
+
</svg>
|
|
86
109
|
{:else if name === 'loader'}
|
|
87
110
|
<svg
|
|
88
111
|
width={size}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type $$ComponentProps = {
|
|
2
|
-
name: 'mic' | 'video' | 'monitor' | 'square' | 'loader' | 'x' | 'file' | 'trash' | 'plus' | 'type';
|
|
2
|
+
name: 'mic' | 'video' | 'monitor' | 'square' | 'loader' | 'x' | 'file' | 'trash' | 'plus' | 'type' | 'pause' | 'play';
|
|
3
3
|
size?: number;
|
|
4
4
|
class?: string;
|
|
5
5
|
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic thumbnail item — the decoupled chimaro shape. Unlike chimaro's
|
|
6
|
+
* UnifiedMediaItem this carries only what a thumbnail needs; callers with
|
|
7
|
+
* richer records pass a projection ({ id, url, alt }) or render a `cell`
|
|
8
|
+
* snippet for full control.
|
|
9
|
+
*/
|
|
10
|
+
export interface ThumbnailItem {
|
|
11
|
+
id: string;
|
|
12
|
+
/** Image URL; when absent the alt text renders as a text tile. */
|
|
13
|
+
url?: string | null;
|
|
14
|
+
/** Alt text / display label. */
|
|
15
|
+
alt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
items: ThumbnailItem[];
|
|
20
|
+
/** How many thumbnails to show before collapsing the rest into a +N counter. Default 8. */
|
|
21
|
+
max?: number;
|
|
22
|
+
/** Tile size. Default 'md'. */
|
|
23
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
24
|
+
/** Show remove (×) buttons on hover; requires `onRemove`. */
|
|
25
|
+
removable?: boolean;
|
|
26
|
+
/** Show the dashed add tile at the end; requires `onAdd`. */
|
|
27
|
+
addable?: boolean;
|
|
28
|
+
/** Tooltip / aria-label for the add tile. Default 'Add items'. */
|
|
29
|
+
addLabel?: string;
|
|
30
|
+
/** Fired when a thumbnail is clicked. */
|
|
31
|
+
onSelect?: (item: ThumbnailItem) => void;
|
|
32
|
+
/** Fired when a remove (×) button is clicked. */
|
|
33
|
+
onRemove?: (item: ThumbnailItem) => void;
|
|
34
|
+
/** Fired when the add tile is clicked. */
|
|
35
|
+
onAdd?: () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Replace the default image/text tile rendering for each item — e.g. a
|
|
38
|
+
* video poster, an initials avatar, a FileThumbnail. The snippet renders
|
|
39
|
+
* inside the clickable tile, so it should fill it (`h-full w-full`).
|
|
40
|
+
*/
|
|
41
|
+
cell?: Snippet<[ThumbnailItem]>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let {
|
|
45
|
+
items,
|
|
46
|
+
max = 8,
|
|
47
|
+
size = 'md',
|
|
48
|
+
removable = false,
|
|
49
|
+
addable = false,
|
|
50
|
+
addLabel = 'Add items',
|
|
51
|
+
onSelect,
|
|
52
|
+
onRemove,
|
|
53
|
+
onAdd,
|
|
54
|
+
cell
|
|
55
|
+
}: Props = $props();
|
|
56
|
+
|
|
57
|
+
const sizeClasses: Record<NonNullable<Props['size']>, string> = {
|
|
58
|
+
xs: 'h-8 w-8',
|
|
59
|
+
sm: 'h-12 w-12',
|
|
60
|
+
md: 'h-16 w-16',
|
|
61
|
+
lg: 'h-20 w-20'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const visible = $derived(items.slice(0, max));
|
|
65
|
+
const overflow = $derived(Math.max(0, items.length - max));
|
|
66
|
+
|
|
67
|
+
function shortLabel(alt: string): string {
|
|
68
|
+
return alt.length > 14 ? `${alt.slice(0, 14)}…` : alt;
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
73
|
+
{#each visible as item (item.id)}
|
|
74
|
+
<div class="group relative">
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
class="{sizeClasses[size]} cursor-pointer overflow-hidden rounded border transition-all duration-200
|
|
78
|
+
{removable ? 'border-2 border-accent' : 'border-base-300 hover:border-primary'}"
|
|
79
|
+
onclick={() => onSelect?.(item)}
|
|
80
|
+
title={item.alt}
|
|
81
|
+
aria-label={item.alt}
|
|
82
|
+
>
|
|
83
|
+
{#if cell}
|
|
84
|
+
{@render cell(item)}
|
|
85
|
+
{:else if item.url}
|
|
86
|
+
<img src={item.url} alt={item.alt} class="h-full w-full object-cover" loading="lazy" />
|
|
87
|
+
{:else}
|
|
88
|
+
<div class="flex h-full w-full items-center justify-center bg-base-200 p-1">
|
|
89
|
+
<span class="text-2xs text-center font-semibold leading-tight break-all text-base-content/80">
|
|
90
|
+
{shortLabel(item.alt)}
|
|
91
|
+
</span>
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</button>
|
|
95
|
+
|
|
96
|
+
{#if removable && onRemove}
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
class="absolute -top-1 -right-1 z-10 flex h-4 w-4 items-center justify-center rounded-full bg-error text-error-content opacity-0 transition-opacity group-hover:opacity-100"
|
|
100
|
+
onclick={(e) => {
|
|
101
|
+
e.stopPropagation();
|
|
102
|
+
onRemove(item);
|
|
103
|
+
}}
|
|
104
|
+
title="Remove"
|
|
105
|
+
aria-label="Remove {item.alt}"
|
|
106
|
+
>
|
|
107
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" class="h-2.5 w-2.5">
|
|
108
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
109
|
+
</svg>
|
|
110
|
+
</button>
|
|
111
|
+
{/if}
|
|
112
|
+
</div>
|
|
113
|
+
{/each}
|
|
114
|
+
|
|
115
|
+
{#if overflow > 0}
|
|
116
|
+
<div
|
|
117
|
+
class="{sizeClasses[size]} flex items-center justify-center rounded border border-base-300 bg-base-200"
|
|
118
|
+
title="{overflow} more item{overflow === 1 ? '' : 's'}"
|
|
119
|
+
>
|
|
120
|
+
<span class="text-xs opacity-70">+{overflow}</span>
|
|
121
|
+
</div>
|
|
122
|
+
{/if}
|
|
123
|
+
|
|
124
|
+
{#if addable && onAdd}
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
class="{sizeClasses[size]} flex scale-90 cursor-pointer items-center justify-center rounded border-2 border-dashed border-primary/30 bg-primary/5 transition-all duration-200 hover:border-primary/50 hover:bg-primary/10"
|
|
128
|
+
onclick={onAdd}
|
|
129
|
+
title={addLabel}
|
|
130
|
+
aria-label={addLabel}
|
|
131
|
+
>
|
|
132
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="h-4 w-4 text-primary/70">
|
|
133
|
+
<path d="M12 5v14M5 12h14" />
|
|
134
|
+
</svg>
|
|
135
|
+
</button>
|
|
136
|
+
{/if}
|
|
137
|
+
</div>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Generic thumbnail item — the decoupled chimaro shape. Unlike chimaro's
|
|
4
|
+
* UnifiedMediaItem this carries only what a thumbnail needs; callers with
|
|
5
|
+
* richer records pass a projection ({ id, url, alt }) or render a `cell`
|
|
6
|
+
* snippet for full control.
|
|
7
|
+
*/
|
|
8
|
+
export interface ThumbnailItem {
|
|
9
|
+
id: string;
|
|
10
|
+
/** Image URL; when absent the alt text renders as a text tile. */
|
|
11
|
+
url?: string | null;
|
|
12
|
+
/** Alt text / display label. */
|
|
13
|
+
alt: string;
|
|
14
|
+
}
|
|
15
|
+
interface Props {
|
|
16
|
+
items: ThumbnailItem[];
|
|
17
|
+
/** How many thumbnails to show before collapsing the rest into a +N counter. Default 8. */
|
|
18
|
+
max?: number;
|
|
19
|
+
/** Tile size. Default 'md'. */
|
|
20
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
21
|
+
/** Show remove (×) buttons on hover; requires `onRemove`. */
|
|
22
|
+
removable?: boolean;
|
|
23
|
+
/** Show the dashed add tile at the end; requires `onAdd`. */
|
|
24
|
+
addable?: boolean;
|
|
25
|
+
/** Tooltip / aria-label for the add tile. Default 'Add items'. */
|
|
26
|
+
addLabel?: string;
|
|
27
|
+
/** Fired when a thumbnail is clicked. */
|
|
28
|
+
onSelect?: (item: ThumbnailItem) => void;
|
|
29
|
+
/** Fired when a remove (×) button is clicked. */
|
|
30
|
+
onRemove?: (item: ThumbnailItem) => void;
|
|
31
|
+
/** Fired when the add tile is clicked. */
|
|
32
|
+
onAdd?: () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Replace the default image/text tile rendering for each item — e.g. a
|
|
35
|
+
* video poster, an initials avatar, a FileThumbnail. The snippet renders
|
|
36
|
+
* inside the clickable tile, so it should fill it (`h-full w-full`).
|
|
37
|
+
*/
|
|
38
|
+
cell?: Snippet<[ThumbnailItem]>;
|
|
39
|
+
}
|
|
40
|
+
declare const ThumbnailGrid: import("svelte").Component<Props, {}, "">;
|
|
41
|
+
type ThumbnailGrid = ReturnType<typeof ThumbnailGrid>;
|
|
42
|
+
export default ThumbnailGrid;
|