@enigmax/primitives 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.
Files changed (79) hide show
  1. package/dist/{chunk-QQFNAKMY.js → chunk-45UHLZYT.js} +1 -1
  2. package/dist/chunk-4VUHQFAT.js +130 -0
  3. package/dist/{chunk-D5A2ZMAG.js → chunk-4ZPMP47J.js} +6 -1
  4. package/dist/chunk-DZMHY3SU.js +777 -0
  5. package/dist/{chunk-FWVWX67R.js → chunk-FGBIZDV2.js} +3 -3
  6. package/dist/chunk-N6PDHMAX.js +213 -0
  7. package/dist/chunk-NONGREXC.js +248 -0
  8. package/dist/{chunk-R4ZAEE7V.js → chunk-QJJ34E4G.js} +34 -10
  9. package/dist/chunk-S2FPN5ID.js +115 -0
  10. package/dist/chunk-S7EE57YB.js +9 -0
  11. package/dist/chunk-VKL3DEIQ.js +804 -0
  12. package/dist/chunk-WSQC3PCC.js +466 -0
  13. package/dist/chunk-Z3VDE7OA.js +532 -0
  14. package/dist/clipboard-menu-B_ouitfS.d.ts +264 -0
  15. package/dist/color-D_rZ83Oc.d.ts +152 -0
  16. package/dist/color-OPV3BJV6.js +452 -0
  17. package/dist/{index-BHpOZncw.d.ts → index-ZPvlf9vs.d.ts} +53 -5
  18. package/dist/index.d.ts +6 -2
  19. package/dist/index.js +9 -4
  20. package/dist/keys-D2zJs1uB.d.ts +100 -0
  21. package/dist/next/index.d.ts +10 -3
  22. package/dist/next/index.js +22 -13
  23. package/dist/react/context-menu.d.ts +219 -0
  24. package/dist/react/context-menu.js +6 -0
  25. package/dist/react/index.d.ts +12 -5
  26. package/dist/react/index.js +21 -12
  27. package/dist/react/input.d.ts +3 -3
  28. package/dist/react/input.js +2 -1
  29. package/dist/react/palette.d.ts +1 -1
  30. package/dist/react/palette.js +3 -3
  31. package/dist/react/search.d.ts +1 -1
  32. package/dist/react/search.js +2 -2
  33. package/dist/react/select.d.ts +217 -0
  34. package/dist/react/select.js +7 -0
  35. package/dist/react/selection.d.ts +104 -0
  36. package/dist/react/selection.js +4 -0
  37. package/dist/react-router/index.d.ts +10 -3
  38. package/dist/react-router/index.js +22 -13
  39. package/dist/search/index.d.ts +2 -2
  40. package/dist/search/index.js +1 -1
  41. package/dist/{search-BD9-5O5U.d.ts → search-DYgqRp37.d.ts} +13 -1
  42. package/dist/{search-UQEXAPQB.js → search-PBZORZ7P.js} +1 -1
  43. package/dist/select-ClSy-J1f.d.ts +100 -0
  44. package/dist/selection-B_pmzHpy.d.ts +150 -0
  45. package/package.json +23 -2
  46. package/recipes/color/styles.css +143 -0
  47. package/recipes/context-menu/styles.css +185 -0
  48. package/recipes/input/styles.css +9 -0
  49. package/recipes/palette/styles.css +3 -0
  50. package/recipes/search/tailwind.tsx +3 -2
  51. package/recipes/select/styles.css +230 -0
  52. package/recipes/toast/styles.css +1 -1
  53. package/registry.json +432 -5
  54. package/src/core/clipboard-menu.ts +270 -0
  55. package/src/core/color.ts +248 -0
  56. package/src/core/context-menu.ts +694 -0
  57. package/src/core/keys.ts +264 -0
  58. package/src/core/search.ts +19 -0
  59. package/src/core/select.ts +404 -0
  60. package/src/core/selection.ts +648 -0
  61. package/src/index.ts +92 -1
  62. package/src/react/context-menu/context.ts +64 -0
  63. package/src/react/context-menu/index.tsx +108 -0
  64. package/src/react/context-menu/root.tsx +970 -0
  65. package/src/react/context-menu/styles.ts +194 -0
  66. package/src/react/index.ts +100 -1
  67. package/src/react/input/color-styles.ts +156 -0
  68. package/src/react/input/color.tsx +466 -0
  69. package/src/react/input/index.tsx +54 -6
  70. package/src/react/input/types.ts +59 -3
  71. package/src/react/palette/root.tsx +2 -2
  72. package/src/react/select/context.ts +56 -0
  73. package/src/react/select/index.tsx +96 -0
  74. package/src/react/select/root.tsx +839 -0
  75. package/src/react/select/styles.ts +238 -0
  76. package/src/react/selection/index.tsx +115 -0
  77. package/src/react/selection/use-selection.ts +260 -0
  78. package/dist/password-C8lG4Zm9.d.ts +0 -71
  79. /package/dist/{chunk-U3V4EHOB.js → chunk-MSOCCQGH.js} +0 -0
@@ -1,6 +1,6 @@
1
- import { createRecentStore, shortcutLabel, moveActive, groupRows, isPaletteShortcut } from './chunk-DTWZDONY.js';
2
- import { subsequenceMatcher, createSearch } from './chunk-D5A2ZMAG.js';
3
1
  import { Slot } from './chunk-HS3X3XCW.js';
2
+ import { createRecentStore, shortcutLabel, moveActive, groupRows, isPaletteShortcut } from './chunk-DTWZDONY.js';
3
+ import { subsequenceMatcher, createSearch, shortenQuery } from './chunk-4ZPMP47J.js';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { createContext, useContext, useState, useRef, useId, useMemo, useCallback, useEffect } from 'react';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -373,7 +373,7 @@ function PaletteList({ children, empty, heading, ...props }) {
373
373
  role: "listbox",
374
374
  "aria-label": "Results",
375
375
  "data-enigma-palette-list": "",
376
- 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: [
376
+ children: palette.rows.length === 0 ? empty ?? /* @__PURE__ */ jsx("p", { "data-enigma-palette-empty": "", children: palette.query.trim() ? `Nothing matches "${shortenQuery(palette.query)}".` : "Type to search." }) : groups.map((group) => /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": group.label, "data-enigma-palette-group": "", children: [
377
377
  heading !== null && (heading?.(group.label) ?? // The group carries the name already, so announcing the
378
378
  // heading again would only repeat it.
379
379
  /* @__PURE__ */ jsx("p", { "data-enigma-palette-group-label": "", "aria-hidden": "true", children: group.label })),
@@ -0,0 +1,213 @@
1
+ import { createSelection } from './chunk-WSQC3PCC.js';
2
+ import { useId, useRef, useMemo, useState, useEffect, useCallback } from 'react';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ var MARQUEE_SLOP = 4;
6
+ function useSelection(options) {
7
+ const { items, scrollIntoView = true, ...rest } = options;
8
+ const id = useId();
9
+ const listRef = useRef(null);
10
+ const latest = useRef(options);
11
+ latest.current = options;
12
+ const instance = useMemo(() => createSelection({
13
+ items,
14
+ getId: (item, index) => (latest.current.getId ?? ((_, position) => String(position)))(item, index),
15
+ disabled: (item, index) => Boolean(latest.current.disabled?.(item, index)),
16
+ onCommand: (event) => latest.current.onCommand?.(event),
17
+ onSelectionChange: (ids, chosen) => latest.current.onSelectionChange?.(ids, chosen)
18
+ // Built once: rebuilding it would drop the selection and the cursor on every render.
19
+ // Everything below is pushed in through update().
20
+ // eslint-disable-next-line react-hooks/exhaustive-deps
21
+ }), []);
22
+ const [state, setState] = useState(() => instance.state);
23
+ useEffect(() => {
24
+ const unsubscribe = instance.subscribe(setState);
25
+ setState(instance.state);
26
+ return () => {
27
+ unsubscribe();
28
+ instance.destroy();
29
+ };
30
+ }, [instance]);
31
+ const { multiple, columns, page, shortcuts } = rest;
32
+ const shortcutSignature = JSON.stringify(shortcuts ?? null);
33
+ useEffect(() => {
34
+ instance.update({ items, multiple, columns, page, shortcuts: latest.current.shortcuts });
35
+ }, [instance, items, multiple, columns, page, shortcutSignature]);
36
+ const cursor = state.cursor;
37
+ useEffect(() => {
38
+ if (!scrollIntoView || cursor < 0) return;
39
+ const row = listRef.current?.querySelector(`[data-enigma-selection-index="${cursor}"]`);
40
+ row?.scrollIntoView({ block: "nearest", inline: "nearest" });
41
+ }, [cursor, scrollIntoView]);
42
+ const isSelected = useCallback((rowId) => state.selectedSet.has(rowId), [state.selectedSet]);
43
+ const itemId = useCallback((index) => `${id}-item-${index}`, [id]);
44
+ const getListProps = useCallback((props = {}) => ({
45
+ ...props,
46
+ ref: (node) => {
47
+ listRef.current = node;
48
+ },
49
+ id: `${id}-list`,
50
+ role: props.role ?? "listbox",
51
+ "aria-multiselectable": (latest.current.multiple ?? true) || void 0,
52
+ // A single tab stop, with the arrows moving a cursor INSIDE it: a list of two hundred
53
+ // rows that are each tabbable is a list nobody can tab past.
54
+ tabIndex: props.tabIndex ?? 0,
55
+ "aria-activedescendant": state.cursor >= 0 ? itemId(state.cursor) : void 0,
56
+ "data-enigma-selection-list": "",
57
+ "data-selected-count": String(state.count),
58
+ onKeyDown: (event) => {
59
+ props.onKeyDown?.(event);
60
+ if (event.defaultPrevented) return;
61
+ if (instance.keyDown(event)) event.preventDefault();
62
+ }
63
+ }), [id, instance, itemId, state.cursor, state.count]);
64
+ const getItemProps = useCallback((index, props = {}) => {
65
+ const item = latest.current.items[index];
66
+ const rowId = item === void 0 ? null : (latest.current.getId ?? ((_, position) => String(position)))(item, index);
67
+ const disabled = item !== void 0 && Boolean(latest.current.disabled?.(item, index));
68
+ const chosen = rowId !== null && state.selectedSet.has(rowId);
69
+ return {
70
+ ...props,
71
+ id: itemId(index),
72
+ role: props.role ?? "option",
73
+ "aria-selected": chosen,
74
+ "aria-disabled": disabled || void 0,
75
+ "data-enigma-selection-item": "",
76
+ "data-enigma-selection-index": String(index),
77
+ "data-selected": chosen ? "" : void 0,
78
+ "data-cursor": state.cursor === index ? "" : void 0,
79
+ "data-disabled": disabled ? "" : void 0,
80
+ onPointerDown: (event) => {
81
+ props.onPointerDown?.(event);
82
+ if (event.defaultPrevented || disabled || event.button !== 0) return;
83
+ instance.click(index, event);
84
+ },
85
+ onContextMenu: (event) => {
86
+ props.onContextMenu?.(event);
87
+ if (event.defaultPrevented || disabled) return;
88
+ if (rowId !== null && !state.selectedSet.has(rowId)) instance.click(index);
89
+ }
90
+ };
91
+ }, [instance, itemId, state.selectedSet, state.cursor]);
92
+ const [marquee, setMarquee] = useState(null);
93
+ const drag = useRef(null);
94
+ const getMarqueeProps = useCallback((props = {}) => ({
95
+ ...props,
96
+ onPointerDown: (event) => {
97
+ props.onPointerDown?.(event);
98
+ if (event.defaultPrevented || event.button !== 0) return;
99
+ if (event.target.closest("[data-enigma-selection-item]")) return;
100
+ drag.current = { x: event.clientX, y: event.clientY, pointer: event.pointerId };
101
+ }
102
+ }), []);
103
+ useEffect(() => {
104
+ const list = listRef.current;
105
+ if (!list) return;
106
+ const move = (event) => {
107
+ const start = drag.current;
108
+ if (!start || event.pointerId !== start.pointer) return;
109
+ if (!marquee && Math.abs(event.clientX - start.x) < MARQUEE_SLOP && Math.abs(event.clientY - start.y) < MARQUEE_SLOP) return;
110
+ if (!marquee) instance.beginMarquee(event.ctrlKey || event.metaKey);
111
+ const bounds = list.getBoundingClientRect();
112
+ const left = Math.min(start.x, event.clientX);
113
+ const top = Math.min(start.y, event.clientY);
114
+ const rect = {
115
+ left: left - bounds.left + list.scrollLeft,
116
+ top: top - bounds.top + list.scrollTop,
117
+ width: Math.abs(event.clientX - start.x),
118
+ height: Math.abs(event.clientY - start.y)
119
+ };
120
+ setMarquee(rect);
121
+ const covered = [];
122
+ for (const row of list.querySelectorAll("[data-enigma-selection-item]")) {
123
+ const box = row.getBoundingClientRect();
124
+ const hits = box.right >= Math.min(start.x, event.clientX) && box.left <= Math.max(start.x, event.clientX) && box.bottom >= Math.min(start.y, event.clientY) && box.top <= Math.max(start.y, event.clientY);
125
+ if (hits) covered.push(Number(row.dataset.enigmaSelectionIndex));
126
+ }
127
+ instance.marqueeTo(covered);
128
+ };
129
+ const up = () => {
130
+ if (!drag.current) return;
131
+ drag.current = null;
132
+ setMarquee(null);
133
+ instance.endMarquee();
134
+ };
135
+ window.addEventListener("pointermove", move);
136
+ window.addEventListener("pointerup", up);
137
+ window.addEventListener("pointercancel", up);
138
+ return () => {
139
+ window.removeEventListener("pointermove", move);
140
+ window.removeEventListener("pointerup", up);
141
+ window.removeEventListener("pointercancel", up);
142
+ };
143
+ }, [instance, marquee]);
144
+ return {
145
+ instance,
146
+ state,
147
+ isSelected,
148
+ getListProps,
149
+ getItemProps,
150
+ getMarqueeProps,
151
+ marquee,
152
+ ids: { list: `${id}-list` },
153
+ itemId
154
+ };
155
+ }
156
+ function SelectionList(props) {
157
+ const {
158
+ items,
159
+ getId,
160
+ disabled,
161
+ multiple,
162
+ columns,
163
+ page,
164
+ shortcuts,
165
+ onCommand,
166
+ onSelectionChange,
167
+ scrollIntoView,
168
+ children,
169
+ empty,
170
+ marquee = true,
171
+ ...rest
172
+ } = props;
173
+ const selection = useSelection({
174
+ items,
175
+ getId,
176
+ disabled,
177
+ multiple,
178
+ columns,
179
+ page,
180
+ shortcuts,
181
+ onCommand,
182
+ onSelectionChange,
183
+ scrollIntoView
184
+ });
185
+ const listProps = selection.getListProps(rest);
186
+ const containerProps = marquee ? selection.getMarqueeProps(listProps) : listProps;
187
+ return (
188
+ // `position: relative` is the one style this component sets, and it is not a look: the
189
+ // rubber band is absolutely positioned inside, and without a positioned ancestor it
190
+ // would be drawn against the page instead of against the list.
191
+ /* @__PURE__ */ jsxs("div", { ...containerProps, style: { position: "relative", ...rest.style }, children: [
192
+ items.length === 0 && empty !== void 0 ? /* @__PURE__ */ jsx("div", { "data-enigma-selection-empty": "", children: empty }) : items.map((item, index) => /* @__PURE__ */ jsx("div", { ...selection.getItemProps(index), children: children({
193
+ item,
194
+ index,
195
+ selected: selection.state.selectedSet.has(getId ? getId(item, index) : String(index)),
196
+ cursor: selection.state.cursor === index,
197
+ disabled: Boolean(disabled?.(item, index))
198
+ }) }, getId ? getId(item, index) : index)),
199
+ selection.marquee && // The band is drawn, not just computed: without something on screen a drag over
200
+ // empty space looks like the list selecting rows at random.
201
+ /* @__PURE__ */ jsx(
202
+ "div",
203
+ {
204
+ "aria-hidden": "true",
205
+ "data-enigma-selection-marquee": "",
206
+ style: { position: "absolute", pointerEvents: "none", ...selection.marquee }
207
+ }
208
+ )
209
+ ] })
210
+ );
211
+ }
212
+
213
+ export { SelectionList, useSelection };
@@ -0,0 +1,248 @@
1
+ import { typeaheadStep } from './chunk-4VUHQFAT.js';
2
+ import { createSearch } from './chunk-4ZPMP47J.js';
3
+
4
+ // src/core/select.ts
5
+ var SELECT_SEARCH_KEYS = ["label", "description", "group", "keywords"];
6
+ var PAGE = 5;
7
+ function fold(value) {
8
+ return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
9
+ }
10
+ function toList(value) {
11
+ if (value == null) return [];
12
+ const list = Array.isArray(value) ? [...value] : [value];
13
+ return list.filter((entry) => entry !== "");
14
+ }
15
+ function sameValues(left, right) {
16
+ return left.length === right.length && left.every((entry, index) => entry === right[index]);
17
+ }
18
+ function sameRows(left, right) {
19
+ return left.length === right.length && left.every((option, index) => option === right[index]);
20
+ }
21
+ function sameState(left, right) {
22
+ return left.open === right.open && left.query === right.query && left.active === right.active && left.multiple === right.multiple && left.empty === right.empty && sameValues(left.value, right.value) && sameRows(left.visible, right.visible) && sameRows(left.selected, right.selected);
23
+ }
24
+ function createSelect(options = {}) {
25
+ let opts = { ...options };
26
+ let all = opts.options ?? [];
27
+ let value = toList(opts.value);
28
+ let open = false;
29
+ let query = "";
30
+ let active = -1;
31
+ let visible = [...all];
32
+ let typed = "";
33
+ let typedAt = 0;
34
+ let destroyed = false;
35
+ const listeners = /* @__PURE__ */ new Set();
36
+ const engine = createSearch({
37
+ items: all,
38
+ keys: opts.searchKeys ?? SELECT_SEARCH_KEYS,
39
+ fuse: opts.fuse,
40
+ fuseOptions: opts.fuseOptions,
41
+ matcher: opts.matcher,
42
+ // A select filters as you type. There is nothing to debounce: the data is already
43
+ // in memory, and a delay here is felt as the list lagging behind the field.
44
+ debounce: 0,
45
+ // An empty query in a select means "everything", not "nothing" - the opposite of a
46
+ // search field, where an empty query has nothing to show.
47
+ empty: "all"
48
+ });
49
+ function optionOf(candidate) {
50
+ return all.find((option) => option.value === candidate);
51
+ }
52
+ function selectedOptions() {
53
+ return value.map(optionOf).filter((option) => Boolean(option));
54
+ }
55
+ function snapshot() {
56
+ return {
57
+ open,
58
+ query,
59
+ active,
60
+ visible,
61
+ selected: selectedOptions(),
62
+ value: [...value],
63
+ multiple: Boolean(opts.multiple),
64
+ empty: visible.length === 0
65
+ };
66
+ }
67
+ function emit() {
68
+ const state = snapshot();
69
+ opts.onChange?.(state);
70
+ for (const listener of listeners) listener(state);
71
+ }
72
+ function wrap(index) {
73
+ const length = visible.length;
74
+ return (index % length + length) % length;
75
+ }
76
+ function enabledIndex(from, step) {
77
+ if (visible.length === 0) return -1;
78
+ for (let attempt = 0; attempt < visible.length; attempt++) {
79
+ const index = wrap(from + step * attempt);
80
+ if (!visible[index]?.disabled) return index;
81
+ }
82
+ return -1;
83
+ }
84
+ let lastActive;
85
+ function refilter() {
86
+ visible = opts.searchable && query.trim() ? engine.searchNow(query).map((match) => match.item) : [...all];
87
+ const current = active >= 0 ? visible.findIndex((option) => option === lastActive) : -1;
88
+ active = current >= 0 && !visible[current]?.disabled ? current : enabledIndex(0, 1);
89
+ lastActive = visible[active];
90
+ }
91
+ function reportValue() {
92
+ const chosen = selectedOptions();
93
+ opts.onValueChange?.(opts.multiple ? [...value] : value[0] ?? "", chosen);
94
+ }
95
+ function closesOnSelect() {
96
+ return opts.closeOnSelect ?? !opts.multiple;
97
+ }
98
+ refilter();
99
+ return {
100
+ get state() {
101
+ return snapshot();
102
+ },
103
+ setOpen(next) {
104
+ if (destroyed || open === next) return;
105
+ open = next;
106
+ if (open) {
107
+ query = "";
108
+ refilter();
109
+ const chosen = value[0] ? visible.findIndex((option) => option.value === value[0]) : -1;
110
+ active = chosen >= 0 && !visible[chosen]?.disabled ? chosen : enabledIndex(0, 1);
111
+ lastActive = visible[active];
112
+ } else {
113
+ query = "";
114
+ typed = "";
115
+ refilter();
116
+ }
117
+ emit();
118
+ },
119
+ toggleOpen() {
120
+ this.setOpen(!open);
121
+ },
122
+ setQuery(next) {
123
+ if (destroyed) return;
124
+ query = next;
125
+ refilter();
126
+ emit();
127
+ },
128
+ move(key) {
129
+ if (destroyed || visible.length === 0) return;
130
+ const from = active < 0 ? -1 : active;
131
+ switch (key) {
132
+ // Wrapping, because a select is a short list and the row after the last one
133
+ // is the first: clamping leaves the arrow key doing nothing, which reads as
134
+ // the panel having frozen.
135
+ case "ArrowDown":
136
+ active = enabledIndex(from + 1, 1);
137
+ break;
138
+ case "ArrowUp":
139
+ active = enabledIndex(from - 1 + visible.length, -1);
140
+ break;
141
+ case "Home":
142
+ active = enabledIndex(0, 1);
143
+ break;
144
+ case "End":
145
+ active = enabledIndex(visible.length - 1, -1);
146
+ break;
147
+ case "PageDown":
148
+ active = enabledIndex(Math.min(visible.length - 1, from + PAGE), 1);
149
+ break;
150
+ case "PageUp":
151
+ active = enabledIndex(Math.max(0, from - PAGE), -1);
152
+ break;
153
+ }
154
+ lastActive = visible[active];
155
+ emit();
156
+ },
157
+ setActive(index) {
158
+ if (destroyed || index === active || index < 0 || index >= visible.length || visible[index]?.disabled) return;
159
+ active = index;
160
+ lastActive = visible[active];
161
+ emit();
162
+ },
163
+ select(next) {
164
+ if (destroyed) return;
165
+ const option = optionOf(next);
166
+ if (!option || option.disabled) return;
167
+ if (opts.multiple) {
168
+ value = value.includes(next) ? value.filter((entry) => entry !== next) : [...value, next];
169
+ } else {
170
+ value = [next];
171
+ }
172
+ reportValue();
173
+ if (closesOnSelect()) {
174
+ open = false;
175
+ query = "";
176
+ refilter();
177
+ }
178
+ emit();
179
+ },
180
+ selectActive() {
181
+ const option = visible[active];
182
+ if (option) this.select(option.value);
183
+ },
184
+ remove(next) {
185
+ if (destroyed || !value.includes(next)) return;
186
+ value = value.filter((entry) => entry !== next);
187
+ reportValue();
188
+ emit();
189
+ },
190
+ clear() {
191
+ if (destroyed || value.length === 0) return;
192
+ value = [];
193
+ reportValue();
194
+ emit();
195
+ },
196
+ typeahead(character) {
197
+ if (destroyed || character.length !== 1) return;
198
+ const step = typeaheadStep({ typed, at: typedAt }, character);
199
+ typed = step.typed;
200
+ typedAt = step.at;
201
+ const needle = fold(step.needle);
202
+ const from = step.cycle ? 1 : 0;
203
+ for (let step2 = from; step2 < visible.length + from; step2++) {
204
+ const index = wrap(Math.max(active, 0) + step2);
205
+ const option = visible[index];
206
+ if (!option || option.disabled) continue;
207
+ if (fold(option.label).startsWith(needle)) {
208
+ active = index;
209
+ lastActive = option;
210
+ emit();
211
+ return;
212
+ }
213
+ }
214
+ },
215
+ update(next) {
216
+ if (destroyed) return;
217
+ const before = snapshot();
218
+ const hadOptions = next.options !== void 0;
219
+ opts = { ...opts, ...next };
220
+ if (hadOptions) all = next.options ?? [];
221
+ if (next.value !== void 0) value = toList(next.value);
222
+ if (hadOptions || next.searchKeys || next.fuse || next.fuseOptions || next.matcher) {
223
+ engine.update({
224
+ items: all,
225
+ keys: opts.searchKeys ?? SELECT_SEARCH_KEYS,
226
+ fuse: opts.fuse,
227
+ fuseOptions: opts.fuseOptions,
228
+ matcher: opts.matcher
229
+ });
230
+ }
231
+ refilter();
232
+ if (!sameState(before, snapshot())) emit();
233
+ },
234
+ subscribe(listener) {
235
+ listeners.add(listener);
236
+ return () => {
237
+ listeners.delete(listener);
238
+ };
239
+ },
240
+ destroy() {
241
+ destroyed = true;
242
+ listeners.clear();
243
+ engine.destroy();
244
+ }
245
+ };
246
+ }
247
+
248
+ export { SELECT_SEARCH_KEYS, createSelect };
@@ -1,3 +1,4 @@
1
+ import { writeValue } from './chunk-S7EE57YB.js';
1
2
  import { INPUT_ICON_PATHS } from './chunk-F25CGNQC.js';
2
3
  import { jsx, jsxs } from 'react/jsx-runtime';
3
4
  import { lazy, forwardRef, useRef, useState, useId, useCallback, useEffect, Suspense } from 'react';
@@ -16,16 +17,15 @@ var ICON_PROPS = {
16
17
  function Icon({ paths }) {
17
18
  return /* @__PURE__ */ jsx("svg", { ...ICON_PROPS, children: paths.map((path) => /* @__PURE__ */ jsx("path", { d: path }, path)) });
18
19
  }
19
-
20
- // src/react/input/write-value.ts
21
- function writeValue(input, next) {
22
- const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
23
- if (setter) setter.call(input, next);
24
- else input.value = next;
25
- input.dispatchEvent(new Event("input", { bubbles: true }));
26
- }
20
+ var COLOR_FIELD_PROPS = {
21
+ autoComplete: "off",
22
+ autoCorrect: "off",
23
+ autoCapitalize: "off",
24
+ spellCheck: false
25
+ };
27
26
  var PasswordExtras = lazy(() => import('./password-3DRQYAYQ.js').then((module) => ({ default: module.PasswordExtras })));
28
- var SearchExtras = lazy(() => import('./search-UQEXAPQB.js').then((module) => ({ default: module.SearchExtras })));
27
+ var SearchExtras = lazy(() => import('./search-PBZORZ7P.js').then((module) => ({ default: module.SearchExtras })));
28
+ var ColorExtras = lazy(() => import('./color-OPV3BJV6.js').then((module) => ({ default: module.ColorExtras })));
29
29
  var Input = forwardRef(function Input2(props, forwardedRef) {
30
30
  const {
31
31
  reveal,
@@ -53,6 +53,13 @@ var Input = forwardRef(function Input2(props, forwardedRef) {
53
53
  renderResults,
54
54
  clearable,
55
55
  clearLabel = "Clear",
56
+ format,
57
+ alpha,
58
+ swatches,
59
+ eyedropper,
60
+ placement,
61
+ styles,
62
+ colorLabels,
56
63
  wrapperProps,
57
64
  fieldProps,
58
65
  classNames,
@@ -67,6 +74,7 @@ var Input = forwardRef(function Input2(props, forwardedRef) {
67
74
  const describedBy = useId();
68
75
  const isPassword = type === "password";
69
76
  const isSearch = type === "search";
77
+ const isColor = type === "color";
70
78
  const showReveal = reveal ?? isPassword;
71
79
  const showGenerate = generate !== false && isPassword;
72
80
  const showClear = (clearable ?? isSearch) && isSearch;
@@ -192,10 +200,26 @@ var Input = forwardRef(function Input2(props, forwardedRef) {
192
200
  "data-score": score ?? void 0,
193
201
  children: [
194
202
  /* @__PURE__ */ jsxs("div", { ...fieldProps, "data-enigma-input-field": "", children: [
203
+ isColor && /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
204
+ ColorExtras,
205
+ {
206
+ input: element,
207
+ value,
208
+ format,
209
+ alpha,
210
+ swatches,
211
+ eyedropper,
212
+ placement,
213
+ styles,
214
+ locked,
215
+ labels: colorLabels
216
+ }
217
+ ) }),
195
218
  position === "start" && buttons,
196
219
  /* @__PURE__ */ jsx(
197
220
  "input",
198
221
  {
222
+ ...isColor ? COLOR_FIELD_PROPS : null,
199
223
  ...rest,
200
224
  ref: (node) => {
201
225
  innerRef.current = node;
@@ -203,7 +227,7 @@ var Input = forwardRef(function Input2(props, forwardedRef) {
203
227
  if (typeof forwardedRef === "function") forwardedRef(node);
204
228
  else if (forwardedRef) forwardedRef.current = node;
205
229
  },
206
- type: revealed && isPassword ? "text" : type,
230
+ type: isColor ? "text" : revealed && isPassword ? "text" : type,
207
231
  onChange: handleChange,
208
232
  "data-enigma-input": "",
209
233
  "aria-describedby": score !== null ? describedBy : rest["aria-describedby"]
@@ -0,0 +1,115 @@
1
+ // src/core/color.ts
2
+ function clamp(value, min, max) {
3
+ return value < min ? min : value > max ? max : value;
4
+ }
5
+ function clampAlpha(value) {
6
+ return Number.isFinite(value) ? clamp(value, 0, 1) : 1;
7
+ }
8
+ function channel(value) {
9
+ return Math.round(clamp(value, 0, 255));
10
+ }
11
+ function ratio(token, scale) {
12
+ const text = token.trim();
13
+ const value = Number.parseFloat(text);
14
+ if (!Number.isFinite(value)) return Number.NaN;
15
+ return text.endsWith("%") ? value / 100 * scale : value;
16
+ }
17
+ function components(body) {
18
+ return body.split(/[\s,/]+/).map((part) => part.trim()).filter(Boolean);
19
+ }
20
+ function parseColor(input) {
21
+ const text = input.trim().toLowerCase();
22
+ if (!text) return null;
23
+ if (text === "transparent") return { r: 0, g: 0, b: 0, a: 0 };
24
+ const hex = /^#?([0-9a-f]+)$/.exec(text);
25
+ if (hex) return fromHex(hex[1]);
26
+ const functional = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(text);
27
+ if (!functional) return null;
28
+ const parts = components(functional[2]);
29
+ if (parts.length < 3 || parts.length > 4) return null;
30
+ const alpha = parts.length === 4 ? clampAlpha(ratio(parts[3], 1)) : 1;
31
+ if (functional[1].startsWith("rgb")) {
32
+ const values = parts.slice(0, 3).map((part) => ratio(part, 255));
33
+ if (values.some((value) => !Number.isFinite(value))) return null;
34
+ return { r: channel(values[0]), g: channel(values[1]), b: channel(values[2]), a: alpha };
35
+ }
36
+ const hue = Number.parseFloat(parts[0]);
37
+ const saturation = ratio(parts[1], 1);
38
+ const lightness = ratio(parts[2], 1);
39
+ if (!Number.isFinite(hue) || !Number.isFinite(saturation) || !Number.isFinite(lightness)) return null;
40
+ return hslToRgb({ h: hue, s: clamp(saturation, 0, 1), l: clamp(lightness, 0, 1), a: alpha });
41
+ }
42
+ function fromHex(digits) {
43
+ const size = digits.length;
44
+ if (size !== 3 && size !== 4 && size !== 6 && size !== 8) return null;
45
+ const short = size <= 4;
46
+ const at = (index) => {
47
+ const slice = short ? digits[index].repeat(2) : digits.slice(index * 2, index * 2 + 2);
48
+ return Number.parseInt(slice, 16);
49
+ };
50
+ const alpha = size === 4 || size === 8 ? at(3) / 255 : 1;
51
+ return { r: at(0), g: at(1), b: at(2), a: clampAlpha(alpha) };
52
+ }
53
+ function toHex(color, options = {}) {
54
+ const pair = (value) => channel(value).toString(16).padStart(2, "0");
55
+ const alpha = clampAlpha(color.a);
56
+ const suffix = options.alpha && alpha < 1 ? pair(Math.round(alpha * 255)) : "";
57
+ return `#${pair(color.r)}${pair(color.g)}${pair(color.b)}${suffix}`;
58
+ }
59
+ function formatColor(color, format = "hex", options = {}) {
60
+ const alpha = clampAlpha(color.a);
61
+ const opaque = !options.alpha || alpha >= 1;
62
+ const printed = Number.parseFloat(alpha.toFixed(3));
63
+ if (format === "hex") return toHex(color, options);
64
+ if (format === "rgb") {
65
+ const body2 = `${channel(color.r)}, ${channel(color.g)}, ${channel(color.b)}`;
66
+ return opaque ? `rgb(${body2})` : `rgba(${body2}, ${printed})`;
67
+ }
68
+ const hsl = rgbToHsl(color);
69
+ const body = `${Math.round(hsl.h)}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%`;
70
+ return opaque ? `hsl(${body})` : `hsla(${body}, ${printed})`;
71
+ }
72
+ function rgbToHsv(color, fallbackHue = 0) {
73
+ const r = clamp(color.r, 0, 255) / 255;
74
+ const g = clamp(color.g, 0, 255) / 255;
75
+ const b = clamp(color.b, 0, 255) / 255;
76
+ const max = Math.max(r, g, b);
77
+ const span = max - Math.min(r, g, b);
78
+ let h = fallbackHue;
79
+ if (span > 0) {
80
+ if (max === r) h = (g - b) / span % 6;
81
+ else if (max === g) h = (b - r) / span + 2;
82
+ else h = (r - g) / span + 4;
83
+ h *= 60;
84
+ if (h < 0) h += 360;
85
+ }
86
+ return { h, s: max === 0 ? 0 : span / max, v: max, a: clampAlpha(color.a) };
87
+ }
88
+ function hsvToRgb(color) {
89
+ const h = (color.h % 360 + 360) % 360;
90
+ const s = clamp(color.s, 0, 1);
91
+ const v = clamp(color.v, 0, 1);
92
+ const c = v * s;
93
+ const x = c * (1 - Math.abs(h / 60 % 2 - 1));
94
+ const m = v - c;
95
+ const [r, g, b] = h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x] : h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x];
96
+ return { r: channel((r + m) * 255), g: channel((g + m) * 255), b: channel((b + m) * 255), a: clampAlpha(color.a) };
97
+ }
98
+ function rgbToHsl(color, fallbackHue = 0) {
99
+ const hsv = rgbToHsv(color, fallbackHue);
100
+ const l = hsv.v * (1 - hsv.s / 2);
101
+ const s = l === 0 || l === 1 ? 0 : (hsv.v - l) / Math.min(l, 1 - l);
102
+ return { h: hsv.h, s: clamp(s, 0, 1), l, a: hsv.a };
103
+ }
104
+ function hslToRgb(color) {
105
+ const s = clamp(color.s, 0, 1);
106
+ const l = clamp(color.l, 0, 1);
107
+ const v = l + s * Math.min(l, 1 - l);
108
+ return hsvToRgb({ h: color.h, s: v === 0 ? 0 : 2 * (1 - l / v), v, a: color.a });
109
+ }
110
+ function colorEquals(a, b) {
111
+ if (!a || !b) return a === b;
112
+ return channel(a.r) === channel(b.r) && channel(a.g) === channel(b.g) && channel(a.b) === channel(b.b) && Math.round(clampAlpha(a.a) * 255) === Math.round(clampAlpha(b.a) * 255);
113
+ }
114
+
115
+ export { colorEquals, formatColor, hslToRgb, hsvToRgb, parseColor, rgbToHsl, rgbToHsv, toHex };
@@ -0,0 +1,9 @@
1
+ // src/react/input/write-value.ts
2
+ function writeValue(input, next) {
3
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
4
+ if (setter) setter.call(input, next);
5
+ else input.value = next;
6
+ input.dispatchEvent(new Event("input", { bubbles: true }));
7
+ }
8
+
9
+ export { writeValue };