@abuzareal/contour-ui 0.4.0 → 0.6.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 +24 -0
- package/COMBOBOX.md +87 -0
- package/CONTRACTS.md +23 -17
- package/DOCS.md +80 -0
- package/FOUNDATION.md +35 -0
- package/GETTING-STARTED.md +63 -0
- package/NEXTJS.md +5 -1
- package/OVERLAYS.md +52 -0
- package/PUBLIC-API.md +189 -162
- package/README.md +44 -28
- package/RECIPES.md +43 -0
- package/RELEASING.md +2 -2
- package/SIZE.md +51 -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.js +3 -3
- package/dist/components/ui/Combobox.d.ts +325 -0
- package/dist/components/ui/Combobox.js +120 -0
- package/dist/components/ui/Combobox.types.d.ts +46 -0
- package/dist/components/ui/Combobox.types.js +1 -0
- 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 +15 -10
- 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 +13 -0
- package/dist/components/ui/FloatingScope.d.ts +9 -0
- package/dist/components/ui/FloatingScope.js +43 -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.js +5 -5
- 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 +3 -2
- package/dist/components/ui/Skeleton.js +2 -2
- package/dist/components/ui/Slider.js +3 -3
- 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 +2 -0
- package/dist/components/ui/Switch.js +6 -7
- package/dist/components/ui/Table.d.ts +20 -0
- package/dist/components/ui/Table.js +9 -0
- 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 +2 -1
- package/dist/components/ui/TextField.js +3 -2
- 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/useComboboxAnchor.d.ts +17 -0
- package/dist/hooks/useComboboxAnchor.js +61 -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/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 +7 -0
- package/dist/index.js +3 -0
- package/dist/kit/data-display.css +6 -0
- package/dist/kit/forms.css +45 -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 +498 -0
- package/dist/scoped/kit/effects.css +140 -0
- package/dist/scoped/kit/feedback.css +309 -0
- package/dist/scoped/kit/forms.css +404 -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/optional/LazyScene.tsx +78 -0
- package/examples/optional/OptionalSmoothScroll.tsx +25 -0
- package/examples/optional/SmoothScrollOwner.tsx +7 -0
- package/examples/optional/optional.css +25 -0
- package/examples/recipes/AsyncProjectPicker.tsx +82 -0
- package/examples/recipes/DeleteProjectConfirmation.tsx +97 -0
- package/examples/recipes/InputCompositions.tsx +49 -0
- package/examples/recipes/RemoteRecordsTable.tsx +256 -0
- package/examples/recipes/useRemoteResource.ts +81 -0
- package/examples/reproduction/README.md +9 -0
- package/examples/reproduction/index.html +14 -0
- package/examples/reproduction/package.json +27 -0
- package/examples/reproduction/src/main.tsx +33 -0
- package/examples/reproduction/src/style.css +19 -0
- package/examples/reproduction/tsconfig.json +18 -0
- package/examples/reproduction/vite.config.ts +19 -0
- package/examples/starters/next/README.md +18 -0
- package/examples/starters/next/app/Demo.tsx +65 -0
- package/examples/starters/next/app/layout.tsx +25 -0
- package/examples/starters/next/app/next-page/page.tsx +9 -0
- package/examples/starters/next/app/page.tsx +11 -0
- package/examples/starters/next/app/style.css +19 -0
- package/examples/starters/next/next-env.d.ts +2 -0
- package/examples/starters/next/next.config.mjs +1 -0
- package/examples/starters/next/package.json +27 -0
- package/examples/starters/next/proxy.ts +16 -0
- package/examples/starters/next/tsconfig.json +30 -0
- package/examples/starters/vite/README.md +16 -0
- package/examples/starters/vite/index.html +14 -0
- package/examples/starters/vite/package.json +27 -0
- package/examples/starters/vite/src/main.tsx +81 -0
- package/examples/starters/vite/src/style.css +19 -0
- package/examples/starters/vite/tsconfig.json +18 -0
- package/examples/starters/vite/vite.config.ts +19 -0
- package/package.json +41 -5
|
@@ -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;
|
|
@@ -1,12 +1,13 @@
|
|
|
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
|
/** Header button that switches between the light and dark themes. */
|
|
4
5
|
import { forwardRef } from "react";
|
|
5
6
|
import { Moon, Sun } from "lucide-react";
|
|
6
7
|
import useTheme from "../../hooks/useTheme.js";
|
|
7
|
-
const ThemeToggle = forwardRef(function ThemeToggle({ animate }, ref) {
|
|
8
|
-
const { theme, toggle } = useTheme(animate);
|
|
8
|
+
const ThemeToggle = forwardRef(function ThemeToggle({ animate, darkLabel = "Dark theme", lightTitle = "Switch to light theme", darkTitle = "Switch to dark theme", }, ref) {
|
|
9
|
+
const { theme, toggle, canToggle } = useTheme(animate);
|
|
9
10
|
const dark = theme === "dark";
|
|
10
|
-
return (_jsxs("button", { ref: ref, type: "button", className: "theme-toggle", "aria-label":
|
|
11
|
+
return (_jsxs("button", { ref: ref, type: "button", className: ccx("theme-toggle"), "aria-label": darkLabel, "aria-pressed": dark, disabled: !canToggle, title: dark ? lightTitle : darkTitle, onClick: (event) => toggle(event.currentTarget), children: [_jsx(Moon, { className: ccx("theme-icon-moon"), size: 16, "aria-hidden": "true" }), _jsx(Sun, { className: ccx("theme-icon-sun"), size: 17, "aria-hidden": "true" })] }));
|
|
11
12
|
});
|
|
12
13
|
export default ThemeToggle;
|
|
@@ -1,11 +1,12 @@
|
|
|
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
|
/** Card that tilts in 3D toward a fine pointer with a moving glare highlight. */
|
|
4
5
|
import { useRef } from "react";
|
|
5
6
|
import { motionDisabledFor } from "../../lib/motion.js";
|
|
6
7
|
export default function TiltCard({ children, maxTilt = 12 }) {
|
|
7
8
|
const ref = useRef(null);
|
|
8
|
-
return (_jsx("div", { className: "tilt-stage", children: _jsxs("div", { ref: ref, className: "tilt-card", onPointerMove: (event) => {
|
|
9
|
+
return (_jsx("div", { className: ccx("tilt-stage"), children: _jsxs("div", { ref: ref, className: ccx("tilt-card"), onPointerMove: (event) => {
|
|
9
10
|
const element = ref.current;
|
|
10
11
|
if (!element ||
|
|
11
12
|
event.pointerType !== "mouse" ||
|
|
@@ -20,5 +21,5 @@ export default function TiltCard({ children, maxTilt = 12 }) {
|
|
|
20
21
|
element.style.setProperty("--glare-y", `${y * 100}%`);
|
|
21
22
|
}, onPointerLeave: () => {
|
|
22
23
|
["--tilt-x", "--tilt-y", "--glare-x", "--glare-y"].forEach((name) => ref.current?.style.removeProperty(name));
|
|
23
|
-
}, children: [children, _jsx("span", { className: "tilt-glare", "aria-hidden": "true" })] }) }));
|
|
24
|
+
}, children: [children, _jsx("span", { className: ccx("tilt-glare"), "aria-hidden": "true" })] }) }));
|
|
24
25
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/** Vertical timeline of dated entries joined by a rule, with an acid marker for the current one. */
|
|
3
|
-
import {
|
|
3
|
+
import { ccx } from "../../lib/classNames.js";
|
|
4
4
|
export default function Timeline({ entries }) {
|
|
5
|
-
return (_jsx("ol", { className: "timeline", children: entries.map((entry) => (_jsxs("li", { className:
|
|
5
|
+
return (_jsx("ol", { className: ccx("timeline"), children: entries.map((entry) => (_jsxs("li", { className: ccx("timeline-entry", entry.current && "timeline-current"), children: [_jsx("span", { className: ccx("timeline-date mono"), children: entry.date }), _jsx("span", { className: ccx("timeline-title"), children: entry.title }), entry.body && (_jsx("span", { className: ccx("timeline-body"), children: entry.body }))] }, `${entry.date}-${entry.title}`))) }));
|
|
6
6
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** Renders queued toasts in a polite live region; each auto-dismisses unless hovered or focused. */
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
|
-
export
|
|
3
|
+
export type ToastProviderProps = {
|
|
4
4
|
children: ReactNode;
|
|
5
|
-
|
|
5
|
+
label?: string;
|
|
6
|
+
dismissLabel?: string;
|
|
7
|
+
};
|
|
8
|
+
export default function ToastProvider({ children, label, dismissLabel, }: ToastProviderProps): import("react").JSX.Element;
|
|
@@ -4,9 +4,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
5
5
|
import { X } from "lucide-react";
|
|
6
6
|
import { ToastContext, } from "../../hooks/useToast.js";
|
|
7
|
-
import {
|
|
7
|
+
import { ccx } from "../../lib/classNames.js";
|
|
8
8
|
const autoDismissAfter = 5000;
|
|
9
|
-
function Toast({ toast, onDismiss, }) {
|
|
9
|
+
function Toast({ toast, onDismiss, dismissLabel, }) {
|
|
10
10
|
const [paused, setPaused] = useState(false);
|
|
11
11
|
useEffect(() => {
|
|
12
12
|
if (paused)
|
|
@@ -14,9 +14,9 @@ function Toast({ toast, onDismiss, }) {
|
|
|
14
14
|
const timer = window.setTimeout(() => onDismiss(toast.id), autoDismissAfter);
|
|
15
15
|
return () => window.clearTimeout(timer);
|
|
16
16
|
}, [paused, toast.id, onDismiss]);
|
|
17
|
-
return (_jsxs("li", { className:
|
|
17
|
+
return (_jsxs("li", { className: ccx("toast", `toast-${toast.tone ?? "neutral"}`), onPointerEnter: () => setPaused(true), onPointerLeave: () => setPaused(false), onFocus: () => setPaused(true), onBlur: () => setPaused(false), children: [_jsx("span", { className: ccx("toast-bar"), "aria-hidden": "true" }), _jsxs("div", { className: ccx("toast-content"), children: [_jsx("strong", { children: toast.title }), toast.message && _jsx("p", { children: toast.message })] }), _jsx("button", { type: "button", className: ccx("toast-close"), "aria-label": dismissLabel, onClick: () => onDismiss(toast.id), children: _jsx(X, { size: 15 }) })] }));
|
|
18
18
|
}
|
|
19
|
-
export default function ToastProvider({ children }) {
|
|
19
|
+
export default function ToastProvider({ children, label = "Notifications", dismissLabel = "Dismiss notification", }) {
|
|
20
20
|
const [toasts, setToasts] = useState([]);
|
|
21
21
|
const nextId = useRef(0);
|
|
22
22
|
const dismiss = useCallback((id) => setToasts((current) => current.filter((toast) => toast.id !== id)), []);
|
|
@@ -26,5 +26,5 @@ export default function ToastProvider({ children }) {
|
|
|
26
26
|
{ ...toast, id: (nextId.current += 1) },
|
|
27
27
|
]),
|
|
28
28
|
}), []);
|
|
29
|
-
return (_jsxs(ToastContext.Provider, { value: value, children: [children, _jsx("ol", { className: "toast-viewport", "aria-live": "polite", "aria-label":
|
|
29
|
+
return (_jsxs(ToastContext.Provider, { value: value, children: [children, _jsx("ol", { className: ccx("toast-viewport"), "aria-live": "polite", "aria-label": label, children: toasts.map((toast) => (_jsx(Toast, { toast: toast, onDismiss: dismiss, dismissLabel: dismissLabel }, toast.id))) })] }));
|
|
30
30
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/** Ink tooltip shown on hover or focus, dismissible with Escape, described via aria-describedby. */
|
|
2
1
|
import { type ReactElement } from "react";
|
|
3
|
-
|
|
2
|
+
import type { FloatingOptions } from "../../hooks/useFloatingPanel.js";
|
|
3
|
+
export type TooltipProps = Omit<FloatingOptions, "side"> & {
|
|
4
4
|
content: string;
|
|
5
|
-
/**
|
|
5
|
+
/** One focusable element. Supplemental noninteractive text only. */
|
|
6
6
|
children: ReactElement<{
|
|
7
7
|
"aria-describedby"?: string;
|
|
8
8
|
}>;
|
|
9
9
|
placement?: "top" | "bottom" | "left" | "right";
|
|
10
10
|
};
|
|
11
|
-
export default function Tooltip({ content, children, placement, }: TooltipProps): import("react").JSX.Element;
|
|
11
|
+
export default function Tooltip({ content, children, placement, align, sideOffset, collisionPadding, portal, container, }: TooltipProps): import("react").JSX.Element;
|
|
@@ -1,17 +1,77 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
3
|
+
import { useContext, cloneElement, useMemo, useEffect, useId, useRef, useState, } from "react";
|
|
4
|
+
import { composeRefs } from "../../lib/composeRefs.js";
|
|
5
|
+
import { ccx } from "../../lib/classNames.js";
|
|
6
|
+
import { LayerVisibility } from "../../lib/layers.js";
|
|
7
|
+
import useDismiss from "../../hooks/useLayerDismiss.js";
|
|
8
|
+
import FloatingPortal from "./FloatingPortal.js";
|
|
9
|
+
export default function Tooltip({ content, children, placement = "top", align = "center", sideOffset = 10, collisionPadding = 8, portal = true, container, }) {
|
|
7
10
|
const id = useId();
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const anchor = useRef(null), panel = useRef(null);
|
|
12
|
+
const [anchorElement, setAnchorElement] = useState(null);
|
|
13
|
+
const attachAnchor = useMemo(() => composeRefs(anchor, setAnchorElement), []);
|
|
14
|
+
const [hovered, setHovered] = useState(false), [focused, setFocused] = useState(false);
|
|
15
|
+
const [dismissed, setDismissed] = useState(false);
|
|
16
|
+
const parentVisible = useContext(LayerVisibility);
|
|
17
|
+
const open = (hovered || focused) && !dismissed && parentVisible;
|
|
18
|
+
const timer = useRef(undefined);
|
|
19
|
+
const clear = () => {
|
|
20
|
+
clearTimeout(timer.current);
|
|
21
|
+
timer.current = undefined;
|
|
22
|
+
};
|
|
23
|
+
const enter = () => {
|
|
24
|
+
clear();
|
|
25
|
+
setHovered(true);
|
|
26
|
+
if (!focused && !hovered)
|
|
27
|
+
setDismissed(false);
|
|
28
|
+
};
|
|
29
|
+
const leave = () => {
|
|
30
|
+
clear();
|
|
31
|
+
timer.current = setTimeout(() => {
|
|
32
|
+
setHovered(false);
|
|
33
|
+
}, 160);
|
|
34
|
+
};
|
|
35
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!hovered)
|
|
38
|
+
return;
|
|
39
|
+
const doc = anchor.current?.ownerDocument;
|
|
40
|
+
const move = (event) => {
|
|
41
|
+
const a = anchor.current?.getBoundingClientRect(), p = panel.current?.getBoundingClientRect();
|
|
42
|
+
if (!a || !p)
|
|
43
|
+
return;
|
|
44
|
+
// Keep the pointer corridor between trigger and hint available, including slow travel.
|
|
45
|
+
const inside = event.clientX >= Math.min(a.left, p.left) - 4 &&
|
|
46
|
+
event.clientX <= Math.max(a.right, p.right) + 4 &&
|
|
47
|
+
event.clientY >= Math.min(a.top, p.top) - 4 &&
|
|
48
|
+
event.clientY <= Math.max(a.bottom, p.bottom) + 4;
|
|
49
|
+
if (inside)
|
|
50
|
+
clear();
|
|
51
|
+
else
|
|
52
|
+
leave();
|
|
53
|
+
};
|
|
54
|
+
doc?.addEventListener("pointermove", move);
|
|
55
|
+
return () => doc?.removeEventListener("pointermove", move);
|
|
56
|
+
}, [hovered]);
|
|
57
|
+
useDismiss(anchor, open, () => setDismissed(true), {
|
|
58
|
+
branches: [panel],
|
|
59
|
+
layerElement: panel,
|
|
60
|
+
});
|
|
61
|
+
return (_jsxs("span", { ref: attachAnchor, className: ccx("tooltip-anchor"), onPointerEnter: enter, onPointerLeave: leave, onFocus: () => {
|
|
62
|
+
setFocused(true);
|
|
63
|
+
if (!hovered)
|
|
64
|
+
setDismissed(false);
|
|
65
|
+
}, onBlur: () => setFocused(false), children: [cloneElement(children, {
|
|
13
66
|
"aria-describedby": [children.props["aria-describedby"], id]
|
|
14
67
|
.filter(Boolean)
|
|
15
68
|
.join(" "),
|
|
16
|
-
}), _jsx(
|
|
69
|
+
}), _jsx(FloatingPortal, { anchorElement: anchorElement, anchor: anchor, panel: panel, open: open, options: {
|
|
70
|
+
side: placement,
|
|
71
|
+
align,
|
|
72
|
+
sideOffset,
|
|
73
|
+
collisionPadding,
|
|
74
|
+
portal,
|
|
75
|
+
container,
|
|
76
|
+
}, children: _jsx("span", { ref: panel, id: id, role: "tooltip", className: ccx("tooltip mono", `tooltip-${placement}`, open && "is-open"), onPointerEnter: enter, onPointerLeave: leave, children: content }) })] }));
|
|
17
77
|
}
|
package/dist/effects.css
ADDED
package/dist/fonts-latin.css
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
font-style: normal;
|
|
5
5
|
font-display: swap;
|
|
6
6
|
font-weight: 100 1000;
|
|
7
|
-
src: url("./fonts/dm-sans-latin-wght-normal.woff2")
|
|
8
|
-
format("woff2-variations");
|
|
7
|
+
src: url("./fonts/dm-sans-latin-wght-normal.woff2") format("woff2-variations");
|
|
9
8
|
unicode-range:
|
|
10
9
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
|
11
10
|
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
|
|
@@ -30,6 +29,13 @@
|
|
|
30
29
|
font-style: normal;
|
|
31
30
|
font-display: swap;
|
|
32
31
|
font-weight: 400;
|
|
33
|
-
src: url("./fonts/dm-mono-latin-400-normal.woff2")
|
|
34
|
-
|
|
32
|
+
src: url("./fonts/dm-mono-latin-400-normal.woff2") format("woff2");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Explicit branded typography for isolated components; import after scoped styles. */
|
|
36
|
+
:where(:root) {
|
|
37
|
+
--contour-font-sans: "DM Sans Variable", "DM Sans", system-ui, sans-serif;
|
|
38
|
+
--contour-font-display:
|
|
39
|
+
"Space Grotesk Variable", "Space Grotesk", system-ui, sans-serif;
|
|
40
|
+
--contour-font-mono: "DM Mono", ui-monospace, monospace;
|
|
35
41
|
}
|
package/dist/fonts.css
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
@import "@fontsource-variable/dm-sans/wght.css";
|
|
2
2
|
@import "@fontsource-variable/space-grotesk/wght.css";
|
|
3
3
|
@import "@fontsource/dm-mono/400.css";
|
|
4
|
+
|
|
5
|
+
/* Explicit branded typography for isolated components; import after scoped styles. */
|
|
6
|
+
:where(:root) {
|
|
7
|
+
--contour-font-sans: "DM Sans Variable", "DM Sans", system-ui, sans-serif;
|
|
8
|
+
--contour-font-display:
|
|
9
|
+
"Space Grotesk Variable", "Space Grotesk", system-ui, sans-serif;
|
|
10
|
+
--contour-font-mono: "DM Mono", ui-monospace, monospace;
|
|
11
|
+
}
|
package/dist/forms.css
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
/** Use public virtual-anchor/Rect extension points; keep upstream focus/selection and positioning. */
|
|
3
|
+
export default function useComboboxAnchor(input: HTMLInputElement | null, floating: RefObject<HTMLDivElement | null>): {
|
|
4
|
+
anchor?: undefined;
|
|
5
|
+
collisionBoundary?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
anchor: {
|
|
8
|
+
contextElement: HTMLElement;
|
|
9
|
+
getBoundingClientRect(): DOMRect;
|
|
10
|
+
};
|
|
11
|
+
collisionBoundary: {
|
|
12
|
+
readonly x: number;
|
|
13
|
+
readonly y: number;
|
|
14
|
+
readonly width: number;
|
|
15
|
+
readonly height: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { platform } from "@floating-ui/dom";
|
|
4
|
+
/** Fixed children of a zoomed body need anchor and viewport in the same local CSS units. */
|
|
5
|
+
function windowScale(element) {
|
|
6
|
+
if (!element ||
|
|
7
|
+
platform.getOffsetParent?.(element) !== element.ownerDocument.defaultView)
|
|
8
|
+
return { x: 1, y: 1 };
|
|
9
|
+
const scale = platform.getScale?.(element);
|
|
10
|
+
if (!scale ||
|
|
11
|
+
!("x" in scale) ||
|
|
12
|
+
!("y" in scale) ||
|
|
13
|
+
!Number.isFinite(scale.x) ||
|
|
14
|
+
!Number.isFinite(scale.y) ||
|
|
15
|
+
scale.x <= 0 ||
|
|
16
|
+
scale.y <= 0)
|
|
17
|
+
return { x: 1, y: 1 };
|
|
18
|
+
return scale;
|
|
19
|
+
}
|
|
20
|
+
/** Use public virtual-anchor/Rect extension points; keep upstream focus/selection and positioning. */
|
|
21
|
+
export default function useComboboxAnchor(input, floating) {
|
|
22
|
+
return useMemo(() => {
|
|
23
|
+
const source = input?.parentElement ?? input;
|
|
24
|
+
if (!source)
|
|
25
|
+
return {};
|
|
26
|
+
return {
|
|
27
|
+
anchor: {
|
|
28
|
+
contextElement: source,
|
|
29
|
+
getBoundingClientRect() {
|
|
30
|
+
const rect = source.getBoundingClientRect(), scale = windowScale(floating.current);
|
|
31
|
+
rect.x /= scale.x;
|
|
32
|
+
rect.y /= scale.y;
|
|
33
|
+
rect.width /= scale.x;
|
|
34
|
+
rect.height /= scale.y;
|
|
35
|
+
return rect;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
// Getters are evaluated on each upstream update, including scroll, zoom and mobile viewport resize.
|
|
39
|
+
collisionBoundary: {
|
|
40
|
+
get x() {
|
|
41
|
+
return ((source.ownerDocument.defaultView?.visualViewport?.offsetLeft ??
|
|
42
|
+
0) / windowScale(floating.current).x);
|
|
43
|
+
},
|
|
44
|
+
get y() {
|
|
45
|
+
return ((source.ownerDocument.defaultView?.visualViewport?.offsetTop ?? 0) /
|
|
46
|
+
windowScale(floating.current).y);
|
|
47
|
+
},
|
|
48
|
+
get width() {
|
|
49
|
+
return ((source.ownerDocument.defaultView?.visualViewport?.width ??
|
|
50
|
+
source.ownerDocument.documentElement.clientWidth) /
|
|
51
|
+
windowScale(floating.current).x);
|
|
52
|
+
},
|
|
53
|
+
get height() {
|
|
54
|
+
return ((source.ownerDocument.defaultView?.visualViewport?.height ??
|
|
55
|
+
source.ownerDocument.documentElement.clientHeight) /
|
|
56
|
+
windowScale(floating.current).y);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}, [input, floating]);
|
|
61
|
+
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
export
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import type { DismissReason } from "../lib/layers.js";
|
|
3
|
+
export type DismissOptions = {
|
|
4
|
+
/** Additional owned elements, including portaled content and the trigger. */
|
|
5
|
+
layerElement?: RefObject<HTMLElement | null>;
|
|
6
|
+
branches?: RefObject<HTMLElement | null>[];
|
|
7
|
+
/** Native modal backdrops own outside-click behavior. */
|
|
8
|
+
outside?: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** Topmost layer dismissal in the container's document; caller cancellation is respected. */
|
|
11
|
+
/** Preserves the public void-returning dismissal hook; built-in layers use an internal handle. */
|
|
12
|
+
export default function useDismiss(container: RefObject<HTMLElement | null>, active: boolean, onDismiss: (reason: DismissReason, event: KeyboardEvent | PointerEvent) => void, options?: DismissOptions): void;
|
package/dist/hooks/useDismiss.js
CHANGED
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return;
|
|
8
|
-
const onKeyDown = (event) => {
|
|
9
|
-
if (event.key === "Escape")
|
|
10
|
-
onDismiss("escape");
|
|
11
|
-
};
|
|
12
|
-
const onPointerDown = (event) => {
|
|
13
|
-
if (!container.current?.contains(event.target))
|
|
14
|
-
onDismiss("outside");
|
|
15
|
-
};
|
|
16
|
-
document.addEventListener("keydown", onKeyDown);
|
|
17
|
-
document.addEventListener("pointerdown", onPointerDown);
|
|
18
|
-
return () => {
|
|
19
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
20
|
-
document.removeEventListener("pointerdown", onPointerDown);
|
|
21
|
-
};
|
|
22
|
-
}, [container, active, onDismiss]);
|
|
2
|
+
import useLayerDismiss from "./useLayerDismiss.js";
|
|
3
|
+
/** Topmost layer dismissal in the container's document; caller cancellation is respected. */
|
|
4
|
+
/** Preserves the public void-returning dismissal hook; built-in layers use an internal handle. */
|
|
5
|
+
export default function useDismiss(container, active, onDismiss, options = {}) {
|
|
6
|
+
useLayerDismiss(container, active, onDismiss, options);
|
|
23
7
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
export type FloatingOptions = {
|
|
3
|
+
side?: "top" | "bottom" | "left" | "right";
|
|
4
|
+
align?: "start" | "center" | "end";
|
|
5
|
+
sideOffset?: number;
|
|
6
|
+
collisionPadding?: number;
|
|
7
|
+
/** false keeps content inline; default portals into its owner body or active modal with inherited theme variables. */
|
|
8
|
+
portal?: boolean;
|
|
9
|
+
/** Must belong to the trigger's document and be inside the active modal, when applicable. */
|
|
10
|
+
container?: HTMLElement | null;
|
|
11
|
+
};
|
|
12
|
+
export declare function floatingContainer(anchor: HTMLElement, options: FloatingOptions): HTMLElement;
|
|
13
|
+
/** DOM adapter: clean up observers and ignore obsolete asynchronous positioning results. */
|
|
14
|
+
export default function useFloatingPanel(anchor: RefObject<HTMLElement | null>, panel: RefObject<HTMLElement | null>, open: boolean, { side, align, sideOffset, collisionPadding, container, }: FloatingOptions): void;
|
|
@@ -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
|
+
}
|
|
@@ -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;
|