@abuzareal/contour-ui 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CODE_OF_CONDUCT.md +5 -0
  3. package/CONTRIBUTING.md +15 -0
  4. package/NEXTJS.md +51 -0
  5. package/README.md +58 -6
  6. package/RELEASING.md +14 -40
  7. package/SECURITY.md +1 -1
  8. package/dist/components/hero/SceneBoundary.js +1 -0
  9. package/dist/components/hero/Sculpture.js +6 -4
  10. package/dist/components/three/ObjectStudy.js +7 -3
  11. package/dist/components/ui/Alert.js +1 -0
  12. package/dist/components/ui/BrandMark.js +1 -0
  13. package/dist/components/ui/Checkbox.js +1 -0
  14. package/dist/components/ui/CountUp.js +1 -0
  15. package/dist/components/ui/DataTable.js +1 -0
  16. package/dist/components/ui/Dialog.js +1 -0
  17. package/dist/components/ui/DropdownMenu.d.ts +7 -1
  18. package/dist/components/ui/DropdownMenu.js +62 -14
  19. package/dist/components/ui/FlipCard.js +1 -0
  20. package/dist/components/ui/Magnetic.js +1 -0
  21. package/dist/components/ui/Pagination.js +1 -0
  22. package/dist/components/ui/Popover.d.ts +4 -1
  23. package/dist/components/ui/Popover.js +7 -5
  24. package/dist/components/ui/RadioGroup.js +1 -0
  25. package/dist/components/ui/Reveal.js +1 -0
  26. package/dist/components/ui/ScrollProgress.js +1 -0
  27. package/dist/components/ui/SegmentedControl.js +1 -0
  28. package/dist/components/ui/SelectField.d.ts +11 -1
  29. package/dist/components/ui/SelectField.js +6 -4
  30. package/dist/components/ui/Slider.js +1 -0
  31. package/dist/components/ui/Switch.js +1 -0
  32. package/dist/components/ui/Tabs.d.ts +4 -1
  33. package/dist/components/ui/Tabs.js +12 -5
  34. package/dist/components/ui/Tag.js +1 -0
  35. package/dist/components/ui/TextArea.d.ts +8 -1
  36. package/dist/components/ui/TextArea.js +21 -7
  37. package/dist/components/ui/TextField.d.ts +10 -1
  38. package/dist/components/ui/TextField.js +17 -6
  39. package/dist/components/ui/ThemeToggle.js +1 -0
  40. package/dist/components/ui/TiltCard.js +1 -0
  41. package/dist/components/ui/ToastProvider.js +1 -0
  42. package/dist/components/ui/Tooltip.js +1 -0
  43. package/dist/fonts/dm-mono-LICENSE.txt +93 -0
  44. package/dist/fonts/dm-mono-latin-400-normal.woff2 +0 -0
  45. package/dist/fonts/dm-sans-LICENSE.txt +93 -0
  46. package/dist/fonts/dm-sans-latin-wght-normal.woff2 +0 -0
  47. package/dist/fonts/space-grotesk-LICENSE.txt +93 -0
  48. package/dist/fonts/space-grotesk-latin-wght-normal.woff2 +0 -0
  49. package/dist/fonts-latin.css +3 -3
  50. package/dist/hooks/useClipboardCopy.js +1 -0
  51. package/dist/hooks/useControlRef.d.ts +6 -0
  52. package/dist/hooks/useControlRef.js +13 -0
  53. package/dist/hooks/useDismiss.js +1 -0
  54. package/dist/hooks/useFormReset.d.ts +3 -0
  55. package/dist/hooks/useFormReset.js +31 -0
  56. package/dist/hooks/useHeaderScroll.js +1 -0
  57. package/dist/hooks/useIdleTrigger.js +1 -0
  58. package/dist/hooks/useInitialHashScroll.js +1 -0
  59. package/dist/hooks/useMobileMenu.js +1 -0
  60. package/dist/hooks/useMotionPreference.js +1 -0
  61. package/dist/hooks/useOpenState.d.ts +2 -0
  62. package/dist/hooks/useOpenState.js +15 -0
  63. package/dist/hooks/usePrintPreparation.js +1 -0
  64. package/dist/hooks/useScrollSpy.js +1 -0
  65. package/dist/hooks/useSmoothScroll.js +1 -0
  66. package/dist/hooks/useTheme.js +1 -0
  67. package/dist/hooks/useThreeStage.d.ts +4 -0
  68. package/dist/hooks/useThreeStage.js +165 -128
  69. package/dist/hooks/useToast.js +1 -0
  70. package/package.json +17 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
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
4
 
5
+ ## Unreleased
6
+
7
+ No changes recorded.
8
+
9
+ ## 0.2.0
10
+
11
+ - TextField, TextArea and SelectField expose native refs while retaining React 18/19 compatibility. Caller-supplied aria-invalid is preserved when no component error overrides it.
12
+ - Uncontrolled field counters and textarea height follow native form reset, including external form association and canceled resets. A browser task waits for the native reset default action, and native input keeps counters current when an edit repeats the pre-reset value.
13
+ - Tabs adds activeTabId/onTabChange; Popover and DropdownMenu add open/defaultOpen/onOpenChange. Existing uncontrolled defaults remain.
14
+ - DropdownMenu skips disabled items, opens from ArrowUp/ArrowDown, supports label typeahead and accepts stable item ids.
15
+ - Three stages release resources after partial initialization failure; setup can register disposal immediately. ObjectStudy reflows paused scenes on resize instead of arranging every animation frame.
16
+ - Interactive modules retain client boundaries; ESM export targets also provide a default condition for framework dynamic-module resolution. A pinned packed Next.js App Router fixture checks the candidate. Latin font assets use relative packaged URLs with their licenses for webpack compatibility.
17
+ - Contributor setup and issue/PR templates are portable. Release checks reuse one archive across compatibility consumers and publish that verified archive without rerunning source lifecycle gates.
18
+
19
+ These changes are absent from 0.1.2. A separate Next.js fixture mode covers lazy Three.js; Lenis, alternate-bundler and wider framework support remain outside these checks.
20
+
5
21
  ## 0.1.2 — 2026-10-07
6
22
 
7
23
  An optional font-loading entry and clearer setup documentation. Component APIs and runtime dependencies are unchanged from 0.1.1.
@@ -0,0 +1,5 @@
1
+ # Code of conduct
2
+
3
+ Treat contributors and users with respect. Focus criticism on behavior and code. Harassment, discrimination, threats, sharing private information without permission and repeated personal attacks are unacceptable.
4
+
5
+ Maintainers may remove disruptive material or restrict participation to keep the project usable. Send conduct concerns privately to abuzart1999@gmail.com, including links or evidence you are comfortable sharing. Reports are handled privately where possible; do not publish other people's personal details in issues.
@@ -0,0 +1,15 @@
1
+ # Contributing to Contour UI
2
+
3
+ Contour is a styled React library. The catalogue is a separate application at [contour.abuzr.in](https://contour.abuzr.in/). Domain workflows and product scenes belong in applications; reusable controls, semantic tokens and shared browser lifecycles belong here.
4
+
5
+ Start with an issue describing the user task, current behavior and a small reproduction. For a new component, describe its stable props, native semantics, keyboard behavior, failure/empty states and why existing exports cannot compose the result. Do not include secrets or private application data. Report security concerns through [SECURITY.md](SECURITY.md).
6
+
7
+ Use Node 22.12+ or 24+, then `npm ci --ignore-scripts`. Install Chromium once with `npx playwright install chromium`. Run `npm run validate` for types, lint, build, component and browser regressions, and archive checks. Run `npm run test:compat` for packed React 18/19 consumers and source declarations; run `npm run test:next` for the pinned App Router fixture. Compatibility installs need network access. The core fixture must work without Three.js or Lenis. Optional entries have separate consumers.
8
+
9
+ Keep strict types and validate untrusted data before use. Components own presentation and nearby state; hooks own subscriptions and browser lifecycles; helpers own pure calculations. Use native controls, linked labels/errors, visible focus and explicit button types. Respect reduced motion. Release every listener, timer, observer and GPU resource on unmount and partial initialization failure. Use `onDispose` immediately for resources allocated in a custom stage setup.
10
+
11
+ For a fix, add a regression that fails for the previous behavior. Use component tests for interaction contracts and real browsers for native dialogs, event timing, layout and GPU integration. Check narrow layouts, long content and both themes. Do not replace browser evidence with jsdom when a browser owns the behavior. Avoid duplicate tests that only restate implementation.
12
+
13
+ Update the README and Unreleased changelog when public behavior changes. Keep documented APIs consistent with the archive. New APIs need a migration/usage example. Generic CSS class names and the legacy `aa-theme` storage key are existing compatibility constraints; do not rename them silently.
14
+
15
+ A pull request should name the concrete behavior change, why it helps, which checks passed and any remaining limitation. Review public exports, packed files and dependency changes. No publish, deploy or merge is implied by opening a pull request. Consumers adopt an exact published release with a lockfile update; local source edits do not update them.
package/NEXTJS.md ADDED
@@ -0,0 +1,51 @@
1
+ # Next.js App Router integration
2
+
3
+ This guide describes **Contour 0.2.0**. npm 0.1.2 does not include the client directives, native refs or relative Latin font files described here. Check npm for release availability before installing. The fixture pins Next 16.4.0 with React/React DOM 19.3.0, uses the webpack production build, and consumes an npm archive. It does not establish every Next version, Turbopack, Pages Router, scroll entries or hosting platform. A separate optional mode checks a lazy Three.js scene with SSR disabled and disposal on navigation.
4
+
5
+ In `app/layout.tsx`, load CSS once:
6
+
7
+ ```tsx
8
+ import "@abuzareal/contour-ui/styles.css";
9
+ import "@abuzareal/contour-ui/fonts-latin.css"; // Optional; use your own fonts if preferred.
10
+ import { themeBootstrapScript } from "@abuzareal/contour-ui/theme-bootstrap";
11
+
12
+ export default function Layout({ children }: { children: React.ReactNode }) {
13
+ return (
14
+ <html lang="en" suppressHydrationWarning>
15
+ <head>
16
+ <script dangerouslySetInnerHTML={{ __html: themeBootstrapScript }} />
17
+ </head>
18
+ <body>{children}</body>
19
+ </html>
20
+ );
21
+ }
22
+ ```
23
+
24
+ The narrowly scoped hydration suppression handles the root theme attribute changed before hydration; do not use it to hide arbitrary mismatch errors. A restrictive CSP requires a nonce or hash for the bootstrap script. `reset.css` is optional and global; decide whether your host reset already supplies equivalent defaults, visible focus and a `[hidden]` rule.
25
+
26
+ A client component owns event handlers, state and providers:
27
+
28
+ ```tsx
29
+ "use client";
30
+ import { useState } from "react";
31
+ import { TextField, Button } from "@abuzareal/contour-ui";
32
+
33
+ export default function SearchForm() {
34
+ const [query, setQuery] = useState("");
35
+ return (
36
+ <form>
37
+ <TextField
38
+ label="Search"
39
+ name="query"
40
+ value={query}
41
+ onChange={(event) => setQuery(event.target.value)}
42
+ />
43
+ <Button type="submit">Search</Button>
44
+ </form>
45
+ );
46
+ }
47
+ ```
48
+
49
+ Provide real submission behavior in the application. Toast consumers need ToastProvider; dialogs need application-owned open/onClose. Keep function props inside client boundaries. A server component can pass serializable props to a package client component or import pure `safeHref`; a client directive does not remove normal server prerendering.
50
+
51
+ Run `npm run test:next` to rebuild and consume the candidate archive. The fixture checks server imports/prerendering, hydration, CSS/fonts, refs, native resets, dialog dismissal/focus, toast, tabs, theme persistence and client navigation without Three.js/Lenis installed. Run `CONTOUR_NEXT_OPTIONAL=1 node scripts/test-next.mjs` for the separate lazy `/three` route; ordinary consumers still require neither optional peer. Compatibility evidence applies to the fixture's exact pair and local runtime, not deployment.
package/README.md CHANGED
@@ -6,14 +6,14 @@ Use it when you want this visual style with typed controls, feedback, navigation
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
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.
9
+ This source checkout targets **0.2.0**. Package publication and application upgrades are separate; check the npm registry for release availability and each application's manifest for its installed version. Catalogue examples include application patterns as well as package components. Check the exports below before treating an example as an importable component.
10
10
 
11
11
  ## Install
12
12
 
13
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
14
 
15
15
  ```sh
16
- npm install @abuzareal/contour-ui@0.1.2 --save-exact
16
+ npm install @abuzareal/contour-ui@0.2.0 --save-exact
17
17
  ```
18
18
 
19
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.
@@ -157,15 +157,15 @@ Give scenes a sized container and a usable static fallback. `SceneBoundary` hide
157
157
 
158
158
  ## Release notes and upgrades
159
159
 
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.
160
+ **0.2.0** adds native field refs, controlled tabs and disclosure state, menu keyboard improvements, native reset synchronization, Three.js lifecycle fixes and a tested Next.js App Router integration. Existing uncontrolled defaults and React 18/19 support remain. `fonts-latin.css`, introduced in 0.1.2, now uses packaged relative font URLs for webpack resolution. `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.
161
161
 
162
162
  ```sh
163
- npm install @abuzareal/contour-ui@0.1.2 --save-exact
163
+ npm install @abuzareal/contour-ui@0.2.0 --save-exact
164
164
  ```
165
165
 
166
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
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.
168
+ The package and [source repository](https://github.com/abuzareal/contour-ui) are public under MIT. Report reproducible defects in the [issue tracker](https://github.com/abuzareal/contour-ui/issues); use [SECURITY.md](SECURITY.md) for private vulnerability reporting. Public users can inspect code, CSS, declarations and release documents through GitHub or npm.
169
169
 
170
170
  ## Develop this library
171
171
 
@@ -180,8 +180,60 @@ npm audit
180
180
  npm pack --dry-run
181
181
  ```
182
182
 
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.
183
+ `validate` checks types, lint, builds, Node/component tests, dialog and Three.js 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
184
 
185
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.
186
186
 
187
187
  MIT licensed. Fontsource fonts and dependencies retain their own licenses.
188
+
189
+ ## APIs added in 0.2.0
190
+
191
+ Version 0.2.0 adds native refs to TextField, TextArea and SelectField; synchronizes uncontrolled counters and auto-resize after native form reset; adds controlled Tabs (`activeTabId`, `onTabChange`) and disclosures (`open`, `defaultOpen`, `onOpenChange`); and adds disabled menu items, stable item ids and typeahead. These additions are absent from 0.1.2. Switching a field/disclosure between controlled and uncontrolled ownership during its lifetime is unsupported.
192
+
193
+ Existing uncontrolled calls remain valid. Use a native ref to focus a field or integrate a form controller; controlled tabs and disclosures require the caller to update the selected id or open state:
194
+
195
+ ```tsx
196
+ import { useRef, useState } from "react";
197
+ import { Button, Popover, Tabs, TextField } from "@abuzareal/contour-ui";
198
+
199
+ export function AccountControls() {
200
+ const input = useRef<HTMLInputElement>(null);
201
+ const [activeTabId, setActiveTabId] = useState("details");
202
+ const [open, setOpen] = useState(false);
203
+
204
+ return (
205
+ <>
206
+ <TextField ref={input} label="Name" name="name" />
207
+ <Button onClick={() => input.current?.focus()}>Focus name</Button>
208
+ <Tabs
209
+ label="Account"
210
+ activeTabId={activeTabId}
211
+ onTabChange={setActiveTabId}
212
+ tabs={[
213
+ { id: "details", label: "Details", content: <p>Account details</p> },
214
+ {
215
+ id: "settings",
216
+ label: "Settings",
217
+ content: <p>Account settings</p>,
218
+ },
219
+ ]}
220
+ />
221
+ <Popover trigger="Help" title="Help" open={open} onOpenChange={setOpen}>
222
+ <p>Account help</p>
223
+ </Popover>
224
+ </>
225
+ );
226
+ }
227
+ ```
228
+
229
+ Interactive module boundaries retain `use client` in the built package. The pinned Next.js App Router consumer checks the packed 0.2.0 archive. Follow [Next.js setup](NEXTJS.md) for the pinned Next.js 16.4.0 / React 19.3.0 pair. Pure URL/theme-bootstrap helpers remain usable from the server. A separate fixture mode checks lazy optional Three.js loading and navigation cleanup. Lenis integration, other Next versions and alternate bundlers remain separate compatibility questions.
230
+
231
+ Custom `useThreeStage` setup can register disposal through `context.onDispose?.(cleanup)` immediately after acquiring each resource, including resources allocated before setup returns. Registered resources are released on failed initialization and unmount; avoid also disposing the same resource in `StageScene.dispose`. A stage may implement `resize()` to reflow after aspect changes while motion is paused.
232
+
233
+ ## Compatibility boundaries
234
+
235
+ Styles are global, not a scoped CSS engine. Existing class names such as `.field` and `.menu` can collide with host CSS. Import Contour styles before deliberate application overrides; keep application selectors area-prefixed. The optional reset and fonts affect the document. Tokens use root `data-theme`; `useTheme` retains the legacy `aa-theme` local-storage key for compatibility. A multi-library page must decide which system owns its root theme.
236
+
237
+ Menus and popovers remain local panels without portals or collision detection; use them where the container can accommodate their content. No submenu or arbitrary trigger-composition contract is claimed. BrandMark and page/editorial helpers preserve existing identity and layout assumptions; they are optional exports rather than universal primitives. Do not copy private internals or deep-import them.
238
+
239
+ For contribution setup and review expectations, see [CONTRIBUTING.md](CONTRIBUTING.md). Contour currently provides React components; no Vue, Svelte or Angular adapter is included.
package/RELEASING.md CHANGED
@@ -1,49 +1,23 @@
1
1
  # Releasing Contour UI
2
2
 
3
- ## Package-page documentation and release notes
3
+ Source is public at [github.com/abuzareal/contour-ui](https://github.com/abuzareal/contour-ui); the package is public under MIT. Query npm for the latest published version; the source manifest does not prove publication. Source changes are not delivered to consumers until publication and an exact dependency/lockfile update. The npm README changes only when a new version is published; do not overwrite or unpublish an existing version to edit it.
4
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.
5
+ Before selecting a release version, review public API and visual changes, types, dependencies, licenses, changelog and compatibility limits. Use patch for compatible fixes, minor for additions, and document every incompatible 0.x change explicitly. README examples must match the archive. Record publication dates only after verifying the registry. A local check is not deployment evidence.
6
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.
7
+ ## Verify and publish one archive
8
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.
9
+ 1. Obtain maintainer authorization to publish; validation alone is not authorization.
10
+ 2. Select an unused registry version and update package.json, its lockfile and release notes. Never reuse an existing registry version; a locally verified archive does not make it publishable under an already released version. Use a reviewed release commit on main and a matching `vVERSION` tag. The hosted workflow checks the tag version and that its source is an ancestor of origin/main. It cannot publish from a pull request.
11
+ 3. Run `npm ci --ignore-scripts`; install Chromium once with `npx playwright install chromium`. Use Node 22.12+ or 24+.
12
+ 4. Set `CONTOUR_RELEASE_DIR` to an existing temporary directory and run `npm run verify:release`. It runs validate once, packs one archive, tests that archive in clean React 18/19 core and optional consumers plus the pinned Next App Router fixture, audits dependencies, and writes `verified-archive.json` with its SHA-256. Core consumers must have no Three.js or Lenis installed. Review failures and advisories instead of bypassing gates.
13
+ 5. Inspect the archive contents, verify its checksum against `verified-archive.json`, and publish that exact path using `npm publish /absolute/path/to/archive.tgz --ignore-scripts --access public`. `--ignore-scripts` is appropriate here because that exact archive has already passed the release gates; it is not permission to publish an unchecked package. Source `npm publish` retains a prepublishOnly guard. The hosted workflow verifies the checksum again and publishes the archive with provenance, avoiding duplicate source gates.
14
+ 6. The owner completes npm authentication/2FA personally. Never put tokens, OTPs or credentials in this repository or logs. Prefer configured trusted publishing and short-lived identity over persistent tokens.
15
+ 7. Verify registry version, tarball/integrity and publication date; install that exact registry version in each affected consumer, commit its manifest/lockfile, and run its required checks before deployment. Do not substitute a local link or source copy for release validation.
10
16
 
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.
17
+ The tag workflow is configured for a public repository on main. Configuration does not prove that its npm trusted-publisher relationship is active; check npm/GitHub settings before relying on it. Publication requires authorization even when CI passes.
12
18
 
13
- ## Initial release
19
+ ## Validation cost
14
20
 
15
- 1. Verify `npm whoami` is `abuzareal`; the package is `@abuzareal/contour-ui` and public under MIT.
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.
18
- 4. Check `npm pack --dry-run --json` and licenses. No private data, credentials, unrelated assets, maps, or app content may enter the archive.
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.
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.
21
- 7. Verify registry metadata, install the exact registry version in both consumers, rerun checks, and commit the manifest/lockfile changes. Do not commit temporary filesystem/archive dependencies.
22
- 8. Tag the matching package version and record the release. Every future release requires a new version; npm versions cannot be overwritten.
21
+ Browser checks remain for native dialog, reset timing and graphics lifecycle behavior. jsdom does not reproduce all browser default actions. Compatibility checks reuse one archive and avoid another library build when `CONTOUR_ARCHIVE` is provided. Documentation-only diffs retain types, lint, build, archive checks and audit; runtime diffs keep component, browser and complete compatibility checks. Changes to components, hooks, entries, styles, build/package configuration or the Next fixture also run the pinned Next check. Unknown history falls back to complete gates. No measured CI speedup is claimed until the updated workflow has run.
23
22
 
24
- ## Future trusted releases
25
-
26
- After the first package exists, configure npm's GitHub trusted publisher for owner `abuzareal`, repository `contour-ui`, workflow `release.yml`. Authenticate interactively to set this relationship; do not paste credentials into code or chat. Enable two-factor authentication on the owner account and prefer disabling ordinary token publishing after trusted publishing has been successfully verified.
27
-
28
- With account 2FA enabled, configure and verify the relationship using npm 11.15 or later:
29
-
30
- ```sh
31
- npm trust github @abuzareal/contour-ui --file release.yml --repo abuzareal/contour-ui --allow-publish
32
- npm trust list @abuzareal/contour-ui
33
- ```
34
-
35
- The release workflow accepts a manually dispatched version already present in package.json and requires the matching `vVERSION` tag to identify its source. It reruns validation, dependency audit, and clean-consumer tests before publishing with provenance. Push the reviewed release commit to main before tagging. Never publish from a pull-request event or add a persistent npm token to the workflow.
36
-
37
- The repository is currently private, so the configured workflow deliberately skips publishing. Release locally after running the checklist. Trusted publishing can be configured for a private repository, but npm does not support provenance attestations from private source repositories. If hosted releases are enabled later, review private-runner spending settings and remove the explicit provenance requirement for private source. The current workflow is ready for a future public repository and must not be presented as active automation while the repository stays private.
38
-
39
- Increment PATCH for compatible fixes, MINOR for compatible additions, and MAJOR for incompatible APIs or substantial incompatible default behavior. While the package is at 0.x, document every change carefully and keep consumers pinned to exact versions. Review visual changes in consuming apps even when the API is compatible.
40
-
41
- ## Dependency updates and cost
42
-
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.
44
-
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.
48
-
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.
23
+ Dependabot proposes dependency/action changes for review; changes are not auto-merged. Actions use pinned revisions and minimal permissions. Do not enable paid runners or private distribution as part of a release. Contributor and application checks remain separate from registry/deployment verification.
package/SECURITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Security
2
2
 
3
- Report suspected vulnerabilities privately using GitHub's private vulnerability reporting for this repository once enabled. If that option is unavailable, contact the maintainer privately through their GitHub profile; do not post exploit details or credentials in a public issue.
3
+ Report suspected vulnerabilities privately to abuzart1999@gmail.com. Include the affected version and a minimal reproduction without credentials or unrelated personal data. If GitHub private vulnerability reporting is available on this repository, it is another private channel. Do not post exploit details or credentials in a public issue.
4
4
 
5
5
  The latest published release is the supported version. Dependency audits identify known reported vulnerabilities, not all possible defects. No claim of complete security or formal accessibility certification is made.
6
6
 
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  /** Error boundary that hides the 3D scene if its lazy module or renderer throws. */
2
3
  import { Component } from "react";
3
4
  export default class SceneBoundary extends Component {
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  /** Hero sculpture: three chrome/lime links that follow the pointer and drift while motion is on. */
3
4
  import { useRef } from "react";
@@ -5,10 +6,13 @@ import * as THREE from "three";
5
6
  import useThreeStage from "../../hooks/useThreeStage.js";
6
7
  import { createChrome, createLime, createLinkGeometry, } from "../../lib/threeMaterials.js";
7
8
  const restRotation = new THREE.Euler(-0.15, -0.1, -0.1);
8
- function setupSculpture({ scene }) {
9
+ function setupSculpture({ scene, onDispose }) {
9
10
  const chrome = createChrome();
11
+ onDispose?.(() => chrome.dispose());
10
12
  const lime = createLime();
13
+ onDispose?.(() => lime.dispose());
11
14
  const geometry = createLinkGeometry();
15
+ onDispose?.(() => geometry.dispose());
12
16
  // Three interlocking links arranged as one assembly.
13
17
  const assembly = new THREE.Group();
14
18
  const firstLink = new THREE.Mesh(geometry, chrome);
@@ -36,9 +40,7 @@ function setupSculpture({ scene }) {
36
40
  assembly.position.y = 0;
37
41
  },
38
42
  dispose: () => {
39
- geometry.dispose();
40
- chrome.dispose();
41
- lime.dispose();
43
+ scene.remove(assembly);
42
44
  },
43
45
  };
44
46
  }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  /** Lays out a set of 3D objects in a responsive grid; each turns slowly and the set leans toward the pointer. */
3
4
  import { useRef } from "react";
@@ -22,8 +23,9 @@ export default function ObjectStudy({ label, motionEnabled, study, }) {
22
23
  useThreeStage(hostRef, {
23
24
  motionEnabled,
24
25
  cameraDistance,
25
- setup: ({ scene, camera }) => {
26
+ setup: ({ scene, camera, onDispose }) => {
26
27
  const set = objectStudies[study]();
28
+ onDispose?.(set.dispose);
27
29
  const group = new THREE.Group();
28
30
  set.objects.forEach((object) => {
29
31
  object.rotation.copy(restTilt);
@@ -42,7 +44,6 @@ export default function ObjectStudy({ label, motionEnabled, study, }) {
42
44
  arrange();
43
45
  return {
44
46
  update: ({ delta, elapsed, pointer }) => {
45
- arrange();
46
47
  set.objects.forEach((object, index) => {
47
48
  object.rotation.y += delta * 0.45;
48
49
  object.rotation.x =
@@ -57,7 +58,10 @@ export default function ObjectStudy({ label, motionEnabled, study, }) {
57
58
  group.rotation.set(0, 0, 0);
58
59
  set.objects.forEach((object) => object.rotation.copy(restTilt));
59
60
  },
60
- dispose: set.dispose,
61
+ resize: arrange,
62
+ dispose: () => {
63
+ scene.remove(group);
64
+ },
61
65
  };
62
66
  },
63
67
  });
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { Asterisk, CircleCheck, CircleX, Info, TriangleAlert, X, } from "lucide-react";
3
4
  import { cx } from "../../lib/classNames.js";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { safeHref } from "../../lib/safeHref.js";
3
4
  export default function BrandMark({ label, href = "#hero", onClick, }) {
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  /** Square checkbox with an acid check, optional description, error, and indeterminate state. */
3
4
  import { useEffect, useId, useRef } from "react";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  /** Number that counts up the first time it scrolls into view; screen readers get the final value. */
3
4
  import { useEffect, useRef, useState } from "react";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  /** Editorial data table with mono headers, optional column sorting, and horizontal scroll on small screens. */
3
4
  import { useMemo, useState } from "react";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  /** Modal dialog on the native <dialog> element; also slides in from either edge as a drawer. */
3
4
  import { useEffect, useId, useRef } from "react";
@@ -5,10 +5,16 @@ export type MenuItem = {
5
5
  onSelect: () => void;
6
6
  icon?: ReactNode;
7
7
  danger?: boolean;
8
+ disabled?: boolean;
9
+ /** Stable identity when labels are duplicated or translated. */
10
+ id?: string;
8
11
  };
9
12
  export type DropdownMenuProps = {
10
13
  label: string;
11
14
  items: MenuItem[];
12
15
  align?: "start" | "end";
16
+ open?: boolean;
17
+ defaultOpen?: boolean;
18
+ onOpenChange?: (open: boolean) => void;
13
19
  };
14
- export default function DropdownMenu({ label, items, align, }: DropdownMenuProps): import("react").JSX.Element;
20
+ export default function DropdownMenu({ label, items, align, open: controlledOpen, defaultOpen, onOpenChange, }: DropdownMenuProps): import("react").JSX.Element;
@@ -1,38 +1,86 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  /** Button-triggered action menu with roving focus, Escape/outside dismissal, and focus return. */
3
- import { useCallback, useEffect, useId, useRef, useState, } from "react";
4
+ import { useCallback, useEffect, useId, useRef } from "react";
4
5
  import { ChevronDown } from "lucide-react";
6
+ import useOpenState from "../../hooks/useOpenState.js";
5
7
  import useDismiss from "../../hooks/useDismiss.js";
6
8
  import { cx } from "../../lib/classNames.js";
7
9
  import { nextIndexForKey } from "../../lib/keyboard.js";
8
- export default function DropdownMenu({ label, items, align = "start", }) {
10
+ export default function DropdownMenu({ label, items, align = "start", open: controlledOpen, defaultOpen = false, onOpenChange, }) {
9
11
  const menuId = useId();
10
- const [open, setOpen] = useState(false);
12
+ const [open, setOpen] = useOpenState(controlledOpen, defaultOpen, onOpenChange);
11
13
  const container = useRef(null);
12
14
  const trigger = useRef(null);
15
+ const initialFocus = useRef("first");
16
+ const search = useRef({ value: "", time: 0 });
13
17
  const itemRefs = useRef([]);
14
18
  const close = useCallback((returnFocus = true) => {
15
19
  setOpen(false);
16
20
  if (returnFocus)
17
21
  trigger.current?.focus();
18
- }, []);
22
+ }, [setOpen]);
19
23
  useDismiss(container, open, useCallback((reason) => close(reason === "escape"), [close]));
20
24
  useEffect(() => {
21
- if (open)
22
- itemRefs.current[0]?.focus();
25
+ if (!open)
26
+ return;
27
+ search.current = { value: "", time: 0 };
28
+ const enabled = itemRefs.current.filter((item) => item && !item.disabled);
29
+ (initialFocus.current === "last" ? enabled.at(-1) : enabled[0])?.focus();
23
30
  }, [open]);
24
- return (_jsxs("div", { ref: container, className: cx("menu", `menu-${align}`), children: [_jsxs("button", { ref: trigger, type: "button", className: "menu-trigger", "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": menuId, onClick: () => setOpen((value) => !value), children: [label, _jsx(ChevronDown, { size: 15, "aria-hidden": "true", className: "menu-chevron" })] }), _jsx("div", { id: menuId, role: "menu", "aria-label": label, className: "menu-list", hidden: !open, children: items.map((item, index) => (_jsxs("button", { ref: (element) => {
31
+ return (_jsxs("div", { ref: container, className: cx("menu", `menu-${align}`), children: [_jsxs("button", { ref: trigger, type: "button", className: "menu-trigger", "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": menuId, onClick: () => {
32
+ initialFocus.current = "first";
33
+ setOpen(!open);
34
+ }, onKeyDown: (event) => {
35
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp")
36
+ return;
37
+ event.preventDefault();
38
+ initialFocus.current = event.key === "ArrowUp" ? "last" : "first";
39
+ if (open) {
40
+ const enabled = itemRefs.current.filter((item) => item && !item.disabled);
41
+ (initialFocus.current === "last"
42
+ ? enabled.at(-1)
43
+ : enabled[0])?.focus();
44
+ }
45
+ else
46
+ setOpen(true);
47
+ }, children: [label, _jsx(ChevronDown, { size: 15, "aria-hidden": "true", className: "menu-chevron" })] }), _jsx("div", { id: menuId, role: "menu", "aria-label": label, className: "menu-list", hidden: !open, children: items.map((item, index) => (_jsxs("button", { ref: (element) => {
25
48
  itemRefs.current[index] = element;
26
- }, type: "button", role: "menuitem", tabIndex: -1, className: cx("menu-item", item.danger && "menu-item-danger"), onClick: () => {
49
+ }, type: "button", role: "menuitem", tabIndex: -1, disabled: item.disabled, className: cx("menu-item", item.danger && "menu-item-danger"), onClick: () => {
27
50
  item.onSelect();
28
51
  close();
29
52
  }, onKeyDown: (event) => {
30
53
  if (event.key === "Tab")
31
54
  return close(false);
32
- const next = nextIndexForKey(event.key, index, items.length, "vertical");
33
- if (next === null)
34
- return;
35
- event.preventDefault();
36
- itemRefs.current[next]?.focus();
37
- }, children: [item.icon, item.label] }, item.label))) })] }));
55
+ const enabled = items
56
+ .map((entry, position) => (entry.disabled ? -1 : position))
57
+ .filter((position) => position !== -1);
58
+ const next = nextIndexForKey(event.key, enabled.indexOf(index), enabled.length, "vertical");
59
+ if (next !== null) {
60
+ event.preventDefault();
61
+ itemRefs.current[enabled[next]]?.focus();
62
+ }
63
+ else if (event.key.length === 1 &&
64
+ !event.ctrlKey &&
65
+ !event.altKey &&
66
+ !event.metaKey) {
67
+ const now = performance.now();
68
+ const previous = now - search.current.time < 500 ? search.current.value : "";
69
+ const value = previous &&
70
+ [...previous].every((letter) => letter === event.key.toLowerCase())
71
+ ? event.key.toLowerCase()
72
+ : previous + event.key.toLowerCase();
73
+ search.current = { value, time: now };
74
+ const start = enabled.indexOf(index) + 1;
75
+ const ordered = [
76
+ ...enabled.slice(start),
77
+ ...enabled.slice(0, start),
78
+ ];
79
+ const match = ordered.find((position) => items[position].label.toLowerCase().startsWith(value));
80
+ if (match !== undefined) {
81
+ event.preventDefault();
82
+ itemRefs.current[match]?.focus();
83
+ }
84
+ }
85
+ }, children: [item.icon, item.label] }, item.id ?? item.label))) })] }));
38
86
  }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  /** Two-sided card that flips in 3D; a button toggles it so the back is reachable without hover. */
3
4
  import { useState, version } from "react";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  /** Pulls its child toward a fine pointer while hovered, then springs back (an awwwards staple). */
3
4
  import { useRef } from "react";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  /** Page navigation with previous/next controls and a condensed list of two-digit page numbers. */
3
4
  import { ArrowLeft, ArrowRight } from "lucide-react";
@@ -6,5 +6,8 @@ export type PopoverProps = {
6
6
  title: string;
7
7
  children: ReactNode;
8
8
  align?: "start" | "end";
9
+ open?: boolean;
10
+ defaultOpen?: boolean;
11
+ onOpenChange?: (open: boolean) => void;
9
12
  };
10
- export default function Popover({ trigger, title, children, align, }: PopoverProps): import("react").JSX.Element;
13
+ export default function Popover({ trigger, title, children, align, open: controlledOpen, defaultOpen, onOpenChange, }: PopoverProps): import("react").JSX.Element;
@@ -1,18 +1,20 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  /** Non-modal floating panel anchored to a trigger; closes on Escape or an outside press. */
3
- import { useCallback, useId, useRef, useState } from "react";
4
+ import { useCallback, useId, useRef } from "react";
5
+ import useOpenState from "../../hooks/useOpenState.js";
4
6
  import useDismiss from "../../hooks/useDismiss.js";
5
7
  import { cx } from "../../lib/classNames.js";
6
- export default function Popover({ trigger, title, children, align = "start", }) {
8
+ export default function Popover({ trigger, title, children, align = "start", open: controlledOpen, defaultOpen = false, onOpenChange, }) {
7
9
  const panelId = useId();
8
10
  const titleId = useId();
9
- const [open, setOpen] = useState(false);
11
+ const [open, setOpen] = useOpenState(controlledOpen, defaultOpen, onOpenChange);
10
12
  const container = useRef(null);
11
13
  const button = useRef(null);
12
14
  useDismiss(container, open, useCallback((reason) => {
13
15
  setOpen(false);
14
16
  if (reason === "escape")
15
17
  button.current?.focus();
16
- }, []));
17
- return (_jsxs("div", { ref: container, className: cx("popover", `popover-${align}`), children: [_jsx("button", { ref: button, type: "button", className: "menu-trigger", "aria-expanded": open, "aria-controls": panelId, onClick: () => setOpen((value) => !value), children: trigger }), _jsxs("div", { id: panelId, role: "dialog", "aria-labelledby": titleId, className: "popover-panel", hidden: !open, children: [_jsx("strong", { id: titleId, className: "popover-title", children: title }), children] })] }));
18
+ }, [setOpen]));
19
+ return (_jsxs("div", { ref: container, className: cx("popover", `popover-${align}`), children: [_jsx("button", { ref: button, type: "button", className: "menu-trigger", "aria-expanded": open, "aria-controls": panelId, onClick: () => setOpen(!open), children: trigger }), _jsxs("div", { id: panelId, role: "dialog", "aria-labelledby": titleId, className: "popover-panel", hidden: !open, children: [_jsx("strong", { id: titleId, className: "popover-title", children: title }), children] })] }));
18
20
  }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  /** Fieldset of round radio options sharing one name, with a mono legend. */
3
4
  import { useId } from "react";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  /** Fades and lifts its children into view the first time they enter the viewport. */
3
4
  import { useEffect, useRef } from "react";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  /** Thin acid bar fixed to the top of the viewport that tracks page scroll progress. */
3
4
  import { useEffect, useRef } from "react";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  /** Single-choice pill group (filter buttons) with arrow-key navigation. */
3
4
  import { useRef } from "react";