@enigmax/primitives 0.2.0 → 0.4.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-UZFEEFMF.js +145 -0
- package/dist/{chunk-OKNWKBQI.js → chunk-W2DRHYC2.js} +1 -145
- package/dist/index.d.ts +3 -68
- package/dist/index.js +2 -1
- package/dist/react/index.d.ts +4 -2
- package/dist/react/index.js +2 -1
- 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 +7 -2
- package/recipes/input.css +32 -0
- package/recipes/input.css.tsx +28 -0
- package/recipes/input.tailwind.tsx +44 -0
- package/recipes/marquee.css +22 -0
- package/recipes/marquee.css.tsx +52 -0
- package/recipes/marquee.tailwind.tsx +61 -0
- package/recipes/search.tailwind.tsx +73 -0
- package/registry.json +80 -6
- package/src/search/index.ts +35 -0
|
@@ -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 };
|
|
@@ -470,148 +470,4 @@ function createInput(input, options = {}) {
|
|
|
470
470
|
};
|
|
471
471
|
}
|
|
472
472
|
|
|
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 };
|
|
473
|
+
export { createInput, createMarquee };
|
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
|
*
|
|
@@ -145,71 +147,4 @@ interface InputInstance {
|
|
|
145
147
|
*/
|
|
146
148
|
declare function createInput(input: HTMLInputElement, options?: InputOptions): InputInstance;
|
|
147
149
|
|
|
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 };
|
|
150
|
+
export { type InputAction, type InputActionState, type InputIcon, type InputInstance, type InputOptions, type MarqueeInstance, type MarqueeOptions, createInput, createMarquee };
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { createInput, createMarquee
|
|
1
|
+
export { createInput, createMarquee } from './chunk-W2DRHYC2.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, 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. */
|
package/dist/react/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createMarquee, createInput
|
|
1
|
+
import { createMarquee, createInput } from '../chunk-W2DRHYC2.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;
|
|
@@ -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.4.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",
|
|
@@ -26,6 +30,7 @@
|
|
|
26
30
|
"files": [
|
|
27
31
|
"dist",
|
|
28
32
|
"src",
|
|
33
|
+
"recipes",
|
|
29
34
|
"registry.json",
|
|
30
35
|
"README.md",
|
|
31
36
|
"LICENSE"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* A starting point for the field, yours to edit. The primitive creates the action
|
|
2
|
+
buttons and publishes their state; it decides nothing about how they look. */
|
|
3
|
+
|
|
4
|
+
.enigma-field { display: grid; gap: 0.375rem; }
|
|
5
|
+
.enigma-field__label { font-size: 0.75rem; color: #a3a3a3; }
|
|
6
|
+
|
|
7
|
+
.enigma-field__box {
|
|
8
|
+
display: flex; align-items: center; gap: 0.375rem;
|
|
9
|
+
padding: 0 0.75rem;
|
|
10
|
+
background: #171717; border: 1px solid #404040; border-radius: 0.5rem;
|
|
11
|
+
}
|
|
12
|
+
.enigma-field__box:focus-within { border-color: #a3a3a3; }
|
|
13
|
+
|
|
14
|
+
.enigma-field__input {
|
|
15
|
+
flex: 1; min-width: 0; padding: 0.625rem 0;
|
|
16
|
+
font-size: 0.875rem; color: #f5f5f5;
|
|
17
|
+
background: transparent; border: 0; outline: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* The engine's buttons. Styled through its hooks, never through a className. */
|
|
21
|
+
[data-enigma-input-actions] { display: inline-flex; gap: 0.125rem; }
|
|
22
|
+
[data-enigma-input-actions][data-position="start"] { order: -1; }
|
|
23
|
+
|
|
24
|
+
[data-enigma-input-action] {
|
|
25
|
+
display: grid; place-items: center;
|
|
26
|
+
width: 1.75rem; height: 1.75rem;
|
|
27
|
+
color: #a3a3a3; background: none; border: 0; border-radius: 0.375rem;
|
|
28
|
+
cursor: pointer; font-size: 0.9375rem;
|
|
29
|
+
}
|
|
30
|
+
[data-enigma-input-action]:hover { color: #f5f5f5; background: #262626; }
|
|
31
|
+
[data-enigma-input-action][aria-pressed="true"] { color: #fbbf24; }
|
|
32
|
+
[data-enigma-input-action]:disabled { opacity: 0.5; cursor: default; }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import "./input.css";
|
|
2
|
+
import type { InputHTMLAttributes } from "react";
|
|
3
|
+
import { useInput } from "@enigmax/primitives/react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A styled field, yours to edit.
|
|
7
|
+
*
|
|
8
|
+
* The reveal toggle, its accessible name, its pressed state and the caret handling all
|
|
9
|
+
* come from the primitive. input.css beside this file is a starting point.
|
|
10
|
+
*/
|
|
11
|
+
export interface FieldProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Remove the automatic password reveal. */
|
|
14
|
+
reveal?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Field({ label, reveal = true, className = "", ...props }: FieldProps) {
|
|
18
|
+
const { inputRef } = useInput({ reveal });
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<label className="enigma-field">
|
|
22
|
+
{label && <span className="enigma-field__label">{label}</span>}
|
|
23
|
+
<span className="enigma-field__box">
|
|
24
|
+
<input ref={inputRef} className={`enigma-field__input ${className}`.trim()} {...props} />
|
|
25
|
+
</span>
|
|
26
|
+
</label>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { InputHTMLAttributes } from "react";
|
|
2
|
+
import { useInput } from "@enigmax/primitives/react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A styled field, yours to edit.
|
|
6
|
+
*
|
|
7
|
+
* The reveal toggle, its accessible name, its pressed state and the caret handling all
|
|
8
|
+
* come from the primitive. Every class below is a suggestion.
|
|
9
|
+
*
|
|
10
|
+
* The action buttons are created by the engine, so they are styled through the
|
|
11
|
+
* data-* hooks rather than a className prop - hence the arbitrary-variant selectors.
|
|
12
|
+
*/
|
|
13
|
+
export interface FieldProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
14
|
+
label?: string;
|
|
15
|
+
/** Remove the automatic password reveal. */
|
|
16
|
+
reveal?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Field({ label, reveal = true, className = "", ...props }: FieldProps) {
|
|
20
|
+
const { inputRef } = useInput({ reveal });
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<label className="grid gap-1.5">
|
|
24
|
+
{label && <span className="text-xs text-neutral-400">{label}</span>}
|
|
25
|
+
<span
|
|
26
|
+
className="
|
|
27
|
+
flex items-center gap-1.5 rounded-lg border border-neutral-700 bg-neutral-900 px-3
|
|
28
|
+
focus-within:border-neutral-400
|
|
29
|
+
[&_[data-enigma-input-actions]]:inline-flex
|
|
30
|
+
[&_[data-enigma-input-action]]:grid [&_[data-enigma-input-action]]:size-7 [&_[data-enigma-input-action]]:place-items-center
|
|
31
|
+
[&_[data-enigma-input-action]]:rounded-md [&_[data-enigma-input-action]]:text-neutral-400
|
|
32
|
+
[&_[data-enigma-input-action]:hover]:bg-neutral-800 [&_[data-enigma-input-action]:hover]:text-neutral-100
|
|
33
|
+
[&_[data-enigma-input-action][aria-pressed=true]]:text-amber-400
|
|
34
|
+
"
|
|
35
|
+
>
|
|
36
|
+
<input
|
|
37
|
+
ref={inputRef}
|
|
38
|
+
className={`min-w-0 flex-1 bg-transparent py-2.5 text-sm text-neutral-100 outline-none placeholder:text-neutral-500 ${className}`}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
</span>
|
|
42
|
+
</label>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* A starting point for the marquee, yours to edit. The primitive ships no styles;
|
|
2
|
+
it sets only what the behaviour needs (overflow, touch-action, user-select,
|
|
3
|
+
will-change, transform) and publishes its state as data-* attributes. */
|
|
4
|
+
|
|
5
|
+
.enigma-marquee { position: relative; width: 100%; cursor: grab; }
|
|
6
|
+
.enigma-marquee[data-grabbing] { cursor: grabbing; }
|
|
7
|
+
|
|
8
|
+
/* Fade both ends so items enter and leave instead of being cut. */
|
|
9
|
+
.enigma-marquee.is-faded {
|
|
10
|
+
-webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
|
|
11
|
+
mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Never set `transform` or `translate` on the track: the engine owns it, and in
|
|
15
|
+
Tailwind v4 a translate-* utility composes with it rather than replacing it. */
|
|
16
|
+
.enigma-marquee__track { display: flex; }
|
|
17
|
+
.enigma-marquee__copy { display: flex; flex-shrink: 0; align-items: center; gap: 2.5rem; padding-right: 2.5rem; }
|
|
18
|
+
.enigma-marquee__item { flex-shrink: 0; }
|
|
19
|
+
|
|
20
|
+
/* State the engine publishes, for you to style. */
|
|
21
|
+
.enigma-marquee[data-hovering] .enigma-marquee__item { opacity: 1; }
|
|
22
|
+
.enigma-marquee[data-reduced-motion="true"] { /* autoplay is off; the drag still works */ }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "./marquee.css";
|
|
2
|
+
import type { ReactNode, Ref } from "react";
|
|
3
|
+
import { useMarquee } from "@enigmax/primitives/react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A styled marquee, yours to edit.
|
|
7
|
+
*
|
|
8
|
+
* The behaviour comes from the primitive, which ships no styles at all; marquee.css
|
|
9
|
+
* beside this file is a starting point you are meant to change.
|
|
10
|
+
*/
|
|
11
|
+
export interface MarqueeProps<T> {
|
|
12
|
+
items: T[];
|
|
13
|
+
/** Pixels per second. Never a duration - the row would speed up as items are added. */
|
|
14
|
+
speed?: number;
|
|
15
|
+
/** Speed multiplier while a mouse rests on the row. 0 pauses it. */
|
|
16
|
+
hoverScale?: number;
|
|
17
|
+
reverse?: boolean;
|
|
18
|
+
/** Fade both ends so items enter and leave instead of being cut. */
|
|
19
|
+
fade?: boolean;
|
|
20
|
+
className?: string;
|
|
21
|
+
children: (item: T, index: number) => ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Marquee<T>({
|
|
25
|
+
items,
|
|
26
|
+
speed = 70,
|
|
27
|
+
hoverScale = 1,
|
|
28
|
+
reverse = false,
|
|
29
|
+
fade = true,
|
|
30
|
+
className = "",
|
|
31
|
+
children
|
|
32
|
+
}: MarqueeProps<T>) {
|
|
33
|
+
const { laneRef, trackRef, copies, dragging } = useMarquee({ speed, hoverScale, reverse });
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
ref={laneRef as Ref<HTMLDivElement>}
|
|
38
|
+
className={["enigma-marquee", fade ? "is-faded" : "", className].filter(Boolean).join(" ")}
|
|
39
|
+
data-grabbing={dragging ? "" : undefined}
|
|
40
|
+
>
|
|
41
|
+
<div ref={trackRef as Ref<HTMLDivElement>} className="enigma-marquee__track">
|
|
42
|
+
{Array.from({ length: copies }, (_, copy) => (
|
|
43
|
+
<div key={copy} aria-hidden={copy > 0} className="enigma-marquee__copy">
|
|
44
|
+
{items.map((item, index) => (
|
|
45
|
+
<div key={index} className="enigma-marquee__item">{children(item, index)}</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode, Ref } from "react";
|
|
2
|
+
import { useMarquee } from "@enigmax/primitives/react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A styled marquee, yours to edit.
|
|
6
|
+
*
|
|
7
|
+
* The behaviour comes from the primitive, which ships no styles at all; every class
|
|
8
|
+
* below is a suggestion you are meant to change.
|
|
9
|
+
*
|
|
10
|
+
* TAILWIND v4 WARNING: never put a `translate-*` utility on the track. v4 writes those
|
|
11
|
+
* to the CSS `translate` property, which COMPOSES with `transform` rather than replacing
|
|
12
|
+
* it, so the class and the engine's transform add up and the row drifts. Anything you
|
|
13
|
+
* need to offset goes on the lane or on an inner element, never on the moved one.
|
|
14
|
+
*/
|
|
15
|
+
export interface MarqueeProps<T> {
|
|
16
|
+
items: T[];
|
|
17
|
+
/** Pixels per second. Never a duration - the row would speed up as items are added. */
|
|
18
|
+
speed?: number;
|
|
19
|
+
/** Speed multiplier while a mouse rests on the row. 0 pauses it. */
|
|
20
|
+
hoverScale?: number;
|
|
21
|
+
reverse?: boolean;
|
|
22
|
+
/** Fade both ends so items enter and leave instead of being cut. */
|
|
23
|
+
fade?: boolean;
|
|
24
|
+
className?: string;
|
|
25
|
+
children: (item: T, index: number) => ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Marquee<T>({
|
|
29
|
+
items,
|
|
30
|
+
speed = 70,
|
|
31
|
+
hoverScale = 1,
|
|
32
|
+
reverse = false,
|
|
33
|
+
fade = true,
|
|
34
|
+
className = "",
|
|
35
|
+
children
|
|
36
|
+
}: MarqueeProps<T>) {
|
|
37
|
+
const { laneRef, trackRef, copies, dragging } = useMarquee({ speed, hoverScale, reverse });
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
ref={laneRef as Ref<HTMLDivElement>}
|
|
42
|
+
className={[
|
|
43
|
+
"relative w-full",
|
|
44
|
+
dragging ? "cursor-grabbing" : "cursor-grab",
|
|
45
|
+
fade ? "[mask-image:linear-gradient(90deg,transparent,#000_6%,#000_94%,transparent)]" : "",
|
|
46
|
+
className
|
|
47
|
+
].filter(Boolean).join(" ")}
|
|
48
|
+
>
|
|
49
|
+
{/* No translate-* utility here. See the note at the top of this file. */}
|
|
50
|
+
<div ref={trackRef as Ref<HTMLDivElement>} className="flex">
|
|
51
|
+
{Array.from({ length: copies }, (_, copy) => (
|
|
52
|
+
<div key={copy} aria-hidden={copy > 0} className="flex shrink-0 items-center gap-10 pr-10">
|
|
53
|
+
{items.map((item, index) => (
|
|
54
|
+
<div key={index} className="shrink-0">{children(item, index)}</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Fuse from "fuse.js";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useSearch } from "@enigmax/primitives/react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A styled search field with its result list, yours to edit.
|
|
7
|
+
*
|
|
8
|
+
* Debouncing, ranking, cancellation and the Escape-to-clear all come from the primitive.
|
|
9
|
+
* Drop the Fuse import and the `fuse` prop to fall back to the built-in
|
|
10
|
+
* accent-insensitive substring matcher and no dependency at all.
|
|
11
|
+
*/
|
|
12
|
+
export interface SearchProps<T> {
|
|
13
|
+
items: T[];
|
|
14
|
+
/** Fields to search. Dotted paths work: "author.name". */
|
|
15
|
+
keys: string[];
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
/** Fuzzy matching. Omit for the zero-dependency substring matcher. */
|
|
18
|
+
fuzzy?: boolean;
|
|
19
|
+
emptyMessage?: string;
|
|
20
|
+
children: (item: T, score: number) => ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Search<T>({
|
|
24
|
+
items,
|
|
25
|
+
keys,
|
|
26
|
+
placeholder = "Search",
|
|
27
|
+
fuzzy = true,
|
|
28
|
+
emptyMessage = "No matches",
|
|
29
|
+
children
|
|
30
|
+
}: SearchProps<T>) {
|
|
31
|
+
const { inputRef, results, query } = useSearch<T>({
|
|
32
|
+
items,
|
|
33
|
+
keys,
|
|
34
|
+
fuse: fuzzy ? (Fuse as never) : undefined,
|
|
35
|
+
debounce: 120
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="grid gap-2.5">
|
|
40
|
+
<input
|
|
41
|
+
ref={inputRef}
|
|
42
|
+
type="search"
|
|
43
|
+
aria-label={placeholder}
|
|
44
|
+
placeholder={placeholder}
|
|
45
|
+
className="
|
|
46
|
+
rounded-lg border border-neutral-700 bg-neutral-900 px-3 py-2.5
|
|
47
|
+
text-sm text-neutral-100 outline-none
|
|
48
|
+
placeholder:text-neutral-500 focus:border-neutral-400
|
|
49
|
+
"
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
{query && (
|
|
53
|
+
<p className="font-mono text-[11px] text-neutral-500">
|
|
54
|
+
{results.length} result{results.length === 1 ? "" : "s"} for "{query}"
|
|
55
|
+
</p>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
<ul className="grid list-none gap-1.5 p-0">
|
|
59
|
+
{results.map((result, index) => (
|
|
60
|
+
<li
|
|
61
|
+
key={index}
|
|
62
|
+
className="rounded-lg border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm text-neutral-100"
|
|
63
|
+
>
|
|
64
|
+
{children(result.item, result.score)}
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
{query && results.length === 0 && (
|
|
68
|
+
<li className="text-sm text-neutral-500">{emptyMessage}</li>
|
|
69
|
+
)}
|
|
70
|
+
</ul>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
package/registry.json
CHANGED
|
@@ -47,6 +47,32 @@
|
|
|
47
47
|
"rewrite": {
|
|
48
48
|
"@/core/marquee": "./marquee"
|
|
49
49
|
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"path": "recipes/marquee.tailwind.tsx",
|
|
53
|
+
"dest": "Marquee.tsx",
|
|
54
|
+
"targets": [
|
|
55
|
+
"react"
|
|
56
|
+
],
|
|
57
|
+
"style": "tailwind"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"path": "recipes/marquee.css.tsx",
|
|
61
|
+
"dest": "Marquee.tsx",
|
|
62
|
+
"targets": [
|
|
63
|
+
"react"
|
|
64
|
+
],
|
|
65
|
+
"style": "css"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"path": "recipes/marquee.css",
|
|
69
|
+
"dest": "marquee.css",
|
|
70
|
+
"targets": [
|
|
71
|
+
"react",
|
|
72
|
+
"vanilla",
|
|
73
|
+
"astro"
|
|
74
|
+
],
|
|
75
|
+
"style": "css"
|
|
50
76
|
}
|
|
51
77
|
],
|
|
52
78
|
"styles": false,
|
|
@@ -58,7 +84,11 @@
|
|
|
58
84
|
"[data-enigma-marquee-track]",
|
|
59
85
|
"[data-enigma-marquee-copy]"
|
|
60
86
|
],
|
|
61
|
-
"docs": "docs/notes/primitives.md#marquee"
|
|
87
|
+
"docs": "docs/notes/primitives.md#marquee",
|
|
88
|
+
"recipes": [
|
|
89
|
+
"tailwind",
|
|
90
|
+
"css"
|
|
91
|
+
]
|
|
62
92
|
},
|
|
63
93
|
{
|
|
64
94
|
"name": "input",
|
|
@@ -104,6 +134,32 @@
|
|
|
104
134
|
"rewrite": {
|
|
105
135
|
"@/core/input": "./input"
|
|
106
136
|
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"path": "recipes/input.tailwind.tsx",
|
|
140
|
+
"dest": "Field.tsx",
|
|
141
|
+
"targets": [
|
|
142
|
+
"react"
|
|
143
|
+
],
|
|
144
|
+
"style": "tailwind"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"path": "recipes/input.css.tsx",
|
|
148
|
+
"dest": "Field.tsx",
|
|
149
|
+
"targets": [
|
|
150
|
+
"react"
|
|
151
|
+
],
|
|
152
|
+
"style": "css"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"path": "recipes/input.css",
|
|
156
|
+
"dest": "input.css",
|
|
157
|
+
"targets": [
|
|
158
|
+
"react",
|
|
159
|
+
"vanilla",
|
|
160
|
+
"astro"
|
|
161
|
+
],
|
|
162
|
+
"style": "css"
|
|
107
163
|
}
|
|
108
164
|
],
|
|
109
165
|
"styles": false,
|
|
@@ -115,20 +171,24 @@
|
|
|
115
171
|
"[data-enigma-input-action]",
|
|
116
172
|
"[data-enigma-input-action=reveal]"
|
|
117
173
|
],
|
|
118
|
-
"docs": "docs/notes/primitives.md#input"
|
|
174
|
+
"docs": "docs/notes/primitives.md#input",
|
|
175
|
+
"recipes": [
|
|
176
|
+
"tailwind",
|
|
177
|
+
"css"
|
|
178
|
+
]
|
|
119
179
|
},
|
|
120
180
|
{
|
|
121
181
|
"name": "search",
|
|
122
182
|
"title": "Search as you type",
|
|
123
|
-
"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.",
|
|
124
184
|
"targets": [
|
|
125
185
|
"vanilla",
|
|
126
186
|
"astro",
|
|
127
187
|
"react"
|
|
128
188
|
],
|
|
129
189
|
"entry": {
|
|
130
|
-
"vanilla": "@enigmax/primitives",
|
|
131
|
-
"astro": "@enigmax/primitives",
|
|
190
|
+
"vanilla": "@enigmax/primitives/search",
|
|
191
|
+
"astro": "@enigmax/primitives/search",
|
|
132
192
|
"react": "@enigmax/primitives/react"
|
|
133
193
|
},
|
|
134
194
|
"exports": {
|
|
@@ -161,6 +221,14 @@
|
|
|
161
221
|
"rewrite": {
|
|
162
222
|
"@/core/search": "./search"
|
|
163
223
|
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"path": "recipes/search.tailwind.tsx",
|
|
227
|
+
"dest": "Search.tsx",
|
|
228
|
+
"targets": [
|
|
229
|
+
"react"
|
|
230
|
+
],
|
|
231
|
+
"style": "tailwind"
|
|
164
232
|
}
|
|
165
233
|
],
|
|
166
234
|
"styles": false,
|
|
@@ -170,7 +238,13 @@
|
|
|
170
238
|
"optionalDependencies": {
|
|
171
239
|
"fuse.js": "Fuzzy matching. Omit it and a built-in substring matcher is used."
|
|
172
240
|
},
|
|
173
|
-
"docs": "docs/notes/primitives.md#search"
|
|
241
|
+
"docs": "docs/notes/primitives.md#search",
|
|
242
|
+
"recipes": [
|
|
243
|
+
"tailwind"
|
|
244
|
+
],
|
|
245
|
+
"dependencies": {
|
|
246
|
+
"fuse.js": "^7.0.0"
|
|
247
|
+
}
|
|
174
248
|
}
|
|
175
249
|
]
|
|
176
250
|
}
|
|
@@ -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
|
+
}
|