@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,315 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ComponentPropsWithRef,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
import { Button } from "@/components/button";
|
|
13
|
+
import { Spinner } from "@/components/spinner";
|
|
14
|
+
import { focusRing } from "@/lib/styles";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* "Add this record at your DNS provider."
|
|
19
|
+
*
|
|
20
|
+
* Vercel, Resend, Cloudflare, Postmark, Google Workspace and every product
|
|
21
|
+
* that verifies a domain or routes its mail draws this card by hand, and
|
|
22
|
+
* they all learn the same three things the hard way.
|
|
23
|
+
*
|
|
24
|
+
* The parts are copied separately. A provider's form has a Name field, a
|
|
25
|
+
* Type field and a Value field, so one button that copies the whole line
|
|
26
|
+
* copies something nobody can paste anywhere.
|
|
27
|
+
*
|
|
28
|
+
* The Name field is a trap. Some providers want the host relative to the
|
|
29
|
+
* zone, `_dmarc`; some want the full name, `_dmarc.acme.com`; and some take
|
|
30
|
+
* the relative form and append the zone themselves, so a full name pasted
|
|
31
|
+
* in becomes `_dmarc.acme.com.acme.com` and the check fails for a reason
|
|
32
|
+
* nobody can see. So the host is shown both ways, with the sentence that
|
|
33
|
+
* explains which to use.
|
|
34
|
+
*
|
|
35
|
+
* A failed check says what was found. "Not verified" sends people back to
|
|
36
|
+
* their provider to stare at a record that is correct and has not
|
|
37
|
+
* propagated yet; "found v=spf1 -all, expected v=spf1 include:…" sends them
|
|
38
|
+
* to the typo. And nothing found at all is said as nothing found, with how
|
|
39
|
+
* long that can take, because it is not the same as wrong.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export type DnsRecordStatus = "pending" | "checking" | "verified" | "failed";
|
|
43
|
+
|
|
44
|
+
export interface DnsHostForms {
|
|
45
|
+
/** Relative to the zone: `_dmarc`, or `@` for the zone itself. */
|
|
46
|
+
relative: string;
|
|
47
|
+
/** Fully qualified: `_dmarc.acme.com`. */
|
|
48
|
+
full: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The host both ways a provider might want it. Accepts either form and a
|
|
53
|
+
* trailing dot, so the record can be declared however the backend has it.
|
|
54
|
+
*/
|
|
55
|
+
export function dnsHostForms(name: string, zone: string): DnsHostForms {
|
|
56
|
+
const cleanZone = zone.replace(/\.$/, "");
|
|
57
|
+
const cleanName = name.replace(/\.$/, "");
|
|
58
|
+
if (cleanName === "@" || cleanName === "" || cleanName === cleanZone) {
|
|
59
|
+
return { relative: "@", full: cleanZone };
|
|
60
|
+
}
|
|
61
|
+
if (cleanName.endsWith(`.${cleanZone}`)) {
|
|
62
|
+
return { relative: cleanName.slice(0, -cleanZone.length - 1), full: cleanName };
|
|
63
|
+
}
|
|
64
|
+
return { relative: cleanName, full: `${cleanName}.${cleanZone}` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DnsRecordProps extends Omit<ComponentPropsWithRef<"section">, "children"> {
|
|
68
|
+
type: string;
|
|
69
|
+
/** The host, relative or fully qualified. */
|
|
70
|
+
name: string;
|
|
71
|
+
value: string;
|
|
72
|
+
/** The zone the record belongs to. Enables the host to be shown both ways. */
|
|
73
|
+
zone?: string;
|
|
74
|
+
ttl?: number | string;
|
|
75
|
+
priority?: number;
|
|
76
|
+
/** What the record is for, in the reader's language: "Proves you own acme.com". */
|
|
77
|
+
purpose?: string;
|
|
78
|
+
status?: DnsRecordStatus;
|
|
79
|
+
/** What the check found instead, when it failed. The useful part. */
|
|
80
|
+
found?: string[];
|
|
81
|
+
checkedAt?: Date;
|
|
82
|
+
locale?: string;
|
|
83
|
+
onCheck?: () => void;
|
|
84
|
+
children?: ReactNode;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type CopyState = "idle" | "copied" | "error";
|
|
88
|
+
|
|
89
|
+
export function DnsRecord({
|
|
90
|
+
className,
|
|
91
|
+
type,
|
|
92
|
+
name,
|
|
93
|
+
value,
|
|
94
|
+
zone,
|
|
95
|
+
ttl,
|
|
96
|
+
priority,
|
|
97
|
+
purpose,
|
|
98
|
+
status = "pending",
|
|
99
|
+
found,
|
|
100
|
+
checkedAt,
|
|
101
|
+
locale,
|
|
102
|
+
onCheck,
|
|
103
|
+
children,
|
|
104
|
+
...props
|
|
105
|
+
}: DnsRecordProps) {
|
|
106
|
+
const id = useId();
|
|
107
|
+
const forms = zone ? dnsHostForms(name, zone) : null;
|
|
108
|
+
const host = forms ? forms.relative : name;
|
|
109
|
+
|
|
110
|
+
// Which part was copied, and whether it worked, so the button that was
|
|
111
|
+
// pressed is the one that says so.
|
|
112
|
+
const [copy, setCopy] = useState<{ part: string; state: CopyState } | null>(null);
|
|
113
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
114
|
+
useEffect(
|
|
115
|
+
() => () => {
|
|
116
|
+
if (timer.current) clearTimeout(timer.current);
|
|
117
|
+
},
|
|
118
|
+
[],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const copyPart = (part: string, text: string) => {
|
|
122
|
+
const settle = (state: CopyState) => {
|
|
123
|
+
setCopy({ part, state });
|
|
124
|
+
if (timer.current) clearTimeout(timer.current);
|
|
125
|
+
timer.current = setTimeout(() => {
|
|
126
|
+
setCopy(null);
|
|
127
|
+
}, 2000);
|
|
128
|
+
};
|
|
129
|
+
Promise.resolve()
|
|
130
|
+
.then(() => navigator.clipboard.writeText(text))
|
|
131
|
+
.then(
|
|
132
|
+
() => {
|
|
133
|
+
settle("copied");
|
|
134
|
+
},
|
|
135
|
+
() => {
|
|
136
|
+
settle("error");
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const statusText =
|
|
142
|
+
status === "verified"
|
|
143
|
+
? "Verified"
|
|
144
|
+
: status === "checking"
|
|
145
|
+
? "Checking…"
|
|
146
|
+
: status === "failed"
|
|
147
|
+
? found && found.length > 0
|
|
148
|
+
? "Not verified. Something else was found."
|
|
149
|
+
: "Not verified. Nothing found yet — DNS changes can take up to 48 hours to appear."
|
|
150
|
+
: "Not checked yet";
|
|
151
|
+
|
|
152
|
+
const checked =
|
|
153
|
+
checkedAt !== undefined
|
|
154
|
+
? new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(
|
|
155
|
+
checkedAt,
|
|
156
|
+
)
|
|
157
|
+
: null;
|
|
158
|
+
|
|
159
|
+
const parts: { key: string; label: string; text: string; note?: ReactNode }[] = [
|
|
160
|
+
{ key: "type", label: "Type", text: type },
|
|
161
|
+
{
|
|
162
|
+
key: "name",
|
|
163
|
+
label: "Name",
|
|
164
|
+
text: host,
|
|
165
|
+
note: forms ? (
|
|
166
|
+
forms.relative === "@" ? (
|
|
167
|
+
<>
|
|
168
|
+
Some providers want <code className="font-mono">{forms.full}</code> or leave it
|
|
169
|
+
blank instead of <code className="font-mono">@</code>.
|
|
170
|
+
</>
|
|
171
|
+
) : (
|
|
172
|
+
<>
|
|
173
|
+
Some providers want the full name, <code className="font-mono">{forms.full}</code>.
|
|
174
|
+
If the field already ends in <code className="font-mono">{zone}</code>, enter just
|
|
175
|
+
the part shown.
|
|
176
|
+
</>
|
|
177
|
+
)
|
|
178
|
+
) : undefined,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
key: "value",
|
|
182
|
+
label: "Value",
|
|
183
|
+
text: value,
|
|
184
|
+
note:
|
|
185
|
+
type.toUpperCase() === "TXT"
|
|
186
|
+
? "Enter without quotes; your provider adds them if it needs to."
|
|
187
|
+
: undefined,
|
|
188
|
+
},
|
|
189
|
+
...(priority !== undefined
|
|
190
|
+
? [{ key: "priority", label: "Priority", text: String(priority) }]
|
|
191
|
+
: []),
|
|
192
|
+
...(ttl !== undefined ? [{ key: "ttl", label: "TTL", text: String(ttl) }] : []),
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<section
|
|
197
|
+
data-slot="dns-record"
|
|
198
|
+
data-status={status}
|
|
199
|
+
aria-labelledby={`${id}-heading`}
|
|
200
|
+
aria-busy={status === "checking" || undefined}
|
|
201
|
+
className={cn(
|
|
202
|
+
"flex flex-col gap-3 rounded-lg border border-border bg-card p-4",
|
|
203
|
+
className,
|
|
204
|
+
)}
|
|
205
|
+
{...props}
|
|
206
|
+
>
|
|
207
|
+
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
208
|
+
<h3 id={`${id}-heading`} className="text-sm font-medium">
|
|
209
|
+
{type} record
|
|
210
|
+
{purpose ? (
|
|
211
|
+
<span className="font-normal text-muted-foreground"> · {purpose}</span>
|
|
212
|
+
) : null}
|
|
213
|
+
</h3>
|
|
214
|
+
{/* Status as a word, and announced when it changes: a check was asked
|
|
215
|
+
for, and the answer should arrive where the asker is. */}
|
|
216
|
+
<p
|
|
217
|
+
data-slot="dns-record-status"
|
|
218
|
+
role="status"
|
|
219
|
+
aria-live="polite"
|
|
220
|
+
className={cn(
|
|
221
|
+
"flex items-center gap-1.5 text-xs",
|
|
222
|
+
status === "verified" && "text-success",
|
|
223
|
+
status === "failed" && "text-destructive",
|
|
224
|
+
(status === "pending" || status === "checking") && "text-muted-foreground",
|
|
225
|
+
)}
|
|
226
|
+
>
|
|
227
|
+
{status === "checking" ? <Spinner size="sm" /> : null}
|
|
228
|
+
{statusText}
|
|
229
|
+
{checked && status !== "checking" ? (
|
|
230
|
+
<span className="text-muted-foreground"> · checked {checked}</span>
|
|
231
|
+
) : null}
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{children}
|
|
236
|
+
|
|
237
|
+
<dl className="m-0 grid gap-x-4 gap-y-2 sm:grid-cols-[auto_1fr]">
|
|
238
|
+
{parts.map((part) => (
|
|
239
|
+
<div key={part.key} className="contents">
|
|
240
|
+
<dt className="text-xs font-medium text-muted-foreground sm:pt-1.5">
|
|
241
|
+
{part.label}
|
|
242
|
+
</dt>
|
|
243
|
+
<dd className="m-0 flex flex-col gap-1">
|
|
244
|
+
<div className="flex items-start gap-2">
|
|
245
|
+
<code
|
|
246
|
+
data-slot="dns-record-part"
|
|
247
|
+
data-part={part.key}
|
|
248
|
+
className="min-w-0 flex-1 rounded-md bg-muted px-2 py-1 font-mono text-xs break-all"
|
|
249
|
+
>
|
|
250
|
+
{part.text}
|
|
251
|
+
</code>
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
aria-label={`Copy ${part.label.toLowerCase()}`}
|
|
255
|
+
data-state={copy?.part === part.key ? copy.state : "idle"}
|
|
256
|
+
className={cn(
|
|
257
|
+
"shrink-0 rounded-md border border-input bg-background px-2 py-1 text-xs font-medium",
|
|
258
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
259
|
+
focusRing,
|
|
260
|
+
)}
|
|
261
|
+
onClick={() => {
|
|
262
|
+
copyPart(part.key, part.text);
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
{copy?.part === part.key && copy.state === "copied" ? "Copied" : "Copy"}
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
{copy?.part === part.key && copy.state === "error" ? (
|
|
269
|
+
<p data-slot="dns-record-copy-error" className="text-xs text-destructive">
|
|
270
|
+
Could not copy. Select it and copy by hand.
|
|
271
|
+
</p>
|
|
272
|
+
) : null}
|
|
273
|
+
{part.note ? <p className="text-xs text-muted-foreground">{part.note}</p> : null}
|
|
274
|
+
</dd>
|
|
275
|
+
</div>
|
|
276
|
+
))}
|
|
277
|
+
</dl>
|
|
278
|
+
|
|
279
|
+
{status === "failed" && found && found.length > 0 ? (
|
|
280
|
+
<div
|
|
281
|
+
data-slot="dns-record-found"
|
|
282
|
+
className="rounded-md border border-destructive/40 bg-destructive/5 p-3 text-xs"
|
|
283
|
+
>
|
|
284
|
+
<p className="font-medium text-destructive">Found instead:</p>
|
|
285
|
+
<ul className="m-0 mt-1 list-none p-0">
|
|
286
|
+
{found.map((entry) => (
|
|
287
|
+
<li key={entry}>
|
|
288
|
+
<code className="font-mono break-all">{entry}</code>
|
|
289
|
+
</li>
|
|
290
|
+
))}
|
|
291
|
+
</ul>
|
|
292
|
+
</div>
|
|
293
|
+
) : null}
|
|
294
|
+
|
|
295
|
+
{/* A separate region from the verification status, so "Copied name"
|
|
296
|
+
never replaces or interrupts a check result that is being read.
|
|
297
|
+
Present from the start, so the first copy is heard. */}
|
|
298
|
+
<span role="status" aria-live="polite" className="sr-only">
|
|
299
|
+
{copy?.state === "copied"
|
|
300
|
+
? `Copied ${copy.part}`
|
|
301
|
+
: copy?.state === "error"
|
|
302
|
+
? `Could not copy ${copy.part}. Select it and copy by hand.`
|
|
303
|
+
: ""}
|
|
304
|
+
</span>
|
|
305
|
+
|
|
306
|
+
{onCheck ? (
|
|
307
|
+
<div className="flex items-center gap-2">
|
|
308
|
+
<Button variant="outline" size="sm" loading={status === "checking"} onClick={onCheck}>
|
|
309
|
+
{status === "verified" ? "Check again" : "Check now"}
|
|
310
|
+
</Button>
|
|
311
|
+
</div>
|
|
312
|
+
) : null}
|
|
313
|
+
</section>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineMeta } from "@/registry/schema";
|
|
2
|
+
|
|
3
|
+
export const meta = defineMeta({
|
|
4
|
+
name: "dns-record",
|
|
5
|
+
title: "DNS Record",
|
|
6
|
+
description:
|
|
7
|
+
"Add this record at your provider: parts copied separately, checked with what was found.",
|
|
8
|
+
category: "data",
|
|
9
|
+
status: "stable",
|
|
10
|
+
dependencies: [],
|
|
11
|
+
registryDependencies: ["button", "spinner"],
|
|
12
|
+
files: ["dns-record.tsx"],
|
|
13
|
+
a11y:
|
|
14
|
+
"The card is a region named by the record's type and purpose. Each part is a definition " +
|
|
15
|
+
"with its own copy button, named by the part it copies, so five Copy buttons are five " +
|
|
16
|
+
"different buttons. Verification status is a word in a polite live region, so the answer to " +
|
|
17
|
+
"a check arrives where it was asked for, and the card is aria-busy while checking. Copy " +
|
|
18
|
+
"results are announced in a separate region, present from the start, so a copy never " +
|
|
19
|
+
"replaces a check result mid-sentence; failure is announced with what to do instead. What " +
|
|
20
|
+
"the check found is a list, not a colour.",
|
|
21
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { PermissionMatrix, type PermissionMatrixProps } from "./permission-matrix";
|
|
2
|
+
export {
|
|
3
|
+
cellState,
|
|
4
|
+
countGranted,
|
|
5
|
+
groupPermissions,
|
|
6
|
+
groupState,
|
|
7
|
+
inheritedFrom,
|
|
8
|
+
type CellState,
|
|
9
|
+
type Grants,
|
|
10
|
+
type GroupState,
|
|
11
|
+
type Permission,
|
|
12
|
+
type PermissionGroup,
|
|
13
|
+
type Role,
|
|
14
|
+
} from "./permission-model";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineMeta } from "@/registry/schema";
|
|
2
|
+
|
|
3
|
+
export const meta = defineMeta({
|
|
4
|
+
name: "permission-matrix",
|
|
5
|
+
title: "Permission Matrix",
|
|
6
|
+
description:
|
|
7
|
+
"Roles across, permissions down, with inheritance, locked roles and one tab stop.",
|
|
8
|
+
category: "data",
|
|
9
|
+
status: "stable",
|
|
10
|
+
dependencies: [],
|
|
11
|
+
registryDependencies: ["checkbox"],
|
|
12
|
+
files: ["permission-model.ts", "permission-matrix.tsx"],
|
|
13
|
+
a11y:
|
|
14
|
+
'A grid in the WAI-ARIA sense: role="grid" with one tab stop, arrow keys between cells, Home ' +
|
|
15
|
+
"and End along a row and Ctrl with them across the whole matrix — the right call for a " +
|
|
16
|
+
"matrix that is operated, where it was the wrong one for a diff that is read. Every checkbox " +
|
|
17
|
+
'is named by both coordinates, "Delete projects for Editor", so a reader arriving by arrow ' +
|
|
18
|
+
"key knows where they are without re-reading the headers. An inherited grant is a checked " +
|
|
19
|
+
"box that cannot be changed here, marked aria-disabled rather than disabled so it stays in " +
|
|
20
|
+
"the keyboard path, with the role it came from as its description. A locked role says in " +
|
|
21
|
+
"each cell's description that nothing can be changed. Group toggles are tri-state, and the " +
|
|
22
|
+
"column header counts each role's grants in text.",
|
|
23
|
+
});
|