A modal covered the thing being edited, which for a page builder is the whole subject: you are choosing how words sit in a column, and a dialog in front of the column answers a different question. It sits under the preview now, the way the Layout section's controls already do. AND THE TEXT LANDS IN THE PAGE AS IT IS TYPED. The panel writes each change straight into the component's node in the frame, so the loop stops being "type, save, look" and becomes looking. That is the same door everything else here goes through — the frame is same origin — and it is what the modal made impossible rather than merely awkward. The node's children are replaced and the injected tools put back. Those live inside the component and belong to the admin, so an innerHTML that forgot them would take the Edit button away mid-edit. Not sanitised while typing. The server cleans on write, so what is on screen can be marginally more permissive than what is stored — and the difference shows the moment it is saved, which is the right place to find out. Cancel reloads the frame rather than restoring the markup by hand. The node has been written into on every keystroke, and putting it back correctly means remembering what was there before the first one; a reload is the same answer with nothing to get wrong. The editor is capped and scrolls, so a long piece does not push the page off the top of the panel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
58 lines
2.3 KiB
TypeScript
58 lines
2.3 KiB
TypeScript
import { Loader2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { RichText } from '../RichText';
|
|
|
|
/**
|
|
* Writing the words in a Free Text component, BELOW the page rather than over it.
|
|
*
|
|
* ── Why this stopped being a modal ──
|
|
*
|
|
* A modal covers the thing being edited. For a page builder that is the whole subject: you are choosing
|
|
* how words sit in a column, and a dialog in front of the column answers a different question. The
|
|
* Layout section already put its controls under the preview for the same reason, and this follows it.
|
|
*
|
|
* The frame keeps the height it had; the editor takes what it needs from the bottom, the same
|
|
* arrangement as everything else in these panels.
|
|
*
|
|
* ── And the text lands in the page as it is typed ──
|
|
*
|
|
* The panel writes each change straight into the component's node in the frame — see the note in
|
|
* PageBuilderPanel. So the loop is not "type, save, look" any more; it is just looking.
|
|
*/
|
|
export const FreeTextEditor = ({
|
|
html,
|
|
busy,
|
|
onChange,
|
|
onCancel,
|
|
onSave,
|
|
}: {
|
|
html: string;
|
|
busy: boolean;
|
|
onChange: (html: string) => void;
|
|
onCancel: () => void;
|
|
onSave: () => void;
|
|
}) => (
|
|
<div className="flex shrink-0 flex-col gap-2 rounded-lg border border-border p-3">
|
|
<div className="flex items-center gap-2">
|
|
<h4 className="text-xs font-medium">Text</h4>
|
|
<div className="ml-auto flex items-center gap-2">
|
|
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs" onClick={onCancel} disabled={busy}>
|
|
Cancel
|
|
</Button>
|
|
{/* Not disabled when empty, unlike a section's name: clearing a paragraph is deleting its
|
|
words, which is a legitimate thing to save. */}
|
|
<Button size="sm" className="h-7 gap-1.5 px-2 text-xs" onClick={onSave} disabled={busy}>
|
|
{busy && <Loader2 className="size-3 animate-spin" />}
|
|
Save text
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Capped, so a long piece scrolls in the editor rather than pushing the page off the top of the
|
|
panel. The page is the thing being judged and it keeps its height. */}
|
|
<div className="max-h-64 min-h-0 overflow-auto">
|
|
<RichText value={html} onChange={onChange} placeholder="Write something." />
|
|
</div>
|
|
</div>
|
|
);
|