@abuzareal/contour-ui 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/COMBOBOX.md +87 -0
- package/CONTRACTS.md +23 -17
- package/DOCS.md +80 -0
- package/FOUNDATION.md +35 -0
- package/GETTING-STARTED.md +63 -0
- package/NEXTJS.md +5 -1
- package/OVERLAYS.md +52 -0
- package/PUBLIC-API.md +189 -162
- package/README.md +44 -28
- package/RECIPES.md +43 -0
- package/RELEASING.md +2 -2
- package/SIZE.md +51 -0
- package/STYLING.md +68 -0
- package/dist/actions.css +7 -0
- package/dist/components/hero/Sculpture.js +2 -1
- package/dist/components/three/ObjectStudy.js +2 -1
- package/dist/components/ui/Alert.d.ts +2 -1
- package/dist/components/ui/Alert.js +3 -3
- package/dist/components/ui/Avatar.d.ts +3 -1
- package/dist/components/ui/Avatar.js +3 -3
- package/dist/components/ui/Badge.js +2 -2
- package/dist/components/ui/BrandMark.js +2 -1
- package/dist/components/ui/Breadcrumbs.d.ts +2 -1
- package/dist/components/ui/Breadcrumbs.js +3 -2
- package/dist/components/ui/Button.d.ts +2 -0
- package/dist/components/ui/Button.js +3 -2
- package/dist/components/ui/Card.js +2 -2
- package/dist/components/ui/Checkbox.js +3 -3
- package/dist/components/ui/Combobox.d.ts +325 -0
- package/dist/components/ui/Combobox.js +120 -0
- package/dist/components/ui/Combobox.types.d.ts +46 -0
- package/dist/components/ui/Combobox.types.js +1 -0
- package/dist/components/ui/CountUp.d.ts +2 -1
- package/dist/components/ui/CountUp.js +6 -3
- package/dist/components/ui/DataTable.d.ts +1 -0
- package/dist/components/ui/DataTable.js +15 -10
- package/dist/components/ui/DepthButton.js +2 -2
- package/dist/components/ui/Dialog.d.ts +5 -1
- package/dist/components/ui/Dialog.js +49 -18
- package/dist/components/ui/Disclosure.js +2 -1
- package/dist/components/ui/Divider.js +3 -2
- package/dist/components/ui/DropdownMenu.d.ts +11 -4
- package/dist/components/ui/DropdownMenu.js +109 -66
- package/dist/components/ui/EmptyState.js +2 -1
- package/dist/components/ui/Field.js +2 -2
- package/dist/components/ui/FlipCard.js +2 -2
- package/dist/components/ui/FloatingPortal.d.ts +10 -0
- package/dist/components/ui/FloatingPortal.js +13 -0
- package/dist/components/ui/FloatingScope.d.ts +9 -0
- package/dist/components/ui/FloatingScope.js +43 -0
- package/dist/components/ui/IconButton.js +2 -2
- package/dist/components/ui/Kbd.js +2 -1
- package/dist/components/ui/Magnetic.js +2 -1
- package/dist/components/ui/Marquee.js +3 -2
- package/dist/components/ui/OverlayTrigger.d.ts +11 -0
- package/dist/components/ui/OverlayTrigger.js +5 -0
- package/dist/components/ui/Pagination.d.ts +5 -1
- package/dist/components/ui/Pagination.js +3 -2
- package/dist/components/ui/Popover.d.ts +18 -8
- package/dist/components/ui/Popover.js +58 -17
- package/dist/components/ui/ProgressBar.js +2 -2
- package/dist/components/ui/RadioGroup.js +5 -5
- package/dist/components/ui/Reveal.d.ts +0 -1
- package/dist/components/ui/Reveal.js +4 -3
- package/dist/components/ui/RevealHeading.js +2 -1
- package/dist/components/ui/ScrollProgress.js +2 -1
- package/dist/components/ui/SectionLabel.js +2 -1
- package/dist/components/ui/SegmentedControl.js +6 -3
- package/dist/components/ui/SelectField.js +3 -2
- package/dist/components/ui/Skeleton.js +2 -2
- package/dist/components/ui/Slider.js +3 -3
- package/dist/components/ui/Spinner.js +2 -2
- package/dist/components/ui/Stat.js +2 -1
- package/dist/components/ui/Stepper.js +3 -3
- package/dist/components/ui/Switch.d.ts +2 -0
- package/dist/components/ui/Switch.js +6 -7
- package/dist/components/ui/Table.d.ts +20 -0
- package/dist/components/ui/Table.js +9 -0
- package/dist/components/ui/Tabs.js +7 -5
- package/dist/components/ui/Tag.d.ts +2 -1
- package/dist/components/ui/Tag.js +4 -3
- package/dist/components/ui/TextArea.js +2 -1
- package/dist/components/ui/TextField.js +3 -2
- package/dist/components/ui/ThemeScope.d.ts +13 -0
- package/dist/components/ui/ThemeScope.js +10 -0
- package/dist/components/ui/ThemeToggle.d.ts +3 -0
- package/dist/components/ui/ThemeToggle.js +4 -3
- package/dist/components/ui/TiltCard.js +3 -2
- package/dist/components/ui/Timeline.js +2 -2
- package/dist/components/ui/ToastProvider.d.ts +5 -2
- package/dist/components/ui/ToastProvider.js +5 -5
- package/dist/components/ui/Tooltip.d.ts +4 -4
- package/dist/components/ui/Tooltip.js +70 -10
- package/dist/data-display.css +5 -0
- package/dist/effects.css +6 -0
- package/dist/feedback.css +5 -0
- package/dist/fonts-latin.css +10 -4
- package/dist/fonts.css +8 -0
- package/dist/forms.css +5 -0
- package/dist/hooks/useComboboxAnchor.d.ts +17 -0
- package/dist/hooks/useComboboxAnchor.js +61 -0
- package/dist/hooks/useDismiss.d.ts +12 -3
- package/dist/hooks/useDismiss.js +5 -21
- package/dist/hooks/useFloatingPanel.d.ts +14 -0
- package/dist/hooks/useFloatingPanel.js +157 -0
- package/dist/hooks/useLayerDismiss.d.ts +4 -0
- package/dist/hooks/useLayerDismiss.js +48 -0
- package/dist/hooks/useMobileMenu.d.ts +0 -1
- package/dist/hooks/useMobileMenu.js +11 -16
- package/dist/hooks/usePanelFocus.d.ts +10 -0
- package/dist/hooks/usePanelFocus.js +60 -0
- package/dist/hooks/useTheme.d.ts +1 -0
- package/dist/hooks/useTheme.js +18 -5
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -0
- package/dist/kit/data-display.css +6 -0
- package/dist/kit/forms.css +45 -2
- package/dist/kit/navigation.css +4 -0
- package/dist/kit/overlays.css +8 -4
- package/dist/lib/classNames.d.ts +2 -0
- package/dist/lib/classNames.js +16 -1
- package/dist/lib/componentClasses.d.ts +1 -0
- package/dist/lib/componentClasses.js +2 -0
- package/dist/lib/keyboard.d.ts +1 -1
- package/dist/lib/keyboard.js +5 -2
- package/dist/lib/layers.d.ts +18 -0
- package/dist/lib/layers.js +128 -0
- package/dist/lib/themeScope.d.ts +5 -0
- package/dist/lib/themeScope.js +3 -0
- package/dist/minimal-reset.css +6 -0
- package/dist/navigation.css +5 -0
- package/dist/overlays.css +5 -0
- package/dist/scoped/base.css +183 -0
- package/dist/scoped/kit/actions.css +209 -0
- package/dist/scoped/kit/data-display.css +498 -0
- package/dist/scoped/kit/effects.css +140 -0
- package/dist/scoped/kit/feedback.css +309 -0
- package/dist/scoped/kit/forms.css +404 -0
- package/dist/scoped/kit/navigation.css +266 -0
- package/dist/scoped/kit/overlays.css +299 -0
- package/dist/scoped/marquee.css +58 -0
- package/dist/scoped/spinner.css +20 -0
- package/dist/scoped/theme-toggle.css +38 -0
- package/dist/scoped/tokens.css +119 -0
- package/dist/scoped/ui.css +127 -0
- package/dist/scoped/utilities.css +17 -0
- package/dist/scoped-tokens.css +1 -0
- package/dist/scoped.css +12 -0
- package/dist/theme-toggle.css +2 -2
- package/dist/tokens.json +710 -0
- package/examples/optional/LazyScene.tsx +78 -0
- package/examples/optional/OptionalSmoothScroll.tsx +25 -0
- package/examples/optional/SmoothScrollOwner.tsx +7 -0
- package/examples/optional/optional.css +25 -0
- package/examples/recipes/AsyncProjectPicker.tsx +82 -0
- package/examples/recipes/DeleteProjectConfirmation.tsx +97 -0
- package/examples/recipes/InputCompositions.tsx +49 -0
- package/examples/recipes/RemoteRecordsTable.tsx +256 -0
- package/examples/recipes/useRemoteResource.ts +81 -0
- package/examples/reproduction/README.md +9 -0
- package/examples/reproduction/index.html +14 -0
- package/examples/reproduction/package.json +27 -0
- package/examples/reproduction/src/main.tsx +33 -0
- package/examples/reproduction/src/style.css +19 -0
- package/examples/reproduction/tsconfig.json +18 -0
- package/examples/reproduction/vite.config.ts +19 -0
- package/examples/starters/next/README.md +18 -0
- package/examples/starters/next/app/Demo.tsx +65 -0
- package/examples/starters/next/app/layout.tsx +25 -0
- package/examples/starters/next/app/next-page/page.tsx +9 -0
- package/examples/starters/next/app/page.tsx +11 -0
- package/examples/starters/next/app/style.css +19 -0
- package/examples/starters/next/next-env.d.ts +2 -0
- package/examples/starters/next/next.config.mjs +1 -0
- package/examples/starters/next/package.json +27 -0
- package/examples/starters/next/proxy.ts +16 -0
- package/examples/starters/next/tsconfig.json +30 -0
- package/examples/starters/vite/README.md +16 -0
- package/examples/starters/vite/index.html +14 -0
- package/examples/starters/vite/package.json +27 -0
- package/examples/starters/vite/src/main.tsx +81 -0
- package/examples/starters/vite/src/style.css +19 -0
- package/examples/starters/vite/tsconfig.json +18 -0
- package/examples/starters/vite/vite.config.ts +19 -0
- package/package.json +41 -5
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ 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
|
-
This source checkout targets **0.
|
|
9
|
+
This source checkout targets **0.6.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
|
[Public API and maturity register](PUBLIC-API.md) · [Architecture, scope and compatibility](CONTRACTS.md)
|
|
12
12
|
|
|
@@ -14,44 +14,56 @@ Version 0.3.0 adds native refs for actions/choices and typed root/part props for
|
|
|
14
14
|
|
|
15
15
|
Version 0.4.0 adds native named/external form submission and controlled/default/reset support for Switch, Slider and RadioGroup, plus caller IDs for Checkbox. See [the form guide](FORMS.md) and the copyable native/RHF source recipes shipped in examples/forms. React Hook Form is an application dependency; no runtime dependency or CSS rewrite is added. New form props are absent from 0.3.0.
|
|
16
16
|
|
|
17
|
+
Version 0.5 adds topmost overlay dismissal, hoverable hints, collision-aware body/modal portals, scoped/prefixed CSS and themes, local labels/formatters, selective style entries and measured size gates. Floating UI DOM is a new runtime positioning dependency. Default portal DOM placement changes ancestor selectors; see [overlay migration](OVERLAYS.md), [styling and embedding](STYLING.md) and [measured costs](SIZE.md). Legacy styles/imports remain available. This is an early 0.x release, not a 1.0 milestone.
|
|
18
|
+
|
|
19
|
+
Version 0.6 adds a single-select [Combobox](COMBOBOX.md) backed by pinned Base UI, a semantic [Table and application recipes](RECIPES.md), public standalone Vite/Next starters, CSP setup and a minimal reproduction. [Start here](GETTING-STARTED.md); [foundation decision and measured dependency cost](FOUNDATION.md); [documentation coverage](DOCS.md). DataTable retains local scalar sorting. Async requests, deletion and remote table state stay application-owned. The private catalogue and independent consumers are not upgraded by this release.
|
|
20
|
+
|
|
17
21
|
## Install
|
|
18
22
|
|
|
19
23
|
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+.
|
|
20
24
|
|
|
21
25
|
```sh
|
|
22
|
-
npm install @abuzareal/contour-ui@0.
|
|
26
|
+
npm install @abuzareal/contour-ui@0.6.0 --save-exact
|
|
23
27
|
```
|
|
24
28
|
|
|
25
29
|
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.
|
|
26
30
|
|
|
27
31
|
## First component
|
|
28
32
|
|
|
29
|
-
Import the styles once in your application entry. Import components directly from the package; no generated files or local re-exports are needed.
|
|
33
|
+
Import the styles once in your application entry. This isolated first-use recipe uses system fonts and no global reset; its scope owns only local theme state. Import components directly from the package; no generated files or local re-exports are needed.
|
|
30
34
|
|
|
31
35
|
```tsx
|
|
32
36
|
import { useState } from "react";
|
|
33
|
-
import "@abuzareal/contour-ui/
|
|
34
|
-
import
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
import "@abuzareal/contour-ui/scoped.css";
|
|
38
|
+
import {
|
|
39
|
+
Button,
|
|
40
|
+
TextField,
|
|
41
|
+
ThemeToggle,
|
|
42
|
+
ThemeScope,
|
|
43
|
+
} from "@abuzareal/contour-ui";
|
|
37
44
|
|
|
38
45
|
export default function App() {
|
|
46
|
+
const [theme, setTheme] = useState<"light" | "dark">("light");
|
|
39
47
|
const [name, setName] = useState("");
|
|
40
48
|
const [savedName, setSavedName] = useState("");
|
|
41
49
|
|
|
42
50
|
return (
|
|
43
|
-
<
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
<ThemeScope theme={theme} onThemeChange={setTheme}>
|
|
52
|
+
<main data-motion="off">
|
|
53
|
+
<ThemeToggle animate={false} />
|
|
54
|
+
<TextField
|
|
55
|
+
label="Name"
|
|
56
|
+
value={name}
|
|
57
|
+
onChange={(event) => setName(event.target.value)}
|
|
58
|
+
/>
|
|
59
|
+
<Button variant="accent" onClick={() => setSavedName(name)}>
|
|
60
|
+
Save locally
|
|
61
|
+
</Button>
|
|
62
|
+
<p role="status">
|
|
63
|
+
{savedName && `Saved for this session: ${savedName}`}
|
|
64
|
+
</p>
|
|
65
|
+
</main>
|
|
66
|
+
</ThemeScope>
|
|
55
67
|
);
|
|
56
68
|
}
|
|
57
69
|
```
|
|
@@ -72,13 +84,15 @@ import "@abuzareal/contour-ui/fonts-latin.css";
|
|
|
72
84
|
|
|
73
85
|
| Import | Purpose |
|
|
74
86
|
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
|
-
| `styles.css` |
|
|
87
|
+
| `styles.css` | Legacy global aggregate: tokens, component styles and layout compatibility utilities. |
|
|
76
88
|
| `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. |
|
|
77
89
|
| `fonts-latin.css` | Optional Latin-only WOFF2 fonts, available since 0.1.2. Use instead of `fonts.css`, not alongside it. |
|
|
78
90
|
| `fonts.css` | Optional self-hosted Space Grotesk, DM Sans, and DM Mono via Fontsource. Your bundler emits the font assets. |
|
|
79
91
|
| `tokens.css` | Tokens alone, for an app that supplies its own component styles. Already included by `styles.css`. |
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
For new integrations, prefer `scoped.css`, optional `minimal-reset.css`, and `--contour-*` overrides. Family entries and the shipped `tokens.json` schema are documented in [STYLING.md](STYLING.md) and [SIZE.md](SIZE.md). The table and override example below describe **legacy compatibility imports**.
|
|
94
|
+
|
|
95
|
+
Override legacy semantic tokens after importing those styles:
|
|
82
96
|
|
|
83
97
|
```css
|
|
84
98
|
:root {
|
|
@@ -92,13 +106,13 @@ Override semantic tokens after importing the styles:
|
|
|
92
106
|
}
|
|
93
107
|
```
|
|
94
108
|
|
|
95
|
-
|
|
109
|
+
Legacy styles use global component classes and document-level theme tokens; isolated styles use prefixed hooks and ThemeScope. 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.
|
|
96
110
|
|
|
97
|
-
The three Fontsource packages
|
|
111
|
+
The three Fontsource packages, `lucide-react` and `@floating-ui/dom` 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.
|
|
98
112
|
|
|
99
113
|
## Theme and motion
|
|
100
114
|
|
|
101
|
-
|
|
115
|
+
For embedded/mixed themes, use controlled ThemeScope with isolated CSS; scoped toggles do not write document state or storage. For legacy document ownership, set `data-theme="light"` or `"dark"` on `<html>`. `ThemeToggle` and `useTheme(animate)` read the same document state. The hook returns `{ theme, toggle, canToggle }`; call `toggle(buttonElement)` from a user action. Preferences use the `aa-theme` local-storage key and are local to the site's origin.
|
|
102
116
|
|
|
103
117
|
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.
|
|
104
118
|
|
|
@@ -109,9 +123,9 @@ For theme selection before first paint, inject `themeBootstrapScript` from `@abu
|
|
|
109
123
|
| Area | Public exports |
|
|
110
124
|
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
111
125
|
| Actions | `Button`, `IconButton`, `ExternalLink`, `SegmentedControl`, `ThemeToggle` |
|
|
112
|
-
| Forms | `Field`, `TextField`, `TextArea`, `SelectField`, `Checkbox`, `RadioGroup`, `Switch`, `Slider`
|
|
126
|
+
| Forms | `Field`, `TextField`, `TextArea`, `SelectField`, `Combobox`, `Checkbox`, `RadioGroup`, `Switch`, `Slider` |
|
|
113
127
|
| Navigation | `Tabs`, `Breadcrumbs`, `Pagination`, `Stepper`, `DropdownMenu` |
|
|
114
|
-
| Data and layout | `Avatar`, `Badge`, `Card`, `DataTable`, `Disclosure`, `Divider`, `EmptyState`, `Kbd`, `SectionLabel`, `Stat`, `Tag`, `Timeline`
|
|
128
|
+
| Data and layout | `Avatar`, `Badge`, `Card`, `DataTable`, `Table`, `Disclosure`, `Divider`, `EmptyState`, `Kbd`, `SectionLabel`, `Stat`, `Tag`, `Timeline` |
|
|
115
129
|
| Feedback | `Alert`, `ProgressBar`, `Skeleton`, `Spinner`, `ToastProvider`, `useToast` |
|
|
116
130
|
| Motion and identity | `BrandMark`, `CountUp`, `DepthButton`, `FlipCard`, `Magnetic`, `Marquee`, `Reveal`, `RevealHeading`, `ScrollProgress`, `TiltCard`, `GitHubIcon`, `LinkedInIcon` |
|
|
117
131
|
| Overlays and scene fallback | `Dialog`, `Popover`, `Tooltip`, `SceneBoundary` |
|
|
@@ -166,7 +180,7 @@ Give scenes a sized container and a usable static fallback. `SceneBoundary` hide
|
|
|
166
180
|
**0.2.0** adds native field refs, controlled tabs and popover/menu open 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.
|
|
167
181
|
|
|
168
182
|
```sh
|
|
169
|
-
npm install @abuzareal/contour-ui@0.
|
|
183
|
+
npm install @abuzareal/contour-ui@0.6.0 --save-exact
|
|
170
184
|
```
|
|
171
185
|
|
|
172
186
|
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.
|
|
@@ -188,6 +202,8 @@ npm pack --dry-run
|
|
|
188
202
|
|
|
189
203
|
`validate` checks types, lint, builds, Node/component tests, dialog, native form/composition 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 also runs shipped native/RHF recipe browser checks on React 18/19 and compiles the legacy form recipe against npm 0.3.0. It runs both optional/core checks and verifies the Latin font assets. Compatibility runs need registry access for clean installs.
|
|
190
204
|
|
|
205
|
+
`measure:size` / `check:size` build real Vite/Next consumers and keep JS/CSS/fonts/archive/install/network costs separate. `build:styles` generates isolated CSS from the token schema; public source links are checked against declarations.
|
|
206
|
+
|
|
191
207
|
`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.
|
|
192
208
|
|
|
193
209
|
MIT licensed. Fontsource fonts and dependencies retain their own licenses.
|
|
@@ -238,7 +254,7 @@ Custom `useThreeStage` setup can register disposal through `context.onDispose?.(
|
|
|
238
254
|
|
|
239
255
|
## Compatibility boundaries
|
|
240
256
|
|
|
241
|
-
|
|
257
|
+
Legacy aggregate styles remain global. The new isolated CSS path is documented in STYLING.md; it is not Shadow DOM or immunity from arbitrary host selectors. 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.
|
|
242
258
|
|
|
243
259
|
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.
|
|
244
260
|
|
package/RECIPES.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Application recipes — 0.6.0
|
|
2
|
+
|
|
3
|
+
The source linked here is the code copied, compiled and exercised by the package checks. These files are application code, not additional exports. Import components directly from the public package; copy only the recipe needed. There are no live services or fabricated successful operations. [Source at this release](https://github.com/abuzareal/contour-ui/tree/v0.6.0/examples/recipes).
|
|
4
|
+
|
|
5
|
+
## Async single-select
|
|
6
|
+
|
|
7
|
+
Copy [AsyncProjectPicker.tsx](examples/recipes/AsyncProjectPicker.tsx) and [useRemoteResource.ts](examples/recipes/useRemoteResource.ts). Supply a stable searchProjects(query, signal) function, normally defined outside render or with useCallback. It must validate response IDs/labels, reject malformed data, bound results, and map denied requests to RecipeServiceError("permission"). The component retains one selected record separately from current results. It aborts obsolete work and ignores a response even if the transport ignores abort. Debounce, authentication, caching and data updates are application policies.
|
|
8
|
+
|
|
9
|
+
The controlled selection and text query are independent; typed text never becomes a project ID. Filtering/reordering preserves identity. Loading, no matches, permission failure, service failure and retry are explicit. A failed or stale response cannot replace a newer selection. Use the native name/form/value contracts from [Combobox](COMBOBOX.md); parent form reset should reset the application's controlled selected/query state when required. No hidden network request is made by the library.
|
|
10
|
+
|
|
11
|
+
## Destructive confirmation
|
|
12
|
+
|
|
13
|
+
Copy [DeleteProjectConfirmation.tsx](examples/recipes/DeleteProjectConfirmation.tsx). It composes the existing native **Dialog**; no new AlertDialog export is added. This confirmation is a user-initiated decision with clear consequences, not an urgent unsolicited alert. The title names the project, the description states irreversible consequences, “Keep project” receives initial focus and “Permanently delete project” is explicit. The trigger regains focus after dismissal while it remains connected.
|
|
14
|
+
|
|
15
|
+
The deleteProject(id, signal) function performs the real server operation; resolving it means the server confirmed success. A disabled pending action prevents duplicate submissions; an immediate ref also guards repeated activation before rendering. A rejected operation keeps the dialog open with a persistent error and an explicit retry action. The example does not announce success until the operation resolves. Target identity keys the interaction, so changing projects or unmounting invalidates the old request.
|
|
16
|
+
|
|
17
|
+
Escape, backdrop, close and “Keep project” cancel waiting and dismiss, including while pending. Abort cannot guarantee a server rollback; the pending message says the server may still complete. Cancelled or obsolete requests cannot invoke onDeleted or close a later confirmation. The application must reconcile server state before retrying an uncertain outcome and implement authorization/idempotency at its server boundary. Use a different explicit contract if a product must prohibit cancellation during a critical phase. Dialog semantics, keyboard containment and return are inherited from the released Dialog; physical assistive-technology verification is separate.
|
|
18
|
+
|
|
19
|
+
## Tables: local versus remote
|
|
20
|
+
|
|
21
|
+
**DataTable** is deliberately small: `Row extends Record<string, string | number>`, caption, columns and a stable rowKey. Local sorting compares string/number values or column.compare; render formats a cell but does not remove the row constraint. Use immutable stable IDs, not array indexes or translated labels. DataTable has no remote sorting/filtering/pagination, selection, bulk actions, virtualization, editing or adjustable density.
|
|
22
|
+
|
|
23
|
+
**Table** is a new semantic presentation primitive for application-owned native thead/tbody/tr/th/td markup. It accepts native table attributes/events, forwards ref to HTMLTableElement, requires a string caption and children, hides the caption visually by default (`showCaption=false`) and offers native scrollProps/ref for the existing focusable scroll region. It owns one caption; do not supply a second caption in children. Use scope, headers, aria-sort and text equivalents according to the data. It adds no sorting engine, ARIA grid keyboard model or layout algorithm. Overflow is horizontally scrollable. Use data-display.css or scoped.css; contour-table-scroll/contour-data-table and semantic typography/border tokens supply the shared styling.
|
|
24
|
+
|
|
25
|
+
Copy [RemoteRecordsTable.tsx](examples/recipes/RemoteRecordsTable.tsx) and useRemoteResource.ts. Install **@tanstack/react-table@9.3.1** and **@tanstack/react-store@0.11.2** as your application's direct dependencies; neither is a Contour runtime dependency. This recipe uses v9 useTable/tableFeatures/FlexRender and controlled application state, not the older v8 useReactTable API. [Official v9 state guide](https://tanstack.com/table/latest/docs/framework/react/guide/table-state), [migration guide](https://tanstack.com/table/latest/docs/framework/react/guide/migrating).
|
|
26
|
+
|
|
27
|
+
loadRecords(query, signal) must perform the requested filter, sorting and pagination remotely. Validate parsed JSON with parseRecordsPage and validate/whitelist query fields at the server; never interpolate arbitrary column IDs into SQL. Only name/owner columns exist in this example. Return rows, filtered total and unfilteredTotal consistently. Name/id/owner are demo record fields, not a prescribed business model.
|
|
28
|
+
|
|
29
|
+
The app controls query, resets the page when filter/sort changes, ignores obsolete responses, retains and **labels** the previous successful snapshot on loading/service failure, and hides old rows after permission failure. Headers describe the displayed snapshot; pending controls request new data rather than locally sorting old rows. A table caption, native headers, stable row/cell IDs and status text preserve semantics. Loading, empty database, no filter matches, missing page, denied/service error and retry are distinct. Pagination is disabled while the requested result is unresolved or failed. Changes to dataset totals may require “First page”; they do not masquerade as an empty database.
|
|
30
|
+
|
|
31
|
+
Selection/bulk actions, density, virtualization, pinned columns and enterprise grid behavior are explicitly unsupported in this bounded recipe. If required, define stable selection across pages, permissions, indeterminate/select-all scope, destructive bulk outcomes and keyboard behavior before enabling engine features. Use a mature grid for grid behavior; do not add role=grid to this table without its keyboard contract. Styling is not evidence of a complete enterprise grid.
|
|
32
|
+
|
|
33
|
+
## Input and typography decisions
|
|
34
|
+
|
|
35
|
+
[InputCompositions.tsx](examples/recipes/InputCompositions.tsx) demonstrates existing TextField leadingIcon/trailing slots and native form identity. Decorative icons/units use aria-hidden; meaningful units belong in the label/hint. Interactive trailing content needs its own accessible name, button type and focus style; keep long content responsive. No new prefix/suffix API is needed for the demonstrated cases.
|
|
36
|
+
|
|
37
|
+
A native type=number field is sufficient for the bounded integer seat quantity. There is no recorded adopter requirement for a new locale-aware NumberField. Decimal/group separators, transient minus/decimal input, locale parsing/formatting, range/step, currency precision and serialization need a separate demand/contract gate. Financial amounts must not inherit a binary-floating point money model from a generic input. The conditional NumberField remains deferred.
|
|
38
|
+
|
|
39
|
+
The optional **password visibility recipe** keeps the same input mounted, name and current-password autocomplete stable, and a named aria-pressed toggle. Focus returns to the input after toggling; no value is copied, transformed or logged. Real password-manager/browser compatibility remains app/device verification. A standalone library PasswordInput is unnecessary without a stronger reusable requirement.
|
|
40
|
+
|
|
41
|
+
Use native p/span and h1–h6 with [typography tokens](STYLING.md). Heading level follows the document outline; size/weight are CSS choices. No Text/Heading wrappers are added without an independent semantic/variant contract. These explicit scope decisions do not complete deferred NumberField, full grid, calendar or other optional features.
|
|
42
|
+
|
|
43
|
+
[Report Table/recipes 0.6.0](https://github.com/abuzareal/contour-ui/issues/new?template=bug_report.yml&title=Table%20recipes%200.6.0%3A%20&component=Table%20recipes&version=0.6.0); [standalone reproduction](examples/reproduction). See [public API](PUBLIC-API.md) for exact types, [styling](STYLING.md) and [onboarding](GETTING-STARTED.md). Release source links become available with the published tag.
|
package/RELEASING.md
CHANGED
|
@@ -9,10 +9,10 @@ Use [the public contract and compatibility policy](CONTRACTS.md#change-deprecati
|
|
|
9
9
|
1. Obtain maintainer authorization to publish; validation alone is not authorization.
|
|
10
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
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
|
|
12
|
+
4. Set `CONTOUR_RELEASE_DIR` to a 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 fixtures, checks public API/documentation/generated styles and production size budgets, audits dependencies, and writes `verified-archive.json` with its SHA-256. Core consumers must have no Three.js, Lenis, RHF, TanStack Table or optional date peers installed. Copied adoption recipes run on React 18/19; standalone Vite/Next/reproduction folders run from that archive with their exact versions and production CSP. Review failures and advisories instead of bypassing gates.
|
|
13
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
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.
|
|
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. Before closing the starter publication gate, run `CONTOUR_STARTER_REGISTRY=1 npm run test:starters` to verify clean published-package installs; the prepublish archive run alone does not satisfy that gate. Do not substitute a local link or source copy for release validation.
|
|
16
16
|
|
|
17
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.
|
|
18
18
|
|
package/SIZE.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Package and application size
|
|
2
|
+
|
|
3
|
+
A package archive, installed dependencies and a browser download are different costs. Contour does not claim to be zero-dependency or the lightest library. `scripts/measure-size.mjs` builds public-import Button, settings-form, table and Combobox journeys in Vite and pinned Next App Router/webpack, loads fresh browser pages, and writes a JSON report. An additional Button-with-ArrowRight fixture measures the incremental selected-icon journey; it is not an isolated icon-function byte count.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm run build
|
|
7
|
+
CONTOUR_SIZE_OUTPUT=/absolute/path/sizes.json npm run measure:size
|
|
8
|
+
CONTOUR_SIZE_OUTPUT=/absolute/path/check.json npm run check:size
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Set `CONTOUR_ARCHIVE` to the already packed candidate to reuse it. Fixtures install React/React DOM 19.3.0; Next uses 16.4.0, and Vite/Chromium/Node versions are recorded from the running tools. The report names package/source commit/dirty state, archive checksum/files/compressed/unpacked bytes, the fresh dependency lock hash, resolved dependency versions, fixtures, exclusions, raw/gzip file sizes and actual Resource Timing transfers. These are reproducible fixtures, not latency or production-site benchmarks. No React/framework externals are removed from the application totals. Next shared JS is the intersection of actually fetched URLs across the measured fresh routes; route-specific JS is listed separately. Server build output is excluded from client bytes.
|
|
12
|
+
|
|
13
|
+
Raw minified JS/CSS is reported per actual fetched file and by fixture. Gzip level 9 is reported **per file**, never summed and advertised as an application gzip total. Network transfer, encoded body and decoded body measurements are kept separately, including document/RSC resources. CSS/font requests, archive size and library dependency-closure installed file bytes are separate categories. Install counts exclude Next/Vite/development tooling and record React peers separately. Optional Three.js, Lenis and RHF are absent from core fixtures.
|
|
14
|
+
|
|
15
|
+
## Selective CSS
|
|
16
|
+
|
|
17
|
+
The 13 October Vite comparison used identical React 19.3.0 journeys against 0.4.0 and the 0.5 candidate. Raw fetched CSS was 34,642 bytes for the old aggregate, 45,881 bytes for the isolated aggregate, and 14,368 / 22,985 / 38,897 bytes for selective Button / settings / table imports. The isolated aggregate grows by 11,239 bytes because it supplies owned defaults, accessibility rules and prefixed hooks. Selective Button CSS saves about 59% against the old aggregate and 69% against the new aggregate; larger compositions save less. Raw application JS grew roughly 2.9–3.7 KB across Vite/Next fixtures for shared dismissal, positioning and styling support. These are whole application totals, not library-only costs. All system-font fixtures made zero font requests. Exact reports and the reviewed gate baseline record tools, dependency versions and per-file compression; these observations are not immutable promises.
|
|
18
|
+
|
|
19
|
+
`scoped.css` remains the convenient isolated aggregate; existing `styles.css` remains compatibility mode. Optional family entries are:
|
|
20
|
+
|
|
21
|
+
| Entry | Controls/patterns |
|
|
22
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| actions.css | Button/IconButton/DepthButton, SegmentedControl, Magnetic, ThemeToggle; required loading Spinner styles |
|
|
24
|
+
| forms.css | Field/TextField/TextArea/SelectField, Checkbox/RadioGroup/Switch/Slider |
|
|
25
|
+
| navigation.css | Tabs, Breadcrumbs, Pagination, Stepper |
|
|
26
|
+
| data-display.css | Card/Avatar/Badge/Tag/Stat, Table/DataTable/Timeline and value-display patterns |
|
|
27
|
+
| feedback.css | Alert, ToastProvider, ProgressBar, Spinner, Skeleton, EmptyState |
|
|
28
|
+
| overlays.css | Tooltip, Popover, DropdownMenu, Dialog/drawers |
|
|
29
|
+
| effects.css | TiltCard/FlipCard/Marquee and CSS effects |
|
|
30
|
+
|
|
31
|
+
Every entry includes the canonical tokens, owned base/accessibility defaults and shared UI styles. Import all families used by a composition; a table page with filters/buttons/pagination needs forms/actions/data-display/navigation. Family imports do not include fonts, page layout or the legacy reset. Shared CSS is resolved/deduplicated by the tested bundlers; import order remains tokens/base before components, application overrides after. Core JS remains named public imports, not unsupported deep paths. Browser fixtures verify selected-family production styling as well as request sizes; size alone is not a styling check.
|
|
32
|
+
|
|
33
|
+
## Fonts and installed dependencies
|
|
34
|
+
|
|
35
|
+
System typography is the isolated default; no Fontsource CSS import is needed. Branded typography is explicit: import exactly one of `fonts-latin.css` (relative packaged assets) or `fonts.css` (existing broader Fontsource coverage). Font binaries/licenses and existing broader coverage are retained. Importing both repeats font-face declarations unnecessarily. Custom fonts can override the three documented prefixed font tokens.
|
|
36
|
+
|
|
37
|
+
The 13 October initial fresh install used Fontsource 5.3.0 packages (approximately 0.93 MB of installed files together), while Lucide 1.56.0 occupied approximately 35.8 MB of installed files. File sizes are logical byte sums, not filesystem allocation. Those values are resolved-install observations, not browser bytes or immutable promises for caret ranges. Exact versions/bytes are in the fixture report. Selected icon code is tree-shaken; compare the plain/icon Button fixture and separate install-tree cost before deciding to replace Lucide. Contour retains Lucide, its license and API; no icons were copied to advertise fewer dependencies.
|
|
38
|
+
|
|
39
|
+
Font packaging was evaluated against vendoring broader assets or creating another optional package. This release **retains** the three runtime Fontsource dependencies: they keep existing `fonts.css` imports resolvable and its current subset/license behavior intact. Moving them to devDependencies alone would break consumers. Vendoring would move cost into the archive and add coverage/license maintenance; a separate package would require a versioned migration. There is no mandatory-font-install reduction claim in 0.5. Optional browser loading is already verified; revisit installation packaging only with a separately reviewed migration.
|
|
40
|
+
|
|
41
|
+
Three.js/Lenis stay optional peers behind their entries. Core imports do not install/load them. Existing lazy Next scene and static-fallback guidance remain tested; downloading 3D after opting in is a separate journey, not an omitted core-byte claim.
|
|
42
|
+
|
|
43
|
+
## Regression policy
|
|
44
|
+
|
|
45
|
+
`scripts/size-baseline.json` records the actual measured baseline and budgets. The gate allows 10% plus 1,024 bytes on raw fetched JS/CSS totals for deliberate review and chunk/minifier drift, and requires zero font requests in system-font fixtures. Update a baseline only after reviewing a real report and explaining intentional growth; do not automatically ratchet it to pass. Source/package/dependency changes can alter results even with the same components. Release verification and CI use the same packed archive and run this gate. The library-only samples from earlier research are not substituted for these application measurements.
|
|
46
|
+
|
|
47
|
+
## 0.6 Combobox measurement
|
|
48
|
+
|
|
49
|
+
On 14 October 2026, an initial 0.6.0 candidate used the same production fixtures to measure the new Combobox journey with React 19.3.0, Vite 8.3.2, Next 16.4.0/webpack, Node 26.10.0 and Chromium 153. A fresh Vite page fetched 365,549 raw JS bytes; selective forms/actions CSS fetched 24,729 bytes (aggregate 47,625). The Next Combobox route fetched 593,605 raw client JS bytes and 46,807 CSS bytes, including framework/shared assets. These are dated candidate measurements, not immutable final-archive byte counts. The release gate remeasures its exact archive. These whole-application fixtures are not an isolated component size or an upstream bundle-size badge. Per-file gzip and actual Resource Timing are retained in the report; independent gzip files are not advertised as an additive component total.
|
|
50
|
+
|
|
51
|
+
Every previous journey stays within its existing budget. Initial Combobox budgets are added separately under the same 10% plus 1,024-byte policy; old budgets are not reset to the new measurements. The root Button fixture remains 227,291 raw Vite JS bytes with zero font requests; importing one component does not require downloading the Combobox route's full client code. All runtime dependencies still install with the package. Base UI adds installed files and an unstyled interaction dependency; see FOUNDATION.md for the identical-host comparison and its limitations. Its optional date/time peers are absent from core consumers. TanStack Table/Store and RHF are recipe dependencies only.
|
package/STYLING.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Styling and embedding
|
|
2
|
+
|
|
3
|
+
0.5 adds an isolated CSS path. Existing `styles.css`, `tokens.css`, `reset.css` and generic component classes remain compatibility mode; importing them still exposes global rules. They are not silently converted into isolated CSS. Choose one style path per component tree.
|
|
4
|
+
|
|
5
|
+
## First use with system fonts
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Button } from "@abuzareal/contour-ui";
|
|
9
|
+
import "@abuzareal/contour-ui/scoped.css";
|
|
10
|
+
export function FirstControl() {
|
|
11
|
+
return (
|
|
12
|
+
<div className="contour-root">
|
|
13
|
+
<Button variant="accent">Continue</Button>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
No fonts or reset import is needed. `scoped.css` defines only prefixed component hooks/tokens and scope defaults, not page width, section offsets or a site shell. `minimal-reset.css` is optional global box-sizing/body margin/form-font normalization. The original `reset.css` remains the legacy reset/layout compatibility path. Applications own section scroll margins, page-width/site-shell/split-layout/section-intro recipes and their typography/layout. Their old rules remain available through the original styles/reset imports.
|
|
20
|
+
|
|
21
|
+
Import foreign CSS first, isolated Contour styles next, and deliberate application overrides last. Components emit `contour-*` hooks **and** their old classes. Isolated CSS targets only prefixed hooks; old generic `.button/.card/.tabs/.menu/.mono` rules are not defined by it. A host rule targeting a generic compatibility class on the same Contour element can still compete; put Contour CSS after it or narrow that host rule. This is CSS isolation by names and scopes, not Shadow DOM, arbitrary hostile-selector immunity or a reset of the entire host application.
|
|
22
|
+
|
|
23
|
+
## Themes without document ownership
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { useState } from "react";
|
|
27
|
+
import { ThemeScope, ThemeToggle, Button } from "@abuzareal/contour-ui";
|
|
28
|
+
export function EmbeddedControls() {
|
|
29
|
+
const [theme, setTheme] = useState<"light" | "dark">("light");
|
|
30
|
+
return (
|
|
31
|
+
<ThemeScope theme={theme} onThemeChange={setTheme}>
|
|
32
|
+
<ThemeToggle animate={false} />
|
|
33
|
+
<Button>Scoped action</Button>
|
|
34
|
+
<ThemeScope theme="dark">
|
|
35
|
+
<Button>Dark surface</Button>
|
|
36
|
+
</ThemeScope>
|
|
37
|
+
</ThemeScope>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`ThemeScope` is a controlled div with `data-contour-theme`, a native ref and native attributes/children. Adjacent/nested light and dark scopes inherit typography/consumer overrides but set semantic color values and color-scheme locally. Omitting `onThemeChange` makes scoped ThemeToggle controls disabled/read-only. Scoped useTheme/ThemeToggle updates only the caller's state; it does not write storage, metadata or the document root and uses an instant transition. Caller-owned persistence can use an application-specific key; do not share a document key across embedded apps accidentally. Plain `[data-contour-theme]` controls CSS only; use ThemeScope for scoped React theme controls.
|
|
43
|
+
|
|
44
|
+
Without ThemeScope, the original useTheme remains document-owned: root `data-theme`, theme-color metadata, `aa-theme` storage, same-window theme events, cross-tab storage, system preference fallback and optional document view transition. `themeBootstrapScript` retains that legacy pre-paint behavior. The isolated entries omit document-wide view-transition animation rules; an application choosing animated root transitions must provide their CSS (the legacy aggregate supplies its original rules). Embedding does not require invoking it. An application must explicitly choose root ownership; a scope does not sandbox independent root hooks outside it. Storage failures remain tolerated.
|
|
45
|
+
|
|
46
|
+
Default body/modal portals carry the trigger's variables, direction and font across the DOM move, including theme changes. Native modal portals stay in the browser's top layer. See [overlay container rules](OVERLAYS.md).
|
|
47
|
+
|
|
48
|
+
## Public tokens and precedence
|
|
49
|
+
|
|
50
|
+
[The shipped token schema](dist/tokens.json) / `@abuzareal/contour-ui/tokens.json` records each canonical name, semantic role, category, light/dark values, legacy alias and compatibility status. `tokens/schema.json` is the authored source; `npm run build:styles` generates isolated tokens and component CSS. Generated outputs are checked for drift. Schema categories cover color/status, typography, spacing, shape, motion, elevation and layer ordering. Manual semantic review remains necessary; generation does not prove contrast or usability.
|
|
51
|
+
|
|
52
|
+
Old `--bg/--fg/--acid/--surface/--font-sans/...` names map to `--contour-bg/--contour-fg/--contour-acid/--contour-surface/--contour-font-sans/...`. The old stylesheet keeps the old values/overrides; isolated styles use the new names. Migrate overrides alongside imports. Do not import both aggregates as a migration shortcut. Known old classes retain prefixed aliases; unknown caller classes are preserved. Exact whole-class-attribute comparisons/snapshots need updating for the added aliases; use class membership selectors. `buttonClassName` and `ccx` include both hook forms; `cx` continues to combine arbitrary strings/IDs without prefixing.
|
|
53
|
+
|
|
54
|
+
Defaults use low-specificity token selectors. Consumer properties on a theme scope override inherited defaults. A nested explicit theme resets themeable semantic colors; set overrides on that nested scope if wanted there. Non-theme constants such as fonts/spacing inherit. Put root/scoped overrides after Contour imports; inline custom properties on an explicit scope win at that element. Customize documented part classes/props; state/ARIA/native behavior remains component-owned. Geometry variables such as progress and tab-indicator coordinates are internal, not design tokens.
|
|
55
|
+
|
|
56
|
+
Header/focus-band/about/experience/expertise/work/footer values are legacy application/product tokens and remain only in the legacy token file. The isolated schema excludes them. Marquee uses reusable inverse-surface/accent tokens rather than inheriting a page focus-band recipe. Avatar marks, banner accents, depth bases and feedback outlines also inherit scoped theme roles. The isolated inverse accent uses the existing banner olive in dark surfaces; the legacy page-specific ticker color remains unchanged. Porting a consumer means moving those values and page/layout recipes into that application, not removing its styling from the library without a migration.
|
|
57
|
+
|
|
58
|
+
## Fonts, density and localization
|
|
59
|
+
|
|
60
|
+
System fonts are the isolated default. Import **one** of `fonts-latin.css` or `fonts.css` after the styles to opt into DM Sans, Space Grotesk and DM Mono. The Latin entry uses packaged relative assets; the original entry keeps Fontsource's broader available character coverage. Neither promises every writing system; fallback fonts still matter. Custom fonts override the three `--contour-font-*` properties on the relevant scope. Fontsource packages still install in this release; optional browser loading is different from optional installation.
|
|
61
|
+
|
|
62
|
+
Comfortable sizes remain the current contract. Compact density is deferred: no concrete application target/long-content requirement justified another size matrix, and compressed targets could reduce usability. Button sizes remain available; do not invent a global compact mode from demo CSS.
|
|
63
|
+
|
|
64
|
+
Caller content/labels remain application-owned. New local overrides cover Button loadingLabel; Alert dismissLabel; Breadcrumbs label; Tag removeLabel; Avatar statusLabel/initials; ThemeToggle darkLabel/lightTitle/darkTitle; ToastProvider label/dismissLabel; Pagination previousLabel/nextLabel/pageLabel/formatPage; Switch requiredMessage; CountUp formatValue; DataTable column compare. Existing Dialog closeButtonProps aria-label, Spinner/Stepper labels and Slider format remain usable. Native validation messages follow the browser locale. Field-counter defaults, formatIndex and default initials/typeahead/binary table sorting are not a locale engine; use caller formatters/comparators where provided and visible caller instructions when defaults are unsuitable. No global translation or date library is installed.
|
|
65
|
+
|
|
66
|
+
Promoted forms, horizontal Tabs/SegmentedControl, pagination icons, menu placement and text alignment are checked in RTL; the keyboard helper accepts an optional direction while retaining LTR defaults. CSS uses logical inline spacing/borders where applicable. Coordinate-driven tab indicators remain physical measurements. No universal RTL promise is made for experimental 3D/effects or application scenes.
|
|
67
|
+
|
|
68
|
+
Forced-color rules preserve focus, selected control outlines, radio marks, invalid field borders and floating panel boundaries. Production emulation checks both themes and focus; they do not certify every Windows theme or manual screen-reader workflow. Branded paper/ink/lime colors, outlines, typography and depth are retained by compatibility mode and by the isolated branded-font recipe; system-font first use is an intentional option.
|
package/dist/actions.css
ADDED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ccx } from "../../lib/classNames.js";
|
|
3
4
|
/** Hero sculpture: three chrome/lime links that follow the pointer and drift while motion is on. */
|
|
4
5
|
import { useRef } from "react";
|
|
5
6
|
import * as THREE from "three";
|
|
@@ -52,5 +53,5 @@ export default function Sculpture({ motionEnabled }) {
|
|
|
52
53
|
setup: setupSculpture,
|
|
53
54
|
cameraDistance,
|
|
54
55
|
});
|
|
55
|
-
return (_jsx("div", { ref: hostRef, className: "sculpture-canvas", "data-state": "loading" }));
|
|
56
|
+
return (_jsx("div", { ref: hostRef, className: ccx("sculpture-canvas"), "data-state": "loading" }));
|
|
56
57
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ccx } from "../../lib/classNames.js";
|
|
3
4
|
/** Lays out a set of 3D objects in a responsive grid; each turns slowly and the set leans toward the pointer. */
|
|
4
5
|
import { useRef } from "react";
|
|
5
6
|
import * as THREE from "three";
|
|
@@ -65,5 +66,5 @@ export default function ObjectStudy({ label, motionEnabled, study, }) {
|
|
|
65
66
|
};
|
|
66
67
|
},
|
|
67
68
|
});
|
|
68
|
-
return (_jsx("div", { ref: hostRef, className: "stage-canvas", "data-state": "loading", role: "img", "aria-label": label }));
|
|
69
|
+
return (_jsx("div", { ref: hostRef, className: ccx("stage-canvas"), "data-state": "loading", role: "img", "aria-label": label }));
|
|
69
70
|
}
|
|
@@ -6,8 +6,9 @@ export type AlertProps = {
|
|
|
6
6
|
title: string;
|
|
7
7
|
children?: ReactNode;
|
|
8
8
|
action?: ReactNode;
|
|
9
|
+
dismissLabel?: string;
|
|
9
10
|
onDismiss?: () => void;
|
|
10
11
|
/** "banner" spans its container edge to edge on one line. */
|
|
11
12
|
layout?: "inline" | "banner";
|
|
12
13
|
};
|
|
13
|
-
export default function Alert({ tone, title, children, action, onDismiss, layout, }: AlertProps): import("react").JSX.Element;
|
|
14
|
+
export default function Alert({ tone, title, children, action, onDismiss, dismissLabel, layout, }: AlertProps): import("react").JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Asterisk, CircleCheck, CircleX, Info, TriangleAlert, X, } from "lucide-react";
|
|
4
|
-
import {
|
|
4
|
+
import { ccx } from "../../lib/classNames.js";
|
|
5
5
|
const toneIcons = {
|
|
6
6
|
neutral: _jsx(Asterisk, { size: 18 }),
|
|
7
7
|
accent: _jsx(Asterisk, { size: 18 }),
|
|
@@ -10,7 +10,7 @@ const toneIcons = {
|
|
|
10
10
|
warning: _jsx(TriangleAlert, { size: 18 }),
|
|
11
11
|
danger: _jsx(CircleX, { size: 18 }),
|
|
12
12
|
};
|
|
13
|
-
export default function Alert({ tone = "neutral", title, children, action, onDismiss, layout = "inline", }) {
|
|
13
|
+
export default function Alert({ tone = "neutral", title, children, action, onDismiss, dismissLabel = "Dismiss", layout = "inline", }) {
|
|
14
14
|
const urgent = tone === "warning" || tone === "danger";
|
|
15
|
-
return (_jsxs("div", { role: urgent ? "alert" : "status", className:
|
|
15
|
+
return (_jsxs("div", { role: urgent ? "alert" : "status", className: ccx("alert", `alert-${tone}`, `alert-${layout}`), children: [_jsx("span", { className: ccx("alert-icon"), "aria-hidden": "true", children: toneIcons[tone] }), _jsxs("div", { className: ccx("alert-content"), children: [_jsx("strong", { className: ccx("alert-title"), children: title }), children && _jsx("div", { className: ccx("alert-body"), children: children })] }), action && _jsx("div", { className: ccx("alert-action"), children: action }), onDismiss && (_jsx("button", { type: "button", className: ccx("alert-dismiss"), "aria-label": dismissLabel, onClick: onDismiss, children: _jsx(X, { size: 16 }) }))] }));
|
|
16
16
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export type AvatarProps = {
|
|
2
2
|
name: string;
|
|
3
|
+
initials?: string;
|
|
3
4
|
src?: string;
|
|
4
5
|
size?: "sm" | "md" | "lg" | "xl";
|
|
6
|
+
statusLabel?: string;
|
|
5
7
|
status?: "online" | "away";
|
|
6
8
|
};
|
|
7
|
-
export default function Avatar({ name, src, size, status, }: AvatarProps): import("react").JSX.Element;
|
|
9
|
+
export default function Avatar({ name, initials, src, size, status, statusLabel, }: AvatarProps): import("react").JSX.Element;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/** Round avatar showing a photo or Space Grotesk initials, with an optional presence dot. */
|
|
3
|
-
import {
|
|
3
|
+
import { ccx } from "../../lib/classNames.js";
|
|
4
4
|
const initialsOf = (name) => name
|
|
5
5
|
.split(" ")
|
|
6
6
|
.map((part) => part[0])
|
|
7
7
|
.slice(0, 2)
|
|
8
8
|
.join("")
|
|
9
9
|
.toUpperCase();
|
|
10
|
-
export default function Avatar({ name, src, size = "md", status, }) {
|
|
11
|
-
return (_jsxs("span", { className:
|
|
10
|
+
export default function Avatar({ name, initials, src, size = "md", status, statusLabel, }) {
|
|
11
|
+
return (_jsxs("span", { className: ccx("avatar", `avatar-${size}`), children: [src ? (_jsx("img", { src: src, alt: name, loading: "lazy", decoding: "async" })) : (_jsx("span", { role: "img", "aria-label": name, className: ccx("avatar-initials"), children: initials ?? initialsOf(name) })), status && (_jsx("span", { className: ccx("avatar-status", `avatar-status-${status}`), role: "img", "aria-label": statusLabel ?? (status === "online" ? "Online" : "Away") }))] }));
|
|
12
12
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { ccx } from "../../lib/classNames.js";
|
|
3
3
|
export default function Badge({ children, tone = "accent", dot = false, icon, }) {
|
|
4
|
-
return (_jsxs("span", { className:
|
|
4
|
+
return (_jsxs("span", { className: ccx("badge mono", `badge-${tone}`), children: [dot && _jsx("span", { className: ccx("badge-dot"), "aria-hidden": "true" }), icon, children] }));
|
|
5
5
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ccx } from "../../lib/classNames.js";
|
|
3
4
|
import { safeHref } from "../../lib/safeHref.js";
|
|
4
5
|
export default function BrandMark({ label, href = "#hero", onClick, }) {
|
|
5
|
-
return (_jsxs("a", { href: safeHref(href), className: "brand", "aria-label": label, onClick: onClick, children: ["aa", _jsx("span", { children: "." })] }));
|
|
6
|
+
return (_jsxs("a", { href: safeHref(href), className: ccx("brand"), "aria-label": label, onClick: onClick, children: ["aa", _jsx("span", { children: "." })] }));
|
|
6
7
|
}
|
|
@@ -5,6 +5,7 @@ type Crumb = {
|
|
|
5
5
|
};
|
|
6
6
|
export type BreadcrumbsProps = {
|
|
7
7
|
items: Crumb[];
|
|
8
|
+
label?: string;
|
|
8
9
|
};
|
|
9
|
-
export default function Breadcrumbs({ items }: BreadcrumbsProps): import("react").JSX.Element;
|
|
10
|
+
export default function Breadcrumbs({ items, label, }: BreadcrumbsProps): import("react").JSX.Element;
|
|
10
11
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ccx } from "../../lib/classNames.js";
|
|
2
3
|
import { safeHref } from "../../lib/safeHref.js";
|
|
3
|
-
export default function Breadcrumbs({ items }) {
|
|
4
|
-
return (_jsx("nav", { "aria-label":
|
|
4
|
+
export default function Breadcrumbs({ items, label = "Breadcrumb", }) {
|
|
5
|
+
return (_jsx("nav", { "aria-label": label, className: ccx("breadcrumbs mono"), children: _jsx("ol", { children: items.map((item, index) => {
|
|
5
6
|
const current = index === items.length - 1;
|
|
6
7
|
return (_jsx("li", { children: current || !item.href ? (_jsx("span", { "aria-current": current ? "page" : undefined, children: item.label })) : (_jsx("a", { href: safeHref(item.href), children: item.label })) }, item.label));
|
|
7
8
|
}) }) }));
|
|
@@ -8,6 +8,7 @@ export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
|
8
8
|
icon?: ReactNode;
|
|
9
9
|
leadingIcon?: ReactNode;
|
|
10
10
|
loading?: boolean;
|
|
11
|
+
loadingLabel?: string;
|
|
11
12
|
};
|
|
12
13
|
declare const Button: import("react").ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
13
14
|
variant?: ButtonVariant;
|
|
@@ -16,5 +17,6 @@ declare const Button: import("react").ForwardRefExoticComponent<ButtonHTMLAttrib
|
|
|
16
17
|
icon?: ReactNode;
|
|
17
18
|
leadingIcon?: ReactNode;
|
|
18
19
|
loading?: boolean;
|
|
20
|
+
loadingLabel?: string;
|
|
19
21
|
} & import("react").RefAttributes<HTMLButtonElement>>;
|
|
20
22
|
export default Button;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ccx } from "../../lib/classNames.js";
|
|
3
4
|
/** Pill button in four emphasis levels and three sizes, with loading and icon slots. */
|
|
4
5
|
import { forwardRef } from "react";
|
|
5
6
|
import { buttonClassName, } from "../../lib/classNames.js";
|
|
6
7
|
import Spinner from "./Spinner.js";
|
|
7
|
-
const Button = forwardRef(function Button({ variant, size, icon, leadingIcon, loading = false, disabled, className, children, type = "button", ...rest }, ref) {
|
|
8
|
-
return (_jsxs("button", { ref: ref, ...rest, type: type, className: buttonClassName(variant, size, className), disabled: disabled || loading, "aria-busy": loading || undefined, children: [loading ? _jsx(Spinner, { size: "sm", label:
|
|
8
|
+
const Button = forwardRef(function Button({ variant, size, icon, leadingIcon, loading = false, loadingLabel = "Loading", disabled, className, children, type = "button", ...rest }, ref) {
|
|
9
|
+
return (_jsxs("button", { ref: ref, ...rest, type: type, className: buttonClassName(variant, size, className), disabled: disabled || loading, "aria-busy": loading || undefined, children: [loading ? _jsx(Spinner, { size: "sm", label: loadingLabel }) : leadingIcon, _jsx("span", { children: children }), icon && _jsx("span", { className: ccx("button-icon"), children: icon })] }));
|
|
9
10
|
});
|
|
10
11
|
export default Button;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { ccx } from "../../lib/classNames.js";
|
|
3
3
|
export default function Card({ title, children, eyebrow, footer, icon, tone = "surface", interactive = false, }) {
|
|
4
|
-
return (_jsxs("article", { className:
|
|
4
|
+
return (_jsxs("article", { className: ccx("card", `card-${tone}`, interactive && "card-interactive"), children: [(eyebrow || icon) && (_jsxs("div", { className: ccx("card-top mono"), children: [eyebrow && _jsx("span", { children: eyebrow }), icon && _jsx("span", { className: ccx("card-icon"), children: icon })] })), _jsx("h3", { className: ccx("card-title"), children: title }), children && _jsx("div", { className: ccx("card-body"), children: children }), footer && _jsx("div", { className: ccx("card-footer"), children: footer })] }));
|
|
5
5
|
}
|
|
@@ -5,7 +5,7 @@ import { forwardRef, useEffect, useId } from "react";
|
|
|
5
5
|
import { Check, Minus } from "lucide-react";
|
|
6
6
|
import useControlRef from "../../hooks/useControlRef.js";
|
|
7
7
|
import useFormReset from "../../hooks/useFormReset.js";
|
|
8
|
-
import {
|
|
8
|
+
import { ccx } from "../../lib/classNames.js";
|
|
9
9
|
const Checkbox = forwardRef(function Checkbox({ label, description, error, indeterminate = false, className, id: providedId, ...rest }, forwardedRef) {
|
|
10
10
|
const generatedId = useId();
|
|
11
11
|
const id = providedId ?? generatedId;
|
|
@@ -21,12 +21,12 @@ const Checkbox = forwardRef(function Checkbox({ label, description, error, indet
|
|
|
21
21
|
ref.current.checked = rest.checked;
|
|
22
22
|
ref.current.indeterminate = indeterminate;
|
|
23
23
|
});
|
|
24
|
-
return (_jsxs("div", { className:
|
|
24
|
+
return (_jsxs("div", { className: ccx("choice", error && "choice-invalid", className), children: [_jsxs("span", { className: ccx("choice-control"), children: [_jsx("input", { ...rest, ref: attach, id: id, type: "checkbox", className: ccx("choice-input"), "aria-invalid": error ? true : rest["aria-invalid"], "aria-describedby": [
|
|
25
25
|
rest["aria-describedby"],
|
|
26
26
|
description && `${id}-description`,
|
|
27
27
|
error && `${id}-error`,
|
|
28
28
|
]
|
|
29
29
|
.filter(Boolean)
|
|
30
|
-
.join(" ") || undefined }), _jsx("span", { className: "choice-box", "aria-hidden": "true", children: indeterminate ? (_jsx(Minus, { size: 13, strokeWidth: 3 })) : (_jsx(Check, { size: 13, strokeWidth: 3 })) })] }), _jsxs("label", { htmlFor: id, className: "choice-text", children: [_jsx("span", { className: "choice-label", children: label }), description && (_jsx("span", { id: `${id}-description`, className: "choice-description", children: description })), error && (_jsx("span", { id: `${id}-error`, className: "choice-error mono", children: error }))] })] }));
|
|
30
|
+
.join(" ") || undefined }), _jsx("span", { className: ccx("choice-box"), "aria-hidden": "true", children: indeterminate ? (_jsx(Minus, { size: 13, strokeWidth: 3 })) : (_jsx(Check, { size: 13, strokeWidth: 3 })) })] }), _jsxs("label", { htmlFor: id, className: ccx("choice-text"), children: [_jsx("span", { className: ccx("choice-label"), children: label }), description && (_jsx("span", { id: `${id}-description`, className: ccx("choice-description"), children: description })), error && (_jsx("span", { id: `${id}-error`, className: ccx("choice-error mono"), children: error }))] })] }));
|
|
31
31
|
});
|
|
32
32
|
export default Checkbox;
|