milestone 23 s2: react aria primitives and their cluster

This commit is contained in:
2026-08-12 22:12:18 +02:00
parent 994bbf922c
commit 01e455c8af
32 changed files with 2316 additions and 532 deletions
+147 -38
View File
@@ -1,5 +1,6 @@
import { useId, useState, type FormEvent } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";
import * as stylex from "@stylexjs/stylex";
import {
forwardZoneCreateMutation,
forwardZoneDeleteMutation,
@@ -8,17 +9,97 @@ import {
} from "@/lib/queries";
import type { ForwardZone, ForwardZoneInput } from "@/lib/types";
import InlineError from "@/lib/InlineError";
import ConfirmDialog from "@/ui/ConfirmDialog";
import { useCrudForm } from "@/ui/useCrudForm";
import {
formCardClass,
inputClass,
largeButtonClass,
largePrimaryButtonClass,
rowButtonClass,
tableWrapClass,
} from "@/ui/classes";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
import { READ_ONLY_HINT, useReadOnlyConfig } from "@/features/settings/authority";
const styles = stylex.create({
formHeading: {
fontWeight: 500,
},
fieldLabel: {
display: "block",
fontSize: "0.875rem",
lineHeight: "1.25rem",
fontWeight: 500,
},
buttonRow: {
display: "flex",
gap: "0.5rem",
},
toolbar: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginTop: "1rem",
},
intro: {
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.textMuted,
},
table: {
width: "100%",
textAlign: "left",
fontSize: "0.875rem",
lineHeight: "1.25rem",
},
headRow: {
borderBottomWidth: 1,
borderBottomStyle: "solid",
borderBottomColor: colors.border,
color: colors.textMuted,
},
headCell: {
paddingBlock: "0.5rem",
paddingRight: "1rem",
fontWeight: 500,
},
headCellLast: {
paddingBlock: "0.5rem",
},
bodyRow: {
borderBottomWidth: 1,
borderBottomStyle: "solid",
borderBottomColor: colors.border,
},
cell: {
paddingBlock: "0.5rem",
paddingRight: "1rem",
},
mono: {
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
},
emptyCell: {
paddingBlock: "1rem",
color: colors.textMuted,
},
actionCell: {
paddingBlock: "0.5rem",
textAlign: "right",
whiteSpace: "nowrap",
},
dangerText: {
color: colors.danger,
},
dimWhenDisabled: {
opacity: { default: 1, ":disabled": 0.5 },
},
srOnly: {
position: "absolute",
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: "hidden",
clipPath: "inset(50%)",
whiteSpace: "nowrap",
borderWidth: 0,
},
});
function ZoneForm({
initial,
busy,
@@ -44,10 +125,12 @@ function ZoneForm({
}
return (
<form onSubmit={submit} className={formCardClass}>
<h3 className="font-medium">{initial === undefined ? "New forward zone" : `Edit ${initial.zone}`}</h3>
<form onSubmit={submit} {...stylex.props(shared.formCard)}>
<h3 {...stylex.props(styles.formHeading)}>
{initial === undefined ? "New forward zone" : `Edit ${initial.zone}`}
</h3>
<div>
<label htmlFor={`${id}-zone`} className="block text-sm font-medium">
<label htmlFor={`${id}-zone`} {...stylex.props(styles.fieldLabel)}>
Zone
</label>
<input
@@ -56,11 +139,11 @@ function ZoneForm({
value={zone}
onChange={(event) => setZone(event.target.value)}
placeholder="lan.home"
className={inputClass}
{...stylex.props(shared.input, shared.focusRing)}
/>
</div>
<div>
<label htmlFor={`${id}-resolver`} className="block text-sm font-medium">
<label htmlFor={`${id}-resolver`} {...stylex.props(styles.fieldLabel)}>
Resolver
</label>
<input
@@ -69,19 +152,19 @@ function ZoneForm({
value={resolver}
onChange={(event) => setResolver(event.target.value)}
placeholder="udp://192.168.1.1:53"
className={inputClass}
{...stylex.props(shared.input, shared.focusRing)}
/>
</div>
<div className="flex gap-2">
<div {...stylex.props(styles.buttonRow)}>
<button
type="submit"
disabled={busy || readOnly}
title={readOnly ? READ_ONLY_HINT : undefined}
className={largePrimaryButtonClass}
{...stylex.props(shared.largePrimaryButton, shared.focusRing)}
>
{busy ? "Saving…" : "Save"}
</button>
<button type="button" onClick={onCancel} className={largeButtonClass}>
<button type="button" onClick={onCancel} {...stylex.props(shared.largeButton, shared.focusRing)}>
Cancel
</button>
</div>
@@ -92,10 +175,19 @@ function ZoneForm({
export default function ZonesTab() {
const zones = useSuspenseQuery(forwardZonesQuery()).data;
const { create, update, remove, form, openForm, closeForm, onSubmit, onDelete } = useCrudForm<
ForwardZone,
ForwardZoneInput
>({
const {
create,
update,
remove,
form,
openForm,
closeForm,
onSubmit,
onDelete,
pendingDelete,
confirmPendingDelete,
cancelPendingDelete,
} = useCrudForm<ForwardZone, ForwardZoneInput>({
create: forwardZoneCreateMutation,
update: forwardZoneUpdateMutation,
remove: forwardZoneDeleteMutation,
@@ -105,8 +197,8 @@ export default function ZonesTab() {
return (
<div>
<div className="mt-4 flex items-center justify-between">
<p className="text-sm text-zinc-500">
<div {...stylex.props(styles.toolbar)}>
<p {...stylex.props(styles.intro)}>
Names under these zones go to their own resolver. Changes apply live.
</p>
<button
@@ -114,7 +206,7 @@ export default function ZonesTab() {
onClick={() => openForm({ mode: "create" })}
disabled={readOnly}
title={readOnly ? READ_ONLY_HINT : undefined}
className={largePrimaryButtonClass}
{...stylex.props(shared.largePrimaryButton, shared.focusRing)}
>
Add zone
</button>
@@ -129,31 +221,31 @@ export default function ZonesTab() {
onCancel={closeForm}
/>
)}
<div className={tableWrapClass}>
<table className="w-full text-left text-sm">
<div {...stylex.props(shared.tableWrap)}>
<table {...stylex.props(styles.table)}>
<thead>
<tr className="border-b border-zinc-200 text-zinc-500 dark:border-zinc-800">
<th scope="col" className="py-2 pr-4 font-medium">
<tr {...stylex.props(styles.headRow)}>
<th scope="col" {...stylex.props(styles.headCell)}>
Zone
</th>
<th scope="col" className="py-2 pr-4 font-medium">
<th scope="col" {...stylex.props(styles.headCell)}>
Resolver
</th>
<th scope="col" className="py-2">
<span className="sr-only">Actions</span>
<th scope="col" {...stylex.props(styles.headCellLast)}>
<span {...stylex.props(styles.srOnly)}>Actions</span>
</th>
</tr>
</thead>
<tbody>
{zones.length === 0 && (
<tr>
<td colSpan={3} className="py-4 text-zinc-500">
<td colSpan={3} {...stylex.props(styles.emptyCell)}>
No forward zones yet.
</td>
</tr>
)}
{zones.map((zone) => (
<tr key={zone.id} className="border-b border-zinc-100 dark:border-zinc-900">
<tr key={zone.id} {...stylex.props(styles.bodyRow)}>
{form?.mode === "edit" && form.entity.id === zone.id ? (
<td colSpan={3}>
<ZoneForm
@@ -167,15 +259,19 @@ export default function ZonesTab() {
</td>
) : (
<>
<td className="py-2 pr-4 font-mono">{zone.zone}</td>
<td className="py-2 pr-4 font-mono">{zone.resolver}</td>
<td className="py-2 text-right whitespace-nowrap">
<td {...stylex.props(styles.cell, styles.mono)}>{zone.zone}</td>
<td {...stylex.props(styles.cell, styles.mono)}>{zone.resolver}</td>
<td {...stylex.props(styles.actionCell)}>
<button
type="button"
onClick={() => openForm({ mode: "edit", entity: zone })}
disabled={readOnly}
title={readOnly ? READ_ONLY_HINT : undefined}
className={`${rowButtonClass} disabled:opacity-50`}
{...stylex.props(
shared.rowButton,
styles.dimWhenDisabled,
shared.focusRing,
)}
>
Edit
</button>
@@ -184,7 +280,12 @@ export default function ZonesTab() {
onClick={() => onDelete(zone)}
disabled={remove.isPending || readOnly}
title={readOnly ? READ_ONLY_HINT : undefined}
className={`${rowButtonClass} text-red-600 disabled:opacity-50 dark:text-red-400`}
{...stylex.props(
shared.rowButton,
styles.dangerText,
styles.dimWhenDisabled,
shared.focusRing,
)}
>
Delete
</button>
@@ -196,6 +297,14 @@ export default function ZonesTab() {
</tbody>
</table>
</div>
<ConfirmDialog
isOpen={pendingDelete !== null}
title="Delete forward zone"
message={pendingDelete?.message ?? ""}
confirmLabel="Delete"
onConfirm={confirmPendingDelete}
onCancel={cancelPendingDelete}
/>
</div>
);
}