it was a permanent line under the controls saying "Saved." — which said nothing, because everything on this screen has been saved, all the time. it looked exactly like feedback, so the one moment it actually meant something was indistinguishable from every moment it did not. now it appears when a save LANDS and clears itself. top-right of the block, beside the title rather than in the flow: it is a remark about the block, not part of what you are doing, and inline it pushes everything below down a line and then pulls it back up — a layout that jumps every time somebody saves. "Not set yet" stays. that IS a state, and it is the one worth knowing; it goes when it stops being true rather than being replaced by a line that never changes. the timer is a ref and is cleared before each new one. without that, saving twice in quick succession leaves the first timeout running and it fires against the second save — the confirmation vanishing a moment after appearing, which reads as the second save failing. Block moved to its own file and each setting now renders its own. the status belongs to the setting that produced it, and lifting one block's timer into the panel would put it in a component with nothing to do with it. that is also what made the top-right slot possible without absolutely positioning anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
import type { LucideIcon } from 'lucide-react';
|
|
|
|
/**
|
|
* One titled block of Administration.
|
|
*
|
|
* Every setting gets one, so the second and third cost nothing to add — as against a single form that
|
|
* has to be broken up the first time anything joins it.
|
|
*
|
|
* ── `status` is top-right, and it is for things that come and go ──
|
|
*
|
|
* Beside the title rather than under the controls: it is a remark about the block, not part of what
|
|
* you are doing, and put in the flow it pushes everything below it down by a line and then pulls it
|
|
* back up — a layout that jumps every time somebody saves.
|
|
*
|
|
* Each block renders its OWN Block, rather than the panel wrapping them. The status belongs to the
|
|
* setting that produced it, and lifting that state up to the panel would put one block's timer in a
|
|
* component that has nothing to do with it.
|
|
*/
|
|
export const Block = ({
|
|
icon: Icon,
|
|
title,
|
|
hint,
|
|
status,
|
|
children,
|
|
}: {
|
|
icon: LucideIcon;
|
|
title: string;
|
|
hint: string;
|
|
/** Top-right. Usually ephemeral — see `useFlash`. */
|
|
status?: React.ReactNode;
|
|
children?: React.ReactNode;
|
|
}) => (
|
|
<section className="flex flex-col gap-3 rounded-lg border border-border p-4">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0 space-y-1">
|
|
<h3 className="flex items-center gap-2 text-sm font-medium">
|
|
<Icon className="size-4" />
|
|
{title}
|
|
</h3>
|
|
<p className="text-xs text-muted-foreground">{hint}</p>
|
|
</div>
|
|
{/* Reserves nothing when empty. The status is short and the title is not, so there is no reflow
|
|
to guard against — and an always-present placeholder would read as a field. */}
|
|
{status && <div className="shrink-0">{status}</div>}
|
|
</div>
|
|
{children}
|
|
</section>
|
|
);
|