@enigmax/primitives 0.17.0 → 0.19.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 (97) 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-UZFEEFMF.js → chunk-D5A2ZMAG.js} +34 -1
  6. package/dist/chunk-DTWZDONY.js +99 -0
  7. package/dist/chunk-F25CGNQC.js +19 -0
  8. package/dist/chunk-FWVWX67R.js +462 -0
  9. package/dist/chunk-HC2ME5PU.js +168 -0
  10. package/dist/chunk-HS3X3XCW.js +43 -0
  11. package/dist/chunk-IXVMRVD4.js +96 -0
  12. package/dist/chunk-KJINGUQN.js +188 -0
  13. package/dist/chunk-MMQPZGSU.js +161 -0
  14. package/dist/chunk-OCMI7R6H.js +79 -0
  15. package/dist/chunk-QQFNAKMY.js +47 -0
  16. package/dist/chunk-QYMUIW5I.js +28 -0
  17. package/dist/chunk-R4ZAEE7V.js +249 -0
  18. package/dist/chunk-S653GLSF.js +17 -0
  19. package/dist/chunk-SNYUBXWQ.js +149 -0
  20. package/dist/chunk-U3V4EHOB.js +41 -0
  21. package/dist/chunk-UOSSNUSC.js +309 -0
  22. package/dist/chunk-XNNQRA35.js +31 -0
  23. package/dist/chunk-XQHCZAPJ.js +102 -0
  24. package/dist/chunk-ZCUFYBPB.js +154 -0
  25. package/dist/chunk-ZWR2EXHQ.js +55 -0
  26. package/dist/flags-BBJc9unY.d.ts +133 -0
  27. package/dist/index-dTdAbOWl.d.ts +144 -0
  28. package/dist/index.d.ts +18 -603
  29. package/dist/index.js +11 -2
  30. package/dist/input-BwXjFenq.d.ts +77 -0
  31. package/dist/marquee-CJ3Uwy3E.d.ts +81 -0
  32. package/dist/network-D2LsBG_k.d.ts +39 -0
  33. package/dist/next/index.d.ts +22 -4
  34. package/dist/next/index.js +25 -4
  35. package/dist/notifications-BpVV6sel.d.ts +70 -0
  36. package/dist/palette-D7iuQh_T.d.ts +86 -0
  37. package/dist/password-3DRQYAYQ.js +2 -0
  38. package/dist/password-C8lG4Zm9.d.ts +71 -0
  39. package/dist/password-FB2CUEKJ.js +1 -0
  40. package/dist/react/button.d.ts +75 -0
  41. package/dist/react/button.js +4 -0
  42. package/dist/react/flag.d.ts +37 -0
  43. package/dist/react/flag.js +3 -0
  44. package/dist/react/index.d.ts +29 -312
  45. package/dist/react/index.js +24 -3
  46. package/dist/react/input.d.ts +4 -0
  47. package/dist/react/input.js +3 -0
  48. package/dist/react/marquee.d.ts +44 -0
  49. package/dist/react/marquee.js +3 -0
  50. package/dist/react/network.d.ts +20 -0
  51. package/dist/react/network.js +3 -0
  52. package/dist/react/notifications.d.ts +17 -0
  53. package/dist/react/notifications.js +3 -0
  54. package/dist/react/palette.d.ts +231 -0
  55. package/dist/react/palette.js +5 -0
  56. package/dist/react/relative-time.d.ts +21 -0
  57. package/dist/react/relative-time.js +3 -0
  58. package/dist/react/search.d.ts +30 -0
  59. package/dist/react/search.js +3 -0
  60. package/dist/react/slot.d.ts +47 -0
  61. package/dist/react/slot.js +2 -0
  62. package/dist/react/toast.d.ts +40 -0
  63. package/dist/react/toast.js +4 -0
  64. package/dist/react-router/index.d.ts +22 -4
  65. package/dist/react-router/index.js +25 -4
  66. package/dist/relative-time-YpRTG7YH.d.ts +106 -0
  67. package/dist/search/index.d.ts +2 -2
  68. package/dist/search/index.js +1 -1
  69. package/dist/{search-CsO3L1Lw.d.ts → search-DXxY8SEH.d.ts} +15 -1
  70. package/dist/search-UQEXAPQB.js +50 -0
  71. package/package.json +54 -3
  72. package/recipes/input/styles.css +60 -20
  73. package/recipes/palette/styles.css +243 -0
  74. package/registry.json +186 -18
  75. package/src/core/flags.ts +88 -42
  76. package/src/core/input-icons.ts +34 -0
  77. package/src/core/input.ts +5 -21
  78. package/src/core/palette.ts +0 -0
  79. package/src/core/search.ts +60 -0
  80. package/src/index.ts +19 -3
  81. package/src/react/button.tsx +70 -4
  82. package/src/react/flag.tsx +18 -6
  83. package/src/react/index.ts +50 -5
  84. package/src/react/input/icon.tsx +17 -0
  85. package/src/react/input/index.tsx +307 -0
  86. package/src/react/input/password.tsx +174 -0
  87. package/src/react/input/search.tsx +82 -0
  88. package/src/react/input/types.ts +146 -0
  89. package/src/react/input/write-value.ts +18 -0
  90. package/src/react/palette/context.ts +60 -0
  91. package/src/react/palette/index.tsx +66 -0
  92. package/src/react/palette/root.tsx +661 -0
  93. package/src/react/slot.tsx +91 -0
  94. package/src/react/use-button.ts +3 -1
  95. package/dist/chunk-4GLYRF4B.js +0 -1259
  96. package/dist/chunk-JFNND6P4.js +0 -749
  97. package/src/react/input.tsx +0 -429
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Button behaviour: what makes it unavailable, and everything that follows from that.
3
+ *
4
+ * Disabled, loading and a cooldown are three reasons for the same state, so they collapse
5
+ * into one `available` the renderer reads, instead of three flags every call site has to
6
+ * combine correctly. The element to render is reported rather than chosen, because a
7
+ * framework-agnostic package cannot import next/link.
8
+ */
9
+ /** Which tag the consumer should render. An href makes it a link, and a link is not a button. */
10
+ type ButtonElement = "button" | "a";
11
+ interface ButtonCooldown {
12
+ /** How long the button stays unavailable after a press, in ms. */
13
+ ms: number;
14
+ /**
15
+ * Survive a reload under this key. Without it the cooldown is in memory only, and a
16
+ * refresh is a free retry - which is the whole thing a cooldown exists to prevent.
17
+ */
18
+ key?: string;
19
+ storage?: "local" | "session";
20
+ }
21
+ interface ButtonOptions {
22
+ /** Turns into an `a`, and a link cannot be `disabled` - only `aria-disabled`. */
23
+ href?: string;
24
+ disabled?: boolean;
25
+ /** Unavailable and busy. Set it yourself, or let an async `onPress` manage it. */
26
+ loading?: boolean;
27
+ /** ms, or the full shape for a cooldown that outlives a reload. */
28
+ cooldown?: number | ButtonCooldown;
29
+ /**
30
+ * A single key that presses the button. Ignored while the visitor is typing, and
31
+ * while any modifier is held, so it never steals a real shortcut.
32
+ */
33
+ shortcut?: string;
34
+ /** Async work flips `loading` for its duration and only then starts the cooldown. */
35
+ onPress?: (event?: Event) => void | Promise<void>;
36
+ /** Called whenever anything below changes. */
37
+ onChange?: (state: ButtonState) => void;
38
+ }
39
+ interface ButtonState {
40
+ /** The tag to render. */
41
+ element: ButtonElement;
42
+ /** Pressable: not disabled, not loading, not cooling down. */
43
+ available: boolean;
44
+ loading: boolean;
45
+ disabled: boolean;
46
+ /** ms left on the cooldown, 0 when there is none. */
47
+ cooldown: number;
48
+ /** The accessible name for the shortcut, when there is one. */
49
+ shortcut: string | null;
50
+ }
51
+ interface ButtonInstance {
52
+ readonly state: ButtonState;
53
+ /** Run the press as if it had been clicked. Ignored while unavailable. */
54
+ press(event?: Event): Promise<void>;
55
+ update(options: Partial<ButtonOptions>): void;
56
+ /** Clear a cooldown early, including its stored entry. */
57
+ reset(): void;
58
+ subscribe(listener: (state: ButtonState) => void): () => void;
59
+ destroy(): void;
60
+ }
61
+ declare function createButton(options?: ButtonOptions): ButtonInstance;
62
+
63
+ export { type ButtonCooldown as B, type ButtonElement as a, type ButtonInstance as b, type ButtonOptions as c, type ButtonState as d, createButton as e };
@@ -0,0 +1,88 @@
1
+ // src/core/network.ts
2
+ var SERVER_NETWORK_STATE = {
3
+ online: true,
4
+ downlink: null,
5
+ downlinkMax: null,
6
+ effectiveType: null,
7
+ rtt: null,
8
+ saveData: null,
9
+ type: null,
10
+ recovered: false,
11
+ slow: false
12
+ };
13
+ function connection() {
14
+ if (typeof navigator === "undefined") return null;
15
+ const nav = navigator;
16
+ return nav.connection ?? nav.mozConnection ?? nav.webkitConnection ?? null;
17
+ }
18
+ function shallowEqual(left, right) {
19
+ for (const key of Object.keys(left)) {
20
+ if (left[key] !== right[key]) return false;
21
+ }
22
+ return true;
23
+ }
24
+ function createNetworkMonitor() {
25
+ if (typeof window === "undefined") {
26
+ return {
27
+ get state() {
28
+ return SERVER_NETWORK_STATE;
29
+ },
30
+ subscribe() {
31
+ return () => {
32
+ };
33
+ },
34
+ destroy() {
35
+ }
36
+ };
37
+ }
38
+ const listeners = /* @__PURE__ */ new Set();
39
+ let hasBeenOffline = false;
40
+ let state = read();
41
+ function read() {
42
+ const link2 = connection();
43
+ const online = navigator.onLine;
44
+ const effectiveType = link2?.effectiveType ?? null;
45
+ const saveData = link2?.saveData ?? null;
46
+ return {
47
+ online,
48
+ downlink: link2?.downlink ?? null,
49
+ downlinkMax: link2?.downlinkMax ?? null,
50
+ effectiveType,
51
+ rtt: link2?.rtt ?? null,
52
+ saveData,
53
+ type: link2?.type ?? null,
54
+ recovered: online && hasBeenOffline,
55
+ slow: saveData === true || effectiveType === "2g" || effectiveType === "slow-2g"
56
+ };
57
+ }
58
+ function update() {
59
+ if (!navigator.onLine) hasBeenOffline = true;
60
+ const next = read();
61
+ if (shallowEqual(state, next)) return;
62
+ state = next;
63
+ for (const listener of listeners) listener(state);
64
+ }
65
+ const link = connection();
66
+ window.addEventListener("online", update, { passive: true });
67
+ window.addEventListener("offline", update, { passive: true });
68
+ link?.addEventListener("change", update, { passive: true });
69
+ return {
70
+ get state() {
71
+ return state;
72
+ },
73
+ subscribe(listener) {
74
+ listeners.add(listener);
75
+ return () => {
76
+ listeners.delete(listener);
77
+ };
78
+ },
79
+ destroy() {
80
+ listeners.clear();
81
+ window.removeEventListener("online", update);
82
+ window.removeEventListener("offline", update);
83
+ link?.removeEventListener("change", update);
84
+ }
85
+ };
86
+ }
87
+
88
+ export { SERVER_NETWORK_STATE, createNetworkMonitor };
@@ -0,0 +1,114 @@
1
+ import { createButton } from './chunk-MMQPZGSU.js';
2
+ import { Slot } from './chunk-HS3X3XCW.js';
3
+ import { forwardRef, createElement, useRef, useMemo, useState, useEffect, useCallback, isValidElement } from 'react';
4
+
5
+ function useButton(options = {}) {
6
+ const optionsRef = useRef(options);
7
+ optionsRef.current = options;
8
+ const instance = useMemo(() => createButton({
9
+ ...optionsRef.current,
10
+ onPress: (event) => optionsRef.current.onPress?.(event),
11
+ onChange: (next) => optionsRef.current.onChange?.(next)
12
+ // Built once: recreating it would drop a running cooldown on every render.
13
+ // eslint-disable-next-line react-hooks/exhaustive-deps
14
+ }), []);
15
+ const [state, setState] = useState(() => instance.state);
16
+ useEffect(() => {
17
+ const unsubscribe = instance.subscribe(setState);
18
+ setState(instance.state);
19
+ return () => {
20
+ unsubscribe();
21
+ instance.destroy();
22
+ };
23
+ }, [instance]);
24
+ useEffect(() => {
25
+ instance.update({
26
+ href: options.href,
27
+ disabled: options.disabled,
28
+ loading: options.loading,
29
+ cooldown: options.cooldown,
30
+ shortcut: options.shortcut
31
+ });
32
+ }, [instance, options.href, options.disabled, options.loading, options.cooldown, options.shortcut]);
33
+ const press = useCallback(() => {
34
+ void instance.press();
35
+ }, [instance]);
36
+ return {
37
+ ...state,
38
+ press,
39
+ reset: () => instance.reset(),
40
+ props: {
41
+ onClick: (event) => {
42
+ if (!state.available) {
43
+ event.preventDefault();
44
+ return;
45
+ }
46
+ void instance.press(event);
47
+ },
48
+ "aria-disabled": !state.available,
49
+ "aria-busy": state.loading,
50
+ ...state.loading ? { "data-loading": "" } : {},
51
+ ...state.cooldown > 0 ? { "data-cooldown": "" } : {},
52
+ ...state.element === "button" ? { disabled: state.disabled } : { href: options.href },
53
+ ...state.shortcut ? { title: `Shortcut: ${state.shortcut.toUpperCase()}`, "aria-keyshortcuts": state.shortcut } : {}
54
+ }
55
+ };
56
+ }
57
+
58
+ // src/react/link.ts
59
+ var linkComponent = null;
60
+ function setLinkComponent(component) {
61
+ linkComponent = component;
62
+ }
63
+ function getLinkComponent() {
64
+ return linkComponent ?? "a";
65
+ }
66
+ function hasText(node) {
67
+ if (node === null || node === void 0 || typeof node === "boolean") return false;
68
+ if (typeof node === "string") return node.trim().length > 0;
69
+ if (typeof node === "number") return true;
70
+ if (Array.isArray(node)) return node.some(hasText);
71
+ if (isValidElement(node)) return hasText(node.props.children);
72
+ return false;
73
+ }
74
+ var Button = forwardRef(function Button2({
75
+ as,
76
+ children,
77
+ pending,
78
+ href,
79
+ disabled,
80
+ loading,
81
+ cooldown,
82
+ shortcut,
83
+ onPress,
84
+ onClick,
85
+ onChange,
86
+ type = "button",
87
+ shortcutHint = "auto",
88
+ renderShortcut,
89
+ asChild = false,
90
+ ...rest
91
+ }, ref) {
92
+ const button = useButton({ href, disabled, loading, cooldown, shortcut, onPress: onPress ?? onClick, onChange });
93
+ const state = button;
94
+ const Tag = asChild ? Slot : as ?? (state.element === "a" ? getLinkComponent() : "button");
95
+ const native = Tag === "button" ? { type } : {};
96
+ const label = typeof children === "function" ? children(state) : children;
97
+ const content = state.loading && pending !== void 0 ? pending : label;
98
+ const showHint = !asChild && Boolean(state.shortcut) && !state.loading && (shortcutHint === "auto" ? hasText(content) : shortcutHint);
99
+ const hint = showHint && state.shortcut ? renderShortcut?.(state.shortcut) ?? createElement(
100
+ "kbd",
101
+ // Hidden from the accessible name - the button already carries
102
+ // `aria-keyshortcuts`, and "Save n" is not what anyone wants read out.
103
+ { "data-enigma-button-key": "", "aria-hidden": true },
104
+ state.shortcut
105
+ ) : null;
106
+ return createElement(
107
+ Tag,
108
+ { ref, ...button.props, ...native, ...rest },
109
+ content,
110
+ hint
111
+ );
112
+ });
113
+
114
+ export { Button, getLinkComponent, setLinkComponent, useButton };
@@ -0,0 +1,107 @@
1
+ // src/core/flags.ts
2
+ var FLAG_CDN = "https://cdn.jsdelivr.net/gh/FJRG2007/enigma@main/assets/flags";
3
+ var DEFAULTS = {
4
+ shape: "rect",
5
+ format: "svg",
6
+ source: "cdn",
7
+ basePath: "/flags",
8
+ size: 16
9
+ };
10
+ var config = { ...DEFAULTS };
11
+ function configureFlags(patch) {
12
+ config = { ...config, ...patch };
13
+ return config;
14
+ }
15
+ function flagConfig() {
16
+ return config;
17
+ }
18
+ function resetFlagConfig() {
19
+ config = { ...DEFAULTS };
20
+ return config;
21
+ }
22
+ var FLAG_RATIO = { rect: 4 / 3, square: 1, circle: 1 };
23
+ var EMOJI_FLAG = /^[\u{1F1E6}-\u{1F1FF}]{2}$/u;
24
+ var LOCALE_TAG = /^([A-Za-z]{2,3})[-_]([A-Z]{2})$/;
25
+ var FILE_CODE = /^[a-z]{2,}(?:[-_][a-z0-9]+)*$/;
26
+ var COUNTRY_CODE = /^[a-z]{2}$/;
27
+ function normalizeFlagCode(value) {
28
+ if (!value) return null;
29
+ const raw = value.trim();
30
+ if (!raw) return null;
31
+ if (EMOJI_FLAG.test(raw)) {
32
+ const letters = [...raw].map((char) => String.fromCharCode((char.codePointAt(0) ?? 0) - 127462 + 97));
33
+ return letters.join("");
34
+ }
35
+ const locale = LOCALE_TAG.exec(raw);
36
+ if (locale) return locale[2].toLowerCase();
37
+ const code = raw.toLowerCase();
38
+ return FILE_CODE.test(code) ? code : null;
39
+ }
40
+ function flagName(code, locale) {
41
+ const normalized = normalizeFlagCode(code);
42
+ if (!normalized || !COUNTRY_CODE.test(normalized)) return null;
43
+ const region = normalized.toUpperCase();
44
+ try {
45
+ const names = new Intl.DisplayNames([locale ?? documentLocale() ?? "en"], { type: "region" });
46
+ const name = names.of(region);
47
+ return !name || name === region ? null : name;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function documentLocale() {
53
+ if (typeof document === "undefined") return void 0;
54
+ return document.documentElement.lang || void 0;
55
+ }
56
+ var warnedAboutFormat = false;
57
+ function flagSrc(code, options = {}) {
58
+ const settings = { ...config, ...options };
59
+ const normalized = normalizeFlagCode(code);
60
+ if (!normalized) return null;
61
+ const remote = settings.source === "cdn";
62
+ let format = settings.format;
63
+ if (remote && format !== "svg") {
64
+ if (!warnedAboutFormat && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
65
+ warnedAboutFormat = true;
66
+ console.warn(`[enigma/flags] The flags are served as SVG, so "${format}" was served as SVG. Download a local set for raster formats: enigma add flags --flags local --flag-formats ${format}`);
67
+ }
68
+ format = "svg";
69
+ }
70
+ const base = remote ? FLAG_CDN : (settings.source === "local" ? settings.basePath : settings.source).replace(/\/+$/, "");
71
+ return `${base}/${settings.shape}/${normalized}.${format}`;
72
+ }
73
+ function flagView(code, options = {}) {
74
+ const settings = { ...config, ...options };
75
+ const normalized = normalizeFlagCode(code);
76
+ const src = flagSrc(normalized, options);
77
+ if (!normalized || !src) return null;
78
+ const height = Math.max(1, Math.round(settings.size));
79
+ return {
80
+ code: normalized,
81
+ src,
82
+ shape: settings.shape,
83
+ format: settings.source === "cdn" ? "svg" : settings.format,
84
+ width: Math.round(height * FLAG_RATIO[settings.shape]),
85
+ height,
86
+ name: flagName(normalized, settings.locale)
87
+ };
88
+ }
89
+ function flagAttributes(code, options = {}) {
90
+ const view = flagView(code, options);
91
+ if (!view) return null;
92
+ const label = options.decorative ? "" : options.label?.trim() || view.name || "";
93
+ return {
94
+ src: view.src,
95
+ alt: label,
96
+ width: view.width,
97
+ height: view.height,
98
+ loading: "lazy",
99
+ decoding: "async",
100
+ "data-enigma-flag": "",
101
+ "data-flag-code": view.code,
102
+ "data-flag-shape": view.shape,
103
+ ...label ? {} : { "aria-hidden": "true" }
104
+ };
105
+ }
106
+
107
+ export { FLAG_CDN, FLAG_RATIO, configureFlags, flagAttributes, flagConfig, flagName, flagSrc, flagView, normalizeFlagCode, resetFlagConfig };
@@ -29,6 +29,38 @@ function substringMatcher(query, items, keys) {
29
29
  }
30
30
  return results.sort((left, right) => left.score - right.score);
31
31
  }
32
+ function subsequenceMatcher(keys = []) {
33
+ return (query, items) => {
34
+ const needle = fold(query);
35
+ const results = [];
36
+ for (const item of items) {
37
+ const fields = keys.length ? keys : [""];
38
+ let best = null;
39
+ for (const key of fields) {
40
+ const haystack = fold(key ? readPath(item, key) : String(item));
41
+ const score = subsequenceScore(needle, haystack);
42
+ if (score < 0) continue;
43
+ const ranked = -score;
44
+ if (!best || ranked < best.score) best = { item, score: ranked, key: key || void 0 };
45
+ }
46
+ if (best) results.push(best);
47
+ }
48
+ return results.sort((left, right) => left.score - right.score);
49
+ };
50
+ }
51
+ function subsequenceScore(query, text) {
52
+ if (!query) return 0;
53
+ let from = 0, score = 0, run = 0;
54
+ for (let i = 0; i < query.length; i++) {
55
+ const at = text.indexOf(query[i], from);
56
+ if (at === -1) return -1;
57
+ run = i > 0 && at === from ? run + 1 : 0;
58
+ const wordStart = at === 0 || /[^a-z0-9]/.test(text[at - 1]);
59
+ score += 10 + run * 6 + (wordStart ? 8 : 0) - Math.min(at - from, 6);
60
+ from = at + 1;
61
+ }
62
+ return score;
63
+ }
32
64
  function createSearch(options = {}) {
33
65
  let opts = { ...options };
34
66
  let items = opts.items ?? [];
@@ -121,6 +153,7 @@ function createSearch(options = {}) {
121
153
  input.addEventListener("input", onInput);
122
154
  input.addEventListener("keydown", onKeyDown);
123
155
  input.dataset.enigmaSearch = "";
156
+ if (input.value) this.searchNow(input.value);
124
157
  return () => {
125
158
  input.removeEventListener("input", onInput);
126
159
  input.removeEventListener("keydown", onKeyDown);
@@ -142,4 +175,4 @@ function createSearch(options = {}) {
142
175
  };
143
176
  }
144
177
 
145
- export { createSearch };
178
+ export { createSearch, subsequenceMatcher };
@@ -0,0 +1,99 @@
1
+ // src/core/palette.ts
2
+ function recentKey(entry) {
3
+ return `${entry.scope ?? ""}\0${entry.href ?? ""}\0${entry.href ? "" : entry.term.trim().toLowerCase()}`;
4
+ }
5
+ function createRecentStore(options = {}) {
6
+ const { key = "enigma:palette:recent", limit = 8, storage = "local" } = options;
7
+ function backing() {
8
+ if (typeof window === "undefined") return null;
9
+ try {
10
+ return storage === "session" ? window.sessionStorage : window.localStorage;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function read() {
16
+ const store = backing();
17
+ if (!store) return [];
18
+ try {
19
+ const raw = store.getItem(key);
20
+ if (!raw) return [];
21
+ const parsed = JSON.parse(raw);
22
+ if (!Array.isArray(parsed)) return [];
23
+ return parsed.filter((entry) => Boolean(entry) && typeof entry.term === "string").sort((a, b) => b.at - a.at).slice(0, limit);
24
+ } catch {
25
+ return [];
26
+ }
27
+ }
28
+ function write(entries) {
29
+ const store = backing();
30
+ const kept = entries.slice(0, limit);
31
+ try {
32
+ store?.setItem(key, JSON.stringify(kept));
33
+ } catch {
34
+ }
35
+ return kept;
36
+ }
37
+ return {
38
+ list: read,
39
+ remember(entry) {
40
+ const next = { ...entry, at: entry.at ?? Date.now() };
41
+ if (!next.term.trim() && !next.href) return read();
42
+ const id = recentKey(next);
43
+ return write([next, ...read().filter((existing) => recentKey(existing) !== id)]);
44
+ },
45
+ forget(entry) {
46
+ const id = recentKey(entry);
47
+ return write(read().filter((existing) => recentKey(existing) !== id));
48
+ },
49
+ clear() {
50
+ const store = backing();
51
+ try {
52
+ store?.removeItem(key);
53
+ } catch {
54
+ }
55
+ }
56
+ };
57
+ }
58
+ function groupRows(rows, labelOf) {
59
+ const groups = [];
60
+ const index = /* @__PURE__ */ new Map();
61
+ rows.forEach((row, position) => {
62
+ const label = labelOf(row);
63
+ let group = index.get(label);
64
+ if (!group) {
65
+ group = { label, rows: [] };
66
+ index.set(label, group);
67
+ groups.push(group);
68
+ }
69
+ group.rows.push({ row, position });
70
+ });
71
+ return groups;
72
+ }
73
+ function moveActive(current, length, key, page = 5) {
74
+ if (length <= 0) return 0;
75
+ switch (key) {
76
+ case "ArrowDown":
77
+ return (current + 1) % length;
78
+ case "ArrowUp":
79
+ return (current - 1 + length) % length;
80
+ case "Home":
81
+ return 0;
82
+ case "End":
83
+ return length - 1;
84
+ case "PageDown":
85
+ return Math.min(length - 1, current + page);
86
+ case "PageUp":
87
+ return Math.max(0, current - page);
88
+ }
89
+ }
90
+ function shortcutLabel(key = "K") {
91
+ const mac = typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || "");
92
+ return `${mac ? "\u2318" : "Ctrl"} ${key.toUpperCase()}`;
93
+ }
94
+ function isPaletteShortcut(event, key = "k") {
95
+ if (event.key.toLowerCase() !== key.toLowerCase()) return false;
96
+ return event.metaKey || event.ctrlKey;
97
+ }
98
+
99
+ export { createRecentStore, groupRows, isPaletteShortcut, moveActive, recentKey, shortcutLabel };
@@ -0,0 +1,19 @@
1
+ // src/core/input-icons.ts
2
+ var INPUT_ICON_PATHS = {
3
+ eye: ["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z", "M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"],
4
+ eyeOff: [
5
+ "M10.6 5.2A9.8 9.8 0 0 1 12 5c6.5 0 10 7 10 7a17.6 17.6 0 0 1-3.2 4.2M6.2 6.2A17.7 17.7 0 0 0 2 12s3.5 7 10 7a9.6 9.6 0 0 0 4.2-.9",
6
+ "m2 2 20 20",
7
+ "M9.9 9.9a3 3 0 0 0 4.2 4.2"
8
+ ],
9
+ generate: ["m12 3 1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9Z", "M19 15l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8Z"],
10
+ /** Clears a search field. The platform draws one only in WebKit, and not on mobile. */
11
+ clear: ["m6 6 12 12", "m18 6-12 12"],
12
+ search: ["M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14Z", "m20 20-4.2-4.2"]
13
+ };
14
+ function iconMarkup(paths) {
15
+ const body = paths.map((path) => `<path d="${path}"/>`).join("");
16
+ return `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
17
+ }
18
+
19
+ export { INPUT_ICON_PATHS, iconMarkup };