@abuzareal/contour-ui 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/COMPOSITION.md +28 -28
  3. package/CONTRACTS.md +9 -3
  4. package/FORMS.md +94 -0
  5. package/NEXTJS.md +6 -2
  6. package/OVERLAYS.md +52 -0
  7. package/PUBLIC-API.md +181 -162
  8. package/README.md +44 -28
  9. package/RELEASING.md +1 -1
  10. package/SIZE.md +45 -0
  11. package/STYLING.md +68 -0
  12. package/dist/actions.css +7 -0
  13. package/dist/components/hero/Sculpture.js +2 -1
  14. package/dist/components/three/ObjectStudy.js +2 -1
  15. package/dist/components/ui/Alert.d.ts +2 -1
  16. package/dist/components/ui/Alert.js +3 -3
  17. package/dist/components/ui/Avatar.d.ts +3 -1
  18. package/dist/components/ui/Avatar.js +3 -3
  19. package/dist/components/ui/Badge.js +2 -2
  20. package/dist/components/ui/BrandMark.js +2 -1
  21. package/dist/components/ui/Breadcrumbs.d.ts +2 -1
  22. package/dist/components/ui/Breadcrumbs.js +3 -2
  23. package/dist/components/ui/Button.d.ts +2 -0
  24. package/dist/components/ui/Button.js +3 -2
  25. package/dist/components/ui/Card.js +2 -2
  26. package/dist/components/ui/Checkbox.d.ts +2 -2
  27. package/dist/components/ui/Checkbox.js +14 -5
  28. package/dist/components/ui/CountUp.d.ts +2 -1
  29. package/dist/components/ui/CountUp.js +6 -3
  30. package/dist/components/ui/DataTable.d.ts +1 -0
  31. package/dist/components/ui/DataTable.js +12 -8
  32. package/dist/components/ui/DepthButton.js +2 -2
  33. package/dist/components/ui/Dialog.d.ts +5 -1
  34. package/dist/components/ui/Dialog.js +49 -18
  35. package/dist/components/ui/Disclosure.js +2 -1
  36. package/dist/components/ui/Divider.js +3 -2
  37. package/dist/components/ui/DropdownMenu.d.ts +11 -4
  38. package/dist/components/ui/DropdownMenu.js +109 -66
  39. package/dist/components/ui/EmptyState.js +2 -1
  40. package/dist/components/ui/Field.js +2 -2
  41. package/dist/components/ui/FlipCard.js +2 -2
  42. package/dist/components/ui/FloatingPortal.d.ts +10 -0
  43. package/dist/components/ui/FloatingPortal.js +51 -0
  44. package/dist/components/ui/IconButton.js +2 -2
  45. package/dist/components/ui/Kbd.js +2 -1
  46. package/dist/components/ui/Magnetic.js +2 -1
  47. package/dist/components/ui/Marquee.js +3 -2
  48. package/dist/components/ui/OverlayTrigger.d.ts +11 -0
  49. package/dist/components/ui/OverlayTrigger.js +5 -0
  50. package/dist/components/ui/Pagination.d.ts +5 -1
  51. package/dist/components/ui/Pagination.js +3 -2
  52. package/dist/components/ui/Popover.d.ts +18 -8
  53. package/dist/components/ui/Popover.js +58 -17
  54. package/dist/components/ui/ProgressBar.js +2 -2
  55. package/dist/components/ui/RadioGroup.d.ts +13 -5
  56. package/dist/components/ui/RadioGroup.js +36 -9
  57. package/dist/components/ui/Reveal.d.ts +0 -1
  58. package/dist/components/ui/Reveal.js +4 -3
  59. package/dist/components/ui/RevealHeading.js +2 -1
  60. package/dist/components/ui/ScrollProgress.js +2 -1
  61. package/dist/components/ui/SectionLabel.js +2 -1
  62. package/dist/components/ui/SegmentedControl.js +6 -3
  63. package/dist/components/ui/SelectField.js +18 -3
  64. package/dist/components/ui/Skeleton.js +2 -2
  65. package/dist/components/ui/Slider.d.ts +23 -7
  66. package/dist/components/ui/Slider.js +43 -6
  67. package/dist/components/ui/Spinner.js +2 -2
  68. package/dist/components/ui/Stat.js +2 -1
  69. package/dist/components/ui/Stepper.js +3 -3
  70. package/dist/components/ui/Switch.d.ts +27 -5
  71. package/dist/components/ui/Switch.js +51 -5
  72. package/dist/components/ui/Tabs.js +7 -5
  73. package/dist/components/ui/Tag.d.ts +2 -1
  74. package/dist/components/ui/Tag.js +4 -3
  75. package/dist/components/ui/TextArea.js +4 -1
  76. package/dist/components/ui/TextField.js +8 -3
  77. package/dist/components/ui/ThemeScope.d.ts +13 -0
  78. package/dist/components/ui/ThemeScope.js +10 -0
  79. package/dist/components/ui/ThemeToggle.d.ts +3 -0
  80. package/dist/components/ui/ThemeToggle.js +4 -3
  81. package/dist/components/ui/TiltCard.js +3 -2
  82. package/dist/components/ui/Timeline.js +2 -2
  83. package/dist/components/ui/ToastProvider.d.ts +5 -2
  84. package/dist/components/ui/ToastProvider.js +5 -5
  85. package/dist/components/ui/Tooltip.d.ts +4 -4
  86. package/dist/components/ui/Tooltip.js +70 -10
  87. package/dist/data-display.css +5 -0
  88. package/dist/effects.css +6 -0
  89. package/dist/feedback.css +5 -0
  90. package/dist/fonts-latin.css +10 -4
  91. package/dist/fonts.css +8 -0
  92. package/dist/forms.css +5 -0
  93. package/dist/hooks/useDismiss.d.ts +12 -3
  94. package/dist/hooks/useDismiss.js +5 -21
  95. package/dist/hooks/useFloatingPanel.d.ts +14 -0
  96. package/dist/hooks/useFloatingPanel.js +157 -0
  97. package/dist/hooks/useFormReset.d.ts +1 -1
  98. package/dist/hooks/useLayerDismiss.d.ts +4 -0
  99. package/dist/hooks/useLayerDismiss.js +48 -0
  100. package/dist/hooks/useMobileMenu.d.ts +0 -1
  101. package/dist/hooks/useMobileMenu.js +11 -16
  102. package/dist/hooks/usePanelFocus.d.ts +10 -0
  103. package/dist/hooks/usePanelFocus.js +60 -0
  104. package/dist/hooks/useTheme.d.ts +1 -0
  105. package/dist/hooks/useTheme.js +18 -5
  106. package/dist/index.d.ts +5 -0
  107. package/dist/index.js +1 -0
  108. package/dist/kit/forms.css +11 -2
  109. package/dist/kit/navigation.css +4 -0
  110. package/dist/kit/overlays.css +8 -4
  111. package/dist/lib/classNames.d.ts +2 -0
  112. package/dist/lib/classNames.js +16 -1
  113. package/dist/lib/componentClasses.d.ts +1 -0
  114. package/dist/lib/componentClasses.js +2 -0
  115. package/dist/lib/keyboard.d.ts +1 -1
  116. package/dist/lib/keyboard.js +5 -2
  117. package/dist/lib/layers.d.ts +18 -0
  118. package/dist/lib/layers.js +128 -0
  119. package/dist/lib/themeScope.d.ts +5 -0
  120. package/dist/lib/themeScope.js +3 -0
  121. package/dist/minimal-reset.css +6 -0
  122. package/dist/navigation.css +5 -0
  123. package/dist/overlays.css +5 -0
  124. package/dist/scoped/base.css +183 -0
  125. package/dist/scoped/kit/actions.css +209 -0
  126. package/dist/scoped/kit/data-display.css +492 -0
  127. package/dist/scoped/kit/effects.css +140 -0
  128. package/dist/scoped/kit/feedback.css +309 -0
  129. package/dist/scoped/kit/forms.css +370 -0
  130. package/dist/scoped/kit/navigation.css +266 -0
  131. package/dist/scoped/kit/overlays.css +299 -0
  132. package/dist/scoped/marquee.css +58 -0
  133. package/dist/scoped/spinner.css +20 -0
  134. package/dist/scoped/theme-toggle.css +38 -0
  135. package/dist/scoped/tokens.css +119 -0
  136. package/dist/scoped/ui.css +127 -0
  137. package/dist/scoped/utilities.css +17 -0
  138. package/dist/scoped-tokens.css +1 -0
  139. package/dist/scoped.css +12 -0
  140. package/dist/theme-toggle.css +2 -2
  141. package/dist/tokens.json +710 -0
  142. package/examples/forms/HookFormSettingsForm.tsx +176 -0
  143. package/examples/forms/ManualSettingsForm-0.3.0.tsx +102 -0
  144. package/examples/forms/NativeSettingsForm.tsx +119 -0
  145. package/examples/forms/settings.ts +61 -0
  146. package/examples/forms/useSubmission.ts +77 -0
  147. package/package.json +30 -6
@@ -0,0 +1,176 @@
1
+ "use client";
2
+ import { useEffect } from "react";
3
+ import { Controller, useForm } from "react-hook-form";
4
+ import {
5
+ Button,
6
+ RadioGroup,
7
+ Slider,
8
+ Switch,
9
+ TextField,
10
+ } from "@abuzareal/contour-ui";
11
+ import {
12
+ defaultSettings,
13
+ type SaveSettings,
14
+ type Settings,
15
+ } from "./settings.js";
16
+ import useSubmission from "./useSubmission.js";
17
+
18
+ /** RHF is an application dependency. Native fields register; value callbacks use Controller. */
19
+ export function HookFormSettingsForm({ save }: { save: SaveSettings }) {
20
+ const {
21
+ register,
22
+ control,
23
+ handleSubmit,
24
+ reset,
25
+ setError,
26
+ setFocus,
27
+ formState: { errors },
28
+ } = useForm<Settings>({ defaultValues: defaultSettings });
29
+ const submission = useSubmission(save);
30
+ const pending = submission.status === "pending";
31
+ useEffect(() => {
32
+ if (pending) return;
33
+ for (const key of ["email", "updates", "level", "contact"] as const) {
34
+ if (errors[key]) {
35
+ setFocus(key);
36
+ break;
37
+ }
38
+ }
39
+ }, [errors, pending, setFocus]);
40
+ const submit = handleSubmit(async (values) => {
41
+ if (pending) return;
42
+ const result = await submission.run({
43
+ ...values,
44
+ email: values.email.trim(),
45
+ });
46
+ if (result) {
47
+ for (const key of ["email", "updates", "level", "contact"] as const) {
48
+ const message = result.fields[key];
49
+ if (message)
50
+ setError(key, { type: "server", message }, { shouldFocus: false });
51
+ }
52
+ }
53
+ }, submission.validationFailed);
54
+ return (
55
+ <form
56
+ noValidate
57
+ method="post"
58
+ aria-label="Hook Form settings"
59
+ onSubmit={submit}
60
+ >
61
+ <fieldset disabled={pending} aria-busy={pending}>
62
+ <legend>Notification settings</legend>
63
+ <TextField
64
+ maxLength={80}
65
+ type="email"
66
+ label="Email"
67
+ required
68
+ autoComplete="email"
69
+ error={errors.email?.message}
70
+ {...register("email", {
71
+ required: "Enter an email address.",
72
+ validate: (value) =>
73
+ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()) ||
74
+ "Enter a valid email address.",
75
+ })}
76
+ />
77
+ <Controller
78
+ control={control}
79
+ name="updates"
80
+ render={({ field }) => (
81
+ <Switch
82
+ ref={field.ref}
83
+ name={field.name}
84
+ label="Updates"
85
+ checked={field.value}
86
+ onChange={field.onChange}
87
+ onBlur={field.onBlur}
88
+ error={errors.updates?.message}
89
+ />
90
+ )}
91
+ />
92
+ <Controller
93
+ control={control}
94
+ name="level"
95
+ rules={{
96
+ validate: (value) =>
97
+ (Number.isFinite(value) && value % 5 === 0) ||
98
+ "Use steps of five.",
99
+ min: { value: 0, message: "Choose at least zero." },
100
+ max: { value: 100, message: "Choose at most 100." },
101
+ }}
102
+ render={({ field }) => (
103
+ <Slider
104
+ ref={field.ref}
105
+ name={field.name}
106
+ label="Level"
107
+ value={field.value}
108
+ onChange={field.onChange}
109
+ onBlur={field.onBlur}
110
+ min={0}
111
+ max={100}
112
+ step={5}
113
+ format={(value) => `${value}%`}
114
+ error={errors.level?.message}
115
+ />
116
+ )}
117
+ />
118
+ <Controller
119
+ control={control}
120
+ name="contact"
121
+ rules={{
122
+ validate: (value) =>
123
+ value === "email" ||
124
+ value === "phone" ||
125
+ "Choose a contact method.",
126
+ }}
127
+ render={({ field }) => (
128
+ <RadioGroup
129
+ name={field.name}
130
+ legend="Contact method"
131
+ value={field.value}
132
+ onChange={field.onChange}
133
+ required
134
+ error={errors.contact?.message}
135
+ options={[
136
+ {
137
+ value: "email",
138
+ label: "Email contact",
139
+ inputProps: { ref: field.ref, onBlur: field.onBlur },
140
+ },
141
+ {
142
+ value: "phone",
143
+ label: "Phone contact",
144
+ inputProps: { onBlur: field.onBlur },
145
+ },
146
+ ]}
147
+ />
148
+ )}
149
+ />
150
+ </fieldset>
151
+ <p role="status" aria-label="Submission status">
152
+ {submission.message}
153
+ </p>
154
+ <Button type="submit" disabled={pending}>
155
+ Save settings
156
+ </Button>
157
+ <Button
158
+ type="button"
159
+ variant="secondary"
160
+ onClick={(event) => {
161
+ submission.cancel();
162
+ // Native reset synchronizes registered counters/auto-resize; RHF resets its own state.
163
+ event.currentTarget.form?.reset();
164
+ reset(defaultSettings);
165
+ }}
166
+ >
167
+ Reset settings
168
+ </Button>
169
+ {pending && (
170
+ <Button onClick={submission.cancel} variant="secondary">
171
+ Stop waiting
172
+ </Button>
173
+ )}
174
+ </form>
175
+ );
176
+ }
@@ -0,0 +1,102 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import {
4
+ Button,
5
+ RadioGroup,
6
+ Slider,
7
+ Switch,
8
+ TextField,
9
+ } from "@abuzareal/contour-ui";
10
+ import {
11
+ defaultSettings,
12
+ readSettings,
13
+ type FieldErrors,
14
+ type SaveSettings,
15
+ } from "./settings.js";
16
+ import useSubmission from "./useSubmission.js";
17
+
18
+ /** Legacy 0.3.0 recipe: these three controls submit through application state. */
19
+ export function ManualSettingsForm({ save }: { save: SaveSettings }) {
20
+ const [settings, setSettings] = useState(defaultSettings);
21
+ const [errors, setErrors] = useState<FieldErrors>({});
22
+ const submission = useSubmission(save);
23
+ const pending = submission.status === "pending";
24
+ return (
25
+ <form
26
+ noValidate
27
+ method="post"
28
+ aria-label="Manual 0.3.0 settings"
29
+ onReset={() => {
30
+ submission.cancel();
31
+ setSettings(defaultSettings);
32
+ setErrors({});
33
+ }}
34
+ onSubmit={async (event) => {
35
+ event.preventDefault();
36
+ if (pending) return;
37
+ const data = new FormData(event.currentTarget);
38
+ // The generated radio name is not a stable service field; explicitly serialize state.
39
+ data.set("level", String(settings.level));
40
+ data.set("contact", settings.contact);
41
+ if (settings.updates) data.set("updates", "on");
42
+ else data.delete("updates");
43
+ const parsed = readSettings(data);
44
+ setErrors(parsed.errors);
45
+ if (!parsed.value) {
46
+ submission.validationFailed();
47
+ return;
48
+ }
49
+ const result = await submission.run(parsed.value);
50
+ if (result) setErrors(result.fields);
51
+ }}
52
+ >
53
+ <fieldset disabled={pending} aria-busy={pending}>
54
+ <legend>Notification settings</legend>
55
+ <TextField
56
+ name="email"
57
+ label="Email"
58
+ type="email"
59
+ required
60
+ error={errors.email}
61
+ />
62
+ <Switch
63
+ label="Updates"
64
+ checked={settings.updates}
65
+ onChange={(updates) =>
66
+ setSettings((current) => ({ ...current, updates }))
67
+ }
68
+ />
69
+ <Slider
70
+ label="Level"
71
+ value={settings.level}
72
+ step={5}
73
+ onChange={(level) =>
74
+ setSettings((current) => ({ ...current, level }))
75
+ }
76
+ />
77
+ <RadioGroup
78
+ legend="Contact method"
79
+ value={settings.contact}
80
+ onChange={(contact) =>
81
+ setSettings((current) => ({ ...current, contact }))
82
+ }
83
+ options={[
84
+ { value: "email", label: "Email contact" },
85
+ { value: "phone", label: "Phone contact" },
86
+ ]}
87
+ />
88
+ <p aria-live="polite">
89
+ {errors.updates ?? errors.level ?? errors.contact}
90
+ </p>
91
+ </fieldset>
92
+ <p role="status" aria-label="Submission status">
93
+ {submission.message}
94
+ </p>
95
+ <Button type="submit" disabled={pending}>
96
+ Save settings
97
+ </Button>
98
+ <Button type="reset">Reset settings</Button>
99
+ {pending && <Button onClick={submission.cancel}>Stop waiting</Button>}
100
+ </form>
101
+ );
102
+ }
@@ -0,0 +1,119 @@
1
+ "use client";
2
+ import { useEffect, useRef, useState, type FormEvent } from "react";
3
+ import {
4
+ Button,
5
+ RadioGroup,
6
+ Slider,
7
+ Switch,
8
+ TextField,
9
+ } from "@abuzareal/contour-ui";
10
+ import {
11
+ defaultSettings,
12
+ readSettings,
13
+ type FieldErrors,
14
+ type SaveSettings,
15
+ } from "./settings.js";
16
+ import useSubmission from "./useSubmission.js";
17
+
18
+ /** Provide a real save adapter. This example never invents a successful response. */
19
+ export function NativeSettingsForm({ save }: { save: SaveSettings }) {
20
+ const submission = useSubmission(save);
21
+ const [errors, setErrors] = useState<FieldErrors>({});
22
+ const email = useRef<HTMLInputElement>(null);
23
+ const level = useRef<HTMLInputElement>(null);
24
+ const contact = useRef<HTMLInputElement>(null);
25
+ const updates = useRef<HTMLButtonElement>(null);
26
+ const pending = submission.status === "pending";
27
+ useEffect(() => {
28
+ if (pending) return;
29
+ if (errors.email) email.current?.focus();
30
+ else if (errors.updates) updates.current?.focus();
31
+ else if (errors.level) level.current?.focus();
32
+ else if (errors.contact) contact.current?.focus();
33
+ }, [errors, pending]);
34
+ const submit = async (event: FormEvent<HTMLFormElement>) => {
35
+ event.preventDefault();
36
+ if (pending) return;
37
+ const parsed = readSettings(new FormData(event.currentTarget));
38
+ setErrors(parsed.errors);
39
+ if (!parsed.value) {
40
+ submission.validationFailed();
41
+ return;
42
+ }
43
+ const result = await submission.run(parsed.value);
44
+ if (result) setErrors(result.fields);
45
+ };
46
+ return (
47
+ <form
48
+ noValidate
49
+ method="post"
50
+ onSubmit={submit}
51
+ onReset={() => {
52
+ submission.cancel();
53
+ setErrors({});
54
+ }}
55
+ aria-label="Native settings"
56
+ >
57
+ <fieldset disabled={pending} aria-busy={pending}>
58
+ <legend>Notification settings</legend>
59
+ <TextField
60
+ ref={email}
61
+ name="email"
62
+ maxLength={80}
63
+ type="email"
64
+ label="Email"
65
+ required
66
+ autoComplete="email"
67
+ error={errors.email}
68
+ />
69
+ <Switch
70
+ ref={updates}
71
+ name="updates"
72
+ label="Updates"
73
+ defaultChecked={defaultSettings.updates}
74
+ error={errors.updates}
75
+ />
76
+ <Slider
77
+ ref={level}
78
+ name="level"
79
+ label="Level"
80
+ defaultValue={defaultSettings.level}
81
+ min={0}
82
+ max={100}
83
+ step={5}
84
+ format={(value) => `${value}%`}
85
+ error={errors.level}
86
+ />
87
+ <RadioGroup
88
+ name="contact"
89
+ legend="Contact method"
90
+ defaultValue={defaultSettings.contact}
91
+ required
92
+ error={errors.contact}
93
+ options={[
94
+ {
95
+ value: "email",
96
+ label: "Email contact",
97
+ inputProps: { ref: contact },
98
+ },
99
+ { value: "phone", label: "Phone contact" },
100
+ ]}
101
+ />
102
+ </fieldset>
103
+ <p role="status" aria-label="Submission status">
104
+ {submission.message}
105
+ </p>
106
+ <Button type="submit" disabled={pending}>
107
+ Save settings
108
+ </Button>
109
+ <Button type="reset" variant="secondary">
110
+ Reset settings
111
+ </Button>
112
+ {pending && (
113
+ <Button onClick={submission.cancel} variant="secondary">
114
+ Stop waiting
115
+ </Button>
116
+ )}
117
+ </form>
118
+ );
119
+ }
@@ -0,0 +1,61 @@
1
+ /** Copy these application contracts with the recipe; adapt them to your real service. */
2
+ export type Settings = {
3
+ email: string;
4
+ updates: boolean;
5
+ level: number;
6
+ contact: "email" | "phone";
7
+ };
8
+ export type FieldErrors = Partial<Record<keyof Settings, string>>;
9
+ export type SaveResult =
10
+ { ok: true } | { ok: false; message: string; fields?: FieldErrors };
11
+ export type SaveSettings = (
12
+ settings: Settings,
13
+ signal: AbortSignal,
14
+ ) => Promise<SaveResult>;
15
+ export const defaultSettings: Settings = {
16
+ email: "",
17
+ updates: true,
18
+ level: 20,
19
+ contact: "email",
20
+ };
21
+
22
+ export function readSettings(data: FormData): {
23
+ value?: Settings;
24
+ errors: FieldErrors;
25
+ } {
26
+ const email = data.get("email"),
27
+ level = data.get("level"),
28
+ contact = data.get("contact");
29
+ const errors: FieldErrors = {};
30
+ if (
31
+ typeof email !== "string" ||
32
+ !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())
33
+ )
34
+ errors.email = "Enter a valid email address.";
35
+ const numeric =
36
+ typeof level === "string" && level.trim() !== "" ? Number(level) : NaN;
37
+ if (
38
+ !Number.isFinite(numeric) ||
39
+ numeric < 0 ||
40
+ numeric > 100 ||
41
+ numeric % 5 !== 0
42
+ )
43
+ errors.level = "Choose a level from 0 to 100 in steps of five.";
44
+ if (contact !== "email" && contact !== "phone")
45
+ errors.contact = "Choose a contact method.";
46
+ if (
47
+ Object.keys(errors).length ||
48
+ typeof email !== "string" ||
49
+ (contact !== "email" && contact !== "phone")
50
+ )
51
+ return { errors };
52
+ return {
53
+ errors,
54
+ value: {
55
+ email: email.trim(),
56
+ updates: data.get("updates") === "on",
57
+ level: numeric,
58
+ contact,
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import type { FieldErrors, SaveSettings, Settings } from "./settings.js";
4
+
5
+ type Submission = {
6
+ status: "idle" | "pending" | "error" | "success";
7
+ message: string;
8
+ fields: FieldErrors;
9
+ };
10
+ const ready: Submission = { status: "idle", message: "", fields: {} };
11
+
12
+ /** Application example: the injected adapter confirms success; cancellation cannot undo server work. */
13
+ export default function useSubmission(save: SaveSettings) {
14
+ const active = useRef<AbortController | null>(null);
15
+ const [submission, setSubmission] = useState(ready);
16
+ useEffect(
17
+ () => () => {
18
+ active.current?.abort();
19
+ active.current = null;
20
+ },
21
+ [],
22
+ );
23
+ const cancel = useCallback(() => {
24
+ const wasPending = active.current !== null;
25
+ active.current?.abort();
26
+ active.current = null;
27
+ setSubmission({
28
+ ...ready,
29
+ message: wasPending
30
+ ? "Stopped waiting for the response. The server may still finish this request."
31
+ : "",
32
+ });
33
+ }, []);
34
+ const validationFailed = useCallback(() => {
35
+ if (!active.current)
36
+ setSubmission({
37
+ ...ready,
38
+ status: "error",
39
+ message: "Check the highlighted fields.",
40
+ });
41
+ }, []);
42
+ const run = async (settings: Settings) => {
43
+ if (active.current) return null;
44
+ const operation = new AbortController();
45
+ active.current = operation;
46
+ setSubmission({
47
+ status: "pending",
48
+ message: "Saving settings…",
49
+ fields: {},
50
+ });
51
+ try {
52
+ const result = await save(settings, operation.signal);
53
+ if (active.current !== operation || operation.signal.aborted) return null;
54
+ const next: Submission = result.ok
55
+ ? { status: "success", message: "Settings saved.", fields: {} }
56
+ : {
57
+ status: "error",
58
+ message: result.message,
59
+ fields: result.fields ?? {},
60
+ };
61
+ setSubmission(next);
62
+ return next;
63
+ } catch {
64
+ if (active.current !== operation || operation.signal.aborted) return null;
65
+ const next: Submission = {
66
+ status: "error",
67
+ message: "Could not save settings. Try again.",
68
+ fields: {},
69
+ };
70
+ setSubmission(next);
71
+ return next;
72
+ } finally {
73
+ if (active.current === operation) active.current = null;
74
+ }
75
+ };
76
+ return { ...submission, run, cancel, validationFailed };
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abuzareal/contour-ui",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "Editorial React components, themes, restrained motion, and optional Three.js objects.",
6
6
  "type": "module",
@@ -16,7 +16,12 @@
16
16
  "NEXTJS.md",
17
17
  "CONTRACTS.md",
18
18
  "PUBLIC-API.md",
19
- "COMPOSITION.md"
19
+ "COMPOSITION.md",
20
+ "FORMS.md",
21
+ "examples/forms",
22
+ "OVERLAYS.md",
23
+ "STYLING.md",
24
+ "SIZE.md"
20
25
  ],
21
26
  "sideEffects": [
22
27
  "**/*.css"
@@ -46,24 +51,42 @@
46
51
  "./reset.css": "./dist/reset.css",
47
52
  "./tokens.css": "./dist/tokens.css",
48
53
  "./fonts.css": "./dist/fonts.css",
49
- "./fonts-latin.css": "./dist/fonts-latin.css"
54
+ "./fonts-latin.css": "./dist/fonts-latin.css",
55
+ "./scoped.css": "./dist/scoped.css",
56
+ "./scoped-tokens.css": "./dist/scoped-tokens.css",
57
+ "./minimal-reset.css": "./dist/minimal-reset.css",
58
+ "./tokens.json": "./dist/tokens.json",
59
+ "./actions.css": "./dist/actions.css",
60
+ "./forms.css": "./dist/forms.css",
61
+ "./navigation.css": "./dist/navigation.css",
62
+ "./data-display.css": "./dist/data-display.css",
63
+ "./feedback.css": "./dist/feedback.css",
64
+ "./overlays.css": "./dist/overlays.css",
65
+ "./effects.css": "./dist/effects.css"
50
66
  },
51
67
  "scripts": {
52
68
  "build": "node scripts/build.mjs",
53
69
  "typecheck": "tsc -p tsconfig.json --noEmit",
54
- "lint": "oxlint src --deny-warnings",
70
+ "lint": "oxlint src examples --deny-warnings",
55
71
  "test": "node --test tests/*.test.mjs",
56
72
  "test:browser": "node --test tests/*.browser.mjs",
57
73
  "check:package": "node scripts/check-package.mjs",
58
- "validate": "npm run typecheck && npm run lint && npm run build && npm test && npm run test:browser && npm run check:package",
74
+ "validate": "npm run check:styles && npm run typecheck && npm run lint && npm run build && npm test && npm run test:browser && npm run check:package",
59
75
  "prepublishOnly": "node scripts/verify-release.mjs",
60
76
  "test:consumer": "node scripts/test-consumer.mjs",
61
77
  "test:compat": "node scripts/test-compat.mjs",
62
78
  "test:optional": "node scripts/test-optional-consumer.mjs",
63
79
  "test:next": "npm run build && node scripts/test-next.mjs",
64
- "verify:release": "node scripts/verify-release.mjs"
80
+ "verify:release": "node scripts/verify-release.mjs",
81
+ "test:form-recipes": "node scripts/test-form-recipes.mjs",
82
+ "build:styles": "node scripts/build-styles.mjs",
83
+ "measure:size": "node scripts/measure-size.mjs",
84
+ "check:size": "node scripts/measure-size.mjs --check",
85
+ "check:api-docs": "node scripts/check-api-docs.mjs",
86
+ "check:styles": "node scripts/build-styles.mjs --check"
65
87
  },
66
88
  "dependencies": {
89
+ "@floating-ui/dom": "1.8.0",
67
90
  "@fontsource-variable/dm-sans": "^5.3.0",
68
91
  "@fontsource-variable/space-grotesk": "^5.3.0",
69
92
  "@fontsource/dm-mono": "^5.3.0",
@@ -92,6 +115,7 @@
92
115
  "jsdom": "^30.1.2",
93
116
  "lenis": "^1.3.26",
94
117
  "oxlint": "^1.81.0",
118
+ "postcss": "8.5.29",
95
119
  "react": "18.3.1",
96
120
  "react-dom": "18.3.1",
97
121
  "three": "^0.186.1",