"Saved" is a moment, top-right, and gone in five seconds
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>
This commit is contained in:
+33
-10
@@ -1,14 +1,24 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Check, Loader2, Pencil } from 'lucide-react';
|
||||
import { Check, FolderRoot, Loader2, Pencil } from 'lucide-react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { readableError } from './errors';
|
||||
import { DirectoryPicker } from './DirectoryPicker';
|
||||
import { Block } from './Block';
|
||||
import { useFlash } from './useFlash';
|
||||
|
||||
/**
|
||||
* Where the fofos site is on this machine.
|
||||
*
|
||||
* ── "Saved" is a moment, not a state ──
|
||||
*
|
||||
* It appears top-right when a save actually lands and is gone five seconds later. As a permanent line
|
||||
* it said nothing — everything on this screen has been saved, all the time — while looking exactly
|
||||
* like feedback, so the one moment it meant something was indistinguishable from the rest.
|
||||
*
|
||||
* "Not set yet" is the opposite and stays: that IS a state, and it is the one worth knowing.
|
||||
*
|
||||
* ── The default is a suggestion until it is saved ──
|
||||
*
|
||||
* An unset root reports `<owner home>/fofos` and says so. It is not written to the database by being
|
||||
@@ -48,6 +58,7 @@ export const RootLocationBlock = () => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [forbidden, setForbidden] = useState(false);
|
||||
const [flashing, flash] = useFlash();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
@@ -76,6 +87,7 @@ export const RootLocationBlock = () => {
|
||||
setValue(result.rootPath);
|
||||
setEditing(false);
|
||||
setPicking(false);
|
||||
flash();
|
||||
} catch (failure) {
|
||||
setError(readableError(failure));
|
||||
} finally {
|
||||
@@ -83,12 +95,18 @@ export const RootLocationBlock = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const shell = (children: React.ReactNode, status?: React.ReactNode) => (
|
||||
<Block icon={FolderRoot} title="Root location" hint="Where the fofos site is on this machine." status={status}>
|
||||
{children}
|
||||
</Block>
|
||||
);
|
||||
|
||||
if (forbidden) {
|
||||
return <p className="text-xs text-muted-foreground">Only the owner can change where the site lives.</p>;
|
||||
return shell(<p className="text-xs text-muted-foreground">Only the owner can change where the site lives.</p>);
|
||||
}
|
||||
|
||||
if (!saved) {
|
||||
return <p className="text-xs text-muted-foreground">Loading…</p>;
|
||||
return shell(<p className="text-xs text-muted-foreground">Loading…</p>);
|
||||
}
|
||||
|
||||
const dirty = value.trim() !== saved.rootPath;
|
||||
@@ -102,7 +120,7 @@ export const RootLocationBlock = () => {
|
||||
setError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
return shell(
|
||||
<div className="flex flex-col gap-3">
|
||||
{editing ? (
|
||||
<Input
|
||||
@@ -134,11 +152,10 @@ export const RootLocationBlock = () => {
|
||||
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
|
||||
{/* Says which of the two states this is in. "Not set" reading identically to "set to exactly the
|
||||
default" is how somebody comes to believe they configured something they did not. */}
|
||||
<p className="text-xs text-muted-foreground/80">
|
||||
{saved.isSet ? 'Saved.' : 'Not set yet — this is the default until you save it.'}
|
||||
</p>
|
||||
{/* Only the state worth knowing. Once it is set, this line has nothing to say and goes. */}
|
||||
{!saved.isSet && (
|
||||
<p className="text-xs text-muted-foreground/80">Not set yet — this is the default until you save it.</p>
|
||||
)}
|
||||
|
||||
{/* The block's bottom row. Everything you can DO is here, on its own line, so the path above it
|
||||
gets the full width. */}
|
||||
@@ -163,6 +180,12 @@ export const RootLocationBlock = () => {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
flashing ? (
|
||||
<span className="flex items-center gap-1 text-xs text-success">
|
||||
<Check className="size-3.5" />
|
||||
Saved
|
||||
</span>
|
||||
) : null,
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user