Files
pastilhasandClaude Opus 5 cc283ee7b6 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>
2026-08-29 11:52:54 +01:00

94 lines
3.5 KiB
TypeScript

import type { LucideIcon } from 'lucide-react';
import { ChevronDown } 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.
*
* ── 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;
title: string;
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;
}) => {
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>
);
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>
);
};