@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.
- package/dist/{chunk-QQFNAKMY.js → chunk-45UHLZYT.js} +1 -1
- package/dist/chunk-4VUHQFAT.js +130 -0
- package/dist/{chunk-D5A2ZMAG.js → chunk-4ZPMP47J.js} +6 -1
- package/dist/chunk-DZMHY3SU.js +777 -0
- package/dist/{chunk-FWVWX67R.js → chunk-FGBIZDV2.js} +3 -3
- package/dist/chunk-N6PDHMAX.js +213 -0
- package/dist/chunk-NONGREXC.js +248 -0
- package/dist/{chunk-R4ZAEE7V.js → chunk-QJJ34E4G.js} +34 -10
- package/dist/chunk-S2FPN5ID.js +115 -0
- package/dist/chunk-S7EE57YB.js +9 -0
- package/dist/chunk-VKL3DEIQ.js +804 -0
- package/dist/chunk-WSQC3PCC.js +466 -0
- package/dist/chunk-Z3VDE7OA.js +532 -0
- package/dist/clipboard-menu-B_ouitfS.d.ts +264 -0
- package/dist/color-D_rZ83Oc.d.ts +152 -0
- package/dist/color-OPV3BJV6.js +452 -0
- package/dist/{index-BHpOZncw.d.ts → index-ZPvlf9vs.d.ts} +53 -5
- package/dist/index.d.ts +6 -2
- package/dist/index.js +9 -4
- package/dist/keys-D2zJs1uB.d.ts +100 -0
- package/dist/next/index.d.ts +10 -3
- package/dist/next/index.js +22 -13
- package/dist/react/context-menu.d.ts +219 -0
- package/dist/react/context-menu.js +6 -0
- package/dist/react/index.d.ts +12 -5
- package/dist/react/index.js +21 -12
- package/dist/react/input.d.ts +3 -3
- package/dist/react/input.js +2 -1
- package/dist/react/palette.d.ts +1 -1
- package/dist/react/palette.js +3 -3
- package/dist/react/search.d.ts +1 -1
- package/dist/react/search.js +2 -2
- package/dist/react/select.d.ts +217 -0
- package/dist/react/select.js +7 -0
- package/dist/react/selection.d.ts +104 -0
- package/dist/react/selection.js +4 -0
- package/dist/react-router/index.d.ts +10 -3
- package/dist/react-router/index.js +22 -13
- package/dist/search/index.d.ts +2 -2
- package/dist/search/index.js +1 -1
- package/dist/{search-BD9-5O5U.d.ts → search-DYgqRp37.d.ts} +13 -1
- package/dist/{search-UQEXAPQB.js → search-PBZORZ7P.js} +1 -1
- package/dist/select-ClSy-J1f.d.ts +100 -0
- package/dist/selection-B_pmzHpy.d.ts +150 -0
- package/package.json +23 -2
- package/recipes/color/styles.css +143 -0
- package/recipes/context-menu/styles.css +185 -0
- package/recipes/input/styles.css +9 -0
- package/recipes/palette/styles.css +3 -0
- package/recipes/search/tailwind.tsx +3 -2
- package/recipes/select/styles.css +230 -0
- package/recipes/toast/styles.css +1 -1
- package/registry.json +432 -5
- package/src/core/clipboard-menu.ts +270 -0
- package/src/core/color.ts +248 -0
- package/src/core/context-menu.ts +694 -0
- package/src/core/keys.ts +264 -0
- package/src/core/search.ts +19 -0
- package/src/core/select.ts +404 -0
- package/src/core/selection.ts +648 -0
- package/src/index.ts +92 -1
- package/src/react/context-menu/context.ts +64 -0
- package/src/react/context-menu/index.tsx +108 -0
- package/src/react/context-menu/root.tsx +970 -0
- package/src/react/context-menu/styles.ts +194 -0
- package/src/react/index.ts +100 -1
- package/src/react/input/color-styles.ts +156 -0
- package/src/react/input/color.tsx +466 -0
- package/src/react/input/index.tsx +54 -6
- package/src/react/input/types.ts +59 -3
- package/src/react/palette/root.tsx +2 -2
- package/src/react/select/context.ts +56 -0
- package/src/react/select/index.tsx +96 -0
- package/src/react/select/root.tsx +839 -0
- package/src/react/select/styles.ts +238 -0
- package/src/react/selection/index.tsx +115 -0
- package/src/react/selection/use-selection.ts +260 -0
- package/dist/password-C8lG4Zm9.d.ts +0 -71
- /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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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-
|
|
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 };
|