@enigmax/primitives 0.8.0 → 0.10.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-L4KA342C.js +466 -0
- package/dist/next/index.d.ts +26 -0
- package/dist/next/index.js +13 -0
- package/dist/react/index.d.ts +42 -41
- package/dist/react/index.js +2 -457
- package/package.json +12 -3
- package/registry.json +10 -1
- package/src/next/index.tsx +32 -0
- package/src/react/button.tsx +8 -9
- package/src/react/index.ts +1 -0
- package/src/react/link.ts +45 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { generatePassword, estimatePasswordStrength, INPUT_ICON_PATHS, createButton, createMarquee, createInput } from './chunk-42Y7OOJM.js';
|
|
2
|
+
import { createSearch } from './chunk-UZFEEFMF.js';
|
|
3
|
+
import { forwardRef, createElement, useRef, useState, useId, useCallback, useEffect, useMemo, useLayoutEffect } from 'react';
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
7
|
+
function useMarquee(options = {}) {
|
|
8
|
+
const laneRef = useRef(null);
|
|
9
|
+
const trackRef = useRef(null);
|
|
10
|
+
const instanceRef = useRef(null);
|
|
11
|
+
const optionsRef = useRef(options);
|
|
12
|
+
optionsRef.current = options;
|
|
13
|
+
const [copies, setCopies] = useState(2);
|
|
14
|
+
const [dragging, setDragging] = useState(false);
|
|
15
|
+
const [period, setPeriod] = useState(0);
|
|
16
|
+
useIsomorphicLayoutEffect(() => {
|
|
17
|
+
const lane = laneRef.current;
|
|
18
|
+
const track = trackRef.current;
|
|
19
|
+
if (!lane || !track) return;
|
|
20
|
+
const instance = createMarquee(lane, track, {
|
|
21
|
+
...optionsRef.current,
|
|
22
|
+
// React owns the DOM; cloning behind its back would be undone on render.
|
|
23
|
+
copies: "external",
|
|
24
|
+
onCopyCountChange: setCopies,
|
|
25
|
+
onMeasure: (measured) => setPeriod(measured),
|
|
26
|
+
onItemClick: (event) => optionsRef.current.onItemClick?.(event)
|
|
27
|
+
});
|
|
28
|
+
instanceRef.current = instance;
|
|
29
|
+
const syncDragging = () => setDragging(instance.dragging);
|
|
30
|
+
lane.addEventListener("pointerdown", syncDragging);
|
|
31
|
+
window.addEventListener("pointerup", syncDragging);
|
|
32
|
+
window.addEventListener("pointercancel", syncDragging);
|
|
33
|
+
return () => {
|
|
34
|
+
lane.removeEventListener("pointerdown", syncDragging);
|
|
35
|
+
window.removeEventListener("pointerup", syncDragging);
|
|
36
|
+
window.removeEventListener("pointercancel", syncDragging);
|
|
37
|
+
instance.destroy();
|
|
38
|
+
instanceRef.current = null;
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
useIsomorphicLayoutEffect(() => {
|
|
42
|
+
instanceRef.current?.update(options);
|
|
43
|
+
}, [options.speed, options.reverse, options.vertical, options.draggable, options.hover, options.decay]);
|
|
44
|
+
useIsomorphicLayoutEffect(() => {
|
|
45
|
+
instanceRef.current?.measure();
|
|
46
|
+
}, [copies]);
|
|
47
|
+
return {
|
|
48
|
+
laneRef,
|
|
49
|
+
trackRef,
|
|
50
|
+
copies,
|
|
51
|
+
dragging,
|
|
52
|
+
period,
|
|
53
|
+
measure: () => instanceRef.current?.measure()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
var useIsomorphicLayoutEffect2 = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
57
|
+
function useInput(options = {}) {
|
|
58
|
+
const inputRef = useRef(null);
|
|
59
|
+
const actionsRef = useRef(null);
|
|
60
|
+
const instanceRef = useRef(null);
|
|
61
|
+
const optionsRef = useRef(options);
|
|
62
|
+
optionsRef.current = options;
|
|
63
|
+
const [revealed, setRevealed] = useState(false);
|
|
64
|
+
useIsomorphicLayoutEffect2(() => {
|
|
65
|
+
const input = inputRef.current;
|
|
66
|
+
if (!input) return;
|
|
67
|
+
const instance = createInput(input, {
|
|
68
|
+
...optionsRef.current,
|
|
69
|
+
container: actionsRef.current ?? optionsRef.current.container,
|
|
70
|
+
onRevealChange: (next) => {
|
|
71
|
+
setRevealed(next);
|
|
72
|
+
optionsRef.current.onRevealChange?.(next);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
instanceRef.current = instance;
|
|
76
|
+
return () => {
|
|
77
|
+
instance.destroy();
|
|
78
|
+
instanceRef.current = null;
|
|
79
|
+
};
|
|
80
|
+
}, []);
|
|
81
|
+
useIsomorphicLayoutEffect2(() => {
|
|
82
|
+
instanceRef.current?.update(options);
|
|
83
|
+
}, [options.reveal, options.actions, options.position]);
|
|
84
|
+
return {
|
|
85
|
+
inputRef,
|
|
86
|
+
actionsRef,
|
|
87
|
+
revealed,
|
|
88
|
+
reveal: (next) => instanceRef.current?.reveal(next),
|
|
89
|
+
refresh: () => instanceRef.current?.refresh()
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function useSearch(options = {}) {
|
|
93
|
+
const inputRef = useRef(null);
|
|
94
|
+
const instanceRef = useRef(null);
|
|
95
|
+
const optionsRef = useRef(options);
|
|
96
|
+
optionsRef.current = options;
|
|
97
|
+
const [query, setQuery] = useState("");
|
|
98
|
+
const [results, setResults] = useState([]);
|
|
99
|
+
const instance = useMemo(() => {
|
|
100
|
+
const created = createSearch({
|
|
101
|
+
...optionsRef.current,
|
|
102
|
+
onResults: (next, nextQuery) => {
|
|
103
|
+
setResults(next);
|
|
104
|
+
setQuery(nextQuery);
|
|
105
|
+
optionsRef.current.onResults?.(next, nextQuery);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
instanceRef.current = created;
|
|
109
|
+
return created;
|
|
110
|
+
}, []);
|
|
111
|
+
useEffect(() => () => instance.destroy(), [instance]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
instance.setItems(options.items ?? []);
|
|
114
|
+
}, [instance, options.items]);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const input = inputRef.current;
|
|
117
|
+
if (!input) return;
|
|
118
|
+
return instance.attach(input);
|
|
119
|
+
}, [instance]);
|
|
120
|
+
return {
|
|
121
|
+
inputRef,
|
|
122
|
+
query,
|
|
123
|
+
results,
|
|
124
|
+
search: (next) => instance.search(next),
|
|
125
|
+
searchNow: (next) => {
|
|
126
|
+
instance.searchNow(next);
|
|
127
|
+
},
|
|
128
|
+
clear: () => {
|
|
129
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
130
|
+
instance.searchNow("");
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function useButton(options = {}) {
|
|
135
|
+
const optionsRef = useRef(options);
|
|
136
|
+
optionsRef.current = options;
|
|
137
|
+
const instance = useMemo(() => createButton({
|
|
138
|
+
...optionsRef.current,
|
|
139
|
+
onPress: (event) => optionsRef.current.onPress?.(event),
|
|
140
|
+
onChange: (next) => optionsRef.current.onChange?.(next)
|
|
141
|
+
// Built once: recreating it would drop a running cooldown on every render.
|
|
142
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
|
+
}), []);
|
|
144
|
+
const [state, setState] = useState(() => instance.state);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const unsubscribe = instance.subscribe(setState);
|
|
147
|
+
setState(instance.state);
|
|
148
|
+
return () => {
|
|
149
|
+
unsubscribe();
|
|
150
|
+
instance.destroy();
|
|
151
|
+
};
|
|
152
|
+
}, [instance]);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
instance.update({
|
|
155
|
+
href: options.href,
|
|
156
|
+
disabled: options.disabled,
|
|
157
|
+
loading: options.loading,
|
|
158
|
+
cooldown: options.cooldown,
|
|
159
|
+
shortcut: options.shortcut
|
|
160
|
+
});
|
|
161
|
+
}, [instance, options.href, options.disabled, options.loading, options.cooldown, options.shortcut]);
|
|
162
|
+
const press = useCallback(() => {
|
|
163
|
+
void instance.press();
|
|
164
|
+
}, [instance]);
|
|
165
|
+
return {
|
|
166
|
+
...state,
|
|
167
|
+
press,
|
|
168
|
+
reset: () => instance.reset(),
|
|
169
|
+
props: {
|
|
170
|
+
onClick: (event) => {
|
|
171
|
+
if (!state.available) {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
void instance.press();
|
|
176
|
+
},
|
|
177
|
+
"aria-disabled": !state.available,
|
|
178
|
+
"aria-busy": state.loading,
|
|
179
|
+
...state.loading ? { "data-loading": "" } : {},
|
|
180
|
+
...state.cooldown > 0 ? { "data-cooldown": "" } : {},
|
|
181
|
+
...state.element === "button" ? { disabled: state.disabled } : { href: options.href },
|
|
182
|
+
...state.shortcut ? { title: `Shortcut: ${state.shortcut.toUpperCase()}` } : {}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/react/link.ts
|
|
188
|
+
var linkComponent = null;
|
|
189
|
+
function setLinkComponent(component) {
|
|
190
|
+
linkComponent = component;
|
|
191
|
+
}
|
|
192
|
+
function getLinkComponent() {
|
|
193
|
+
return linkComponent ?? "a";
|
|
194
|
+
}
|
|
195
|
+
var Button = forwardRef(function Button2({
|
|
196
|
+
as,
|
|
197
|
+
children,
|
|
198
|
+
pending,
|
|
199
|
+
href,
|
|
200
|
+
disabled,
|
|
201
|
+
loading,
|
|
202
|
+
cooldown,
|
|
203
|
+
shortcut,
|
|
204
|
+
onPress,
|
|
205
|
+
onChange,
|
|
206
|
+
type = "button",
|
|
207
|
+
...rest
|
|
208
|
+
}, ref) {
|
|
209
|
+
const button = useButton({ href, disabled, loading, cooldown, shortcut, onPress, onChange });
|
|
210
|
+
const state = button;
|
|
211
|
+
const Tag = as ?? (state.element === "a" ? getLinkComponent() : "button");
|
|
212
|
+
const native = Tag === "button" ? { type } : {};
|
|
213
|
+
const label = typeof children === "function" ? children(state) : children;
|
|
214
|
+
return createElement(
|
|
215
|
+
Tag,
|
|
216
|
+
{ ref, ...button.props, ...native, ...rest },
|
|
217
|
+
state.loading && pending !== void 0 ? pending : label
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
var ICON_PROPS = {
|
|
221
|
+
viewBox: "0 0 24 24",
|
|
222
|
+
width: "1em",
|
|
223
|
+
height: "1em",
|
|
224
|
+
fill: "none",
|
|
225
|
+
stroke: "currentColor",
|
|
226
|
+
strokeWidth: 2,
|
|
227
|
+
strokeLinecap: "round",
|
|
228
|
+
strokeLinejoin: "round",
|
|
229
|
+
"aria-hidden": true
|
|
230
|
+
};
|
|
231
|
+
function Icon({ paths }) {
|
|
232
|
+
return /* @__PURE__ */ jsx("svg", { ...ICON_PROPS, children: paths.map((path) => /* @__PURE__ */ jsx("path", { d: path }, path)) });
|
|
233
|
+
}
|
|
234
|
+
function writeValue(input, next) {
|
|
235
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
236
|
+
if (setter) setter.call(input, next);
|
|
237
|
+
else input.value = next;
|
|
238
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
239
|
+
}
|
|
240
|
+
var Input = forwardRef(function Input2({
|
|
241
|
+
reveal,
|
|
242
|
+
revealLabels,
|
|
243
|
+
generate = false,
|
|
244
|
+
generateLabel = "Generate a password",
|
|
245
|
+
revealOnGenerate = true,
|
|
246
|
+
copyOnGenerate = false,
|
|
247
|
+
onGenerate,
|
|
248
|
+
actions = [],
|
|
249
|
+
position = "end",
|
|
250
|
+
strength = false,
|
|
251
|
+
onStrengthChange,
|
|
252
|
+
breach,
|
|
253
|
+
breachDelay = 500,
|
|
254
|
+
onBreachChange,
|
|
255
|
+
wrapperProps,
|
|
256
|
+
fieldProps,
|
|
257
|
+
classNames,
|
|
258
|
+
children,
|
|
259
|
+
type = "text",
|
|
260
|
+
onChange,
|
|
261
|
+
...props
|
|
262
|
+
}, forwardedRef) {
|
|
263
|
+
const innerRef = useRef(null);
|
|
264
|
+
const [revealed, setRevealed] = useState(false);
|
|
265
|
+
const describedBy = useId();
|
|
266
|
+
const isPassword = type === "password";
|
|
267
|
+
const showReveal = reveal ?? isPassword;
|
|
268
|
+
const showGenerate = generate !== false && isPassword;
|
|
269
|
+
const controlled = props.value !== void 0;
|
|
270
|
+
const [ownValue, setOwnValue] = useState(String(props.defaultValue ?? ""));
|
|
271
|
+
const value = controlled ? String(props.value ?? "") : ownValue;
|
|
272
|
+
const handleChange = useCallback((event) => {
|
|
273
|
+
if (!controlled) setOwnValue(event.target.value);
|
|
274
|
+
onChange?.(event);
|
|
275
|
+
}, [controlled, onChange]);
|
|
276
|
+
const caret = useRef(null);
|
|
277
|
+
const toggleReveal = useCallback(() => {
|
|
278
|
+
const input = innerRef.current;
|
|
279
|
+
if (input && typeof document !== "undefined" && document.activeElement === input) {
|
|
280
|
+
try {
|
|
281
|
+
const { selectionStart, selectionEnd } = input;
|
|
282
|
+
caret.current = selectionStart === null || selectionEnd === null ? null : [selectionStart, selectionEnd];
|
|
283
|
+
} catch {
|
|
284
|
+
caret.current = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
setRevealed((current) => !current);
|
|
288
|
+
}, []);
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
const selection = caret.current;
|
|
291
|
+
if (!selection) return;
|
|
292
|
+
caret.current = null;
|
|
293
|
+
const restore = () => {
|
|
294
|
+
const input = innerRef.current;
|
|
295
|
+
if (!input || document.activeElement !== input) return;
|
|
296
|
+
try {
|
|
297
|
+
input.setSelectionRange(selection[0], selection[1]);
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
restore();
|
|
302
|
+
const timer = setTimeout(restore, 0);
|
|
303
|
+
return () => clearTimeout(timer);
|
|
304
|
+
}, [revealed]);
|
|
305
|
+
const handleGenerate = useCallback(() => {
|
|
306
|
+
const input = innerRef.current;
|
|
307
|
+
if (!input) return;
|
|
308
|
+
const password = generatePassword(typeof generate === "object" ? generate : {});
|
|
309
|
+
writeValue(input, password);
|
|
310
|
+
if (revealOnGenerate) setRevealed(true);
|
|
311
|
+
if (copyOnGenerate) void navigator.clipboard?.writeText(password).catch(() => {
|
|
312
|
+
});
|
|
313
|
+
input.focus();
|
|
314
|
+
onGenerate?.(password);
|
|
315
|
+
}, [generate, revealOnGenerate, copyOnGenerate, onGenerate]);
|
|
316
|
+
const estimateOptions = typeof strength === "object" ? strength : void 0;
|
|
317
|
+
const userInputs = estimateOptions?.userInputs;
|
|
318
|
+
const report = useMemo(
|
|
319
|
+
() => strength === false ? null : estimatePasswordStrength(value, { userInputs }),
|
|
320
|
+
// The array is compared by identity, so a literal would re-estimate every render.
|
|
321
|
+
[strength, value, userInputs]
|
|
322
|
+
);
|
|
323
|
+
const strengthListener = useRef(onStrengthChange);
|
|
324
|
+
strengthListener.current = onStrengthChange;
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
if (report) strengthListener.current?.(report);
|
|
327
|
+
}, [report]);
|
|
328
|
+
const [breachState, setBreachState] = useState({ status: "idle", count: 0, error: null });
|
|
329
|
+
const breachListener = useRef(onBreachChange);
|
|
330
|
+
breachListener.current = onBreachChange;
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
if (!breach || !value) {
|
|
333
|
+
setBreachState({ status: "idle", count: 0, error: null });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const controller = new AbortController();
|
|
337
|
+
const timer = setTimeout(() => {
|
|
338
|
+
setBreachState({ status: "checking", count: 0, error: null });
|
|
339
|
+
breach(value, { signal: controller.signal }).then((result) => {
|
|
340
|
+
if (controller.signal.aborted) return;
|
|
341
|
+
setBreachState({ status: result.breached ? "breached" : "safe", count: result.count, error: null });
|
|
342
|
+
}).catch((error) => {
|
|
343
|
+
if (controller.signal.aborted) return;
|
|
344
|
+
setBreachState({ status: "error", count: 0, error });
|
|
345
|
+
});
|
|
346
|
+
}, breachDelay);
|
|
347
|
+
return () => {
|
|
348
|
+
clearTimeout(timer);
|
|
349
|
+
controller.abort();
|
|
350
|
+
};
|
|
351
|
+
}, [breach, value, breachDelay]);
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
breachListener.current?.(breachState);
|
|
354
|
+
}, [breachState]);
|
|
355
|
+
const builtIn = [];
|
|
356
|
+
if (showGenerate) {
|
|
357
|
+
builtIn.push({
|
|
358
|
+
name: "generate",
|
|
359
|
+
label: generateLabel,
|
|
360
|
+
icon: /* @__PURE__ */ jsx(Icon, { paths: INPUT_ICON_PATHS.generate }),
|
|
361
|
+
onSelect: handleGenerate
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (showReveal) {
|
|
365
|
+
builtIn.push({
|
|
366
|
+
name: "reveal",
|
|
367
|
+
label: revealed ? revealLabels?.hide ?? "Hide password" : revealLabels?.show ?? "Show password",
|
|
368
|
+
icon: /* @__PURE__ */ jsx(Icon, { paths: revealed ? INPUT_ICON_PATHS.eyeOff : INPUT_ICON_PATHS.eye }),
|
|
369
|
+
pressed: revealed,
|
|
370
|
+
onSelect: toggleReveal
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const overridden = new Set(actions.map((action) => action.name));
|
|
374
|
+
const rendered = [...builtIn.filter((action) => !overridden.has(action.name)), ...actions].filter((action) => action.visible !== false);
|
|
375
|
+
const locked = props.disabled === true || props.readOnly === true;
|
|
376
|
+
const buttons = rendered.length === 0 ? null : /* @__PURE__ */ jsx("span", { "data-enigma-input-actions": "", "data-position": position, className: classNames?.actions, children: rendered.map((action) => /* @__PURE__ */ jsx(
|
|
377
|
+
"button",
|
|
378
|
+
{
|
|
379
|
+
type: "button",
|
|
380
|
+
"data-enigma-input-action": action.name,
|
|
381
|
+
className: classNames?.action,
|
|
382
|
+
"aria-label": action.label,
|
|
383
|
+
title: action.label,
|
|
384
|
+
"aria-pressed": action.pressed,
|
|
385
|
+
disabled: locked,
|
|
386
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
387
|
+
onClick: action.onSelect,
|
|
388
|
+
children: action.icon
|
|
389
|
+
},
|
|
390
|
+
action.name
|
|
391
|
+
)) });
|
|
392
|
+
return /* @__PURE__ */ jsxs(
|
|
393
|
+
"div",
|
|
394
|
+
{
|
|
395
|
+
...wrapperProps,
|
|
396
|
+
"data-enigma-input-root": "",
|
|
397
|
+
"data-revealed": revealed ? "" : void 0,
|
|
398
|
+
"data-breached": breachState.status === "breached" ? "" : void 0,
|
|
399
|
+
"data-score": report && !report.empty ? report.score : void 0,
|
|
400
|
+
children: [
|
|
401
|
+
/* @__PURE__ */ jsxs("div", { ...fieldProps, "data-enigma-input-field": "", children: [
|
|
402
|
+
position === "start" && buttons,
|
|
403
|
+
/* @__PURE__ */ jsx(
|
|
404
|
+
"input",
|
|
405
|
+
{
|
|
406
|
+
...props,
|
|
407
|
+
ref: (node) => {
|
|
408
|
+
innerRef.current = node;
|
|
409
|
+
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
410
|
+
else if (forwardedRef) forwardedRef.current = node;
|
|
411
|
+
},
|
|
412
|
+
type: revealed && isPassword ? "text" : type,
|
|
413
|
+
onChange: handleChange,
|
|
414
|
+
"data-enigma-input": "",
|
|
415
|
+
"aria-describedby": report ? describedBy : props["aria-describedby"]
|
|
416
|
+
}
|
|
417
|
+
),
|
|
418
|
+
position === "end" && buttons
|
|
419
|
+
] }),
|
|
420
|
+
report && /* @__PURE__ */ jsx(PasswordStrength, { id: describedBy, report, classNames: classNames?.strength }),
|
|
421
|
+
children
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
var SCORE_LABELS = ["Very weak", "Weak", "Fair", "Strong", "Very strong"];
|
|
427
|
+
function PasswordStrength({
|
|
428
|
+
value = "",
|
|
429
|
+
report,
|
|
430
|
+
userInputs,
|
|
431
|
+
segments = 5,
|
|
432
|
+
labels = SCORE_LABELS,
|
|
433
|
+
showWarning = true,
|
|
434
|
+
classNames,
|
|
435
|
+
...props
|
|
436
|
+
}) {
|
|
437
|
+
const computed = useMemo(
|
|
438
|
+
() => report ?? estimatePasswordStrength(value, { userInputs }),
|
|
439
|
+
[report, value, userInputs]
|
|
440
|
+
);
|
|
441
|
+
const score = computed.score;
|
|
442
|
+
return /* @__PURE__ */ jsxs(
|
|
443
|
+
"div",
|
|
444
|
+
{
|
|
445
|
+
...props,
|
|
446
|
+
"data-enigma-password-strength": "",
|
|
447
|
+
"data-score": computed.empty ? void 0 : score,
|
|
448
|
+
"data-empty": computed.empty ? "" : void 0,
|
|
449
|
+
children: [
|
|
450
|
+
/* @__PURE__ */ jsx("div", { "data-enigma-password-strength-track": "", "aria-hidden": "true", className: classNames?.track, children: Array.from({ length: segments }, (unused, index) => /* @__PURE__ */ jsx(
|
|
451
|
+
"span",
|
|
452
|
+
{
|
|
453
|
+
"data-enigma-password-strength-segment": "",
|
|
454
|
+
className: classNames?.segment,
|
|
455
|
+
"data-filled": !computed.empty && index <= score ? "" : void 0
|
|
456
|
+
},
|
|
457
|
+
index
|
|
458
|
+
)) }),
|
|
459
|
+
/* @__PURE__ */ jsx("p", { "data-enigma-password-strength-label": "", role: "status", "aria-live": "polite", className: classNames?.label, children: computed.empty ? "" : labels[score] ?? "" }),
|
|
460
|
+
showWarning && !computed.empty && computed.warnings.length > 0 && /* @__PURE__ */ jsx("p", { "data-enigma-password-strength-warning": "", className: classNames?.warning, children: computed.warnings[0] })
|
|
461
|
+
]
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export { Button, Input, PasswordStrength, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ButtonProps } from '../react/index.js';
|
|
3
|
+
export { BreachChecker, BreachState, BreachStatus, FieldAction, Input, InputProps, PasswordStrength, PasswordStrengthProps, UseButtonResult, UseInputResult, UseMarqueeResult, UseSearchResult, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch } from '../react/index.js';
|
|
4
|
+
export { ButtonOptions, ButtonState, EstimateOptions, GeneratePasswordOptions, InputAction, InputIcon, InputOptions, MarqueeHover, MarqueeInstance, MarqueeOptions, PasswordScore, PasswordStrengthReport, estimatePasswordStrength, generatePassword } from '../index.js';
|
|
5
|
+
export { F as FuseConstructor, b as SearchMatch, c as SearchOptions } from '../search-CsO3L1Lw.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Next.js entry. Same components, wired to `next/link`.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { Button } from "@enigmax/primitives/next";
|
|
12
|
+
*
|
|
13
|
+
* <Button href="/settings">Settings</Button> // renders next/link
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The import path is the configuration. There is nothing to register and nothing to pass at
|
|
17
|
+
* the call site, and a project that is not on Next never resolves this file - which is the
|
|
18
|
+
* whole reason the main entry cannot simply `import Link from "next/link"` itself: that
|
|
19
|
+
* import is a build error for every consumer on Vite, Astro, Remix or plain React.
|
|
20
|
+
*
|
|
21
|
+
* Everything else is re-exported unchanged, so this can be the only import path in a Next
|
|
22
|
+
* app rather than a second one to remember.
|
|
23
|
+
*/
|
|
24
|
+
declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLElement>>;
|
|
25
|
+
|
|
26
|
+
export { Button, ButtonProps };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button } from '../chunk-L4KA342C.js';
|
|
3
|
+
export { Input, PasswordStrength, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch } from '../chunk-L4KA342C.js';
|
|
4
|
+
export { estimatePasswordStrength, generatePassword } from '../chunk-42Y7OOJM.js';
|
|
5
|
+
import '../chunk-UZFEEFMF.js';
|
|
6
|
+
import Link from 'next/link';
|
|
7
|
+
import { forwardRef, createElement } from 'react';
|
|
8
|
+
|
|
9
|
+
var Button2 = forwardRef(function Button3(props, ref) {
|
|
10
|
+
return createElement(Button, { ...props, as: props.as ?? (props.href ? Link : void 0), ref });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export { Button2 as Button };
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { ComponentPropsWithoutRef, ElementType, ReactNode, RefObject } from 'react';
|
|
3
|
+
import { ButtonOptions, ButtonState, MarqueeOptions, InputOptions, GeneratePasswordOptions, EstimateOptions, PasswordStrengthReport } from '../index.js';
|
|
4
4
|
export { InputAction, InputIcon, MarqueeHover, MarqueeInstance, PasswordScore, estimatePasswordStrength, generatePassword } from '../index.js';
|
|
5
5
|
import { b as SearchMatch, c as SearchOptions } from '../search-CsO3L1Lw.js';
|
|
6
6
|
export { F as FuseConstructor } from '../search-CsO3L1Lw.js';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* `<Button>` - the component, for the ninety percent.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <Button onPress={save}>Save</Button>
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* That is the whole thing: disabled while the work runs, `aria-busy`, `data-loading`, and a
|
|
16
|
+
* cooldown afterwards if you asked for one. `useButton` is still there for a button whose
|
|
17
|
+
* markup is nothing like a button - a card, a table row - but reaching for it to render an
|
|
18
|
+
* ordinary one means writing the same six lines at every call site, which is how they drift.
|
|
19
|
+
*/
|
|
20
|
+
interface ButtonProps extends ButtonOptions, Omit<ComponentPropsWithoutRef<"button">, "onClick" | "onChange" | "children" | "type"> {
|
|
21
|
+
/**
|
|
22
|
+
* Override what to render. You rarely need it: an href already renders whatever
|
|
23
|
+
* `setLinkComponent` registered - your router's Link - and a button otherwise.
|
|
24
|
+
*/
|
|
25
|
+
as?: ElementType;
|
|
26
|
+
/**
|
|
27
|
+
* The label, or a function of the state for one that changes with it.
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <Button onPress={send} cooldown={30_000}>
|
|
31
|
+
* {({ loading, cooldown }) => loading ? "Sending" : cooldown ? `Wait ${Math.ceil(cooldown / 1000)}s` : "Resend"}
|
|
32
|
+
* </Button>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
children?: ReactNode | ((state: ButtonState) => ReactNode);
|
|
36
|
+
/** Replaces the label while async work runs - a spinner, or just different words. */
|
|
37
|
+
pending?: ReactNode;
|
|
38
|
+
/** `type` on a real button. Ignored on a link, which has none. */
|
|
39
|
+
type?: "button" | "submit" | "reset";
|
|
40
|
+
}
|
|
41
|
+
declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLElement>>;
|
|
42
|
+
|
|
8
43
|
interface UseMarqueeResult {
|
|
9
44
|
/** Attach to the clipping viewport. */
|
|
10
45
|
laneRef: RefObject<HTMLElement | null>;
|
|
@@ -129,44 +164,10 @@ interface UseButtonResult extends ButtonState {
|
|
|
129
164
|
*/
|
|
130
165
|
declare function useButton(options?: ButtonOptions): UseButtonResult;
|
|
131
166
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
* <Button onPress={save}>Save</Button>
|
|
137
|
-
* ```
|
|
138
|
-
*
|
|
139
|
-
* That is the whole thing: disabled while the work runs, `aria-busy`, `data-loading`, and a
|
|
140
|
-
* cooldown afterwards if you asked for one. `useButton` is still there for a button whose
|
|
141
|
-
* markup is nothing like a button - a card, a table row - but reaching for it to render an
|
|
142
|
-
* ordinary one means writing the same six lines at every call site, which is how they drift.
|
|
143
|
-
*/
|
|
144
|
-
interface ButtonProps extends ButtonOptions, Omit<ComponentPropsWithoutRef<"button">, "onClick" | "onChange" | "children" | "type"> {
|
|
145
|
-
/**
|
|
146
|
-
* What to render. Defaults to `a` when there is an href and `button` otherwise, which
|
|
147
|
-
* is what you want until a router is involved - then pass its Link and keep the rest.
|
|
148
|
-
*
|
|
149
|
-
* ```tsx
|
|
150
|
-
* <Button as={Link} href="/settings">Settings</Button>
|
|
151
|
-
* ```
|
|
152
|
-
*/
|
|
153
|
-
as?: ElementType;
|
|
154
|
-
/**
|
|
155
|
-
* The label, or a function of the state for one that changes with it.
|
|
156
|
-
*
|
|
157
|
-
* ```tsx
|
|
158
|
-
* <Button onPress={send} cooldown={30_000}>
|
|
159
|
-
* {({ loading, cooldown }) => loading ? "Sending" : cooldown ? `Wait ${Math.ceil(cooldown / 1000)}s` : "Resend"}
|
|
160
|
-
* </Button>
|
|
161
|
-
* ```
|
|
162
|
-
*/
|
|
163
|
-
children?: ReactNode | ((state: ButtonState) => ReactNode);
|
|
164
|
-
/** Replaces the label while async work runs - a spinner, or just different words. */
|
|
165
|
-
pending?: ReactNode;
|
|
166
|
-
/** `type` on a real button. Ignored on a link, which has none. */
|
|
167
|
-
type?: "button" | "submit" | "reset";
|
|
168
|
-
}
|
|
169
|
-
declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLElement>>;
|
|
167
|
+
/** Register the router's link. Pass null to go back to a plain anchor. */
|
|
168
|
+
declare function setLinkComponent(component: ElementType | null): void;
|
|
169
|
+
/** What an href should render as right now. */
|
|
170
|
+
declare function getLinkComponent(): ElementType;
|
|
170
171
|
|
|
171
172
|
/** An extra button inside the field. The built-ins are the same shape. */
|
|
172
173
|
interface FieldAction {
|
|
@@ -278,4 +279,4 @@ interface PasswordStrengthProps extends Omit<ComponentPropsWithoutRef<"div">, "c
|
|
|
278
279
|
*/
|
|
279
280
|
declare function PasswordStrength({ value, report, userInputs, segments, labels, showWarning, classNames, ...props }: PasswordStrengthProps): ReactNode;
|
|
280
281
|
|
|
281
|
-
export { type BreachChecker, type BreachState, type BreachStatus, Button, ButtonOptions, type ButtonProps, ButtonState, EstimateOptions, type FieldAction, GeneratePasswordOptions, Input, InputOptions, type InputProps, MarqueeOptions, PasswordStrength, type PasswordStrengthProps, PasswordStrengthReport, SearchMatch, SearchOptions, type UseButtonResult, type UseInputResult, type UseMarqueeResult, type UseSearchResult, useButton, useInput, useMarquee, useSearch };
|
|
282
|
+
export { type BreachChecker, type BreachState, type BreachStatus, Button, ButtonOptions, type ButtonProps, ButtonState, EstimateOptions, type FieldAction, GeneratePasswordOptions, Input, InputOptions, type InputProps, MarqueeOptions, PasswordStrength, type PasswordStrengthProps, PasswordStrengthReport, SearchMatch, SearchOptions, type UseButtonResult, type UseInputResult, type UseMarqueeResult, type UseSearchResult, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch };
|
package/dist/react/index.js
CHANGED
|
@@ -1,459 +1,4 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
2
|
+
export { Button, Input, PasswordStrength, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch } from '../chunk-L4KA342C.js';
|
|
3
3
|
export { estimatePasswordStrength, generatePassword } from '../chunk-42Y7OOJM.js';
|
|
4
|
-
import
|
|
5
|
-
import { forwardRef, createElement, useRef, useState, useId, useCallback, useEffect, useMemo, useLayoutEffect } from 'react';
|
|
6
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
|
-
|
|
8
|
-
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
9
|
-
function useMarquee(options = {}) {
|
|
10
|
-
const laneRef = useRef(null);
|
|
11
|
-
const trackRef = useRef(null);
|
|
12
|
-
const instanceRef = useRef(null);
|
|
13
|
-
const optionsRef = useRef(options);
|
|
14
|
-
optionsRef.current = options;
|
|
15
|
-
const [copies, setCopies] = useState(2);
|
|
16
|
-
const [dragging, setDragging] = useState(false);
|
|
17
|
-
const [period, setPeriod] = useState(0);
|
|
18
|
-
useIsomorphicLayoutEffect(() => {
|
|
19
|
-
const lane = laneRef.current;
|
|
20
|
-
const track = trackRef.current;
|
|
21
|
-
if (!lane || !track) return;
|
|
22
|
-
const instance = createMarquee(lane, track, {
|
|
23
|
-
...optionsRef.current,
|
|
24
|
-
// React owns the DOM; cloning behind its back would be undone on render.
|
|
25
|
-
copies: "external",
|
|
26
|
-
onCopyCountChange: setCopies,
|
|
27
|
-
onMeasure: (measured) => setPeriod(measured),
|
|
28
|
-
onItemClick: (event) => optionsRef.current.onItemClick?.(event)
|
|
29
|
-
});
|
|
30
|
-
instanceRef.current = instance;
|
|
31
|
-
const syncDragging = () => setDragging(instance.dragging);
|
|
32
|
-
lane.addEventListener("pointerdown", syncDragging);
|
|
33
|
-
window.addEventListener("pointerup", syncDragging);
|
|
34
|
-
window.addEventListener("pointercancel", syncDragging);
|
|
35
|
-
return () => {
|
|
36
|
-
lane.removeEventListener("pointerdown", syncDragging);
|
|
37
|
-
window.removeEventListener("pointerup", syncDragging);
|
|
38
|
-
window.removeEventListener("pointercancel", syncDragging);
|
|
39
|
-
instance.destroy();
|
|
40
|
-
instanceRef.current = null;
|
|
41
|
-
};
|
|
42
|
-
}, []);
|
|
43
|
-
useIsomorphicLayoutEffect(() => {
|
|
44
|
-
instanceRef.current?.update(options);
|
|
45
|
-
}, [options.speed, options.reverse, options.vertical, options.draggable, options.hover, options.decay]);
|
|
46
|
-
useIsomorphicLayoutEffect(() => {
|
|
47
|
-
instanceRef.current?.measure();
|
|
48
|
-
}, [copies]);
|
|
49
|
-
return {
|
|
50
|
-
laneRef,
|
|
51
|
-
trackRef,
|
|
52
|
-
copies,
|
|
53
|
-
dragging,
|
|
54
|
-
period,
|
|
55
|
-
measure: () => instanceRef.current?.measure()
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
var useIsomorphicLayoutEffect2 = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
59
|
-
function useInput(options = {}) {
|
|
60
|
-
const inputRef = useRef(null);
|
|
61
|
-
const actionsRef = useRef(null);
|
|
62
|
-
const instanceRef = useRef(null);
|
|
63
|
-
const optionsRef = useRef(options);
|
|
64
|
-
optionsRef.current = options;
|
|
65
|
-
const [revealed, setRevealed] = useState(false);
|
|
66
|
-
useIsomorphicLayoutEffect2(() => {
|
|
67
|
-
const input = inputRef.current;
|
|
68
|
-
if (!input) return;
|
|
69
|
-
const instance = createInput(input, {
|
|
70
|
-
...optionsRef.current,
|
|
71
|
-
container: actionsRef.current ?? optionsRef.current.container,
|
|
72
|
-
onRevealChange: (next) => {
|
|
73
|
-
setRevealed(next);
|
|
74
|
-
optionsRef.current.onRevealChange?.(next);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
instanceRef.current = instance;
|
|
78
|
-
return () => {
|
|
79
|
-
instance.destroy();
|
|
80
|
-
instanceRef.current = null;
|
|
81
|
-
};
|
|
82
|
-
}, []);
|
|
83
|
-
useIsomorphicLayoutEffect2(() => {
|
|
84
|
-
instanceRef.current?.update(options);
|
|
85
|
-
}, [options.reveal, options.actions, options.position]);
|
|
86
|
-
return {
|
|
87
|
-
inputRef,
|
|
88
|
-
actionsRef,
|
|
89
|
-
revealed,
|
|
90
|
-
reveal: (next) => instanceRef.current?.reveal(next),
|
|
91
|
-
refresh: () => instanceRef.current?.refresh()
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function useSearch(options = {}) {
|
|
95
|
-
const inputRef = useRef(null);
|
|
96
|
-
const instanceRef = useRef(null);
|
|
97
|
-
const optionsRef = useRef(options);
|
|
98
|
-
optionsRef.current = options;
|
|
99
|
-
const [query, setQuery] = useState("");
|
|
100
|
-
const [results, setResults] = useState([]);
|
|
101
|
-
const instance = useMemo(() => {
|
|
102
|
-
const created = createSearch({
|
|
103
|
-
...optionsRef.current,
|
|
104
|
-
onResults: (next, nextQuery) => {
|
|
105
|
-
setResults(next);
|
|
106
|
-
setQuery(nextQuery);
|
|
107
|
-
optionsRef.current.onResults?.(next, nextQuery);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
instanceRef.current = created;
|
|
111
|
-
return created;
|
|
112
|
-
}, []);
|
|
113
|
-
useEffect(() => () => instance.destroy(), [instance]);
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
instance.setItems(options.items ?? []);
|
|
116
|
-
}, [instance, options.items]);
|
|
117
|
-
useEffect(() => {
|
|
118
|
-
const input = inputRef.current;
|
|
119
|
-
if (!input) return;
|
|
120
|
-
return instance.attach(input);
|
|
121
|
-
}, [instance]);
|
|
122
|
-
return {
|
|
123
|
-
inputRef,
|
|
124
|
-
query,
|
|
125
|
-
results,
|
|
126
|
-
search: (next) => instance.search(next),
|
|
127
|
-
searchNow: (next) => {
|
|
128
|
-
instance.searchNow(next);
|
|
129
|
-
},
|
|
130
|
-
clear: () => {
|
|
131
|
-
if (inputRef.current) inputRef.current.value = "";
|
|
132
|
-
instance.searchNow("");
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
function useButton(options = {}) {
|
|
137
|
-
const optionsRef = useRef(options);
|
|
138
|
-
optionsRef.current = options;
|
|
139
|
-
const instance = useMemo(() => createButton({
|
|
140
|
-
...optionsRef.current,
|
|
141
|
-
onPress: (event) => optionsRef.current.onPress?.(event),
|
|
142
|
-
onChange: (next) => optionsRef.current.onChange?.(next)
|
|
143
|
-
// Built once: recreating it would drop a running cooldown on every render.
|
|
144
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
145
|
-
}), []);
|
|
146
|
-
const [state, setState] = useState(() => instance.state);
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
const unsubscribe = instance.subscribe(setState);
|
|
149
|
-
setState(instance.state);
|
|
150
|
-
return () => {
|
|
151
|
-
unsubscribe();
|
|
152
|
-
instance.destroy();
|
|
153
|
-
};
|
|
154
|
-
}, [instance]);
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
instance.update({
|
|
157
|
-
href: options.href,
|
|
158
|
-
disabled: options.disabled,
|
|
159
|
-
loading: options.loading,
|
|
160
|
-
cooldown: options.cooldown,
|
|
161
|
-
shortcut: options.shortcut
|
|
162
|
-
});
|
|
163
|
-
}, [instance, options.href, options.disabled, options.loading, options.cooldown, options.shortcut]);
|
|
164
|
-
const press = useCallback(() => {
|
|
165
|
-
void instance.press();
|
|
166
|
-
}, [instance]);
|
|
167
|
-
return {
|
|
168
|
-
...state,
|
|
169
|
-
press,
|
|
170
|
-
reset: () => instance.reset(),
|
|
171
|
-
props: {
|
|
172
|
-
onClick: (event) => {
|
|
173
|
-
if (!state.available) {
|
|
174
|
-
event.preventDefault();
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
void instance.press();
|
|
178
|
-
},
|
|
179
|
-
"aria-disabled": !state.available,
|
|
180
|
-
"aria-busy": state.loading,
|
|
181
|
-
...state.loading ? { "data-loading": "" } : {},
|
|
182
|
-
...state.cooldown > 0 ? { "data-cooldown": "" } : {},
|
|
183
|
-
...state.element === "button" ? { disabled: state.disabled } : { href: options.href },
|
|
184
|
-
...state.shortcut ? { title: `Shortcut: ${state.shortcut.toUpperCase()}` } : {}
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
var Button = forwardRef(function Button2({
|
|
189
|
-
as,
|
|
190
|
-
children,
|
|
191
|
-
pending,
|
|
192
|
-
href,
|
|
193
|
-
disabled,
|
|
194
|
-
loading,
|
|
195
|
-
cooldown,
|
|
196
|
-
shortcut,
|
|
197
|
-
onPress,
|
|
198
|
-
onChange,
|
|
199
|
-
type = "button",
|
|
200
|
-
...rest
|
|
201
|
-
}, ref) {
|
|
202
|
-
const button = useButton({ href, disabled, loading, cooldown, shortcut, onPress, onChange });
|
|
203
|
-
const state = button;
|
|
204
|
-
const Tag = as ?? state.element;
|
|
205
|
-
const native = Tag === "button" ? { type } : {};
|
|
206
|
-
const label = typeof children === "function" ? children(state) : children;
|
|
207
|
-
return createElement(
|
|
208
|
-
Tag,
|
|
209
|
-
{ ref, ...button.props, ...native, ...rest },
|
|
210
|
-
state.loading && pending !== void 0 ? pending : label
|
|
211
|
-
);
|
|
212
|
-
});
|
|
213
|
-
var ICON_PROPS = {
|
|
214
|
-
viewBox: "0 0 24 24",
|
|
215
|
-
width: "1em",
|
|
216
|
-
height: "1em",
|
|
217
|
-
fill: "none",
|
|
218
|
-
stroke: "currentColor",
|
|
219
|
-
strokeWidth: 2,
|
|
220
|
-
strokeLinecap: "round",
|
|
221
|
-
strokeLinejoin: "round",
|
|
222
|
-
"aria-hidden": true
|
|
223
|
-
};
|
|
224
|
-
function Icon({ paths }) {
|
|
225
|
-
return /* @__PURE__ */ jsx("svg", { ...ICON_PROPS, children: paths.map((path) => /* @__PURE__ */ jsx("path", { d: path }, path)) });
|
|
226
|
-
}
|
|
227
|
-
function writeValue(input, next) {
|
|
228
|
-
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
229
|
-
if (setter) setter.call(input, next);
|
|
230
|
-
else input.value = next;
|
|
231
|
-
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
232
|
-
}
|
|
233
|
-
var Input = forwardRef(function Input2({
|
|
234
|
-
reveal,
|
|
235
|
-
revealLabels,
|
|
236
|
-
generate = false,
|
|
237
|
-
generateLabel = "Generate a password",
|
|
238
|
-
revealOnGenerate = true,
|
|
239
|
-
copyOnGenerate = false,
|
|
240
|
-
onGenerate,
|
|
241
|
-
actions = [],
|
|
242
|
-
position = "end",
|
|
243
|
-
strength = false,
|
|
244
|
-
onStrengthChange,
|
|
245
|
-
breach,
|
|
246
|
-
breachDelay = 500,
|
|
247
|
-
onBreachChange,
|
|
248
|
-
wrapperProps,
|
|
249
|
-
fieldProps,
|
|
250
|
-
classNames,
|
|
251
|
-
children,
|
|
252
|
-
type = "text",
|
|
253
|
-
onChange,
|
|
254
|
-
...props
|
|
255
|
-
}, forwardedRef) {
|
|
256
|
-
const innerRef = useRef(null);
|
|
257
|
-
const [revealed, setRevealed] = useState(false);
|
|
258
|
-
const describedBy = useId();
|
|
259
|
-
const isPassword = type === "password";
|
|
260
|
-
const showReveal = reveal ?? isPassword;
|
|
261
|
-
const showGenerate = generate !== false && isPassword;
|
|
262
|
-
const controlled = props.value !== void 0;
|
|
263
|
-
const [ownValue, setOwnValue] = useState(String(props.defaultValue ?? ""));
|
|
264
|
-
const value = controlled ? String(props.value ?? "") : ownValue;
|
|
265
|
-
const handleChange = useCallback((event) => {
|
|
266
|
-
if (!controlled) setOwnValue(event.target.value);
|
|
267
|
-
onChange?.(event);
|
|
268
|
-
}, [controlled, onChange]);
|
|
269
|
-
const caret = useRef(null);
|
|
270
|
-
const toggleReveal = useCallback(() => {
|
|
271
|
-
const input = innerRef.current;
|
|
272
|
-
if (input && typeof document !== "undefined" && document.activeElement === input) {
|
|
273
|
-
try {
|
|
274
|
-
const { selectionStart, selectionEnd } = input;
|
|
275
|
-
caret.current = selectionStart === null || selectionEnd === null ? null : [selectionStart, selectionEnd];
|
|
276
|
-
} catch {
|
|
277
|
-
caret.current = null;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
setRevealed((current) => !current);
|
|
281
|
-
}, []);
|
|
282
|
-
useEffect(() => {
|
|
283
|
-
const selection = caret.current;
|
|
284
|
-
if (!selection) return;
|
|
285
|
-
caret.current = null;
|
|
286
|
-
const restore = () => {
|
|
287
|
-
const input = innerRef.current;
|
|
288
|
-
if (!input || document.activeElement !== input) return;
|
|
289
|
-
try {
|
|
290
|
-
input.setSelectionRange(selection[0], selection[1]);
|
|
291
|
-
} catch {
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
restore();
|
|
295
|
-
const timer = setTimeout(restore, 0);
|
|
296
|
-
return () => clearTimeout(timer);
|
|
297
|
-
}, [revealed]);
|
|
298
|
-
const handleGenerate = useCallback(() => {
|
|
299
|
-
const input = innerRef.current;
|
|
300
|
-
if (!input) return;
|
|
301
|
-
const password = generatePassword(typeof generate === "object" ? generate : {});
|
|
302
|
-
writeValue(input, password);
|
|
303
|
-
if (revealOnGenerate) setRevealed(true);
|
|
304
|
-
if (copyOnGenerate) void navigator.clipboard?.writeText(password).catch(() => {
|
|
305
|
-
});
|
|
306
|
-
input.focus();
|
|
307
|
-
onGenerate?.(password);
|
|
308
|
-
}, [generate, revealOnGenerate, copyOnGenerate, onGenerate]);
|
|
309
|
-
const estimateOptions = typeof strength === "object" ? strength : void 0;
|
|
310
|
-
const userInputs = estimateOptions?.userInputs;
|
|
311
|
-
const report = useMemo(
|
|
312
|
-
() => strength === false ? null : estimatePasswordStrength(value, { userInputs }),
|
|
313
|
-
// The array is compared by identity, so a literal would re-estimate every render.
|
|
314
|
-
[strength, value, userInputs]
|
|
315
|
-
);
|
|
316
|
-
const strengthListener = useRef(onStrengthChange);
|
|
317
|
-
strengthListener.current = onStrengthChange;
|
|
318
|
-
useEffect(() => {
|
|
319
|
-
if (report) strengthListener.current?.(report);
|
|
320
|
-
}, [report]);
|
|
321
|
-
const [breachState, setBreachState] = useState({ status: "idle", count: 0, error: null });
|
|
322
|
-
const breachListener = useRef(onBreachChange);
|
|
323
|
-
breachListener.current = onBreachChange;
|
|
324
|
-
useEffect(() => {
|
|
325
|
-
if (!breach || !value) {
|
|
326
|
-
setBreachState({ status: "idle", count: 0, error: null });
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
const controller = new AbortController();
|
|
330
|
-
const timer = setTimeout(() => {
|
|
331
|
-
setBreachState({ status: "checking", count: 0, error: null });
|
|
332
|
-
breach(value, { signal: controller.signal }).then((result) => {
|
|
333
|
-
if (controller.signal.aborted) return;
|
|
334
|
-
setBreachState({ status: result.breached ? "breached" : "safe", count: result.count, error: null });
|
|
335
|
-
}).catch((error) => {
|
|
336
|
-
if (controller.signal.aborted) return;
|
|
337
|
-
setBreachState({ status: "error", count: 0, error });
|
|
338
|
-
});
|
|
339
|
-
}, breachDelay);
|
|
340
|
-
return () => {
|
|
341
|
-
clearTimeout(timer);
|
|
342
|
-
controller.abort();
|
|
343
|
-
};
|
|
344
|
-
}, [breach, value, breachDelay]);
|
|
345
|
-
useEffect(() => {
|
|
346
|
-
breachListener.current?.(breachState);
|
|
347
|
-
}, [breachState]);
|
|
348
|
-
const builtIn = [];
|
|
349
|
-
if (showGenerate) {
|
|
350
|
-
builtIn.push({
|
|
351
|
-
name: "generate",
|
|
352
|
-
label: generateLabel,
|
|
353
|
-
icon: /* @__PURE__ */ jsx(Icon, { paths: INPUT_ICON_PATHS.generate }),
|
|
354
|
-
onSelect: handleGenerate
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
if (showReveal) {
|
|
358
|
-
builtIn.push({
|
|
359
|
-
name: "reveal",
|
|
360
|
-
label: revealed ? revealLabels?.hide ?? "Hide password" : revealLabels?.show ?? "Show password",
|
|
361
|
-
icon: /* @__PURE__ */ jsx(Icon, { paths: revealed ? INPUT_ICON_PATHS.eyeOff : INPUT_ICON_PATHS.eye }),
|
|
362
|
-
pressed: revealed,
|
|
363
|
-
onSelect: toggleReveal
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
const overridden = new Set(actions.map((action) => action.name));
|
|
367
|
-
const rendered = [...builtIn.filter((action) => !overridden.has(action.name)), ...actions].filter((action) => action.visible !== false);
|
|
368
|
-
const locked = props.disabled === true || props.readOnly === true;
|
|
369
|
-
const buttons = rendered.length === 0 ? null : /* @__PURE__ */ jsx("span", { "data-enigma-input-actions": "", "data-position": position, className: classNames?.actions, children: rendered.map((action) => /* @__PURE__ */ jsx(
|
|
370
|
-
"button",
|
|
371
|
-
{
|
|
372
|
-
type: "button",
|
|
373
|
-
"data-enigma-input-action": action.name,
|
|
374
|
-
className: classNames?.action,
|
|
375
|
-
"aria-label": action.label,
|
|
376
|
-
title: action.label,
|
|
377
|
-
"aria-pressed": action.pressed,
|
|
378
|
-
disabled: locked,
|
|
379
|
-
onMouseDown: (event) => event.preventDefault(),
|
|
380
|
-
onClick: action.onSelect,
|
|
381
|
-
children: action.icon
|
|
382
|
-
},
|
|
383
|
-
action.name
|
|
384
|
-
)) });
|
|
385
|
-
return /* @__PURE__ */ jsxs(
|
|
386
|
-
"div",
|
|
387
|
-
{
|
|
388
|
-
...wrapperProps,
|
|
389
|
-
"data-enigma-input-root": "",
|
|
390
|
-
"data-revealed": revealed ? "" : void 0,
|
|
391
|
-
"data-breached": breachState.status === "breached" ? "" : void 0,
|
|
392
|
-
"data-score": report && !report.empty ? report.score : void 0,
|
|
393
|
-
children: [
|
|
394
|
-
/* @__PURE__ */ jsxs("div", { ...fieldProps, "data-enigma-input-field": "", children: [
|
|
395
|
-
position === "start" && buttons,
|
|
396
|
-
/* @__PURE__ */ jsx(
|
|
397
|
-
"input",
|
|
398
|
-
{
|
|
399
|
-
...props,
|
|
400
|
-
ref: (node) => {
|
|
401
|
-
innerRef.current = node;
|
|
402
|
-
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
403
|
-
else if (forwardedRef) forwardedRef.current = node;
|
|
404
|
-
},
|
|
405
|
-
type: revealed && isPassword ? "text" : type,
|
|
406
|
-
onChange: handleChange,
|
|
407
|
-
"data-enigma-input": "",
|
|
408
|
-
"aria-describedby": report ? describedBy : props["aria-describedby"]
|
|
409
|
-
}
|
|
410
|
-
),
|
|
411
|
-
position === "end" && buttons
|
|
412
|
-
] }),
|
|
413
|
-
report && /* @__PURE__ */ jsx(PasswordStrength, { id: describedBy, report, classNames: classNames?.strength }),
|
|
414
|
-
children
|
|
415
|
-
]
|
|
416
|
-
}
|
|
417
|
-
);
|
|
418
|
-
});
|
|
419
|
-
var SCORE_LABELS = ["Very weak", "Weak", "Fair", "Strong", "Very strong"];
|
|
420
|
-
function PasswordStrength({
|
|
421
|
-
value = "",
|
|
422
|
-
report,
|
|
423
|
-
userInputs,
|
|
424
|
-
segments = 5,
|
|
425
|
-
labels = SCORE_LABELS,
|
|
426
|
-
showWarning = true,
|
|
427
|
-
classNames,
|
|
428
|
-
...props
|
|
429
|
-
}) {
|
|
430
|
-
const computed = useMemo(
|
|
431
|
-
() => report ?? estimatePasswordStrength(value, { userInputs }),
|
|
432
|
-
[report, value, userInputs]
|
|
433
|
-
);
|
|
434
|
-
const score = computed.score;
|
|
435
|
-
return /* @__PURE__ */ jsxs(
|
|
436
|
-
"div",
|
|
437
|
-
{
|
|
438
|
-
...props,
|
|
439
|
-
"data-enigma-password-strength": "",
|
|
440
|
-
"data-score": computed.empty ? void 0 : score,
|
|
441
|
-
"data-empty": computed.empty ? "" : void 0,
|
|
442
|
-
children: [
|
|
443
|
-
/* @__PURE__ */ jsx("div", { "data-enigma-password-strength-track": "", "aria-hidden": "true", className: classNames?.track, children: Array.from({ length: segments }, (unused, index) => /* @__PURE__ */ jsx(
|
|
444
|
-
"span",
|
|
445
|
-
{
|
|
446
|
-
"data-enigma-password-strength-segment": "",
|
|
447
|
-
className: classNames?.segment,
|
|
448
|
-
"data-filled": !computed.empty && index <= score ? "" : void 0
|
|
449
|
-
},
|
|
450
|
-
index
|
|
451
|
-
)) }),
|
|
452
|
-
/* @__PURE__ */ jsx("p", { "data-enigma-password-strength-label": "", role: "status", "aria-live": "polite", className: classNames?.label, children: computed.empty ? "" : labels[score] ?? "" }),
|
|
453
|
-
showWarning && !computed.empty && computed.warnings.length > 0 && /* @__PURE__ */ jsx("p", { "data-enigma-password-strength-warning": "", className: classNames?.warning, children: computed.warnings[0] })
|
|
454
|
-
]
|
|
455
|
-
}
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
export { Button, Input, PasswordStrength, useButton, useInput, useMarquee, useSearch };
|
|
4
|
+
import '../chunk-UZFEEFMF.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -18,13 +18,17 @@
|
|
|
18
18
|
"./search": {
|
|
19
19
|
"types": "./dist/search/index.d.ts",
|
|
20
20
|
"import": "./dist/search/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./next": {
|
|
23
|
+
"types": "./dist/next/index.d.ts",
|
|
24
|
+
"import": "./dist/next/index.js"
|
|
21
25
|
}
|
|
22
26
|
},
|
|
23
27
|
"scripts": {
|
|
24
28
|
"build": "tsup",
|
|
25
29
|
"typecheck": "tsc --noEmit",
|
|
26
30
|
"test": "npm run test:fixtures && playwright test",
|
|
27
|
-
"test:fixtures": "esbuild test/fixture/react.tsx --bundle --format=esm --jsx=automatic --outfile=test/fixture/build/react.js --tsconfig=tsconfig.json",
|
|
31
|
+
"test:fixtures": "esbuild test/fixture/react.tsx --bundle --format=esm --jsx=automatic --outfile=test/fixture/build/react.js --tsconfig=tsconfig.json --alias:next/link=./test/fixture/next-link-stub.tsx",
|
|
28
32
|
"test:install": "playwright install chromium",
|
|
29
33
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
30
34
|
},
|
|
@@ -38,7 +42,8 @@
|
|
|
38
42
|
],
|
|
39
43
|
"peerDependencies": {
|
|
40
44
|
"fuse.js": ">=6",
|
|
41
|
-
"react": ">=18"
|
|
45
|
+
"react": ">=18",
|
|
46
|
+
"next": ">=13"
|
|
42
47
|
},
|
|
43
48
|
"peerDependenciesMeta": {
|
|
44
49
|
"react": {
|
|
@@ -46,6 +51,9 @@
|
|
|
46
51
|
},
|
|
47
52
|
"fuse.js": {
|
|
48
53
|
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"next": {
|
|
56
|
+
"optional": true
|
|
49
57
|
}
|
|
50
58
|
},
|
|
51
59
|
"devDependencies": {
|
|
@@ -53,6 +61,7 @@
|
|
|
53
61
|
"@types/react": "^19.2.0",
|
|
54
62
|
"@types/react-dom": "^19.2.4",
|
|
55
63
|
"fuse.js": "^7.0.0",
|
|
64
|
+
"next": "^15.5.23",
|
|
56
65
|
"react": "^19.2.0",
|
|
57
66
|
"react-dom": "^19.2.8",
|
|
58
67
|
"tsup": "^8.0.0",
|
package/registry.json
CHANGED
|
@@ -301,6 +301,7 @@
|
|
|
301
301
|
],
|
|
302
302
|
"react": [
|
|
303
303
|
"Button",
|
|
304
|
+
"setLinkComponent",
|
|
304
305
|
"useButton"
|
|
305
306
|
]
|
|
306
307
|
},
|
|
@@ -324,6 +325,13 @@
|
|
|
324
325
|
"@/core/button": "./button"
|
|
325
326
|
}
|
|
326
327
|
},
|
|
328
|
+
{
|
|
329
|
+
"path": "src/react/link.ts",
|
|
330
|
+
"dest": "button/link.ts",
|
|
331
|
+
"targets": [
|
|
332
|
+
"react"
|
|
333
|
+
]
|
|
334
|
+
},
|
|
327
335
|
{
|
|
328
336
|
"path": "src/react/button.tsx",
|
|
329
337
|
"dest": "button/index.tsx",
|
|
@@ -333,7 +341,8 @@
|
|
|
333
341
|
],
|
|
334
342
|
"rewrite": {
|
|
335
343
|
"@/core/button": "./button",
|
|
336
|
-
"@/react/use-button": "./use-button"
|
|
344
|
+
"@/react/use-button": "./use-button",
|
|
345
|
+
"@/react/link": "./link"
|
|
337
346
|
}
|
|
338
347
|
}
|
|
339
348
|
],
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { forwardRef, createElement } from "react";
|
|
5
|
+
import { Button as BaseButton, type ButtonProps } from "@/react/button";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Next.js entry. Same components, wired to `next/link`.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { Button } from "@enigmax/primitives/next";
|
|
12
|
+
*
|
|
13
|
+
* <Button href="/settings">Settings</Button> // renders next/link
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The import path is the configuration. There is nothing to register and nothing to pass at
|
|
17
|
+
* the call site, and a project that is not on Next never resolves this file - which is the
|
|
18
|
+
* whole reason the main entry cannot simply `import Link from "next/link"` itself: that
|
|
19
|
+
* import is a build error for every consumer on Vite, Astro, Remix or plain React.
|
|
20
|
+
*
|
|
21
|
+
* Everything else is re-exported unchanged, so this can be the only import path in a Next
|
|
22
|
+
* app rather than a second one to remember.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const Button = forwardRef<HTMLElement, ButtonProps>(function Button(props, ref) {
|
|
26
|
+
// Only an href makes it a link; a plain button must not be handed a router component.
|
|
27
|
+
return createElement(BaseButton, { ...props, as: props.as ?? (props.href ? Link : undefined), ref });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// `export *` skips names this module exports itself, so the Button above is the one a
|
|
31
|
+
// consumer gets and everything else comes straight from the React entry.
|
|
32
|
+
export * from "@/react/index";
|
package/src/react/button.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useButton } from "@/react/use-button";
|
|
4
|
+
import { getLinkComponent } from "@/react/link";
|
|
4
5
|
import type { ButtonOptions, ButtonState } from "@/core/button";
|
|
5
6
|
import { createElement, forwardRef, type ComponentPropsWithoutRef, type ElementType, type ReactNode } from "react";
|
|
6
7
|
|
|
@@ -21,12 +22,8 @@ import { createElement, forwardRef, type ComponentPropsWithoutRef, type ElementT
|
|
|
21
22
|
// reports the button's state - would otherwise clash with the form event of the same name.
|
|
22
23
|
export interface ButtonProps extends ButtonOptions, Omit<ComponentPropsWithoutRef<"button">, "onClick" | "onChange" | "children" | "type"> {
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* ```tsx
|
|
28
|
-
* <Button as={Link} href="/settings">Settings</Button>
|
|
29
|
-
* ```
|
|
25
|
+
* Override what to render. You rarely need it: an href already renders whatever
|
|
26
|
+
* `setLinkComponent` registered - your router's Link - and a button otherwise.
|
|
30
27
|
*/
|
|
31
28
|
as?: ElementType;
|
|
32
29
|
/**
|
|
@@ -62,9 +59,11 @@ export const Button = forwardRef<HTMLElement, ButtonProps>(function Button({
|
|
|
62
59
|
const button = useButton({ href, disabled, loading, cooldown, shortcut, onPress, onChange });
|
|
63
60
|
const state: ButtonState = button;
|
|
64
61
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
62
|
+
// An href renders the registered link - next/link, react-router's, whatever was passed
|
|
63
|
+
// to setLinkComponent - and a plain <a> when nothing was. The package cannot import a
|
|
64
|
+
// router itself, so registering one is what turns every href into a client navigation
|
|
65
|
+
// without a word at the call site.
|
|
66
|
+
const Tag: ElementType = as ?? (state.element === "a" ? getLinkComponent() : "button");
|
|
68
67
|
|
|
69
68
|
// `type="button"` is the default on purpose. A bare <button> inside a form submits it,
|
|
70
69
|
// so an action button that forgot it posts the form instead of doing its job.
|
package/src/react/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { type InputOptions, type InputAction, type InputIcon } from "@/core/inpu
|
|
|
8
8
|
export { type SearchOptions, type SearchMatch, type FuseConstructor } from "@/core/search";
|
|
9
9
|
export { useButton, type UseButtonResult } from "@/react/use-button";
|
|
10
10
|
export { Button, type ButtonProps } from "@/react/button";
|
|
11
|
+
export { setLinkComponent, getLinkComponent } from "@/react/link";
|
|
11
12
|
export { type ButtonOptions, type ButtonState } from "@/core/button";
|
|
12
13
|
export {
|
|
13
14
|
Input,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ElementType } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The component every `href` in this package renders through.
|
|
7
|
+
*
|
|
8
|
+
* The package cannot `import Link from "next/link"`. It is framework-agnostic and Next is
|
|
9
|
+
* not a dependency, so that import would fail to resolve for every consumer who is not on
|
|
10
|
+
* Next - a hard build error in Vite, Astro, Remix and plain React.
|
|
11
|
+
*
|
|
12
|
+
* So it is registered instead, ONCE, and after that an href is a router link everywhere:
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* // app/providers.tsx, or wherever your app starts
|
|
16
|
+
* import Link from "next/link";
|
|
17
|
+
* import { setLinkComponent } from "@enigmax/primitives/react";
|
|
18
|
+
*
|
|
19
|
+
* setLinkComponent(Link);
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* // and from then on, every call site
|
|
24
|
+
* <Button href="/settings">Settings</Button>
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Without it an href renders a plain `<a>`, which is correct everywhere and merely means a
|
|
28
|
+
* full page load under a router. Any router works the same way - react-router's `Link`,
|
|
29
|
+
* TanStack Router's, Astro has none to register.
|
|
30
|
+
*
|
|
31
|
+
* A module-level value rather than a context: it is one component reference for the whole
|
|
32
|
+
* app, identical on every request, so threading a provider through the tree would buy
|
|
33
|
+
* nothing and cost every consumer a wrapper.
|
|
34
|
+
*/
|
|
35
|
+
let linkComponent: ElementType | null = null;
|
|
36
|
+
|
|
37
|
+
/** Register the router's link. Pass null to go back to a plain anchor. */
|
|
38
|
+
export function setLinkComponent(component: ElementType | null): void {
|
|
39
|
+
linkComponent = component;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What an href should render as right now. */
|
|
43
|
+
export function getLinkComponent(): ElementType {
|
|
44
|
+
return linkComponent ?? "a";
|
|
45
|
+
}
|