@abuzareal/contour-ui 0.2.1 → 0.4.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 +28 -0
- package/COMPOSITION.md +193 -0
- package/CONTRACTS.md +25 -25
- package/FORMS.md +94 -0
- package/NEXTJS.md +5 -1
- package/PUBLIC-API.md +176 -174
- package/README.md +11 -9
- package/dist/components/ui/Button.d.ts +10 -2
- package/dist/components/ui/Button.js +7 -3
- package/dist/components/ui/Checkbox.d.ts +8 -2
- package/dist/components/ui/Checkbox.js +18 -7
- package/dist/components/ui/DepthButton.d.ts +5 -2
- package/dist/components/ui/DepthButton.js +8 -3
- package/dist/components/ui/Dialog.d.ts +21 -3
- package/dist/components/ui/Dialog.js +17 -11
- package/dist/components/ui/ExternalLink.d.ts +3 -2
- package/dist/components/ui/ExternalLink.js +7 -3
- package/dist/components/ui/IconButton.d.ts +10 -2
- package/dist/components/ui/IconButton.js +7 -3
- package/dist/components/ui/Popover.d.ts +19 -4
- package/dist/components/ui/Popover.js +16 -9
- package/dist/components/ui/RadioGroup.d.ts +13 -5
- package/dist/components/ui/RadioGroup.js +35 -8
- package/dist/components/ui/SelectField.js +16 -2
- package/dist/components/ui/Slider.d.ts +24 -7
- package/dist/components/ui/Slider.js +45 -7
- package/dist/components/ui/Switch.d.ts +26 -5
- package/dist/components/ui/Switch.js +54 -6
- package/dist/components/ui/Tabs.d.ts +16 -4
- package/dist/components/ui/Tabs.js +46 -24
- package/dist/components/ui/TextArea.js +2 -0
- package/dist/components/ui/TextField.js +5 -1
- package/dist/components/ui/ThemeToggle.d.ts +2 -1
- package/dist/components/ui/ThemeToggle.js +5 -3
- package/dist/hooks/useControlRef.js +2 -2
- package/dist/hooks/useFormReset.d.ts +1 -1
- package/dist/hooks/useTabRefs.d.ts +4 -0
- package/dist/hooks/useTabRefs.js +12 -0
- package/dist/lib/componentProps.d.ts +5 -0
- package/dist/lib/componentProps.js +1 -0
- package/dist/lib/composeEventHandlers.d.ts +4 -0
- package/dist/lib/composeEventHandlers.js +8 -0
- package/dist/lib/composeRefs.d.ts +3 -0
- package/dist/lib/composeRefs.js +24 -0
- package/examples/forms/HookFormSettingsForm.tsx +176 -0
- package/examples/forms/ManualSettingsForm-0.3.0.tsx +102 -0
- package/examples/forms/NativeSettingsForm.tsx +119 -0
- package/examples/forms/settings.ts +61 -0
- package/examples/forms/useSubmission.ts +77 -0
- package/package.json +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@ Changes to the published `@abuzareal/contour-ui` package. Version headings refer
|
|
|
6
6
|
|
|
7
7
|
No changes recorded.
|
|
8
8
|
|
|
9
|
+
## 0.4.0
|
|
10
|
+
|
|
11
|
+
Native forms and tested application recipes. This heading records source work, not npm publication.
|
|
12
|
+
|
|
13
|
+
- Switch supports name/form/value, defaultChecked and native required validation through a checkbox backing; checked submits, unchecked/disabled omit. The native ref remains a button and onChange remains boolean.
|
|
14
|
+
- Slider supports native name/form/defaultValue, reset, input attributes and formatted native sanitized values; onChange remains numeric. RadioGroup supports caller name/form/defaultValue, required/disabled and per-option native input props/refs; onChange remains a string value.
|
|
15
|
+
- Native reset and cancellation work with external owners. Controlled TextField/TextArea/SelectField/Checkbox DOM values now remain aligned with their parent state after native reset, including the select-first-option regression. Controlled values remain parent-owned; defaults for these controls are captured on mount. Checkbox accepts caller IDs and preserves descriptions/validity/indeterminate/ref behavior.
|
|
16
|
+
- Ship version-qualified 0.3.0 manual submission guidance and copyable native/React Hook Form recipes. Tested field/service errors, real pending/success states, retry, duplicate-submit prevention, reset/cancel/unmount and stale-result cleanup. RHF is example-only; no new runtime dependency, CSS/theme change or overlay rewrite.
|
|
17
|
+
|
|
18
|
+
Existing controlled JSX calls remain compatible. Add names only when native submission is intended; unchecked Switch values are omitted rather than serialized as false. Use Controller for value callbacks; native registered fields retain native events. See [FORMS.md](FORMS.md) for copyable sources, reset/registration details and service responsibilities. Independent consumer updates require an exact version/lockfile change and validation.
|
|
19
|
+
|
|
20
|
+
## 0.3.0
|
|
21
|
+
|
|
22
|
+
Native refs and focused composition APIs for existing components. This heading records source work, not npm publication.
|
|
23
|
+
|
|
24
|
+
- Button/IconButton/DepthButton/ThemeToggle, ExternalLink, Checkbox, Slider and Switch forward their native targets on React 18/19. Existing native field refs remain; callback refs stay stable across ordinary prop updates and support React 19 cleanup.
|
|
25
|
+
- Dialog, Tabs and Popover accept native root props and typed props/refs for existing parts. Custom IDs preserve relationships; internal state/ARIA/classes remain intact. Caller handlers can prevent default selection/toggle/dismissal, while native dialog cancel remains prop-controlled.
|
|
26
|
+
- Tabs retain ID-based focus/refs on reorder and remeasure the underline when root/list customization changes. Existing controlled/default state, menu disabled items/typeahead/stable IDs and focus return remain supported.
|
|
27
|
+
- Publish native anchor and pinned Next Link recipes, preserving ordinary/canceled/middle-click navigation without nesting interactive elements or adding Button polymorphism. No runtime dependency or CSS/theme/form contract change is introduced.
|
|
28
|
+
|
|
29
|
+
Existing JSX calls remain valid. Render components through React; newly ref-enabled exports are forwardRef components rather than plain callable functions. Named Switch/Slider/RadioGroup submission, overlay positioning/nesting corrections and CSS isolation remain separate work. See [COMPOSITION.md](COMPOSITION.md).
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install @abuzareal/contour-ui@0.3.0 --save-exact
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use that command after registry availability is verified; update the exact pin/lockfile and validate your app.
|
|
36
|
+
|
|
9
37
|
## 0.2.1
|
|
10
38
|
|
|
11
39
|
A baseline contract release with compatible Checkbox ARIA and clipboard lifecycle fixes. Export names, prop types, visual defaults, dependencies and optional peer boundaries are unchanged from 0.2.0.
|
package/COMPOSITION.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Refs, parts, state and links
|
|
2
|
+
|
|
3
|
+
Contract version **0.4.0**. These additions are absent from npm 0.2.1; a source heading does not establish publication. Use the version named by the release notes. Import components directly and load CSS once. This guide describes existing DOM parts; no copying step or polymorphic Button system is required.
|
|
4
|
+
|
|
5
|
+
## Native ref targets
|
|
6
|
+
|
|
7
|
+
| Export | Target and use |
|
|
8
|
+
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| Button, IconButton, DepthButton, ThemeToggle, Switch | HTMLButtonElement: focus, measurement, external trigger adapter |
|
|
10
|
+
| ExternalLink | HTMLAnchorElement: focus and native-link integration |
|
|
11
|
+
| TextField, Checkbox, Slider | HTMLInputElement: focus/measurement; Checkbox retains indeterminate behavior |
|
|
12
|
+
| TextArea / SelectField | HTMLTextAreaElement / HTMLSelectElement; existing native reset/validation preserved |
|
|
13
|
+
| Dialog | HTMLDialogElement: focus/measurement; open stays prop-owned, do not bypass it with showModal/close |
|
|
14
|
+
| Tabs | Root HTMLDivElement; list/button/panel parts have separate native refs |
|
|
15
|
+
| Popover | Root HTMLDivElement; triggerProps.ref is its owned button, panelProps.ref its local panel |
|
|
16
|
+
|
|
17
|
+
Object refs become null on unmount/ref replacement. Callback refs receive the node then null; on React 19 they may return cleanup instead. Internal refs coexist with caller refs. Stable callback identities avoid deliberate ref replacement. Targets remain mounted while a dialog/panel is closed/hidden.
|
|
18
|
+
|
|
19
|
+
RadioGroup, SegmentedControl, Pagination and DropdownMenu contain multiple controls; no singular input/action ref is claimed. RadioGroup now exposes per-option native input refs through inputProps for form controllers. Field is a frame, Tooltip describes a caller-owned child, Disclosure has native details semantics, and FlipCard/Tag have conditional controls. Presentation/scene wrappers receive no arbitrary native control handle. Part refs for those require a demonstrated reusable need.
|
|
20
|
+
|
|
21
|
+
React 18 compatibility uses forwardRef; React 19 uses the same JSX API. Render through React; do not call exports as plain functions or depend on typeof being function. [React ref guidance](https://react.dev/reference/react/forwardRef) explains why low-level controls expose DOM nodes.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { useRef } from "react";
|
|
25
|
+
import { Button, TextField } from "@abuzareal/contour-ui";
|
|
26
|
+
export function FocusName() {
|
|
27
|
+
const action = useRef<HTMLButtonElement>(null);
|
|
28
|
+
const input = useRef<HTMLInputElement>(null);
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<TextField ref={input} label="Name" name="name" />
|
|
32
|
+
<Button ref={action} onClick={() => input.current?.focus()}>
|
|
33
|
+
Focus name
|
|
34
|
+
</Button>
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
JavaScript uses the same imports/JSX and `useRef(null)` without type arguments.
|
|
41
|
+
|
|
42
|
+
## Root and part customization
|
|
43
|
+
|
|
44
|
+
Root className/style/id/data/ARIA and supported native handlers target the documented native root. Classes append to defaults. Part props customize existing DOM; children/raw markup/replacement components stay owned by Contour. CSS stays global; this API adds no isolation, scoped theme, portal or collision engine.
|
|
45
|
+
|
|
46
|
+
| API | DOM target |
|
|
47
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
48
|
+
| Dialog root/ref | dialog; title is its visible heading, onClose remains a no-argument request |
|
|
49
|
+
| Dialog panelProps/bodyProps/footerProps | div.dialog-panel / div.dialog-body / footer.dialog-footer; footer only mounts with content |
|
|
50
|
+
| Dialog closeButtonProps | Native close button; protected type=button, default label Close dialog |
|
|
51
|
+
| Tabs root/ref / listProps | Outer div.tabs / div with tablist role and state-owned indicator variables |
|
|
52
|
+
| TabItem.buttonProps/panelProps | Tab button / div tabpanel, each with native ref/id/class/style/events/data/ARIA |
|
|
53
|
+
| Popover root/ref / triggerProps/panelProps | Outer div / owned trigger button / local nonmodal panel |
|
|
54
|
+
|
|
55
|
+
Roles, button types, tabIndex, selected/expanded/hidden state and ARIA relationships stay component-owned. Custom tab/panel IDs update both sides of their relationships. Extra label/description IDs compose with required IDs; callers supply referenced nodes. Native styles/classes can still make a composition unusable. Disabled-tab semantics are not introduced by buttonProps.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { useRef, useState } from "react";
|
|
59
|
+
import { Button, Dialog, Popover, Tabs } from "@abuzareal/contour-ui";
|
|
60
|
+
export function SettingsParts() {
|
|
61
|
+
const [open, setOpen] = useState(false);
|
|
62
|
+
const [activeTabId, setActiveTabId] = useState("profile");
|
|
63
|
+
const dialog = useRef<HTMLDialogElement>(null);
|
|
64
|
+
const body = useRef<HTMLDivElement>(null);
|
|
65
|
+
const help = useRef<HTMLButtonElement>(null);
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<Button onClick={() => setOpen(true)}>Open settings</Button>
|
|
69
|
+
<Dialog
|
|
70
|
+
ref={dialog}
|
|
71
|
+
id="settings"
|
|
72
|
+
className="account-dialog"
|
|
73
|
+
open={open}
|
|
74
|
+
onClose={() => setOpen(false)}
|
|
75
|
+
title="Settings"
|
|
76
|
+
bodyProps={{ ref: body, "data-owner": "account" }}
|
|
77
|
+
closeButtonProps={{ "aria-label": "Close settings" }}
|
|
78
|
+
>
|
|
79
|
+
<Tabs
|
|
80
|
+
label="Settings sections"
|
|
81
|
+
activeTabId={activeTabId}
|
|
82
|
+
onTabChange={setActiveTabId}
|
|
83
|
+
listProps={{ className: "account-tabs" }}
|
|
84
|
+
tabs={[
|
|
85
|
+
{
|
|
86
|
+
id: "profile",
|
|
87
|
+
label: "Profile",
|
|
88
|
+
content: <p>Profile content</p>,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "preferences",
|
|
92
|
+
label: "Preferences",
|
|
93
|
+
content: <p>Preferences content</p>,
|
|
94
|
+
},
|
|
95
|
+
]}
|
|
96
|
+
/>
|
|
97
|
+
<Popover
|
|
98
|
+
trigger="Help"
|
|
99
|
+
title="Settings help"
|
|
100
|
+
triggerProps={{ ref: help }}
|
|
101
|
+
panelProps={{ id: "settings-help", className: "account-help" }}
|
|
102
|
+
>
|
|
103
|
+
<p>Local panels need room in their layout.</p>
|
|
104
|
+
</Popover>
|
|
105
|
+
</Dialog>
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Popover still has local absolute placement and the known nested-dismissal limit. trigger is content inside an owned button, not an arbitrary interactive replacement. No submenu/initial-focus API is implied. Overlay corrections remain separate.
|
|
112
|
+
|
|
113
|
+
## Event composition
|
|
114
|
+
|
|
115
|
+
For the composed action handlers on Dialog, Tabs, Popover and the Switch button, the caller handler on the same owned element runs first. preventDefault vetoes internal tab click/arrow selection, popover/switch toggling, and dialog close-button/backdrop dismissal. stopPropagation controls propagation, not this cancellation contract. Root bubbling handlers run after descendant handlers; intercept through the relevant part or capture phase.
|
|
116
|
+
|
|
117
|
+
Dialog onCancel receives the native event. Caller preventDefault vetoes onClose; Contour always prevents the browser from closing independently of open. An allowed cancel requests onClose and the parent must update open. Native disabled buttons cannot activate; Button loading also disables it. Native field counter/resize bookkeeping runs before its caller input/change handler and remains intact; preventDefault on those notifications is not a counter-reset API.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// Fragment: the application owns open, saving and its operation/recovery.
|
|
121
|
+
<Dialog
|
|
122
|
+
open={open}
|
|
123
|
+
title="Unsaved changes"
|
|
124
|
+
onClose={() => setOpen(false)}
|
|
125
|
+
onCancel={(event) => {
|
|
126
|
+
if (saving) event.preventDefault();
|
|
127
|
+
}}
|
|
128
|
+
closeButtonProps={{ disabled: saving }}
|
|
129
|
+
>
|
|
130
|
+
<p>Finish the current operation before closing.</p>
|
|
131
|
+
</Dialog>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
If dismissal is suppressed, provide a usable cancel/recovery path. Do not disable essential interaction indefinitely.
|
|
135
|
+
|
|
136
|
+
## State and callback audit
|
|
137
|
+
|
|
138
|
+
| Family | Existing ownership/callback contract |
|
|
139
|
+
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| Button/IconButton/DepthButton/ExternalLink | Native events; supported button disabled/loading; navigation remains browser/router-owned |
|
|
141
|
+
| TextField/TextArea/SelectField/Checkbox | Native event callbacks; controlled value/checked or native defaults; refs, validity/reset remain intact |
|
|
142
|
+
| Slider / Switch | Number value / boolean checked or captured default; value callbacks; named form/reset support in 0.4.0 |
|
|
143
|
+
| RadioGroup / SegmentedControl | RadioGroup has controlled/default string selection and native form names; SegmentedControl remains a required value/button group |
|
|
144
|
+
| Tabs | activeTabId + onTabChange(id), or internal defaultTabId; invalid/removed IDs fall back to first available |
|
|
145
|
+
| Popover / DropdownMenu | open + onOpenChange(boolean), or internal defaultOpen; unchanged values do not notify |
|
|
146
|
+
| Dialog | Required open and onClose() request; no independent imperative ownership |
|
|
147
|
+
| Disclosure | defaultOpen on native details; no controlled open/onOpenChange |
|
|
148
|
+
| Pagination | page/pageCount and onChange(page); app owns URL/fetch/bounds |
|
|
149
|
+
| FlipCard / Tooltip | Internal flipped / hover-focus state; no new controlled API |
|
|
150
|
+
| ThemeToggle/useTheme | Shared document theme/preference, not scoped provider |
|
|
151
|
+
| Tag, EmptyState, Alert, toast APIs | Caller removal/actions or supplied feedback/library toast commands; no business operation/persistence implied |
|
|
152
|
+
|
|
153
|
+
Do not switch controlled/uncontrolled ownership mid-mount. A later default does not reset established state. Controlled callbacks never silently overwrite parent state. Native event callbacks are not cosmetically renamed into value callbacks. See [the form guide](FORMS.md) for 0.4.0 named submission, defaults, validation and reset. SegmentedControl remains application-state submission.
|
|
154
|
+
|
|
155
|
+
## Native and router links
|
|
156
|
+
|
|
157
|
+
Use Button for actions and an anchor/router Link for navigation. buttonClassName supplies existing styles without interception. Preserve modified clicks, context menus, open-in-new-tab, destinations and keyboard activation. Do not nest interactive Buttons and anchors.
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { buttonClassName } from "@abuzareal/contour-ui";
|
|
161
|
+
export function ReportsLink() {
|
|
162
|
+
return (
|
|
163
|
+
<a href="/reports" className={buttonClassName("secondary")}>
|
|
164
|
+
Open reports
|
|
165
|
+
</a>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The pinned Next App Router configuration renders a native anchor and owns client navigation. Put interactive handlers in a client boundary. This does not claim universal router support.
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
"use client";
|
|
174
|
+
import { useRef } from "react";
|
|
175
|
+
import Link from "next/link";
|
|
176
|
+
import { buttonClassName } from "@abuzareal/contour-ui";
|
|
177
|
+
export function NextReportsLink() {
|
|
178
|
+
const link = useRef<HTMLAnchorElement>(null);
|
|
179
|
+
return (
|
|
180
|
+
<Link ref={link} href="/reports" className={buttonClassName("secondary")}>
|
|
181
|
+
Open reports
|
|
182
|
+
</Link>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Other routers can use the same recipe when their documented Link forwards standard anchor props/ref. Browser checks cover native links and middle-click background tabs; the pinned Next fixture covers ordinary, canceled and middle-click navigation. [Next Link documentation](https://nextjs.org/docs/app/api-reference/components/link) distinguishes onClick from onNavigate: modified clicks retain native behavior.
|
|
188
|
+
|
|
189
|
+
Anchors have no native disabled state. Render a suitable non-link explanation when navigation is unavailable, or omit the destination with explicit disabled semantics and app-owned keyboard/pointer behavior. Use a disabled Button when it is an action. CSS alone does not disable a link. ExternalLink retains scheme filtering and new-tab opener protection; internal routes use the router Link.
|
|
190
|
+
|
|
191
|
+
## Abstraction review
|
|
192
|
+
|
|
193
|
+
Each addition serves a bounded need: native focus/measurement/trigger refs; existing Dialog/Tabs/Popover root/part access; ID-based reorder-safe tab focus; caller handler cancellation. Private helpers implement those lifecycles. No runtime dependency, provider, build/distribution/styling system, generic component-replacement API or blanket polymorphism is introduced. Existing menu disabled items, keyboard opening, typeahead, stable identity and focus return remain covered.
|
package/CONTRACTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Architecture, scope and compatibility
|
|
2
2
|
|
|
3
|
-
Contract version **0.
|
|
3
|
+
Contract version **0.4.0**, reviewed 13 October 2026. Contour is an early 0.x React library with typed controls, paper/ink surfaces, a lime accent, light/dark themes, restrained motion and separate optional depth/scene APIs. The [public API and maturity register](PUBLIC-API.md) lists every exported value, type and stylesheet, its maintainer, status, use and limits.
|
|
4
4
|
|
|
5
5
|
## Intended adoption scope
|
|
6
6
|
|
|
@@ -16,10 +16,10 @@ No promise of enterprise completeness, universal embedding, all locales/RTL, eve
|
|
|
16
16
|
|
|
17
17
|
## Package and application ownership
|
|
18
18
|
|
|
19
|
-
| Area
|
|
20
|
-
|
|
|
21
|
-
| Reusable controls, semantic tokens, shared theme/motion/browser lifecycle, public APIs and package releases
|
|
22
|
-
| Catalogue shell/chapters, executable documentation/source display, discovery, examples and site delivery
|
|
19
|
+
| Area | Owner |
|
|
20
|
+
| ------------------------------------------------------------------------------------------------------------- | ---------------- |
|
|
21
|
+
| Reusable controls, semantic tokens, shared theme/motion/browser lifecycle, public APIs and package releases | contour-ui |
|
|
22
|
+
| Catalogue shell/chapters, executable documentation/source display, discovery, examples and site delivery | Design-System |
|
|
23
23
|
| Product layouts/routes, domain data/calculations, workflows, requests, validation and product-specific scenes | Each application |
|
|
24
24
|
|
|
25
25
|
Repositories remain independent. Contour source is public and MIT licensed; catalogue source is currently private while hosted documentation is public. The catalogue's `private: true` prevents npm publication; it is separate from GitHub visibility. Visibility, history/assets/licensing review and CI runner policy are separate decisions.
|
|
@@ -30,29 +30,29 @@ Catalogue UI/hook/scene compatibility modules intentionally forward public packa
|
|
|
30
30
|
|
|
31
31
|
React is the UI runtime. Strict TypeScript/TSX source compiles through `tsc` into ES2022 ESM modules and declarations. The build copies CSS and selected licensed Latin font assets. It does not bundle React or compile the library into a Vite app. Vite serves development/consumer fixtures; Design-System is the Vite application. Consumers can write JavaScript or TypeScript.
|
|
32
32
|
|
|
33
|
-
| Category
|
|
34
|
-
|
|
|
35
|
-
| Runtime dependencies
|
|
36
|
-
| Required peers
|
|
37
|
-
| Optional peers
|
|
38
|
-
| Development dependencies | TypeScript, Vite, Oxlint, Testing Library, jsdom, Playwright, React/types and optional peer dev copies | Source authoring/tests/build; not downstream Contour production dependencies.
|
|
33
|
+
| Category | Current packages | Consumer consequence |
|
|
34
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| Runtime dependencies | lucide-react; @fontsource-variable/dm-sans; @fontsource-variable/space-grotesk; @fontsource/dm-mono | Install with Contour. Selected imports determine browser output. Font CSS is optional; font installation currently is not. |
|
|
36
|
+
| Required peers | react/react-dom: React 18 (`^18.3.1`) or React 19 (`^19.0.0`) | App supplies a matching pair; allowed range is broader than individually tested versions. |
|
|
37
|
+
| Optional peers | three: `^0.186.1`; lenis: `^1.3.26` | Core works without either. Install explicitly for the matching optional entry. |
|
|
38
|
+
| Development dependencies | TypeScript, Vite, Oxlint, Testing Library, jsdom, Playwright, React/types and optional peer dev copies | Source authoring/tests/build; not downstream Contour production dependencies. |
|
|
39
39
|
|
|
40
|
-
Use explicit public paths; deep imports into dist internals are unsupported. CSS is marked as side-effectful to retain imported styles. The archive contains built modules/declarations/assets and release documents, with no consumer install scripts or private research.
|
|
40
|
+
Use explicit public paths; deep imports into dist internals are unsupported. CSS is marked as side-effectful to retain imported styles. The archive contains built modules/declarations/assets and release documents, plus copyable form recipe sources, with no consumer install scripts or private research. React Hook Form is an example/application dependency, not a runtime dependency.
|
|
41
41
|
|
|
42
42
|
Archive bytes, installed dependency bytes, browser JS/CSS, emitted fonts and actual requested/cached network assets are different measurements. An installed icon collection is not proof all icons download. JavaScript tree shaking does not remove unused aggregate CSS rules automatically. Size claims require version, fixture, tool versions, compression and exclusions; independent component gzip samples cannot be added as an application total.
|
|
43
43
|
|
|
44
44
|
## Environment and validation policy
|
|
45
45
|
|
|
46
|
-
| Environment
|
|
47
|
-
|
|
|
48
|
-
| React/ReactDOM
|
|
49
|
-
| TypeScript
|
|
50
|
-
| Node
|
|
51
|
-
| ESM/CSS-capable bundler | Packed Vite consumers exercise imports/declarations/CSS/fonts with no optional peers
|
|
52
|
-
| Next.js
|
|
53
|
-
| Optional Three
|
|
54
|
-
| Optional scroll
|
|
55
|
-
| Browsers
|
|
46
|
+
| Environment | Declared or exercised scope | Limits |
|
|
47
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
48
|
+
| React/ReactDOM | Peer range above; packed matrix exercises 18.3.1, 19.0.0 and 19.3.0 | Matching pairs required. Other allowed patches are not individually certified. |
|
|
49
|
+
| TypeScript | Strict source/fixtures; pinned optional/Next fixtures use 5.9.3; React 18 and 19 declarations compiled | No universal older/newer compiler support claim; JS users need no compiler at runtime. |
|
|
50
|
+
| Node | Manifest >=22 for installation/tooling; use 22.12+ or 24+ with current Vite tooling | Browser components do not need Node in the browser. Actual runner versions are recorded per check. |
|
|
51
|
+
| ESM/CSS-capable bundler | Packed Vite consumers exercise imports/declarations/CSS/fonts with no optional peers | No CommonJS build or unbundled-browser CSS loader promised. |
|
|
52
|
+
| Next.js | Next 16.4.0 / React and ReactDOM 19.3.0, App Router, webpack production build | [NEXTJS.md](NEXTJS.md); other versions, Turbopack, Pages Router and Next Lenis remain unverified. |
|
|
53
|
+
| Optional Three | Separate consumers and lazy Next mode; explicit Three/types, WebGL, sized host and static fallback | Experimental API; sampled lifecycle checks do not validate every custom scene/GPU/device. |
|
|
54
|
+
| Optional scroll | Separate declaration/build consumer with Lenis and one scroll owner | Caller handles pause/reduced motion; generic Next Lenis integration unverified. |
|
|
55
|
+
| Browsers | Playwright Chromium exercises selected native-dialog/reset/graphics/hydration/catalogue journeys | Emulation is not a physical device. Chromium/axe is not Firefox/WebKit/Safari or manual VoiceOver/NVDA verification. |
|
|
56
56
|
|
|
57
57
|
Release gates use one exact archive: types/lint/build, Node/component tests, native-dialog/graphics browser checks, exports/archive inspection, React matrix, optional consumers, pinned Next core/lazy-Three modes and dependency audit. Catalogue has separate required checks/affected journeys. Record checksum, actual environments and passed/failed/skipped/blocked results. Historical results are dated evidence, not a fresh pass; see [RELEASING.md](RELEASING.md).
|
|
58
58
|
|
|
@@ -70,11 +70,11 @@ Respect system reduced motion and app pause. CSS depth is core; WebGL is separat
|
|
|
70
70
|
|
|
71
71
|
Preserve native input/select/details/dialog behavior where appropriate. Future replacements require a written rationale and equivalent labels/descriptions, submission/disabled omission, validation/reset timing, keyboard/focus, modal scroll locking and cleanup, with relevant component/browser checks.
|
|
72
72
|
|
|
73
|
-
-
|
|
74
|
-
- Checkbox
|
|
73
|
+
- Actions, ExternalLink, native fields/choices, Dialog and the documented Tabs/Popover parts forward their native targets on React 18/19. Multi-control/presentation wrappers do not imply a singular input ref; see [the composition contract](COMPOSITION.md).
|
|
74
|
+
- Checkbox accepts caller IDs while retaining a generated fallback and internal indeterminate behavior alongside the forwarded input ref; caller ARIA validity and descriptions remain composed. Switch/Slider/RadioGroup support named/external form submission and controlled/default/reset behavior; see [the form guide](FORMS.md). Switch refs remain buttons and its checkbox backing submits only while checked; a range has no required constraint.
|
|
75
75
|
- Popover/DropdownMenu use local absolute panels without portal/collision/submenu/custom-trigger contracts. Independent document listeners close both nested Popovers on one Escape: a reproduced defect.
|
|
76
76
|
- Tooltip closes on pointer leave and CSS prevents content hover: a source finding/accessibility risk requiring correction and pointer/focus verification, not a formal conformance audit. Hints must be supplemental and noninteractive.
|
|
77
|
-
- Dialog/Tabs/Popover
|
|
77
|
+
- Dialog/Tabs/Popover expose typed root and owned-part props/refs; state and ARIA wiring stay protected and caller cancellation composes with internal behavior. Arbitrary trigger replacement, portals/collision and general polymorphism remain outside this API.
|
|
78
78
|
- Disclosure is native details with defaultOpen and required className, not controlled open/onOpenChange. Controlled open state belongs to Popover/DropdownMenu.
|
|
79
79
|
- BrandMark/editorial section/heading helpers and selected navigation/print hooks retain inherited page assumptions. They remain supported legacy APIs, not universal product primitives.
|
|
80
80
|
|
package/FORMS.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Native form and form-library integration
|
|
2
|
+
|
|
3
|
+
Contract version **0.4.0**. This source heading does not establish npm publication; new contracts are absent from 0.3.0. Use the version named by the release notes. Existing controlled boolean/number/string callbacks and native ref targets remain compatible.
|
|
4
|
+
|
|
5
|
+
## Existing 0.3.0 installations
|
|
6
|
+
|
|
7
|
+
TextField, TextArea, SelectField and Checkbox accept native name/form/disabled/default/required props and can be read through FormData. Checkbox generates its own ID in 0.3.0. Switch and Slider in 0.3.0 require checked/value and value callbacks; they do not accept a submission name or external form contract. RadioGroup in 0.3.0 uses its own generated radio name; applications should serialize its selected value under their own stable field name. Construct a payload from native fields plus application state rather than guessing the generated name. Keep parent-owned reset explicit.
|
|
8
|
+
|
|
9
|
+
[ManualSettingsForm-0.3.0.tsx](examples/forms/ManualSettingsForm-0.3.0.tsx) is a complete old-release recipe. It is compiled against the actual npm 0.3.0 package. It deliberately whitelists the service payload; generated radio names are not sent to the service.
|
|
10
|
+
|
|
11
|
+
## 0.4.0 control contracts
|
|
12
|
+
|
|
13
|
+
| Control | Submission and defaults | Ownership, validation and ref |
|
|
14
|
+
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| Switch | name + optional form; checked submits name=value (default "on"); unchecked/disabled omits it. defaultChecked defaults to false. | checked controls state; absent checked uses internal state. onChange(boolean), not a native input event. Ref remains HTMLButtonElement. Native checkbox backing is noninteractive; required means checked, invalid feedback focuses the visible switch and supplies a linked message. onInvalid receives that checkbox's native event; preventDefault supplies your own feedback/focus. |
|
|
16
|
+
| Slider | Native range name/form; numeric value is serialized as a string. defaultValue defaults to the native range midpoint. Disabled omits it. | value controls state; absent value uses native uncontrolled input. onChange(number) remains a value callback. Ref is HTMLInputElement. Browser range clamping/stepping governs value and formatted readout. required is not supported because a native range cannot be empty. |
|
|
17
|
+
| RadioGroup | Caller name/form on every radio; selected enabled option submits its string value. Without name the generated name remains compatible but is unsuitable for a service field. Without defaultValue/value no option is selected. | value controls selection; absent value is native uncontrolled selection. onChange(value) stays a string callback. Group disabled and option disabled omit affected radios; required uses native radio validation. No singular group input ref: option.inputProps.ref targets that native radio. |
|
|
18
|
+
| TextField/TextArea/SelectField/Checkbox | Native names/owners/defaults/disabled/required and native event callbacks remain. Checkbox now accepts caller id. | Native input/textarea/select refs, explicit labels, caller ARIA descriptions/validity plus component errors are preserved. Text counters support maxLength=0; textarea auto-resize follows reset. |
|
|
19
|
+
|
|
20
|
+
Set unique IDs and stable radio option values. Equal names in the same form intentionally form one native radio group; unrelated groups need distinct names. Radio option inputProps allows ref/id/blur/focus/style/ARIA/native handlers; type/name/form/value/checked/default/required/disabled/selection remain group-owned. Extra description IDs must point to nodes you provide. Switch change notification is its boolean callback; do not rely on delegated native form onChange to observe its noninteractive backing checkbox. Switch/Slider native props target their visible native control, except Switch name/value/required belong to its backing input; existing styles/classes are retained. Button type and switch/range semantics remain owned.
|
|
21
|
+
|
|
22
|
+
Custom defaults for these three controls are captured on mount. Changing a default prop does not reset current state or replace the captured baseline; remount or use controlled values to replace that baseline. Do not switch ownership mid-mount. Supplying a controlled value wins over an initial default. Numeric props should be finite; native range sanitization is not service/domain validation. Missing selections in an enabled group use native required validation. Disabled groups/options omit values; validate availability and missing fields in the application/service.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { Slider, Switch, RadioGroup } from "@abuzareal/contour-ui";
|
|
26
|
+
export function PreferencesFields() {
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<form id="preferences" />
|
|
30
|
+
<Switch
|
|
31
|
+
label="Updates"
|
|
32
|
+
name="updates"
|
|
33
|
+
form="preferences"
|
|
34
|
+
defaultChecked
|
|
35
|
+
/>
|
|
36
|
+
<Slider
|
|
37
|
+
label="Level"
|
|
38
|
+
name="level"
|
|
39
|
+
form="preferences"
|
|
40
|
+
defaultValue={20}
|
|
41
|
+
min={0}
|
|
42
|
+
max={100}
|
|
43
|
+
step={5}
|
|
44
|
+
/>
|
|
45
|
+
<RadioGroup
|
|
46
|
+
legend="Contact"
|
|
47
|
+
name="contact"
|
|
48
|
+
form="preferences"
|
|
49
|
+
defaultValue="email"
|
|
50
|
+
required
|
|
51
|
+
options={[
|
|
52
|
+
{ value: "email", label: "Email" },
|
|
53
|
+
{ value: "phone", label: "Phone" },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
</>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
JavaScript uses the same imports/JSX without type arguments. RadioGroup does not require a wrapper/provider. Native form owners also work when the controls are rendered outside the form and when a form prop changes. Disabled fieldsets retain native disabled omission.
|
|
62
|
+
|
|
63
|
+
## Reset and controlled responsibilities
|
|
64
|
+
|
|
65
|
+
Native reset fires before the browser restores defaults. Uncontrolled choices/range and supporting readouts/counters synchronize after that default action (a browser task). A canceled reset preserves state/readouts. Reset does not generate a change callback. Listener/timer cleanup uses the owner document and current form owner, including external forms and unmount.
|
|
66
|
+
|
|
67
|
+
For controlled values, the application must update its own state in onReset or a dedicated reset command. The library restores the controlled visible/submitted value after the native reset task and never changes the parent state. Do not read a just-reset controlled form synchronously and assume React has committed; await the state update/task. React Hook Form Controller values need useForm defaultValues and reset(defaults). A later default prop is not a controlled reset command. A submission cancel stops client waiting; it cannot prove the server rolled back a request already received.
|
|
68
|
+
|
|
69
|
+
## Copyable native recipe
|
|
70
|
+
|
|
71
|
+
Copy [NativeSettingsForm.tsx](examples/forms/NativeSettingsForm.tsx), [settings.ts](examples/forms/settings.ts) and [useSubmission.ts](examples/forms/useSubmission.ts) into your application. Their relative .js imports resolve to the TypeScript files under NodeNext; use your bundler's matching source-import convention. Load Contour CSS once and add application layout styles. Pass a real SaveSettings adapter; no service endpoint, delay or fabricated success is built into the recipe.
|
|
72
|
+
|
|
73
|
+
The recipe reads an exact payload through FormData, validates known fields, displays field/service failures, prevents a second submission while waiting for the current request, disables editing while pending, preserves inputs on failure, permits retry/stop-waiting/reset and aborts/ignores obsolete/unmounted results. Stopping waiting abandons client ownership; it does not guarantee server work has stopped. Success is displayed only when your adapter returns ok:true, and a new invalid submission replaces previous success feedback. Submission announcements stay outside the busy fieldset. The example email/step policy is intentionally small; it is not universal address validation. Accessibility/status/keyboard behaviors are verified in the fixture. Service error strings should be meaningful to the user and safe to display.
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { NativeSettingsForm } from "./NativeSettingsForm.js";
|
|
77
|
+
import type { SaveSettings } from "./settings.js";
|
|
78
|
+
// Your adapter validates unknown response data and confirms the server result.
|
|
79
|
+
export function PreferencesPage({ save }: { save: SaveSettings }) {
|
|
80
|
+
return <NativeSettingsForm save={save} />;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Do not substitute an always-resolving callback for a real service and imply data was persisted. The browser fixture uses an explicitly labeled local test service to verify pending/error/success, not a production backend.
|
|
85
|
+
|
|
86
|
+
## React Hook Form recipe
|
|
87
|
+
|
|
88
|
+
Copy [HookFormSettingsForm.tsx](examples/forms/HookFormSettingsForm.tsx) and the same shared files. Install react-hook-form in the **application**, not as a Contour peer. Fixtures pin 7.90.0 with React 18.3.1 and 19.3.0; other versions are separate compatibility questions.
|
|
89
|
+
|
|
90
|
+
Native TextField registration forwards name/onChange/onBlur/ref. RHF submits its field model rather than FormData; use RHF disabled field options when omission is required. A disabled fieldset prevents editing but is not a replacement for that model policy. Switch/Slider/RadioGroup use Controller because onChange carries a boolean/number/string. Switch still exposes a button ref; radio field.ref goes on an enabled option's inputProps.ref. Blur handlers go on the visible control or radio options. The shown RHF recipe uses field errors rather than shouldUseNativeValidation; the Switch button ref has no input validity methods. Native form validity still includes its backing checkbox. Service errors focus after the disabled pending fieldset becomes enabled. Do not register and Controller the same field or spread a radio group's single controller ref onto every option.
|
|
91
|
+
|
|
92
|
+
The recipe uses useForm defaultValues, RHF field validation/server errors, handleSubmit, controller refs and reset. Its reset command performs native form.reset() first so registered field counters/auto-resize observe the reset, then reset(defaults) updates RHF state. For arbitrary programmatic RHF setValue calls on fields with counters/auto-resize, use Controller and pass the controlled value; direct DOM writes through a registered ref do not dispatch native input events. Its shared submission lifecycle ignores late results after reset/cancel/unmount even if the service disregards AbortSignal. Real services must enforce validation, authentication, authorization, CSRF policy and idempotency/overlap handling at the server boundary. Treat parsed responses as unknown until your adapter validates them; TypeScript alone does not validate network data.
|
|
93
|
+
|
|
94
|
+
References: [React native input ownership](https://react.dev/reference/react-dom/components/input), [native range semantics](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/range), [RHF Controller](https://react-hook-form.com/docs/usecontroller/controller), [RHF reset](https://react-hook-form.com/docs/useform/reset), [Radix Switch form contract](https://www.radix-ui.com/primitives/docs/components/switch). These inform bounded contracts; Contour does not add a second interaction foundation or copy their implementation. Native-form checks are scoped Chromium evidence, not cross-browser/screen-reader certification.
|
package/NEXTJS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Next.js App Router integration
|
|
2
2
|
|
|
3
|
-
This guide describes **Contour 0.
|
|
3
|
+
This guide describes **Contour 0.4.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
4
|
|
|
5
5
|
In `app/layout.tsx`, load CSS once:
|
|
6
6
|
|
|
@@ -49,3 +49,7 @@ export default function SearchForm() {
|
|
|
49
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
50
|
|
|
51
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.
|
|
52
|
+
|
|
53
|
+
The 0.4.0 fixture also verifies Button native-ref focus and styled Next Link ordinary/canceled/middle-click navigation. Use [the composition guide](COMPOSITION.md#native-and-router-links) for the Link recipe; Button remains an action rather than a polymorphic navigation component.
|
|
54
|
+
|
|
55
|
+
Named form/reset APIs and copyable form recipes are described in [FORMS.md](FORMS.md). They remain client interactions; server-side validation and persistence belong to your application.
|