@enigmax/primitives 0.7.0 → 0.8.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.
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { RefObject, ReactNode, ComponentPropsWithoutRef } from 'react';
2
+ import { RefObject, ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';
3
3
  import { MarqueeOptions, InputOptions, ButtonState, ButtonOptions, 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';
@@ -129,6 +129,45 @@ interface UseButtonResult extends ButtonState {
129
129
  */
130
130
  declare function useButton(options?: ButtonOptions): UseButtonResult;
131
131
 
132
+ /**
133
+ * `<Button>` - the component, for the ninety percent.
134
+ *
135
+ * ```tsx
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>>;
170
+
132
171
  /** An extra button inside the field. The built-ins are the same shape. */
133
172
  interface FieldAction {
134
173
  /** Stable id. Lands on `data-enigma-input-action`, and replaces a built-in of the same name. */
@@ -239,4 +278,4 @@ interface PasswordStrengthProps extends Omit<ComponentPropsWithoutRef<"div">, "c
239
278
  */
240
279
  declare function PasswordStrength({ value, report, userInputs, segments, labels, showWarning, classNames, ...props }: PasswordStrengthProps): ReactNode;
241
280
 
242
- export { type BreachChecker, type BreachState, type BreachStatus, ButtonOptions, 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 };
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 };
@@ -1,8 +1,8 @@
1
1
  "use client";
2
- import { generatePassword, estimatePasswordStrength, INPUT_ICON_PATHS, createMarquee, createInput, createButton } from '../chunk-42Y7OOJM.js';
2
+ import { generatePassword, estimatePasswordStrength, INPUT_ICON_PATHS, createButton, createMarquee, createInput } from '../chunk-42Y7OOJM.js';
3
3
  export { estimatePasswordStrength, generatePassword } from '../chunk-42Y7OOJM.js';
4
4
  import { createSearch } from '../chunk-UZFEEFMF.js';
5
- import { forwardRef, useRef, useState, useId, useCallback, useEffect, useMemo, useLayoutEffect } from 'react';
5
+ import { forwardRef, createElement, useRef, useState, useId, useCallback, useEffect, useMemo, useLayoutEffect } from 'react';
6
6
  import { jsx, jsxs } from 'react/jsx-runtime';
7
7
 
8
8
  var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
@@ -185,6 +185,31 @@ function useButton(options = {}) {
185
185
  }
186
186
  };
187
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
+ });
188
213
  var ICON_PROPS = {
189
214
  viewBox: "0 0 24 24",
190
215
  width: "1em",
@@ -431,4 +456,4 @@ function PasswordStrength({
431
456
  );
432
457
  }
433
458
 
434
- export { Input, PasswordStrength, useButton, useInput, useMarquee, useSearch };
459
+ export { Button, Input, PasswordStrength, useButton, useInput, useMarquee, useSearch };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enigmax/primitives",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",
@@ -1,4 +1,4 @@
1
- import "./marquee.css";
1
+ import "./styles.css";
2
2
  import type { ReactNode, Ref } from "react";
3
3
  import { useMarquee, type MarqueeHover } from "@enigmax/primitives/react";
4
4
 
package/registry.json CHANGED
@@ -31,7 +31,7 @@
31
31
  "files": [
32
32
  {
33
33
  "path": "src/core/marquee.ts",
34
- "dest": "marquee.ts",
34
+ "dest": "marquee/marquee.ts",
35
35
  "targets": [
36
36
  "vanilla",
37
37
  "astro",
@@ -40,33 +40,34 @@
40
40
  },
41
41
  {
42
42
  "path": "src/react/use-marquee.ts",
43
- "dest": "use-marquee.ts",
43
+ "dest": "marquee/use-marquee.ts",
44
44
  "targets": [
45
45
  "react"
46
46
  ],
47
47
  "rewrite": {
48
48
  "@/core/marquee": "./marquee"
49
- }
49
+ },
50
+ "main": true
50
51
  },
51
52
  {
52
- "path": "recipes/marquee.tailwind.tsx",
53
- "dest": "Marquee.tsx",
53
+ "path": "recipes/marquee/tailwind.tsx",
54
+ "dest": "marquee/Marquee.tsx",
54
55
  "targets": [
55
56
  "react"
56
57
  ],
57
58
  "style": "tailwind"
58
59
  },
59
60
  {
60
- "path": "recipes/marquee.css.tsx",
61
- "dest": "Marquee.tsx",
61
+ "path": "recipes/marquee/css.tsx",
62
+ "dest": "marquee/Marquee.tsx",
62
63
  "targets": [
63
64
  "react"
64
65
  ],
65
66
  "style": "css"
66
67
  },
67
68
  {
68
- "path": "recipes/marquee.css",
69
- "dest": "marquee.css",
69
+ "path": "recipes/marquee/styles.css",
70
+ "dest": "marquee/styles.css",
70
71
  "targets": [
71
72
  "react",
72
73
  "vanilla",
@@ -235,7 +236,7 @@
235
236
  "files": [
236
237
  {
237
238
  "path": "src/core/search.ts",
238
- "dest": "search.ts",
239
+ "dest": "search/search.ts",
239
240
  "targets": [
240
241
  "vanilla",
241
242
  "astro",
@@ -244,17 +245,18 @@
244
245
  },
245
246
  {
246
247
  "path": "src/react/use-search.ts",
247
- "dest": "use-search.ts",
248
+ "dest": "search/use-search.ts",
248
249
  "targets": [
249
250
  "react"
250
251
  ],
251
252
  "rewrite": {
252
253
  "@/core/search": "./search"
253
- }
254
+ },
255
+ "main": true
254
256
  },
255
257
  {
256
- "path": "recipes/search.tailwind.tsx",
257
- "dest": "Search.tsx",
258
+ "path": "recipes/search/tailwind.tsx",
259
+ "dest": "search/Search.tsx",
258
260
  "targets": [
259
261
  "react"
260
262
  ],
@@ -278,8 +280,8 @@
278
280
  },
279
281
  {
280
282
  "name": "button",
281
- "title": "Button behaviour",
282
- "description": "Disabled, loading, a keyboard shortcut and a cooldown that can outlive a reload, collapsed into one `available` the renderer reads. An href reports an anchor rather than a button, so the framework's own Link stays your choice.",
283
+ "title": "Button",
284
+ "description": "A button you pass props to. Disabled, loading, a keyboard shortcut and a cooldown that can outlive a reload collapse into one `available`; an href reports an anchor, so the framework's own Link stays your choice.",
283
285
  "targets": [
284
286
  "vanilla",
285
287
  "astro",
@@ -298,13 +300,14 @@
298
300
  "createButton"
299
301
  ],
300
302
  "react": [
303
+ "Button",
301
304
  "useButton"
302
305
  ]
303
306
  },
304
307
  "files": [
305
308
  {
306
309
  "path": "src/core/button.ts",
307
- "dest": "button.ts",
310
+ "dest": "button/button.ts",
308
311
  "targets": [
309
312
  "vanilla",
310
313
  "astro",
@@ -313,13 +316,25 @@
313
316
  },
314
317
  {
315
318
  "path": "src/react/use-button.ts",
316
- "dest": "use-button.ts",
319
+ "dest": "button/use-button.ts",
317
320
  "targets": [
318
321
  "react"
319
322
  ],
320
323
  "rewrite": {
321
324
  "@/core/button": "./button"
322
325
  }
326
+ },
327
+ {
328
+ "path": "src/react/button.tsx",
329
+ "dest": "button/index.tsx",
330
+ "main": true,
331
+ "targets": [
332
+ "react"
333
+ ],
334
+ "rewrite": {
335
+ "@/core/button": "./button",
336
+ "@/react/use-button": "./use-button"
337
+ }
323
338
  }
324
339
  ],
325
340
  "styles": false,
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { useButton } from "@/react/use-button";
4
+ import type { ButtonOptions, ButtonState } from "@/core/button";
5
+ import { createElement, forwardRef, type ComponentPropsWithoutRef, type ElementType, type ReactNode } from "react";
6
+
7
+ /**
8
+ * `<Button>` - the component, for the ninety percent.
9
+ *
10
+ * ```tsx
11
+ * <Button onPress={save}>Save</Button>
12
+ * ```
13
+ *
14
+ * That is the whole thing: disabled while the work runs, `aria-busy`, `data-loading`, and a
15
+ * cooldown afterwards if you asked for one. `useButton` is still there for a button whose
16
+ * markup is nothing like a button - a card, a table row - but reaching for it to render an
17
+ * ordinary one means writing the same six lines at every call site, which is how they drift.
18
+ */
19
+
20
+ // `onChange` is omitted from the DOM side because the primitive's own onChange - which
21
+ // reports the button's state - would otherwise clash with the form event of the same name.
22
+ export interface ButtonProps extends ButtonOptions, Omit<ComponentPropsWithoutRef<"button">, "onClick" | "onChange" | "children" | "type"> {
23
+ /**
24
+ * What to render. Defaults to `a` when there is an href and `button` otherwise, which
25
+ * is what you want until a router is involved - then pass its Link and keep the rest.
26
+ *
27
+ * ```tsx
28
+ * <Button as={Link} href="/settings">Settings</Button>
29
+ * ```
30
+ */
31
+ as?: ElementType;
32
+ /**
33
+ * The label, or a function of the state for one that changes with it.
34
+ *
35
+ * ```tsx
36
+ * <Button onPress={send} cooldown={30_000}>
37
+ * {({ loading, cooldown }) => loading ? "Sending" : cooldown ? `Wait ${Math.ceil(cooldown / 1000)}s` : "Resend"}
38
+ * </Button>
39
+ * ```
40
+ */
41
+ children?: ReactNode | ((state: ButtonState) => ReactNode);
42
+ /** Replaces the label while async work runs - a spinner, or just different words. */
43
+ pending?: ReactNode;
44
+ /** `type` on a real button. Ignored on a link, which has none. */
45
+ type?: "button" | "submit" | "reset";
46
+ }
47
+
48
+ export const Button = forwardRef<HTMLElement, ButtonProps>(function Button({
49
+ as,
50
+ children,
51
+ pending,
52
+ href,
53
+ disabled,
54
+ loading,
55
+ cooldown,
56
+ shortcut,
57
+ onPress,
58
+ onChange,
59
+ type = "button",
60
+ ...rest
61
+ }, ref) {
62
+ const button = useButton({ href, disabled, loading, cooldown, shortcut, onPress, onChange });
63
+ const state: ButtonState = button;
64
+
65
+ // Reported, not chosen: an href makes it an anchor, and a package that cannot import
66
+ // next/link must not be the thing deciding. `as` overrides both.
67
+ const Tag: ElementType = as ?? state.element;
68
+
69
+ // `type="button"` is the default on purpose. A bare <button> inside a form submits it,
70
+ // so an action button that forgot it posts the form instead of doing its job.
71
+ const native = Tag === "button" ? { type } : {};
72
+
73
+ const label = typeof children === "function" ? children(state) : children;
74
+
75
+ return createElement(
76
+ Tag,
77
+ { ref, ...button.props, ...native, ...rest },
78
+ state.loading && pending !== undefined ? pending : label
79
+ );
80
+ });
@@ -7,6 +7,7 @@ export { useSearch, type UseSearchResult } from "@/react/use-search";
7
7
  export { type InputOptions, type InputAction, type InputIcon } from "@/core/input";
8
8
  export { type SearchOptions, type SearchMatch, type FuseConstructor } from "@/core/search";
9
9
  export { useButton, type UseButtonResult } from "@/react/use-button";
10
+ export { Button, type ButtonProps } from "@/react/button";
10
11
  export { type ButtonOptions, type ButtonState } from "@/core/button";
11
12
  export {
12
13
  Input,
File without changes