@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.
Files changed (67) hide show
  1. package/dist/chunk-3BQVOOAM.js +388 -0
  2. package/dist/{chunk-QQFNAKMY.js → chunk-45UHLZYT.js} +1 -1
  3. package/dist/chunk-4VUHQFAT.js +130 -0
  4. package/dist/{chunk-D5A2ZMAG.js → chunk-4ZPMP47J.js} +6 -1
  5. package/dist/chunk-DSBYVA7V.js +713 -0
  6. package/dist/{chunk-FWVWX67R.js → chunk-FGBIZDV2.js} +3 -3
  7. package/dist/{chunk-R4ZAEE7V.js → chunk-KEVZ5XQV.js} +1 -1
  8. package/dist/chunk-N6PDHMAX.js +213 -0
  9. package/dist/chunk-NONGREXC.js +248 -0
  10. package/dist/chunk-VKL3DEIQ.js +804 -0
  11. package/dist/chunk-WSQC3PCC.js +466 -0
  12. package/dist/context-menu-D3FtTn7v.d.ts +174 -0
  13. package/dist/{index-BHpOZncw.d.ts → index-DQNnohoo.d.ts} +1 -1
  14. package/dist/index.d.ts +5 -1
  15. package/dist/index.js +8 -4
  16. package/dist/keys-D2zJs1uB.d.ts +100 -0
  17. package/dist/next/index.d.ts +9 -2
  18. package/dist/next/index.js +20 -13
  19. package/dist/react/context-menu.d.ts +202 -0
  20. package/dist/react/context-menu.js +6 -0
  21. package/dist/react/index.d.ts +10 -3
  22. package/dist/react/index.js +19 -12
  23. package/dist/react/input.d.ts +2 -2
  24. package/dist/react/input.js +1 -1
  25. package/dist/react/palette.d.ts +1 -1
  26. package/dist/react/palette.js +3 -3
  27. package/dist/react/search.d.ts +1 -1
  28. package/dist/react/search.js +2 -2
  29. package/dist/react/select.d.ts +217 -0
  30. package/dist/react/select.js +7 -0
  31. package/dist/react/selection.d.ts +104 -0
  32. package/dist/react/selection.js +4 -0
  33. package/dist/react-router/index.d.ts +9 -2
  34. package/dist/react-router/index.js +20 -13
  35. package/dist/search/index.d.ts +2 -2
  36. package/dist/search/index.js +1 -1
  37. package/dist/{search-BD9-5O5U.d.ts → search-DYgqRp37.d.ts} +13 -1
  38. package/dist/{search-UQEXAPQB.js → search-PBZORZ7P.js} +1 -1
  39. package/dist/select-ClSy-J1f.d.ts +100 -0
  40. package/dist/selection-B_pmzHpy.d.ts +150 -0
  41. package/package.json +21 -2
  42. package/recipes/context-menu/styles.css +177 -0
  43. package/recipes/input/styles.css +9 -0
  44. package/recipes/palette/styles.css +3 -0
  45. package/recipes/search/tailwind.tsx +3 -2
  46. package/recipes/select/styles.css +230 -0
  47. package/recipes/toast/styles.css +1 -1
  48. package/registry.json +358 -1
  49. package/src/core/context-menu.ts +694 -0
  50. package/src/core/keys.ts +264 -0
  51. package/src/core/search.ts +19 -0
  52. package/src/core/select.ts +404 -0
  53. package/src/core/selection.ts +648 -0
  54. package/src/index.ts +60 -1
  55. package/src/react/context-menu/context.ts +57 -0
  56. package/src/react/context-menu/index.tsx +94 -0
  57. package/src/react/context-menu/root.tsx +846 -0
  58. package/src/react/context-menu/styles.ts +186 -0
  59. package/src/react/index.ts +68 -1
  60. package/src/react/palette/root.tsx +2 -2
  61. package/src/react/select/context.ts +56 -0
  62. package/src/react/select/index.tsx +96 -0
  63. package/src/react/select/root.tsx +839 -0
  64. package/src/react/select/styles.ts +238 -0
  65. package/src/react/selection/index.tsx +115 -0
  66. package/src/react/selection/use-selection.ts +260 -0
  67. /package/dist/{chunk-U3V4EHOB.js → chunk-3HDEZ2E7.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 })),
@@ -25,7 +25,7 @@ function writeValue(input, next) {
25
25
  input.dispatchEvent(new Event("input", { bubbles: true }));
26
26
  }
27
27
  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 })));
28
+ var SearchExtras = lazy(() => import('./search-PBZORZ7P.js').then((module) => ({ default: module.SearchExtras })));
29
29
  var Input = forwardRef(function Input2(props, forwardedRef) {
30
30
  const {
31
31
  reveal,
@@ -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 };