@allxsmith/bestax-bulma 5.63.0 → 5.65.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.
Files changed (65) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/components/Carousel.js +12 -66
  4. package/dist/esm/components/Carousel.js.map +1 -1
  5. package/dist/esm/components/Lightbox.js +756 -0
  6. package/dist/esm/components/Lightbox.js.map +1 -0
  7. package/dist/esm/components/Reveal.js +1 -24
  8. package/dist/esm/components/Reveal.js.map +1 -1
  9. package/dist/esm/components/ScrollArea.js +7 -7
  10. package/dist/esm/components/ScrollArea.js.map +1 -1
  11. package/dist/esm/components/TableOfContents.js +85 -0
  12. package/dist/esm/components/TableOfContents.js.map +1 -0
  13. package/dist/esm/form/PasswordInput.js +105 -0
  14. package/dist/esm/form/PasswordInput.js.map +1 -0
  15. package/dist/esm/form/PasswordInputBase.js +177 -0
  16. package/dist/esm/form/PasswordInputBase.js.map +1 -0
  17. package/dist/esm/helpers/chevronIcons.js +24 -0
  18. package/dist/esm/helpers/chevronIcons.js.map +1 -0
  19. package/dist/esm/helpers/usePrefersReducedMotion.js +34 -0
  20. package/dist/esm/helpers/usePrefersReducedMotion.js.map +1 -0
  21. package/dist/esm/helpers/useScrollSpy.js +286 -0
  22. package/dist/esm/helpers/useScrollSpy.js.map +1 -0
  23. package/dist/esm/helpers/useSwipe.js +82 -0
  24. package/dist/esm/helpers/useSwipe.js.map +1 -0
  25. package/dist/extras.css +1 -1
  26. package/dist/extras.css.map +1 -1
  27. package/dist/index.cjs +1582 -198
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.esm.js +5 -0
  30. package/dist/index.esm.js.map +1 -1
  31. package/dist/types/components/Lightbox.d.ts +244 -0
  32. package/dist/types/components/TableOfContents.d.ts +87 -0
  33. package/dist/types/form/PasswordInput.d.ts +98 -0
  34. package/dist/types/form/PasswordInputBase.d.ts +91 -0
  35. package/dist/types/helpers/chevronIcons.d.ts +14 -0
  36. package/dist/types/helpers/usePrefersReducedMotion.d.ts +10 -0
  37. package/dist/types/helpers/useScrollSpy.d.ts +61 -0
  38. package/dist/types/helpers/useSwipe.d.ts +66 -0
  39. package/dist/types/index.d.ts +6 -0
  40. package/dist/types-cjs/components/Lightbox.d.ts +244 -0
  41. package/dist/types-cjs/components/TableOfContents.d.ts +87 -0
  42. package/dist/types-cjs/form/PasswordInput.d.ts +98 -0
  43. package/dist/types-cjs/form/PasswordInputBase.d.ts +91 -0
  44. package/dist/types-cjs/helpers/chevronIcons.d.ts +14 -0
  45. package/dist/types-cjs/helpers/usePrefersReducedMotion.d.ts +10 -0
  46. package/dist/types-cjs/helpers/useScrollSpy.d.ts +61 -0
  47. package/dist/types-cjs/helpers/useSwipe.d.ts +66 -0
  48. package/dist/types-cjs/index.d.ts +6 -0
  49. package/dist/versions/bestax-no-dark-mode.css +1 -1
  50. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  51. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  52. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  53. package/dist/versions/bestax-no-helpers.css +1 -1
  54. package/dist/versions/bestax-no-helpers.css.map +1 -1
  55. package/dist/versions/bestax-prefixed.css +1 -1
  56. package/dist/versions/bestax-prefixed.css.map +1 -1
  57. package/package.json +1 -1
  58. package/src/scss/CLAUDE.md +3 -0
  59. package/src/scss/_input-trigger.scss +76 -0
  60. package/src/scss/components/_index.scss +2 -0
  61. package/src/scss/components/_lightbox.scss +392 -0
  62. package/src/scss/components/_tableofcontents.scss +42 -0
  63. package/src/scss/form/_index.scss +1 -0
  64. package/src/scss/form/_passwordinput.scss +112 -0
  65. package/src/scss/form/_picker-popover.scss +24 -63
@@ -0,0 +1,105 @@
1
+ /* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
2
+ 'use client';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import { forwardRef } from 'react';
5
+ import { usePrefixedClassNames } from '../helpers/classNames.js';
6
+ import { warnOnce } from '../helpers/devWarnings.js';
7
+ import { Field } from './Field.js';
8
+ import { Control } from './Control.js';
9
+ import { PasswordInputBase } from './PasswordInputBase.js';
10
+ import { useInsideField, useInsideControl, rendersOwnField, rendersOwnControl } from './FormContext.js';
11
+ import { useAutoLabelId } from './useAutoLabelId.js';
12
+
13
+ /** The right-icon props, which the toggle's place leaves no room for. */
14
+ const RIGHT_ICON_PROPS = [
15
+ 'iconRight',
16
+ 'iconRightName',
17
+ 'iconRightSize',
18
+ 'hasIconsRight',
19
+ ];
20
+ /**
21
+ * The `PasswordInput` component is a password field with a button inside it that shows what was typed.
22
+ *
23
+ * By default the button is an eye named "Show password" that reports
24
+ * whether the password shows with `aria-pressed`; `variant="text"` makes it
25
+ * the words "Show" and "Hide". The field asks for
26
+ * `autocomplete="current-password"` and no spellcheck, and hides the
27
+ * password again when its form submits.
28
+ *
29
+ * @function
30
+ * @param {PasswordInputProps} props - Props for PasswordInput.
31
+ * @returns {JSX.Element} The composed field element.
32
+ *
33
+ * @example
34
+ * <PasswordInput label="Password" />
35
+ *
36
+ * @example
37
+ * <PasswordInput label="New password" autoComplete="new-password" variant="text" />
38
+ */
39
+ const PasswordInput = forwardRef((props, ref) => {
40
+ const {
41
+ // Field props
42
+ label, labelSize, labelProps, horizontal,
43
+ // Control props
44
+ iconLeft, iconLeftName, iconLeftSize, hasIconsLeft, isLoading, isExpanded, controlSize,
45
+ // Message props
46
+ message, messageColor,
47
+ // Container class overrides
48
+ fieldClassName, controlClassName,
49
+ // Not offered, and not passed on
50
+ iconRight: _iconRight, iconRightName: _iconRightName, iconRightSize: _iconRightSize, hasIconsRight: _hasIconsRight,
51
+ // Everything else goes to the base
52
+ ...baseProps } = props;
53
+ const rightIcon = RIGHT_ICON_PROPS.filter(prop => props[prop]);
54
+ if (rightIcon.length > 0) {
55
+ const names = rightIcon.join(' ');
56
+ warnOnce(`PasswordInput:right-icon:${names}`, `[bestax-bulma] <PasswordInput ${names}> draws no right icon: the ` +
57
+ `visibility toggle sits at that edge. Leave ` +
58
+ `${rightIcon.length > 1 ? 'them' : 'it'} out, or use a left icon.`);
59
+ }
60
+ const insideField = useInsideField();
61
+ const insideControl = useInsideControl();
62
+ const ownField = rendersOwnField('PasswordInput', {
63
+ insideField,
64
+ insideControl,
65
+ label,
66
+ message,
67
+ horizontal,
68
+ fieldClassName,
69
+ });
70
+ const ownControl = rendersOwnControl('PasswordInput', {
71
+ insideControl,
72
+ isLoading,
73
+ iconLeft,
74
+ iconLeftName,
75
+ iconLeftSize,
76
+ hasIconsLeft,
77
+ isExpanded,
78
+ controlSize,
79
+ controlClassName,
80
+ });
81
+ const { controlId, fieldLabelProps } = useAutoLabelId({
82
+ label,
83
+ id: baseProps.id,
84
+ labelProps,
85
+ rendersLabel: ownField,
86
+ });
87
+ const helpClass = usePrefixedClassNames('help', {
88
+ [`is-${messageColor}`]: !!messageColor,
89
+ });
90
+ // The base hides its toggle while the Control it sits in is loading,
91
+ // whether that is the one rendered below or an enclosing one.
92
+ let content = (jsx(PasswordInputBase, { ref: ref, ...baseProps, id: controlId }));
93
+ if (ownControl) {
94
+ content = (jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
95
+ }
96
+ const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
97
+ if (ownField) {
98
+ return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
99
+ }
100
+ return (jsxs(Fragment, { children: [content, messageEl] }));
101
+ });
102
+ PasswordInput.displayName = 'PasswordInput';
103
+
104
+ export { PasswordInput };
105
+ //# sourceMappingURL=PasswordInput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PasswordInput.js","sources":["../../../src/form/PasswordInput.tsx"],"sourcesContent":["import React, { forwardRef } from 'react';\nimport { usePrefixedClassNames } from '../helpers/classNames';\nimport { warnOnce } from '../helpers/devWarnings';\nimport { Field, FieldProps } from './Field';\nimport { Control, ControlBaseProps } from './Control';\nimport { PasswordInputBase, PasswordInputBaseProps } from './PasswordInputBase';\nimport {\n useInsideField,\n useInsideControl,\n rendersOwnField,\n rendersOwnControl,\n} from './FormContext';\nimport { useAutoLabelId } from './useAutoLabelId';\n\n/**\n * Props for the PasswordInput component: PasswordInputBase's, plus Input's\n * Field and Control props, except the right icon, whose place the toggle\n * takes. Inside an existing `Control` it renders no `Control` of its own, so\n * its Control-level props do nothing there and warn in development; set them\n * on that `Control` instead. Inside an outer `Field`, or a `Control` with no\n * `Field` around it, it renders no `Field` of its own either. The exception\n * is `label`, `message`, `horizontal` or `fieldClassName` in that bare\n * `Control`: it keeps a `Field` for them, nested in the `.control`, and warns\n * in development. Wrap the `Control` in a `Field` instead, and set the\n * `label`, `horizontal` and class name on that `Field`.\n *\n * @helperProps Helper props go on the `.password-input-container` `<div>`, with\n * `className` and `style`, so they move and size the field and its toggle\n * together. The other attributes go on the `<input>`.\n * @extraProp {string} [autoComplete=\"current-password\"] - Tells the browser and password managers this is an existing password. Pass `\"new-password\"` where the user makes one up, on a sign-up or change-password form.\n * @extraProp {boolean} [spellCheck=false] - Off, so a browser's spellcheck never sees the password once it shows.\n * @extraProp {string} [autoCorrect=\"off\"] - Off, so a phone's keyboard doesn't change what's typed.\n * @extraProp {string} [autoCapitalize=\"none\"] - Off, so a phone's keyboard doesn't capitalize the first letter.\n * @extraProp {string} [value] - The password, controlled. React writes a controlled input's value into its `value` attribute, as it does on any input, so the password sits in the page's markup. Uncontrolled, read with `FormData`, the field never puts what's typed there.\n * @extraProp {React.Ref<HTMLInputElement>} [ref] - Ref forwarded to the `<input>`.\n */\nexport interface PasswordInputProps extends PasswordInputBaseProps {\n /** Field label. Automatically associated with the input via `htmlFor`, using your `id` when provided, otherwise a generated one. Dropped inside an outer `Field`, whose own label associates instead when that `Field` generates a target id (not `grouped`/`hasAddons`, no explicit `labelProps.htmlFor`) and you set no `id` here. */\n label?: React.ReactNode;\n /** Size for the label. */\n labelSize?: FieldProps['labelSize'];\n /** Props for the label element when the component renders its own `Field`; dropped inside an outer `Field` (use that `Field`'s `labelProps` instead). An explicit `htmlFor` key, even `undefined`, overrides the automatic association and no id is generated. */\n labelProps?: FieldProps['labelProps'];\n /** Horizontal field layout. */\n horizontal?: boolean;\n /**\n * Icon props for the left icon.\n * Bulma gives control icons `pointer-events: none`, so a clickable node here\n * never receives a click.\n */\n iconLeft?: ControlBaseProps['iconLeft'];\n /** Shortcut for the left icon's name. */\n iconLeftName?: string;\n /** Shortcut for the left icon's size. */\n iconLeftSize?: ControlBaseProps['iconLeftSize'];\n /** Force the left icon container. */\n hasIconsLeft?: boolean;\n /**\n * Shows a loading spinner on the `Control` it renders, at the edge the\n * toggle uses, so the toggle isn't drawn meanwhile, and a shown password is\n * hidden until it's back. Inside your own\n * `Control` it renders none, so this draws nothing and warns in\n * development; set `isLoading` on that `Control`, which hides the toggle\n * too.\n */\n isLoading?: boolean;\n /** Expand the control. */\n isExpanded?: boolean;\n /** Control size. */\n controlSize?: ControlBaseProps['size'];\n /** Help/validation message below the input. */\n message?: React.ReactNode;\n /** Bulma color for the message. */\n messageColor?: 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger';\n /** Additional CSS classes for the Field. */\n fieldClassName?: string;\n /** Additional CSS classes for the Control. */\n controlClassName?: string;\n /** Not offered: the toggle takes the right icon's place. Warns in development if passed. @internal */\n iconRight?: never;\n /** Not offered, as `iconRight`. @internal */\n iconRightName?: never;\n /** Not offered, as `iconRight`. @internal */\n iconRightSize?: never;\n /** Not offered, as `iconRight`. @internal */\n hasIconsRight?: never;\n}\n\n/** The right-icon props, which the toggle's place leaves no room for. */\nconst RIGHT_ICON_PROPS = [\n 'iconRight',\n 'iconRightName',\n 'iconRightSize',\n 'hasIconsRight',\n] as const;\n\n/**\n * The `PasswordInput` component is a password field with a button inside it that shows what was typed.\n *\n * By default the button is an eye named \"Show password\" that reports\n * whether the password shows with `aria-pressed`; `variant=\"text\"` makes it\n * the words \"Show\" and \"Hide\". The field asks for\n * `autocomplete=\"current-password\"` and no spellcheck, and hides the\n * password again when its form submits.\n *\n * @function\n * @param {PasswordInputProps} props - Props for PasswordInput.\n * @returns {JSX.Element} The composed field element.\n *\n * @example\n * <PasswordInput label=\"Password\" />\n *\n * @example\n * <PasswordInput label=\"New password\" autoComplete=\"new-password\" variant=\"text\" />\n */\nexport const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(\n (props, ref) => {\n const {\n // Field props\n label,\n labelSize,\n labelProps,\n horizontal,\n // Control props\n iconLeft,\n iconLeftName,\n iconLeftSize,\n hasIconsLeft,\n isLoading,\n isExpanded,\n controlSize,\n // Message props\n message,\n messageColor,\n // Container class overrides\n fieldClassName,\n controlClassName,\n // Not offered, and not passed on\n iconRight: _iconRight,\n iconRightName: _iconRightName,\n iconRightSize: _iconRightSize,\n hasIconsRight: _hasIconsRight,\n // Everything else goes to the base\n ...baseProps\n } = props;\n\n const rightIcon = RIGHT_ICON_PROPS.filter(prop => props[prop]);\n if (rightIcon.length > 0) {\n const names = rightIcon.join(' ');\n warnOnce(\n `PasswordInput:right-icon:${names}`,\n `[bestax-bulma] <PasswordInput ${names}> draws no right icon: the ` +\n `visibility toggle sits at that edge. Leave ` +\n `${rightIcon.length > 1 ? 'them' : 'it'} out, or use a left icon.`\n );\n }\n\n const insideField = useInsideField();\n const insideControl = useInsideControl();\n const ownField = rendersOwnField('PasswordInput', {\n insideField,\n insideControl,\n label,\n message,\n horizontal,\n fieldClassName,\n });\n const ownControl = rendersOwnControl('PasswordInput', {\n insideControl,\n isLoading,\n iconLeft,\n iconLeftName,\n iconLeftSize,\n hasIconsLeft,\n isExpanded,\n controlSize,\n controlClassName,\n });\n const { controlId, fieldLabelProps } = useAutoLabelId({\n label,\n id: baseProps.id,\n labelProps,\n rendersLabel: ownField,\n });\n const helpClass = usePrefixedClassNames('help', {\n [`is-${messageColor}`]: !!messageColor,\n });\n\n // The base hides its toggle while the Control it sits in is loading,\n // whether that is the one rendered below or an enclosing one.\n let content: React.ReactNode = (\n <PasswordInputBase ref={ref} {...baseProps} id={controlId} />\n );\n\n if (ownControl) {\n content = (\n <Control\n iconLeft={iconLeft}\n iconLeftName={iconLeftName}\n iconLeftSize={iconLeftSize}\n hasIconsLeft={hasIconsLeft}\n isLoading={isLoading}\n isExpanded={isExpanded}\n size={controlSize}\n className={controlClassName}\n >\n {content}\n </Control>\n );\n }\n\n const messageEl = message ? <p className={helpClass}>{message}</p> : null;\n\n if (ownField) {\n return (\n <Field\n label={label}\n labelSize={labelSize}\n labelProps={fieldLabelProps}\n horizontal={horizontal}\n className={fieldClassName}\n >\n {content}\n {messageEl}\n </Field>\n );\n }\n\n return (\n <>\n {content}\n {messageEl}\n </>\n );\n }\n);\n\nPasswordInput.displayName = 'PasswordInput';\n\nexport default PasswordInput;\n"],"names":["_jsx","_jsxs","_Fragment"],"mappings":"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"}
@@ -0,0 +1,177 @@
1
+ /* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
2
+ 'use client';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+ import { forwardRef, useState, useRef, useCallback, useEffect } from 'react';
5
+ import { flushSync } from 'react-dom';
6
+ import { usePrefixedClassNames, classNames } from '../helpers/classNames.js';
7
+ import { warnOnce } from '../helpers/devWarnings.js';
8
+ import { useBulmaClasses } from '../helpers/useBulmaClasses.js';
9
+ import { useClientLayoutEffect } from '../helpers/useClientLayoutEffect.js';
10
+ import { useLatestRef } from '../helpers/useLatestRef.js';
11
+ import { InputBase } from './InputBase.js';
12
+ import { useControlLoading } from './controlLoading.js';
13
+ import { useFieldsetDisabled } from './FormContext.js';
14
+
15
+ /** An eye, drawn while the password is hidden. */
16
+ const EyeIcon = () => (jsx("svg", { viewBox: "0 0 16 16", width: "1.25em", height: "1.25em", "aria-hidden": "true", children: jsxs("g", { fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M1.5 8s2.5-4.5 6.5-4.5 6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8z" }), jsx("circle", { cx: "8", cy: "8", r: "2" })] }) }));
17
+ /** The eye crossed out, drawn while the password shows. */
18
+ const EyeOffIcon = () => (jsx("svg", { viewBox: "0 0 16 16", width: "1.25em", height: "1.25em", "aria-hidden": "true", children: jsxs("g", { fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M1.5 8s2.5-4.5 6.5-4.5 6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8z" }), jsx("circle", { cx: "8", cy: "8", r: "2" }), jsx("path", { d: "M2.5 2.5l11 11" })] }) }));
19
+ /**
20
+ * Warns in development when the `text` toggle's name doesn't contain the
21
+ * word on it, which a voice control user says to press it.
22
+ */
23
+ const checkLabelInName = (name, word) => {
24
+ if (name.toLocaleLowerCase().includes(word.toLocaleLowerCase()))
25
+ return;
26
+ warnOnce(`PasswordInput:label-in-name:${name}:${word}`, `[bestax-bulma] A PasswordInput with variant="text" is named ` +
27
+ `"${name}" while its button says "${word}". A voice control user ` +
28
+ `says the word they see, so the name has to contain it (WCAG 2.5.3, ` +
29
+ `Label in Name). Set labels so each name contains its word.`);
30
+ };
31
+ /**
32
+ * The raw PasswordInput: a password field with a button inside it that shows what was typed, without Field/Control wrapping.
33
+ *
34
+ * Use `PasswordInput` for the convenience wrapper.
35
+ *
36
+ * @function
37
+ * @param {PasswordInputBaseProps} props - Props for PasswordInputBase.
38
+ * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.
39
+ * @returns {JSX.Element} The input and its toggle, in a container.
40
+ */
41
+ const PasswordInputBase = forwardRef((props, ref) => {
42
+ const { visible: visibleProp, defaultVisible = false, onVisibleChange, variant = 'icon', labels, autoComplete = 'current-password', spellCheck = false, autoCorrect = 'off', autoCapitalize = 'none', disabled: disabledProp, color, className, style, ...others } = props;
43
+ // Helper props, `className` and `style` go on the container, so a margin,
44
+ // a width or a display moves the field and its toggle together. `color`
45
+ // stays with the input, whose border it colors.
46
+ const { bulmaHelperClasses, rest } = useBulmaClasses(others);
47
+ const disabled = useFieldsetDisabled(disabledProp);
48
+ // The toggle gives way to the loading spinner of a Control this sits in,
49
+ // which takes its slot, and the field hides the password meanwhile, as
50
+ // there's no button to hide it with.
51
+ const controlLoading = useControlLoading();
52
+ const isText = variant === 'text';
53
+ const showPassword = labels?.showPassword || 'Show password';
54
+ const hidePassword = labels?.hidePassword || 'Hide password';
55
+ const show = labels?.show || 'Show';
56
+ const hide = labels?.hide || 'Hide';
57
+ if (isText) {
58
+ checkLabelInName(showPassword, show);
59
+ checkLabelInName(hidePassword, hide);
60
+ }
61
+ const isControlled = visibleProp !== undefined;
62
+ const [internalVisible, setInternalVisible] = useState(defaultVisible);
63
+ const visible = isControlled ? visibleProp : internalVisible;
64
+ // Whether the field shows now: as last reported, until `visible` changes.
65
+ // Two hide events before React renders, as from an effect, or a caller
66
+ // holding `visible` that keeps it `true`, report `false` once.
67
+ const shownRef = useRef(visible);
68
+ useClientLayoutEffect(() => {
69
+ shownRef.current = visible;
70
+ }, [visible]);
71
+ const setVisible = (next) => {
72
+ shownRef.current = next;
73
+ if (!isControlled)
74
+ setInternalVisible(next);
75
+ onVisibleChange?.(next);
76
+ };
77
+ const inputRef = useRef(null);
78
+ // A caller's callback ref may return a cleanup, as React 19 allows. It's
79
+ // held here and run on detach in place of a call with `null`, on React 18
80
+ // as well, since this ref is the one React calls.
81
+ const refCleanup = useRef(null);
82
+ const combinedRef = useCallback((node) => {
83
+ inputRef.current = node;
84
+ if (typeof ref === 'function') {
85
+ if (node === null) {
86
+ const cleanup = refCleanup.current;
87
+ refCleanup.current = null;
88
+ if (cleanup)
89
+ cleanup();
90
+ else
91
+ ref(null);
92
+ return;
93
+ }
94
+ const cleanup = ref(node);
95
+ refCleanup.current =
96
+ typeof cleanup === 'function' ? cleanup : null;
97
+ }
98
+ else if (ref) {
99
+ ref.current = node;
100
+ }
101
+ }, [ref]);
102
+ // A toggle with focus that leaves while the input stays, as it does when a
103
+ // Control starts loading, hands focus to the input rather than dropping it
104
+ // to the page. React lets go of the toggle before it takes it out of the
105
+ // page, so the toggle still has focus here; when the whole field unmounts,
106
+ // React has let go of the input first.
107
+ const toggleRef = useRef(null);
108
+ const setToggle = useCallback((node) => {
109
+ // React lets go only of a toggle it handed over, so there is one.
110
+ const leaving = toggleRef.current;
111
+ toggleRef.current = node;
112
+ if (node)
113
+ return;
114
+ const root = leaving.getRootNode();
115
+ if (root.activeElement === leaving)
116
+ inputRef.current?.focus();
117
+ }, []);
118
+ // Hides the password again: reports `false`, and sets its own state when it
119
+ // holds it. Inside `flushSync`, React renders that, a caller's own state
120
+ // update included, before the listener returns, so the browser reads the
121
+ // form, or saves the page's form state, from a password field. The
122
+ // component never sets the field's type itself, so a caller holding
123
+ // `visible` that keeps it `true` keeps the field shown. The one catch: an
124
+ // event fired while React is rendering or committing, which includes
125
+ // `useEffect` and `useLayoutEffect` callbacks (a `requestSubmit()` from
126
+ // an effect), can't flush there, so React warns in development and the
127
+ // hide lands after the form is read. Only the listeners below call it,
128
+ // while the input is mounted.
129
+ const hideRef = useLatestRef(() => {
130
+ if (!shownRef.current)
131
+ return;
132
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the form is read, or the page saved, before React would render this on its own
133
+ flushSync(() => setVisible(false));
134
+ });
135
+ // When the form it belongs to submits or resets: whichever form that is
136
+ // when it happens, so a form mounted after the field, or mounted again,
137
+ // counts. Neither event leaves a shadow root, so it listens on the field's
138
+ // own root, the document or the shadow root it renders in, and in the
139
+ // capture phase, before any listener on the form.
140
+ useEffect(() => {
141
+ const input = inputRef.current;
142
+ const root = input.getRootNode();
143
+ const onFormEvent = (event) => {
144
+ if (event.target === input.form)
145
+ hideRef.current();
146
+ };
147
+ root.addEventListener('submit', onFormEvent, true);
148
+ root.addEventListener('reset', onFormEvent, true);
149
+ return () => {
150
+ root.removeEventListener('submit', onFormEvent, true);
151
+ root.removeEventListener('reset', onFormEvent, true);
152
+ };
153
+ }, [hideRef]);
154
+ // When the page is left, so the history the browser keeps of the form
155
+ // holds a password field rather than text. It fires on the way into the
156
+ // back-forward cache too, so a page coming back from there is hidden.
157
+ useEffect(() => {
158
+ const onPageHide = () => hideRef.current();
159
+ window.addEventListener('pagehide', onPageHide);
160
+ return () => window.removeEventListener('pagehide', onPageHide);
161
+ }, [hideRef]);
162
+ const containerClass = usePrefixedClassNames('password-input-container', {
163
+ 'is-text': isText,
164
+ });
165
+ const toggleClass = usePrefixedClassNames('password-input-toggle');
166
+ // The icon keeps one name and reports its state; the words change, so
167
+ // the name follows them and there's no state to report.
168
+ const toggleName = isText && visible ? hidePassword : showPassword;
169
+ const glyph = visible ? jsx(EyeOffIcon, {}) : jsx(EyeIcon, {});
170
+ return (jsxs("div", { className: classNames(containerClass, bulmaHelperClasses, className), style: style, children: [jsx(InputBase, { ref: combinedRef, autoComplete: autoComplete, spellCheck: spellCheck, autoCorrect: autoCorrect, autoCapitalize: autoCapitalize, disabled: disabled, color: color, ...rest, type: visible && !controlLoading ? 'text' : 'password' }), !controlLoading && (jsx("button", { ref: setToggle, type: "button", className: toggleClass, "aria-label": toggleName, "aria-pressed": isText ? undefined : visible, disabled: disabled,
171
+ // A pointer press leaves focus, and the caret, in the input.
172
+ onMouseDown: event => event.preventDefault(), onClick: () => setVisible(!visible), children: isText ? (visible ? hide : show) : glyph }))] }));
173
+ });
174
+ PasswordInputBase.displayName = 'PasswordInputBase';
175
+
176
+ export { PasswordInputBase };
177
+ //# sourceMappingURL=PasswordInputBase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PasswordInputBase.js","sources":["../../../src/form/PasswordInputBase.tsx"],"sourcesContent":["import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';\nimport { flushSync } from 'react-dom';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\nimport { warnOnce } from '../helpers/devWarnings';\nimport { useBulmaClasses } from '../helpers/useBulmaClasses';\nimport { useClientLayoutEffect } from '../helpers/useClientLayoutEffect';\nimport { useLatestRef } from '../helpers/useLatestRef';\nimport { InputBase, InputBaseProps } from './InputBase';\nimport { useControlLoading } from './controlLoading';\nimport { useFieldsetDisabled } from './FormContext';\n\n/** How PasswordInput's visibility toggle draws itself. */\nexport type PasswordInputVariant = 'icon' | 'text';\n\n/** The strings PasswordInput's toggle shows and announces, for translating. */\nexport interface PasswordInputLabels {\n /**\n * The toggle's name. With the `icon` variant it's the name in both states,\n * and `aria-pressed` says whether the password shows. With `text`, it's the\n * name while the password is hidden, and it has to contain `show`, the word\n * on the button.\n * @defaultValue 'Show password'\n */\n showPassword?: string;\n /**\n * The `text` variant's name while the password shows. It has to contain\n * `hide`.\n * @defaultValue 'Hide password'\n */\n hidePassword?: string;\n /**\n * The `text` variant's word while the password is hidden.\n * @defaultValue 'Show'\n */\n show?: string;\n /**\n * The `text` variant's word while the password shows.\n * @defaultValue 'Hide'\n */\n hide?: string;\n}\n\n/**\n * Props for PasswordInputBase, the input and its toggle without a Field or a\n * Control.\n *\n * @helperProps Helper props go on the `.password-input-container` `<div>`, with\n * `className` and `style`, so they move and size the field and its toggle\n * together. The other attributes go on the `<input>`.\n * @extraProp {string} [autoComplete=\"current-password\"] - Tells the browser and password managers this is an existing password. Pass `\"new-password\"` where the user makes one up, on a sign-up or change-password form.\n * @extraProp {boolean} [spellCheck=false] - Off, so a browser's spellcheck never sees the password once it shows.\n * @extraProp {string} [autoCorrect=\"off\"] - Off, so a phone's keyboard doesn't change what's typed.\n * @extraProp {string} [autoCapitalize=\"none\"] - Off, so a phone's keyboard doesn't capitalize the first letter.\n * @extraProp {string} [value] - The password, controlled. React writes a controlled input's value into its `value` attribute, as it does on any input, so the password sits in the page's markup. Uncontrolled, read with `FormData`, the field never puts what's typed there.\n * @extraProp {React.Ref<HTMLInputElement>} [ref] - Ref forwarded to the `<input>`.\n */\nexport interface PasswordInputBaseProps extends Omit<InputBaseProps, 'type'> {\n /**\n * Whether the password shows, when you hold the state. Setting it makes\n * the visibility controlled: the toggle only calls `onVisibleChange`, and\n * so does hiding again on its own. Apply that `false` and the field is a\n * password field by the time the form reads it; keep `visible` true and\n * the field stays shown.\n */\n visible?: boolean;\n /**\n * Whether the password shows at first, when the input holds its own state.\n * @defaultValue false\n */\n defaultVisible?: boolean;\n /**\n * Called with the new visibility on each toggle, and with `false` when the\n * form submits or resets, or the page is left, which it also is on the way\n * into the back-forward cache. React renders those hides at once, a state\n * update you make here included, so the form reads a password field. One\n * that fires while React is rendering or committing, which includes your\n * `useEffect` and `useLayoutEffect` callbacks, such as a `requestSubmit()`\n * from an effect, can't render there, and lands just after the form is\n * read: hide the password first, or submit from an event handler.\n */\n onVisibleChange?: (visible: boolean) => void;\n /**\n * How the toggle draws itself: an eye icon with a fixed name and\n * `aria-pressed`, or the words \"Show\" and \"Hide\" with a name that follows\n * them.\n * @defaultValue 'icon'\n */\n variant?: PasswordInputVariant;\n /** Translations for the toggle. */\n labels?: PasswordInputLabels;\n}\n\n/** An eye, drawn while the password is hidden. */\nconst EyeIcon = () => (\n <svg viewBox=\"0 0 16 16\" width=\"1.25em\" height=\"1.25em\" aria-hidden=\"true\">\n <g\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M1.5 8s2.5-4.5 6.5-4.5 6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8z\" />\n <circle cx=\"8\" cy=\"8\" r=\"2\" />\n </g>\n </svg>\n);\n\n/** The eye crossed out, drawn while the password shows. */\nconst EyeOffIcon = () => (\n <svg viewBox=\"0 0 16 16\" width=\"1.25em\" height=\"1.25em\" aria-hidden=\"true\">\n <g\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M1.5 8s2.5-4.5 6.5-4.5 6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8z\" />\n <circle cx=\"8\" cy=\"8\" r=\"2\" />\n <path d=\"M2.5 2.5l11 11\" />\n </g>\n </svg>\n);\n\n/**\n * Warns in development when the `text` toggle's name doesn't contain the\n * word on it, which a voice control user says to press it.\n */\nconst checkLabelInName = (name: string, word: string) => {\n if (name.toLocaleLowerCase().includes(word.toLocaleLowerCase())) return;\n warnOnce(\n `PasswordInput:label-in-name:${name}:${word}`,\n `[bestax-bulma] A PasswordInput with variant=\"text\" is named ` +\n `\"${name}\" while its button says \"${word}\". A voice control user ` +\n `says the word they see, so the name has to contain it (WCAG 2.5.3, ` +\n `Label in Name). Set labels so each name contains its word.`\n );\n};\n\n/**\n * The raw PasswordInput: a password field with a button inside it that shows what was typed, without Field/Control wrapping.\n *\n * Use `PasswordInput` for the convenience wrapper.\n *\n * @function\n * @param {PasswordInputBaseProps} props - Props for PasswordInputBase.\n * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.\n * @returns {JSX.Element} The input and its toggle, in a container.\n */\nexport const PasswordInputBase = forwardRef<\n HTMLInputElement,\n PasswordInputBaseProps\n>((props, ref) => {\n const {\n visible: visibleProp,\n defaultVisible = false,\n onVisibleChange,\n variant = 'icon',\n labels,\n autoComplete = 'current-password',\n spellCheck = false,\n autoCorrect = 'off',\n autoCapitalize = 'none',\n disabled: disabledProp,\n color,\n className,\n style,\n ...others\n } = props;\n // Helper props, `className` and `style` go on the container, so a margin,\n // a width or a display moves the field and its toggle together. `color`\n // stays with the input, whose border it colors.\n const { bulmaHelperClasses, rest } = useBulmaClasses(others);\n const disabled = useFieldsetDisabled(disabledProp);\n // The toggle gives way to the loading spinner of a Control this sits in,\n // which takes its slot, and the field hides the password meanwhile, as\n // there's no button to hide it with.\n const controlLoading = useControlLoading();\n const isText = variant === 'text';\n\n const showPassword = labels?.showPassword || 'Show password';\n const hidePassword = labels?.hidePassword || 'Hide password';\n const show = labels?.show || 'Show';\n const hide = labels?.hide || 'Hide';\n if (isText) {\n checkLabelInName(showPassword, show);\n checkLabelInName(hidePassword, hide);\n }\n\n const isControlled = visibleProp !== undefined;\n const [internalVisible, setInternalVisible] = useState(defaultVisible);\n const visible = isControlled ? visibleProp : internalVisible;\n // Whether the field shows now: as last reported, until `visible` changes.\n // Two hide events before React renders, as from an effect, or a caller\n // holding `visible` that keeps it `true`, report `false` once.\n const shownRef = useRef(visible);\n useClientLayoutEffect(() => {\n shownRef.current = visible;\n }, [visible]);\n const setVisible = (next: boolean) => {\n shownRef.current = next;\n if (!isControlled) setInternalVisible(next);\n onVisibleChange?.(next);\n };\n\n const inputRef = useRef<HTMLInputElement | null>(null);\n // A caller's callback ref may return a cleanup, as React 19 allows. It's\n // held here and run on detach in place of a call with `null`, on React 18\n // as well, since this ref is the one React calls.\n const refCleanup = useRef<(() => void) | null>(null);\n const combinedRef = useCallback(\n (node: HTMLInputElement | null) => {\n inputRef.current = node;\n if (typeof ref === 'function') {\n if (node === null) {\n const cleanup = refCleanup.current;\n refCleanup.current = null;\n if (cleanup) cleanup();\n else ref(null);\n return;\n }\n const cleanup: unknown = ref(node);\n refCleanup.current =\n typeof cleanup === 'function' ? (cleanup as () => void) : null;\n } else if (ref) {\n ref.current = node;\n }\n },\n [ref]\n );\n\n // A toggle with focus that leaves while the input stays, as it does when a\n // Control starts loading, hands focus to the input rather than dropping it\n // to the page. React lets go of the toggle before it takes it out of the\n // page, so the toggle still has focus here; when the whole field unmounts,\n // React has let go of the input first.\n const toggleRef = useRef<HTMLButtonElement | null>(null);\n const setToggle = useCallback((node: HTMLButtonElement | null) => {\n // React lets go only of a toggle it handed over, so there is one.\n const leaving = toggleRef.current as HTMLButtonElement;\n toggleRef.current = node;\n if (node) return;\n const root = leaving.getRootNode() as Document | ShadowRoot;\n if (root.activeElement === leaving) inputRef.current?.focus();\n }, []);\n\n // Hides the password again: reports `false`, and sets its own state when it\n // holds it. Inside `flushSync`, React renders that, a caller's own state\n // update included, before the listener returns, so the browser reads the\n // form, or saves the page's form state, from a password field. The\n // component never sets the field's type itself, so a caller holding\n // `visible` that keeps it `true` keeps the field shown. The one catch: an\n // event fired while React is rendering or committing, which includes\n // `useEffect` and `useLayoutEffect` callbacks (a `requestSubmit()` from\n // an effect), can't flush there, so React warns in development and the\n // hide lands after the form is read. Only the listeners below call it,\n // while the input is mounted.\n const hideRef = useLatestRef(() => {\n if (!shownRef.current) return;\n // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the form is read, or the page saved, before React would render this on its own\n flushSync(() => setVisible(false));\n });\n\n // When the form it belongs to submits or resets: whichever form that is\n // when it happens, so a form mounted after the field, or mounted again,\n // counts. Neither event leaves a shadow root, so it listens on the field's\n // own root, the document or the shadow root it renders in, and in the\n // capture phase, before any listener on the form.\n useEffect(() => {\n const input = inputRef.current as HTMLInputElement;\n const root = input.getRootNode();\n const onFormEvent = (event: Event) => {\n if (event.target === input.form) hideRef.current();\n };\n root.addEventListener('submit', onFormEvent, true);\n root.addEventListener('reset', onFormEvent, true);\n return () => {\n root.removeEventListener('submit', onFormEvent, true);\n root.removeEventListener('reset', onFormEvent, true);\n };\n }, [hideRef]);\n\n // When the page is left, so the history the browser keeps of the form\n // holds a password field rather than text. It fires on the way into the\n // back-forward cache too, so a page coming back from there is hidden.\n useEffect(() => {\n const onPageHide = () => hideRef.current();\n window.addEventListener('pagehide', onPageHide);\n return () => window.removeEventListener('pagehide', onPageHide);\n }, [hideRef]);\n\n const containerClass = usePrefixedClassNames('password-input-container', {\n 'is-text': isText,\n });\n const toggleClass = usePrefixedClassNames('password-input-toggle');\n\n // The icon keeps one name and reports its state; the words change, so\n // the name follows them and there's no state to report.\n const toggleName = isText && visible ? hidePassword : showPassword;\n const glyph = visible ? <EyeOffIcon /> : <EyeIcon />;\n\n return (\n <div\n className={classNames(containerClass, bulmaHelperClasses, className)}\n style={style}\n >\n <InputBase\n ref={combinedRef}\n autoComplete={autoComplete}\n spellCheck={spellCheck}\n autoCorrect={autoCorrect}\n autoCapitalize={autoCapitalize}\n disabled={disabled}\n color={color}\n {...rest}\n type={visible && !controlLoading ? 'text' : 'password'}\n />\n {!controlLoading && (\n <button\n ref={setToggle}\n type=\"button\"\n className={toggleClass}\n aria-label={toggleName}\n aria-pressed={isText ? undefined : visible}\n disabled={disabled}\n // A pointer press leaves focus, and the caret, in the input.\n onMouseDown={event => event.preventDefault()}\n onClick={() => setVisible(!visible)}\n >\n {isText ? (visible ? hide : show) : glyph}\n </button>\n )}\n </div>\n );\n});\n\nPasswordInputBase.displayName = 'PasswordInputBase';\n\nexport default PasswordInputBase;\n"],"names":["_jsx","_jsxs"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
2
+ 'use client';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ // INTERNAL: deliberately not exported from src/index.ts. The chevrons the
6
+ // Carousel's and the Lightbox's previous and next buttons draw, in one place
7
+ // so the two look the same.
8
+ /**
9
+ * Chevron pointing left, for a previous button.
10
+ *
11
+ * @function
12
+ * @returns {JSX.Element} The rendered SVG chevron-left icon.
13
+ */
14
+ const ChevronPrevIcon = () => (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("polyline", { points: "15 18 9 12 15 6" }) }));
15
+ /**
16
+ * Chevron pointing right, for a next button.
17
+ *
18
+ * @function
19
+ * @returns {JSX.Element} The rendered SVG chevron-right icon.
20
+ */
21
+ const ChevronNextIcon = () => (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsx("polyline", { points: "9 18 15 12 9 6" }) }));
22
+
23
+ export { ChevronNextIcon, ChevronPrevIcon };
24
+ //# sourceMappingURL=chevronIcons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chevronIcons.js","sources":["../../../src/helpers/chevronIcons.tsx"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts. The chevrons the\n// Carousel's and the Lightbox's previous and next buttons draw, in one place\n// so the two look the same.\n\n/**\n * Chevron pointing left, for a previous button.\n *\n * @function\n * @returns {JSX.Element} The rendered SVG chevron-left icon.\n */\nexport const ChevronPrevIcon = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n >\n <polyline points=\"15 18 9 12 15 6\" />\n </svg>\n);\n\n/**\n * Chevron pointing right, for a next button.\n *\n * @function\n * @returns {JSX.Element} The rendered SVG chevron-right icon.\n */\nexport const ChevronNextIcon = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n >\n <polyline points=\"9 18 15 12 9 6\" />\n </svg>\n);\n"],"names":["_jsx"],"mappings":";;;;AAAA;AACA;AACA;AAEA;;;;;AAKG;AACI,MAAM,eAAe,GAAG,OAC7BA,GAAA,CAAA,KAAA,EAAA,EACE,KAAK,EAAC,4BAA4B,EAClC,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EAAA,QAAA,EAEfA,GAAA,CAAA,UAAA,EAAA,EAAU,MAAM,EAAC,iBAAiB,EAAA,CAAG,EAAA,CACjC;AAGR;;;;;AAKG;AACI,MAAM,eAAe,GAAG,OAC7BA,GAAA,CAAA,KAAA,EAAA,EACE,KAAK,EAAC,4BAA4B,EAClC,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EAAA,QAAA,EAEfA,GAAA,CAAA,UAAA,EAAA,EAAU,MAAM,EAAC,gBAAgB,EAAA,CAAG,EAAA,CAChC;;"}
@@ -0,0 +1,34 @@
1
+ /* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
2
+ 'use client';
3
+ import { useState, useEffect } from 'react';
4
+
5
+ // INTERNAL: deliberately not exported from src/index.ts.
6
+ /**
7
+ * Detects the user's `prefers-reduced-motion` preference, for motion that
8
+ * runs in script, where a stylesheet's media query can't reach it. Always
9
+ * `false` on the server and on the very first client render, so SSR output
10
+ * and the initial hydration pass always agree.
11
+ *
12
+ * @function
13
+ * @returns {boolean} Whether the user has requested reduced motion.
14
+ */
15
+ function usePrefersReducedMotion() {
16
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
17
+ // An effect, so it only ever runs in the browser.
18
+ useEffect(() => {
19
+ if (!window.matchMedia)
20
+ return undefined;
21
+ const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
22
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- reads the live media query on mount
23
+ setPrefersReducedMotion(mediaQuery.matches);
24
+ const handleChange = (event) => {
25
+ setPrefersReducedMotion(event.matches);
26
+ };
27
+ mediaQuery.addEventListener('change', handleChange);
28
+ return () => mediaQuery.removeEventListener('change', handleChange);
29
+ }, []);
30
+ return prefersReducedMotion;
31
+ }
32
+
33
+ export { usePrefersReducedMotion };
34
+ //# sourceMappingURL=usePrefersReducedMotion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePrefersReducedMotion.js","sources":["../../../src/helpers/usePrefersReducedMotion.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useEffect, useState } from 'react';\n\n/**\n * Detects the user's `prefers-reduced-motion` preference, for motion that\n * runs in script, where a stylesheet's media query can't reach it. Always\n * `false` on the server and on the very first client render, so SSR output\n * and the initial hydration pass always agree.\n *\n * @function\n * @returns {boolean} Whether the user has requested reduced motion.\n */\nexport function usePrefersReducedMotion(): boolean {\n const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);\n\n // An effect, so it only ever runs in the browser.\n useEffect(() => {\n if (!window.matchMedia) return undefined;\n\n const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');\n // eslint-disable-next-line react-hooks/set-state-in-effect -- reads the live media query on mount\n setPrefersReducedMotion(mediaQuery.matches);\n\n const handleChange = (event: MediaQueryListEvent) => {\n setPrefersReducedMotion(event.matches);\n };\n\n mediaQuery.addEventListener('change', handleChange);\n return () => mediaQuery.removeEventListener('change', handleChange);\n }, []);\n\n return prefersReducedMotion;\n}\n"],"names":[],"mappings":";;;;AAAA;AAGA;;;;;;;;AAQG;SACa,uBAAuB,GAAA;IACrC,MAAM,CAAC,oBAAoB,EAAE,uBAAuB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;;IAGvE,SAAS,CAAC,MAAK;QACb,IAAI,CAAC,MAAM,CAAC,UAAU;AAAE,YAAA,OAAO,SAAS;QAExC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,kCAAkC,CAAC;;AAExE,QAAA,uBAAuB,CAAC,UAAU,CAAC,OAAO,CAAC;AAE3C,QAAA,MAAM,YAAY,GAAG,CAAC,KAA0B,KAAI;AAClD,YAAA,uBAAuB,CAAC,KAAK,CAAC,OAAO,CAAC;AACxC,QAAA,CAAC;AAED,QAAA,UAAU,CAAC,gBAAgB,CAAC,QAAQ,EAAE,YAAY,CAAC;QACnD,OAAO,MAAM,UAAU,CAAC,mBAAmB,CAAC,QAAQ,EAAE,YAAY,CAAC;IACrE,CAAC,EAAE,EAAE,CAAC;AAEN,IAAA,OAAO,oBAAoB;AAC7B;;"}