@abuzareal/contour-ui 0.1.0 → 0.1.1
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 +10 -0
- package/README.md +31 -3
- package/RELEASING.md +2 -2
- package/dist/components/ui/CountUp.js +3 -1
- package/dist/components/ui/Dialog.js +9 -0
- package/dist/components/ui/FlipCard.js +6 -2
- package/dist/components/ui/SelectField.d.ts +2 -2
- package/dist/components/ui/SelectField.js +6 -3
- package/dist/components/ui/TextArea.d.ts +2 -2
- package/dist/components/ui/TextArea.js +8 -3
- package/dist/components/ui/TextField.d.ts +2 -2
- package/dist/components/ui/TextField.js +8 -3
- package/dist/components/ui/ThemeToggle.js +1 -1
- package/dist/hooks/useClipboardCopy.js +1 -1
- package/dist/hooks/useDismiss.d.ts +1 -1
- package/dist/hooks/useHeaderScroll.d.ts +1 -1
- package/dist/hooks/useMobileMenu.d.ts +1 -1
- package/dist/hooks/useThreeStage.d.ts +1 -1
- package/dist/kit/overlays.css +1 -0
- package/dist/lib/lockDocumentScroll.d.ts +1 -0
- package/dist/lib/lockDocumentScroll.js +43 -0
- package/package.json +9 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
- Lock background scrolling while a Dialog or drawer is open. Nested dialogs retain the lock until the last closes; closing or unmounting restores existing document styles and scrollbar space.
|
|
6
|
+
|
|
7
|
+
- Support React and React DOM 19 alongside 18.3.1, verified with strict clean-consumer installs, declaration checks, production builds, and runtime tests on 18.3.1, 19.0.0, and 19.3.0.
|
|
8
|
+
- Preserve flip-card inert faces in both React majors, accept nullable refs in public hooks, and initialize the clipboard timer ref explicitly for React 19 types.
|
|
9
|
+
- Let text fields, text areas, and select fields accept caller IDs and retain caller descriptions; keep controlled text counters synchronized with programmatic values.
|
|
10
|
+
- Prevent theme buttons from submitting forms, and show final count-up values when IntersectionObserver is unavailable.
|
|
11
|
+
- Document direct package imports and retain a repeatable compatibility check before publication.
|
|
12
|
+
|
|
3
13
|
## 0.1.0
|
|
4
14
|
|
|
5
15
|
- Extract reusable React components, theme tokens, styling, and optional 3D/scroll entries from the standalone design-system app.
|
package/README.md
CHANGED
|
@@ -4,12 +4,14 @@ An independent React component library for editorial interfaces: paper and ink s
|
|
|
4
4
|
|
|
5
5
|
The npm package is public under MIT; the development repository is private. Public consumers can inspect the distributed JavaScript, declarations, and CSS, but the repository and its issue tracker are accessible only to invited collaborators.
|
|
6
6
|
|
|
7
|
-
**Status:**
|
|
7
|
+
**Status:** `0.1.1` compatibility update. React 18.3.1 and React 19 are supported; the packed-package matrix checks React 18.3.1, 19.0.0, and 19.3.0. React Server Component execution is not advertised as tested. Interactive components belong in a client boundary in frameworks such as Next.js. The root module can be imported on the server, and basic components can render there.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npm install @abuzareal/contour-ui
|
|
12
|
+
npm install @abuzareal/contour-ui
|
|
13
|
+
# Your application must supply matching React and React DOM versions:
|
|
14
|
+
# React 18.3.1 or React 19.
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
```tsx
|
|
@@ -38,6 +40,28 @@ export function App() {
|
|
|
38
40
|
|
|
39
41
|
For an app with its own reset, include equivalent native button/input defaults, border-box sizing, focus outlines, and the `[hidden]` rule. Styles use globally named component classes and `:root` theme tokens; they are not Shadow DOM isolated. Avoid redefining generic classes such as `.card` or `.button` elsewhere.
|
|
40
42
|
|
|
43
|
+
## Direct imports: no local component files required
|
|
44
|
+
|
|
45
|
+
Use named imports directly in any component file:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { Button } from "@abuzareal/contour-ui";
|
|
49
|
+
|
|
50
|
+
export function SaveAction() {
|
|
51
|
+
return (
|
|
52
|
+
<Button variant="accent" onClick={() => console.log("Saved")}>
|
|
53
|
+
Save
|
|
54
|
+
</Button>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Import `styles.css` once at the application entry. `reset.css` and `fonts.css` are optional. No provider is needed for `Button` or ordinary controls; only toast consumers require `ToastProvider`.
|
|
60
|
+
|
|
61
|
+
The original Portfolio and Design-System applications retained thin compatibility re-exports to preserve their pre-package imports. Those files are an application migration choice, not a requirement of this library. Create a wrapper only when it represents your own reusable application behavior.
|
|
62
|
+
|
|
63
|
+
Text fields accept explicit `id` values and preserve your `aria-describedby` references alongside their own hint or error. Native props follow the documented component types; not every component currently accepts `className`, DOM refs, or arbitrary native attributes. Do not assume router `asChild` composition or form-library ref integration is available in this release.
|
|
64
|
+
|
|
41
65
|
## Theme and motion setup
|
|
42
66
|
|
|
43
67
|
All theme toggles read the same document state. Set `data-theme="light"` or `"dark"` on `<html>`. `useTheme(animate)` returns `{ theme, toggle }`; call `toggle(buttonElement)` for a user action. The existing `aa-theme` local-storage key is preserved for compatibility with the original portfolio. Preference storage is origin-local, not shared across different website domains.
|
|
@@ -83,6 +107,8 @@ The package exports named components and props types. TypeScript declarations ar
|
|
|
83
107
|
- Motion and signature: `BrandMark`, `CountUp`, `DepthButton`, `FlipCard`, `Magnetic`, `Marquee`, `Reveal`, `RevealHeading`, `ScrollProgress`, `TiltCard`, `GitHubIcon`, `LinkedInIcon`.
|
|
84
108
|
- Overlays: `Dialog`, `Popover`, `Tooltip`, `SceneBoundary`.
|
|
85
109
|
|
|
110
|
+
`Dialog` locks document scrolling while open, including drawer placements. Its content remains scrollable on short screens. Nested dialogs share the lock; closing or unmounting the last restores the document's previous styles. This behavior is included in version 0.1.1.
|
|
111
|
+
|
|
86
112
|
Use visible labels for fields, concise action labels for buttons, accessible labels for icon-only controls, and text alongside status colors. Sortable tables expect scalar string/number row values. Pagination expects a positive integer page count and a page in that range. Flip cards switch through their button; the hidden side is inert.
|
|
87
113
|
|
|
88
114
|
Global colors, fonts, radii, duration, and spacing variables are in `tokens.css`. Override semantic tokens rather than copying components into your app. `styles.css` includes tokens and component styles; `fonts.css` and `reset.css` are separately opt-in. Do not import catalogue or portfolio CSS into other apps.
|
|
@@ -92,7 +118,7 @@ Global colors, fonts, radii, duration, and spacing variables are in `tokens.css`
|
|
|
92
118
|
This repository contains only the library. The catalogue and portfolio remain separate repositories and consume the same published npm version. During first-release validation, a packed archive can be installed locally; replace the archive dependency with the registry version before committing consumer manifests.
|
|
93
119
|
|
|
94
120
|
```sh
|
|
95
|
-
npm install @abuzareal/contour-ui@0.1.
|
|
121
|
+
npm install @abuzareal/contour-ui@0.1.1 --save-exact
|
|
96
122
|
```
|
|
97
123
|
|
|
98
124
|
Commit both `package.json` and `package-lock.json`. Dependabot proposes version updates in each consumer repository. Updates reach production after that project's checks, merge, rebuild, and deployment. A registry release does not change deployed applications automatically.
|
|
@@ -101,7 +127,9 @@ Commit both `package.json` and `package-lock.json`. Dependabot proposes version
|
|
|
101
127
|
|
|
102
128
|
```sh
|
|
103
129
|
npm ci --ignore-scripts
|
|
130
|
+
npx playwright install chromium # first browser-test run
|
|
104
131
|
npm run validate
|
|
132
|
+
npm run test:compat # packed consumers in both React majors and source compilation with React 19 types
|
|
105
133
|
npm audit
|
|
106
134
|
npm pack --dry-run
|
|
107
135
|
```
|
package/RELEASING.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
## Initial release
|
|
4
4
|
|
|
5
5
|
1. Verify `npm whoami` is `abuzareal`; the package is `@abuzareal/contour-ui` and public under MIT.
|
|
6
|
-
2. Run `npm ci --ignore-scripts`, `npm run validate`, and `npm audit`. Review every advisory; do not release with unexplained audit findings.
|
|
7
|
-
3. Test the actual packed archive in
|
|
6
|
+
2. Run `npm ci --ignore-scripts`, install Chromium with `npx playwright install chromium` on a new machine, then run `npm run validate`, `npm run test:compat`, and `npm audit`. Review every advisory; do not release with unexplained audit findings.
|
|
7
|
+
3. Test the actual packed archive in clean React 18 and React 19 consumers without optional Three.js or Lenis, and in the catalogue and portfolio with their full browser suites.
|
|
8
8
|
4. Check `npm pack --dry-run --json` and licenses. No private data, credentials, unrelated assets, maps, or app content may enter the archive.
|
|
9
9
|
5. Push the reviewed source and release commit to the independent GitHub repository. The owner currently keeps this repository private; the npm distribution remains public under MIT.
|
|
10
10
|
6. Set `private` to `false` only for an approved, validated release. Publish from the verified owner account: `npm publish --access public`. Complete npm's required authentication/2FA personally.
|
|
@@ -10,7 +10,9 @@ export default function CountUp({ to, duration = 1400, digits = 0, suffix = "",
|
|
|
10
10
|
const element = ref.current;
|
|
11
11
|
if (!element)
|
|
12
12
|
return;
|
|
13
|
-
if (motionDisabledFor(element) ||
|
|
13
|
+
if (motionDisabledFor(element) ||
|
|
14
|
+
duration <= 0 ||
|
|
15
|
+
typeof IntersectionObserver === "undefined") {
|
|
14
16
|
setCurrent(to);
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useId, useRef } from "react";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import { cx } from "../../lib/classNames.js";
|
|
6
|
+
import { lockDocumentScroll } from "../../lib/lockDocumentScroll.js";
|
|
6
7
|
export default function Dialog({ open, onClose, title, children, description, footer, placement = "center", }) {
|
|
7
8
|
const ref = useRef(null);
|
|
8
9
|
const titleId = useId();
|
|
@@ -15,6 +16,14 @@ export default function Dialog({ open, onClose, title, children, description, fo
|
|
|
15
16
|
dialog.showModal();
|
|
16
17
|
if (!open && dialog.open)
|
|
17
18
|
dialog.close();
|
|
19
|
+
if (!open)
|
|
20
|
+
return;
|
|
21
|
+
const release = lockDocumentScroll(dialog.ownerDocument);
|
|
22
|
+
dialog.addEventListener("close", release);
|
|
23
|
+
return () => {
|
|
24
|
+
dialog.removeEventListener("close", release);
|
|
25
|
+
release();
|
|
26
|
+
};
|
|
18
27
|
}, [open]);
|
|
19
28
|
return (_jsx("dialog", { ref: ref, className: cx("dialog", `dialog-${placement}`), "aria-labelledby": titleId, "aria-describedby": description ? descriptionId : undefined, onCancel: (event) => {
|
|
20
29
|
event.preventDefault();
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/** Two-sided card that flips in 3D; a button toggles it so the back is reachable without hover. */
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useState, version } from "react";
|
|
4
4
|
import { RefreshCw } from "lucide-react";
|
|
5
5
|
import { cx } from "../../lib/classNames.js";
|
|
6
|
+
// React 18 treats inert as an unknown string attribute; React 19 treats it as boolean.
|
|
7
|
+
const inertAttributes = {
|
|
8
|
+
inert: Number.parseInt(version, 10) >= 19 ? true : "",
|
|
9
|
+
};
|
|
6
10
|
export default function FlipCard({ front, back, label }) {
|
|
7
11
|
const [flipped, setFlipped] = useState(false);
|
|
8
|
-
return (_jsxs("div", { className: "flip-stage", children: [_jsxs("div", { className: cx("flip-card", flipped && "is-flipped"), children: [_jsx("div", { className: "flip-face flip-front", "aria-hidden": flipped, ...(flipped ?
|
|
12
|
+
return (_jsxs("div", { className: "flip-stage", children: [_jsxs("div", { className: cx("flip-card", flipped && "is-flipped"), children: [_jsx("div", { className: "flip-face flip-front", "aria-hidden": flipped, ...(flipped ? inertAttributes : {}), children: front }), _jsx("div", { className: "flip-face flip-back", "aria-hidden": !flipped, ...(!flipped ? inertAttributes : {}), children: back })] }), _jsxs("button", { type: "button", className: "flip-toggle mono", "aria-pressed": flipped, onClick: () => setFlipped((value) => !value), children: [_jsx(RefreshCw, { size: 13, "aria-hidden": "true" }), label] })] }));
|
|
9
13
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Native select styled as an underlined field with a chevron. */
|
|
2
2
|
import { type SelectHTMLAttributes } from "react";
|
|
3
|
-
export type SelectFieldProps =
|
|
3
|
+
export type SelectFieldProps = SelectHTMLAttributes<HTMLSelectElement> & {
|
|
4
4
|
label: string;
|
|
5
5
|
options: {
|
|
6
6
|
value: string;
|
|
@@ -10,4 +10,4 @@ export type SelectFieldProps = Omit<SelectHTMLAttributes<HTMLSelectElement>, "id
|
|
|
10
10
|
error?: string;
|
|
11
11
|
placeholder?: string;
|
|
12
12
|
};
|
|
13
|
-
export default function SelectField({ label, options, hint, error, placeholder, ...rest }: SelectFieldProps): import("react").JSX.Element;
|
|
13
|
+
export default function SelectField({ id: providedId, "aria-describedby": describedBy, label, options, hint, error, placeholder, ...rest }: SelectFieldProps): import("react").JSX.Element;
|
|
@@ -4,7 +4,10 @@ import { useId } from "react";
|
|
|
4
4
|
import { ChevronDown } from "lucide-react";
|
|
5
5
|
import { fieldDescribedBy } from "../../lib/a11y.js";
|
|
6
6
|
import Field from "./Field.js";
|
|
7
|
-
export default function SelectField({ label, options, hint, error, placeholder, ...rest }) {
|
|
8
|
-
const
|
|
9
|
-
|
|
7
|
+
export default function SelectField({ id: providedId, "aria-describedby": describedBy, label, options, hint, error, placeholder, ...rest }) {
|
|
8
|
+
const generatedId = useId();
|
|
9
|
+
const id = providedId ?? generatedId;
|
|
10
|
+
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, id: id, "aria-invalid": error ? true : undefined, "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.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" })] }) }));
|
|
10
13
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/** Multi-line text input with optional auto-resize and character counter. */
|
|
2
2
|
import { type TextareaHTMLAttributes } from "react";
|
|
3
|
-
export type TextAreaProps =
|
|
3
|
+
export type TextAreaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
|
4
4
|
label: string;
|
|
5
5
|
hint?: string;
|
|
6
6
|
error?: string;
|
|
7
7
|
/** Grows with its content instead of scrolling. */
|
|
8
8
|
autoResize?: boolean;
|
|
9
9
|
};
|
|
10
|
-
export default function TextArea({ label, hint, error, autoResize, maxLength, onChange, rows, ...rest }: TextAreaProps): import("react").JSX.Element;
|
|
10
|
+
export default function TextArea({ id: providedId, "aria-describedby": describedBy, label, hint, error, autoResize, maxLength, onChange, rows, ...rest }: TextAreaProps): import("react").JSX.Element;
|
|
@@ -3,8 +3,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId, useLayoutEffect, useRef, useState, } from "react";
|
|
4
4
|
import { fieldDescribedBy } from "../../lib/a11y.js";
|
|
5
5
|
import Field from "./Field.js";
|
|
6
|
-
export default function TextArea({ label, hint, error, autoResize = false, maxLength, onChange, rows = 3, ...rest }) {
|
|
7
|
-
const
|
|
6
|
+
export default function TextArea({ id: providedId, "aria-describedby": describedBy, label, hint, error, autoResize = false, maxLength, onChange, rows = 3, ...rest }) {
|
|
7
|
+
const generatedId = useId();
|
|
8
|
+
const id = providedId ?? generatedId;
|
|
8
9
|
const ref = useRef(null);
|
|
9
10
|
const [length, setLength] = useState(String(rest.value ?? rest.defaultValue ?? "").length);
|
|
10
11
|
const fitContent = () => {
|
|
@@ -15,7 +16,11 @@ export default function TextArea({ label, hint, error, autoResize = false, maxLe
|
|
|
15
16
|
element.style.height = `${element.scrollHeight}px`;
|
|
16
17
|
};
|
|
17
18
|
useLayoutEffect(fitContent);
|
|
18
|
-
return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, disabled: rest.disabled, counter: maxLength
|
|
19
|
+
return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, disabled: rest.disabled, counter: maxLength
|
|
20
|
+
? `${rest.value !== undefined ? String(rest.value).length : length}/${maxLength}`
|
|
21
|
+
: undefined, children: _jsx("div", { className: "field-control", children: _jsx("textarea", { ...rest, ref: ref, id: id, rows: rows, maxLength: maxLength, "data-auto-resize": autoResize || undefined, "aria-invalid": error ? true : undefined, "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join(" ") || undefined, onChange: (event) => {
|
|
19
24
|
setLength(event.target.value.length);
|
|
20
25
|
fitContent();
|
|
21
26
|
onChange?.(event);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Single-line text input with underline styling, optional icon/adornment, and character counter. */
|
|
2
2
|
import { type InputHTMLAttributes, type ReactNode } from "react";
|
|
3
|
-
export type TextFieldProps =
|
|
3
|
+
export type TextFieldProps = InputHTMLAttributes<HTMLInputElement> & {
|
|
4
4
|
label: string;
|
|
5
5
|
hint?: string;
|
|
6
6
|
error?: string;
|
|
@@ -9,4 +9,4 @@ export type TextFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "id"> &
|
|
|
9
9
|
/** Content after the input, e.g. a unit or an icon button. */
|
|
10
10
|
trailing?: ReactNode;
|
|
11
11
|
};
|
|
12
|
-
export default function TextField({ label, hint, error, hideLabel, leadingIcon, trailing, maxLength, onChange, ...rest }: TextFieldProps): import("react").JSX.Element;
|
|
12
|
+
export default function TextField({ id: providedId, "aria-describedby": describedBy, label, hint, error, hideLabel, leadingIcon, trailing, maxLength, onChange, ...rest }: TextFieldProps): import("react").JSX.Element;
|
|
@@ -3,10 +3,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId, useState, } from "react";
|
|
4
4
|
import { fieldDescribedBy } from "../../lib/a11y.js";
|
|
5
5
|
import Field from "./Field.js";
|
|
6
|
-
export default function TextField({ label, hint, error, hideLabel, leadingIcon, trailing, maxLength, onChange, ...rest }) {
|
|
7
|
-
const
|
|
6
|
+
export default function TextField({ id: providedId, "aria-describedby": describedBy, label, hint, error, hideLabel, leadingIcon, trailing, maxLength, onChange, ...rest }) {
|
|
7
|
+
const generatedId = useId();
|
|
8
|
+
const id = providedId ?? generatedId;
|
|
8
9
|
const [length, setLength] = useState(String(rest.value ?? rest.defaultValue ?? "").length);
|
|
9
|
-
return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, hideLabel: hideLabel, disabled: rest.disabled, counter: maxLength
|
|
10
|
+
return (_jsx(Field, { id: id, label: label, hint: hint, error: error, required: rest.required, hideLabel: hideLabel, disabled: rest.disabled, counter: maxLength
|
|
11
|
+
? `${rest.value !== undefined ? String(rest.value).length : length}/${maxLength}`
|
|
12
|
+
: undefined, children: _jsxs("div", { className: "field-control", children: [leadingIcon && _jsx("span", { className: "field-icon", children: leadingIcon }), _jsx("input", { ...rest, id: id, maxLength: maxLength, "aria-invalid": error ? true : undefined, "aria-describedby": [describedBy, fieldDescribedBy(id, hint, error)]
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.join(" ") || undefined, onChange: (event) => {
|
|
10
15
|
setLength(event.target.value.length);
|
|
11
16
|
onChange?.(event);
|
|
12
17
|
} }), trailing && _jsx("span", { className: "field-trailing", children: trailing })] }) }));
|
|
@@ -5,5 +5,5 @@ import useTheme from "../../hooks/useTheme.js";
|
|
|
5
5
|
export default function ThemeToggle({ animate }) {
|
|
6
6
|
const { theme, toggle } = useTheme(animate);
|
|
7
7
|
const dark = theme === "dark";
|
|
8
|
-
return (_jsxs("button", { className: "theme-toggle", "aria-label": "Dark theme", "aria-pressed": dark, title: dark ? "Switch to light theme" : "Switch to dark theme", onClick: (event) => toggle(event.currentTarget), children: [_jsx(Moon, { className: "theme-icon-moon", size: 16, "aria-hidden": "true" }), _jsx(Sun, { className: "theme-icon-sun", size: 17, "aria-hidden": "true" })] }));
|
|
8
|
+
return (_jsxs("button", { type: "button", className: "theme-toggle", "aria-label": "Dark theme", "aria-pressed": dark, title: dark ? "Switch to light theme" : "Switch to dark theme", onClick: (event) => toggle(event.currentTarget), children: [_jsx(Moon, { className: "theme-icon-moon", size: 16, "aria-hidden": "true" }), _jsx(Sun, { className: "theme-icon-sun", size: 17, "aria-hidden": "true" })] }));
|
|
9
9
|
}
|
|
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
3
3
|
const resetDelay = 4000;
|
|
4
4
|
export default function useClipboardCopy(text) {
|
|
5
5
|
const [status, setStatus] = useState("idle");
|
|
6
|
-
const resetTimer = useRef();
|
|
6
|
+
const resetTimer = useRef(undefined);
|
|
7
7
|
useEffect(() => () => clearTimeout(resetTimer.current), []);
|
|
8
8
|
const copy = useCallback(async () => {
|
|
9
9
|
clearTimeout(resetTimer.current);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Calls onDismiss on Escape or a pointer press outside the container while `active`. */
|
|
2
2
|
import { type RefObject } from "react";
|
|
3
|
-
export default function useDismiss(container: RefObject<HTMLElement>, active: boolean, onDismiss: (reason: "escape" | "outside") => void): void;
|
|
3
|
+
export default function useDismiss(container: RefObject<HTMLElement | null>, active: boolean, onDismiss: (reason: "escape" | "outside") => void): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Derives header state from scroll position: scrolled, over a dark area, and the active section. */
|
|
2
2
|
import { type RefObject } from "react";
|
|
3
|
-
export default function useHeaderScroll(header: RefObject<HTMLElement>): {
|
|
3
|
+
export default function useHeaderScroll(header: RefObject<HTMLElement | null>): {
|
|
4
4
|
scrolled: boolean;
|
|
5
5
|
overDark: boolean;
|
|
6
6
|
activeId: string;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Open state for the mobile menu, closing on Escape, outside pointer, or desktop resize. */
|
|
2
2
|
import { type RefObject } from "react";
|
|
3
|
-
export default function useMobileMenu(container: RefObject<HTMLElement>, toggleButton: RefObject<HTMLButtonElement>): readonly [boolean, import("react").Dispatch<import("react").SetStateAction<boolean>>];
|
|
3
|
+
export default function useMobileMenu(container: RefObject<HTMLElement | null>, toggleButton: RefObject<HTMLButtonElement | null>): readonly [boolean, import("react").Dispatch<import("react").SetStateAction<boolean>>];
|
|
@@ -24,5 +24,5 @@ type StageOptions = {
|
|
|
24
24
|
/** Camera z for a given aspect ratio, keeping the subject framed. */
|
|
25
25
|
cameraDistance: (aspect: number) => number;
|
|
26
26
|
};
|
|
27
|
-
export default function useThreeStage(hostRef: RefObject<HTMLDivElement>, { motionEnabled, setup, cameraDistance }: StageOptions): void;
|
|
27
|
+
export default function useThreeStage(hostRef: RefObject<HTMLDivElement | null>, { motionEnabled, setup, cameraDistance }: StageOptions): void;
|
|
28
28
|
export {};
|
package/dist/kit/overlays.css
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function lockDocumentScroll(document: Document): () => void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Reference-counted per document so nested dialogs cannot unlock each other. */
|
|
2
|
+
const locks = new WeakMap();
|
|
3
|
+
export function lockDocumentScroll(document) {
|
|
4
|
+
let lock = locks.get(document);
|
|
5
|
+
if (!lock) {
|
|
6
|
+
const root = document.documentElement;
|
|
7
|
+
const properties = ["overflow", "overscroll-behavior", "scrollbar-gutter"];
|
|
8
|
+
const previous = properties.map((name) => ({
|
|
9
|
+
name,
|
|
10
|
+
value: root.style.getPropertyValue(name),
|
|
11
|
+
priority: root.style.getPropertyPriority(name),
|
|
12
|
+
}));
|
|
13
|
+
// Preserve an existing scrollbar's space; short pages should gain no gutter.
|
|
14
|
+
if (document.defaultView &&
|
|
15
|
+
document.defaultView.innerWidth > root.clientWidth)
|
|
16
|
+
root.style.setProperty("scrollbar-gutter", "stable", "important");
|
|
17
|
+
root.style.setProperty("overflow", "hidden", "important");
|
|
18
|
+
root.style.setProperty("overscroll-behavior", "none", "important");
|
|
19
|
+
lock = {
|
|
20
|
+
count: 0,
|
|
21
|
+
restore: () => {
|
|
22
|
+
for (const { name, value, priority } of previous) {
|
|
23
|
+
if (value)
|
|
24
|
+
root.style.setProperty(name, value, priority);
|
|
25
|
+
else
|
|
26
|
+
root.style.removeProperty(name);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
locks.set(document, lock);
|
|
31
|
+
}
|
|
32
|
+
lock.count += 1;
|
|
33
|
+
let released = false;
|
|
34
|
+
return () => {
|
|
35
|
+
if (released)
|
|
36
|
+
return;
|
|
37
|
+
released = true;
|
|
38
|
+
if (--lock.count === 0) {
|
|
39
|
+
lock.restore();
|
|
40
|
+
locks.delete(document);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abuzareal/contour-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Editorial React components, themes, restrained motion, and optional Three.js objects.",
|
|
6
6
|
"type": "module",
|
|
@@ -42,10 +42,12 @@
|
|
|
42
42
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
43
|
"lint": "oxlint src --deny-warnings",
|
|
44
44
|
"test": "node --test tests/*.test.mjs",
|
|
45
|
+
"test:browser": "node --test tests/*.browser.mjs",
|
|
45
46
|
"check:package": "node scripts/check-package.mjs",
|
|
46
|
-
"validate": "npm run typecheck && npm run lint && npm run build && npm test && npm run check:package",
|
|
47
|
-
"prepublishOnly": "npm run validate",
|
|
48
|
-
"test:consumer": "node scripts/test-consumer.mjs"
|
|
47
|
+
"validate": "npm run typecheck && npm run lint && npm run build && npm test && npm run test:browser && npm run check:package",
|
|
48
|
+
"prepublishOnly": "npm run validate && npm run test:compat",
|
|
49
|
+
"test:consumer": "node scripts/test-consumer.mjs",
|
|
50
|
+
"test:compat": "node scripts/test-compat.mjs"
|
|
49
51
|
},
|
|
50
52
|
"dependencies": {
|
|
51
53
|
"@fontsource-variable/dm-sans": "^5.3.0",
|
|
@@ -55,8 +57,8 @@
|
|
|
55
57
|
},
|
|
56
58
|
"peerDependencies": {
|
|
57
59
|
"lenis": "^1.3.26",
|
|
58
|
-
"react": "^18.3.1",
|
|
59
|
-
"react-dom": "^18.3.1",
|
|
60
|
+
"react": "^18.3.1 || ^19.0.0",
|
|
61
|
+
"react-dom": "^18.3.1 || ^19.0.0",
|
|
60
62
|
"three": "^0.186.1"
|
|
61
63
|
},
|
|
62
64
|
"peerDependenciesMeta": {
|
|
@@ -68,6 +70,7 @@
|
|
|
68
70
|
}
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|
|
73
|
+
"@playwright/test": "1.63.0",
|
|
71
74
|
"@testing-library/react": "^16.3.3",
|
|
72
75
|
"@types/node": "^24.13.3",
|
|
73
76
|
"@types/react": "^18.3.5",
|