Framer-style text-styles picker for the Text settings section. A chip in the row opens a FloatingPanel picker; per-row Edit / "New Style" hand off to a second FloatingPanel editor. Both panels are lazy-loaded heavy chunks following the […
Framer-style text-styles picker for the Text settings section. A chip in
the row opens a FloatingPanel picker; per-row Edit / "New Style" hand off to
a second FloatingPanel editor. Both panels are lazy-loaded heavy chunks
following the editor-popover-pattern trigger ↔
panel split — no modal dialogs, no window.prompt / window.alert.
| File | Role |
|---|---|
| TypographyPresetInput.tsx | Registered custom input ("TypographyPresetInput"). Owns data flow: reads presets from ROOT.props.theme.typography, computes selectedName from the node's className, and supplies create / update / delete / apply handlers. |
| TextStylePicker.tsx | Chip trigger + state coordinator. Stage union (closed / picker / editor) decides which lazy panel mounts. |
| TextStylePickerPanel.tsx | Lazy chunk. FloatingPanel with search input, scrollable list (per-row Edit pencil revealed on hover), and "New Style" footer. |
| TextStyleEditorPanel.tsx | Lazy chunk. FloatingPanel with name (inline error), font family + color (atom-driven dialogs), size / weight / line-height / letter-spacing / transform / decoration (ToolbarDropdown), text-align (ToolbarSegmentedControl), live preview, Save / Cancel + Delete (edit mode). |
Closed ──chip click──▶ picker open
picker ──row click──▶ applyPreset → closed
picker ──row Edit ──▶ editor (mode: "edit", originalName, draft)
picker ──New Style──▶ editor (mode: "new", draft = node DOM capture)
editor ──Save ──▶ onCreate / onUpdate → closed
editor ──Cancel ──▶ closed
editor ──Delete ──▶ onDelete → closed (edit mode only)
The picker and editor are mutually exclusive — opening one closes the other.
Presets live at ROOT.props.theme.typography as an array. Apply writes
ph-{slug(name)} to the current node's className; the SDK's CSS generator
emits a class with that slug at SSR time.
type TypographyPreset = {
name: string; // also the CSS class slug source
fontFamily: string; // family token used by extractGoogleFontsUrl
fontSize: string; // CSS length
fontWeight: string;
lineHeight: string;
letterSpacing?: string;
textTransform?: string;
// Extended fields — optional; emitted only when set on the preset
color?: string; // any CSS color (rgb/oklch/hex)
textDecoration?: string; // none | underline | line-through | overline
textAlign?: string; // left | center | right | justify
};
Generated by generateTypographyCSSClasses and generateTypographyCSSVariables:
.ph-{slug} {
font-family: var(--{slug}-font-family, …);
font-size: var(--{slug}-font-size, …);
font-weight: var(--{slug}-font-weight, …);
line-height: var(--{slug}-line-height, …);
letter-spacing: var(--{slug}-letter-spacing, …);
text-transform: var(--{slug}-text-transform, …);
/* color / text-decoration / text-align — only emitted when set */
}
Optional fields are omitted entirely from the rule when unset, so presets that predate the extended fields render identically.
The existing toolbar input primitives (FontFamilyAltInput, ColorInput,
MultiToggleInput, tailwind-radio rows) write className via
changeProp + useNode. The text-style editor needs to write preset
definitions (theme.typography[i].*) instead. Rather than wrap or fork
those primitives, the editor:
TextStyleDraft via useState. Nothing touches the editor
theme until the user clicks Save.FontFamilyDialogAtom, ColorPickerAtom)
for font and color — those are already host-agnostic and only take a
changed callback.ToolbarDropdown for selects and ToolbarSegmentedControl for
alignment. Both are pure data-in / value-out — no node binding.The host (TypographyPresetInput) is the only file that touches CraftJS:
buildNewDraft() → readTypographyFromDom(nodeId) (computed-style capture)
buildEditDraft(p) → presetToDraft(rawPreset)
onCreate(draft) → setProp(ROOT, writeTheme(...)) + apply class to node
onUpdate(name, draft) → setProp(ROOT, writeTheme(...)) + swap class on rename
onDelete(name) → setProp(ROOT, writeTheme(...)) + strip class from node
Name validation is inline under the input — never a popup.
The Save button is disabled while nameError is non-null.
FloatingPanel with zIndex={1100}, persistSize={false}.scrollable={false} and manages its own internal flex layout
(search header / scrollable list / footer). The editor uses
scrollable + bodyClassName so the form scrolls cleanly inside AutoHideScrollbar.position: fixed outside FloatingPanel.window.prompt / window.alert / window.confirm anywhere in the flow.Chip (mode="popover"), ToolbarDropdown,
ToolbarSegmentedControl, atom-driven font / color dialogs).popoverModeRegistry because
it owns its own chip; it just borrows the lazy-load pattern).