@dowel-ui/react 0.4.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.
- package/README.md +9 -5
- package/dist/components/ai-extraction-review/ai-extraction-review.d.ts +52 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.d.ts.map +1 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.js +249 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -0
- package/dist/components/ai-extraction-review/extraction-model.d.ts +94 -0
- package/dist/components/ai-extraction-review/extraction-model.d.ts.map +1 -0
- package/dist/components/ai-extraction-review/extraction-model.js +91 -0
- package/dist/components/ai-extraction-review/extraction-model.js.map +1 -0
- package/dist/components/ai-extraction-review/index.d.ts +3 -0
- package/dist/components/ai-extraction-review/index.js +3 -0
- package/dist/components/ai-extraction-review/meta.js +17 -0
- package/dist/components/ai-extraction-review/meta.js.map +1 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.d.ts +72 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.d.ts.map +1 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.js +126 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.js.map +1 -0
- package/dist/components/ai-suggested-value/index.d.ts +2 -0
- package/dist/components/ai-suggested-value/index.js +2 -0
- package/dist/components/ai-suggested-value/meta.js +17 -0
- package/dist/components/ai-suggested-value/meta.js.map +1 -0
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/confirm-typed/confirm-typed.d.ts +47 -0
- package/dist/components/confirm-typed/confirm-typed.d.ts.map +1 -0
- package/dist/components/confirm-typed/confirm-typed.js +106 -0
- package/dist/components/confirm-typed/confirm-typed.js.map +1 -0
- package/dist/components/confirm-typed/index.d.ts +2 -0
- package/dist/components/confirm-typed/index.js +2 -0
- package/dist/components/confirm-typed/meta.js +17 -0
- package/dist/components/confirm-typed/meta.js.map +1 -0
- package/dist/components/cron-editor/cron-editor.d.ts +36 -0
- package/dist/components/cron-editor/cron-editor.d.ts.map +1 -0
- package/dist/components/cron-editor/cron-editor.js +493 -0
- package/dist/components/cron-editor/cron-editor.js.map +1 -0
- package/dist/components/cron-editor/cron-expression.d.ts +82 -0
- package/dist/components/cron-editor/cron-expression.d.ts.map +1 -0
- package/dist/components/cron-editor/cron-expression.js +352 -0
- package/dist/components/cron-editor/cron-expression.js.map +1 -0
- package/dist/components/cron-editor/index.d.ts +3 -0
- package/dist/components/cron-editor/index.js +3 -0
- package/dist/components/cron-editor/meta.js +17 -0
- package/dist/components/cron-editor/meta.js.map +1 -0
- package/dist/components/dns-record/dns-record.d.ts +61 -0
- package/dist/components/dns-record/dns-record.d.ts.map +1 -0
- package/dist/components/dns-record/dns-record.js +227 -0
- package/dist/components/dns-record/dns-record.js.map +1 -0
- package/dist/components/dns-record/index.d.ts +2 -0
- package/dist/components/dns-record/index.js +2 -0
- package/dist/components/dns-record/meta.js +17 -0
- package/dist/components/dns-record/meta.js.map +1 -0
- package/dist/components/permission-matrix/index.d.ts +3 -0
- package/dist/components/permission-matrix/index.js +3 -0
- package/dist/components/permission-matrix/meta.js +17 -0
- package/dist/components/permission-matrix/meta.js.map +1 -0
- package/dist/components/permission-matrix/permission-matrix.d.ts +46 -0
- package/dist/components/permission-matrix/permission-matrix.d.ts.map +1 -0
- package/dist/components/permission-matrix/permission-matrix.js +199 -0
- package/dist/components/permission-matrix/permission-matrix.js.map +1 -0
- package/dist/components/permission-matrix/permission-model.d.ts +53 -0
- package/dist/components/permission-matrix/permission-model.d.ts.map +1 -0
- package/dist/components/permission-matrix/permission-model.js +64 -0
- package/dist/components/permission-matrix/permission-model.js.map +1 -0
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/secret-field/index.d.ts +2 -0
- package/dist/components/secret-field/index.js +2 -0
- package/dist/components/secret-field/meta.js +17 -0
- package/dist/components/secret-field/meta.js.map +1 -0
- package/dist/components/secret-field/secret-field.d.ts +58 -0
- package/dist/components/secret-field/secret-field.d.ts.map +1 -0
- package/dist/components/secret-field/secret-field.js +168 -0
- package/dist/components/secret-field/secret-field.js.map +1 -0
- package/dist/components/session-expiry/index.d.ts +2 -0
- package/dist/components/session-expiry/index.js +2 -0
- package/dist/components/session-expiry/meta.js +17 -0
- package/dist/components/session-expiry/meta.js.map +1 -0
- package/dist/components/session-expiry/session-expiry.d.ts +64 -0
- package/dist/components/session-expiry/session-expiry.d.ts.map +1 -0
- package/dist/components/session-expiry/session-expiry.js +185 -0
- package/dist/components/session-expiry/session-expiry.js.map +1 -0
- package/dist/components/shortcut-recorder/index.d.ts +3 -0
- package/dist/components/shortcut-recorder/index.js +3 -0
- package/dist/components/shortcut-recorder/meta.js +17 -0
- package/dist/components/shortcut-recorder/meta.js.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-model.d.ts +67 -0
- package/dist/components/shortcut-recorder/shortcut-model.d.ts.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-model.js +168 -0
- package/dist/components/shortcut-recorder/shortcut-model.js.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.d.ts +47 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.d.ts.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.js +212 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.js.map +1 -0
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/sync-status/index.d.ts +2 -0
- package/dist/components/sync-status/index.js +2 -0
- package/dist/components/sync-status/meta.js +17 -0
- package/dist/components/sync-status/meta.js.map +1 -0
- package/dist/components/sync-status/sync-status.d.ts +65 -0
- package/dist/components/sync-status/sync-status.d.ts.map +1 -0
- package/dist/components/sync-status/sync-status.js +103 -0
- package/dist/components/sync-status/sync-status.js.map +1 -0
- package/dist/components/toast/toast.d.ts +1 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +16 -2
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +73 -53
- package/dist/registry/components.js.map +1 -1
- package/package.json +4 -4
- package/src/components/ai-extraction-review/ai-extraction-review.tsx +492 -0
- package/src/components/ai-extraction-review/extraction-model.ts +159 -0
- package/src/components/ai-extraction-review/index.ts +13 -0
- package/src/components/ai-extraction-review/meta.ts +24 -0
- package/src/components/ai-suggested-value/ai-suggested-value.tsx +295 -0
- package/src/components/ai-suggested-value/index.ts +6 -0
- package/src/components/ai-suggested-value/meta.ts +24 -0
- package/src/components/confirm-typed/confirm-typed.tsx +191 -0
- package/src/components/confirm-typed/index.ts +1 -0
- package/src/components/confirm-typed/meta.ts +22 -0
- package/src/components/cron-editor/cron-editor.tsx +676 -0
- package/src/components/cron-editor/cron-expression.ts +552 -0
- package/src/components/cron-editor/index.ts +23 -0
- package/src/components/cron-editor/meta.ts +23 -0
- package/src/components/dns-record/dns-record.tsx +315 -0
- package/src/components/dns-record/index.ts +7 -0
- package/src/components/dns-record/meta.ts +21 -0
- package/src/components/permission-matrix/index.ts +14 -0
- package/src/components/permission-matrix/meta.ts +23 -0
- package/src/components/permission-matrix/permission-matrix.tsx +368 -0
- package/src/components/permission-matrix/permission-model.ts +120 -0
- package/src/components/secret-field/index.ts +6 -0
- package/src/components/secret-field/meta.ts +21 -0
- package/src/components/secret-field/secret-field.tsx +284 -0
- package/src/components/session-expiry/index.ts +9 -0
- package/src/components/session-expiry/meta.ts +23 -0
- package/src/components/session-expiry/session-expiry.tsx +321 -0
- package/src/components/shortcut-recorder/index.ts +19 -0
- package/src/components/shortcut-recorder/meta.ts +22 -0
- package/src/components/shortcut-recorder/shortcut-model.ts +204 -0
- package/src/components/shortcut-recorder/shortcut-recorder.tsx +336 -0
- package/src/components/sync-status/index.ts +9 -0
- package/src/components/sync-status/meta.ts +21 -0
- package/src/components/sync-status/sync-status.tsx +217 -0
- package/src/index.ts +10 -0
- package/src/registry/components.ts +20 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, ReactElement } from "react";
|
|
2
|
+
//#region src/components/ai-suggested-value/ai-suggested-value.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* An AI-proposed value for a form control, offered beside it rather than
|
|
5
|
+
* written into it.
|
|
6
|
+
*
|
|
7
|
+
* Autofill is the most common shape AI takes inside ordinary software —
|
|
8
|
+
* enrich this contact, fill this form from the upload, guess the category —
|
|
9
|
+
* and nearly every implementation writes the value straight into the field
|
|
10
|
+
* as if the person had typed it. Two things go wrong. A plausible, wrong
|
|
11
|
+
* value rides through on the person's own Submit, because nothing marked it
|
|
12
|
+
* as needing a look. And once submitted, the record cannot tell a value the
|
|
13
|
+
* model supplied from one a human typed, which is the one fact an audit later
|
|
14
|
+
* needs.
|
|
15
|
+
*
|
|
16
|
+
* So the suggestion stays pending until it is accepted, and acceptance is
|
|
17
|
+
* reported rather than performed: the component hands the value to `onAccept`
|
|
18
|
+
* and never touches the control. That is also what lets it wrap any control —
|
|
19
|
+
* a select, a date, a number, a combobox — where ghost text can only complete
|
|
20
|
+
* a string. Afterwards the field says it was filled by AI, says if it was
|
|
21
|
+
* edited since, and offers to put back what was there.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately no "accept all". A button that takes every suggestion at once
|
|
24
|
+
* is the review deleting itself, and every product that ships one watches its
|
|
25
|
+
* users press it without reading.
|
|
26
|
+
*/
|
|
27
|
+
interface Suggestion<T = string> {
|
|
28
|
+
value: T;
|
|
29
|
+
/** How the value reads — a select's option text, a formatted date. Defaults to the value. */
|
|
30
|
+
label?: string;
|
|
31
|
+
/** Where it came from, in the reader's language: "from the invoice header". */
|
|
32
|
+
source?: string;
|
|
33
|
+
/** 0 to 1. Shown as a number, never only as a colour. */
|
|
34
|
+
confidence?: number;
|
|
35
|
+
}
|
|
36
|
+
type SuggestionStatus = "pending" | "accepted" | "dismissed";
|
|
37
|
+
interface SuggestedValueProps<T = string> extends Omit<ComponentPropsWithRef<"div">, "children" | "id" | "aria-describedby" | "aria-invalid"> {
|
|
38
|
+
/** The field, for the buttons' names: "Accept suggestion — Company". */
|
|
39
|
+
label: string;
|
|
40
|
+
/** Null or undefined offers nothing. A different suggestion starts pending again. */
|
|
41
|
+
suggestion?: Suggestion<T> | null;
|
|
42
|
+
/**
|
|
43
|
+
* The control's current value. Lets the marker say the fill was edited
|
|
44
|
+
* since, and lets Undo restore what the field held before.
|
|
45
|
+
*/
|
|
46
|
+
value?: T;
|
|
47
|
+
/** Supply to own the status; otherwise it is kept here. */
|
|
48
|
+
status?: SuggestionStatus;
|
|
49
|
+
onAccept: (value: T, suggestion: Suggestion<T>) => void;
|
|
50
|
+
onDismiss?: (suggestion: Suggestion<T>) => void;
|
|
51
|
+
/** Called with the value the field held before the fill, so it can be put back. */
|
|
52
|
+
onRevert?: (previous: T | undefined) => void;
|
|
53
|
+
/** How to compare values. Object.is by default. */
|
|
54
|
+
equals?: (a: T, b: T) => boolean;
|
|
55
|
+
/** Confidence below this is called low, in words. */
|
|
56
|
+
lowConfidenceBelow?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Announce a suggestion when it arrives. Off by default: a form that fills
|
|
59
|
+
* twenty fields at once would narrate all twenty, and the description on
|
|
60
|
+
* the control already tells a reader when they reach the field.
|
|
61
|
+
*/
|
|
62
|
+
announce?: boolean;
|
|
63
|
+
/** The control. Receives the description, and any id or ARIA a FormControl passes down. */
|
|
64
|
+
children: ReactElement<Record<string, unknown>>;
|
|
65
|
+
id?: string;
|
|
66
|
+
"aria-describedby"?: string;
|
|
67
|
+
"aria-invalid"?: ComponentPropsWithRef<"input">["aria-invalid"];
|
|
68
|
+
}
|
|
69
|
+
declare function SuggestedValue<T = string>({ className, label, suggestion, value, status: controlledStatus, onAccept, onDismiss, onRevert, equals, lowConfidenceBelow, announce, children, id, "aria-describedby": describedByProp, "aria-invalid": ariaInvalid, ...props }: SuggestedValueProps<T>): import("react").JSX.Element;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { SuggestedValue, SuggestedValueProps, Suggestion, SuggestionStatus };
|
|
72
|
+
//# sourceMappingURL=ai-suggested-value.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-suggested-value.d.ts","names":[],"sources":["../../../src/components/ai-suggested-value/ai-suggested-value.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;UAwCiB,WAAW;EAC1B,OAAO;;EAEP;;EAEA;;EAEA;;KAGU;UAEK,oBAAoB,oBAAoB,KACvD;;EAIA;;EAEA,aAAa,WAAW;;;;;EAKxB,QAAQ;;EAER,SAAS;EACT,WAAW,OAAO,GAAG,YAAY,WAAW;EAC5C,aAAa,YAAY,WAAW;;EAEpC,YAAY,UAAU;;EAEtB,UAAU,GAAG,GAAG,GAAG;;EAEnB;;;;;;EAMA;;EAEA,UAAU,aAAa;EAEvB;EACA;EACA,iBAAiB;;iBAqBH,eAAe,cAC7B,WACA,OACA,YACA,OACA,QAAQ,kBACR,UACA,WACA,UACA,QACA,oBACA,UACA,UACA,wBACoB,iCACJ,gBACb,SACF,oBAAoB,qBAAE,IAAA"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { disabledStyles, focusRing } from "../../lib/styles.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { cloneElement, isValidElement, useId, useState } from "react";
|
|
6
|
+
//#region src/components/ai-suggested-value/ai-suggested-value.tsx
|
|
7
|
+
function defaultLabel(value) {
|
|
8
|
+
if (value === null || value === void 0) return "";
|
|
9
|
+
if (typeof value === "string") return value;
|
|
10
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
11
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
12
|
+
if (value instanceof Date) return value.toLocaleDateString();
|
|
13
|
+
return JSON.stringify(value);
|
|
14
|
+
}
|
|
15
|
+
function sameSuggestion(a, b, equals) {
|
|
16
|
+
if (!a || !b) return a === b;
|
|
17
|
+
return equals(a.value, b.value) && a.label === b.label && a.source === b.source;
|
|
18
|
+
}
|
|
19
|
+
function SuggestedValue({ className, label, suggestion, value, status: controlledStatus, onAccept, onDismiss, onRevert, equals = Object.is, lowConfidenceBelow = .7, announce = false, children, id, "aria-describedby": describedByProp, "aria-invalid": ariaInvalid, ...props }) {
|
|
20
|
+
const descriptionId = useId();
|
|
21
|
+
const [internalStatus, setInternalStatus] = useState("pending");
|
|
22
|
+
const [previous, setPrevious] = useState(void 0);
|
|
23
|
+
const [seen, setSeen] = useState(suggestion);
|
|
24
|
+
if (!sameSuggestion(seen, suggestion, equals)) {
|
|
25
|
+
setSeen(suggestion);
|
|
26
|
+
setInternalStatus("pending");
|
|
27
|
+
}
|
|
28
|
+
const status = controlledStatus ?? internalStatus;
|
|
29
|
+
const offered = suggestion ?? null;
|
|
30
|
+
const pending = offered !== null && status === "pending";
|
|
31
|
+
const accepted = offered !== null && status === "accepted";
|
|
32
|
+
const edited = accepted && value !== void 0 && !equals(value, offered.value);
|
|
33
|
+
const low = offered?.confidence !== void 0 && offered.confidence < lowConfidenceBelow;
|
|
34
|
+
const percent = offered?.confidence === void 0 ? null : new Intl.NumberFormat(void 0, {
|
|
35
|
+
style: "percent",
|
|
36
|
+
maximumFractionDigits: 0
|
|
37
|
+
}).format(offered.confidence);
|
|
38
|
+
const source = offered?.source ? ` ${offered.source}` : "";
|
|
39
|
+
let description = null;
|
|
40
|
+
if (pending) description = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
41
|
+
"Suggested:",
|
|
42
|
+
" ",
|
|
43
|
+
/* @__PURE__ */ jsx("strong", {
|
|
44
|
+
className: "font-medium text-foreground",
|
|
45
|
+
children: offered.label ?? defaultLabel(offered.value)
|
|
46
|
+
}),
|
|
47
|
+
source ? `,${source}` : null,
|
|
48
|
+
percent !== null ? /* @__PURE__ */ jsx("span", {
|
|
49
|
+
className: cn(low && "text-warning"),
|
|
50
|
+
children: low ? `. Low confidence, ${percent} — worth checking` : `. ${percent} confidence`
|
|
51
|
+
}) : null
|
|
52
|
+
] });
|
|
53
|
+
else if (accepted) description = edited ? `Filled by AI${source ? `,${source}` : ""}, then edited` : `Filled by AI${source ? `,${source}` : ""}`;
|
|
54
|
+
const describedBy = [
|
|
55
|
+
isValidElement(children) ? children.props["aria-describedby"] : void 0,
|
|
56
|
+
describedByProp,
|
|
57
|
+
description !== null ? descriptionId : null
|
|
58
|
+
].filter(Boolean).join(" ") || void 0;
|
|
59
|
+
const control = isValidElement(children) ? cloneElement(children, {
|
|
60
|
+
id: id ?? children.props.id,
|
|
61
|
+
"aria-describedby": describedBy,
|
|
62
|
+
"aria-invalid": ariaInvalid ?? children.props["aria-invalid"]
|
|
63
|
+
}) : children;
|
|
64
|
+
const accept = () => {
|
|
65
|
+
if (!offered) return;
|
|
66
|
+
setPrevious(value);
|
|
67
|
+
setInternalStatus("accepted");
|
|
68
|
+
onAccept(offered.value, offered);
|
|
69
|
+
};
|
|
70
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
71
|
+
"data-slot": "suggested-value",
|
|
72
|
+
"data-status": offered ? status : void 0,
|
|
73
|
+
"data-filled-by": accepted ? "ai" : void 0,
|
|
74
|
+
"data-edited": edited || void 0,
|
|
75
|
+
className: cn("flex flex-col gap-1.5", className),
|
|
76
|
+
...props,
|
|
77
|
+
children: [control, /* @__PURE__ */ jsxs("div", {
|
|
78
|
+
"data-slot": "suggested-value-row",
|
|
79
|
+
"aria-live": announce ? "polite" : void 0,
|
|
80
|
+
className: cn("flex flex-wrap items-center gap-x-3 gap-y-1 text-xs", pending && "rounded-md border border-primary/40 bg-primary/5 px-2.5 py-1.5"),
|
|
81
|
+
children: [
|
|
82
|
+
description !== null ? /* @__PURE__ */ jsx("p", {
|
|
83
|
+
id: descriptionId,
|
|
84
|
+
"data-slot": "suggested-value-description",
|
|
85
|
+
className: "m-0 flex-1 text-muted-foreground",
|
|
86
|
+
children: description
|
|
87
|
+
}) : null,
|
|
88
|
+
pending ? /* @__PURE__ */ jsxs("span", {
|
|
89
|
+
className: "flex items-center gap-2",
|
|
90
|
+
children: [/* @__PURE__ */ jsx(RowButton, {
|
|
91
|
+
variant: "primary",
|
|
92
|
+
"aria-label": `Accept suggestion — ${label}`,
|
|
93
|
+
onClick: accept,
|
|
94
|
+
children: "Accept"
|
|
95
|
+
}), /* @__PURE__ */ jsx(RowButton, {
|
|
96
|
+
"aria-label": `Dismiss suggestion — ${label}`,
|
|
97
|
+
onClick: () => {
|
|
98
|
+
setInternalStatus("dismissed");
|
|
99
|
+
onDismiss?.(offered);
|
|
100
|
+
},
|
|
101
|
+
children: "Dismiss"
|
|
102
|
+
})]
|
|
103
|
+
}) : null,
|
|
104
|
+
accepted && onRevert ? /* @__PURE__ */ jsx(RowButton, {
|
|
105
|
+
"aria-label": `Undo — ${label}`,
|
|
106
|
+
onClick: () => {
|
|
107
|
+
setInternalStatus("pending");
|
|
108
|
+
onRevert(previous);
|
|
109
|
+
},
|
|
110
|
+
children: "Undo"
|
|
111
|
+
}) : null
|
|
112
|
+
]
|
|
113
|
+
})]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function RowButton({ className, variant = "default", ...props }) {
|
|
117
|
+
return /* @__PURE__ */ jsx("button", {
|
|
118
|
+
type: "button",
|
|
119
|
+
className: cn("rounded-md border px-2 py-0.5 text-xs font-medium transition-colors", variant === "primary" ? "border-primary bg-primary text-primary-foreground hover:bg-primary-hover" : "border-input bg-background hover:bg-accent hover:text-accent-foreground", focusRing, disabledStyles, className),
|
|
120
|
+
...props
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { SuggestedValue };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=ai-suggested-value.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-suggested-value.js","names":[],"sources":["../../../src/components/ai-suggested-value/ai-suggested-value.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n cloneElement,\n isValidElement,\n useId,\n useState,\n type ComponentPropsWithRef,\n type ReactElement,\n type ReactNode,\n} from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * An AI-proposed value for a form control, offered beside it rather than\n * written into it.\n *\n * Autofill is the most common shape AI takes inside ordinary software —\n * enrich this contact, fill this form from the upload, guess the category —\n * and nearly every implementation writes the value straight into the field\n * as if the person had typed it. Two things go wrong. A plausible, wrong\n * value rides through on the person's own Submit, because nothing marked it\n * as needing a look. And once submitted, the record cannot tell a value the\n * model supplied from one a human typed, which is the one fact an audit later\n * needs.\n *\n * So the suggestion stays pending until it is accepted, and acceptance is\n * reported rather than performed: the component hands the value to `onAccept`\n * and never touches the control. That is also what lets it wrap any control —\n * a select, a date, a number, a combobox — where ghost text can only complete\n * a string. Afterwards the field says it was filled by AI, says if it was\n * edited since, and offers to put back what was there.\n *\n * Deliberately no \"accept all\". A button that takes every suggestion at once\n * is the review deleting itself, and every product that ships one watches its\n * users press it without reading.\n */\n\nexport interface Suggestion<T = string> {\n value: T;\n /** How the value reads — a select's option text, a formatted date. Defaults to the value. */\n label?: string;\n /** Where it came from, in the reader's language: \"from the invoice header\". */\n source?: string;\n /** 0 to 1. Shown as a number, never only as a colour. */\n confidence?: number;\n}\n\nexport type SuggestionStatus = \"pending\" | \"accepted\" | \"dismissed\";\n\nexport interface SuggestedValueProps<T = string> extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"children\" | \"id\" | \"aria-describedby\" | \"aria-invalid\"\n> {\n /** The field, for the buttons' names: \"Accept suggestion — Company\". */\n label: string;\n /** Null or undefined offers nothing. A different suggestion starts pending again. */\n suggestion?: Suggestion<T> | null;\n /**\n * The control's current value. Lets the marker say the fill was edited\n * since, and lets Undo restore what the field held before.\n */\n value?: T;\n /** Supply to own the status; otherwise it is kept here. */\n status?: SuggestionStatus;\n onAccept: (value: T, suggestion: Suggestion<T>) => void;\n onDismiss?: (suggestion: Suggestion<T>) => void;\n /** Called with the value the field held before the fill, so it can be put back. */\n onRevert?: (previous: T | undefined) => void;\n /** How to compare values. Object.is by default. */\n equals?: (a: T, b: T) => boolean;\n /** Confidence below this is called low, in words. */\n lowConfidenceBelow?: number;\n /**\n * Announce a suggestion when it arrives. Off by default: a form that fills\n * twenty fields at once would narrate all twenty, and the description on\n * the control already tells a reader when they reach the field.\n */\n announce?: boolean;\n /** The control. Receives the description, and any id or ARIA a FormControl passes down. */\n children: ReactElement<Record<string, unknown>>;\n /* Passed through to the control, so this can sit inside a FormControl. */\n id?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: ComponentPropsWithRef<\"input\">[\"aria-invalid\"];\n}\n\nfunction defaultLabel(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"bigint\") return String(value);\n if (typeof value === \"boolean\") return value ? \"Yes\" : \"No\";\n if (value instanceof Date) return value.toLocaleDateString();\n return JSON.stringify(value);\n}\n\nfunction sameSuggestion<T>(\n a: Suggestion<T> | null | undefined,\n b: Suggestion<T> | null | undefined,\n equals: (x: T, y: T) => boolean,\n): boolean {\n if (!a || !b) return a === b;\n return equals(a.value, b.value) && a.label === b.label && a.source === b.source;\n}\n\nexport function SuggestedValue<T = string>({\n className,\n label,\n suggestion,\n value,\n status: controlledStatus,\n onAccept,\n onDismiss,\n onRevert,\n equals = Object.is,\n lowConfidenceBelow = 0.7,\n announce = false,\n children,\n id,\n \"aria-describedby\": describedByProp,\n \"aria-invalid\": ariaInvalid,\n ...props\n}: SuggestedValueProps<T>) {\n const descriptionId = useId();\n\n const [internalStatus, setInternalStatus] = useState<SuggestionStatus>(\"pending\");\n const [previous, setPrevious] = useState<T | undefined>(undefined);\n\n // A new suggestion starts pending, whatever happened to the last one.\n // Compared by content rather than identity, so a parent that rebuilds the\n // object on every keystroke does not reopen a suggestion already accepted.\n const [seen, setSeen] = useState(suggestion);\n if (!sameSuggestion(seen, suggestion, equals)) {\n setSeen(suggestion);\n setInternalStatus(\"pending\");\n }\n\n const status = controlledStatus ?? internalStatus;\n const offered = suggestion ?? null;\n const pending = offered !== null && status === \"pending\";\n const accepted = offered !== null && status === \"accepted\";\n const edited = accepted && value !== undefined && !equals(value, offered.value);\n\n const low = offered?.confidence !== undefined && offered.confidence < lowConfidenceBelow;\n const percent =\n offered?.confidence === undefined\n ? null\n : new Intl.NumberFormat(undefined, { style: \"percent\", maximumFractionDigits: 0 }).format(\n offered.confidence,\n );\n\n const source = offered?.source ? ` ${offered.source}` : \"\";\n\n // What the control's description says. This is how a reader who lands on\n // the field learns there is a suggestion, without any announcement.\n let description: ReactNode = null;\n if (pending) {\n description = (\n <>\n Suggested:{\" \"}\n <strong className=\"font-medium text-foreground\">\n {offered.label ?? defaultLabel(offered.value)}\n </strong>\n {source ? `,${source}` : null}\n {percent !== null ? (\n <span className={cn(low && \"text-warning\")}>\n {low ? `. Low confidence, ${percent} — worth checking` : `. ${percent} confidence`}\n </span>\n ) : null}\n </>\n );\n } else if (accepted) {\n description = edited\n ? `Filled by AI${source ? `,${source}` : \"\"}, then edited`\n : `Filled by AI${source ? `,${source}` : \"\"}`;\n }\n\n // Merged with whatever already describes the control — a FormControl's\n // description and message, or the child's own — rather than replacing it.\n const childDescribedBy = isValidElement(children)\n ? (children.props[\"aria-describedby\"] as string | undefined)\n : undefined;\n const describedBy =\n [childDescribedBy, describedByProp, description !== null ? descriptionId : null]\n .filter(Boolean)\n .join(\" \") || undefined;\n\n const control = isValidElement(children)\n ? cloneElement(children, {\n id: id ?? children.props.id,\n \"aria-describedby\": describedBy,\n \"aria-invalid\": ariaInvalid ?? children.props[\"aria-invalid\"],\n })\n : children;\n\n const accept = () => {\n if (!offered) return;\n setPrevious(value);\n setInternalStatus(\"accepted\");\n onAccept(offered.value, offered);\n };\n\n return (\n <div\n data-slot=\"suggested-value\"\n data-status={offered ? status : undefined}\n data-filled-by={accepted ? \"ai\" : undefined}\n data-edited={edited || undefined}\n className={cn(\"flex flex-col gap-1.5\", className)}\n {...props}\n >\n {control}\n\n {/* Present from the start and never display:none, so that with `announce`\n a suggestion arriving after mount is heard — a region that enters the\n accessibility tree at the same moment as its content announces\n nothing. Empty, it has no height. */}\n <div\n data-slot=\"suggested-value-row\"\n aria-live={announce ? \"polite\" : undefined}\n className={cn(\n \"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs\",\n pending && \"rounded-md border border-primary/40 bg-primary/5 px-2.5 py-1.5\",\n )}\n >\n {description !== null ? (\n <p\n id={descriptionId}\n data-slot=\"suggested-value-description\"\n className=\"m-0 flex-1 text-muted-foreground\"\n >\n {description}\n </p>\n ) : null}\n\n {pending ? (\n <span className=\"flex items-center gap-2\">\n <RowButton\n variant=\"primary\"\n aria-label={`Accept suggestion — ${label}`}\n onClick={accept}\n >\n Accept\n </RowButton>\n <RowButton\n aria-label={`Dismiss suggestion — ${label}`}\n onClick={() => {\n setInternalStatus(\"dismissed\");\n onDismiss?.(offered);\n }}\n >\n Dismiss\n </RowButton>\n </span>\n ) : null}\n\n {accepted && onRevert ? (\n <RowButton\n aria-label={`Undo — ${label}`}\n onClick={() => {\n setInternalStatus(\"pending\");\n onRevert(previous);\n }}\n >\n Undo\n </RowButton>\n ) : null}\n </div>\n </div>\n );\n}\n\nfunction RowButton({\n className,\n variant = \"default\",\n ...props\n}: ComponentPropsWithRef<\"button\"> & { variant?: \"default\" | \"primary\" }) {\n return (\n <button\n type=\"button\"\n className={cn(\n \"rounded-md border px-2 py-0.5 text-xs font-medium transition-colors\",\n variant === \"primary\"\n ? \"border-primary bg-primary text-primary-foreground hover:bg-primary-hover\"\n : \"border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n disabledStyles,\n className,\n )}\n {...props}\n />\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/ai-suggested-value/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "ai-suggested-value",
|
|
5
|
+
title: "AI Suggested Value",
|
|
6
|
+
description: "An AI-proposed value for any form control, pending until accepted and marked afterwards.",
|
|
7
|
+
category: "ai",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
registryDependencies: [],
|
|
11
|
+
files: ["ai-suggested-value.tsx"],
|
|
12
|
+
a11y: "The suggestion is the control's description, so a reader who lands on the field hears it without any announcement — and announcing is opt-in, because a form filling twenty fields at once would narrate all twenty. The row that holds it is rendered from the start and never display:none, so when announcing is on, a suggestion arriving after mount is actually heard. Each button's name carries the field it belongs to, so a page of Accepts is a page of different buttons. Provenance and confidence are words with a number, never a colour. Acceptance is reported, not performed: the control is the consumer's, so its value, its label and its own keyboard behaviour are untouched — there is no shortcut hijacked from a select or a date field. The id and ARIA a FormControl passes down are forwarded to the control, and an existing aria-describedby is merged rather than replaced."
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { meta };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/ai-suggested-value/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"ai-suggested-value\",\n title: \"AI Suggested Value\",\n description:\n \"An AI-proposed value for any form control, pending until accepted and marked afterwards.\",\n category: \"ai\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [],\n files: [\"ai-suggested-value.tsx\"],\n a11y:\n \"The suggestion is the control's description, so a reader who lands on the field hears it \" +\n \"without any announcement — and announcing is opt-in, because a form filling twenty fields at \" +\n \"once would narrate all twenty. The row that holds it is rendered from the start and never \" +\n \"display:none, so when announcing is on, a suggestion arriving after mount is actually heard. \" +\n \"Each button's name carries the field it belongs to, so a page of Accepts is a page of \" +\n \"different buttons. Provenance and confidence are words with a number, never a colour. \" +\n \"Acceptance is reported, not performed: the control is the consumer's, so its value, its \" +\n \"label and its own keyboard behaviour are untouched — there is no shortcut hijacked from a \" +\n \"select or a date field. The id and ARIA a FormControl passes down are forwarded to the \" +\n \"control, and an existing aria-describedby is merged rather than replaced.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aACE;CACF,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC;CACvB,OAAO,CAAC,wBAAwB;CAChC,MACE;AAUJ,CAAC"}
|
|
@@ -2,7 +2,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
2
2
|
import { ComponentPropsWithRef } from "react";
|
|
3
3
|
//#region src/components/alert/alert.d.ts
|
|
4
4
|
declare const alertVariants: (props?: ({
|
|
5
|
-
variant?: "
|
|
5
|
+
variant?: "success" | "warning" | "destructive" | "default" | "info" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
/**
|
|
8
8
|
* How urgently the alert should interrupt a screen reader.
|
|
@@ -3,7 +3,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
3
3
|
import { ComponentPropsWithRef } from "react";
|
|
4
4
|
//#region src/components/avatar/avatar.d.ts
|
|
5
5
|
declare const avatarVariants: (props?: ({
|
|
6
|
-
size?: "
|
|
6
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
8
|
interface AvatarProps extends ComponentPropsWithRef<typeof Avatar.Root>, VariantProps<typeof avatarVariants> {}
|
|
9
9
|
/** Image representation of a user or entity, with a text fallback. */
|
|
@@ -2,7 +2,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
2
2
|
import { ComponentPropsWithRef } from "react";
|
|
3
3
|
//#region src/components/badge/badge.d.ts
|
|
4
4
|
declare const badgeVariants: (props?: ({
|
|
5
|
-
variant?: "
|
|
5
|
+
variant?: "success" | "warning" | "destructive" | "default" | "secondary" | "outline" | "info" | null | undefined;
|
|
6
6
|
size?: "sm" | "md" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
8
|
interface BadgeProps extends ComponentPropsWithRef<"span">, VariantProps<typeof badgeVariants> {
|
|
@@ -2,7 +2,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
2
2
|
import { ComponentPropsWithRef } from "react";
|
|
3
3
|
//#region src/components/button/button.d.ts
|
|
4
4
|
declare const buttonVariants: (props?: ({
|
|
5
|
-
variant?: "link" | "
|
|
5
|
+
variant?: "link" | "primary" | "destructive" | "secondary" | "outline" | "ghost" | null | undefined;
|
|
6
6
|
size?: "sm" | "md" | "lg" | "icon" | "icon-sm" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
8
|
interface ButtonProps extends ComponentPropsWithRef<"button">, VariantProps<typeof buttonVariants> {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
//#region src/components/confirm-typed/confirm-typed.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Type the name to confirm.
|
|
5
|
+
*
|
|
6
|
+
* The GitHub pattern for deleting a repository, and the one every product
|
|
7
|
+
* copies for the action that cannot be undone: the name of the thing has to
|
|
8
|
+
* pass through the reader's fingers before the button works. Trivial to
|
|
9
|
+
* build, absent from every component library, and usually built wrong in
|
|
10
|
+
* the one place it matters — what happens when the text does not match.
|
|
11
|
+
* The common version disables the button and says nothing, so a keyboard
|
|
12
|
+
* or screen reader user presses it, or presses Enter, and nothing happens
|
|
13
|
+
* at all.
|
|
14
|
+
*
|
|
15
|
+
* Here a mismatch is a thing that is said. The button stays reachable and
|
|
16
|
+
* pressing it, or Enter, before the text matches announces what was
|
|
17
|
+
* expected and marks the field invalid. The moment the text matches, that
|
|
18
|
+
* is announced too, once, so a reader knows the action has become
|
|
19
|
+
* available without watching the button change colour.
|
|
20
|
+
*
|
|
21
|
+
* Pasting is allowed. Blocking it is a popular piece of friction, and it
|
|
22
|
+
* punishes exactly the people who cannot type a long name easily — switch
|
|
23
|
+
* users, voice users, anyone with a tremor — while stopping nobody who can
|
|
24
|
+
* select-all and copy. The name is on screen either way; the point is that
|
|
25
|
+
* it passed through the reader's attention, not their keyboard.
|
|
26
|
+
*/
|
|
27
|
+
interface ConfirmTypedProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
|
28
|
+
/** The text that must be typed, exactly. */
|
|
29
|
+
expected: string;
|
|
30
|
+
/** What confirming does, as the button reads: "Delete project". */
|
|
31
|
+
action: string;
|
|
32
|
+
onConfirm: () => void;
|
|
33
|
+
/** While the action runs. The button stays focusable and says so. */
|
|
34
|
+
pending?: boolean;
|
|
35
|
+
/** Match regardless of case. Off by default: a name is a name. */
|
|
36
|
+
caseSensitive?: boolean;
|
|
37
|
+
variant?: "destructive" | "primary";
|
|
38
|
+
/** The typed text. Controlled, if you want it. */
|
|
39
|
+
value?: string;
|
|
40
|
+
onValueChange?: (value: string) => void;
|
|
41
|
+
/** The consequence, in a sentence or two, before the field. */
|
|
42
|
+
children?: ReactNode;
|
|
43
|
+
}
|
|
44
|
+
declare function ConfirmTyped({ className, expected, action, onConfirm, pending, caseSensitive, variant, value: valueProp, onValueChange, children, ...props }: ConfirmTypedProps): import("react").JSX.Element;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { ConfirmTyped, ConfirmTypedProps };
|
|
47
|
+
//# sourceMappingURL=confirm-typed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirm-typed.d.ts","names":[],"sources":["../../../src/components/confirm-typed/confirm-typed.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;UAwCiB,0BAA0B,KAAK;;EAE9C;;EAEA;EACA;;EAEA;;EAEA;EACA;;EAEA;EACA,iBAAiB;;EAEjB,WAAW;;iBAGG,eACd,WACA,UACA,QACA,WACA,SACA,eACA,SACA,OAAO,WACP,eACA,aACG,SACF,oCAAiB,IAAA"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { Button } from "../button/button.js";
|
|
4
|
+
import { Input } from "../input/input.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useId, useRef, useState } from "react";
|
|
7
|
+
//#region src/components/confirm-typed/confirm-typed.tsx
|
|
8
|
+
function ConfirmTyped({ className, expected, action, onConfirm, pending = false, caseSensitive = true, variant = "destructive", value: valueProp, onValueChange, children, ...props }) {
|
|
9
|
+
const id = useId();
|
|
10
|
+
const inputRef = useRef(null);
|
|
11
|
+
const [uncontrolled, setUncontrolled] = useState("");
|
|
12
|
+
const value = valueProp ?? uncontrolled;
|
|
13
|
+
const [attempted, setAttempted] = useState(false);
|
|
14
|
+
const [announcement, setAnnouncement] = useState("");
|
|
15
|
+
const normalise = (text) => caseSensitive ? text.trim() : text.trim().toLowerCase();
|
|
16
|
+
const matched = value.length > 0 && normalise(value) === normalise(expected);
|
|
17
|
+
const [wasMatched, setWasMatched] = useState(matched);
|
|
18
|
+
if (wasMatched !== matched) {
|
|
19
|
+
setWasMatched(matched);
|
|
20
|
+
if (matched) {
|
|
21
|
+
setAnnouncement(`Matches. ${action} is available.`);
|
|
22
|
+
setAttempted(false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const setValue = (next) => {
|
|
26
|
+
setUncontrolled(next);
|
|
27
|
+
onValueChange?.(next);
|
|
28
|
+
};
|
|
29
|
+
const attempt = () => {
|
|
30
|
+
if (pending) return;
|
|
31
|
+
if (matched) {
|
|
32
|
+
onConfirm();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setAttempted(true);
|
|
36
|
+
setAnnouncement(`Does not match. Type ${expected} exactly.`);
|
|
37
|
+
inputRef.current?.focus();
|
|
38
|
+
};
|
|
39
|
+
const onKeyDown = (event) => {
|
|
40
|
+
if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
attempt();
|
|
43
|
+
};
|
|
44
|
+
const mismatch = attempted && !matched;
|
|
45
|
+
const hint = matched ? "Matches." : mismatch ? `Does not match. Type ${expected} exactly.` : caseSensitive ? "Type it exactly as shown." : "Type it as shown. Case does not matter.";
|
|
46
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
47
|
+
"data-slot": "confirm-typed",
|
|
48
|
+
"data-state": matched ? "matched" : mismatch ? "mismatched" : "idle",
|
|
49
|
+
className: cn("flex flex-col gap-2", className),
|
|
50
|
+
...props,
|
|
51
|
+
children: [
|
|
52
|
+
children,
|
|
53
|
+
/* @__PURE__ */ jsxs("label", {
|
|
54
|
+
htmlFor: `${id}-input`,
|
|
55
|
+
className: "text-sm",
|
|
56
|
+
children: [
|
|
57
|
+
"To confirm, type",
|
|
58
|
+
" ",
|
|
59
|
+
/* @__PURE__ */ jsx("strong", {
|
|
60
|
+
className: "rounded bg-muted px-1 py-0.5 font-mono text-xs font-medium select-all",
|
|
61
|
+
children: expected
|
|
62
|
+
})
|
|
63
|
+
]
|
|
64
|
+
}),
|
|
65
|
+
/* @__PURE__ */ jsx(Input, {
|
|
66
|
+
ref: inputRef,
|
|
67
|
+
id: `${id}-input`,
|
|
68
|
+
value,
|
|
69
|
+
autoComplete: "off",
|
|
70
|
+
autoCapitalize: "off",
|
|
71
|
+
spellCheck: false,
|
|
72
|
+
"aria-describedby": `${id}-hint`,
|
|
73
|
+
"aria-invalid": mismatch || void 0,
|
|
74
|
+
onChange: (event) => {
|
|
75
|
+
setValue(event.target.value);
|
|
76
|
+
},
|
|
77
|
+
onKeyDown
|
|
78
|
+
}),
|
|
79
|
+
/* @__PURE__ */ jsx("p", {
|
|
80
|
+
id: `${id}-hint`,
|
|
81
|
+
"data-slot": "confirm-typed-hint",
|
|
82
|
+
className: cn("text-xs", mismatch ? "text-destructive" : matched ? "text-success" : "text-muted-foreground"),
|
|
83
|
+
children: hint
|
|
84
|
+
}),
|
|
85
|
+
/* @__PURE__ */ jsx("span", {
|
|
86
|
+
role: "status",
|
|
87
|
+
"aria-live": "polite",
|
|
88
|
+
className: "sr-only",
|
|
89
|
+
children: announcement
|
|
90
|
+
}),
|
|
91
|
+
/* @__PURE__ */ jsx(Button, {
|
|
92
|
+
variant,
|
|
93
|
+
loading: pending,
|
|
94
|
+
"data-ready": matched || void 0,
|
|
95
|
+
"aria-describedby": matched ? void 0 : `${id}-hint`,
|
|
96
|
+
className: cn("self-start", !matched && !pending && "opacity-55"),
|
|
97
|
+
onClick: attempt,
|
|
98
|
+
children: pending ? `${action}…` : action
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { ConfirmTyped };
|
|
105
|
+
|
|
106
|
+
//# sourceMappingURL=confirm-typed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirm-typed.js","names":[],"sources":["../../../src/components/confirm-typed/confirm-typed.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useId,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { Button } from \"@/components/button\";\nimport { Input } from \"@/components/input\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Type the name to confirm.\n *\n * The GitHub pattern for deleting a repository, and the one every product\n * copies for the action that cannot be undone: the name of the thing has to\n * pass through the reader's fingers before the button works. Trivial to\n * build, absent from every component library, and usually built wrong in\n * the one place it matters — what happens when the text does not match.\n * The common version disables the button and says nothing, so a keyboard\n * or screen reader user presses it, or presses Enter, and nothing happens\n * at all.\n *\n * Here a mismatch is a thing that is said. The button stays reachable and\n * pressing it, or Enter, before the text matches announces what was\n * expected and marks the field invalid. The moment the text matches, that\n * is announced too, once, so a reader knows the action has become\n * available without watching the button change colour.\n *\n * Pasting is allowed. Blocking it is a popular piece of friction, and it\n * punishes exactly the people who cannot type a long name easily — switch\n * users, voice users, anyone with a tremor — while stopping nobody who can\n * select-all and copy. The name is on screen either way; the point is that\n * it passed through the reader's attention, not their keyboard.\n */\n\nexport interface ConfirmTypedProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** The text that must be typed, exactly. */\n expected: string;\n /** What confirming does, as the button reads: \"Delete project\". */\n action: string;\n onConfirm: () => void;\n /** While the action runs. The button stays focusable and says so. */\n pending?: boolean;\n /** Match regardless of case. Off by default: a name is a name. */\n caseSensitive?: boolean;\n variant?: \"destructive\" | \"primary\";\n /** The typed text. Controlled, if you want it. */\n value?: string;\n onValueChange?: (value: string) => void;\n /** The consequence, in a sentence or two, before the field. */\n children?: ReactNode;\n}\n\nexport function ConfirmTyped({\n className,\n expected,\n action,\n onConfirm,\n pending = false,\n caseSensitive = true,\n variant = \"destructive\",\n value: valueProp,\n onValueChange,\n children,\n ...props\n}: ConfirmTypedProps) {\n const id = useId();\n const inputRef = useRef<HTMLInputElement>(null);\n\n const [uncontrolled, setUncontrolled] = useState(\"\");\n const value = valueProp ?? uncontrolled;\n\n const [attempted, setAttempted] = useState(false);\n const [announcement, setAnnouncement] = useState(\"\");\n\n // Surrounding whitespace never decides it: a trailing space is not a\n // different name, and a reader cannot see it to know why it failed.\n const normalise = (text: string) => (caseSensitive ? text.trim() : text.trim().toLowerCase());\n const matched = value.length > 0 && normalise(value) === normalise(expected);\n\n // Announce the match once, on the transition. Adjusted during render so the\n // announcement lands in the same commit as the state it describes.\n const [wasMatched, setWasMatched] = useState(matched);\n if (wasMatched !== matched) {\n setWasMatched(matched);\n if (matched) {\n setAnnouncement(`Matches. ${action} is available.`);\n setAttempted(false);\n }\n }\n\n const setValue = (next: string) => {\n setUncontrolled(next);\n onValueChange?.(next);\n };\n\n const attempt = () => {\n if (pending) return;\n if (matched) {\n onConfirm();\n return;\n }\n // Said, not swallowed. A press that does nothing tells a keyboard user\n // nothing; this one tells them what was expected.\n setAttempted(true);\n setAnnouncement(`Does not match. Type ${expected} exactly.`);\n inputRef.current?.focus();\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key !== \"Enter\" || event.nativeEvent.isComposing) return;\n event.preventDefault();\n attempt();\n };\n\n const mismatch = attempted && !matched;\n const hint = matched\n ? \"Matches.\"\n : mismatch\n ? `Does not match. Type ${expected} exactly.`\n : caseSensitive\n ? \"Type it exactly as shown.\"\n : \"Type it as shown. Case does not matter.\";\n\n return (\n <div\n data-slot=\"confirm-typed\"\n data-state={matched ? \"matched\" : mismatch ? \"mismatched\" : \"idle\"}\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n >\n {children}\n\n <label htmlFor={`${id}-input`} className=\"text-sm\">\n To confirm, type{\" \"}\n <strong className=\"rounded bg-muted px-1 py-0.5 font-mono text-xs font-medium select-all\">\n {expected}\n </strong>\n </label>\n\n <Input\n ref={inputRef}\n id={`${id}-input`}\n value={value}\n autoComplete=\"off\"\n autoCapitalize=\"off\"\n spellCheck={false}\n aria-describedby={`${id}-hint`}\n aria-invalid={mismatch || undefined}\n onChange={(event) => {\n setValue(event.target.value);\n }}\n onKeyDown={onKeyDown}\n />\n\n <p\n id={`${id}-hint`}\n data-slot=\"confirm-typed-hint\"\n className={cn(\n \"text-xs\",\n mismatch ? \"text-destructive\" : matched ? \"text-success\" : \"text-muted-foreground\",\n )}\n >\n {hint}\n </p>\n\n {/* Present from the first render, so the first announcement is heard. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {announcement}\n </span>\n\n <Button\n variant={variant}\n loading={pending}\n data-ready={matched || undefined}\n aria-describedby={matched ? undefined : `${id}-hint`}\n // Dimmed rather than disabled while the text does not match: it stays\n // reachable, and pressing it says why nothing happened.\n className={cn(\"self-start\", !matched && !pending && \"opacity-55\")}\n onClick={attempt}\n >\n {pending ? `${action}…` : action}\n </Button>\n </div>\n );\n}\n"],"mappings":";;;;;;;AA0DA,SAAgB,aAAa,EAC3B,WACA,UACA,QACA,WACA,UAAU,OACV,gBAAgB,MAChB,UAAU,eACV,OAAO,WACP,eACA,UACA,GAAG,SACiB;CACpB,MAAM,KAAK,MAAM;CACjB,MAAM,WAAW,OAAyB,IAAI;CAE9C,MAAM,CAAC,cAAc,mBAAmB,SAAS,EAAE;CACnD,MAAM,QAAQ,aAAa;CAE3B,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,cAAc,mBAAmB,SAAS,EAAE;CAInD,MAAM,aAAa,SAAkB,gBAAgB,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC,CAAC,YAAY;CAC3F,MAAM,UAAU,MAAM,SAAS,KAAK,UAAU,KAAK,MAAM,UAAU,QAAQ;CAI3E,MAAM,CAAC,YAAY,iBAAiB,SAAS,OAAO;CACpD,IAAI,eAAe,SAAS;EAC1B,cAAc,OAAO;EACrB,IAAI,SAAS;GACX,gBAAgB,YAAY,OAAO,eAAe;GAClD,aAAa,KAAK;EACpB;CACF;CAEA,MAAM,YAAY,SAAiB;EACjC,gBAAgB,IAAI;EACpB,gBAAgB,IAAI;CACtB;CAEA,MAAM,gBAAgB;EACpB,IAAI,SAAS;EACb,IAAI,SAAS;GACX,UAAU;GACV;EACF;EAGA,aAAa,IAAI;EACjB,gBAAgB,wBAAwB,SAAS,UAAU;EAC3D,SAAS,SAAS,MAAM;CAC1B;CAEA,MAAM,aAAa,UAA2C;EAC5D,IAAI,MAAM,QAAQ,WAAW,MAAM,YAAY,aAAa;EAC5D,MAAM,eAAe;EACrB,QAAQ;CACV;CAEA,MAAM,WAAW,aAAa,CAAC;CAC/B,MAAM,OAAO,UACT,aACA,WACE,wBAAwB,SAAS,aACjC,gBACE,8BACA;CAER,OACE,qBAAC,OAAD;EACE,aAAU;EACV,cAAY,UAAU,YAAY,WAAW,eAAe;EAC5D,WAAW,GAAG,uBAAuB,SAAS;EAC9C,GAAI;EAJN,UAAA;GAMG;GAED,qBAAC,SAAD;IAAO,SAAS,GAAG,GAAG;IAAS,WAAU;IAAzC,UAAA;KAAmD;KAChC;KACjB,oBAAC,UAAD;MAAQ,WAAU;MACf,UAAA;KACK,CAAA;IACH;;GAEP,oBAAC,OAAD;IACE,KAAK;IACL,IAAI,GAAG,GAAG;IACH;IACP,cAAa;IACb,gBAAe;IACf,YAAY;IACZ,oBAAkB,GAAG,GAAG;IACxB,gBAAc,YAAY,KAAA;IAC1B,WAAW,UAAU;KACnB,SAAS,MAAM,OAAO,KAAK;IAC7B;IACW;GACZ,CAAA;GAED,oBAAC,KAAD;IACE,IAAI,GAAG,GAAG;IACV,aAAU;IACV,WAAW,GACT,WACA,WAAW,qBAAqB,UAAU,iBAAiB,uBAC7D;IAEC,UAAA;GACA,CAAA;GAGH,oBAAC,QAAD;IAAM,MAAK;IAAS,aAAU;IAAS,WAAU;IAC9C,UAAA;GACG,CAAA;GAEN,oBAAC,QAAD;IACW;IACT,SAAS;IACT,cAAY,WAAW,KAAA;IACvB,oBAAkB,UAAU,KAAA,IAAY,GAAG,GAAG;IAG9C,WAAW,GAAG,cAAc,CAAC,WAAW,CAAC,WAAW,YAAY;IAChE,SAAS;IAER,UAAA,UAAU,GAAG,OAAO,KAAK;GACpB,CAAA;EACL;;AAET"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/confirm-typed/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "confirm-typed",
|
|
5
|
+
title: "Confirm Typed",
|
|
6
|
+
description: "Type the name to confirm — and be told, not ignored, when it does not match.",
|
|
7
|
+
category: "form",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
registryDependencies: ["button", "input"],
|
|
11
|
+
files: ["confirm-typed.tsx"],
|
|
12
|
+
a11y: "The button stays reachable while the text does not match, dimmed rather than disabled, and pressing it or Enter announces what was expected, marks the field invalid and returns focus to it — the common version disables the button and says nothing, so a keyboard user cannot tell whether anything happened. The match is announced once, on the transition, naming the action that became available, so nobody has to watch the button change colour. Typing itself stays silent, because a verdict on every keystroke is noise. The hint is the field's description and the button's, and the status region exists from the first render so the first announcement is heard. Pasting is allowed: blocking it punishes people who cannot type a long name easily and stops nobody who can select-all and copy."
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { meta };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/confirm-typed/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"confirm-typed\",\n title: \"Confirm Typed\",\n description: \"Type the name to confirm — and be told, not ignored, when it does not match.\",\n category: \"form\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [\"button\", \"input\"],\n files: [\"confirm-typed.tsx\"],\n a11y:\n \"The button stays reachable while the text does not match, dimmed rather than disabled, and \" +\n \"pressing it or Enter announces what was expected, marks the field invalid and returns focus \" +\n \"to it — the common version disables the button and says nothing, so a keyboard user cannot \" +\n \"tell whether anything happened. The match is announced once, on the transition, naming the \" +\n \"action that became available, so nobody has to watch the button change colour. Typing \" +\n \"itself stays silent, because a verdict on every keystroke is noise. The hint is the field's \" +\n \"description and the button's, and the status region exists from the first render so the \" +\n \"first announcement is heard. Pasting is allowed: blocking it punishes people who cannot \" +\n \"type a long name easily and stops nobody who can select-all and copy.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aAAa;CACb,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC,UAAU,OAAO;CACxC,OAAO,CAAC,mBAAmB;CAC3B,MACE;AASJ,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
//#region src/components/cron-editor/cron-editor.d.ts
|
|
3
|
+
interface CronProps extends Omit<ComponentPropsWithRef<"div">, "defaultValue" | "onChange" | "children"> {
|
|
4
|
+
/** The expression. Controlled. */
|
|
5
|
+
value?: string;
|
|
6
|
+
defaultValue?: string;
|
|
7
|
+
onValueChange?: (expression: string) => void;
|
|
8
|
+
/** IANA zone the expression's times are in. Defaults to the runtime zone. */
|
|
9
|
+
timeZone?: string;
|
|
10
|
+
locale?: string;
|
|
11
|
+
/** The instant next runs are counted from. Pass one that changes when you refresh. */
|
|
12
|
+
now?: Date;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
declare function Cron({ className, value: valueProp, defaultValue, onValueChange, timeZone, locale, now, children, ...props }: CronProps): import("react").JSX.Element;
|
|
16
|
+
type CronBuilderProps = Omit<ComponentPropsWithRef<"div">, "children">;
|
|
17
|
+
declare function CronBuilder({ className, ...props }: CronBuilderProps): import("react").JSX.Element;
|
|
18
|
+
type CronExpressionProps = Omit<ComponentPropsWithRef<"div">, "children">;
|
|
19
|
+
/**
|
|
20
|
+
* The expression, editable, with its reading underneath — or the reason it is
|
|
21
|
+
* invalid. An invalid entry is never applied: the last valid schedule stands
|
|
22
|
+
* until the field says something that parses.
|
|
23
|
+
*/
|
|
24
|
+
declare function CronExpression({ className, ...props }: CronExpressionProps): import("react").JSX.Element;
|
|
25
|
+
interface CronNextRunsProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
|
26
|
+
count?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* When it will fire, in the zone it is defined in. The zone is in the
|
|
30
|
+
* heading because a time with no zone is the classic scheduling mistake, and
|
|
31
|
+
* a schedule that never runs says so rather than showing an empty list.
|
|
32
|
+
*/
|
|
33
|
+
declare function CronNextRuns({ className, count, ...props }: CronNextRunsProps): import("react").JSX.Element | null;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { Cron, CronBuilder, CronBuilderProps, CronExpression, CronExpressionProps, CronNextRuns, CronNextRunsProps, CronProps };
|
|
36
|
+
//# sourceMappingURL=cron-editor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-editor.d.ts","names":[],"sources":["../../../src/components/cron-editor/cron-editor.tsx"],"mappings":";;UA0EiB,kBAAkB,KACjC;;EAIA;EACA;EACA,iBAAiB;;EAEjB;EACA;;EAEA,MAAM;EACN,UAAU;;iBAGI,OACd,WACA,OAAO,WACP,cACA,eACA,UACA,QACA,KACA,aACG,SACF,4BAAS,IAAA;KAwKA,mBAAmB,KAAK;iBAEpB,cAAc,cAAc,SAAS,mCAAgB,IAAA;KA0QzD,sBAAsB,KAAK;;;;;;iBAOvB,iBAAiB,cAAc,SAAS,sCAAmB,IAAA;UAqE1D,0BAA0B,KAAK;EAC9C;;;;;;;iBAQc,eAAe,WAAW,UAAc,SAAS,oCAAiB,IAAA"}
|