@abuzareal/contour-ui 0.1.0 → 0.1.2
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 +74 -5
- package/README.md +134 -58
- package/RELEASING.md +16 -4
- 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/fonts-latin.css +35 -0
- 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 +21 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
Changes to the published `@abuzareal/contour-ui` package. Version headings refer to package versions, not catalogue or application versions. Consumer projects pin their own version and update separately.
|
|
4
|
+
|
|
5
|
+
## 0.1.2 — 2026-10-07
|
|
6
|
+
|
|
7
|
+
An optional font-loading entry and clearer setup documentation. Component APIs and runtime dependencies are unchanged from 0.1.1.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `fonts-latin.css`: load Latin WOFF2 files for the three system typefaces. The existing `fonts.css` retains full character coverage.
|
|
12
|
+
|
|
13
|
+
### Documentation
|
|
14
|
+
|
|
15
|
+
- Clarify installation, CSS import order, public exports, optional dependencies, and upgrade guidance.
|
|
16
|
+
- Pair live examples with their source code on the documentation site. Site navigation, theme transitions, responsive layouts, and the homepage specimen were refined; these site changes do not alter package component APIs.
|
|
17
|
+
|
|
18
|
+
### Compatibility
|
|
19
|
+
|
|
20
|
+
- React 18 and 19 remain supported. Three.js and Lenis remain optional. Packed optional-entry checks cover strict TypeScript declarations and production builds.
|
|
21
|
+
|
|
22
|
+
### Upgrading from 0.1.1
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install @abuzareal/contour-ui@0.1.2 --save-exact
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
No component changes are required. To use the smaller Latin font set, replace your `fonts.css` import with `@abuzareal/contour-ui/fonts-latin.css`. Keep the existing import for additional character coverage. Update your manifest and lockfile and check your application's rendered text.
|
|
29
|
+
|
|
30
|
+
## 0.1.1
|
|
31
|
+
|
|
32
|
+
A compatibility and interaction update for apps using React 18.3.1 or React 19.
|
|
33
|
+
|
|
34
|
+
### Compatibility
|
|
35
|
+
|
|
36
|
+
- Extend the React and React DOM peer ranges to React 19. The compatibility script checks packed consumers on 18.3.1, 19.0.0, and 19.3.0 and compiles library source against React 19 types.
|
|
37
|
+
- Preserve inert flip-card faces in both React majors, accept nullable refs in public hooks, and initialize the clipboard timer ref for React 19 types.
|
|
38
|
+
|
|
39
|
+
### Fixes
|
|
40
|
+
|
|
41
|
+
- **Dialog and drawers:** lock background scrolling while open. Nested dialogs retain the lock until the last closes; closing or unmounting restores the document's previous styles and scrollbar space. Dialog content remains scrollable on short screens.
|
|
42
|
+
- **TextField, TextArea, SelectField:** accept caller IDs and retain caller `aria-describedby` references alongside field hints and errors.
|
|
43
|
+
- **Text counters:** reflect programmatic changes to controlled values.
|
|
44
|
+
- **ThemeToggle:** use a button type that doesn't submit an enclosing form.
|
|
45
|
+
- **CountUp:** show the final value when IntersectionObserver is unavailable.
|
|
46
|
+
|
|
47
|
+
### Upgrading from 0.1.0
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
npm install @abuzareal/contour-ui@0.1.1 --save-exact
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Commit `package.json` and `package-lock.json`, then run your app's checks. Review dialogs and drawers on short screens and with nested overlays. If your app has its own background-scroll workaround, test its interaction with the library lock before removing it. Check caller-supplied field IDs/descriptions and controlled counter updates.
|
|
54
|
+
|
|
55
|
+
React 18.3.1 remains supported; adopting React 19 isn't required. Three.js and Lenis remain optional peers behind `/three` and `/scroll`. The 3D API remains experimental. The compatibility matrix doesn't establish React Server Component support or universal framework compatibility.
|
|
56
|
+
|
|
57
|
+
### Developer tooling
|
|
58
|
+
|
|
59
|
+
- Document direct package imports and add a repeatable packed-consumer compatibility check before publication.
|
|
60
|
+
|
|
3
61
|
## 0.1.0
|
|
4
62
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
63
|
+
The first package release, extracted from the portfolio and standalone catalogue.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- Reusable React components, theme tokens, CSS, and separate optional 3D and smooth-scroll entries.
|
|
68
|
+
- Typed ESM exports with font and reset styles available as opt-in imports.
|
|
69
|
+
- Regression tests, archive-content checks, and consumer build validation.
|
|
70
|
+
|
|
71
|
+
### Interaction behavior
|
|
72
|
+
|
|
73
|
+
- Synchronize theme controls, tolerate blocked preference storage, and respect system reduced motion for theme changes.
|
|
74
|
+
- Reject executable link schemes, preserve tooltip descriptions, and make hidden flip-card faces inert.
|
|
75
|
+
- Handle empty tab lists and zero-range sliders.
|
|
76
|
+
|
|
77
|
+
### Compatibility at release
|
|
9
78
|
|
|
10
|
-
|
|
79
|
+
React 18.3.1 was the verified baseline for 0.1.0; React 19 compatibility was added in 0.1.1. The 3D API was introduced as experimental.
|
package/README.md
CHANGED
|
@@ -1,111 +1,187 @@
|
|
|
1
1
|
# Contour UI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React components and CSS tokens for interfaces with paper and ink surfaces, a lime accent, and light and dark themes. Contour started as the shared UI in a personal portfolio; it now supplies components to the portfolio, its catalogue, Atmos, and Orbit.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use it when you want this visual style with typed controls, feedback, navigation, and motion. Applications own their layouts, data, validation, and workflows. Three.js scenes and smooth scrolling are separate, optional entries.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[Live catalogue](https://contour.abuzr.in/) · [npm package](https://www.npmjs.com/package/@abuzareal/contour-ui) · [Maintainer](https://github.com/abuzareal)
|
|
8
|
+
|
|
9
|
+
The catalogue currently uses **0.1.1**; this release is **0.1.2**. Catalogue examples include application patterns as well as package components. Check the exports below before treating an example as an importable component.
|
|
8
10
|
|
|
9
11
|
## Install
|
|
10
12
|
|
|
13
|
+
Use an existing React application with a bundler that resolves package CSS imports. Contour ships ESM and TypeScript declarations. The package declares Node.js 22 or newer; repository tooling uses Vite 8, which needs Node.js 22.12+ or 24+.
|
|
14
|
+
|
|
11
15
|
```sh
|
|
12
|
-
npm install @abuzareal/contour-ui
|
|
16
|
+
npm install @abuzareal/contour-ui@0.1.2 --save-exact
|
|
13
17
|
```
|
|
14
18
|
|
|
19
|
+
Your app must provide matching `react` and `react-dom` versions: **18.3.1 or React 19**. The compatibility script covers 18.3.1, 19.0.0, and 19.3.0; it doesn't establish support for every framework or React Server Component setup. Put interactive components inside a client boundary when your framework requires one.
|
|
20
|
+
|
|
21
|
+
## First component
|
|
22
|
+
|
|
23
|
+
Import the styles once in your application entry. Import components directly from the package; no generated files or local re-exports are needed.
|
|
24
|
+
|
|
15
25
|
```tsx
|
|
16
|
-
import
|
|
17
|
-
import "@abuzareal/contour-ui/
|
|
26
|
+
import { useState } from "react";
|
|
27
|
+
import "@abuzareal/contour-ui/reset.css";
|
|
28
|
+
import "@abuzareal/contour-ui/fonts.css";
|
|
18
29
|
import "@abuzareal/contour-ui/styles.css";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export function App() {
|
|
30
|
+
import { Button, TextField, ThemeToggle } from "@abuzareal/contour-ui";
|
|
31
|
+
|
|
32
|
+
export default function App() {
|
|
33
|
+
const [name, setName] = useState("");
|
|
34
|
+
const [savedName, setSavedName] = useState("");
|
|
35
|
+
|
|
27
36
|
return (
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
<main data-motion="off">
|
|
38
|
+
<ThemeToggle animate={false} />
|
|
39
|
+
<TextField
|
|
40
|
+
label="Name"
|
|
41
|
+
value={name}
|
|
42
|
+
onChange={(event) => setName(event.target.value)}
|
|
43
|
+
/>
|
|
44
|
+
<Button variant="accent" onClick={() => setSavedName(name)}>
|
|
45
|
+
Save locally
|
|
46
|
+
</Button>
|
|
47
|
+
<p role="status">{savedName && `Saved for this session: ${savedName}`}</p>
|
|
48
|
+
</main>
|
|
35
49
|
);
|
|
36
50
|
}
|
|
37
51
|
```
|
|
38
52
|
|
|
39
|
-
|
|
53
|
+
This example keeps the value in React state until the component unmounts. To persist it or send it to a server, add that behavior in your app.
|
|
54
|
+
|
|
55
|
+
`Button` and ordinary controls need no provider. Toasts require `ToastProvider` around components that call `useToast()`.
|
|
56
|
+
|
|
57
|
+
### Optional Latin fonts
|
|
58
|
+
|
|
59
|
+
`fonts-latin.css`, added in 0.1.2, keeps DM Sans, Space Grotesk, and DM Mono while loading only Latin WOFF2 files. Use it instead of `fonts.css` for Latin text; keep `fonts.css` when the additional character sets are needed. Existing font imports continue to work.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import "@abuzareal/contour-ui/fonts-latin.css";
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Styles and customization
|
|
66
|
+
|
|
67
|
+
| Import | Purpose |
|
|
68
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `styles.css` | Required for the supplied appearance: tokens, component styles, and utilities. |
|
|
70
|
+
| `reset.css` | Optional global reset. Omit it if your app already has equivalent native-control defaults, border-box sizing, focus styles, and a `[hidden]` rule. |
|
|
71
|
+
| `fonts-latin.css` | Optional Latin-only WOFF2 fonts, available since 0.1.2. Use instead of `fonts.css`, not alongside it. |
|
|
72
|
+
| `fonts.css` | Optional self-hosted Space Grotesk, DM Sans, and DM Mono via Fontsource. Your bundler emits the font assets. |
|
|
73
|
+
| `tokens.css` | Tokens alone, for an app that supplies its own component styles. Already included by `styles.css`. |
|
|
74
|
+
|
|
75
|
+
Override semantic tokens after importing the styles:
|
|
76
|
+
|
|
77
|
+
```css
|
|
78
|
+
:root {
|
|
79
|
+
--font-sans: system-ui, sans-serif;
|
|
80
|
+
--font-display: system-ui, sans-serif;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
:root[data-theme="dark"] {
|
|
84
|
+
--bg: #111411;
|
|
85
|
+
--fg: #edf0e8;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
40
88
|
|
|
41
|
-
|
|
89
|
+
Styles use global component classes and document-level theme tokens. Scope your application's classes to avoid collisions with names such as `.card` and `.button`. Contour's CSS isn't isolated with Shadow DOM or CSS Modules.
|
|
42
90
|
|
|
43
|
-
|
|
91
|
+
The three Fontsource packages and `lucide-react` are direct dependencies. Omitting `fonts.css` keeps its font assets out of your application's bundle; the font packages still install. React and React DOM are peers. Three.js and Lenis are optional peers. Development tools aren't installed as Contour's consumer dependencies; tests and tooling are excluded from the npm archive.
|
|
44
92
|
|
|
45
|
-
|
|
93
|
+
## Theme and motion
|
|
46
94
|
|
|
47
|
-
Set `data-
|
|
95
|
+
Set `data-theme="light"` or `"dark"` on `<html>`. `ThemeToggle` and `useTheme(animate)` read the same document state. The hook returns `{ theme, toggle }`; call `toggle(buttonElement)` from a user action. Preferences use the `aa-theme` local-storage key and are local to the site's origin.
|
|
48
96
|
|
|
49
|
-
|
|
97
|
+
For theme selection before first paint, inject `themeBootstrapScript` from `@abuzareal/contour-ui/theme-bootstrap` into the HTML head at build time. The exported string has no user interpolation. With a strict Content Security Policy, authorize that script with a nonce or hash.
|
|
50
98
|
|
|
51
|
-
|
|
99
|
+
`useMotionPreference()` returns whether the user prefers reduced motion. Combine it with your app's pause state, set `data-motion="off"` on the app wrapper when needed, and pass `motionEnabled` to scenes and `animate` to theme controls. System reduced motion also disables the theme reveal.
|
|
100
|
+
|
|
101
|
+
## Components
|
|
102
|
+
|
|
103
|
+
| Area | Public exports |
|
|
104
|
+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
105
|
+
| Actions | `Button`, `IconButton`, `ExternalLink`, `SegmentedControl`, `ThemeToggle` |
|
|
106
|
+
| Forms | `Field`, `TextField`, `TextArea`, `SelectField`, `Checkbox`, `RadioGroup`, `Switch`, `Slider` |
|
|
107
|
+
| Navigation | `Tabs`, `Breadcrumbs`, `Pagination`, `Stepper`, `DropdownMenu` |
|
|
108
|
+
| Data and layout | `Avatar`, `Badge`, `Card`, `DataTable`, `Disclosure`, `Divider`, `EmptyState`, `Kbd`, `SectionLabel`, `Stat`, `Tag`, `Timeline` |
|
|
109
|
+
| Feedback | `Alert`, `ProgressBar`, `Skeleton`, `Spinner`, `ToastProvider`, `useToast` |
|
|
110
|
+
| Motion and identity | `BrandMark`, `CountUp`, `DepthButton`, `FlipCard`, `Magnetic`, `Marquee`, `Reveal`, `RevealHeading`, `ScrollProgress`, `TiltCard`, `GitHubIcon`, `LinkedInIcon` |
|
|
111
|
+
| Overlays and scene fallback | `Dialog`, `Popover`, `Tooltip`, `SceneBoundary` |
|
|
112
|
+
|
|
113
|
+
The root entry also exports theme, motion, clipboard, dismissal, navigation, and browser-lifecycle hooks plus utility functions. Check `src/index.ts` in a source checkout or `dist/index.d.ts` in the installed package for the complete API. Component declarations describe their props.
|
|
114
|
+
|
|
115
|
+
Use visible field labels, accessible names for icon buttons, and text alongside status colors. `DataTable` sorts scalar string/number values; `Pagination` expects a positive integer page count and a page within that range. Flip cards use a button and make the hidden face inert.
|
|
116
|
+
|
|
117
|
+
Props support varies between components. Don't assume every export forwards DOM refs, accepts arbitrary native attributes, supports router `asChild` composition, or integrates with form libraries through refs. Verify the relevant type before adopting it.
|
|
118
|
+
|
|
119
|
+
## Optional scenes and scrolling
|
|
120
|
+
|
|
121
|
+
Core imports don't load Three.js or Lenis. Install either only if you use its entry:
|
|
52
122
|
|
|
53
123
|
```sh
|
|
54
|
-
npm install three@^0.186.1
|
|
55
|
-
npm install lenis@^1.3.26
|
|
124
|
+
npm install three@^0.186.1 # @abuzareal/contour-ui/three
|
|
125
|
+
npm install lenis@^1.3.26 # @abuzareal/contour-ui/scroll
|
|
126
|
+
# TypeScript consumers of /three also need the matching declarations:
|
|
127
|
+
npm install -D @types/three@^0.186.0
|
|
56
128
|
```
|
|
57
129
|
|
|
130
|
+
`/scroll` exports `useSmoothScroll`. `/three` exports `Sculpture`, `ObjectStudy`, `useThreeStage`, geometry/material helpers, and associated types. CSS depth controls such as `TiltCard` and `FlipCard` are core exports and don't require Three.js.
|
|
131
|
+
|
|
58
132
|
```tsx
|
|
59
133
|
import { lazy, Suspense } from "react";
|
|
134
|
+
import { SceneBoundary } from "@abuzareal/contour-ui";
|
|
135
|
+
|
|
60
136
|
const Sculpture = lazy(() =>
|
|
61
137
|
import("@abuzareal/contour-ui/three").then((module) => ({
|
|
62
138
|
default: module.Sculpture,
|
|
63
139
|
})),
|
|
64
140
|
);
|
|
65
141
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
142
|
+
export function Preview() {
|
|
143
|
+
return (
|
|
144
|
+
<div>
|
|
145
|
+
<p>Three interlocking links, shown in a WebGL preview when available.</p>
|
|
146
|
+
<SceneBoundary>
|
|
147
|
+
<Suspense fallback={<p>Loading preview…</p>}>
|
|
148
|
+
<Sculpture motionEnabled={false} />
|
|
149
|
+
</Suspense>
|
|
150
|
+
</SceneBoundary>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
70
154
|
```
|
|
71
155
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
## Components and typed APIs
|
|
75
|
-
|
|
76
|
-
The package exports named components and props types. TypeScript declarations are shipped with every entry; editors provide the complete props API.
|
|
77
|
-
|
|
78
|
-
- Actions: `Button`, `IconButton`, `ExternalLink`, `SegmentedControl`, `ThemeToggle`.
|
|
79
|
-
- Forms: `Field`, `TextField`, `TextArea`, `SelectField`, `Checkbox`, `RadioGroup`, `Switch`, `Slider`.
|
|
80
|
-
- Navigation: `Tabs`, `Breadcrumbs`, `Pagination`, `Stepper`, `DropdownMenu`.
|
|
81
|
-
- Data and layout: `Avatar`, `Badge`, `Card`, `DataTable`, `Disclosure`, `Divider`, `EmptyState`, `Kbd`, `SectionLabel`, `Stat`, `Tag`, `Timeline`.
|
|
82
|
-
- Feedback: `Alert`, `ProgressBar`, `Skeleton`, `Spinner`, `ToastProvider`, `useToast`.
|
|
83
|
-
- Motion and signature: `BrandMark`, `CountUp`, `DepthButton`, `FlipCard`, `Magnetic`, `Marquee`, `Reveal`, `RevealHeading`, `ScrollProgress`, `TiltCard`, `GitHubIcon`, `LinkedInIcon`.
|
|
84
|
-
- Overlays: `Dialog`, `Popover`, `Tooltip`, `SceneBoundary`.
|
|
156
|
+
Give scenes a sized container and a usable static fallback. `SceneBoundary` hides a scene on failure; it doesn't supply fallback content, so keep that content outside the boundary. The 3D API is experimental. Stage setup functions are captured on mount; remount a scene when its setup configuration changes. WebGL availability is a runtime requirement for scenes.
|
|
85
157
|
|
|
86
|
-
|
|
158
|
+
## Release notes and upgrades
|
|
87
159
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
## Independent repositories and updates
|
|
91
|
-
|
|
92
|
-
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.
|
|
160
|
+
**0.1.2** adds the optional `fonts-latin.css` export, clearer setup documentation, and links to the Contour UI documentation site. Component APIs and runtime dependencies remain unchanged from 0.1.1. React 19 support and the interaction fixes from 0.1.1 are retained. `CHANGELOG.md` is included in the package archive and [npm's Code view](https://www.npmjs.com/package/@abuzareal/contour-ui?activeTab=code); it contains the upgrade details for each version.
|
|
93
161
|
|
|
94
162
|
```sh
|
|
95
|
-
npm install @abuzareal/contour-ui@0.1.
|
|
163
|
+
npm install @abuzareal/contour-ui@0.1.2 --save-exact
|
|
96
164
|
```
|
|
97
165
|
|
|
98
|
-
Commit both
|
|
166
|
+
Commit your manifest and lockfile, run your application's checks, and review both themes, dialogs, forms, and motion before deploying. Version 0.1.1 includes a document scroll lock for dialogs and drawers. Review any app-level lock before removing it, including nested dialogs and short screens. Package publication doesn't update deployed consumers automatically.
|
|
167
|
+
|
|
168
|
+
The package is public under MIT. The source repository and its issue tracker currently require collaborator access. Public users can inspect the distributed code, CSS, declarations, and release documents through npm. Contact the [maintainer](https://github.com/abuzareal) for support; see `SECURITY.md` in the package for vulnerability reporting guidance.
|
|
99
169
|
|
|
100
|
-
##
|
|
170
|
+
## Develop this library
|
|
171
|
+
|
|
172
|
+
From a source checkout:
|
|
101
173
|
|
|
102
174
|
```sh
|
|
103
175
|
npm ci --ignore-scripts
|
|
176
|
+
npx playwright install chromium
|
|
104
177
|
npm run validate
|
|
178
|
+
npm run test:compat
|
|
105
179
|
npm audit
|
|
106
180
|
npm pack --dry-run
|
|
107
181
|
```
|
|
108
182
|
|
|
109
|
-
|
|
183
|
+
`validate` checks types, lint, builds, Node/component tests, dialog browser behavior, and archive contents. `test:compat` builds packed React 18/19 consumers and compiles the source with React 19 types. `test:consumer` checks one packed consumer, with optional peers absent. `test:optional` verifies strict `/three` and `/scroll` declarations and builds their imports with explicit peers and Three.js types. The compatibility gate runs both checks and verifies the Latin font assets. Compatibility runs need registry access for clean installs.
|
|
184
|
+
|
|
185
|
+
`src/` owns components, hooks, helpers, and public entries. `styles/` owns CSS. `tests/` owns regressions and browser checks; `scripts/` owns builds and package/consumer checks. `dist/` is generated. `RELEASING.md` in the source checkout and package archive covers validation and publication.
|
|
110
186
|
|
|
111
|
-
MIT licensed.
|
|
187
|
+
MIT licensed. Fontsource fonts and dependencies retain their own licenses.
|
package/RELEASING.md
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
# Releasing Contour UI
|
|
2
2
|
|
|
3
|
+
## Package-page documentation and release notes
|
|
4
|
+
|
|
5
|
+
The npm Readme tab renders the root `README.md`. npm updates it when a new package version is published; editing this repository does not update the existing npm page. Follow [npm's README update guidance](https://docs.npmjs.com/about-package-readme-files/). Do not try to overwrite a published version or unpublish it to change documentation.
|
|
6
|
+
|
|
7
|
+
Keep installation, a working example, CSS/theme setup, supported APIs, optional dependencies, limits, and a release summary in the README. Keep the detailed version history in `CHANGELOG.md`, which ships in the archive and is available through npm's Code view. The private GitHub repository is not a public documentation destination. When public docs and changelog routes exist, add their verified URLs to the README and package metadata.
|
|
8
|
+
|
|
9
|
+
Write new changes under **Unreleased**, or under a version heading explicitly marked **Pending release** when the next version has been chosen. At publication, remove the pending label and record the verified publication date. Use sections only when they contain useful changes: Added, Fixes, Compatibility, Upgrading, and Known limitations. Each entry should name the affected component/API and describe the user's resulting behavior. Include a migration example for incompatible changes. For a documentation-only patch, state that runtime behavior and dependency contracts are unchanged.
|
|
10
|
+
|
|
11
|
+
Link each release to available evidence: changed APIs, relevant tests, and public issues/PRs when readers can access them. Do not invent dates, issue numbers, performance gains, accessibility certification, or compatibility results. Confirm that README examples use the exports and props in the release archive. Run the same release gates for a documentation patch; the archive, manifest, tag, changelog, and registry version must agree. Publishing still requires the maintainer's authorization.
|
|
12
|
+
|
|
3
13
|
## Initial release
|
|
4
14
|
|
|
5
15
|
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
|
|
16
|
+
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.
|
|
17
|
+
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
18
|
4. Check `npm pack --dry-run --json` and licenses. No private data, credentials, unrelated assets, maps, or app content may enter the archive.
|
|
9
19
|
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
20
|
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.
|
|
@@ -32,6 +42,8 @@ Increment PATCH for compatible fixes, MINOR for compatible additions, and MAJOR
|
|
|
32
42
|
|
|
33
43
|
Dependabot is selected because it is integrated with GitHub and requires no extra hosted service or bot account. It proposes weekly npm and action updates; the library is not auto-merged. npm distribution is public. Standard-hosted Actions run only if the repository is public; checks run locally while it stays private. Do not enable paid runners, private npm distribution, or paid security subscriptions as part of this setup.
|
|
34
44
|
|
|
35
|
-
The validation workflows deliberately skip private repositories.
|
|
45
|
+
The library's validation workflows deliberately skip private repositories. Consumer policies vary: Portfolio allows private pull requests to opt in with the `run-validation` label. Check each consumer's workflow and run its required checks locally when CI is skipped. Enabling private-repository Actions requires reviewing that account's included usage and spending settings first.
|
|
46
|
+
|
|
47
|
+
Consumers install with `npm install @abuzareal/contour-ui@VERSION --save-exact`, commit their lockfile, run their checks, and redeploy. Portfolio, Design-System, Atmos, Orbit, and the library remain independent repositories.
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
The compatibility gate also installs the optional Three.js and Lenis peers in separate packed consumers. It checks `/three` and `/scroll` with `skipLibCheck: false`, builds their imports, and verifies the Latin WOFF2 export. Core consumers continue to run without those optional peers. Run `npm run test:optional` for that focused check.
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Optional Latin-only WOFF2 fonts. Use fonts.css for the complete character sets. */
|
|
2
|
+
@font-face {
|
|
3
|
+
font-family: "DM Sans Variable";
|
|
4
|
+
font-style: normal;
|
|
5
|
+
font-display: swap;
|
|
6
|
+
font-weight: 100 1000;
|
|
7
|
+
src: url(@fontsource-variable/dm-sans/files/dm-sans-latin-wght-normal.woff2)
|
|
8
|
+
format("woff2-variations");
|
|
9
|
+
unicode-range:
|
|
10
|
+
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
|
11
|
+
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
|
|
12
|
+
U+2215, U+FEFF, U+FFFD;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@font-face {
|
|
16
|
+
font-family: "Space Grotesk Variable";
|
|
17
|
+
font-style: normal;
|
|
18
|
+
font-display: swap;
|
|
19
|
+
font-weight: 300 700;
|
|
20
|
+
src: url(@fontsource-variable/space-grotesk/files/space-grotesk-latin-wght-normal.woff2)
|
|
21
|
+
format("woff2-variations");
|
|
22
|
+
unicode-range:
|
|
23
|
+
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
|
24
|
+
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
|
|
25
|
+
U+2215, U+FEFF, U+FFFD;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@font-face {
|
|
29
|
+
font-family: "DM Mono";
|
|
30
|
+
font-style: normal;
|
|
31
|
+
font-display: swap;
|
|
32
|
+
font-weight: 400;
|
|
33
|
+
src: url(@fontsource/dm-mono/files/dm-mono-latin-400-normal.woff2)
|
|
34
|
+
format("woff2");
|
|
35
|
+
}
|
|
@@ -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.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Editorial React components, themes, restrained motion, and optional Three.js objects.",
|
|
6
6
|
"type": "module",
|
|
@@ -35,17 +35,21 @@
|
|
|
35
35
|
"./styles.css": "./dist/styles.css",
|
|
36
36
|
"./reset.css": "./dist/reset.css",
|
|
37
37
|
"./tokens.css": "./dist/tokens.css",
|
|
38
|
-
"./fonts.css": "./dist/fonts.css"
|
|
38
|
+
"./fonts.css": "./dist/fonts.css",
|
|
39
|
+
"./fonts-latin.css": "./dist/fonts-latin.css"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"build": "node scripts/build.mjs",
|
|
42
43
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
44
|
"lint": "oxlint src --deny-warnings",
|
|
44
45
|
"test": "node --test tests/*.test.mjs",
|
|
46
|
+
"test:browser": "node --test tests/*.browser.mjs",
|
|
45
47
|
"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"
|
|
48
|
+
"validate": "npm run typecheck && npm run lint && npm run build && npm test && npm run test:browser && npm run check:package",
|
|
49
|
+
"prepublishOnly": "npm run validate && npm run test:compat",
|
|
50
|
+
"test:consumer": "node scripts/test-consumer.mjs",
|
|
51
|
+
"test:compat": "node scripts/test-compat.mjs",
|
|
52
|
+
"test:optional": "node scripts/test-optional-consumer.mjs"
|
|
49
53
|
},
|
|
50
54
|
"dependencies": {
|
|
51
55
|
"@fontsource-variable/dm-sans": "^5.3.0",
|
|
@@ -55,8 +59,8 @@
|
|
|
55
59
|
},
|
|
56
60
|
"peerDependencies": {
|
|
57
61
|
"lenis": "^1.3.26",
|
|
58
|
-
"react": "^18.3.1",
|
|
59
|
-
"react-dom": "^18.3.1",
|
|
62
|
+
"react": "^18.3.1 || ^19.0.0",
|
|
63
|
+
"react-dom": "^18.3.1 || ^19.0.0",
|
|
60
64
|
"three": "^0.186.1"
|
|
61
65
|
},
|
|
62
66
|
"peerDependenciesMeta": {
|
|
@@ -68,8 +72,8 @@
|
|
|
68
72
|
}
|
|
69
73
|
},
|
|
70
74
|
"devDependencies": {
|
|
75
|
+
"@playwright/test": "1.63.0",
|
|
71
76
|
"@testing-library/react": "^16.3.3",
|
|
72
|
-
"@types/node": "^24.13.3",
|
|
73
77
|
"@types/react": "^18.3.5",
|
|
74
78
|
"@types/react-dom": "^18.3.0",
|
|
75
79
|
"@types/three": "^0.186.0",
|
|
@@ -94,8 +98,15 @@
|
|
|
94
98
|
"type": "git",
|
|
95
99
|
"url": "git+https://github.com/abuzareal/contour-ui.git"
|
|
96
100
|
},
|
|
97
|
-
"homepage": "https://
|
|
101
|
+
"homepage": "https://contour.abuzr.in/",
|
|
98
102
|
"bugs": {
|
|
99
103
|
"url": "https://github.com/abuzareal/contour-ui/issues"
|
|
100
|
-
}
|
|
104
|
+
},
|
|
105
|
+
"keywords": [
|
|
106
|
+
"react",
|
|
107
|
+
"typescript",
|
|
108
|
+
"components",
|
|
109
|
+
"design-system",
|
|
110
|
+
"contour-ui"
|
|
111
|
+
]
|
|
101
112
|
}
|