@enigmax/primitives 0.22.0 → 0.23.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/chunk-3BQVOOAM.js +388 -0
- package/dist/{chunk-QQFNAKMY.js → chunk-45UHLZYT.js} +1 -1
- package/dist/chunk-4VUHQFAT.js +130 -0
- package/dist/{chunk-D5A2ZMAG.js → chunk-4ZPMP47J.js} +6 -1
- package/dist/chunk-DSBYVA7V.js +713 -0
- package/dist/{chunk-FWVWX67R.js → chunk-FGBIZDV2.js} +3 -3
- package/dist/{chunk-R4ZAEE7V.js → chunk-KEVZ5XQV.js} +1 -1
- package/dist/chunk-N6PDHMAX.js +213 -0
- package/dist/chunk-NONGREXC.js +248 -0
- package/dist/chunk-VKL3DEIQ.js +804 -0
- package/dist/chunk-WSQC3PCC.js +466 -0
- package/dist/context-menu-D3FtTn7v.d.ts +174 -0
- package/dist/{index-BHpOZncw.d.ts → index-DQNnohoo.d.ts} +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +8 -4
- package/dist/keys-D2zJs1uB.d.ts +100 -0
- package/dist/next/index.d.ts +9 -2
- package/dist/next/index.js +20 -13
- package/dist/react/context-menu.d.ts +202 -0
- package/dist/react/context-menu.js +6 -0
- package/dist/react/index.d.ts +10 -3
- package/dist/react/index.js +19 -12
- package/dist/react/input.d.ts +2 -2
- package/dist/react/input.js +1 -1
- package/dist/react/palette.d.ts +1 -1
- package/dist/react/palette.js +3 -3
- package/dist/react/search.d.ts +1 -1
- package/dist/react/search.js +2 -2
- package/dist/react/select.d.ts +217 -0
- package/dist/react/select.js +7 -0
- package/dist/react/selection.d.ts +104 -0
- package/dist/react/selection.js +4 -0
- package/dist/react-router/index.d.ts +9 -2
- package/dist/react-router/index.js +20 -13
- package/dist/search/index.d.ts +2 -2
- package/dist/search/index.js +1 -1
- package/dist/{search-BD9-5O5U.d.ts → search-DYgqRp37.d.ts} +13 -1
- package/dist/{search-UQEXAPQB.js → search-PBZORZ7P.js} +1 -1
- package/dist/select-ClSy-J1f.d.ts +100 -0
- package/dist/selection-B_pmzHpy.d.ts +150 -0
- package/package.json +21 -2
- package/recipes/context-menu/styles.css +177 -0
- package/recipes/input/styles.css +9 -0
- package/recipes/palette/styles.css +3 -0
- package/recipes/search/tailwind.tsx +3 -2
- package/recipes/select/styles.css +230 -0
- package/recipes/toast/styles.css +1 -1
- package/registry.json +358 -1
- package/src/core/context-menu.ts +694 -0
- package/src/core/keys.ts +264 -0
- package/src/core/search.ts +19 -0
- package/src/core/select.ts +404 -0
- package/src/core/selection.ts +648 -0
- package/src/index.ts +60 -1
- package/src/react/context-menu/context.ts +57 -0
- package/src/react/context-menu/index.tsx +94 -0
- package/src/react/context-menu/root.tsx +846 -0
- package/src/react/context-menu/styles.ts +186 -0
- package/src/react/index.ts +68 -1
- package/src/react/palette/root.tsx +2 -2
- package/src/react/select/context.ts +56 -0
- package/src/react/select/index.tsx +96 -0
- package/src/react/select/root.tsx +839 -0
- package/src/react/select/styles.ts +238 -0
- package/src/react/selection/index.tsx +115 -0
- package/src/react/selection/use-selection.ts +260 -0
- /package/dist/{chunk-U3V4EHOB.js → chunk-3HDEZ2E7.js} +0 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { typeaheadStep } from './chunk-4VUHQFAT.js';
|
|
2
|
+
import { createSearch } from './chunk-4ZPMP47J.js';
|
|
3
|
+
|
|
4
|
+
// src/core/context-menu.ts
|
|
5
|
+
function isAction(entry) {
|
|
6
|
+
return (entry.type ?? "item") === "item";
|
|
7
|
+
}
|
|
8
|
+
var CONTEXT_MENU_SEARCH_KEYS = ["label", "description", "group", "keywords"];
|
|
9
|
+
var SEARCHABLE_FROM = 8;
|
|
10
|
+
var PAGE = 5;
|
|
11
|
+
var DEFAULT_CACHE_MS = 5 * 60 * 1e3;
|
|
12
|
+
function fold(value) {
|
|
13
|
+
return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
function createContextMenu(options = {}) {
|
|
16
|
+
let opts = { ...options };
|
|
17
|
+
let root = opts.items ?? [];
|
|
18
|
+
let open = false;
|
|
19
|
+
let point = null;
|
|
20
|
+
let levels = [];
|
|
21
|
+
let typed = "";
|
|
22
|
+
let typedAt = 0;
|
|
23
|
+
let destroyed = false;
|
|
24
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
25
|
+
const cache = /* @__PURE__ */ new Map();
|
|
26
|
+
const wantsFocus = /* @__PURE__ */ new Set();
|
|
27
|
+
const engines = /* @__PURE__ */ new Map();
|
|
28
|
+
function cacheMs() {
|
|
29
|
+
return opts.cacheMs ?? DEFAULT_CACHE_MS;
|
|
30
|
+
}
|
|
31
|
+
function keyOf(path) {
|
|
32
|
+
return path.join("\0");
|
|
33
|
+
}
|
|
34
|
+
function searchableFor(item, entries) {
|
|
35
|
+
const asked = item?.searchable ?? "auto";
|
|
36
|
+
if (asked !== "auto") return asked;
|
|
37
|
+
return entries.filter(isAction).length >= SEARCHABLE_FROM;
|
|
38
|
+
}
|
|
39
|
+
function filtered(level) {
|
|
40
|
+
if (!level.searchable || !level.query.trim()) return [...level.entries];
|
|
41
|
+
const actions = level.entries.filter(isAction);
|
|
42
|
+
const matched = new Set(engineFor(level, actions).searchNow(level.query).map((match) => match.item));
|
|
43
|
+
return trimFurniture(level.entries.filter((entry) => !isAction(entry) || matched.has(entry)));
|
|
44
|
+
}
|
|
45
|
+
function engineFor(level, actions) {
|
|
46
|
+
const id = keyOf(level.path);
|
|
47
|
+
const held = engines.get(id);
|
|
48
|
+
if (held && held.items.length === actions.length && held.items.every((item, index) => item === actions[index])) return held.engine;
|
|
49
|
+
held?.engine.destroy();
|
|
50
|
+
const engine = createSearch({
|
|
51
|
+
items: actions,
|
|
52
|
+
keys: opts.searchKeys ?? CONTEXT_MENU_SEARCH_KEYS,
|
|
53
|
+
fuse: opts.fuse,
|
|
54
|
+
fuseOptions: opts.fuseOptions,
|
|
55
|
+
matcher: opts.matcher,
|
|
56
|
+
// A menu filters as you type: the rows are in memory, and a delay is felt as the
|
|
57
|
+
// panel lagging behind the field.
|
|
58
|
+
debounce: 0,
|
|
59
|
+
empty: "all"
|
|
60
|
+
});
|
|
61
|
+
engines.set(id, { items: actions, engine });
|
|
62
|
+
return engine;
|
|
63
|
+
}
|
|
64
|
+
function trimFurniture(entries) {
|
|
65
|
+
const result = [];
|
|
66
|
+
let separator = null;
|
|
67
|
+
let caption = null;
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (!isAction(entry)) {
|
|
70
|
+
if (entry.type === "separator") separator = entry;
|
|
71
|
+
else caption = entry;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (separator && result.length > 0) result.push(separator);
|
|
75
|
+
if (caption) result.push(caption);
|
|
76
|
+
separator = null;
|
|
77
|
+
caption = null;
|
|
78
|
+
result.push(entry);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function wrap(index, length) {
|
|
83
|
+
return (index % length + length) % length;
|
|
84
|
+
}
|
|
85
|
+
function enabledIndex(level, from, step) {
|
|
86
|
+
const length = level.visible.length;
|
|
87
|
+
if (length === 0) return -1;
|
|
88
|
+
for (let attempt = 0; attempt < length; attempt++) {
|
|
89
|
+
const index = wrap(from + step * attempt, length);
|
|
90
|
+
const entry = level.visible[index];
|
|
91
|
+
if (entry && isAction(entry) && !entry.disabled) return index;
|
|
92
|
+
}
|
|
93
|
+
return -1;
|
|
94
|
+
}
|
|
95
|
+
function makeLevel(path, item, entries, loading = false, error = null) {
|
|
96
|
+
const level = {
|
|
97
|
+
path,
|
|
98
|
+
title: (item ? item.title : opts.title) ?? null,
|
|
99
|
+
entries: [...entries],
|
|
100
|
+
visible: [],
|
|
101
|
+
active: -1,
|
|
102
|
+
query: "",
|
|
103
|
+
searchable: searchableFor(item, entries),
|
|
104
|
+
loading,
|
|
105
|
+
error
|
|
106
|
+
};
|
|
107
|
+
level.visible = filtered(level);
|
|
108
|
+
return level;
|
|
109
|
+
}
|
|
110
|
+
function snapshot() {
|
|
111
|
+
return {
|
|
112
|
+
open,
|
|
113
|
+
point,
|
|
114
|
+
// Copied one level deep: a subscriber that keeps a snapshot must not see the next
|
|
115
|
+
// state mutate the one it is holding.
|
|
116
|
+
levels: levels.map((level) => ({ ...level, path: [...level.path], entries: [...level.entries], visible: [...level.visible] }))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function emit() {
|
|
120
|
+
const state = snapshot();
|
|
121
|
+
opts.onChange?.(state);
|
|
122
|
+
for (const listener of listeners) listener(state);
|
|
123
|
+
}
|
|
124
|
+
function deepest() {
|
|
125
|
+
return levels[levels.length - 1];
|
|
126
|
+
}
|
|
127
|
+
function actionAt(level, index) {
|
|
128
|
+
const entry = level?.visible[index];
|
|
129
|
+
return entry && isAction(entry) ? entry : null;
|
|
130
|
+
}
|
|
131
|
+
function hasSubmenu(item) {
|
|
132
|
+
return Boolean(item.loadItems || item.items && item.items.some(isAction));
|
|
133
|
+
}
|
|
134
|
+
function hasActions(entries) {
|
|
135
|
+
return entries.some(isAction);
|
|
136
|
+
}
|
|
137
|
+
function pushSubmenu(parent, index, focus) {
|
|
138
|
+
const level = levels[parent];
|
|
139
|
+
const item = actionAt(level, index);
|
|
140
|
+
if (!item || item.disabled || !hasSubmenu(item)) return;
|
|
141
|
+
const path = [...level.path, item.id];
|
|
142
|
+
const id = keyOf(path);
|
|
143
|
+
const shown = levels[parent + 1];
|
|
144
|
+
if (shown && keyOf(shown.path) === id) {
|
|
145
|
+
level.active = index;
|
|
146
|
+
if (focus) {
|
|
147
|
+
wantsFocus.add(id);
|
|
148
|
+
if (shown.active < 0) focusFirst(parent + 1);
|
|
149
|
+
}
|
|
150
|
+
emit();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
levels = levels.slice(0, parent + 1);
|
|
154
|
+
level.active = index;
|
|
155
|
+
if (focus) wantsFocus.add(id);
|
|
156
|
+
else wantsFocus.delete(id);
|
|
157
|
+
if (item.items) {
|
|
158
|
+
levels.push(makeLevel(path, item, item.items));
|
|
159
|
+
if (focus) focusFirst(levels.length - 1);
|
|
160
|
+
emit();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const entry = cache.get(id);
|
|
164
|
+
const fresh = entry?.entries && (cacheMs() <= 0 ? false : Date.now() - entry.at < cacheMs());
|
|
165
|
+
if (fresh && entry?.entries) {
|
|
166
|
+
levels.push(makeLevel(path, item, entry.entries));
|
|
167
|
+
if (focus) focusFirst(levels.length - 1);
|
|
168
|
+
emit();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
levels.push(makeLevel(path, item, [], true));
|
|
172
|
+
emit();
|
|
173
|
+
const request = entry?.pending ?? Promise.resolve().then(() => item.loadItems());
|
|
174
|
+
cache.set(id, { at: entry?.at ?? Date.now(), entries: entry?.entries, pending: request });
|
|
175
|
+
request.then(
|
|
176
|
+
(entries) => {
|
|
177
|
+
cache.set(id, { at: Date.now(), entries });
|
|
178
|
+
settle(id, path, item, entries, null);
|
|
179
|
+
},
|
|
180
|
+
(reason) => {
|
|
181
|
+
cache.delete(id);
|
|
182
|
+
settle(id, path, item, [], reason instanceof Error ? reason : new Error(String(reason)));
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
function settle(id, path, item, entries, error) {
|
|
187
|
+
if (destroyed) return;
|
|
188
|
+
const index = levels.findIndex((level) => keyOf(level.path) === id);
|
|
189
|
+
if (index === -1) return;
|
|
190
|
+
levels = levels.slice(0, index);
|
|
191
|
+
levels.push(makeLevel(path, item, entries, false, error));
|
|
192
|
+
if (wantsFocus.has(id)) focusFirst(index);
|
|
193
|
+
emit();
|
|
194
|
+
}
|
|
195
|
+
function focusFirst(index) {
|
|
196
|
+
const level = levels[index];
|
|
197
|
+
if (!level) return;
|
|
198
|
+
level.active = enabledIndex(level, 0, 1);
|
|
199
|
+
}
|
|
200
|
+
function refilter(level) {
|
|
201
|
+
const current = level.active >= 0 ? level.visible[level.active] : void 0;
|
|
202
|
+
level.visible = filtered(level);
|
|
203
|
+
const kept = current ? level.visible.indexOf(current) : -1;
|
|
204
|
+
const entry = kept >= 0 ? level.visible[kept] : void 0;
|
|
205
|
+
level.active = entry && isAction(entry) && !entry.disabled ? kept : enabledIndex(level, 0, 1);
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
get state() {
|
|
209
|
+
return snapshot();
|
|
210
|
+
},
|
|
211
|
+
open(next) {
|
|
212
|
+
if (destroyed) return false;
|
|
213
|
+
if (!hasActions(root)) return false;
|
|
214
|
+
const wasOpen = open;
|
|
215
|
+
open = true;
|
|
216
|
+
point = { x: next.x, y: next.y };
|
|
217
|
+
levels = [makeLevel([], null, root)];
|
|
218
|
+
typed = "";
|
|
219
|
+
wantsFocus.clear();
|
|
220
|
+
if (!wasOpen) opts.onOpenChange?.(true);
|
|
221
|
+
emit();
|
|
222
|
+
return true;
|
|
223
|
+
},
|
|
224
|
+
close() {
|
|
225
|
+
if (destroyed || !open) return;
|
|
226
|
+
open = false;
|
|
227
|
+
point = null;
|
|
228
|
+
levels = [];
|
|
229
|
+
typed = "";
|
|
230
|
+
wantsFocus.clear();
|
|
231
|
+
opts.onOpenChange?.(false);
|
|
232
|
+
emit();
|
|
233
|
+
},
|
|
234
|
+
move(key) {
|
|
235
|
+
if (destroyed) return;
|
|
236
|
+
const level = deepest();
|
|
237
|
+
if (!level || level.visible.length === 0) return;
|
|
238
|
+
const from = level.active < 0 ? -1 : level.active;
|
|
239
|
+
switch (key) {
|
|
240
|
+
// Wrapping, because a menu is a short list and the row after the last one is
|
|
241
|
+
// the first: clamping leaves the arrow key doing nothing, which reads as a
|
|
242
|
+
// frozen panel.
|
|
243
|
+
case "ArrowDown":
|
|
244
|
+
level.active = enabledIndex(level, from + 1, 1);
|
|
245
|
+
break;
|
|
246
|
+
// From the LAST row when nothing is highlighted, which is how a menu opened at
|
|
247
|
+
// the pointer starts: `from - 1` there is the row before the last one.
|
|
248
|
+
case "ArrowUp":
|
|
249
|
+
level.active = enabledIndex(level, from < 0 ? level.visible.length - 1 : from - 1 + level.visible.length, -1);
|
|
250
|
+
break;
|
|
251
|
+
case "Home":
|
|
252
|
+
level.active = enabledIndex(level, 0, 1);
|
|
253
|
+
break;
|
|
254
|
+
case "End":
|
|
255
|
+
level.active = enabledIndex(level, level.visible.length - 1, -1);
|
|
256
|
+
break;
|
|
257
|
+
case "PageDown":
|
|
258
|
+
level.active = enabledIndex(level, Math.min(level.visible.length - 1, from + PAGE), 1);
|
|
259
|
+
break;
|
|
260
|
+
case "PageUp":
|
|
261
|
+
level.active = enabledIndex(level, Math.max(0, from - PAGE), -1);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
emit();
|
|
265
|
+
},
|
|
266
|
+
setActive(levelIndex, index) {
|
|
267
|
+
const level = levels[levelIndex];
|
|
268
|
+
if (destroyed || !level || level.active === index) return;
|
|
269
|
+
if (index < 0 || index >= level.visible.length) return;
|
|
270
|
+
const item = actionAt(level, index);
|
|
271
|
+
if (!item || item.disabled) return;
|
|
272
|
+
level.active = index;
|
|
273
|
+
emit();
|
|
274
|
+
},
|
|
275
|
+
setQuery(levelIndex, query) {
|
|
276
|
+
const level = levels[levelIndex];
|
|
277
|
+
if (destroyed || !level) return;
|
|
278
|
+
level.query = query;
|
|
279
|
+
levels = levels.slice(0, levelIndex + 1);
|
|
280
|
+
refilter(level);
|
|
281
|
+
emit();
|
|
282
|
+
},
|
|
283
|
+
openSubmenu(levelIndex, index, focus = false) {
|
|
284
|
+
if (destroyed) return;
|
|
285
|
+
pushSubmenu(levelIndex, index, focus);
|
|
286
|
+
},
|
|
287
|
+
closeBelow(levelIndex) {
|
|
288
|
+
if (destroyed || levels.length <= levelIndex + 1) return;
|
|
289
|
+
levels = levels.slice(0, levelIndex + 1);
|
|
290
|
+
emit();
|
|
291
|
+
},
|
|
292
|
+
select(levelIndex, index) {
|
|
293
|
+
if (destroyed) return;
|
|
294
|
+
const level = levels[levelIndex];
|
|
295
|
+
const item = actionAt(level, index);
|
|
296
|
+
if (!item || item.disabled) return;
|
|
297
|
+
if (hasSubmenu(item)) {
|
|
298
|
+
pushSubmenu(levelIndex, index, true);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const path = [...level?.path ?? [], item.id];
|
|
302
|
+
open = false;
|
|
303
|
+
point = null;
|
|
304
|
+
levels = [];
|
|
305
|
+
typed = "";
|
|
306
|
+
wantsFocus.clear();
|
|
307
|
+
opts.onOpenChange?.(false);
|
|
308
|
+
emit();
|
|
309
|
+
opts.onSelect?.(item, path);
|
|
310
|
+
},
|
|
311
|
+
selectActive() {
|
|
312
|
+
const index = levels.length - 1;
|
|
313
|
+
const level = levels[index];
|
|
314
|
+
if (level && level.active >= 0) this.select(index, level.active);
|
|
315
|
+
},
|
|
316
|
+
enterSubmenu() {
|
|
317
|
+
const index = levels.length - 1;
|
|
318
|
+
const level = levels[index];
|
|
319
|
+
if (!level || level.active < 0) return;
|
|
320
|
+
const item = actionAt(level, level.active);
|
|
321
|
+
if (item && hasSubmenu(item)) pushSubmenu(index, level.active, true);
|
|
322
|
+
},
|
|
323
|
+
leaveSubmenu() {
|
|
324
|
+
if (destroyed || levels.length <= 1) return;
|
|
325
|
+
levels = levels.slice(0, levels.length - 1);
|
|
326
|
+
emit();
|
|
327
|
+
},
|
|
328
|
+
typeahead(character) {
|
|
329
|
+
if (destroyed || character.length !== 1) return;
|
|
330
|
+
const level = deepest();
|
|
331
|
+
if (!level || level.visible.length === 0) return;
|
|
332
|
+
const step = typeaheadStep({ typed, at: typedAt }, character);
|
|
333
|
+
typed = step.typed;
|
|
334
|
+
typedAt = step.at;
|
|
335
|
+
const needle = fold(step.needle);
|
|
336
|
+
const from = step.cycle && level.active >= 0 ? 1 : 0;
|
|
337
|
+
for (let step2 = from; step2 < level.visible.length + from; step2++) {
|
|
338
|
+
const index = wrap(Math.max(level.active, 0) + step2, level.visible.length);
|
|
339
|
+
const entry = level.visible[index];
|
|
340
|
+
if (!entry || !isAction(entry) || entry.disabled) continue;
|
|
341
|
+
if (fold(entry.label).startsWith(needle)) {
|
|
342
|
+
level.active = index;
|
|
343
|
+
emit();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
invalidate(path) {
|
|
349
|
+
if (!path) {
|
|
350
|
+
cache.clear();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
cache.delete(keyOf(path));
|
|
354
|
+
},
|
|
355
|
+
update(next) {
|
|
356
|
+
if (destroyed) return;
|
|
357
|
+
const hadItems = next.items !== void 0;
|
|
358
|
+
opts = { ...opts, ...next };
|
|
359
|
+
if (hadItems) {
|
|
360
|
+
root = next.items ?? [];
|
|
361
|
+
const level = levels[0];
|
|
362
|
+
if (level) {
|
|
363
|
+
level.entries = [...root];
|
|
364
|
+
level.title = opts.title ?? null;
|
|
365
|
+
level.searchable = searchableFor(null, root);
|
|
366
|
+
refilter(level);
|
|
367
|
+
emit();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
subscribe(listener) {
|
|
372
|
+
listeners.add(listener);
|
|
373
|
+
return () => {
|
|
374
|
+
listeners.delete(listener);
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
destroy() {
|
|
378
|
+
destroyed = true;
|
|
379
|
+
listeners.clear();
|
|
380
|
+
cache.clear();
|
|
381
|
+
wantsFocus.clear();
|
|
382
|
+
for (const held of engines.values()) held.engine.destroy();
|
|
383
|
+
engines.clear();
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export { CONTEXT_MENU_SEARCH_KEYS, createContextMenu, isAction };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// src/core/keys.ts
|
|
2
|
+
var MODIFIERS = /* @__PURE__ */ new Set(["mod", "ctrl", "control", "meta", "cmd", "command", "super", "win", "shift", "alt", "option", "opt"]);
|
|
3
|
+
var NAMED = {
|
|
4
|
+
esc: "escape",
|
|
5
|
+
escape: "escape",
|
|
6
|
+
del: "delete",
|
|
7
|
+
delete: "delete",
|
|
8
|
+
back: "backspace",
|
|
9
|
+
backspace: "backspace",
|
|
10
|
+
enter: "enter",
|
|
11
|
+
return: "enter",
|
|
12
|
+
space: " ",
|
|
13
|
+
spacebar: " ",
|
|
14
|
+
tab: "tab",
|
|
15
|
+
up: "arrowup",
|
|
16
|
+
down: "arrowdown",
|
|
17
|
+
left: "arrowleft",
|
|
18
|
+
right: "arrowright",
|
|
19
|
+
arrowup: "arrowup",
|
|
20
|
+
arrowdown: "arrowdown",
|
|
21
|
+
arrowleft: "arrowleft",
|
|
22
|
+
arrowright: "arrowright",
|
|
23
|
+
home: "home",
|
|
24
|
+
end: "end",
|
|
25
|
+
pageup: "pageup",
|
|
26
|
+
pagedown: "pagedown",
|
|
27
|
+
plus: "+"
|
|
28
|
+
};
|
|
29
|
+
function isApplePlatform() {
|
|
30
|
+
if (typeof navigator === "undefined") return false;
|
|
31
|
+
const source = navigator.platform || navigator.userAgent || "";
|
|
32
|
+
return /mac|iphone|ipad|ipod/i.test(source);
|
|
33
|
+
}
|
|
34
|
+
function parseShortcut(spec) {
|
|
35
|
+
if (typeof spec !== "string") return { ...spec, key: normalizeKey(spec.key) };
|
|
36
|
+
const shortcut = { key: "" };
|
|
37
|
+
const parts = spec.split(/[+-](?!$)/).map((part) => part.trim()).filter(Boolean);
|
|
38
|
+
if (parts.length === 0) return { key: normalizeKey(spec.trim()) };
|
|
39
|
+
parts.forEach((part, index) => {
|
|
40
|
+
const word = part.toLowerCase();
|
|
41
|
+
const last = index === parts.length - 1;
|
|
42
|
+
if (!last && MODIFIERS.has(word)) {
|
|
43
|
+
applyModifier(shortcut, word);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (last && MODIFIERS.has(word) && parts.length > 1) {
|
|
47
|
+
applyModifier(shortcut, word);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
shortcut.key = normalizeKey(part);
|
|
51
|
+
});
|
|
52
|
+
if (!shortcut.key) shortcut.key = normalizeKey(parts[parts.length - 1]);
|
|
53
|
+
return shortcut;
|
|
54
|
+
}
|
|
55
|
+
function applyModifier(shortcut, word) {
|
|
56
|
+
if (word === "mod") shortcut.mod = true;
|
|
57
|
+
else if (word === "ctrl" || word === "control") shortcut.ctrl = true;
|
|
58
|
+
else if (word === "meta" || word === "cmd" || word === "command" || word === "super" || word === "win") shortcut.meta = true;
|
|
59
|
+
else if (word === "shift") shortcut.shift = true;
|
|
60
|
+
else shortcut.alt = true;
|
|
61
|
+
}
|
|
62
|
+
function normalizeKey(key) {
|
|
63
|
+
const lower = key.toLowerCase();
|
|
64
|
+
return NAMED[lower] ?? lower;
|
|
65
|
+
}
|
|
66
|
+
function shortcutList(spec) {
|
|
67
|
+
if (spec === false || spec == null) return [];
|
|
68
|
+
const entries = Array.isArray(spec) ? spec : [spec];
|
|
69
|
+
return entries.map(parseShortcut);
|
|
70
|
+
}
|
|
71
|
+
function matchesShortcut(event, spec, apple = isApplePlatform()) {
|
|
72
|
+
return shortcutList(spec).some((shortcut) => matchesOne(event, shortcut, apple));
|
|
73
|
+
}
|
|
74
|
+
function matchesOne(event, shortcut, apple) {
|
|
75
|
+
const key = normalizeKey(event.key);
|
|
76
|
+
const wantCtrl = Boolean(shortcut.ctrl || shortcut.mod && !apple);
|
|
77
|
+
const wantMeta = Boolean(shortcut.meta || shortcut.mod && apple);
|
|
78
|
+
return key === shortcut.key && Boolean(event.ctrlKey) === wantCtrl && Boolean(event.metaKey) === wantMeta && Boolean(event.shiftKey) === Boolean(shortcut.shift) && Boolean(event.altKey) === Boolean(shortcut.alt);
|
|
79
|
+
}
|
|
80
|
+
function shortcutTokens(spec, apple = isApplePlatform()) {
|
|
81
|
+
const shortcut = parseShortcut(spec);
|
|
82
|
+
const tokens = [];
|
|
83
|
+
if (shortcut.ctrl || shortcut.mod && !apple) tokens.push(apple ? "\u2303" : "Ctrl");
|
|
84
|
+
if (shortcut.alt) tokens.push(apple ? "\u2325" : "Alt");
|
|
85
|
+
if (shortcut.shift) tokens.push(apple ? "\u21E7" : "Shift");
|
|
86
|
+
if (shortcut.meta || shortcut.mod && apple) tokens.push(apple ? "\u2318" : "Win");
|
|
87
|
+
tokens.push(keyLabel(shortcut.key, apple));
|
|
88
|
+
return tokens;
|
|
89
|
+
}
|
|
90
|
+
function shortcutText(spec, apple = isApplePlatform()) {
|
|
91
|
+
return shortcutTokens(spec, apple).join(apple ? "" : "+");
|
|
92
|
+
}
|
|
93
|
+
var KEY_LABELS = {
|
|
94
|
+
" ": "Space",
|
|
95
|
+
escape: "Esc",
|
|
96
|
+
enter: "Enter",
|
|
97
|
+
backspace: "Backspace",
|
|
98
|
+
delete: "Del",
|
|
99
|
+
arrowup: "\u2191",
|
|
100
|
+
arrowdown: "\u2193",
|
|
101
|
+
arrowleft: "\u2190",
|
|
102
|
+
arrowright: "\u2192",
|
|
103
|
+
pageup: "PgUp",
|
|
104
|
+
pagedown: "PgDn"
|
|
105
|
+
};
|
|
106
|
+
var APPLE_KEY_LABELS = {
|
|
107
|
+
delete: "\u2326",
|
|
108
|
+
backspace: "\u232B",
|
|
109
|
+
enter: "\u21A9",
|
|
110
|
+
escape: "esc"
|
|
111
|
+
};
|
|
112
|
+
function keyLabel(key, apple) {
|
|
113
|
+
if (apple && APPLE_KEY_LABELS[key]) return APPLE_KEY_LABELS[key];
|
|
114
|
+
const named = KEY_LABELS[key];
|
|
115
|
+
if (named) return named;
|
|
116
|
+
return key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1);
|
|
117
|
+
}
|
|
118
|
+
var TYPEAHEAD_MS = 600;
|
|
119
|
+
function typeaheadStep(state, character, now = Date.now(), windowMs = TYPEAHEAD_MS) {
|
|
120
|
+
const typed = now - state.at > windowMs ? character : state.typed + character;
|
|
121
|
+
const repeated = typed.length > 1 && [...typed].every((letter) => letter === typed[0]);
|
|
122
|
+
return {
|
|
123
|
+
typed,
|
|
124
|
+
at: now,
|
|
125
|
+
needle: repeated ? typed[0] : typed,
|
|
126
|
+
cycle: typed.length === 1 || repeated
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { TYPEAHEAD_MS, isApplePlatform, matchesShortcut, parseShortcut, shortcutList, shortcutText, shortcutTokens, typeaheadStep };
|
|
@@ -61,6 +61,11 @@ function subsequenceScore(query, text) {
|
|
|
61
61
|
}
|
|
62
62
|
return score;
|
|
63
63
|
}
|
|
64
|
+
function shortenQuery(query, max = 32) {
|
|
65
|
+
const trimmed = query.trim();
|
|
66
|
+
if (trimmed.length <= max) return trimmed;
|
|
67
|
+
return `${trimmed.slice(0, max - 1).trimEnd()}\u2026`;
|
|
68
|
+
}
|
|
64
69
|
function createSearch(options = {}) {
|
|
65
70
|
let opts = { ...options };
|
|
66
71
|
let items = opts.items ?? [];
|
|
@@ -175,4 +180,4 @@ function createSearch(options = {}) {
|
|
|
175
180
|
};
|
|
176
181
|
}
|
|
177
182
|
|
|
178
|
-
export { createSearch, subsequenceMatcher };
|
|
183
|
+
export { createSearch, shortenQuery, subsequenceMatcher };
|