milestone 23 s2: react aria primitives and their cluster
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user