@enigmax/primitives 0.16.0 → 0.18.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.
Files changed (89) hide show
  1. package/dist/button-CaXaqG_K.d.ts +63 -0
  2. package/dist/chunk-2QFTRNAZ.js +88 -0
  3. package/dist/chunk-6BGBYUSZ.js +114 -0
  4. package/dist/chunk-AU3H5WIY.js +107 -0
  5. package/dist/chunk-F25CGNQC.js +19 -0
  6. package/dist/chunk-HC2ME5PU.js +168 -0
  7. package/dist/chunk-HS3X3XCW.js +43 -0
  8. package/dist/chunk-IIT6U7LM.js +249 -0
  9. package/dist/chunk-IXVMRVD4.js +96 -0
  10. package/dist/chunk-KJINGUQN.js +188 -0
  11. package/dist/chunk-LKA2UG4P.js +542 -0
  12. package/dist/chunk-MMQPZGSU.js +161 -0
  13. package/dist/chunk-OCMI7R6H.js +79 -0
  14. package/dist/chunk-QYMUIW5I.js +28 -0
  15. package/dist/chunk-S653GLSF.js +17 -0
  16. package/dist/chunk-SNYUBXWQ.js +149 -0
  17. package/dist/chunk-U3V4EHOB.js +41 -0
  18. package/dist/chunk-UOSSNUSC.js +309 -0
  19. package/dist/chunk-WSB655JB.js +47 -0
  20. package/dist/chunk-XNNQRA35.js +31 -0
  21. package/dist/chunk-XQHCZAPJ.js +102 -0
  22. package/dist/chunk-ZCUFYBPB.js +154 -0
  23. package/dist/chunk-ZWR2EXHQ.js +55 -0
  24. package/dist/flags-BBJc9unY.d.ts +133 -0
  25. package/dist/index-D-ARvWpQ.d.ts +306 -0
  26. package/dist/index-DNXPtY9X.d.ts +144 -0
  27. package/dist/index.d.ts +16 -496
  28. package/dist/index.js +9 -1
  29. package/dist/input-BwXjFenq.d.ts +77 -0
  30. package/dist/marquee-CJ3Uwy3E.d.ts +81 -0
  31. package/dist/network-D2LsBG_k.d.ts +39 -0
  32. package/dist/next/index.d.ts +20 -3
  33. package/dist/next/index.js +23 -3
  34. package/dist/notifications-BpVV6sel.d.ts +70 -0
  35. package/dist/password-3DRQYAYQ.js +2 -0
  36. package/dist/password-C8lG4Zm9.d.ts +71 -0
  37. package/dist/password-FB2CUEKJ.js +1 -0
  38. package/dist/react/button.d.ts +75 -0
  39. package/dist/react/button.js +4 -0
  40. package/dist/react/flag.d.ts +37 -0
  41. package/dist/react/flag.js +3 -0
  42. package/dist/react/index.d.ts +28 -286
  43. package/dist/react/index.js +22 -2
  44. package/dist/react/input.d.ts +4 -0
  45. package/dist/react/input.js +3 -0
  46. package/dist/react/marquee.d.ts +44 -0
  47. package/dist/react/marquee.js +3 -0
  48. package/dist/react/network.d.ts +20 -0
  49. package/dist/react/network.js +3 -0
  50. package/dist/react/notifications.d.ts +17 -0
  51. package/dist/react/notifications.js +3 -0
  52. package/dist/react/palette.d.ts +3 -0
  53. package/dist/react/palette.js +4 -0
  54. package/dist/react/relative-time.d.ts +21 -0
  55. package/dist/react/relative-time.js +3 -0
  56. package/dist/react/search.d.ts +30 -0
  57. package/dist/react/search.js +3 -0
  58. package/dist/react/slot.d.ts +47 -0
  59. package/dist/react/slot.js +2 -0
  60. package/dist/react/toast.d.ts +40 -0
  61. package/dist/react/toast.js +4 -0
  62. package/dist/react-router/index.d.ts +20 -3
  63. package/dist/react-router/index.js +23 -3
  64. package/dist/relative-time-YpRTG7YH.d.ts +106 -0
  65. package/dist/search-P4OHCRXD.js +50 -0
  66. package/package.json +53 -1
  67. package/registry.json +235 -16
  68. package/src/core/flags.ts +266 -0
  69. package/src/core/input-icons.ts +34 -0
  70. package/src/core/input.ts +5 -21
  71. package/src/core/palette.ts +0 -0
  72. package/src/index.ts +18 -0
  73. package/src/react/button.tsx +70 -4
  74. package/src/react/flag.tsx +70 -0
  75. package/src/react/index.ts +52 -5
  76. package/src/react/input/icon.tsx +17 -0
  77. package/src/react/input/index.tsx +307 -0
  78. package/src/react/input/password.tsx +174 -0
  79. package/src/react/input/search.tsx +82 -0
  80. package/src/react/input/types.ts +146 -0
  81. package/src/react/input/write-value.ts +18 -0
  82. package/src/react/palette/context.ts +60 -0
  83. package/src/react/palette/index.tsx +66 -0
  84. package/src/react/palette/root.tsx +621 -0
  85. package/src/react/slot.tsx +91 -0
  86. package/src/react/use-button.ts +3 -1
  87. package/dist/chunk-53DQY6LP.js +0 -1164
  88. package/dist/chunk-U2KYYMBJ.js +0 -724
  89. package/src/react/input.tsx +0 -429
@@ -0,0 +1,542 @@
1
+ import { createSearch } from './chunk-UZFEEFMF.js';
2
+ import { Slot } from './chunk-HS3X3XCW.js';
3
+ import { createPortal } from 'react-dom';
4
+ import { createContext, useContext, useState, useRef, useId, useMemo, useCallback, useEffect } from 'react';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+
7
+ var PaletteContext = createContext(null);
8
+ function usePaletteContext(part) {
9
+ const value = useContext(PaletteContext);
10
+ if (!value) throw new Error(`<${part}> must be rendered inside <SearchPalette.Root>.`);
11
+ return value;
12
+ }
13
+
14
+ // src/core/palette.ts
15
+ function recentKey(entry) {
16
+ return `${entry.scope ?? ""}\0${entry.href ?? ""}\0${entry.href ? "" : entry.term.trim().toLowerCase()}`;
17
+ }
18
+ function createRecentStore(options = {}) {
19
+ const { key = "enigma:palette:recent", limit = 8, storage = "local" } = options;
20
+ function backing() {
21
+ if (typeof window === "undefined") return null;
22
+ try {
23
+ return storage === "session" ? window.sessionStorage : window.localStorage;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function read() {
29
+ const store = backing();
30
+ if (!store) return [];
31
+ try {
32
+ const raw = store.getItem(key);
33
+ if (!raw) return [];
34
+ const parsed = JSON.parse(raw);
35
+ if (!Array.isArray(parsed)) return [];
36
+ return parsed.filter((entry) => Boolean(entry) && typeof entry.term === "string").sort((a, b) => b.at - a.at).slice(0, limit);
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+ function write(entries) {
42
+ const store = backing();
43
+ const kept = entries.slice(0, limit);
44
+ try {
45
+ store?.setItem(key, JSON.stringify(kept));
46
+ } catch {
47
+ }
48
+ return kept;
49
+ }
50
+ return {
51
+ list: read,
52
+ remember(entry) {
53
+ const next = { ...entry, at: entry.at ?? Date.now() };
54
+ if (!next.term.trim() && !next.href) return read();
55
+ const id = recentKey(next);
56
+ return write([next, ...read().filter((existing) => recentKey(existing) !== id)]);
57
+ },
58
+ forget(entry) {
59
+ const id = recentKey(entry);
60
+ return write(read().filter((existing) => recentKey(existing) !== id));
61
+ },
62
+ clear() {
63
+ const store = backing();
64
+ try {
65
+ store?.removeItem(key);
66
+ } catch {
67
+ }
68
+ }
69
+ };
70
+ }
71
+ function groupRows(rows, labelOf) {
72
+ const groups = [];
73
+ const index = /* @__PURE__ */ new Map();
74
+ rows.forEach((row, position) => {
75
+ const label = labelOf(row);
76
+ let group = index.get(label);
77
+ if (!group) {
78
+ group = { label, rows: [] };
79
+ index.set(label, group);
80
+ groups.push(group);
81
+ }
82
+ group.rows.push({ row, position });
83
+ });
84
+ return groups;
85
+ }
86
+ function moveActive(current, length, key, page = 5) {
87
+ if (length <= 0) return 0;
88
+ switch (key) {
89
+ case "ArrowDown":
90
+ return (current + 1) % length;
91
+ case "ArrowUp":
92
+ return (current - 1 + length) % length;
93
+ case "Home":
94
+ return 0;
95
+ case "End":
96
+ return length - 1;
97
+ case "PageDown":
98
+ return Math.min(length - 1, current + page);
99
+ case "PageUp":
100
+ return Math.max(0, current - page);
101
+ }
102
+ }
103
+ function shortcutLabel(key = "K") {
104
+ const mac = typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || "");
105
+ return `${mac ? "\u2318" : "Ctrl"} ${key.toUpperCase()}`;
106
+ }
107
+ function isPaletteShortcut(event, key = "k") {
108
+ if (event.key.toLowerCase() !== key.toLowerCase()) return false;
109
+ return event.metaKey || event.ctrlKey;
110
+ }
111
+ var RECENTS_SHOWN = 5;
112
+ function firstString(item) {
113
+ if (typeof item === "string") return item;
114
+ if (item && typeof item === "object") {
115
+ for (const value of Object.values(item)) {
116
+ if (typeof value === "string" && value.trim()) return value;
117
+ }
118
+ }
119
+ return "";
120
+ }
121
+ function PaletteRoot({
122
+ items,
123
+ keys,
124
+ fuse,
125
+ fuseOptions,
126
+ matcher,
127
+ delay = 120,
128
+ limit = 40,
129
+ groupBy,
130
+ labelOf = firstString,
131
+ descriptionOf,
132
+ sections = [],
133
+ onSelect,
134
+ recents = true,
135
+ recentsKey,
136
+ recentsLimit = 8,
137
+ shortcut = "k",
138
+ open: openProp,
139
+ onOpenChange,
140
+ defaultOpen = false,
141
+ resultsLabel = "Results",
142
+ recentsLabel = "Recent",
143
+ children
144
+ }) {
145
+ const [ownOpen, setOwnOpen] = useState(defaultOpen);
146
+ const controlled = openProp !== void 0;
147
+ const open = controlled ? openProp : ownOpen;
148
+ const [query, setQuery] = useState("");
149
+ const [active, setActive] = useState(0);
150
+ const [results, setResults] = useState([]);
151
+ const [remembered, setRemembered] = useState([]);
152
+ const [busy, setBusy] = useState(false);
153
+ const triggerRef = useRef(null);
154
+ const fieldRef = useRef(null);
155
+ const base = useId();
156
+ const ids = useMemo(() => ({ field: `${base}-field`, list: `${base}-list`, title: `${base}-title` }), [base]);
157
+ const store = useMemo(
158
+ () => createRecentStore({ key: recentsKey, limit: recentsLimit }),
159
+ [recentsKey, recentsLimit]
160
+ );
161
+ const setOpen = useCallback((next) => {
162
+ if (!controlled) setOwnOpen(next);
163
+ onOpenChange?.(next);
164
+ }, [controlled, onOpenChange]);
165
+ const engine = useMemo(() => createSearch({
166
+ items,
167
+ keys,
168
+ fuse,
169
+ fuseOptions,
170
+ matcher,
171
+ debounce: delay,
172
+ limit,
173
+ onResults: (next) => {
174
+ setResults(next);
175
+ setBusy(false);
176
+ }
177
+ // Built once: the engine indexes on construction, so rebuilding it per render would
178
+ // re-index the whole corpus on every keystroke.
179
+ // eslint-disable-next-line react-hooks/exhaustive-deps
180
+ }), []);
181
+ useEffect(() => () => engine.destroy(), [engine]);
182
+ useEffect(() => {
183
+ engine.setItems(items ?? []);
184
+ }, [engine, items]);
185
+ useEffect(() => {
186
+ engine.update({ keys, fuse, fuseOptions, matcher, debounce: delay, limit });
187
+ }, [engine, keys, fuse, fuseOptions, matcher, delay, limit]);
188
+ useEffect(() => {
189
+ if (!open) return;
190
+ setRemembered(recents ? store.list() : []);
191
+ setQuery("");
192
+ engine.searchNow("");
193
+ setActive(0);
194
+ }, [open, recents, store, engine]);
195
+ useEffect(() => {
196
+ if (shortcut === null || typeof window === "undefined") return;
197
+ const onKeyDown = (event) => {
198
+ if (!isPaletteShortcut(event, shortcut)) return;
199
+ event.preventDefault();
200
+ setOpen(!open);
201
+ };
202
+ window.addEventListener("keydown", onKeyDown);
203
+ return () => window.removeEventListener("keydown", onKeyDown);
204
+ }, [shortcut, open, setOpen]);
205
+ const rows = useMemo(() => {
206
+ const trimmed = query.trim();
207
+ const out = [];
208
+ if (!trimmed && recents && remembered.length) {
209
+ remembered.slice(0, RECENTS_SHOWN).forEach((entry, index) => {
210
+ out.push({
211
+ id: `recent-${index}`,
212
+ kind: "recent",
213
+ group: recentsLabel,
214
+ recent: entry,
215
+ label: entry.label ?? entry.term
216
+ });
217
+ });
218
+ }
219
+ for (const section of sections) {
220
+ const pool = section.always || !trimmed ? section.items : section.items.filter((item) => labelOf(item).toLowerCase().includes(trimmed.toLowerCase()));
221
+ pool.forEach((item, index) => {
222
+ out.push({
223
+ id: `section-${section.label}-${index}`,
224
+ kind: "action",
225
+ group: section.label,
226
+ item,
227
+ label: labelOf(item),
228
+ description: descriptionOf?.(item)
229
+ });
230
+ });
231
+ }
232
+ results.forEach((match, index) => {
233
+ out.push({
234
+ id: `result-${index}`,
235
+ kind: "item",
236
+ group: groupBy?.(match.item) ?? resultsLabel,
237
+ item: match.item,
238
+ match,
239
+ label: labelOf(match.item),
240
+ description: descriptionOf?.(match.item)
241
+ });
242
+ });
243
+ return out;
244
+ }, [query, recents, remembered, recentsLabel, sections, results, groupBy, labelOf, descriptionOf, resultsLabel]);
245
+ useEffect(() => {
246
+ setActive((current) => current < rows.length ? current : 0);
247
+ }, [rows.length]);
248
+ const select = useCallback((row) => {
249
+ if (!row) return;
250
+ if (row.kind === "recent" && row.recent) {
251
+ if (!row.recent.href) {
252
+ setQuery(row.recent.term);
253
+ setBusy(true);
254
+ engine.search(row.recent.term);
255
+ fieldRef.current?.focus();
256
+ return;
257
+ }
258
+ }
259
+ const stay = row.onSelect ? row.onSelect() : row.item !== void 0 ? onSelect?.(row.item) : void 0;
260
+ if (recents) {
261
+ setRemembered(store.remember({
262
+ term: query.trim(),
263
+ label: row.label,
264
+ scope: row.group
265
+ }));
266
+ }
267
+ if (stay !== false) setOpen(false);
268
+ }, [engine, onSelect, query, recents, setOpen, store]);
269
+ const handleQuery = useCallback((next) => {
270
+ setQuery(next);
271
+ setActive(0);
272
+ setBusy(Boolean(next.trim()));
273
+ engine.search(next);
274
+ }, [engine]);
275
+ const value = useMemo(() => ({
276
+ open,
277
+ setOpen,
278
+ query,
279
+ setQuery: handleQuery,
280
+ rows,
281
+ active,
282
+ setActive,
283
+ select,
284
+ clearRecents: () => {
285
+ store.clear();
286
+ setRemembered([]);
287
+ },
288
+ forgetRecent: (entry) => setRemembered(store.forget(entry)),
289
+ busy,
290
+ ids,
291
+ shortcutLabel: shortcutLabel(shortcut ?? "k"),
292
+ triggerRef,
293
+ fieldRef,
294
+ rowId: (row) => `${ids.list}-${row.id}`
295
+ }), [open, setOpen, query, handleQuery, rows, active, select, busy, ids, shortcut, store]);
296
+ return /* @__PURE__ */ jsx(PaletteContext.Provider, { value, children });
297
+ }
298
+ function PaletteTrigger({ asChild = false, children, onClick, ...props }) {
299
+ const palette = usePaletteContext("SearchPalette.Trigger");
300
+ const Tag = asChild ? Slot : "button";
301
+ return /* @__PURE__ */ jsx(
302
+ Tag,
303
+ {
304
+ ...asChild ? {} : { type: "button" },
305
+ ...props,
306
+ ref: palette.triggerRef,
307
+ "data-enigma-palette-trigger": "",
308
+ "aria-haspopup": "dialog",
309
+ "aria-expanded": palette.open,
310
+ "aria-keyshortcuts": "Control+K Meta+K",
311
+ onClick: (event) => {
312
+ onClick?.(event);
313
+ if (!event.defaultPrevented) palette.setOpen(true);
314
+ },
315
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
316
+ /* @__PURE__ */ jsx("span", { "data-enigma-palette-trigger-label": "", children: "Search" }),
317
+ /* @__PURE__ */ jsx("kbd", { "data-enigma-palette-trigger-key": "", children: palette.shortcutLabel })
318
+ ] })
319
+ }
320
+ );
321
+ }
322
+ function PaletteContent({ title = "Search", portal = true, overlayProps, children, ...props }) {
323
+ const palette = usePaletteContext("SearchPalette.Content");
324
+ const panelRef = useRef(null);
325
+ const [mounted, setMounted] = useState(false);
326
+ useEffect(() => setMounted(true), []);
327
+ useEffect(() => {
328
+ if (!palette.open || typeof document === "undefined") return;
329
+ const previous = document.activeElement;
330
+ const body = document.body;
331
+ const overflow = body.style.overflow;
332
+ const gap = window.innerWidth - document.documentElement.clientWidth;
333
+ const padding = body.style.paddingRight;
334
+ body.style.overflow = "hidden";
335
+ if (gap > 0) body.style.paddingRight = `${gap}px`;
336
+ const onKeyDown = (event) => {
337
+ if (event.key === "Escape") {
338
+ event.preventDefault();
339
+ palette.setOpen(false);
340
+ return;
341
+ }
342
+ if (event.key !== "Tab") return;
343
+ const focusable = panelRef.current?.querySelectorAll(
344
+ 'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
345
+ );
346
+ if (!focusable || focusable.length === 0) return;
347
+ const first = focusable[0];
348
+ const last = focusable[focusable.length - 1];
349
+ if (!event.shiftKey && document.activeElement === last) {
350
+ event.preventDefault();
351
+ first.focus();
352
+ } else if (event.shiftKey && document.activeElement === first) {
353
+ event.preventDefault();
354
+ last.focus();
355
+ }
356
+ };
357
+ document.addEventListener("keydown", onKeyDown);
358
+ return () => {
359
+ document.removeEventListener("keydown", onKeyDown);
360
+ body.style.overflow = overflow;
361
+ body.style.paddingRight = padding;
362
+ (palette.triggerRef.current ?? previous)?.focus?.();
363
+ };
364
+ }, [palette.open, palette.setOpen, palette.triggerRef]);
365
+ if (!palette.open) return null;
366
+ const panel = /* @__PURE__ */ jsxs("div", { "data-enigma-palette-portal": "", children: [
367
+ overlayProps !== null && /* @__PURE__ */ jsx(
368
+ "div",
369
+ {
370
+ ...overlayProps,
371
+ "data-enigma-palette-overlay": "",
372
+ onClick: (event) => {
373
+ overlayProps?.onClick?.(event);
374
+ if (!event.defaultPrevented) palette.setOpen(false);
375
+ }
376
+ }
377
+ ),
378
+ /* @__PURE__ */ jsxs(
379
+ "div",
380
+ {
381
+ ...props,
382
+ ref: panelRef,
383
+ role: "dialog",
384
+ "aria-modal": "true",
385
+ "aria-labelledby": palette.ids.title,
386
+ "data-enigma-palette-content": "",
387
+ children: [
388
+ /* @__PURE__ */ jsx("h2", { id: palette.ids.title, "data-enigma-palette-title": "", children: title }),
389
+ children
390
+ ]
391
+ }
392
+ )
393
+ ] });
394
+ if (!portal) return panel;
395
+ if (!mounted || typeof document === "undefined") return null;
396
+ return createPortal(panel, document.body);
397
+ }
398
+ function PaletteField({ asChild = false, onKeyDown, ...props }) {
399
+ const palette = usePaletteContext("SearchPalette.Field");
400
+ const Tag = asChild ? Slot : "input";
401
+ const activeRow = palette.rows[palette.active];
402
+ return /* @__PURE__ */ jsx(
403
+ Tag,
404
+ {
405
+ ...props,
406
+ ref: palette.fieldRef,
407
+ id: palette.ids.field,
408
+ type: "search",
409
+ value: palette.query,
410
+ autoFocus: true,
411
+ autoComplete: "off",
412
+ autoCorrect: "off",
413
+ autoCapitalize: "none",
414
+ spellCheck: false,
415
+ enterKeyHint: "go",
416
+ role: "combobox",
417
+ "aria-expanded": true,
418
+ "aria-autocomplete": "list",
419
+ "aria-controls": palette.ids.list,
420
+ "aria-activedescendant": activeRow ? palette.rowId(activeRow) : void 0,
421
+ "data-enigma-palette-field": "",
422
+ onChange: (event) => palette.setQuery(event.target.value),
423
+ onKeyDown: (event) => {
424
+ onKeyDown?.(event);
425
+ if (event.defaultPrevented) return;
426
+ const key = event.key;
427
+ if (key === "Enter") {
428
+ event.preventDefault();
429
+ palette.select(palette.rows[palette.active]);
430
+ return;
431
+ }
432
+ if (["ArrowDown", "ArrowUp", "Home", "End", "PageDown", "PageUp"].includes(key)) {
433
+ event.preventDefault();
434
+ palette.setActive(moveActive(palette.active, palette.rows.length, key));
435
+ }
436
+ }
437
+ }
438
+ );
439
+ }
440
+ function PaletteList({ children, empty, heading, ...props }) {
441
+ const palette = usePaletteContext("SearchPalette.List");
442
+ const listRef = useRef(null);
443
+ const groups = useMemo(() => groupRows(palette.rows, (row) => row.group), [palette.rows]);
444
+ useEffect(() => {
445
+ listRef.current?.querySelector('[data-active="true"]')?.scrollIntoView({ block: "nearest" });
446
+ }, [palette.active, palette.rows]);
447
+ return /* @__PURE__ */ jsx(
448
+ "div",
449
+ {
450
+ ...props,
451
+ ref: listRef,
452
+ id: palette.ids.list,
453
+ role: "listbox",
454
+ "aria-label": "Results",
455
+ "data-enigma-palette-list": "",
456
+ children: palette.rows.length === 0 ? empty ?? /* @__PURE__ */ jsx("p", { "data-enigma-palette-empty": "", children: palette.query.trim() ? `Nothing matches "${palette.query.trim()}".` : "Type to search." }) : groups.map((group) => /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": group.label, "data-enigma-palette-group": "", children: [
457
+ heading !== null && (heading?.(group.label) ?? // The group carries the name already, so announcing the
458
+ // heading again would only repeat it.
459
+ /* @__PURE__ */ jsx("p", { "data-enigma-palette-group-label": "", "aria-hidden": "true", children: group.label })),
460
+ group.rows.map(({ row, position }) => /* @__PURE__ */ jsx(
461
+ PaletteItem,
462
+ {
463
+ row,
464
+ index: position,
465
+ children: children?.(row, { active: position === palette.active, index: position })
466
+ },
467
+ row.id
468
+ ))
469
+ ] }, group.label))
470
+ }
471
+ );
472
+ }
473
+ function PaletteItem({ row, index, children, ...props }) {
474
+ const palette = usePaletteContext("SearchPalette.Item");
475
+ const active = index === palette.active;
476
+ return /* @__PURE__ */ jsx(
477
+ "div",
478
+ {
479
+ ...props,
480
+ id: palette.rowId(row),
481
+ role: "option",
482
+ "aria-selected": active,
483
+ "data-enigma-palette-item": "",
484
+ "data-kind": row.kind,
485
+ "data-active": active ? "true" : void 0,
486
+ onMouseMove: () => {
487
+ if (!active) palette.setActive(index);
488
+ },
489
+ onClick: () => palette.select(row),
490
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
491
+ /* @__PURE__ */ jsx("span", { "data-enigma-palette-item-label": "", children: row.label }),
492
+ row.description && /* @__PURE__ */ jsx("span", { "data-enigma-palette-item-description": "", children: row.description })
493
+ ] })
494
+ }
495
+ );
496
+ }
497
+ function PaletteFooter({ hints, children, ...props }) {
498
+ usePaletteContext("SearchPalette.Footer");
499
+ return /* @__PURE__ */ jsx("div", { ...props, "data-enigma-palette-footer": "", children: children ?? hints ?? /* @__PURE__ */ jsxs(Fragment, { children: [
500
+ /* @__PURE__ */ jsxs("span", { "data-enigma-palette-hint": "", children: [
501
+ /* @__PURE__ */ jsx("kbd", { children: "up" }),
502
+ /* @__PURE__ */ jsx("kbd", { children: "down" }),
503
+ " to move"
504
+ ] }),
505
+ /* @__PURE__ */ jsxs("span", { "data-enigma-palette-hint": "", children: [
506
+ /* @__PURE__ */ jsx("kbd", { children: "enter" }),
507
+ " to open"
508
+ ] }),
509
+ /* @__PURE__ */ jsxs("span", { "data-enigma-palette-hint": "", children: [
510
+ /* @__PURE__ */ jsx("kbd", { children: "esc" }),
511
+ " to close"
512
+ ] })
513
+ ] }) });
514
+ }
515
+ function SearchPalette({
516
+ placeholder = "Search",
517
+ trigger,
518
+ renderItem,
519
+ empty,
520
+ footer,
521
+ children,
522
+ ...root
523
+ }) {
524
+ return /* @__PURE__ */ jsxs(PaletteRoot, { ...root, children: [
525
+ trigger !== null && /* @__PURE__ */ jsx(PaletteTrigger, { children: trigger }),
526
+ /* @__PURE__ */ jsxs(PaletteContent, { children: [
527
+ /* @__PURE__ */ jsx(PaletteField, { placeholder, "aria-label": placeholder }),
528
+ /* @__PURE__ */ jsx(PaletteList, { empty, children: renderItem }),
529
+ footer === null ? null : /* @__PURE__ */ jsx(PaletteFooter, { children: footer })
530
+ ] }),
531
+ children
532
+ ] });
533
+ }
534
+ SearchPalette.Root = PaletteRoot;
535
+ SearchPalette.Trigger = PaletteTrigger;
536
+ SearchPalette.Content = PaletteContent;
537
+ SearchPalette.Field = PaletteField;
538
+ SearchPalette.List = PaletteList;
539
+ SearchPalette.Item = PaletteItem;
540
+ SearchPalette.Footer = PaletteFooter;
541
+
542
+ export { PaletteContent, PaletteField, PaletteFooter, PaletteItem, PaletteList, PaletteRoot, PaletteTrigger, SearchPalette, createRecentStore, groupRows, isPaletteShortcut, moveActive, recentKey, shortcutLabel, usePaletteContext };
@@ -0,0 +1,161 @@
1
+ // src/core/button.ts
2
+ var TICK_MS = 100;
3
+ function store(cooldown) {
4
+ if (!cooldown.key || typeof window === "undefined") return null;
5
+ try {
6
+ const target = cooldown.storage === "local" ? window.localStorage : window.sessionStorage;
7
+ const probe = "__enigma_probe__";
8
+ target.setItem(probe, "1");
9
+ target.removeItem(probe);
10
+ return target;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function normalize(cooldown) {
16
+ if (cooldown == null) return null;
17
+ return typeof cooldown === "number" ? { ms: cooldown } : cooldown;
18
+ }
19
+ function isTyping(target) {
20
+ const element = target;
21
+ if (!element) return false;
22
+ const tag = element.tagName;
23
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || element.isContentEditable === true;
24
+ }
25
+ function createButton(options = {}) {
26
+ let opts = { ...options };
27
+ let loading = Boolean(opts.loading);
28
+ let readyAt = 0;
29
+ let timer = null;
30
+ let destroyed = false;
31
+ const listeners = /* @__PURE__ */ new Set();
32
+ const storageKey = () => {
33
+ const cooldown = normalize(opts.cooldown);
34
+ return cooldown?.key ? `enigma:cooldown:${cooldown.key}` : null;
35
+ };
36
+ function restore() {
37
+ const cooldown = normalize(opts.cooldown);
38
+ if (!cooldown) return;
39
+ const target = store(cooldown);
40
+ const key = storageKey();
41
+ if (!target || !key) return;
42
+ const saved = Number(target.getItem(key));
43
+ if (Number.isFinite(saved) && saved > Date.now()) readyAt = saved;
44
+ }
45
+ function remaining() {
46
+ return Math.max(0, readyAt - Date.now());
47
+ }
48
+ function snapshot() {
49
+ const disabled = Boolean(opts.disabled);
50
+ const cooldown = remaining();
51
+ return {
52
+ element: opts.href ? "a" : "button",
53
+ available: !disabled && !loading && cooldown === 0,
54
+ loading,
55
+ disabled,
56
+ cooldown,
57
+ shortcut: opts.shortcut ?? null
58
+ };
59
+ }
60
+ function emit() {
61
+ const state = snapshot();
62
+ opts.onChange?.(state);
63
+ for (const listener of listeners) listener(state);
64
+ }
65
+ function stopTicking() {
66
+ if (timer === null) return;
67
+ clearInterval(timer);
68
+ timer = null;
69
+ }
70
+ function startTicking() {
71
+ if (timer !== null || remaining() === 0) return;
72
+ timer = setInterval(() => {
73
+ if (remaining() > 0) {
74
+ emit();
75
+ return;
76
+ }
77
+ stopTicking();
78
+ emit();
79
+ }, TICK_MS);
80
+ }
81
+ function beginCooldown() {
82
+ const cooldown = normalize(opts.cooldown);
83
+ if (!cooldown || cooldown.ms <= 0) return;
84
+ readyAt = Date.now() + cooldown.ms;
85
+ const target = store(cooldown);
86
+ const key = storageKey();
87
+ if (target && key) {
88
+ try {
89
+ target.setItem(key, String(readyAt));
90
+ } catch {
91
+ }
92
+ }
93
+ startTicking();
94
+ }
95
+ async function press(event) {
96
+ if (destroyed || !snapshot().available) return;
97
+ const result = opts.onPress?.(event);
98
+ if (result instanceof Promise) {
99
+ loading = true;
100
+ emit();
101
+ try {
102
+ await result;
103
+ } finally {
104
+ loading = false;
105
+ beginCooldown();
106
+ emit();
107
+ }
108
+ return;
109
+ }
110
+ beginCooldown();
111
+ emit();
112
+ }
113
+ function onKeyDown(event) {
114
+ if (!opts.shortcut || isTyping(event.target)) return;
115
+ if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
116
+ if (event.key.toLowerCase() !== opts.shortcut.toLowerCase()) return;
117
+ if (!snapshot().available) return;
118
+ event.preventDefault();
119
+ void press(event);
120
+ }
121
+ if (typeof window !== "undefined") {
122
+ restore();
123
+ startTicking();
124
+ window.addEventListener("keydown", onKeyDown);
125
+ }
126
+ return {
127
+ get state() {
128
+ return snapshot();
129
+ },
130
+ press,
131
+ update(next) {
132
+ const hadCooldown = JSON.stringify(normalize(opts.cooldown));
133
+ opts = { ...opts, ...next };
134
+ if (next.loading !== void 0) loading = Boolean(next.loading);
135
+ if (JSON.stringify(normalize(opts.cooldown)) !== hadCooldown) restore();
136
+ emit();
137
+ },
138
+ reset() {
139
+ readyAt = 0;
140
+ stopTicking();
141
+ const cooldown = normalize(opts.cooldown);
142
+ const key = storageKey();
143
+ if (cooldown && key) store(cooldown)?.removeItem(key);
144
+ emit();
145
+ },
146
+ subscribe(listener) {
147
+ listeners.add(listener);
148
+ return () => {
149
+ listeners.delete(listener);
150
+ };
151
+ },
152
+ destroy() {
153
+ destroyed = true;
154
+ stopTicking();
155
+ listeners.clear();
156
+ if (typeof window !== "undefined") window.removeEventListener("keydown", onKeyDown);
157
+ }
158
+ };
159
+ }
160
+
161
+ export { createButton };