@enigmax/primitives 0.3.0 → 0.5.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-OKNWKBQI.js → chunk-2DVVWPHM.js} +10 -147
- package/dist/chunk-UZFEEFMF.js +145 -0
- package/dist/index.d.ts +20 -70
- package/dist/index.js +2 -1
- package/dist/react/index.d.ts +5 -3
- package/dist/react/index.js +3 -2
- package/dist/search/index.d.ts +29 -0
- package/dist/search/index.js +12 -0
- package/dist/search-CsO3L1Lw.d.ts +68 -0
- package/package.json +6 -2
- package/recipes/marquee.css.tsx +9 -5
- package/recipes/marquee.tailwind.tsx +9 -5
- package/registry.json +7 -4
- package/src/core/marquee.ts +28 -4
- package/src/index.ts +1 -1
- package/src/react/index.ts +1 -1
- package/src/react/use-marquee.ts +2 -2
- package/src/search/index.ts +35 -0
|
@@ -11,7 +11,7 @@ var DEFAULTS = {
|
|
|
11
11
|
reverse: false,
|
|
12
12
|
vertical: false,
|
|
13
13
|
draggable: true,
|
|
14
|
-
|
|
14
|
+
hover: "off",
|
|
15
15
|
decay: DEFAULT_DECAY,
|
|
16
16
|
manageStyles: true,
|
|
17
17
|
copies: "clone"
|
|
@@ -37,10 +37,17 @@ function createMarquee(lane, track, options = {}) {
|
|
|
37
37
|
let lastTouchAt = 0;
|
|
38
38
|
const motionQuery = typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
|
|
39
39
|
let reducedMotion = motionQuery?.matches ?? false;
|
|
40
|
+
function hoverTarget(cruise) {
|
|
41
|
+
const hover = opts.hover ?? "off";
|
|
42
|
+
if (hover === "off") return cruise;
|
|
43
|
+
if (hover === "pause") return 0;
|
|
44
|
+
if (typeof hover === "number") return cruise * hover;
|
|
45
|
+
return (cruise < 0 ? -1 : 1) * Math.abs(hover.speed);
|
|
46
|
+
}
|
|
40
47
|
function target() {
|
|
41
48
|
if (reducedMotion || paused) return 0;
|
|
42
49
|
const cruise = opts.reverse ? -opts.speed : opts.speed;
|
|
43
|
-
return hovering ? cruise
|
|
50
|
+
return hovering ? hoverTarget(cruise) : cruise;
|
|
44
51
|
}
|
|
45
52
|
function axisSize(element) {
|
|
46
53
|
return opts.vertical ? element.offsetHeight : element.offsetWidth;
|
|
@@ -470,148 +477,4 @@ function createInput(input, options = {}) {
|
|
|
470
477
|
};
|
|
471
478
|
}
|
|
472
479
|
|
|
473
|
-
|
|
474
|
-
var DEFAULT_DEBOUNCE = 120;
|
|
475
|
-
function fold(value) {
|
|
476
|
-
return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
|
477
|
-
}
|
|
478
|
-
function readPath(item, path) {
|
|
479
|
-
let cursor = item;
|
|
480
|
-
for (const step of path.split(".")) {
|
|
481
|
-
if (cursor == null || typeof cursor !== "object") return "";
|
|
482
|
-
cursor = cursor[step];
|
|
483
|
-
}
|
|
484
|
-
return cursor == null ? "" : String(cursor);
|
|
485
|
-
}
|
|
486
|
-
function substringMatcher(query, items, keys) {
|
|
487
|
-
const needle = fold(query);
|
|
488
|
-
const results = [];
|
|
489
|
-
for (const item of items) {
|
|
490
|
-
const fields = keys.length ? keys : [""];
|
|
491
|
-
let best = null;
|
|
492
|
-
for (let index = 0; index < fields.length; index++) {
|
|
493
|
-
const key = fields[index];
|
|
494
|
-
const haystack = fold(key ? readPath(item, key) : String(item));
|
|
495
|
-
const at = haystack.indexOf(needle);
|
|
496
|
-
if (at < 0) continue;
|
|
497
|
-
const score = haystack === needle ? 0 : (at + 1) / (haystack.length + 1) + index * 0.01;
|
|
498
|
-
if (!best || score < best.score) best = { item, score, key: key || void 0 };
|
|
499
|
-
}
|
|
500
|
-
if (best) results.push(best);
|
|
501
|
-
}
|
|
502
|
-
return results.sort((left, right) => left.score - right.score);
|
|
503
|
-
}
|
|
504
|
-
function createSearch(options = {}) {
|
|
505
|
-
let opts = { ...options };
|
|
506
|
-
let items = opts.items ?? [];
|
|
507
|
-
let query = "";
|
|
508
|
-
let results = [];
|
|
509
|
-
let timer = null;
|
|
510
|
-
let engine = null;
|
|
511
|
-
let destroyed = false;
|
|
512
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
513
|
-
function buildEngine() {
|
|
514
|
-
engine = null;
|
|
515
|
-
if (opts.matcher || !opts.fuse) return;
|
|
516
|
-
engine = new opts.fuse(items, { keys: opts.keys ?? [], includeScore: true, includeMatches: true, threshold: 0.4, ...opts.fuseOptions });
|
|
517
|
-
}
|
|
518
|
-
function run(next) {
|
|
519
|
-
const trimmed = next.trim();
|
|
520
|
-
const minLength = opts.minLength ?? 1;
|
|
521
|
-
let found;
|
|
522
|
-
if (trimmed.length < minLength) {
|
|
523
|
-
found = (opts.empty ?? "none") === "all" ? items.map((item) => ({ item, score: 0 })) : [];
|
|
524
|
-
} else if (opts.matcher) {
|
|
525
|
-
found = opts.matcher(trimmed, items);
|
|
526
|
-
} else if (engine) {
|
|
527
|
-
found = engine.search(trimmed).map((hit) => ({
|
|
528
|
-
item: hit.item,
|
|
529
|
-
score: hit.score ?? 0,
|
|
530
|
-
key: hit.matches?.[0]?.key
|
|
531
|
-
}));
|
|
532
|
-
} else {
|
|
533
|
-
found = substringMatcher(trimmed, items, opts.keys ?? []);
|
|
534
|
-
}
|
|
535
|
-
return opts.limit != null ? found.slice(0, opts.limit) : found;
|
|
536
|
-
}
|
|
537
|
-
function publish(next) {
|
|
538
|
-
query = next;
|
|
539
|
-
results = run(next);
|
|
540
|
-
opts.onResults?.(results, query);
|
|
541
|
-
for (const listener of listeners) listener(results, query);
|
|
542
|
-
return results;
|
|
543
|
-
}
|
|
544
|
-
function clearTimer() {
|
|
545
|
-
if (timer === null) return;
|
|
546
|
-
clearTimeout(timer);
|
|
547
|
-
timer = null;
|
|
548
|
-
}
|
|
549
|
-
buildEngine();
|
|
550
|
-
return {
|
|
551
|
-
get query() {
|
|
552
|
-
return query;
|
|
553
|
-
},
|
|
554
|
-
get results() {
|
|
555
|
-
return results;
|
|
556
|
-
},
|
|
557
|
-
search(next) {
|
|
558
|
-
if (destroyed) return;
|
|
559
|
-
clearTimer();
|
|
560
|
-
const wait = opts.debounce ?? DEFAULT_DEBOUNCE;
|
|
561
|
-
if (wait <= 0) {
|
|
562
|
-
publish(next);
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
timer = setTimeout(() => {
|
|
566
|
-
timer = null;
|
|
567
|
-
publish(next);
|
|
568
|
-
}, wait);
|
|
569
|
-
},
|
|
570
|
-
searchNow(next) {
|
|
571
|
-
clearTimer();
|
|
572
|
-
return publish(next);
|
|
573
|
-
},
|
|
574
|
-
setItems(next) {
|
|
575
|
-
items = next;
|
|
576
|
-
buildEngine();
|
|
577
|
-
if (query) publish(query);
|
|
578
|
-
},
|
|
579
|
-
update(next) {
|
|
580
|
-
opts = { ...opts, ...next };
|
|
581
|
-
if (next.items) items = next.items;
|
|
582
|
-
buildEngine();
|
|
583
|
-
if (query) publish(query);
|
|
584
|
-
},
|
|
585
|
-
attach(input) {
|
|
586
|
-
const onInput = () => this.search(input.value);
|
|
587
|
-
const onKeyDown = (event) => {
|
|
588
|
-
if (event.key !== "Escape" || !input.value) return;
|
|
589
|
-
event.stopPropagation();
|
|
590
|
-
input.value = "";
|
|
591
|
-
this.searchNow("");
|
|
592
|
-
};
|
|
593
|
-
input.addEventListener("input", onInput);
|
|
594
|
-
input.addEventListener("keydown", onKeyDown);
|
|
595
|
-
input.dataset.enigmaSearch = "";
|
|
596
|
-
return () => {
|
|
597
|
-
input.removeEventListener("input", onInput);
|
|
598
|
-
input.removeEventListener("keydown", onKeyDown);
|
|
599
|
-
delete input.dataset.enigmaSearch;
|
|
600
|
-
};
|
|
601
|
-
},
|
|
602
|
-
subscribe(listener) {
|
|
603
|
-
listeners.add(listener);
|
|
604
|
-
return () => {
|
|
605
|
-
listeners.delete(listener);
|
|
606
|
-
};
|
|
607
|
-
},
|
|
608
|
-
destroy() {
|
|
609
|
-
destroyed = true;
|
|
610
|
-
clearTimer();
|
|
611
|
-
listeners.clear();
|
|
612
|
-
engine = null;
|
|
613
|
-
}
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
export { createInput, createMarquee, createSearch };
|
|
480
|
+
export { createInput, createMarquee };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/core/search.ts
|
|
2
|
+
var DEFAULT_DEBOUNCE = 120;
|
|
3
|
+
function fold(value) {
|
|
4
|
+
return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
function readPath(item, path) {
|
|
7
|
+
let cursor = item;
|
|
8
|
+
for (const step of path.split(".")) {
|
|
9
|
+
if (cursor == null || typeof cursor !== "object") return "";
|
|
10
|
+
cursor = cursor[step];
|
|
11
|
+
}
|
|
12
|
+
return cursor == null ? "" : String(cursor);
|
|
13
|
+
}
|
|
14
|
+
function substringMatcher(query, items, keys) {
|
|
15
|
+
const needle = fold(query);
|
|
16
|
+
const results = [];
|
|
17
|
+
for (const item of items) {
|
|
18
|
+
const fields = keys.length ? keys : [""];
|
|
19
|
+
let best = null;
|
|
20
|
+
for (let index = 0; index < fields.length; index++) {
|
|
21
|
+
const key = fields[index];
|
|
22
|
+
const haystack = fold(key ? readPath(item, key) : String(item));
|
|
23
|
+
const at = haystack.indexOf(needle);
|
|
24
|
+
if (at < 0) continue;
|
|
25
|
+
const score = haystack === needle ? 0 : (at + 1) / (haystack.length + 1) + index * 0.01;
|
|
26
|
+
if (!best || score < best.score) best = { item, score, key: key || void 0 };
|
|
27
|
+
}
|
|
28
|
+
if (best) results.push(best);
|
|
29
|
+
}
|
|
30
|
+
return results.sort((left, right) => left.score - right.score);
|
|
31
|
+
}
|
|
32
|
+
function createSearch(options = {}) {
|
|
33
|
+
let opts = { ...options };
|
|
34
|
+
let items = opts.items ?? [];
|
|
35
|
+
let query = "";
|
|
36
|
+
let results = [];
|
|
37
|
+
let timer = null;
|
|
38
|
+
let engine = null;
|
|
39
|
+
let destroyed = false;
|
|
40
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
41
|
+
function buildEngine() {
|
|
42
|
+
engine = null;
|
|
43
|
+
if (opts.matcher || !opts.fuse) return;
|
|
44
|
+
engine = new opts.fuse(items, { keys: opts.keys ?? [], includeScore: true, includeMatches: true, threshold: 0.4, ...opts.fuseOptions });
|
|
45
|
+
}
|
|
46
|
+
function run(next) {
|
|
47
|
+
const trimmed = next.trim();
|
|
48
|
+
const minLength = opts.minLength ?? 1;
|
|
49
|
+
let found;
|
|
50
|
+
if (trimmed.length < minLength) {
|
|
51
|
+
found = (opts.empty ?? "none") === "all" ? items.map((item) => ({ item, score: 0 })) : [];
|
|
52
|
+
} else if (opts.matcher) {
|
|
53
|
+
found = opts.matcher(trimmed, items);
|
|
54
|
+
} else if (engine) {
|
|
55
|
+
found = engine.search(trimmed).map((hit) => ({
|
|
56
|
+
item: hit.item,
|
|
57
|
+
score: hit.score ?? 0,
|
|
58
|
+
key: hit.matches?.[0]?.key
|
|
59
|
+
}));
|
|
60
|
+
} else {
|
|
61
|
+
found = substringMatcher(trimmed, items, opts.keys ?? []);
|
|
62
|
+
}
|
|
63
|
+
return opts.limit != null ? found.slice(0, opts.limit) : found;
|
|
64
|
+
}
|
|
65
|
+
function publish(next) {
|
|
66
|
+
query = next;
|
|
67
|
+
results = run(next);
|
|
68
|
+
opts.onResults?.(results, query);
|
|
69
|
+
for (const listener of listeners) listener(results, query);
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
function clearTimer() {
|
|
73
|
+
if (timer === null) return;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
timer = null;
|
|
76
|
+
}
|
|
77
|
+
buildEngine();
|
|
78
|
+
return {
|
|
79
|
+
get query() {
|
|
80
|
+
return query;
|
|
81
|
+
},
|
|
82
|
+
get results() {
|
|
83
|
+
return results;
|
|
84
|
+
},
|
|
85
|
+
search(next) {
|
|
86
|
+
if (destroyed) return;
|
|
87
|
+
clearTimer();
|
|
88
|
+
const wait = opts.debounce ?? DEFAULT_DEBOUNCE;
|
|
89
|
+
if (wait <= 0) {
|
|
90
|
+
publish(next);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
timer = setTimeout(() => {
|
|
94
|
+
timer = null;
|
|
95
|
+
publish(next);
|
|
96
|
+
}, wait);
|
|
97
|
+
},
|
|
98
|
+
searchNow(next) {
|
|
99
|
+
clearTimer();
|
|
100
|
+
return publish(next);
|
|
101
|
+
},
|
|
102
|
+
setItems(next) {
|
|
103
|
+
items = next;
|
|
104
|
+
buildEngine();
|
|
105
|
+
if (query) publish(query);
|
|
106
|
+
},
|
|
107
|
+
update(next) {
|
|
108
|
+
opts = { ...opts, ...next };
|
|
109
|
+
if (next.items) items = next.items;
|
|
110
|
+
buildEngine();
|
|
111
|
+
if (query) publish(query);
|
|
112
|
+
},
|
|
113
|
+
attach(input) {
|
|
114
|
+
const onInput = () => this.search(input.value);
|
|
115
|
+
const onKeyDown = (event) => {
|
|
116
|
+
if (event.key !== "Escape" || !input.value) return;
|
|
117
|
+
event.stopPropagation();
|
|
118
|
+
input.value = "";
|
|
119
|
+
this.searchNow("");
|
|
120
|
+
};
|
|
121
|
+
input.addEventListener("input", onInput);
|
|
122
|
+
input.addEventListener("keydown", onKeyDown);
|
|
123
|
+
input.dataset.enigmaSearch = "";
|
|
124
|
+
return () => {
|
|
125
|
+
input.removeEventListener("input", onInput);
|
|
126
|
+
input.removeEventListener("keydown", onKeyDown);
|
|
127
|
+
delete input.dataset.enigmaSearch;
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
subscribe(listener) {
|
|
131
|
+
listeners.add(listener);
|
|
132
|
+
return () => {
|
|
133
|
+
listeners.delete(listener);
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
destroy() {
|
|
137
|
+
destroyed = true;
|
|
138
|
+
clearTimer();
|
|
139
|
+
listeners.clear();
|
|
140
|
+
engine = null;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { createSearch };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { F as FuseConstructor, a as FuseLike, S as SearchInstance, b as SearchMatch, c as SearchOptions, d as createSearch } from './search-CsO3L1Lw.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Draggable infinite marquee: the behaviour engine, with no styling of its own.
|
|
3
5
|
*
|
|
@@ -9,6 +11,21 @@
|
|
|
9
11
|
* comments marked "non-negotiable" name the bug that the obvious implementation
|
|
10
12
|
* shipped with.
|
|
11
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* What a MOUSE resting on the row does to its speed. One option covers every case, so
|
|
16
|
+
* there is no pile of booleans to reconcile:
|
|
17
|
+
*
|
|
18
|
+
* - `"off"` ignore hover entirely (the default)
|
|
19
|
+
* - `"pause"` stop while the pointer is on it
|
|
20
|
+
* - a number multiply the cruise speed: `0.15` crawls, `1` changes nothing, `2` doubles
|
|
21
|
+
* - `{ speed }` an ABSOLUTE speed in px/s, whatever the cruise is; direction still
|
|
22
|
+
* follows `reverse`, so you give a magnitude and not a sign
|
|
23
|
+
*
|
|
24
|
+
* Touch never triggers any of it - see the pointerType note on the hover handler.
|
|
25
|
+
*/
|
|
26
|
+
type MarqueeHover = number | "off" | "pause" | {
|
|
27
|
+
speed: number;
|
|
28
|
+
};
|
|
12
29
|
interface MarqueeOptions {
|
|
13
30
|
/**
|
|
14
31
|
* Pixels per second. NOT a duration: a duration ties the speed to the item
|
|
@@ -21,8 +38,8 @@ interface MarqueeOptions {
|
|
|
21
38
|
vertical?: boolean;
|
|
22
39
|
/** Allow grabbing and throwing the row. */
|
|
23
40
|
draggable?: boolean;
|
|
24
|
-
/**
|
|
25
|
-
|
|
41
|
+
/** What a mouse resting on the row does to its speed. See {@link MarqueeHover}. */
|
|
42
|
+
hover?: MarqueeHover;
|
|
26
43
|
/** Fraction of the remaining velocity gap left after one second. */
|
|
27
44
|
decay?: number;
|
|
28
45
|
/**
|
|
@@ -145,71 +162,4 @@ interface InputInstance {
|
|
|
145
162
|
*/
|
|
146
163
|
declare function createInput(input: HTMLInputElement, options?: InputOptions): InputInstance;
|
|
147
164
|
|
|
148
|
-
|
|
149
|
-
* Search-as-you-type: debouncing, ordering, cancellation and the field wiring, with the
|
|
150
|
-
* matching left pluggable.
|
|
151
|
-
*
|
|
152
|
-
* Fuse.js is NOT a dependency of this package. Hand it the constructor and you get fuzzy
|
|
153
|
-
* matching configured exactly as if you had called it yourself - `fuseOptions` is passed
|
|
154
|
-
* through untouched. Hand it nothing and a built-in accent-insensitive substring matcher
|
|
155
|
-
* runs instead, so the primitive works with zero dependencies. Hand it a `matcher` and
|
|
156
|
-
* every part of this is replaced by your own.
|
|
157
|
-
*/
|
|
158
|
-
interface SearchMatch<T> {
|
|
159
|
-
item: T;
|
|
160
|
-
/** Lower is better, matching Fuse's convention. 0 is an exact hit. */
|
|
161
|
-
score: number;
|
|
162
|
-
/** Which key matched, when the matcher reports it. */
|
|
163
|
-
key?: string;
|
|
164
|
-
}
|
|
165
|
-
/** The shape of Fuse's constructor, declared here so the package need not depend on it. */
|
|
166
|
-
interface FuseLike<T> {
|
|
167
|
-
search(query: string): {
|
|
168
|
-
item: T;
|
|
169
|
-
score?: number;
|
|
170
|
-
matches?: {
|
|
171
|
-
key?: string;
|
|
172
|
-
}[];
|
|
173
|
-
}[];
|
|
174
|
-
}
|
|
175
|
-
type FuseConstructor = new <T>(items: readonly T[], options?: Record<string, unknown>) => FuseLike<T>;
|
|
176
|
-
interface SearchOptions<T> {
|
|
177
|
-
items?: readonly T[];
|
|
178
|
-
/** Fields to search. Dotted paths work with the built-in matcher and with Fuse. */
|
|
179
|
-
keys?: string[];
|
|
180
|
-
/**
|
|
181
|
-
* Pass Fuse.js's constructor to get fuzzy matching. Omit it for the built-in
|
|
182
|
-
* substring matcher. Ignored when `matcher` is set.
|
|
183
|
-
*/
|
|
184
|
-
fuse?: FuseConstructor;
|
|
185
|
-
/** Handed to Fuse verbatim, so any Fuse option behaves exactly as documented there. */
|
|
186
|
-
fuseOptions?: Record<string, unknown>;
|
|
187
|
-
/** Replaces the engine entirely. Return the results in the order you want them. */
|
|
188
|
-
matcher?: (query: string, items: readonly T[]) => SearchMatch<T>[];
|
|
189
|
-
/** ms to wait after the last keystroke. 0 searches on every one. */
|
|
190
|
-
debounce?: number;
|
|
191
|
-
/** Queries shorter than this return nothing rather than everything. */
|
|
192
|
-
minLength?: number;
|
|
193
|
-
/** Cap the result list. */
|
|
194
|
-
limit?: number;
|
|
195
|
-
/** What an empty query returns. "none" (default) or "all". */
|
|
196
|
-
empty?: "none" | "all";
|
|
197
|
-
onResults?: (results: SearchMatch<T>[], query: string) => void;
|
|
198
|
-
}
|
|
199
|
-
interface SearchInstance<T> {
|
|
200
|
-
readonly query: string;
|
|
201
|
-
readonly results: SearchMatch<T>[];
|
|
202
|
-
/** Debounced. */
|
|
203
|
-
search(query: string): void;
|
|
204
|
-
/** Skips the debounce, e.g. on Enter. */
|
|
205
|
-
searchNow(query: string): SearchMatch<T>[];
|
|
206
|
-
setItems(items: readonly T[]): void;
|
|
207
|
-
update(options: Partial<SearchOptions<T>>): void;
|
|
208
|
-
/** Bind to a field: input events search, Escape clears. Returns an unbind. */
|
|
209
|
-
attach(input: HTMLInputElement): () => void;
|
|
210
|
-
subscribe(listener: (results: SearchMatch<T>[], query: string) => void): () => void;
|
|
211
|
-
destroy(): void;
|
|
212
|
-
}
|
|
213
|
-
declare function createSearch<T>(options?: SearchOptions<T>): SearchInstance<T>;
|
|
214
|
-
|
|
215
|
-
export { type FuseConstructor, type FuseLike, type InputAction, type InputActionState, type InputIcon, type InputInstance, type InputOptions, type MarqueeInstance, type MarqueeOptions, type SearchInstance, type SearchMatch, type SearchOptions, createInput, createMarquee, createSearch };
|
|
165
|
+
export { type InputAction, type InputActionState, type InputIcon, type InputInstance, type InputOptions, type MarqueeHover, type MarqueeInstance, type MarqueeOptions, createInput, createMarquee };
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { createInput, createMarquee
|
|
1
|
+
export { createInput, createMarquee } from './chunk-2DVVWPHM.js';
|
|
2
|
+
export { createSearch } from './chunk-UZFEEFMF.js';
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { RefObject } from 'react';
|
|
2
|
-
import { MarqueeOptions, InputOptions
|
|
3
|
-
export {
|
|
2
|
+
import { MarqueeOptions, InputOptions } from '../index.js';
|
|
3
|
+
export { InputAction, InputIcon, MarqueeHover, MarqueeInstance } from '../index.js';
|
|
4
|
+
import { b as SearchMatch, c as SearchOptions } from '../search-CsO3L1Lw.js';
|
|
5
|
+
export { F as FuseConstructor } from '../search-CsO3L1Lw.js';
|
|
4
6
|
|
|
5
7
|
interface UseMarqueeResult {
|
|
6
8
|
/** Attach to the clipping viewport. */
|
|
@@ -26,7 +28,7 @@ interface UseMarqueeResult {
|
|
|
26
28
|
* to the item count, so the row accelerates every time content is added.
|
|
27
29
|
*
|
|
28
30
|
* ```tsx
|
|
29
|
-
* const { laneRef, trackRef, copies } = useMarquee({ speed: 80,
|
|
31
|
+
* const { laneRef, trackRef, copies } = useMarquee({ speed: 80, hover: 0.15 });
|
|
30
32
|
* return (
|
|
31
33
|
* <div ref={laneRef}>
|
|
32
34
|
* <div ref={trackRef} style={{ display: "flex" }}>
|
package/dist/react/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createMarquee, createInput
|
|
1
|
+
import { createMarquee, createInput } from '../chunk-2DVVWPHM.js';
|
|
2
|
+
import { createSearch } from '../chunk-UZFEEFMF.js';
|
|
2
3
|
import { useRef, useState, useMemo, useEffect, useLayoutEffect } from 'react';
|
|
3
4
|
|
|
4
5
|
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
@@ -38,7 +39,7 @@ function useMarquee(options = {}) {
|
|
|
38
39
|
}, []);
|
|
39
40
|
useIsomorphicLayoutEffect(() => {
|
|
40
41
|
instanceRef.current?.update(options);
|
|
41
|
-
}, [options.speed, options.reverse, options.vertical, options.draggable, options.
|
|
42
|
+
}, [options.speed, options.reverse, options.vertical, options.draggable, options.hover, options.decay]);
|
|
42
43
|
useIsomorphicLayoutEffect(() => {
|
|
43
44
|
instanceRef.current?.measure();
|
|
44
45
|
}, [copies]);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { c as SearchOptions, S as SearchInstance } from '../search-CsO3L1Lw.js';
|
|
2
|
+
export { b as SearchMatch } from '../search-CsO3L1Lw.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Search with Fuse.js already wired: the batteries-included entry point.
|
|
6
|
+
*
|
|
7
|
+
* `@enigmax/primitives` stays dependency-free, so someone who only wants the marquee
|
|
8
|
+
* never pulls a search engine into their bundle. Importing THIS subpath is the explicit
|
|
9
|
+
* opt-in, and it is what `enigma add search` sets a project up for - it installs fuse.js
|
|
10
|
+
* alongside the package.
|
|
11
|
+
*
|
|
12
|
+
* Everything else is identical to `createSearch` from the main entry: pass `fuseOptions`
|
|
13
|
+
* to configure Fuse, or `matcher` to replace it outright.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Search as you type, fuzzy by default.
|
|
18
|
+
*
|
|
19
|
+
* ```js
|
|
20
|
+
* import { createSearch } from "@enigmax/primitives/search";
|
|
21
|
+
*
|
|
22
|
+
* const search = createSearch({ items: docs, keys: ["title", "body"] });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Pass `fuse: undefined` explicitly to fall back to the built-in substring matcher.
|
|
26
|
+
*/
|
|
27
|
+
declare function createSearch<T>(options?: SearchOptions<T>): SearchInstance<T>;
|
|
28
|
+
|
|
29
|
+
export { SearchInstance, SearchOptions, createSearch };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createSearch } from '../chunk-UZFEEFMF.js';
|
|
2
|
+
import Fuse from 'fuse.js';
|
|
3
|
+
|
|
4
|
+
function createSearch2(options = {}) {
|
|
5
|
+
return createSearch({
|
|
6
|
+
// A caller-supplied `fuse` or `matcher` still wins; this only fills the blank.
|
|
7
|
+
fuse: Fuse,
|
|
8
|
+
...options
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { createSearch2 as createSearch };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search-as-you-type: debouncing, ordering, cancellation and the field wiring, with the
|
|
3
|
+
* matching left pluggable.
|
|
4
|
+
*
|
|
5
|
+
* Fuse.js is NOT a dependency of this package. Hand it the constructor and you get fuzzy
|
|
6
|
+
* matching configured exactly as if you had called it yourself - `fuseOptions` is passed
|
|
7
|
+
* through untouched. Hand it nothing and a built-in accent-insensitive substring matcher
|
|
8
|
+
* runs instead, so the primitive works with zero dependencies. Hand it a `matcher` and
|
|
9
|
+
* every part of this is replaced by your own.
|
|
10
|
+
*/
|
|
11
|
+
interface SearchMatch<T> {
|
|
12
|
+
item: T;
|
|
13
|
+
/** Lower is better, matching Fuse's convention. 0 is an exact hit. */
|
|
14
|
+
score: number;
|
|
15
|
+
/** Which key matched, when the matcher reports it. */
|
|
16
|
+
key?: string;
|
|
17
|
+
}
|
|
18
|
+
/** The shape of Fuse's constructor, declared here so the package need not depend on it. */
|
|
19
|
+
interface FuseLike<T> {
|
|
20
|
+
search(query: string): {
|
|
21
|
+
item: T;
|
|
22
|
+
score?: number;
|
|
23
|
+
matches?: {
|
|
24
|
+
key?: string;
|
|
25
|
+
}[];
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
28
|
+
type FuseConstructor = new <T>(items: readonly T[], options?: Record<string, unknown>) => FuseLike<T>;
|
|
29
|
+
interface SearchOptions<T> {
|
|
30
|
+
items?: readonly T[];
|
|
31
|
+
/** Fields to search. Dotted paths work with the built-in matcher and with Fuse. */
|
|
32
|
+
keys?: string[];
|
|
33
|
+
/**
|
|
34
|
+
* Pass Fuse.js's constructor to get fuzzy matching. Omit it for the built-in
|
|
35
|
+
* substring matcher. Ignored when `matcher` is set.
|
|
36
|
+
*/
|
|
37
|
+
fuse?: FuseConstructor;
|
|
38
|
+
/** Handed to Fuse verbatim, so any Fuse option behaves exactly as documented there. */
|
|
39
|
+
fuseOptions?: Record<string, unknown>;
|
|
40
|
+
/** Replaces the engine entirely. Return the results in the order you want them. */
|
|
41
|
+
matcher?: (query: string, items: readonly T[]) => SearchMatch<T>[];
|
|
42
|
+
/** ms to wait after the last keystroke. 0 searches on every one. */
|
|
43
|
+
debounce?: number;
|
|
44
|
+
/** Queries shorter than this return nothing rather than everything. */
|
|
45
|
+
minLength?: number;
|
|
46
|
+
/** Cap the result list. */
|
|
47
|
+
limit?: number;
|
|
48
|
+
/** What an empty query returns. "none" (default) or "all". */
|
|
49
|
+
empty?: "none" | "all";
|
|
50
|
+
onResults?: (results: SearchMatch<T>[], query: string) => void;
|
|
51
|
+
}
|
|
52
|
+
interface SearchInstance<T> {
|
|
53
|
+
readonly query: string;
|
|
54
|
+
readonly results: SearchMatch<T>[];
|
|
55
|
+
/** Debounced. */
|
|
56
|
+
search(query: string): void;
|
|
57
|
+
/** Skips the debounce, e.g. on Enter. */
|
|
58
|
+
searchNow(query: string): SearchMatch<T>[];
|
|
59
|
+
setItems(items: readonly T[]): void;
|
|
60
|
+
update(options: Partial<SearchOptions<T>>): void;
|
|
61
|
+
/** Bind to a field: input events search, Escape clears. Returns an unbind. */
|
|
62
|
+
attach(input: HTMLInputElement): () => void;
|
|
63
|
+
subscribe(listener: (results: SearchMatch<T>[], query: string) => void): () => void;
|
|
64
|
+
destroy(): void;
|
|
65
|
+
}
|
|
66
|
+
declare function createSearch<T>(options?: SearchOptions<T>): SearchInstance<T>;
|
|
67
|
+
|
|
68
|
+
export { type FuseConstructor as F, type SearchInstance as S, type FuseLike as a, type SearchMatch as b, type SearchOptions as c, createSearch as d };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Headless interaction primitives: the behaviour, the timing and the accessibility of components like a draggable infinite marquee, with no styles of their own. Framework-agnostic core plus thin adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
"types": "./dist/react/index.d.ts",
|
|
15
15
|
"import": "./dist/react/index.js"
|
|
16
16
|
},
|
|
17
|
-
"./registry.json": "./registry.json"
|
|
17
|
+
"./registry.json": "./registry.json",
|
|
18
|
+
"./search": {
|
|
19
|
+
"types": "./dist/search/index.d.ts",
|
|
20
|
+
"import": "./dist/search/index.js"
|
|
21
|
+
}
|
|
18
22
|
},
|
|
19
23
|
"scripts": {
|
|
20
24
|
"build": "tsup",
|
package/recipes/marquee.css.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./marquee.css";
|
|
2
2
|
import type { ReactNode, Ref } from "react";
|
|
3
|
-
import { useMarquee } from "@enigmax/primitives/react";
|
|
3
|
+
import { useMarquee, type MarqueeHover } from "@enigmax/primitives/react";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A styled marquee, yours to edit.
|
|
@@ -12,8 +12,12 @@ export interface MarqueeProps<T> {
|
|
|
12
12
|
items: T[];
|
|
13
13
|
/** Pixels per second. Never a duration - the row would speed up as items are added. */
|
|
14
14
|
speed?: number;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* What a mouse resting on the row does to its speed:
|
|
17
|
+
* "off" ignores hover, "pause" stops, a number multiplies the cruise speed
|
|
18
|
+
* (0.15 crawls, 2 doubles), { speed } sets an absolute px/s.
|
|
19
|
+
*/
|
|
20
|
+
hover?: MarqueeHover;
|
|
17
21
|
reverse?: boolean;
|
|
18
22
|
/** Fade both ends so items enter and leave instead of being cut. */
|
|
19
23
|
fade?: boolean;
|
|
@@ -24,13 +28,13 @@ export interface MarqueeProps<T> {
|
|
|
24
28
|
export function Marquee<T>({
|
|
25
29
|
items,
|
|
26
30
|
speed = 70,
|
|
27
|
-
|
|
31
|
+
hover = "off",
|
|
28
32
|
reverse = false,
|
|
29
33
|
fade = true,
|
|
30
34
|
className = "",
|
|
31
35
|
children
|
|
32
36
|
}: MarqueeProps<T>) {
|
|
33
|
-
const { laneRef, trackRef, copies, dragging } = useMarquee({ speed,
|
|
37
|
+
const { laneRef, trackRef, copies, dragging } = useMarquee({ speed, hover, reverse });
|
|
34
38
|
|
|
35
39
|
return (
|
|
36
40
|
<div
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode, Ref } from "react";
|
|
2
|
-
import { useMarquee } from "@enigmax/primitives/react";
|
|
2
|
+
import { useMarquee, type MarqueeHover } from "@enigmax/primitives/react";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A styled marquee, yours to edit.
|
|
@@ -16,8 +16,12 @@ export interface MarqueeProps<T> {
|
|
|
16
16
|
items: T[];
|
|
17
17
|
/** Pixels per second. Never a duration - the row would speed up as items are added. */
|
|
18
18
|
speed?: number;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* What a mouse resting on the row does to its speed:
|
|
21
|
+
* "off" ignores hover, "pause" stops, a number multiplies the cruise speed
|
|
22
|
+
* (0.15 crawls, 2 doubles), { speed } sets an absolute px/s.
|
|
23
|
+
*/
|
|
24
|
+
hover?: MarqueeHover;
|
|
21
25
|
reverse?: boolean;
|
|
22
26
|
/** Fade both ends so items enter and leave instead of being cut. */
|
|
23
27
|
fade?: boolean;
|
|
@@ -28,13 +32,13 @@ export interface MarqueeProps<T> {
|
|
|
28
32
|
export function Marquee<T>({
|
|
29
33
|
items,
|
|
30
34
|
speed = 70,
|
|
31
|
-
|
|
35
|
+
hover = "off",
|
|
32
36
|
reverse = false,
|
|
33
37
|
fade = true,
|
|
34
38
|
className = "",
|
|
35
39
|
children
|
|
36
40
|
}: MarqueeProps<T>) {
|
|
37
|
-
const { laneRef, trackRef, copies, dragging } = useMarquee({ speed,
|
|
41
|
+
const { laneRef, trackRef, copies, dragging } = useMarquee({ speed, hover, reverse });
|
|
38
42
|
|
|
39
43
|
return (
|
|
40
44
|
<div
|
package/registry.json
CHANGED
|
@@ -180,15 +180,15 @@
|
|
|
180
180
|
{
|
|
181
181
|
"name": "search",
|
|
182
182
|
"title": "Search as you type",
|
|
183
|
-
"description": "
|
|
183
|
+
"description": "Search as you type, fuzzy by default with Fuse.js. Debouncing, ranking and cancellation are the primitive's; the matching is replaceable, and dropping Fuse falls back to a built-in accent-insensitive substring matcher.",
|
|
184
184
|
"targets": [
|
|
185
185
|
"vanilla",
|
|
186
186
|
"astro",
|
|
187
187
|
"react"
|
|
188
188
|
],
|
|
189
189
|
"entry": {
|
|
190
|
-
"vanilla": "@enigmax/primitives",
|
|
191
|
-
"astro": "@enigmax/primitives",
|
|
190
|
+
"vanilla": "@enigmax/primitives/search",
|
|
191
|
+
"astro": "@enigmax/primitives/search",
|
|
192
192
|
"react": "@enigmax/primitives/react"
|
|
193
193
|
},
|
|
194
194
|
"exports": {
|
|
@@ -241,7 +241,10 @@
|
|
|
241
241
|
"docs": "docs/notes/primitives.md#search",
|
|
242
242
|
"recipes": [
|
|
243
243
|
"tailwind"
|
|
244
|
-
]
|
|
244
|
+
],
|
|
245
|
+
"dependencies": {
|
|
246
|
+
"fuse.js": "^7.0.0"
|
|
247
|
+
}
|
|
245
248
|
}
|
|
246
249
|
]
|
|
247
250
|
}
|
package/src/core/marquee.ts
CHANGED
|
@@ -29,6 +29,20 @@ const TOUCH_HOVER_SUPPRESS_MS = 1000;
|
|
|
29
29
|
/** Below this the row is idle and the frame loop can stop. */
|
|
30
30
|
const IDLE_EPSILON = 0.01;
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* What a MOUSE resting on the row does to its speed. One option covers every case, so
|
|
34
|
+
* there is no pile of booleans to reconcile:
|
|
35
|
+
*
|
|
36
|
+
* - `"off"` ignore hover entirely (the default)
|
|
37
|
+
* - `"pause"` stop while the pointer is on it
|
|
38
|
+
* - a number multiply the cruise speed: `0.15` crawls, `1` changes nothing, `2` doubles
|
|
39
|
+
* - `{ speed }` an ABSOLUTE speed in px/s, whatever the cruise is; direction still
|
|
40
|
+
* follows `reverse`, so you give a magnitude and not a sign
|
|
41
|
+
*
|
|
42
|
+
* Touch never triggers any of it - see the pointerType note on the hover handler.
|
|
43
|
+
*/
|
|
44
|
+
export type MarqueeHover = number | "off" | "pause" | { speed: number; };
|
|
45
|
+
|
|
32
46
|
export interface MarqueeOptions {
|
|
33
47
|
/**
|
|
34
48
|
* Pixels per second. NOT a duration: a duration ties the speed to the item
|
|
@@ -41,8 +55,8 @@ export interface MarqueeOptions {
|
|
|
41
55
|
vertical?: boolean;
|
|
42
56
|
/** Allow grabbing and throwing the row. */
|
|
43
57
|
draggable?: boolean;
|
|
44
|
-
/**
|
|
45
|
-
|
|
58
|
+
/** What a mouse resting on the row does to its speed. See {@link MarqueeHover}. */
|
|
59
|
+
hover?: MarqueeHover;
|
|
46
60
|
/** Fraction of the remaining velocity gap left after one second. */
|
|
47
61
|
decay?: number;
|
|
48
62
|
/**
|
|
@@ -94,7 +108,7 @@ const DEFAULTS: ResolvedOptions = {
|
|
|
94
108
|
reverse: false,
|
|
95
109
|
vertical: false,
|
|
96
110
|
draggable: true,
|
|
97
|
-
|
|
111
|
+
hover: "off",
|
|
98
112
|
decay: DEFAULT_DECAY,
|
|
99
113
|
manageStyles: true,
|
|
100
114
|
copies: "clone"
|
|
@@ -134,11 +148,21 @@ export function createMarquee(lane: HTMLElement, track: HTMLElement, options: Ma
|
|
|
134
148
|
: null;
|
|
135
149
|
let reducedMotion = motionQuery?.matches ?? false;
|
|
136
150
|
|
|
151
|
+
/** The speed a mouse on the row asks for. One option, four shapes, no flags to reconcile. */
|
|
152
|
+
function hoverTarget(cruise: number): number {
|
|
153
|
+
const hover = opts.hover ?? "off";
|
|
154
|
+
if (hover === "off") return cruise;
|
|
155
|
+
if (hover === "pause") return 0;
|
|
156
|
+
if (typeof hover === "number") return cruise * hover;
|
|
157
|
+
// An absolute speed is a magnitude: the row keeps travelling the way it was.
|
|
158
|
+
return (cruise < 0 ? -1 : 1) * Math.abs(hover.speed);
|
|
159
|
+
}
|
|
160
|
+
|
|
137
161
|
/** Read live every frame, never captured: cruise, hover and reduced motion all ease. */
|
|
138
162
|
function target(): number {
|
|
139
163
|
if (reducedMotion || paused) return 0;
|
|
140
164
|
const cruise = opts.reverse ? -opts.speed : opts.speed;
|
|
141
|
-
return hovering ? cruise
|
|
165
|
+
return hovering ? hoverTarget(cruise) : cruise;
|
|
142
166
|
}
|
|
143
167
|
|
|
144
168
|
function axisSize(element: HTMLElement): number {
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { createMarquee, type MarqueeOptions, type MarqueeInstance } from "@/core/marquee";
|
|
1
|
+
export { createMarquee, type MarqueeOptions, type MarqueeInstance, type MarqueeHover } from "@/core/marquee";
|
|
2
2
|
export { createInput, type InputOptions, type InputInstance, type InputAction, type InputIcon, type InputActionState } from "@/core/input";
|
|
3
3
|
export { createSearch, type SearchOptions, type SearchInstance, type SearchMatch, type FuseConstructor, type FuseLike } from "@/core/search";
|
package/src/react/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { useMarquee, type UseMarqueeResult } from "@/react/use-marquee";
|
|
2
|
-
export { type MarqueeOptions, type MarqueeInstance } from "@/core/marquee";
|
|
2
|
+
export { type MarqueeOptions, type MarqueeInstance, type MarqueeHover } from "@/core/marquee";
|
|
3
3
|
export { useInput, type UseInputResult } from "@/react/use-input";
|
|
4
4
|
export { useSearch, type UseSearchResult } from "@/react/use-search";
|
|
5
5
|
export { type InputOptions, type InputAction, type InputIcon } from "@/core/input";
|
package/src/react/use-marquee.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface UseMarqueeResult {
|
|
|
29
29
|
* to the item count, so the row accelerates every time content is added.
|
|
30
30
|
*
|
|
31
31
|
* ```tsx
|
|
32
|
-
* const { laneRef, trackRef, copies } = useMarquee({ speed: 80,
|
|
32
|
+
* const { laneRef, trackRef, copies } = useMarquee({ speed: 80, hover: 0.15 });
|
|
33
33
|
* return (
|
|
34
34
|
* <div ref={laneRef}>
|
|
35
35
|
* <div ref={trackRef} style={{ display: "flex" }}>
|
|
@@ -87,7 +87,7 @@ export function useMarquee(options: MarqueeOptions = {}): UseMarqueeResult {
|
|
|
87
87
|
|
|
88
88
|
useIsomorphicLayoutEffect(() => {
|
|
89
89
|
instanceRef.current?.update(options);
|
|
90
|
-
}, [options.speed, options.reverse, options.vertical, options.draggable, options.
|
|
90
|
+
}, [options.speed, options.reverse, options.vertical, options.draggable, options.hover, options.decay]);
|
|
91
91
|
|
|
92
92
|
// A copy count change means new children; the lap has to be read again.
|
|
93
93
|
useIsomorphicLayoutEffect(() => {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search with Fuse.js already wired: the batteries-included entry point.
|
|
3
|
+
*
|
|
4
|
+
* `@enigmax/primitives` stays dependency-free, so someone who only wants the marquee
|
|
5
|
+
* never pulls a search engine into their bundle. Importing THIS subpath is the explicit
|
|
6
|
+
* opt-in, and it is what `enigma add search` sets a project up for - it installs fuse.js
|
|
7
|
+
* alongside the package.
|
|
8
|
+
*
|
|
9
|
+
* Everything else is identical to `createSearch` from the main entry: pass `fuseOptions`
|
|
10
|
+
* to configure Fuse, or `matcher` to replace it outright.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import Fuse from "fuse.js";
|
|
14
|
+
import { createSearch as createBaseSearch, type SearchOptions, type SearchInstance } from "@/core/search";
|
|
15
|
+
|
|
16
|
+
export type { SearchOptions, SearchInstance, SearchMatch } from "@/core/search";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Search as you type, fuzzy by default.
|
|
20
|
+
*
|
|
21
|
+
* ```js
|
|
22
|
+
* import { createSearch } from "@enigmax/primitives/search";
|
|
23
|
+
*
|
|
24
|
+
* const search = createSearch({ items: docs, keys: ["title", "body"] });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Pass `fuse: undefined` explicitly to fall back to the built-in substring matcher.
|
|
28
|
+
*/
|
|
29
|
+
export function createSearch<T>(options: SearchOptions<T> = {}): SearchInstance<T> {
|
|
30
|
+
return createBaseSearch<T>({
|
|
31
|
+
// A caller-supplied `fuse` or `matcher` still wins; this only fills the blank.
|
|
32
|
+
fuse: Fuse as never,
|
|
33
|
+
...options
|
|
34
|
+
});
|
|
35
|
+
}
|