@abuzareal/contour-ui 0.3.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.
- package/CHANGELOG.md +23 -0
- package/COMPOSITION.md +28 -28
- package/CONTRACTS.md +9 -3
- package/FORMS.md +94 -0
- package/NEXTJS.md +6 -2
- package/OVERLAYS.md +52 -0
- package/PUBLIC-API.md +181 -162
- package/README.md +44 -28
- package/RELEASING.md +1 -1
- package/SIZE.md +45 -0
- package/STYLING.md +68 -0
- package/dist/actions.css +7 -0
- package/dist/components/hero/Sculpture.js +2 -1
- package/dist/components/three/ObjectStudy.js +2 -1
- package/dist/components/ui/Alert.d.ts +2 -1
- package/dist/components/ui/Alert.js +3 -3
- package/dist/components/ui/Avatar.d.ts +3 -1
- package/dist/components/ui/Avatar.js +3 -3
- package/dist/components/ui/Badge.js +2 -2
- package/dist/components/ui/BrandMark.js +2 -1
- package/dist/components/ui/Breadcrumbs.d.ts +2 -1
- package/dist/components/ui/Breadcrumbs.js +3 -2
- package/dist/components/ui/Button.d.ts +2 -0
- package/dist/components/ui/Button.js +3 -2
- package/dist/components/ui/Card.js +2 -2
- package/dist/components/ui/Checkbox.d.ts +2 -2
- package/dist/components/ui/Checkbox.js +14 -5
- package/dist/components/ui/CountUp.d.ts +2 -1
- package/dist/components/ui/CountUp.js +6 -3
- package/dist/components/ui/DataTable.d.ts +1 -0
- package/dist/components/ui/DataTable.js +12 -8
- package/dist/components/ui/DepthButton.js +2 -2
- package/dist/components/ui/Dialog.d.ts +5 -1
- package/dist/components/ui/Dialog.js +49 -18
- package/dist/components/ui/Disclosure.js +2 -1
- package/dist/components/ui/Divider.js +3 -2
- package/dist/components/ui/DropdownMenu.d.ts +11 -4
- package/dist/components/ui/DropdownMenu.js +109 -66
- package/dist/components/ui/EmptyState.js +2 -1
- package/dist/components/ui/Field.js +2 -2
- package/dist/components/ui/FlipCard.js +2 -2
- package/dist/components/ui/FloatingPortal.d.ts +10 -0
- package/dist/components/ui/FloatingPortal.js +51 -0
- package/dist/components/ui/IconButton.js +2 -2
- package/dist/components/ui/Kbd.js +2 -1
- package/dist/components/ui/Magnetic.js +2 -1
- package/dist/components/ui/Marquee.js +3 -2
- package/dist/components/ui/OverlayTrigger.d.ts +11 -0
- package/dist/components/ui/OverlayTrigger.js +5 -0
- package/dist/components/ui/Pagination.d.ts +5 -1
- package/dist/components/ui/Pagination.js +3 -2
- package/dist/components/ui/Popover.d.ts +18 -8
- package/dist/components/ui/Popover.js +58 -17
- package/dist/components/ui/ProgressBar.js +2 -2
- package/dist/components/ui/RadioGroup.d.ts +13 -5
- package/dist/components/ui/RadioGroup.js +36 -9
- package/dist/components/ui/Reveal.d.ts +0 -1
- package/dist/components/ui/Reveal.js +4 -3
- package/dist/components/ui/RevealHeading.js +2 -1
- package/dist/components/ui/ScrollProgress.js +2 -1
- package/dist/components/ui/SectionLabel.js +2 -1
- package/dist/components/ui/SegmentedControl.js +6 -3
- package/dist/components/ui/SelectField.js +18 -3
- package/dist/components/ui/Skeleton.js +2 -2
- package/dist/components/ui/Slider.d.ts +23 -7
- package/dist/components/ui/Slider.js +43 -6
- package/dist/components/ui/Spinner.js +2 -2
- package/dist/components/ui/Stat.js +2 -1
- package/dist/components/ui/Stepper.js +3 -3
- package/dist/components/ui/Switch.d.ts +27 -5
- package/dist/components/ui/Switch.js +51 -5
- package/dist/components/ui/Tabs.js +7 -5
- package/dist/components/ui/Tag.d.ts +2 -1
- package/dist/components/ui/Tag.js +4 -3
- package/dist/components/ui/TextArea.js +4 -1
- package/dist/components/ui/TextField.js +8 -3
- package/dist/components/ui/ThemeScope.d.ts +13 -0
- package/dist/components/ui/ThemeScope.js +10 -0
- package/dist/components/ui/ThemeToggle.d.ts +3 -0
- package/dist/components/ui/ThemeToggle.js +4 -3
- package/dist/components/ui/TiltCard.js +3 -2
- package/dist/components/ui/Timeline.js +2 -2
- package/dist/components/ui/ToastProvider.d.ts +5 -2
- package/dist/components/ui/ToastProvider.js +5 -5
- package/dist/components/ui/Tooltip.d.ts +4 -4
- package/dist/components/ui/Tooltip.js +70 -10
- package/dist/data-display.css +5 -0
- package/dist/effects.css +6 -0
- package/dist/feedback.css +5 -0
- package/dist/fonts-latin.css +10 -4
- package/dist/fonts.css +8 -0
- package/dist/forms.css +5 -0
- package/dist/hooks/useDismiss.d.ts +12 -3
- package/dist/hooks/useDismiss.js +5 -21
- package/dist/hooks/useFloatingPanel.d.ts +14 -0
- package/dist/hooks/useFloatingPanel.js +157 -0
- package/dist/hooks/useFormReset.d.ts +1 -1
- package/dist/hooks/useLayerDismiss.d.ts +4 -0
- package/dist/hooks/useLayerDismiss.js +48 -0
- package/dist/hooks/useMobileMenu.d.ts +0 -1
- package/dist/hooks/useMobileMenu.js +11 -16
- package/dist/hooks/usePanelFocus.d.ts +10 -0
- package/dist/hooks/usePanelFocus.js +60 -0
- package/dist/hooks/useTheme.d.ts +1 -0
- package/dist/hooks/useTheme.js +18 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -0
- package/dist/kit/forms.css +11 -2
- package/dist/kit/navigation.css +4 -0
- package/dist/kit/overlays.css +8 -4
- package/dist/lib/classNames.d.ts +2 -0
- package/dist/lib/classNames.js +16 -1
- package/dist/lib/componentClasses.d.ts +1 -0
- package/dist/lib/componentClasses.js +2 -0
- package/dist/lib/keyboard.d.ts +1 -1
- package/dist/lib/keyboard.js +5 -2
- package/dist/lib/layers.d.ts +18 -0
- package/dist/lib/layers.js +128 -0
- package/dist/lib/themeScope.d.ts +5 -0
- package/dist/lib/themeScope.js +3 -0
- package/dist/minimal-reset.css +6 -0
- package/dist/navigation.css +5 -0
- package/dist/overlays.css +5 -0
- package/dist/scoped/base.css +183 -0
- package/dist/scoped/kit/actions.css +209 -0
- package/dist/scoped/kit/data-display.css +492 -0
- package/dist/scoped/kit/effects.css +140 -0
- package/dist/scoped/kit/feedback.css +309 -0
- package/dist/scoped/kit/forms.css +370 -0
- package/dist/scoped/kit/navigation.css +266 -0
- package/dist/scoped/kit/overlays.css +299 -0
- package/dist/scoped/marquee.css +58 -0
- package/dist/scoped/spinner.css +20 -0
- package/dist/scoped/theme-toggle.css +38 -0
- package/dist/scoped/tokens.css +119 -0
- package/dist/scoped/ui.css +127 -0
- package/dist/scoped/utilities.css +17 -0
- package/dist/scoped-tokens.css +1 -0
- package/dist/scoped.css +12 -0
- package/dist/theme-toggle.css +2 -2
- package/dist/tokens.json +710 -0
- package/examples/forms/HookFormSettingsForm.tsx +176 -0
- package/examples/forms/ManualSettingsForm-0.3.0.tsx +102 -0
- package/examples/forms/NativeSettingsForm.tsx +119 -0
- package/examples/forms/settings.ts +61 -0
- package/examples/forms/useSubmission.ts +77 -0
- package/package.json +30 -6
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useLayoutEffect } from "react";
|
|
3
|
+
import { autoUpdate, computePosition, flip, offset, shift, size, platform, } from "@floating-ui/dom";
|
|
4
|
+
export function floatingContainer(anchor, options) {
|
|
5
|
+
// Select the enclosing native dialog before showModal runs: default-open children
|
|
6
|
+
// render in the same commit, while its open attribute is still absent.
|
|
7
|
+
const modal = anchor.closest("dialog");
|
|
8
|
+
if (options.container &&
|
|
9
|
+
options.container.ownerDocument === anchor.ownerDocument &&
|
|
10
|
+
(!modal || modal.contains(options.container)))
|
|
11
|
+
return options.container;
|
|
12
|
+
return modal ?? anchor.ownerDocument.body;
|
|
13
|
+
}
|
|
14
|
+
/** DOM adapter: clean up observers and ignore obsolete asynchronous positioning results. */
|
|
15
|
+
export default function useFloatingPanel(anchor, panel, open, { side = "bottom", align = "start", sideOffset = 8, collisionPadding = 8, container, }) {
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
const reference = anchor.current, floating = panel.current;
|
|
18
|
+
const gap = Number.isFinite(sideOffset) ? sideOffset : 8;
|
|
19
|
+
const inset = Number.isFinite(collisionPadding)
|
|
20
|
+
? Math.max(0, collisionPadding)
|
|
21
|
+
: 8;
|
|
22
|
+
if (!open || !reference || !floating)
|
|
23
|
+
return;
|
|
24
|
+
let disposed = false, revision = 0;
|
|
25
|
+
const properties = [
|
|
26
|
+
"position",
|
|
27
|
+
"left",
|
|
28
|
+
"right",
|
|
29
|
+
"top",
|
|
30
|
+
"bottom",
|
|
31
|
+
"transform",
|
|
32
|
+
"max-width",
|
|
33
|
+
"max-height",
|
|
34
|
+
"min-width",
|
|
35
|
+
];
|
|
36
|
+
const previous = properties.map((name) => ({
|
|
37
|
+
name,
|
|
38
|
+
value: floating.style.getPropertyValue(name),
|
|
39
|
+
priority: floating.style.getPropertyPriority(name),
|
|
40
|
+
}));
|
|
41
|
+
const previousPlacement = floating.getAttribute("data-placement");
|
|
42
|
+
const view = reference.ownerDocument.defaultView;
|
|
43
|
+
const update = async () => {
|
|
44
|
+
const request = ++revision;
|
|
45
|
+
if (disposed || !floating.isConnected)
|
|
46
|
+
return;
|
|
47
|
+
floating.style.position = "fixed";
|
|
48
|
+
try {
|
|
49
|
+
const scale = await platform.getScale(floating);
|
|
50
|
+
const offsetParent = await platform.getOffsetParent(floating);
|
|
51
|
+
if (disposed || request !== revision)
|
|
52
|
+
return;
|
|
53
|
+
// A window offset parent does not normalize inherited CSS zoom. Measure its
|
|
54
|
+
// rendered dimensions and convert viewport coordinates back to local CSS pixels.
|
|
55
|
+
const windowScaled = offsetParent === view && (scale.x !== 1 || scale.y !== 1);
|
|
56
|
+
const factorX = windowScaled ? scale.x : 1;
|
|
57
|
+
const factorY = windowScaled ? scale.y : 1;
|
|
58
|
+
const padding = {
|
|
59
|
+
left: inset * factorX,
|
|
60
|
+
right: inset * factorX,
|
|
61
|
+
top: inset * factorY,
|
|
62
|
+
bottom: inset * factorY,
|
|
63
|
+
};
|
|
64
|
+
const result = await computePosition(reference, floating, {
|
|
65
|
+
strategy: "fixed",
|
|
66
|
+
placement: align === "center" ? side : `${side}-${align}`,
|
|
67
|
+
platform: windowScaled
|
|
68
|
+
? {
|
|
69
|
+
...platform,
|
|
70
|
+
getDimensions(element) {
|
|
71
|
+
if (element === floating) {
|
|
72
|
+
const rect = floating.getBoundingClientRect();
|
|
73
|
+
return { width: rect.width, height: rect.height };
|
|
74
|
+
}
|
|
75
|
+
return platform.getDimensions(element);
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
: platform,
|
|
79
|
+
middleware: [
|
|
80
|
+
offset(gap * (side === "left" || side === "right" ? factorX : factorY)),
|
|
81
|
+
flip({ padding }),
|
|
82
|
+
shift({ padding, crossAxis: true }),
|
|
83
|
+
size({
|
|
84
|
+
padding,
|
|
85
|
+
apply({ availableWidth, availableHeight }) {
|
|
86
|
+
if (disposed || request !== revision)
|
|
87
|
+
return;
|
|
88
|
+
const maximumWidth = Math.max(0, availableWidth / factorX);
|
|
89
|
+
floating.style.maxWidth = `${maximumWidth}px`;
|
|
90
|
+
floating.style.maxHeight = `${Math.max(0, availableHeight / factorY)}px`;
|
|
91
|
+
// A CSS minimum must not defeat the viewport constraint at high zoom.
|
|
92
|
+
const minimum = Number.parseFloat(view?.getComputedStyle(floating).minWidth ?? "0");
|
|
93
|
+
if (Number.isFinite(minimum) && minimum > maximumWidth)
|
|
94
|
+
floating.style.minWidth = `${maximumWidth}px`;
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
if (disposed || request !== revision)
|
|
100
|
+
return;
|
|
101
|
+
Object.assign(floating.style, {
|
|
102
|
+
position: "fixed",
|
|
103
|
+
left: `${result.x / factorX}px`,
|
|
104
|
+
top: `${result.y / factorY}px`,
|
|
105
|
+
right: "auto",
|
|
106
|
+
bottom: "auto",
|
|
107
|
+
transform: "none",
|
|
108
|
+
});
|
|
109
|
+
floating.dataset.placement = result.placement;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (disposed || request !== revision)
|
|
113
|
+
return;
|
|
114
|
+
// Preserve reachable content if the owner document cannot provide geometry.
|
|
115
|
+
Object.assign(floating.style, {
|
|
116
|
+
position: "fixed",
|
|
117
|
+
left: `${inset}px`,
|
|
118
|
+
top: `${inset}px`,
|
|
119
|
+
right: "auto",
|
|
120
|
+
bottom: "auto",
|
|
121
|
+
transform: "none",
|
|
122
|
+
});
|
|
123
|
+
floating.dataset.placement = "fallback";
|
|
124
|
+
console.warn("Contour floating positioning used its viewport fallback.", error);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const cleanup = autoUpdate(reference, floating, () => {
|
|
128
|
+
void update();
|
|
129
|
+
}, {
|
|
130
|
+
elementResize: typeof view?.ResizeObserver === "function",
|
|
131
|
+
layoutShift: typeof view?.IntersectionObserver === "function",
|
|
132
|
+
});
|
|
133
|
+
return () => {
|
|
134
|
+
disposed = true;
|
|
135
|
+
cleanup();
|
|
136
|
+
for (const { name, value, priority } of previous) {
|
|
137
|
+
if (value)
|
|
138
|
+
floating.style.setProperty(name, value, priority);
|
|
139
|
+
else
|
|
140
|
+
floating.style.removeProperty(name);
|
|
141
|
+
}
|
|
142
|
+
if (previousPlacement === null)
|
|
143
|
+
floating.removeAttribute("data-placement");
|
|
144
|
+
else
|
|
145
|
+
floating.setAttribute("data-placement", previousPlacement);
|
|
146
|
+
};
|
|
147
|
+
}, [
|
|
148
|
+
anchor,
|
|
149
|
+
panel,
|
|
150
|
+
open,
|
|
151
|
+
side,
|
|
152
|
+
align,
|
|
153
|
+
sideOffset,
|
|
154
|
+
collisionPadding,
|
|
155
|
+
container,
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type RefObject } from "react";
|
|
2
2
|
/** Native reset fires before the browser restores default values. */
|
|
3
|
-
export default function useFormReset<T extends HTMLInputElement | HTMLTextAreaElement>(ref: RefObject<T | null>, sync: () => void): void;
|
|
3
|
+
export default function useFormReset<T extends HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLFieldSetElement>(ref: RefObject<T | null>, sync: () => void): void;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
import { type DismissReason } from "../lib/layers.js";
|
|
3
|
+
import type { DismissOptions } from "./useDismiss.js";
|
|
4
|
+
export default function useLayerDismiss(container: RefObject<HTMLElement | null>, active: boolean, onDismiss: (reason: DismissReason, event: KeyboardEvent | PointerEvent) => void, options?: DismissOptions): symbol;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useContext, useState, useLayoutEffect, useRef, } from "react";
|
|
3
|
+
import { LayerParent, registerLayer, refreshLayerStack, } from "../lib/layers.js";
|
|
4
|
+
export default function useLayerDismiss(container, active, onDismiss, options = {}) {
|
|
5
|
+
const [id] = useState(() => Symbol("contour-layer"));
|
|
6
|
+
const parent = useContext(LayerParent);
|
|
7
|
+
const latest = useRef({ onDismiss, options });
|
|
8
|
+
const registration = useRef(undefined);
|
|
9
|
+
// Reconcile each commit so owner-document moves rebind listeners, without reordering on updates.
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
latest.current = { onDismiss, options };
|
|
12
|
+
const doc = active ? container.current?.ownerDocument : undefined;
|
|
13
|
+
if (registration.current &&
|
|
14
|
+
(registration.current.doc !== doc ||
|
|
15
|
+
registration.current.parent !== parent)) {
|
|
16
|
+
registration.current.dispose();
|
|
17
|
+
registration.current = undefined;
|
|
18
|
+
}
|
|
19
|
+
if (!doc)
|
|
20
|
+
return;
|
|
21
|
+
if (registration.current) {
|
|
22
|
+
refreshLayerStack(doc);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
registration.current = {
|
|
26
|
+
doc,
|
|
27
|
+
parent,
|
|
28
|
+
dispose: registerLayer(doc, {
|
|
29
|
+
id,
|
|
30
|
+
parent,
|
|
31
|
+
elements: () => [
|
|
32
|
+
container.current,
|
|
33
|
+
...(latest.current.options.branches ?? []).map((ref) => ref.current),
|
|
34
|
+
],
|
|
35
|
+
floating: () => latest.current.options.layerElement?.current ?? null,
|
|
36
|
+
dismiss: (reason, event) => latest.current.onDismiss(reason, event),
|
|
37
|
+
get outside() {
|
|
38
|
+
return latest.current.options.outside !== false;
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
useLayoutEffect(() => () => {
|
|
44
|
+
registration.current?.dispose();
|
|
45
|
+
registration.current = undefined;
|
|
46
|
+
}, []);
|
|
47
|
+
return id;
|
|
48
|
+
}
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
/** Open state for the mobile menu, closing on Escape, outside pointer, or desktop resize. */
|
|
2
1
|
import { type RefObject } from "react";
|
|
3
2
|
export default function useMobileMenu(container: RefObject<HTMLElement | null>, toggleButton: RefObject<HTMLButtonElement | null>): readonly [boolean, import("react").Dispatch<import("react").SetStateAction<boolean>>];
|
|
@@ -1,35 +1,30 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
/** Open state for the mobile menu, closing on Escape, outside pointer, or desktop resize. */
|
|
3
|
+
import useDismiss from "./useDismiss.js";
|
|
3
4
|
import { useEffect, useState } from "react";
|
|
4
5
|
const desktopQuery = "(min-width: 768px)";
|
|
5
6
|
export default function useMobileMenu(container, toggleButton) {
|
|
6
7
|
const [open, setOpen] = useState(false);
|
|
8
|
+
useDismiss(container, open, (reason) => {
|
|
9
|
+
setOpen(false);
|
|
10
|
+
if (reason === "escape" && toggleButton.current?.isConnected)
|
|
11
|
+
toggleButton.current.focus();
|
|
12
|
+
}, { branches: [toggleButton] });
|
|
7
13
|
useEffect(() => {
|
|
8
14
|
if (!open)
|
|
9
15
|
return;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
toggleButton.current?.focus();
|
|
15
|
-
};
|
|
16
|
-
const closeOnOutsidePointer = (event) => {
|
|
17
|
-
if (!container.current?.contains(event.target))
|
|
18
|
-
setOpen(false);
|
|
19
|
-
};
|
|
20
|
-
const desktop = window.matchMedia(desktopQuery);
|
|
16
|
+
const view = container.current?.ownerDocument.defaultView;
|
|
17
|
+
if (!view)
|
|
18
|
+
return;
|
|
19
|
+
const desktop = view.matchMedia(desktopQuery);
|
|
21
20
|
const closeOnDesktop = () => {
|
|
22
21
|
if (desktop.matches)
|
|
23
22
|
setOpen(false);
|
|
24
23
|
};
|
|
25
|
-
document.addEventListener("keydown", closeOnEscape);
|
|
26
|
-
document.addEventListener("pointerdown", closeOnOutsidePointer);
|
|
27
24
|
desktop.addEventListener("change", closeOnDesktop);
|
|
28
25
|
return () => {
|
|
29
|
-
document.removeEventListener("keydown", closeOnEscape);
|
|
30
|
-
document.removeEventListener("pointerdown", closeOnOutsidePointer);
|
|
31
26
|
desktop.removeEventListener("change", closeOnDesktop);
|
|
32
27
|
};
|
|
33
|
-
}
|
|
28
|
+
});
|
|
34
29
|
return [open, setOpen];
|
|
35
30
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
export type PanelFocusOptions = {
|
|
3
|
+
initialFocus?: RefObject<HTMLElement | null> | false;
|
|
4
|
+
onOpenAutoFocus?: (event: Event) => void;
|
|
5
|
+
onCloseAutoFocus?: (event: Event) => void;
|
|
6
|
+
};
|
|
7
|
+
/** Reconcile actual node/document moves without treating new callback props as a close. */
|
|
8
|
+
export default function usePanelFocus(open: boolean, trigger: RefObject<HTMLElement | null>, panel: RefObject<HTMLElement | null>, reason: RefObject<{
|
|
9
|
+
value: string | undefined;
|
|
10
|
+
}>, options: PanelFocusOptions, anchorElement?: HTMLElement | null, layer?: symbol): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { layerOwnsFocus, isTopLayer } from "../lib/layers.js";
|
|
3
|
+
import { useLayoutEffect, useRef } from "react";
|
|
4
|
+
/** Reconcile actual node/document moves without treating new callback props as a close. */
|
|
5
|
+
export default function usePanelFocus(open, trigger, panel, reason, options, anchorElement, layer) {
|
|
6
|
+
const policy = useRef(options);
|
|
7
|
+
const registration = useRef(undefined);
|
|
8
|
+
useLayoutEffect(() => {
|
|
9
|
+
policy.current = options;
|
|
10
|
+
const node = open && anchorElement ? panel.current : null;
|
|
11
|
+
const old = registration.current;
|
|
12
|
+
if (old && (!node || old.node !== node || old.doc !== node.ownerDocument)) {
|
|
13
|
+
old.dispose(!open);
|
|
14
|
+
registration.current = undefined;
|
|
15
|
+
}
|
|
16
|
+
if (!node || registration.current)
|
|
17
|
+
return;
|
|
18
|
+
const doc = node.ownerDocument, returnTarget = trigger.current, returnReason = reason.current;
|
|
19
|
+
const owns = () => node.contains(doc.activeElement) ||
|
|
20
|
+
(layer !== undefined && layerOwnsFocus(doc, layer));
|
|
21
|
+
let ownedFocus = owns();
|
|
22
|
+
const observeFocus = () => {
|
|
23
|
+
ownedFocus = owns();
|
|
24
|
+
};
|
|
25
|
+
doc.addEventListener("focusin", observeFocus);
|
|
26
|
+
const EventType = doc.defaultView?.Event ?? Event;
|
|
27
|
+
const event = new EventType("contour-open-auto-focus", {
|
|
28
|
+
cancelable: true,
|
|
29
|
+
});
|
|
30
|
+
policy.current.onOpenAutoFocus?.(event);
|
|
31
|
+
if (!event.defaultPrevented &&
|
|
32
|
+
(layer === undefined || isTopLayer(doc, layer)) &&
|
|
33
|
+
policy.current.initialFocus !== false)
|
|
34
|
+
(policy.current.initialFocus?.current ?? node).focus();
|
|
35
|
+
ownedFocus = owns();
|
|
36
|
+
const currentPolicy = policy;
|
|
37
|
+
registration.current = {
|
|
38
|
+
node,
|
|
39
|
+
doc,
|
|
40
|
+
dispose(returnFocus) {
|
|
41
|
+
doc.removeEventListener("focusin", observeFocus);
|
|
42
|
+
if (!returnFocus)
|
|
43
|
+
return;
|
|
44
|
+
const closeEvent = new EventType("contour-close-auto-focus", {
|
|
45
|
+
cancelable: true,
|
|
46
|
+
});
|
|
47
|
+
currentPolicy.current.onCloseAutoFocus?.(closeEvent);
|
|
48
|
+
if (returnReason?.value !== "outside" &&
|
|
49
|
+
!closeEvent.defaultPrevented &&
|
|
50
|
+
returnTarget?.isConnected &&
|
|
51
|
+
(returnReason?.value === "escape" || ownedFocus))
|
|
52
|
+
returnTarget.focus();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
useLayoutEffect(() => () => {
|
|
57
|
+
registration.current?.dispose(true);
|
|
58
|
+
registration.current = undefined;
|
|
59
|
+
}, []);
|
|
60
|
+
}
|
package/dist/hooks/useTheme.d.ts
CHANGED
package/dist/hooks/useTheme.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
/** Light/dark theme state: persisted choice, system fallback, and the animated switch. */
|
|
3
|
-
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
|
3
|
+
import { useCallback, useContext, useEffect, useSyncExternalStore, } from "react";
|
|
4
|
+
import { ThemeScopeContext } from "../lib/themeScope.js";
|
|
4
5
|
import { flushSync } from "react-dom";
|
|
5
6
|
import { readStorage, storageKeys, writeStorage } from "../lib/storage.js";
|
|
6
7
|
import { themeColors } from "../lib/themeBootstrap.js";
|
|
@@ -63,8 +64,12 @@ function subscribe(onChange) {
|
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
export default function useTheme(animate = true) {
|
|
66
|
-
const
|
|
67
|
+
const scope = useContext(ThemeScopeContext);
|
|
68
|
+
const scopedSubscribe = useCallback((listener) => (scope ? () => { } : subscribe(listener)), [scope]);
|
|
69
|
+
const theme = useSyncExternalStore(scopedSubscribe, readTheme, () => "light");
|
|
67
70
|
useEffect(() => {
|
|
71
|
+
if (scope)
|
|
72
|
+
return;
|
|
68
73
|
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
69
74
|
const followSystem = () => {
|
|
70
75
|
if (readStorage("local", storageKeys.theme) !== null)
|
|
@@ -74,8 +79,12 @@ export default function useTheme(animate = true) {
|
|
|
74
79
|
};
|
|
75
80
|
media.addEventListener("change", followSystem);
|
|
76
81
|
return () => media.removeEventListener("change", followSystem);
|
|
77
|
-
}, []);
|
|
82
|
+
}, [scope]);
|
|
78
83
|
const toggle = useCallback((origin) => {
|
|
84
|
+
if (scope) {
|
|
85
|
+
scope.onThemeChange?.(scope.theme === "dark" ? "light" : "dark");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
79
88
|
const next = readTheme() === "dark" ? "light" : "dark";
|
|
80
89
|
writeStorage("local", storageKeys.theme, next);
|
|
81
90
|
const commit = () => {
|
|
@@ -94,6 +103,10 @@ export default function useTheme(animate = true) {
|
|
|
94
103
|
return;
|
|
95
104
|
}
|
|
96
105
|
animateReveal(document.startViewTransition(commit), origin);
|
|
97
|
-
}, [animate]);
|
|
98
|
-
return {
|
|
106
|
+
}, [animate, scope]);
|
|
107
|
+
return {
|
|
108
|
+
theme: scope?.theme ?? theme,
|
|
109
|
+
toggle,
|
|
110
|
+
canToggle: scope ? !!scope.onThemeChange : true,
|
|
111
|
+
};
|
|
99
112
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -127,3 +127,8 @@ export * from "./lib/motion.js";
|
|
|
127
127
|
export type { Section } from "./types.js";
|
|
128
128
|
export { default as SceneBoundary } from "./components/hero/SceneBoundary.js";
|
|
129
129
|
export { safeHref } from "./lib/safeHref.js";
|
|
130
|
+
export { default as ThemeScope } from "./components/ui/ThemeScope.js";
|
|
131
|
+
export type * from "./components/ui/ThemeScope.js";
|
|
132
|
+
export type { FloatingOptions } from "./hooks/useFloatingPanel.js";
|
|
133
|
+
export type { PanelFocusOptions } from "./hooks/usePanelFocus.js";
|
|
134
|
+
export type { DismissReason } from "./lib/layers.js";
|
package/dist/index.js
CHANGED
|
@@ -66,3 +66,4 @@ export * from "./lib/a11y.js";
|
|
|
66
66
|
export * from "./lib/motion.js";
|
|
67
67
|
export { default as SceneBoundary } from "./components/hero/SceneBoundary.js";
|
|
68
68
|
export { safeHref } from "./lib/safeHref.js";
|
|
69
|
+
export { default as ThemeScope } from "./components/ui/ThemeScope.js";
|
package/dist/kit/forms.css
CHANGED
|
@@ -272,7 +272,7 @@ textarea {
|
|
|
272
272
|
.switch-thumb {
|
|
273
273
|
position: absolute;
|
|
274
274
|
top: 3px;
|
|
275
|
-
|
|
275
|
+
inset-inline-start: 3px;
|
|
276
276
|
width: 16px;
|
|
277
277
|
height: 16px;
|
|
278
278
|
border-radius: 50%;
|
|
@@ -287,7 +287,7 @@ textarea {
|
|
|
287
287
|
|
|
288
288
|
.switch-track[aria-checked="true"] .switch-thumb {
|
|
289
289
|
background: var(--ink);
|
|
290
|
-
|
|
290
|
+
inset-inline-start: 23px;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
.switch-track:disabled {
|
|
@@ -361,3 +361,12 @@ textarea {
|
|
|
361
361
|
outline: 2px solid var(--fg);
|
|
362
362
|
outline-offset: 6px;
|
|
363
363
|
}
|
|
364
|
+
|
|
365
|
+
:dir(rtl) .slider input[type="range"]::-webkit-slider-runnable-track,
|
|
366
|
+
:dir(rtl) .slider input[type="range"]::-moz-range-track {
|
|
367
|
+
background: linear-gradient(
|
|
368
|
+
to left,
|
|
369
|
+
var(--fg) var(--progress),
|
|
370
|
+
var(--line-strong) var(--progress)
|
|
371
|
+
);
|
|
372
|
+
}
|
package/dist/kit/navigation.css
CHANGED
package/dist/kit/overlays.css
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
z-index: 40;
|
|
11
11
|
width: max-content;
|
|
12
12
|
max-width: 220px;
|
|
13
|
+
overflow: auto;
|
|
14
|
+
overflow-wrap: anywhere;
|
|
13
15
|
padding: 6px 10px;
|
|
14
|
-
background: var(--
|
|
15
|
-
color: var(--
|
|
16
|
-
pointer-events:
|
|
16
|
+
background: var(--invert-bg);
|
|
17
|
+
color: var(--invert-fg);
|
|
18
|
+
pointer-events: auto;
|
|
17
19
|
opacity: 0;
|
|
18
20
|
visibility: hidden;
|
|
19
21
|
transition:
|
|
@@ -230,7 +232,9 @@
|
|
|
230
232
|
position: absolute;
|
|
231
233
|
top: calc(100% + 8px);
|
|
232
234
|
z-index: 30;
|
|
233
|
-
min-width: 220px;
|
|
235
|
+
min-width: min(220px, calc(100vw - 16px));
|
|
236
|
+
overflow: auto;
|
|
237
|
+
overflow-wrap: anywhere;
|
|
234
238
|
padding: 6px;
|
|
235
239
|
border: 1px solid var(--line);
|
|
236
240
|
background: var(--surface);
|
package/dist/lib/classNames.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Builds a className string from optional parts, skipping falsy values. */
|
|
2
2
|
export declare const cx: (...parts: (string | false | null | undefined)[]) => string;
|
|
3
|
+
/** Prefixed hooks alongside compatibility classes; never use this for IDs or ARIA references. */
|
|
4
|
+
export declare const ccx: (...parts: (string | false | null | undefined)[]) => string;
|
|
3
5
|
export type ButtonVariant = "primary" | "accent" | "secondary" | "ghost" | "danger";
|
|
4
6
|
export type ButtonSize = "sm" | "md" | "lg";
|
|
5
7
|
/** Button classes, shared so anchors can look like buttons without duplicating styles. */
|
package/dist/lib/classNames.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
+
import { componentClasses } from "./componentClasses.js";
|
|
1
2
|
/** Builds a className string from optional parts, skipping falsy values. */
|
|
2
3
|
export const cx = (...parts) => parts.filter(Boolean).join(" ");
|
|
4
|
+
/** Prefixed hooks alongside compatibility classes; never use this for IDs or ARIA references. */
|
|
5
|
+
export const ccx = (...parts) => {
|
|
6
|
+
const names = cx(...parts)
|
|
7
|
+
.split(/\s+/)
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
return [
|
|
10
|
+
...new Set([
|
|
11
|
+
...names,
|
|
12
|
+
...names
|
|
13
|
+
.filter((name) => componentClasses.has(name))
|
|
14
|
+
.map((name) => `contour-${name}`),
|
|
15
|
+
]),
|
|
16
|
+
].join(" ");
|
|
17
|
+
};
|
|
3
18
|
/** Button classes, shared so anchors can look like buttons without duplicating styles. */
|
|
4
|
-
export const buttonClassName = (variant = "primary", size = "md", className) =>
|
|
19
|
+
export const buttonClassName = (variant = "primary", size = "md", className) => ccx("button", `button-${variant}`, `button-${size}`, className);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const componentClasses: Set<string>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Generated compatibility aliases; unknown consumer classes pass through unchanged.
|
|
2
|
+
export const componentClasses = new Set(["alert", "alert-accent", "alert-action", "alert-banner", "alert-body", "alert-content", "alert-danger", "alert-dismiss", "alert-icon", "alert-info", "alert-success", "alert-title", "alert-warning", "avatar", "avatar-group", "avatar-initials", "avatar-lg", "avatar-sm", "avatar-status", "avatar-status-away", "avatar-status-online", "avatar-xl", "badge", "badge-danger", "badge-dot", "badge-info", "badge-invert", "badge-neutral", "badge-success", "badge-warning", "brand", "breadcrumbs", "button", "button-accent", "button-danger", "button-ghost", "button-icon", "button-lg", "button-secondary", "button-sm", "card", "card-acid", "card-body", "card-footer", "card-icon", "card-ink", "card-interactive", "card-title", "card-top", "choice", "choice-box", "choice-control", "choice-description", "choice-error", "choice-input", "choice-invalid", "choice-label", "choice-radio", "choice-text", "count-up", "css", "data-table", "data-table-idle", "depth-button", "depth-button-face", "dialog", "dialog-body", "dialog-close", "dialog-description", "dialog-footer", "dialog-header", "dialog-left", "dialog-panel", "dialog-right", "disclosure", "disclosure-icon", "divider", "divider-labelled", "empty-state", "empty-state-icon", "field", "field-chevron", "field-control", "field-counter", "field-disabled", "field-error", "field-footer", "field-icon", "field-invalid", "field-label", "field-required", "field-select", "field-trailing", "flip-back", "flip-card", "flip-face", "flip-front", "flip-stage", "flip-toggle", "hero-letter", "icon-button", "icon-button-accent", "icon-button-lg", "icon-button-sm", "icon-button-solid", "icon-button-spin", "inline-link", "is-flipped", "is-open", "is-visible", "kbd", "magnetic", "marquee", "marquee-inner", "marquee-track", "menu", "menu-chevron", "menu-end", "menu-item", "menu-item-danger", "menu-list", "menu-start", "menu-trigger", "mono", "opening-sequence", "page-width", "pagination", "pagination-gap", "pagination-page", "pagination-step", "popover", "popover-end", "popover-panel", "popover-start", "popover-title", "progress", "progress-accent", "progress-danger", "progress-fill", "progress-header", "progress-indeterminate", "progress-success", "progress-track", "radio-group", "radio-group-inline", "reveal", "scroll-progress", "sculpture-canvas", "section-label", "segmented", "segmented-option", "site-shell", "skeleton", "skeleton-block", "skeleton-circle", "skeleton-line", "skip-link", "slider", "slider-header", "spinner", "stage-canvas", "stat", "stat-suffix", "stepper", "stepper-complete", "stepper-current", "stepper-description", "stepper-label", "stepper-marker", "stepper-step", "stepper-text", "stepper-upcoming", "switch", "switch-thumb", "switch-track", "table-scroll", "tabs", "tabs-list", "tabs-panel", "tabs-tab", "tag", "tag-removable", "tag-remove", "text-link", "theme-fading", "theme-icon-moon", "theme-icon-sun", "theme-toggle", "tilt-card", "tilt-glare", "tilt-stage", "timeline", "timeline-body", "timeline-current", "timeline-date", "timeline-entry", "timeline-title", "toast", "toast-bar", "toast-close", "toast-content", "toast-danger", "toast-info", "toast-success", "toast-viewport", "toast-warning", "tooltip", "tooltip-anchor", "tooltip-bottom", "tooltip-left", "tooltip-right", "tooltip-top", "ts", "type-line", "type-run", "visually-hidden"]);
|
package/dist/lib/keyboard.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ type Orientation = "horizontal" | "vertical";
|
|
|
4
4
|
* Returns the index to move to for an arrow, Home, or End key press,
|
|
5
5
|
* wrapping at both ends, or null when the key is not a navigation key.
|
|
6
6
|
*/
|
|
7
|
-
export declare function nextIndexForKey(key: string, current: number, count: number, orientation?: Orientation): number | null;
|
|
7
|
+
export declare function nextIndexForKey(key: string, current: number, count: number, orientation?: Orientation, direction?: "ltr" | "rtl"): number | null;
|
|
8
8
|
export {};
|
package/dist/lib/keyboard.js
CHANGED
|
@@ -6,10 +6,13 @@ const keysFor = (orientation) => orientation === "horizontal"
|
|
|
6
6
|
* Returns the index to move to for an arrow, Home, or End key press,
|
|
7
7
|
* wrapping at both ends, or null when the key is not a navigation key.
|
|
8
8
|
*/
|
|
9
|
-
export function nextIndexForKey(key, current, count, orientation = "horizontal") {
|
|
9
|
+
export function nextIndexForKey(key, current, count, orientation = "horizontal", direction = "ltr") {
|
|
10
10
|
if (count <= 0)
|
|
11
11
|
return null;
|
|
12
|
-
const
|
|
12
|
+
const keys = keysFor(orientation);
|
|
13
|
+
const { previous, next } = orientation === "horizontal" && direction === "rtl"
|
|
14
|
+
? { previous: keys.next, next: keys.previous }
|
|
15
|
+
: keys;
|
|
13
16
|
if (key === next)
|
|
14
17
|
return (current + 1) % count;
|
|
15
18
|
if (key === previous)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type DismissReason = "escape" | "outside";
|
|
2
|
+
export declare const LayerVisibility: import("react").Context<boolean>;
|
|
3
|
+
export declare const LayerParent: import("react").Context<symbol | undefined>;
|
|
4
|
+
type Layer = {
|
|
5
|
+
id: symbol;
|
|
6
|
+
parent?: symbol;
|
|
7
|
+
elements: () => (HTMLElement | null)[];
|
|
8
|
+
dismiss: (reason: DismissReason, event: KeyboardEvent | PointerEvent) => void;
|
|
9
|
+
outside: boolean;
|
|
10
|
+
floating?: () => HTMLElement | null;
|
|
11
|
+
};
|
|
12
|
+
export declare function layerOwnsFocus(doc: Document, id: symbol): boolean;
|
|
13
|
+
/** Paint order follows dismissal order, including later-opened siblings and portaled children. */
|
|
14
|
+
export declare function refreshLayerStack(doc: Document): void;
|
|
15
|
+
export declare function isTopLayer(doc: Document, id: symbol): boolean;
|
|
16
|
+
/** One bubble listener per owner document. Never stop propagation or cascade a dismissal. */
|
|
17
|
+
export declare function registerLayer(doc: Document, layer: Layer): () => void;
|
|
18
|
+
export {};
|