the handles arrive under the cursor

Edit and Delete sat permanently at the top-right of whatever they belonged to, on top of the text — and
on a paragraph that fills its column there is nowhere for them to go that is not on top of something.

So they are hidden until the component is hovered, which is when they are wanted and the only time they
are. The outline goes the same way: an editor should show the PAGE, with the handles arriving under the
cursor. One that outlines everything permanently is showing you the editor.

The tools also moved out of the component's corner into the gap above it, so even while visible they are
beside the content rather than over its first line.

The section's Delete gets the same treatment; its NAME stays, because that is what tells you which band
you are looking at, and it already sits above the section rather than on it. A destructive verb does not
need to be on screen at all times.

Done with one injected stylesheet rather than listeners on every node — `:hover` is the one thing inline
styles cannot express, and it is the only reason this file now has a <style> in it. `:focus-within` and
`:focus` keep both reachable by keyboard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 05:51:15 +01:00
co-authored by Claude Opus 5
parent 8ab0a4a516
commit 43a447801d
+42 -5
View File
@@ -94,6 +94,38 @@ export function paintCanvas(document: Document, canvas: Canvas): () => void {
/** Everything to undo, in the order it was done. */ /** Everything to undo, in the order it was done. */
const undo: (() => void)[] = []; const undo: (() => void)[] = [];
/*
A stylesheet, for the one thing inline styles cannot express: `:hover`.
The component tools sat permanently at the top-right of whatever they belonged to, on top of the
text. On a paragraph that fills its column there is nowhere for them to go that is not on top of
something — so they are hidden until the component is hovered, which is when they are wanted and
the only time they are.
The outline goes the same way. An editor should show the page, with the handles arriving under the
cursor; an editor that outlines everything all the time is showing you the editor.
*/
const sheet = document.createElement('style');
sheet.setAttribute(MARK, 'stylesheet');
sheet.textContent = `
[data-fofos-component] { outline: 1px dotted transparent; transition: outline-color .12s; }
[data-fofos-component]:hover { outline-color: color-mix(in srgb, ${ACCENT} 55%, transparent); }
[${MARK}="component-tools"] { opacity: 0; transition: opacity .12s; }
[data-fofos-component]:hover > [${MARK}="component-tools"] { opacity: 1; }
[${MARK}="component-tools"]:focus-within { opacity: 1; }
/*
The section's Delete, same treatment. Its NAME stays visible — that is what tells you which band
you are looking at, and it sits above the section rather than on it — but a destructive verb does
not need to be on screen at all times.
*/
[${MARK}="section-delete"] { opacity: 0; transition: opacity .12s; }
[data-section-id]:hover [${MARK}="section-delete"] { opacity: 1; }
[${MARK}="section-delete"]:focus { opacity: 1; }
`;
document.head.append(sheet);
undo.push(() => sheet.remove());
/* /*
The site's empty-page placeholder, hidden while the builder draws its own controls in that space. The site's empty-page placeholder, hidden while the builder draws its own controls in that space.
@@ -166,6 +198,7 @@ export function paintCanvas(document: Document, canvas: Canvas): () => void {
style(shape, ['opacity:0.7']); style(shape, ['opacity:0.7']);
const remove = document.createElement('button'); const remove = document.createElement('button');
remove.setAttribute(MARK, 'section-delete');
remove.type = 'button'; remove.type = 'button';
remove.textContent = 'Delete'; remove.textContent = 'Delete';
style(remove, [ style(remove, [
@@ -217,27 +250,31 @@ export function paintCanvas(document: Document, canvas: Canvas): () => void {
if (!node) continue; if (!node) continue;
const wasPosition = node.style.position; const wasPosition = node.style.position;
const wasOutline = node.style.outline;
node.style.position = 'relative'; node.style.position = 'relative';
node.style.outline = `1px dotted color-mix(in srgb, ${ACCENT} 60%, transparent)`; // The outline and the tools' visibility are the stylesheet's, so they can depend on :hover.
node.setAttribute('data-fofos-component', '');
undo.push(() => { undo.push(() => {
node.style.position = wasPosition; node.style.position = wasPosition;
node.style.outline = wasOutline; node.removeAttribute('data-fofos-component');
}); });
const tools = document.createElement('div'); const tools = document.createElement('div');
tools.setAttribute(MARK, 'component-tools'); tools.setAttribute(MARK, 'component-tools');
style(tools, [ style(tools, [
'position:absolute', 'position:absolute',
'top:0', // Above its own top-right corner rather than inside it, so even while visible it sits in the
// gap between components instead of over the first line of this one.
'top:-1.25rem',
'right:0', 'right:0',
'display:flex', 'display:flex',
'gap:0.5rem', 'gap:0.5rem',
'padding:0.125rem 0.25rem', 'padding:0 0.25rem',
'font-size:0.625rem', 'font-size:0.625rem',
'font-family:ui-sans-serif, system-ui, sans-serif', 'font-family:ui-sans-serif, system-ui, sans-serif',
`color:${ACCENT}`, `color:${ACCENT}`,
'background:var(--site-bg, #fff)', 'background:var(--site-bg, #fff)',
// Above the page, below the section bar, and never a target when invisible.
'z-index:5',
]); ]);
for (const [label, run] of [ for (const [label, run] of [