@abuzareal/contour-ui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CONTRACTS.md +7 -1
  3. package/NEXTJS.md +3 -1
  4. package/OVERLAYS.md +52 -0
  5. package/PUBLIC-API.md +179 -162
  6. package/README.md +40 -26
  7. package/RELEASING.md +1 -1
  8. package/SIZE.md +45 -0
  9. package/STYLING.md +68 -0
  10. package/dist/actions.css +7 -0
  11. package/dist/components/hero/Sculpture.js +2 -1
  12. package/dist/components/three/ObjectStudy.js +2 -1
  13. package/dist/components/ui/Alert.d.ts +2 -1
  14. package/dist/components/ui/Alert.js +3 -3
  15. package/dist/components/ui/Avatar.d.ts +3 -1
  16. package/dist/components/ui/Avatar.js +3 -3
  17. package/dist/components/ui/Badge.js +2 -2
  18. package/dist/components/ui/BrandMark.js +2 -1
  19. package/dist/components/ui/Breadcrumbs.d.ts +2 -1
  20. package/dist/components/ui/Breadcrumbs.js +3 -2
  21. package/dist/components/ui/Button.d.ts +2 -0
  22. package/dist/components/ui/Button.js +3 -2
  23. package/dist/components/ui/Card.js +2 -2
  24. package/dist/components/ui/Checkbox.js +3 -3
  25. package/dist/components/ui/CountUp.d.ts +2 -1
  26. package/dist/components/ui/CountUp.js +6 -3
  27. package/dist/components/ui/DataTable.d.ts +1 -0
  28. package/dist/components/ui/DataTable.js +12 -8
  29. package/dist/components/ui/DepthButton.js +2 -2
  30. package/dist/components/ui/Dialog.d.ts +5 -1
  31. package/dist/components/ui/Dialog.js +49 -18
  32. package/dist/components/ui/Disclosure.js +2 -1
  33. package/dist/components/ui/Divider.js +3 -2
  34. package/dist/components/ui/DropdownMenu.d.ts +11 -4
  35. package/dist/components/ui/DropdownMenu.js +109 -66
  36. package/dist/components/ui/EmptyState.js +2 -1
  37. package/dist/components/ui/Field.js +2 -2
  38. package/dist/components/ui/FlipCard.js +2 -2
  39. package/dist/components/ui/FloatingPortal.d.ts +10 -0
  40. package/dist/components/ui/FloatingPortal.js +51 -0
  41. package/dist/components/ui/IconButton.js +2 -2
  42. package/dist/components/ui/Kbd.js +2 -1
  43. package/dist/components/ui/Magnetic.js +2 -1
  44. package/dist/components/ui/Marquee.js +3 -2
  45. package/dist/components/ui/OverlayTrigger.d.ts +11 -0
  46. package/dist/components/ui/OverlayTrigger.js +5 -0
  47. package/dist/components/ui/Pagination.d.ts +5 -1
  48. package/dist/components/ui/Pagination.js +3 -2
  49. package/dist/components/ui/Popover.d.ts +18 -8
  50. package/dist/components/ui/Popover.js +58 -17
  51. package/dist/components/ui/ProgressBar.js +2 -2
  52. package/dist/components/ui/RadioGroup.js +5 -5
  53. package/dist/components/ui/Reveal.d.ts +0 -1
  54. package/dist/components/ui/Reveal.js +4 -3
  55. package/dist/components/ui/RevealHeading.js +2 -1
  56. package/dist/components/ui/ScrollProgress.js +2 -1
  57. package/dist/components/ui/SectionLabel.js +2 -1
  58. package/dist/components/ui/SegmentedControl.js +6 -3
  59. package/dist/components/ui/SelectField.js +3 -2
  60. package/dist/components/ui/Skeleton.js +2 -2
  61. package/dist/components/ui/Slider.js +3 -3
  62. package/dist/components/ui/Spinner.js +2 -2
  63. package/dist/components/ui/Stat.js +2 -1
  64. package/dist/components/ui/Stepper.js +3 -3
  65. package/dist/components/ui/Switch.d.ts +2 -0
  66. package/dist/components/ui/Switch.js +6 -7
  67. package/dist/components/ui/Tabs.js +7 -5
  68. package/dist/components/ui/Tag.d.ts +2 -1
  69. package/dist/components/ui/Tag.js +4 -3
  70. package/dist/components/ui/TextArea.js +2 -1
  71. package/dist/components/ui/TextField.js +3 -2
  72. package/dist/components/ui/ThemeScope.d.ts +13 -0
  73. package/dist/components/ui/ThemeScope.js +10 -0
  74. package/dist/components/ui/ThemeToggle.d.ts +3 -0
  75. package/dist/components/ui/ThemeToggle.js +4 -3
  76. package/dist/components/ui/TiltCard.js +3 -2
  77. package/dist/components/ui/Timeline.js +2 -2
  78. package/dist/components/ui/ToastProvider.d.ts +5 -2
  79. package/dist/components/ui/ToastProvider.js +5 -5
  80. package/dist/components/ui/Tooltip.d.ts +4 -4
  81. package/dist/components/ui/Tooltip.js +70 -10
  82. package/dist/data-display.css +5 -0
  83. package/dist/effects.css +6 -0
  84. package/dist/feedback.css +5 -0
  85. package/dist/fonts-latin.css +10 -4
  86. package/dist/fonts.css +8 -0
  87. package/dist/forms.css +5 -0
  88. package/dist/hooks/useDismiss.d.ts +12 -3
  89. package/dist/hooks/useDismiss.js +5 -21
  90. package/dist/hooks/useFloatingPanel.d.ts +14 -0
  91. package/dist/hooks/useFloatingPanel.js +157 -0
  92. package/dist/hooks/useLayerDismiss.d.ts +4 -0
  93. package/dist/hooks/useLayerDismiss.js +48 -0
  94. package/dist/hooks/useMobileMenu.d.ts +0 -1
  95. package/dist/hooks/useMobileMenu.js +11 -16
  96. package/dist/hooks/usePanelFocus.d.ts +10 -0
  97. package/dist/hooks/usePanelFocus.js +60 -0
  98. package/dist/hooks/useTheme.d.ts +1 -0
  99. package/dist/hooks/useTheme.js +18 -5
  100. package/dist/index.d.ts +5 -0
  101. package/dist/index.js +1 -0
  102. package/dist/kit/forms.css +11 -2
  103. package/dist/kit/navigation.css +4 -0
  104. package/dist/kit/overlays.css +8 -4
  105. package/dist/lib/classNames.d.ts +2 -0
  106. package/dist/lib/classNames.js +16 -1
  107. package/dist/lib/componentClasses.d.ts +1 -0
  108. package/dist/lib/componentClasses.js +2 -0
  109. package/dist/lib/keyboard.d.ts +1 -1
  110. package/dist/lib/keyboard.js +5 -2
  111. package/dist/lib/layers.d.ts +18 -0
  112. package/dist/lib/layers.js +128 -0
  113. package/dist/lib/themeScope.d.ts +5 -0
  114. package/dist/lib/themeScope.js +3 -0
  115. package/dist/minimal-reset.css +6 -0
  116. package/dist/navigation.css +5 -0
  117. package/dist/overlays.css +5 -0
  118. package/dist/scoped/base.css +183 -0
  119. package/dist/scoped/kit/actions.css +209 -0
  120. package/dist/scoped/kit/data-display.css +492 -0
  121. package/dist/scoped/kit/effects.css +140 -0
  122. package/dist/scoped/kit/feedback.css +309 -0
  123. package/dist/scoped/kit/forms.css +370 -0
  124. package/dist/scoped/kit/navigation.css +266 -0
  125. package/dist/scoped/kit/overlays.css +299 -0
  126. package/dist/scoped/marquee.css +58 -0
  127. package/dist/scoped/spinner.css +20 -0
  128. package/dist/scoped/theme-toggle.css +38 -0
  129. package/dist/scoped/tokens.css +119 -0
  130. package/dist/scoped/ui.css +127 -0
  131. package/dist/scoped/utilities.css +17 -0
  132. package/dist/scoped-tokens.css +1 -0
  133. package/dist/scoped.css +12 -0
  134. package/dist/theme-toggle.css +2 -2
  135. package/dist/tokens.json +710 -0
  136. package/package.json +26 -5
@@ -1,28 +1,38 @@
1
- /** Nonmodal local panel; root and existing trigger/panel props preserve owned state wiring. */
2
- import { type ReactNode } from "react";
1
+ import { type ReactNode, type ReactElement } from "react";
2
+ import { type PanelFocusOptions } from "../../hooks/usePanelFocus.js";
3
+ import type { FloatingOptions } from "../../hooks/useFloatingPanel.js";
4
+ import { type DismissReason } from "../../lib/layers.js";
3
5
  import type { NativePartProps } from "../../lib/componentProps.js";
4
- export type PopoverProps = Omit<NativePartProps<"div">, "ref" | "title"> & {
5
- /** Content inside the owned trigger button, not a replacement interactive element. */
6
+ export type PopoverProps = Omit<NativePartProps<"div">, "ref" | "title"> & FloatingOptions & PanelFocusOptions & {
6
7
  trigger: ReactNode;
7
8
  title: string;
8
9
  children: ReactNode;
9
- align?: "start" | "end";
10
10
  open?: boolean;
11
11
  defaultOpen?: boolean;
12
12
  onOpenChange?: (open: boolean) => void;
13
13
  triggerProps?: NativePartProps<"button">;
14
14
  panelProps?: NativePartProps<"div">;
15
+ /** Render one ref-forwarding native button with the supplied owned props. */
16
+ renderTrigger?: (props: NativePartProps<"button"> & {
17
+ children: ReactNode;
18
+ }) => ReactElement;
19
+ /** preventDefault vetoes dismissal. Native document events still propagate. */
20
+ onDismiss?: (reason: DismissReason, event: KeyboardEvent | PointerEvent) => void;
15
21
  };
16
- declare const Popover: import("react").ForwardRefExoticComponent<Omit<NativePartProps<"div">, "title" | "ref"> & {
17
- /** Content inside the owned trigger button, not a replacement interactive element. */
22
+ declare const Popover: import("react").ForwardRefExoticComponent<Omit<NativePartProps<"div">, "title" | "ref"> & FloatingOptions & PanelFocusOptions & {
18
23
  trigger: ReactNode;
19
24
  title: string;
20
25
  children: ReactNode;
21
- align?: "start" | "end";
22
26
  open?: boolean;
23
27
  defaultOpen?: boolean;
24
28
  onOpenChange?: (open: boolean) => void;
25
29
  triggerProps?: NativePartProps<"button">;
26
30
  panelProps?: NativePartProps<"div">;
31
+ /** Render one ref-forwarding native button with the supplied owned props. */
32
+ renderTrigger?: (props: NativePartProps<"button"> & {
33
+ children: ReactNode;
34
+ }) => ReactElement;
35
+ /** preventDefault vetoes dismissal. Native document events still propagate. */
36
+ onDismiss?: (reason: DismissReason, event: KeyboardEvent | PointerEvent) => void;
27
37
  } & import("react").RefAttributes<HTMLDivElement>>;
28
38
  export default Popover;
@@ -1,27 +1,68 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- /** Nonmodal local panel; root and existing trigger/panel props preserve owned state wiring. */
4
- import { forwardRef, useCallback, useId } from "react";
3
+ import { useContext, useLayoutEffect, forwardRef, useId, useMemo, useRef, useState, } from "react";
5
4
  import useOpenState from "../../hooks/useOpenState.js";
6
- import useDismiss from "../../hooks/useDismiss.js";
5
+ import useDismiss from "../../hooks/useLayerDismiss.js";
7
6
  import useControlRef from "../../hooks/useControlRef.js";
8
- import { cx } from "../../lib/classNames.js";
9
- import { composeEventHandlers } from "../../lib/composeEventHandlers.js";
10
- const Popover = forwardRef(function Popover({ trigger, title, children, align = "start", open: controlledOpen, defaultOpen = false, onOpenChange, className, triggerProps, panelProps, ...rest }, forwardedRef) {
11
- const generatedPanelId = useId();
7
+ import usePanelFocus from "../../hooks/usePanelFocus.js";
8
+ import { cx, ccx } from "../../lib/classNames.js";
9
+ import { composeRefs } from "../../lib/composeRefs.js";
10
+ import { LayerVisibility, LayerParent, } from "../../lib/layers.js";
11
+ import OverlayTrigger from "./OverlayTrigger.js";
12
+ import FloatingPortal from "./FloatingPortal.js";
13
+ const Popover = forwardRef(function Popover({ trigger, title, children, align = "start", side = "bottom", sideOffset = 8, collisionPadding = 8, portal = true, container: portalContainer, initialFocus, onOpenAutoFocus, onCloseAutoFocus, renderTrigger, onDismiss, open: controlledOpen, defaultOpen = false, onOpenChange, className, triggerProps, panelProps, ...rest }, forwardedRef) {
14
+ const generatedPanelId = useId(), titleId = useId();
12
15
  const panelId = panelProps?.id ?? generatedPanelId;
13
- const titleId = useId();
14
16
  const [open, setOpen] = useOpenState(controlledOpen, defaultOpen, onOpenChange);
15
- const { ref: container, attach: attachContainer } = useControlRef(forwardedRef);
17
+ const parentVisible = useContext(LayerVisibility);
18
+ const visibleOpen = open && parentVisible;
19
+ const { ref: root, attach: attachRoot } = useControlRef(forwardedRef);
16
20
  const { ref: button, attach: attachButton } = useControlRef(triggerProps?.ref ?? null);
17
- useDismiss(container, open, useCallback((reason) => {
21
+ const [anchorElement, setAnchorElement] = useState(null);
22
+ const attachAnchor = useMemo(() => composeRefs(attachButton, setAnchorElement), [attachButton]);
23
+ const panel = useRef(null);
24
+ const attachPanel = useMemo(() => composeRefs(panel, panelProps?.ref), [panelProps?.ref]);
25
+ const reason = useRef({ value: undefined });
26
+ const layer = useDismiss(root, visibleOpen, (why, event) => {
27
+ onDismiss?.(why, event);
28
+ if (event.defaultPrevented)
29
+ return;
30
+ reason.current.value = why;
18
31
  setOpen(false);
19
- if (reason === "escape")
20
- button.current?.focus();
21
- }, [setOpen, button]));
22
- return (_jsxs("div", { ...rest, ref: attachContainer, className: cx("popover", `popover-${align}`, className), children: [_jsx("button", { ...triggerProps, ref: attachButton, type: "button", className: cx("menu-trigger", triggerProps?.className), "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": panelId, onClick: composeEventHandlers(triggerProps?.onClick, () => {
23
- if (!triggerProps?.disabled)
24
- setOpen(!open);
25
- }), children: trigger }), _jsxs("div", { ...panelProps, id: panelId, role: "dialog", "aria-labelledby": cx(titleId, panelProps?.["aria-labelledby"]), className: cx("popover-panel", panelProps?.className), hidden: !open, children: [_jsx("strong", { id: titleId, className: "popover-title", children: title }), children] })] }));
32
+ if (why === "escape" && !onCloseAutoFocus && button.current?.isConnected)
33
+ button.current.focus();
34
+ }, { branches: [panel, button], layerElement: panel });
35
+ useLayoutEffect(() => {
36
+ if (visibleOpen)
37
+ reason.current.value = undefined;
38
+ }, [visibleOpen, anchorElement]);
39
+ usePanelFocus(visibleOpen, button, panel, reason, { initialFocus, onOpenAutoFocus, onCloseAutoFocus }, anchorElement, layer);
40
+ const ownedTrigger = {
41
+ ...triggerProps,
42
+ ref: attachAnchor,
43
+ type: "button",
44
+ className: ccx("menu-trigger", triggerProps?.className),
45
+ "aria-haspopup": "dialog",
46
+ "aria-expanded": visibleOpen,
47
+ "aria-controls": panelId,
48
+ children: trigger,
49
+ onClick: (event) => {
50
+ triggerProps?.onClick?.(event);
51
+ if (event.defaultPrevented)
52
+ return;
53
+ if (!triggerProps?.disabled) {
54
+ reason.current.value = "trigger";
55
+ setOpen(!open);
56
+ }
57
+ },
58
+ };
59
+ return (_jsx(LayerVisibility.Provider, { value: visibleOpen, children: _jsx(LayerParent.Provider, { value: layer, children: _jsxs("div", { ...rest, ref: attachRoot, className: ccx("popover", `popover-${align}`, className), children: [_jsx(OverlayTrigger, { renderer: renderTrigger, props: ownedTrigger }), _jsx(FloatingPortal, { anchorElement: anchorElement, anchor: button, panel: panel, open: visibleOpen, options: {
60
+ side,
61
+ align,
62
+ sideOffset,
63
+ collisionPadding,
64
+ portal,
65
+ container: portalContainer,
66
+ }, children: _jsxs("div", { ...panelProps, ref: attachPanel, id: panelId, role: "dialog", tabIndex: panelProps?.tabIndex ?? -1, "aria-labelledby": cx(titleId, panelProps?.["aria-labelledby"]), className: ccx("popover-panel", panelProps?.className), hidden: !visibleOpen, children: [_jsx("strong", { id: titleId, className: ccx("popover-title"), children: title }), children] }) })] }) }) }));
26
67
  });
27
68
  export default Popover;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { cx } from "../../lib/classNames.js";
2
+ import { ccx } from "../../lib/classNames.js";
3
3
  export default function ProgressBar({ label, value, tone = "accent", }) {
4
4
  const indeterminate = value === undefined;
5
- return (_jsxs("div", { className: cx("progress", `progress-${tone}`), children: [_jsxs("div", { className: "progress-header mono", children: [_jsx("span", { children: label }), !indeterminate && _jsxs("span", { children: [Math.round(value), "%"] })] }), _jsx("div", { role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": indeterminate ? undefined : value, className: cx("progress-track", indeterminate && "progress-indeterminate"), style: { "--progress": `${value ?? 0}%` }, children: _jsx("span", { className: "progress-fill" }) })] }));
5
+ return (_jsxs("div", { className: ccx("progress", `progress-${tone}`), children: [_jsxs("div", { className: ccx("progress-header mono"), children: [_jsx("span", { children: label }), !indeterminate && _jsxs("span", { children: [Math.round(value), "%"] })] }), _jsx("div", { role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": indeterminate ? undefined : value, className: ccx("progress-track", indeterminate && "progress-indeterminate"), style: { "--progress": `${value ?? 0}%` }, children: _jsx("span", { className: ccx("progress-fill") }) })] }));
6
6
  }
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /** Native radios with caller form/name, controlled/default selection and option input refs. */
4
4
  import { useId, useLayoutEffect, useRef, useState, } from "react";
5
- import { cx } from "../../lib/classNames.js";
5
+ import { cx, ccx } from "../../lib/classNames.js";
6
6
  import useFormReset from "../../hooks/useFormReset.js";
7
7
  export default function RadioGroup({ legend, options, value, defaultValue, onChange, inline = false, name: providedName, form, id: providedId, disabled, required, description, error, className, "aria-describedby": describedBy, ...rest }) {
8
8
  const generatedId = useId();
@@ -25,15 +25,15 @@ export default function RadioGroup({ legend, options, value, defaultValue, onCha
25
25
  });
26
26
  });
27
27
  const context = cx(describedBy, description && `${id}-description`, error && `${id}-error`) || undefined;
28
- return (_jsxs("fieldset", { ...rest, ref: ref, id: id, name: name, form: form, disabled: disabled, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": context, className: cx("radio-group", inline && "radio-group-inline", className), children: [_jsx("legend", { className: "field-label mono", children: legend }), description && (_jsx("span", { id: `${id}-description`, className: "choice-description", children: description })), options.map((option) => {
28
+ return (_jsxs("fieldset", { ...rest, ref: ref, id: id, name: name, form: form, disabled: disabled, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": context, className: ccx("radio-group", inline && "radio-group-inline", className), children: [_jsx("legend", { className: ccx("field-label mono"), children: legend }), description && (_jsx("span", { id: `${id}-description`, className: ccx("choice-description"), children: description })), options.map((option) => {
29
29
  const optionId = option.inputProps?.id ?? `${id}-${option.value}`;
30
- return (_jsxs("div", { className: "choice choice-radio", children: [_jsxs("span", { className: "choice-control", children: [_jsx("input", { ...option.inputProps, id: optionId, type: "radio", name: name, form: form, value: option.value, checked: value === undefined ? undefined : option.value === value, defaultChecked: value === undefined ? option.value === initial : undefined, required: required, disabled: disabled || option.disabled, className: cx("choice-input", option.inputProps?.className), "aria-invalid": error
30
+ return (_jsxs("div", { className: ccx("choice choice-radio"), children: [_jsxs("span", { className: ccx("choice-control"), children: [_jsx("input", { ...option.inputProps, id: optionId, type: "radio", name: name, form: form, value: option.value, checked: value === undefined ? undefined : option.value === value, defaultChecked: value === undefined ? option.value === initial : undefined, required: required, disabled: disabled || option.disabled, className: ccx("choice-input", option.inputProps?.className), "aria-invalid": error
31
31
  ? true
32
32
  : (option.inputProps?.["aria-invalid"] ??
33
33
  rest["aria-invalid"]), "aria-describedby": cx(option.inputProps?.["aria-describedby"], context, option.description && `${optionId}-description`) || undefined, onChange: (event) => {
34
34
  if (!event.currentTarget.checked)
35
35
  return;
36
36
  onChange?.(option.value);
37
- } }), _jsx("span", { className: "choice-box", "aria-hidden": "true" })] }), _jsxs("label", { htmlFor: optionId, className: "choice-text", children: [_jsx("span", { className: "choice-label", children: option.label }), option.description && (_jsx("span", { id: `${optionId}-description`, className: "choice-description", children: option.description }))] })] }, option.value));
38
- }), error && (_jsx("span", { id: `${id}-error`, className: "choice-error mono", children: error }))] }));
37
+ } }), _jsx("span", { className: ccx("choice-box"), "aria-hidden": "true" })] }), _jsxs("label", { htmlFor: optionId, className: ccx("choice-text"), children: [_jsx("span", { className: ccx("choice-label"), children: option.label }), option.description && (_jsx("span", { id: `${optionId}-description`, className: ccx("choice-description"), children: option.description }))] })] }, option.value));
38
+ }), error && (_jsx("span", { id: `${id}-error`, className: ccx("choice-error mono"), children: error }))] }));
39
39
  }
@@ -1,4 +1,3 @@
1
- /** Fades and lifts its children into view the first time they enter the viewport. */
2
1
  import { type ReactNode } from "react";
3
2
  export type RevealProps = {
4
3
  children: ReactNode;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  /** Fades and lifts its children into view the first time they enter the viewport. */
4
+ import { ccx } from "../../lib/classNames.js";
4
5
  import { useEffect, useRef } from "react";
5
6
  export default function Reveal({ children, className = "", delay = 0, }) {
6
7
  const ref = useRef(null);
@@ -9,17 +10,17 @@ export default function Reveal({ children, className = "", delay = 0, }) {
9
10
  if (!element)
10
11
  return;
11
12
  if (typeof IntersectionObserver === "undefined") {
12
- element.classList.add("is-visible");
13
+ element.classList.add("is-visible", "contour-is-visible");
13
14
  return;
14
15
  }
15
16
  const observer = new IntersectionObserver(([entry]) => {
16
17
  if (!entry.isIntersecting)
17
18
  return;
18
- element.classList.add("is-visible");
19
+ element.classList.add("is-visible", "contour-is-visible");
19
20
  observer.disconnect();
20
21
  }, { threshold: 0, rootMargin: "0px 0px -24px 0px" });
21
22
  observer.observe(element);
22
23
  return () => observer.disconnect();
23
24
  }, []);
24
- return (_jsx("div", { ref: ref, className: `reveal ${className}`, style: { transitionDelay: `${delay}ms` }, children: children }));
25
+ return (_jsx("div", { ref: ref, className: ccx("reveal", className), style: { transitionDelay: `${delay}ms` }, children: children }));
25
26
  }
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ccx } from "../../lib/classNames.js";
2
3
  export default function RevealHeading({ id, lines }) {
3
- return (_jsx("h2", { id: id, children: lines.map((line, index) => (_jsx("span", { className: line.accent ? "type-line type-accent" : "type-line", children: _jsx("span", { className: "type-run", children: line.text }) }, index))) }));
4
+ return (_jsx("h2", { id: id, children: lines.map((line, index) => (_jsx("span", { className: ccx(line.accent ? "type-line type-accent" : "type-line"), children: _jsx("span", { className: ccx("type-run"), children: line.text }) }, index))) }));
4
5
  }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ccx } from "../../lib/classNames.js";
3
4
  /** Thin acid bar fixed to the top of the viewport that tracks page scroll progress. */
4
5
  import { useEffect, useRef } from "react";
5
6
  export default function ScrollProgress() {
@@ -24,5 +25,5 @@ export default function ScrollProgress() {
24
25
  window.removeEventListener("resize", update);
25
26
  };
26
27
  }, []);
27
- return _jsx("div", { ref: ref, className: "scroll-progress", "aria-hidden": "true" });
28
+ return (_jsx("div", { ref: ref, className: ccx("scroll-progress"), "aria-hidden": "true" }));
28
29
  }
@@ -1,5 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { ccx } from "../../lib/classNames.js";
2
3
  import { formatIndex } from "../../lib/format.js";
3
4
  export default function SectionLabel({ section, aside }) {
4
- return (_jsxs("div", { className: "section-label mono", children: [_jsxs("span", { children: [formatIndex(section.number), " / ", section.title] }), _jsx("span", { children: aside })] }));
5
+ return (_jsxs("div", { className: ccx("section-label mono"), children: [_jsxs("span", { children: [formatIndex(section.number), " / ", section.title] }), _jsx("span", { children: aside })] }));
5
6
  }
@@ -1,16 +1,19 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ccx } from "../../lib/classNames.js";
3
4
  /** Single-choice pill group (filter buttons) with arrow-key navigation. */
4
5
  import { useRef } from "react";
5
6
  import { nextIndexForKey } from "../../lib/keyboard.js";
6
7
  export default function SegmentedControl({ label, options, value, onChange, }) {
7
8
  const buttons = useRef([]);
8
- return (_jsx("div", { className: "segmented", role: "radiogroup", "aria-label": label, children: options.map((option, index) => {
9
+ return (_jsx("div", { className: ccx("segmented"), role: "radiogroup", "aria-label": label, children: options.map((option, index) => {
9
10
  const selected = option.value === value;
10
11
  return (_jsx("button", { ref: (element) => {
11
12
  buttons.current[index] = element;
12
- }, type: "button", role: "radio", "aria-checked": selected, tabIndex: selected ? 0 : -1, className: "segmented-option", onClick: () => onChange(option.value), onKeyDown: (event) => {
13
- const next = nextIndexForKey(event.key, index, options.length);
13
+ }, type: "button", role: "radio", "aria-checked": selected, tabIndex: selected ? 0 : -1, className: ccx("segmented-option"), onClick: () => onChange(option.value), onKeyDown: (event) => {
14
+ const next = nextIndexForKey(event.key, index, options.length, "horizontal", event.currentTarget.ownerDocument.defaultView?.getComputedStyle(event.currentTarget).direction === "rtl"
15
+ ? "rtl"
16
+ : "ltr");
14
17
  if (next === null)
15
18
  return;
16
19
  event.preventDefault();
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ccx } from "../../lib/classNames.js";
3
4
  /** Native select styled as an underlined field with a chevron. */
4
5
  import { forwardRef, useId } from "react";
5
6
  import { ChevronDown } from "lucide-react";
@@ -22,8 +23,8 @@ const SelectField = forwardRef(function SelectField({ id: providedId, "aria-desc
22
23
  return;
23
24
  restoreSelection(ref.current, rest.value);
24
25
  });
25
- return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, disabled: rest.disabled, children: _jsxs("div", { className: "field-control field-select", children: [_jsxs("select", { ...rest, ref: attach, id: id, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
26
+ return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, disabled: rest.disabled, children: _jsxs("div", { className: ccx("field-control field-select"), children: [_jsxs("select", { ...rest, ref: attach, id: id, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
26
27
  .filter(Boolean)
27
- .join(" ") || undefined, children: [placeholder && (_jsx("option", { value: "", disabled: true, children: placeholder })), options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] }), _jsx(ChevronDown, { className: "field-chevron", size: 16, "aria-hidden": "true" })] }) }));
28
+ .join(" ") || undefined, children: [placeholder && (_jsx("option", { value: "", disabled: true, children: placeholder })), options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] }), _jsx(ChevronDown, { className: ccx("field-chevron"), size: 16, "aria-hidden": "true" })] }) }));
28
29
  });
29
30
  export default SelectField;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cx } from "../../lib/classNames.js";
2
+ import { ccx } from "../../lib/classNames.js";
3
3
  export default function Skeleton({ shape = "line", width, height, }) {
4
- return (_jsx("span", { "aria-hidden": "true", className: cx("skeleton", `skeleton-${shape}`), style: { width, height } }));
4
+ return (_jsx("span", { "aria-hidden": "true", className: ccx("skeleton", `skeleton-${shape}`), style: { width, height } }));
5
5
  }
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /** Native range submission with controlled/default state, reset and a formatted readout. */
4
4
  import { forwardRef, useId, useLayoutEffect, useState, } from "react";
5
- import { cx } from "../../lib/classNames.js";
5
+ import { cx, ccx } from "../../lib/classNames.js";
6
6
  import useControlRef from "../../hooks/useControlRef.js";
7
7
  import useFormReset from "../../hooks/useFormReset.js";
8
8
  const Slider = forwardRef(function Slider({ label, value, defaultValue, onChange, min = 0, max = 100, step = 1, format = String, disabled, id: providedId, className, style, onInput, hint, error, "aria-describedby": describedBy, ...rest }, forwardedRef) {
@@ -34,7 +34,7 @@ const Slider = forwardRef(function Slider({ label, value, defaultValue, onChange
34
34
  const progress = max > min
35
35
  ? Math.min(100, Math.max(0, ((readout - min) / (max - min)) * 100))
36
36
  : 0;
37
- return (_jsxs("div", { className: "slider", children: [_jsxs("div", { className: "slider-header mono", children: [_jsx("label", { htmlFor: id, className: "field-label", children: label }), _jsx("output", { htmlFor: id, children: format(readout) })] }), _jsx("input", { ...rest, ref: attach, id: id, type: "range", min: min, max: max, step: step, value: value, defaultValue: value === undefined ? initial : undefined, disabled: disabled, className: className, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": cx(describedBy, hint && `${id}-hint`, error && `${id}-error`) ||
37
+ return (_jsxs("div", { className: ccx("slider"), children: [_jsxs("div", { className: ccx("slider-header mono"), children: [_jsx("label", { htmlFor: id, className: ccx("field-label"), children: label }), _jsx("output", { htmlFor: id, children: format(readout) })] }), _jsx("input", { ...rest, ref: attach, id: id, type: "range", min: min, max: max, step: step, value: value, defaultValue: value === undefined ? initial : undefined, disabled: disabled, className: className, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": cx(describedBy, hint && `${id}-hint`, error && `${id}-error`) ||
38
38
  undefined, "aria-valuetext": rest["aria-valuetext"] ?? format(readout), style: { ...style, "--progress": `${progress}%` }, onInput: (event) => {
39
39
  if (value === undefined)
40
40
  setReadout(event.currentTarget.valueAsNumber);
@@ -44,6 +44,6 @@ const Slider = forwardRef(function Slider({ label, value, defaultValue, onChange
44
44
  if (value === undefined)
45
45
  setReadout(next);
46
46
  onChange?.(next);
47
- } }), hint && (_jsx("span", { id: `${id}-hint`, className: "choice-description", children: hint })), error && (_jsx("span", { id: `${id}-error`, className: "choice-error mono", children: error }))] }));
47
+ } }), hint && (_jsx("span", { id: `${id}-hint`, className: ccx("choice-description"), children: hint })), error && (_jsx("span", { id: `${id}-error`, className: ccx("choice-error mono"), children: error }))] }));
48
48
  });
49
49
  export default Slider;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** Brand asterisk that spins as a loading indicator; announces its label to assistive tech. */
3
3
  import { Asterisk } from "lucide-react";
4
- import { cx } from "../../lib/classNames.js";
4
+ import { ccx } from "../../lib/classNames.js";
5
5
  const iconSizes = { sm: 16, md: 24, lg: 40 };
6
6
  export default function Spinner({ size = "md", label = "Loading", className, }) {
7
- return (_jsxs("span", { role: "status", className: cx("spinner", `spinner-${size}`, className), children: [_jsx(Asterisk, { size: iconSizes[size], strokeWidth: 1.6, "aria-hidden": "true" }), _jsx("span", { className: "visually-hidden", children: label })] }));
7
+ return (_jsxs("span", { role: "status", className: ccx("spinner", `spinner-${size}`, className), children: [_jsx(Asterisk, { size: iconSizes[size], strokeWidth: 1.6, "aria-hidden": "true" }), _jsx("span", { className: ccx("visually-hidden"), children: label })] }));
8
8
  }
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ccx } from "../../lib/classNames.js";
2
3
  export default function Stat({ value, label, suffix }) {
3
- return (_jsxs("div", { className: "stat", children: [_jsxs("strong", { children: [value, suffix && _jsx("span", { className: "stat-suffix", children: suffix })] }), _jsx("span", { className: "mono", children: label })] }));
4
+ return (_jsxs("div", { className: ccx("stat"), children: [_jsxs("strong", { children: [value, suffix && _jsx("span", { className: ccx("stat-suffix"), children: suffix })] }), _jsx("span", { className: ccx("mono"), children: label })] }));
4
5
  }
@@ -1,15 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** Numbered progress steps joined by a rule; completed steps show a check. */
3
3
  import { Check } from "lucide-react";
4
- import { cx } from "../../lib/classNames.js";
4
+ import { ccx } from "../../lib/classNames.js";
5
5
  import { formatIndex } from "../../lib/format.js";
6
6
  export default function Stepper({ steps, current, label = "Progress", }) {
7
- return (_jsx("ol", { className: "stepper", "aria-label": label, children: steps.map((step, index) => {
7
+ return (_jsx("ol", { className: ccx("stepper"), "aria-label": label, children: steps.map((step, index) => {
8
8
  const state = index < current
9
9
  ? "complete"
10
10
  : index === current
11
11
  ? "current"
12
12
  : "upcoming";
13
- return (_jsxs("li", { className: cx("stepper-step", `stepper-${state}`), "aria-current": state === "current" ? "step" : undefined, children: [_jsx("span", { className: "stepper-marker mono", "aria-hidden": "true", children: state === "complete" ? (_jsx(Check, { size: 14, strokeWidth: 2.5 })) : (formatIndex(index + 1)) }), _jsxs("span", { className: "stepper-text", children: [_jsx("span", { className: "stepper-label", children: step.label }), step.description && (_jsx("span", { className: "stepper-description mono", children: step.description })), _jsxs("span", { className: "visually-hidden", children: ["(", state, ")"] })] })] }, step.label));
13
+ return (_jsxs("li", { className: ccx("stepper-step", `stepper-${state}`), "aria-current": state === "current" ? "step" : undefined, children: [_jsx("span", { className: ccx("stepper-marker mono"), "aria-hidden": "true", children: state === "complete" ? (_jsx(Check, { size: 14, strokeWidth: 2.5 })) : (formatIndex(index + 1)) }), _jsxs("span", { className: ccx("stepper-text"), children: [_jsx("span", { className: ccx("stepper-label"), children: step.label }), step.description && (_jsx("span", { className: ccx("stepper-description mono"), children: step.description })), _jsxs("span", { className: ccx("visually-hidden"), children: ["(", state, ")"] })] })] }, step.label));
14
14
  }) }));
15
15
  }
@@ -8,6 +8,7 @@ export type SwitchProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onChang
8
8
  /** Submitted only when checked; defaults to the native checkbox value "on". */
9
9
  value?: string;
10
10
  required?: boolean;
11
+ requiredMessage?: string;
11
12
  description?: string;
12
13
  error?: string;
13
14
  /** Native invalid event from the backing checkbox; preventDefault owns feedback/focus. */
@@ -21,6 +22,7 @@ declare const Switch: import("react").ForwardRefExoticComponent<Omit<ButtonHTMLA
21
22
  /** Submitted only when checked; defaults to the native checkbox value "on". */
22
23
  value?: string;
23
24
  required?: boolean;
25
+ requiredMessage?: string;
24
26
  description?: string;
25
27
  error?: string;
26
28
  /** Native invalid event from the backing checkbox; preventDefault owns feedback/focus. */
@@ -2,10 +2,10 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /** Button switch with a native checkbox backing submission, validation and reset. */
4
4
  import { forwardRef, useId, useLayoutEffect, useRef, useState, } from "react";
5
- import { cx } from "../../lib/classNames.js";
5
+ import { cx, ccx } from "../../lib/classNames.js";
6
6
  import useControlRef from "../../hooks/useControlRef.js";
7
7
  import useFormReset from "../../hooks/useFormReset.js";
8
- const Switch = forwardRef(function Switch({ label, checked, defaultChecked, onChange, description, error, disabled, required, name, form, value = "on", id: providedId, className, onClick, onInvalid, "aria-describedby": describedBy, ...rest }, forwardedRef) {
8
+ const Switch = forwardRef(function Switch({ label, checked, defaultChecked, onChange, description, error, disabled, required, requiredMessage = "Turn on this option to continue.", name, form, value = "on", id: providedId, className, onClick, onInvalid, "aria-describedby": describedBy, ...rest }, forwardedRef) {
9
9
  const generatedId = useId();
10
10
  const id = providedId ?? generatedId;
11
11
  const [initial] = useState(defaultChecked ?? checked ?? false);
@@ -14,8 +14,7 @@ const Switch = forwardRef(function Switch({ label, checked, defaultChecked, onCh
14
14
  const current = checked ?? localChecked;
15
15
  const { ref: button, attach } = useControlRef(forwardedRef);
16
16
  const input = useRef(null);
17
- const message = error ??
18
- (invalid && !current ? "Turn on this option to continue." : undefined);
17
+ const message = error ?? (invalid && !current ? requiredMessage : undefined);
19
18
  useLayoutEffect(() => {
20
19
  if (input.current) {
21
20
  input.current.defaultChecked = initial;
@@ -29,7 +28,7 @@ const Switch = forwardRef(function Switch({ label, checked, defaultChecked, onCh
29
28
  input.current.checked = checked;
30
29
  setInvalid(false);
31
30
  });
32
- return (_jsxs("div", { className: "switch", children: [_jsx("button", { ...rest, ref: attach, id: id, name: undefined, form: form, type: "button", role: "switch", "aria-labelledby": cx(`${id}-label`, rest["aria-labelledby"]), "aria-checked": current, "aria-required": required || undefined, "aria-invalid": message ? true : rest["aria-invalid"], "aria-describedby": cx(describedBy, description && `${id}-description`, message && `${id}-error`) || undefined, disabled: disabled, className: cx("switch-track", className), onClick: (event) => {
31
+ return (_jsxs("div", { className: ccx("switch"), children: [_jsx("button", { ...rest, ref: attach, id: id, name: undefined, form: form, type: "button", role: "switch", "aria-labelledby": cx(`${id}-label`, rest["aria-labelledby"]), "aria-checked": current, "aria-required": required || undefined, "aria-invalid": message ? true : rest["aria-invalid"], "aria-describedby": cx(describedBy, description && `${id}-description`, message && `${id}-error`) || undefined, disabled: disabled, className: ccx("switch-track", className), onClick: (event) => {
33
32
  onClick?.(event);
34
33
  if (event.defaultPrevented)
35
34
  return;
@@ -42,7 +41,7 @@ const Switch = forwardRef(function Switch({ label, checked, defaultChecked, onCh
42
41
  }
43
42
  setInvalid(false);
44
43
  onChange?.(!current);
45
- }, children: _jsx("span", { className: "switch-thumb" }) }), _jsx("input", { ref: input, type: "checkbox", hidden: true, "aria-hidden": "true", tabIndex: -1, name: name, form: form, value: value, disabled: disabled, required: required, checked: checked, defaultChecked: checked === undefined ? initial : undefined, onChange: () => { }, onInvalid: (event) => {
44
+ }, children: _jsx("span", { className: ccx("switch-thumb") }) }), _jsx("input", { ref: input, type: "checkbox", hidden: true, "aria-hidden": "true", tabIndex: -1, name: name, form: form, value: value, disabled: disabled, required: required, checked: checked, defaultChecked: checked === undefined ? initial : undefined, onChange: () => { }, onInvalid: (event) => {
46
45
  onInvalid?.(event);
47
46
  const canceled = event.defaultPrevented;
48
47
  // Native validation must never try to focus the noninteractive backing control.
@@ -51,6 +50,6 @@ const Switch = forwardRef(function Switch({ label, checked, defaultChecked, onCh
51
50
  setInvalid(true);
52
51
  button.current?.focus();
53
52
  }
54
- } }), _jsxs("label", { htmlFor: id, className: "choice-text", children: [_jsx("span", { id: `${id}-label`, className: "choice-label", children: label }), description && (_jsx("span", { id: `${id}-description`, className: "choice-description", children: description })), message && (_jsx("span", { id: `${id}-error`, className: "choice-error mono", children: message }))] })] }));
53
+ } }), _jsxs("label", { htmlFor: id, className: ccx("choice-text"), children: [_jsx("span", { id: `${id}-label`, className: ccx("choice-label"), children: label }), description && (_jsx("span", { id: `${id}-description`, className: ccx("choice-description"), children: description })), message && (_jsx("span", { id: `${id}-error`, className: ccx("choice-error mono"), children: message }))] })] }));
55
54
  });
56
55
  export default Switch;
@@ -4,7 +4,7 @@ import { createElement as _createElement } from "react";
4
4
  /** Controlled/default tabs, with props and native refs for root/list/buttons/panels. */
5
5
  import { forwardRef, useId, useLayoutEffect, useRef, useState, } from "react";
6
6
  import { nextIndexForKey } from "../../lib/keyboard.js";
7
- import { cx } from "../../lib/classNames.js";
7
+ import { cx, ccx } from "../../lib/classNames.js";
8
8
  import useTabRefs from "../../hooks/useTabRefs.js";
9
9
  const Tabs = forwardRef(function Tabs({ label, tabs, defaultTabId, activeTabId, onTabChange, listProps, className, id, ...rest }, ref) {
10
10
  const generatedId = useId();
@@ -45,13 +45,13 @@ const Tabs = forwardRef(function Tabs({ label, tabs, defaultTabId, activeTabId,
45
45
  };
46
46
  const buttonId = (tab) => tab.buttonProps?.id ?? `${baseId}-tab-${tab.id}`;
47
47
  const panelId = (tab) => tab.panelProps?.id ?? `${baseId}-panel-${tab.id}`;
48
- return (_jsxs("div", { ...rest, ref: ref, id: id, className: cx("tabs", className), children: [_jsx("div", { ...listProps, className: cx("tabs-list", listProps?.className), role: "tablist", "aria-label": label, style: {
48
+ return (_jsxs("div", { ...rest, ref: ref, id: id, className: ccx("tabs", className), children: [_jsx("div", { ...listProps, className: ccx("tabs-list", listProps?.className), role: "tablist", "aria-label": label, style: {
49
49
  ...listProps?.style,
50
50
  "--indicator-left": `${indicator.left}px`,
51
51
  "--indicator-width": `${indicator.width}px`,
52
52
  }, children: tabs.map((tab, index) => {
53
53
  const selected = tab.id === selectedId;
54
- return (_createElement("button", { ...tab.buttonProps, key: tab.id, ref: buttonRefs[index], id: buttonId(tab), type: "button", role: "tab", "aria-selected": selected, "aria-controls": panelId(tab), tabIndex: selected ? 0 : -1, className: cx("tabs-tab", tab.buttonProps?.className), onClick: (event) => {
54
+ return (_createElement("button", { ...tab.buttonProps, key: tab.id, ref: buttonRefs[index], id: buttonId(tab), type: "button", role: "tab", "aria-selected": selected, "aria-controls": panelId(tab), tabIndex: selected ? 0 : -1, className: ccx("tabs-tab", tab.buttonProps?.className), onClick: (event) => {
55
55
  tab.buttonProps?.onClick?.(event);
56
56
  if (!event.defaultPrevented)
57
57
  select(tab);
@@ -59,12 +59,14 @@ const Tabs = forwardRef(function Tabs({ label, tabs, defaultTabId, activeTabId,
59
59
  tab.buttonProps?.onKeyDown?.(event);
60
60
  if (event.defaultPrevented)
61
61
  return;
62
- const next = nextIndexForKey(event.key, index, tabs.length);
62
+ const next = nextIndexForKey(event.key, index, tabs.length, "horizontal", event.currentTarget.ownerDocument.defaultView?.getComputedStyle(event.currentTarget).direction === "rtl"
63
+ ? "rtl"
64
+ : "ltr");
63
65
  if (next === null)
64
66
  return;
65
67
  event.preventDefault();
66
68
  select(tabs[next], true);
67
69
  } }, tab.label));
68
- }) }), tabs.map((tab) => (_createElement("div", { ...tab.panelProps, key: tab.id, id: panelId(tab), role: "tabpanel", "aria-labelledby": cx(buttonId(tab), tab.panelProps?.["aria-labelledby"]), hidden: tab.id !== selectedId, tabIndex: 0, className: cx("tabs-panel", tab.panelProps?.className) }, tab.content)))] }));
70
+ }) }), tabs.map((tab) => (_createElement("div", { ...tab.panelProps, key: tab.id, id: panelId(tab), role: "tabpanel", "aria-labelledby": cx(buttonId(tab), tab.panelProps?.["aria-labelledby"]), hidden: tab.id !== selectedId, tabIndex: 0, className: ccx("tabs-panel", tab.panelProps?.className) }, tab.content)))] }));
69
71
  });
70
72
  export default Tabs;
@@ -1,5 +1,6 @@
1
1
  export type TagProps = {
2
2
  children: string;
3
+ removeLabel?: string;
3
4
  onRemove?: () => void;
4
5
  };
5
- export default function Tag({ children, onRemove }: TagProps): import("react").JSX.Element;
6
+ export default function Tag({ children, onRemove, removeLabel }: TagProps): import("react").JSX.Element;
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ccx } from "../../lib/classNames.js";
3
4
  /** Mono "+ tag" label (as in project notes); becomes a removable chip when given onRemove. */
4
5
  import { X } from "lucide-react";
5
- export default function Tag({ children, onRemove }) {
6
+ export default function Tag({ children, onRemove, removeLabel }) {
6
7
  if (!onRemove)
7
- return _jsx("span", { className: "tag mono", children: children });
8
- return (_jsxs("span", { className: "tag tag-removable mono", children: [children, _jsx("button", { type: "button", className: "tag-remove", "aria-label": `Remove ${children}`, onClick: onRemove, children: _jsx(X, { size: 12 }) })] }));
8
+ return _jsx("span", { className: ccx("tag mono"), children: children });
9
+ return (_jsxs("span", { className: ccx("tag tag-removable mono"), children: [children, _jsx("button", { type: "button", className: ccx("tag-remove"), "aria-label": removeLabel ?? `Remove ${children}`, onClick: onRemove, children: _jsx(X, { size: 12 }) })] }));
9
10
  }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ccx } from "../../lib/classNames.js";
3
4
  /** Multi-line text input with optional auto-resize and character counter. */
4
5
  import { forwardRef, useId, useLayoutEffect, useState, } from "react";
5
6
  import { fieldDescribedBy } from "../../lib/a11y.js";
@@ -27,7 +28,7 @@ const TextArea = forwardRef(function TextArea({ id: providedId, "aria-describedb
27
28
  });
28
29
  return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, disabled: rest.disabled, counter: maxLength !== undefined
29
30
  ? `${rest.value !== undefined ? String(rest.value).length : length}/${maxLength}`
30
- : undefined, children: _jsx("div", { className: "field-control", children: _jsx("textarea", { ...rest, ref: attach, id: id, rows: rows, maxLength: maxLength, "data-auto-resize": autoResize || undefined, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
31
+ : undefined, children: _jsx("div", { className: ccx("field-control"), children: _jsx("textarea", { ...rest, ref: attach, id: id, rows: rows, maxLength: maxLength, "data-auto-resize": autoResize || undefined, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
31
32
  .filter(Boolean)
32
33
  .join(" ") || undefined,
33
34
  // Native input also runs when React deduplicates a repeated value after form.reset().
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ccx } from "../../lib/classNames.js";
3
4
  /** Single-line text input with underline styling, optional icon/adornment, and character counter. */
4
5
  import { forwardRef, useId, useState, } from "react";
5
6
  import { fieldDescribedBy } from "../../lib/a11y.js";
@@ -18,7 +19,7 @@ const TextField = forwardRef(function TextField({ id: providedId, "aria-describe
18
19
  });
19
20
  return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, hideLabel: hideLabel, disabled: rest.disabled, counter: maxLength !== undefined
20
21
  ? `${rest.value !== undefined ? String(rest.value).length : length}/${maxLength}`
21
- : undefined, children: _jsxs("div", { className: "field-control", children: [leadingIcon && _jsx("span", { className: "field-icon", children: leadingIcon }), _jsx("input", { ...rest, ref: attach, id: id, maxLength: maxLength, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
22
+ : undefined, children: _jsxs("div", { className: ccx("field-control"), children: [leadingIcon && (_jsx("span", { className: ccx("field-icon"), children: leadingIcon })), _jsx("input", { ...rest, ref: attach, id: id, maxLength: maxLength, "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
22
23
  .filter(Boolean)
23
24
  .join(" ") || undefined,
24
25
  // Native input also runs when React deduplicates a repeated value after form.reset().
@@ -28,6 +29,6 @@ const TextField = forwardRef(function TextField({ id: providedId, "aria-describe
28
29
  }, onChange: (event) => {
29
30
  setLength(event.target.value.length);
30
31
  onChange?.(event);
31
- } }), trailing && _jsx("span", { className: "field-trailing", children: trailing })] }) }));
32
+ } }), trailing && (_jsx("span", { className: ccx("field-trailing"), children: trailing }))] }) }));
32
33
  });
33
34
  export default TextField;
@@ -0,0 +1,13 @@
1
+ import type { HTMLAttributes } from "react";
2
+ import type { Theme } from "../../hooks/useTheme.js";
3
+ export type ThemeScopeProps = HTMLAttributes<HTMLDivElement> & {
4
+ theme: Theme;
5
+ /** Caller owns persistence; omitting this makes embedded theme controls read-only. */
6
+ onThemeChange?: (theme: Theme) => void;
7
+ };
8
+ declare const ThemeScope: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & {
9
+ theme: Theme;
10
+ /** Caller owns persistence; omitting this makes embedded theme controls read-only. */
11
+ onThemeChange?: (theme: Theme) => void;
12
+ } & import("react").RefAttributes<HTMLDivElement>>;
13
+ export default ThemeScope;
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { forwardRef, useMemo } from "react";
4
+ import { ThemeScopeContext } from "../../lib/themeScope.js";
5
+ import { cx } from "../../lib/classNames.js";
6
+ const ThemeScope = forwardRef(function ThemeScope({ theme, onThemeChange, className, children, ...rest }, ref) {
7
+ const context = useMemo(() => ({ theme, onThemeChange }), [theme, onThemeChange]);
8
+ return (_jsx(ThemeScopeContext.Provider, { value: context, children: _jsx("div", { ...rest, ref: ref, "data-contour-theme": theme, className: cx("contour-root", className), children: children }) }));
9
+ });
10
+ export default ThemeScope;
@@ -1,6 +1,9 @@
1
1
  export type ThemeToggleProps = {
2
2
  /** When false the theme switches instantly instead of animating. */
3
3
  animate: boolean;
4
+ darkLabel?: string;
5
+ lightTitle?: string;
6
+ darkTitle?: string;
4
7
  };
5
8
  declare const ThemeToggle: import("react").ForwardRefExoticComponent<ThemeToggleProps & import("react").RefAttributes<HTMLButtonElement>>;
6
9
  export default ThemeToggle;