@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 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
- - Extract reusable React components, theme tokens, styling, and optional 3D/scroll entries from the standalone design-system app.
6
- - Synchronize theme controls, tolerate blocked preference storage, and honor system reduced motion for theme changes.
7
- - Reject executable link schemes, preserve tooltip descriptions, make hidden flip-card faces inert, and handle empty tabs and zero-range sliders.
8
- - Ship typed ESM exports, optional font/reset styles, regression tests, and archive-content validation.
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
- This is the first release. The 3D interface is experimental; React 19 compatibility has not been verified.
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
- An independent React component library for editorial interfaces: paper and ink surfaces, an acid accent, readable typography, restrained motion, and optional Three.js objects.
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
- 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.
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
- **Status:** initial `0.1.0` release. React 18.3 is supported; React 19 and React Server Component execution are 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.
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 react@18 react-dom@18
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 "@abuzareal/contour-ui/reset.css"; // optional, global reset; omit with an existing reset
17
- import "@abuzareal/contour-ui/fonts.css"; // optional, self-hosted font assets bundled by your app
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
- Button,
21
- TextField,
22
- ToastProvider,
23
- ThemeToggle,
24
- } from "@abuzareal/contour-ui";
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
- <ToastProvider>
29
- <main data-motion="off">
30
- <ThemeToggle animate={false} />
31
- <TextField label="City" placeholder="Pune" />
32
- <Button variant="accent">Show forecast</Button>
33
- </main>
34
- </ToastProvider>
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
- 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.
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
- ## Theme and motion setup
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
- 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.
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
- To avoid a first-paint theme flash, inject the exported static `themeBootstrapScript` from `@abuzareal/contour-ui/theme-bootstrap` into the HTML head at build time. It has no user-supplied interpolation. A strict Content Security Policy needs the appropriate script hash or nonce; do not enable unsafe inline scripts generally.
93
+ ## Theme and motion
46
94
 
47
- Set `data-motion="off"` on your application wrapper when animation is disabled. `useMotionPreference()` tracks the system setting; combine it with your app's pause state. Pass the resulting `motionEnabled` to Three.js scenes and `animate` to theme controls. Reduced motion overrides theme reveal animations even if `animate` is true. No animation is required to operate the UI.
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
- `useToast()` must run below `ToastProvider`. The library does not send messages, collect analytics, fetch application data, or require a service account. Links reject executable URL schemes; application authentication, validation, authorization, and data handling remain the application's responsibility.
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
- ## Optional entries
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 # only for the /three entry
55
- npm install lenis@^1.3.26 # only for the /scroll entry
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
- // Provide a sized container and an application-owned static fallback.
67
- <Suspense fallback={<p>Loading preview…</p>}>
68
- <Sculpture motionEnabled={false} />
69
- </Suspense>;
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
- `/three` also exports `ObjectStudy`, `useThreeStage`, geometry/material helpers, and their types. Treat the 3D API as experimental in this initial release. Scene setup functions are captured on mount; remount the scene when its setup configuration changes. Scenes require WebGL and should always have a visible fallback. No Three.js or Lenis imports occur in the core runtime graph.
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
- 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.
158
+ ## Release notes and upgrades
87
159
 
88
- 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.
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.0 --save-exact
163
+ npm install @abuzareal/contour-ui@0.1.2 --save-exact
96
164
  ```
97
165
 
98
- 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.
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
- ## Development and release
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
- See `RELEASING.md` for the release checklist, authentication, and trusted-publishing setup. The package ships only built code/styles and release documents, with no consumer installation scripts. The complete-source Git repository contains tests and tooling; those are excluded from the npm archive.
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. Font dependencies retain their own font licenses; Lucide, React, Three.js, and Lenis retain their respective licenses.
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 a clean React consumer without optional Three.js or Lenis, and in the catalogue and portfolio with their full browser suites.
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. If a consumer stays private, run its checks locally before merging dependency updates. Enabling private-repository Actions later requires reviewing that account's included usage and spending settings first.
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
- Consumers install with `npm install @abuzareal/contour-ui@VERSION --save-exact`, commit their lockfile, run their checks, and redeploy. Keep the three Git repositories independent.
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) || duration <= 0) {
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 ? { inert: "" } : {}), children: front }), _jsx("div", { className: "flip-face flip-back", "aria-hidden": !flipped, ...(!flipped ? { inert: "" } : {}), 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] })] }));
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 = Omit<SelectHTMLAttributes<HTMLSelectElement>, "id"> & {
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 id = useId();
9
- 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": fieldDescribedBy(id, hint, error), 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" })] }) }));
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 = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "id"> & {
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 id = useId();
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 ? `${length}/${maxLength}` : 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": fieldDescribedBy(id, hint, error), onChange: (event) => {
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 = Omit<InputHTMLAttributes<HTMLInputElement>, "id"> & {
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 id = useId();
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 ? `${length}/${maxLength}` : 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": fieldDescribedBy(id, hint, error), onChange: (event) => {
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 {};
@@ -67,6 +67,7 @@
67
67
  }
68
68
 
69
69
  .dialog {
70
+ overscroll-behavior: contain;
70
71
  width: min(560px, calc(100vw - 32px));
71
72
  max-width: none;
72
73
  max-height: calc(100dvh - 48px);
@@ -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.0",
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://github.com/abuzareal/contour-ui#readme",
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
  }