Root location folds away once there is nothing left to do
Everything under it is in order most of the time, and a card that is four paragraphs of "fine" pushes the settings that are not fine off the screen. So it shuts, with a check on the right, and opens on a click. "Finished" is three bits reported upward by the parts that own them: the path is saved, the checkout is a repository, and media/ both exists and is current. None of those is knowable in the parent — each child fetches its own status — and lifting all of it up to answer one question would be a far worse trade than three booleans. It is only collapsible while settled. A block you can shut on unfinished setup is a way to hide the thing you came here to fix. And the children are NOT RENDERED while it is shut rather than hidden, because a collapsed block whose children still poll is a settled panel making requests all afternoon about something nobody is looking at. The `null` in `collapsed` is a sentinel for "nobody has decided", which is what lets a deliberate expand survive. Setting it to false while unsettled spent it on the first render — before any status had arrived — so the block could never fold itself away at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+51
-6
@@ -1,4 +1,5 @@
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* One titled block of Administration.
|
||||
@@ -15,12 +16,26 @@ import type { LucideIcon } from 'lucide-react';
|
||||
* 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.
|
||||
*
|
||||
* ── Collapsing is CONTROLLED, and that is the whole design ──
|
||||
*
|
||||
* This component does not decide when a block is finished — it cannot, because "finished" is different
|
||||
* for every setting and only the block itself knows. So the owner passes `collapsed` and gets told
|
||||
* about a click, and what remains here is the chrome: a header that is a button, and children that are
|
||||
* not rendered while it is shut.
|
||||
*
|
||||
* NOT RENDERED rather than hidden. A collapsed block's children keep polling if they are merely
|
||||
* display:none, which is how a settled panel goes on making requests all afternoon about something
|
||||
* nobody is looking at.
|
||||
*/
|
||||
export const Block = ({
|
||||
icon: Icon,
|
||||
title,
|
||||
hint,
|
||||
status,
|
||||
collapsible = false,
|
||||
collapsed = false,
|
||||
onCollapsedChange,
|
||||
children,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
@@ -28,21 +43,51 @@ export const Block = ({
|
||||
hint: string;
|
||||
/** Top-right. Usually ephemeral — see `useFlash`. */
|
||||
status?: React.ReactNode;
|
||||
/** The header becomes a button. The owner decides when this is worth offering. */
|
||||
collapsible?: boolean;
|
||||
collapsed?: boolean;
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
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">
|
||||
}) => {
|
||||
const shut = collapsible && collapsed;
|
||||
|
||||
const header = (
|
||||
<div className="flex items-start justify-between gap-3 text-left">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<h3 className="flex items-center gap-2 text-sm font-medium">
|
||||
<Icon className="size-4" />
|
||||
{title}
|
||||
{collapsible && (
|
||||
<ChevronDown
|
||||
className={`size-3.5 text-muted-foreground transition-transform ${shut ? '-rotate-90' : ''}`}
|
||||
/>
|
||||
)}
|
||||
</h3>
|
||||
{/* The hint is what the block is FOR, so it stays whether or not the block is open — a shut
|
||||
row with only a title says nothing to somebody who has not opened it before. */}
|
||||
<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>
|
||||
);
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="flex flex-col gap-3 rounded-lg border border-border p-4">
|
||||
{collapsible ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCollapsedChange?.(!collapsed)}
|
||||
aria-expanded={!shut}
|
||||
className="-m-1 cursor-pointer rounded p-1 hover:bg-muted/40"
|
||||
>
|
||||
{header}
|
||||
</button>
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
{!shut && children}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user