From 33eb7845e4a02650551204ed2f3743b1228cea05 Mon Sep 17 00:00:00 2001 From: JoeJoeflyn Date: Mon, 27 Jul 2026 00:55:03 +0700 Subject: [PATCH] fix: unlock font-size field for global editing without selection The font-size input, increment, and decrement buttons were disabled unless a text node was selected, blocking the global workflow: typing a value with nothing selected should apply to every text box on the page, and clearing it should reset all boxes to auto. What changed: - Switch the disabled guard from !selectedNode to !hasNodes so the controls stay enabled whenever the page has text boxes. - Route commits through applyStyleToSelected when a node is selected and applyStyleToAll otherwise, so the existing per-node behavior is preserved. - Buffer the input in local state (globalFontSizeInput) while in global scope, otherwise the controlled value resets to '' on each keystroke because currentFontSize stays undefined with no selection. User-visible behavior differences: - With no text node selected, the font-size field is now editable (previously greyed out). Typing a number applies it to every text box on the page; clearing the field resets all boxes to auto. - Selecting a single text box still applies changes only to that node, unchanged. How I verified: - bun run format and bun run test:ui (158/158 passed, including new tests covering the global behavior). - Manually confirmed against a running app: with no text node selected, typing 24 sets fontSize=24 on all 8 text boxes; clearing the field reverts all to auto (null) in the backend scene. Closes #649. This patch was made with AI assistance (Devin/Claude). The code was reviewed and tested before submission. --- ui/components/panels/RenderControlsPanel.tsx | 73 ++++++++++++++++--- .../components/RenderControlsPanel.test.tsx | 55 ++++++++++++++ 2 files changed, 116 insertions(+), 12 deletions(-) diff --git a/ui/components/panels/RenderControlsPanel.tsx b/ui/components/panels/RenderControlsPanel.tsx index 56a3759e9..7b665e21c 100644 --- a/ui/components/panels/RenderControlsPanel.tsx +++ b/ui/components/panels/RenderControlsPanel.tsx @@ -148,6 +148,14 @@ export function RenderControlsPanel() { const sectionRef = useRef(null) const [sectionWidth, setSectionWidth] = useState(0) + // Buffers font-size input in global scope (no selection) so multi-digit + // values can be typed before committing — the controlled `value` would + // otherwise reset to '' on each keystroke because `currentFontSize` stays + // undefined when nothing is selected. + const [globalFontSizeInput, setGlobalFontSizeInput] = useState('') + useEffect(() => { + setGlobalFontSizeInput('') + }, [selectedNode?.id, page?.id]) useEffect(() => { if (!sectionRef.current) return @@ -565,10 +573,20 @@ export function RenderControlsPanel() { variant='ghost' size='icon-sm' className='size-6 shrink-0 rounded-r-none border-r' - disabled={!selectedNode} + disabled={!hasNodes} onClick={() => { - const next = Math.max(6, Math.round((currentFontSize ?? 16) - 1)) - applyStyleToSelected({ fontSize: next }) + const base = selectedNode + ? (currentFontSize ?? 16) + : globalFontSizeInput !== '' + ? Number.parseInt(globalFontSizeInput, 10) + : 16 + const next = Math.max(6, Math.round((Number.isFinite(base) ? base : 16) - 1)) + if (selectedNode) { + applyStyleToSelected({ fontSize: next }) + } else { + setGlobalFontSizeInput(String(next)) + applyStyleToAll({ fontSize: next }) + } }} > @@ -581,16 +599,37 @@ export function RenderControlsPanel() { inputMode='numeric' className='h-6 min-w-0 flex-1 [appearance:textfield] rounded-none border-0 px-0.5 text-center text-xs shadow-none focus-visible:ring-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none' data-testid='render-font-size' - disabled={!selectedNode} - value={currentFontSize !== undefined ? Math.round(currentFontSize) : ''} + disabled={!hasNodes} + value={ + selectedNode + ? currentFontSize !== undefined + ? Math.round(currentFontSize) + : '' + : globalFontSizeInput + } placeholder='auto' onChange={(event) => { - if (event.target.value === '') { - applyStyleToSelected({ fontSize: null }) + const raw = event.target.value + if (selectedNode) { + if (raw === '') { + applyStyleToSelected({ fontSize: null }) + return + } + const parsed = Number.parseInt(raw, 10) + if (!Number.isFinite(parsed) || parsed < 1) return + applyStyleToSelected({ fontSize: Math.min(300, parsed) }) + return + } + // Global scope: buffer locally so multi-digit values survive + // re-renders, then commit to every text box. + setGlobalFontSizeInput(raw) + if (raw === '') { + applyStyleToAll({ fontSize: null }) + return } - const parsed = Number.parseInt(event.target.value, 10) + const parsed = Number.parseInt(raw, 10) if (!Number.isFinite(parsed) || parsed < 1) return - applyStyleToSelected({ fontSize: Math.min(300, parsed) }) + applyStyleToAll({ fontSize: Math.min(300, parsed) }) }} />