@abuzareal/contour-ui 0.5.0 → 0.6.1
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 +16 -0
- package/COMBOBOX.md +87 -0
- package/COMPOSITION.md +4 -0
- package/CONTRACTS.md +27 -27
- package/DOCS.md +86 -0
- package/FORMS.md +4 -0
- package/FOUNDATION.md +35 -0
- package/GETTING-STARTED.md +67 -0
- package/HELPERS.md +33 -0
- package/HOOKS.md +67 -0
- package/MANUAL-QA.md +38 -0
- package/MIGRATION.md +44 -0
- package/MODELS.md +18 -0
- package/NEXTJS.md +3 -1
- package/OVERLAYS.md +4 -0
- package/PROP-REFERENCE.md +37 -0
- package/PUBLIC-API.md +174 -164
- package/README.md +9 -5
- package/RECIPES.md +43 -0
- package/REFERENCE.md +193 -0
- package/RELEASING.md +8 -2
- package/SIZE.md +9 -3
- package/STYLING.md +4 -0
- package/SUPPORT.md +36 -0
- package/THREE.md +40 -0
- package/dist/components/ui/Combobox.d.ts +325 -0
- package/dist/components/ui/Combobox.js +135 -0
- package/dist/components/ui/Combobox.types.d.ts +46 -0
- package/dist/components/ui/Combobox.types.js +1 -0
- package/dist/components/ui/DataTable.js +5 -4
- package/dist/components/ui/Field.js +1 -1
- package/dist/components/ui/FloatingPortal.js +2 -40
- package/dist/components/ui/FloatingScope.d.ts +9 -0
- package/dist/components/ui/FloatingScope.js +43 -0
- package/dist/components/ui/Table.d.ts +20 -0
- package/dist/components/ui/Table.js +9 -0
- package/dist/hooks/useComboboxAnchor.d.ts +17 -0
- package/dist/hooks/useComboboxAnchor.js +61 -0
- package/dist/hooks/useInitialHashScroll.js +5 -1
- package/dist/hooks/usePrintPreparation.js +12 -1
- package/dist/hooks/useScrollSpy.js +6 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/kit/data-display.css +6 -0
- package/dist/kit/forms.css +34 -0
- package/dist/lib/componentClasses.js +1 -1
- package/dist/scoped/kit/data-display.css +6 -0
- package/dist/scoped/kit/forms.css +34 -0
- package/docs/prop-metadata.json +2668 -0
- package/examples/onboarding/SettingsForm.jsx +30 -0
- package/examples/onboarding/SettingsForm.tsx +50 -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/reference/HookExamples.tsx +146 -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 +37 -4
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Generated from SettingsForm.tsx by scripts/build-onboarding.mjs.
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { Button, Checkbox, Combobox, TextField } from "@abuzareal/contour-ui";
|
|
4
|
+
const options = [
|
|
5
|
+
{ value: "atlas", label: "Atlas" },
|
|
6
|
+
{ value: "field", label: "Field notes" },
|
|
7
|
+
];
|
|
8
|
+
/** Local inspection only. Validate and authorize real submissions on the server. */
|
|
9
|
+
export default function SettingsForm() {
|
|
10
|
+
const [payload, setPayload] = useState("");
|
|
11
|
+
const name = useRef(null);
|
|
12
|
+
return (<section aria-label="Settings example">
|
|
13
|
+
<h2>Project settings</h2>
|
|
14
|
+
<p>This form inspects local values; it does not save them.</p>
|
|
15
|
+
<form onSubmit={(event) => {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
setPayload(JSON.stringify(Object.fromEntries(new FormData(event.currentTarget))));
|
|
18
|
+
}} onReset={() => setPayload("")}>
|
|
19
|
+
<TextField ref={name} label="Name" name="name" defaultValue="Ada" required/>
|
|
20
|
+
<Combobox label="Project" name="project" options={options} defaultValue="atlas" required/>
|
|
21
|
+
<Checkbox label="Allow updates" name="updates" defaultChecked/>
|
|
22
|
+
<Button type="submit">Inspect values</Button>
|
|
23
|
+
<Button type="reset" variant="secondary">
|
|
24
|
+
Reset form
|
|
25
|
+
</Button>
|
|
26
|
+
</form>
|
|
27
|
+
<output aria-label="Form values">{payload}</output>
|
|
28
|
+
<Button onClick={() => name.current?.focus()}>Focus name</Button>
|
|
29
|
+
</section>);
|
|
30
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { Button, Checkbox, Combobox, TextField } from "@abuzareal/contour-ui";
|
|
3
|
+
const options = [
|
|
4
|
+
{ value: "atlas", label: "Atlas" },
|
|
5
|
+
{ value: "field", label: "Field notes" },
|
|
6
|
+
];
|
|
7
|
+
/** Local inspection only. Validate and authorize real submissions on the server. */
|
|
8
|
+
export default function SettingsForm() {
|
|
9
|
+
const [payload, setPayload] = useState("");
|
|
10
|
+
const name = useRef<HTMLInputElement>(null);
|
|
11
|
+
return (
|
|
12
|
+
<section aria-label="Settings example">
|
|
13
|
+
<h2>Project settings</h2>
|
|
14
|
+
<p>This form inspects local values; it does not save them.</p>
|
|
15
|
+
<form
|
|
16
|
+
onSubmit={(event) => {
|
|
17
|
+
event.preventDefault();
|
|
18
|
+
setPayload(
|
|
19
|
+
JSON.stringify(
|
|
20
|
+
Object.fromEntries(new FormData(event.currentTarget)),
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
}}
|
|
24
|
+
onReset={() => setPayload("")}
|
|
25
|
+
>
|
|
26
|
+
<TextField
|
|
27
|
+
ref={name}
|
|
28
|
+
label="Name"
|
|
29
|
+
name="name"
|
|
30
|
+
defaultValue="Ada"
|
|
31
|
+
required
|
|
32
|
+
/>
|
|
33
|
+
<Combobox
|
|
34
|
+
label="Project"
|
|
35
|
+
name="project"
|
|
36
|
+
options={options}
|
|
37
|
+
defaultValue="atlas"
|
|
38
|
+
required
|
|
39
|
+
/>
|
|
40
|
+
<Checkbox label="Allow updates" name="updates" defaultChecked />
|
|
41
|
+
<Button type="submit">Inspect values</Button>
|
|
42
|
+
<Button type="reset" variant="secondary">
|
|
43
|
+
Reset form
|
|
44
|
+
</Button>
|
|
45
|
+
</form>
|
|
46
|
+
<output aria-label="Form values">{payload}</output>
|
|
47
|
+
<Button onClick={() => name.current?.focus()}>Focus name</Button>
|
|
48
|
+
</section>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { lazy, Suspense, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
SceneBoundary,
|
|
6
|
+
useMotionPreference,
|
|
7
|
+
} from "@abuzareal/contour-ui";
|
|
8
|
+
import "./optional.css";
|
|
9
|
+
const Sculpture = lazy(() =>
|
|
10
|
+
import("@abuzareal/contour-ui/three").then((module) => ({
|
|
11
|
+
default: module.Sculpture,
|
|
12
|
+
})),
|
|
13
|
+
);
|
|
14
|
+
export function LazyScene() {
|
|
15
|
+
const [loaded, setLoaded] = useState(false),
|
|
16
|
+
[paused, setPaused] = useState(false);
|
|
17
|
+
const reducedMotion = useMotionPreference();
|
|
18
|
+
return (
|
|
19
|
+
<figure>
|
|
20
|
+
<div className="recipe-scene-visual">
|
|
21
|
+
<div
|
|
22
|
+
className="recipe-scene-fallback"
|
|
23
|
+
role="img"
|
|
24
|
+
aria-label="Three interlocking sculptural links"
|
|
25
|
+
>
|
|
26
|
+
<svg aria-hidden="true" viewBox="0 0 300 180">
|
|
27
|
+
<g fill="none" stroke="currentColor" strokeWidth="16">
|
|
28
|
+
<ellipse
|
|
29
|
+
cx="95"
|
|
30
|
+
cy="78"
|
|
31
|
+
rx="52"
|
|
32
|
+
ry="29"
|
|
33
|
+
transform="rotate(-25 95 78)"
|
|
34
|
+
/>
|
|
35
|
+
<ellipse
|
|
36
|
+
cx="165"
|
|
37
|
+
cy="95"
|
|
38
|
+
rx="52"
|
|
39
|
+
ry="29"
|
|
40
|
+
transform="rotate(25 165 95)"
|
|
41
|
+
/>
|
|
42
|
+
<ellipse
|
|
43
|
+
cx="215"
|
|
44
|
+
cy="62"
|
|
45
|
+
rx="40"
|
|
46
|
+
ry="25"
|
|
47
|
+
transform="rotate(-35 215 62)"
|
|
48
|
+
/>
|
|
49
|
+
</g>
|
|
50
|
+
</svg>
|
|
51
|
+
</div>
|
|
52
|
+
{loaded && (
|
|
53
|
+
<div className="recipe-scene-layer" aria-hidden="true">
|
|
54
|
+
<SceneBoundary>
|
|
55
|
+
<Suspense fallback={null}>
|
|
56
|
+
<Sculpture motionEnabled={!paused && !reducedMotion} />
|
|
57
|
+
</Suspense>
|
|
58
|
+
</SceneBoundary>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
<figcaption>
|
|
63
|
+
Three linked forms. The static study remains available if 3D cannot
|
|
64
|
+
load.
|
|
65
|
+
</figcaption>
|
|
66
|
+
<Button onClick={() => setLoaded(true)} disabled={loaded}>
|
|
67
|
+
Load 3D study
|
|
68
|
+
</Button>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
aria-pressed={paused}
|
|
72
|
+
onClick={() => setPaused((value) => !value)}
|
|
73
|
+
>
|
|
74
|
+
Pause motion
|
|
75
|
+
</button>
|
|
76
|
+
</figure>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { lazy, Suspense, useState } from "react";
|
|
3
|
+
import { useMotionPreference } from "@abuzareal/contour-ui";
|
|
4
|
+
const SmoothScrollOwner = lazy(() => import("./SmoothScrollOwner.js"));
|
|
5
|
+
/** Mount at most one owner for the page; ordinary native scrolling works before opt-in. */
|
|
6
|
+
export function OptionalSmoothScroll() {
|
|
7
|
+
const [enabled, setEnabled] = useState(false);
|
|
8
|
+
const reducedMotion = useMotionPreference();
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<button
|
|
12
|
+
type="button"
|
|
13
|
+
aria-pressed={enabled}
|
|
14
|
+
onClick={() => setEnabled((value) => !value)}
|
|
15
|
+
>
|
|
16
|
+
Smooth scrolling
|
|
17
|
+
</button>
|
|
18
|
+
{enabled && !reducedMotion && (
|
|
19
|
+
<Suspense fallback={null}>
|
|
20
|
+
<SmoothScrollOwner enabled />
|
|
21
|
+
</Suspense>
|
|
22
|
+
)}
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.recipe-scene-visual {
|
|
2
|
+
position: relative;
|
|
3
|
+
block-size: 18rem;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
}
|
|
6
|
+
.recipe-scene-fallback,
|
|
7
|
+
.recipe-scene-layer {
|
|
8
|
+
position: absolute;
|
|
9
|
+
inset: 0;
|
|
10
|
+
}
|
|
11
|
+
.recipe-scene-fallback {
|
|
12
|
+
display: grid;
|
|
13
|
+
place-items: center;
|
|
14
|
+
}
|
|
15
|
+
.recipe-scene-fallback svg {
|
|
16
|
+
inline-size: min(100%, 24rem);
|
|
17
|
+
block-size: 100%;
|
|
18
|
+
}
|
|
19
|
+
.recipe-scene-layer .contour-sculpture-canvas {
|
|
20
|
+
position: absolute;
|
|
21
|
+
inset: 0;
|
|
22
|
+
}
|
|
23
|
+
.recipe-scene-visual:has([data-state="ready"]) .recipe-scene-fallback {
|
|
24
|
+
visibility: hidden;
|
|
25
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { Combobox, type ComboboxOption } from "@abuzareal/contour-ui";
|
|
4
|
+
import { useRemoteResource, type Loader } from "./useRemoteResource.js";
|
|
5
|
+
|
|
6
|
+
export type ProjectSearch = Loader<string, readonly ComboboxOption[]>;
|
|
7
|
+
export type AsyncProjectPickerProps = {
|
|
8
|
+
/** Stable function: validate response IDs/labels before resolving. */
|
|
9
|
+
searchProjects: ProjectSearch;
|
|
10
|
+
initialProject?: ComboboxOption;
|
|
11
|
+
onProjectChange?: (project: ComboboxOption | null) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Copy this into the application; fetching and selected-record retention are not library features. */
|
|
15
|
+
export function AsyncProjectPicker({
|
|
16
|
+
searchProjects,
|
|
17
|
+
initialProject,
|
|
18
|
+
onProjectChange,
|
|
19
|
+
}: AsyncProjectPickerProps) {
|
|
20
|
+
const [query, setQuery] = useState(initialProject?.label ?? "");
|
|
21
|
+
const [selected, setSelected] = useState<ComboboxOption | null>(
|
|
22
|
+
initialProject ?? null,
|
|
23
|
+
);
|
|
24
|
+
const search = useCallback(
|
|
25
|
+
async (text: string, signal: AbortSignal) => {
|
|
26
|
+
const matches = await searchProjects(text, signal);
|
|
27
|
+
if (!signal.aborted)
|
|
28
|
+
setSelected((previous) =>
|
|
29
|
+
previous
|
|
30
|
+
? (matches.find((option) => option.value === previous.value) ??
|
|
31
|
+
previous)
|
|
32
|
+
: null,
|
|
33
|
+
);
|
|
34
|
+
return matches;
|
|
35
|
+
},
|
|
36
|
+
[searchProjects],
|
|
37
|
+
);
|
|
38
|
+
const result = useRemoteResource(query, search);
|
|
39
|
+
// Never display the previous query's rows as if they matched this query.
|
|
40
|
+
const matches = useMemo(
|
|
41
|
+
() =>
|
|
42
|
+
!result.loading && !result.error && result.appliedQuery === query
|
|
43
|
+
? (result.data ?? [])
|
|
44
|
+
: [],
|
|
45
|
+
[result.loading, result.error, result.appliedQuery, result.data, query],
|
|
46
|
+
);
|
|
47
|
+
// Bounded retention: keep the current result set and one selected record, not an unbounded cache.
|
|
48
|
+
const options = useMemo(
|
|
49
|
+
() =>
|
|
50
|
+
selected && !matches.some((option) => option.value === selected.value)
|
|
51
|
+
? [...matches, selected]
|
|
52
|
+
: matches,
|
|
53
|
+
[matches, selected],
|
|
54
|
+
);
|
|
55
|
+
return (
|
|
56
|
+
<Combobox
|
|
57
|
+
label="Project"
|
|
58
|
+
name="project"
|
|
59
|
+
required
|
|
60
|
+
options={options}
|
|
61
|
+
filteredOptions={matches}
|
|
62
|
+
value={selected?.value ?? null}
|
|
63
|
+
onValueChange={(value) => {
|
|
64
|
+
const next = options.find((option) => option.value === value) ?? null;
|
|
65
|
+
setSelected(next);
|
|
66
|
+
onProjectChange?.(next);
|
|
67
|
+
}}
|
|
68
|
+
inputValue={query}
|
|
69
|
+
onInputValueChange={setQuery}
|
|
70
|
+
loading={result.loading}
|
|
71
|
+
searchError={
|
|
72
|
+
result.error === "permission"
|
|
73
|
+
? "You do not have permission to search projects."
|
|
74
|
+
: result.error
|
|
75
|
+
? "Projects could not be loaded. Try again."
|
|
76
|
+
: undefined
|
|
77
|
+
}
|
|
78
|
+
onRetry={result.retry}
|
|
79
|
+
hint="Choose a project from the results. Typed text is not a submitted project."
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Button, Dialog } from "@abuzareal/contour-ui";
|
|
4
|
+
|
|
5
|
+
export type DeleteProjectConfirmationProps = {
|
|
6
|
+
project: { id: string; name: string };
|
|
7
|
+
/** Resolve only after server confirmation. Authorization/idempotency belong on the server. */
|
|
8
|
+
deleteProject: (id: string, signal: AbortSignal) => Promise<void>;
|
|
9
|
+
onDeleted?: (id: string) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Native Dialog composition; no AlertDialog export or simulated successful deletion. */
|
|
13
|
+
export function DeleteProjectConfirmation(
|
|
14
|
+
props: DeleteProjectConfirmationProps,
|
|
15
|
+
) {
|
|
16
|
+
// Target identity owns the whole interaction; changing projects cannot reuse a pending confirmation.
|
|
17
|
+
return <ProjectDeletion key={props.project.id} {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ProjectDeletion({
|
|
21
|
+
project,
|
|
22
|
+
deleteProject,
|
|
23
|
+
onDeleted,
|
|
24
|
+
}: DeleteProjectConfirmationProps) {
|
|
25
|
+
const [open, setOpen] = useState(false),
|
|
26
|
+
[pending, setPending] = useState(false),
|
|
27
|
+
[error, setError] = useState("");
|
|
28
|
+
const cancel = useRef<HTMLButtonElement>(null);
|
|
29
|
+
const active = useRef<AbortController | null>(null);
|
|
30
|
+
// A changed target or unmount invalidates the old operation, even if its transport ignores abort.
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
return () => {
|
|
33
|
+
active.current?.abort();
|
|
34
|
+
active.current = null;
|
|
35
|
+
};
|
|
36
|
+
}, [project.id]);
|
|
37
|
+
const dismiss = () => {
|
|
38
|
+
active.current?.abort();
|
|
39
|
+
active.current = null;
|
|
40
|
+
setPending(false);
|
|
41
|
+
setOpen(false);
|
|
42
|
+
setError("");
|
|
43
|
+
};
|
|
44
|
+
const remove = async () => {
|
|
45
|
+
if (active.current) return;
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
active.current = controller;
|
|
48
|
+
setPending(true);
|
|
49
|
+
setError("");
|
|
50
|
+
try {
|
|
51
|
+
await deleteProject(project.id, controller.signal);
|
|
52
|
+
} catch {
|
|
53
|
+
if (controller.signal.aborted || active.current !== controller) return;
|
|
54
|
+
active.current = null;
|
|
55
|
+
setPending(false);
|
|
56
|
+
setError(
|
|
57
|
+
"The deletion could not be confirmed. Check the project before retrying.",
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (controller.signal.aborted || active.current !== controller) return;
|
|
62
|
+
active.current = null;
|
|
63
|
+
setPending(false);
|
|
64
|
+
setOpen(false);
|
|
65
|
+
onDeleted?.(project.id);
|
|
66
|
+
};
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<Button onClick={() => setOpen(true)}>Delete {project.name}</Button>
|
|
70
|
+
<Dialog
|
|
71
|
+
title={`Delete ${project.name}?`}
|
|
72
|
+
open={open}
|
|
73
|
+
onClose={dismiss}
|
|
74
|
+
initialFocus={cancel}
|
|
75
|
+
description="Deleting this project permanently removes its saved settings. This action cannot be undone."
|
|
76
|
+
closeButtonProps={{ "aria-label": "Cancel deletion" }}
|
|
77
|
+
footer={
|
|
78
|
+
<>
|
|
79
|
+
<Button ref={cancel} variant="secondary" onClick={dismiss}>
|
|
80
|
+
Keep project
|
|
81
|
+
</Button>
|
|
82
|
+
<Button disabled={pending} onClick={() => void remove()}>
|
|
83
|
+
{pending ? "Deleting…" : "Permanently delete project"}
|
|
84
|
+
</Button>
|
|
85
|
+
</>
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
<p role="status">
|
|
89
|
+
{pending
|
|
90
|
+
? "Waiting for server confirmation. Closing stops waiting; the server may still complete the deletion."
|
|
91
|
+
: "No action is taken until you confirm."}
|
|
92
|
+
</p>
|
|
93
|
+
{error && <p role="alert">{error}</p>}
|
|
94
|
+
</Dialog>
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { TextField } from "@abuzareal/contour-ui";
|
|
4
|
+
|
|
5
|
+
/** Native number behavior is sufficient for a bounded integer quantity, not locale-aware money. */
|
|
6
|
+
export function QuantityField() {
|
|
7
|
+
return (
|
|
8
|
+
<TextField
|
|
9
|
+
leadingIcon={<span aria-hidden="true">#</span>}
|
|
10
|
+
label="Seats"
|
|
11
|
+
name="seats"
|
|
12
|
+
type="number"
|
|
13
|
+
min={1}
|
|
14
|
+
max={100}
|
|
15
|
+
step={1}
|
|
16
|
+
defaultValue={1}
|
|
17
|
+
trailing={<span aria-hidden="true">seats</span>}
|
|
18
|
+
hint="Whole number, 1–100 seats."
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Keep the same native input mounted so form identity and password-manager hooks survive toggling. */
|
|
24
|
+
export function PasswordField() {
|
|
25
|
+
const [visible, setVisible] = useState(false);
|
|
26
|
+
const input = useRef<HTMLInputElement>(null);
|
|
27
|
+
return (
|
|
28
|
+
<TextField
|
|
29
|
+
ref={input}
|
|
30
|
+
label="Password"
|
|
31
|
+
name="password"
|
|
32
|
+
type={visible ? "text" : "password"}
|
|
33
|
+
autoComplete="current-password"
|
|
34
|
+
trailing={
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
aria-label="Show password"
|
|
38
|
+
aria-pressed={visible}
|
|
39
|
+
onClick={() => {
|
|
40
|
+
setVisible((previous) => !previous);
|
|
41
|
+
input.current?.focus();
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{visible ? "Hide" : "Show"}
|
|
45
|
+
</button>
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|