Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
629270d
docs: add approved inline text editing design spec
DavidBabinec Jun 10, 2026
f975e9a
feat(module-engine): add inlineTextEdit contract, declare on text/but…
DavidBabinec Jun 10, 2026
a0a7f24
feat(editor-store): add inline text edit session slice with coalesced…
DavidBabinec Jun 10, 2026
6bfcbf6
feat(editor-store): force-close inline edit sessions on deletion and …
DavidBabinec Jun 11, 2026
20edb1d
feat(canvas): add inline-edit typography mirroring helpers
DavidBabinec Jun 11, 2026
6bad4b7
feat(canvas): add InlineTextEditOverlay, mounted per breakpoint frame
DavidBabinec Jun 11, 2026
ee59e9f
feat(canvas): wire double-click inline editing and hide doubled text
DavidBabinec Jun 11, 2026
41b2575
docs: document canvas inline text editing (parent-doc overlay)
DavidBabinec Jun 11, 2026
a617084
docs: fix remaining stale slice counts in editor.md (11 -> 12)
DavidBabinec Jun 11, 2026
83bf384
docs: fix stale slice count in adminUi header comment (11 -> 12)
DavidBabinec Jun 11, 2026
7447335
Merge origin/main into feat/inline-text-editing
DavidBabinec Jun 11, 2026
6cfeb1a
fix(canvas): render canvas + inline-edit field in the site's fonts, n…
DavidBabinec Jun 11, 2026
61b3457
fix(canvas): stop inline-edit field scroll-shifting multi-line text
DavidBabinec Jun 11, 2026
b7b265a
fix(canvas): remove inline-edit open flash by positioning in a layout…
DavidBabinec Jun 11, 2026
a23853d
feat(canvas): rewrite inline text editing to edit the real element in…
DavidBabinec Jun 11, 2026
84b59ea
fix(canvas): stop React clobbering the inline-edit caret every keystroke
DavidBabinec Jun 11, 2026
4fe6f20
fix(canvas): let the spacebar type a space during inline editing
DavidBabinec Jun 11, 2026
3486ff0
fix(canvas): finalize inline editing — keyboard-forward guard, select…
DavidBabinec Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(canvas): add inline-edit typography mirroring helpers
scaleCssLength + mirrorInlineEditTypography — computed styles read through
iframe.contentWindow, px lengths scaled by the iframe zoom factor so the
parent-doc field matches the text it floats over at every zoom level.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
  • Loading branch information
DavidBabinec and claude committed Jun 11, 2026
commit 20edb1d4693f979fcba7a92201f72be4856e4a2e
27 changes: 27 additions & 0 deletions src/__tests__/canvas/canvasOverlayGeometry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* canvasOverlayGeometry — pure-function tests for the inline-edit typography
* scaling. (measureCanvasElementRect is exercised indirectly by the existing
* overlay integration paths; the scaler is the new, directly-testable logic.)
*/
import { describe, it, expect } from 'bun:test'
import { scaleCssLength } from '@site/canvas/canvasOverlayGeometry'

describe('scaleCssLength', () => {
it('scales px lengths by the iframe scale factor', () => {
expect(scaleCssLength('32px', 0.5)).toBe('16px')
})

it('passes keywords through untouched', () => {
expect(scaleCssLength('normal', 0.5)).toBe('normal')
})

it('handles fractional and negative px values', () => {
expect(scaleCssLength('1.5px', 2)).toBe('3px')
expect(scaleCssLength('-0.5px', 2)).toBe('-1px')
})

it('leaves non-px values untouched (unitless line-height, em)', () => {
expect(scaleCssLength('1.4', 0.5)).toBe('1.4')
expect(scaleCssLength('0.02em', 0.5)).toBe('0.02em')
})
})
54 changes: 54 additions & 0 deletions src/admin/pages/site/canvas/canvasOverlayGeometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,57 @@ export function measureCanvasElementRect(
height: editorDocRect.height,
}
}

/** Typography lengths that must shrink/grow with the canvas zoom. */
const SCALED_TYPOGRAPHY_PROPS = ['font-size', 'line-height', 'letter-spacing'] as const

/** Typography that transfers verbatim from the measured element. */
const COPIED_TYPOGRAPHY_PROPS = [
'font-family',
'font-weight',
'font-style',
'color',
'text-align',
'text-transform',
] as const

/**
* Scale a computed CSS px length by the iframe zoom factor. Keywords
* (`normal`), unitless values, and non-px units pass through untouched —
* getComputedStyle resolves lengths to px in browsers, so anything else is
* already zoom-independent for our purposes.
*/
export function scaleCssLength(value: string, scale: number): string {
const match = /^(-?\d*\.?\d+)px$/.exec(value.trim())
if (!match) return value
return `${Number.parseFloat(match[1]) * scale}px`
}

/**
* Mirror the edited element's live typography onto the parent-document
* inline-edit field, so the floating <textarea>/<input> reads as the text
* it replaces. Reads through `iframe.contentWindow.getComputedStyle` (the
* frames are same-origin `srcdoc` iframes) and scales px lengths by the
* iframe zoom factor — the field lives in UNSCALED parent coordinates.
*
* NOTE: this is why the hide-doubled-text rule uses
* `-webkit-text-fill-color: transparent` and not `color: transparent` —
* the computed `color` read here must stay the authored color.
*/
export function mirrorInlineEditTypography(
field: HTMLElement,
target: HTMLElement,
iframe: HTMLIFrameElement,
): void {
const view = iframe.contentWindow
if (!view) return
const computed = view.getComputedStyle(target)
const iframeScale =
iframe.offsetWidth > 0 ? iframe.getBoundingClientRect().width / iframe.offsetWidth : 1
for (const prop of SCALED_TYPOGRAPHY_PROPS) {
field.style.setProperty(prop, scaleCssLength(computed.getPropertyValue(prop), iframeScale))
}
for (const prop of COPIED_TYPOGRAPHY_PROPS) {
field.style.setProperty(prop, computed.getPropertyValue(prop))
}
}