Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
- Authored Content: DesignDocument가 표현하고 ReactDesignRenderer가 실제 browser DOM으로 투영하는 사용자 디자인 내용. live DOM은 derived runtime output이지 document state가 아니다.
- Editor Runtime State: selection, camera, active tool/mode, focus, hover, DOM registration/measurement, overlay, guide, marquee, pointer draft와 live preview처럼 편집 세션 동안만 유효한 상태. DesignDocument, authored history payload, document sync에 들어가지 않는다.
- ReactDesignRenderer: DesignDocument node를 intrinsic element 또는 registered component/widget definition으로 해석해 direct React DOM을 만들고 stable node identity를 DomProjection에 등록하는 renderer. document mutation과 editing policy는 소유하지 않는다.
- ReactDesignEditorRenderer: ReactDesignEditorRuntime의 document, editor, registry, projection을 ReactDesignRenderer에 연결하고 immutable DesignDocument/EditorEngine snapshot identity를 mounted subtree layout commit에서 acknowledge하는 runtime-owned renderer. 이 신호는 DOM mutation과 ref commit 완료를 뜻하며 paint, font, image, async layout settle을 뜻하지 않는다.
- ReactDesign Editor External Change Host: EditorEngineDocumentHost의 preview/mutation gate에 정확히 하나인 canonical ReactDesignEditorRenderer의 mounted render lease와 snapshot commit acknowledgement를 결합하는 browser coordination Module. ready external change는 현재 document/editor state가 React DOM에 commit된 뒤에만 실행되고, renderer 중복 mount가 감지된 runtime은 projection ownership을 증명할 수 없어 남은 lifetime 동안 fail-closed하며, 다음 retry 알림은 generation과 snapshot identity를 다시 확인한다.
- ReactDesign Text Selection: raw DOM Range 대신 stable design node id, UTF-16 anchor/focus offset과 direction을 저장하고 ownership/focus generation이 유지될 때만 현재 text control에 복원하는 ephemeral browser selection Module. 같은 node의 text patch에 대한 offset rebase는 하지 않고 현재 value 길이에 clamp한다.
- DomProjection: stable design node id와 HTMLElement 사이의 ephemeral runtime 관계, measurement, coordinate projection, observation, hit target을 소유하는 Module. DOM element와 측정값은 DesignDocument에 저장하지 않는다.
- EditorEngine: Canvas Foundation/Affordance grammar를 DesignDocument read/command Interface와 DomProjection에 적용해 Figma식 selection, transform, text/layout, keyboard effect를 계획하는 Module. committed edit는 하나의 atomic document command로만 만든다.
- React Component Definition: stable definition id에 React renderer, JSON prop validation/default, creation/edit capability, optional inspector contribution을 결합하는 registered definition. DesignDocument는 definition function 대신 id와 JSON data만 참조한다.
Expand Down
10 changes: 5 additions & 5 deletions docs/adr/0006-canonical-design-document-causal-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,16 @@ validation and widen the public mutation surface.

- Canvas now has the headless seam needed to test delayed stable-id editing
without exposing the internal JSONDocument or adding a runtime dependency on
private labs. A FigJam browser test remains follow-up evidence.
private labs. ADR 0007 adds separate browser evidence for React commit,
composition blur, DOM remount, and stable text selection restoration.
- Structural and positional delayed edits need granular DesignDocument command
patches instead of a root replacement.
- A concrete remote publication discards pre-remote local undo and redo. A
future selective-undo design must rebase owned inverses explicitly before it
can preserve that history safely.
- DOM caret handoff needs a separate selection adapter plus a render-settle
signal; it must not be inferred from the authored graph.
- FigJam composition/blur behavior and a real ReactDesignRenderer commit must
be covered before claiming IME-safe browser coordination.
- DOM caret handoff now has a text-control selection adapter and explicit React
commit acknowledgement. Contenteditable affinity and native cross-browser IME
evidence remain follow-up work.
- Large documents still pay whole-snapshot clone/freeze, validation, and index
rebuild costs. Incremental validation and indexing remain separate
performance work.
Expand Down
115 changes: 115 additions & 0 deletions docs/adr/0007-dom-input-render-coordination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# ADR 0007: DOM Input and Render Coordination

## Status

Provisional — accepted for browser dogfood, not a native IME or convergence claim.

## Context

ADR 0005 makes `DesignDocument` the authored source of truth and direct React
DOM its derived browser surface. ADR 0006 prevents a ready external document
change from running while `EditorEngine` preview owns uncommitted input. The
headless causal tracer intentionally retries in a microtask and does not prove
that React has committed either the local or external snapshot.

That gap matters for browser input. A canonical document publication can finish
synchronously while React is still rendering the previous snapshot. A raw DOM
selection can also point at an element that React is about to replace. Using
`DomProjection.revision()`, `EditorEngineSnapshot.revision`, a timer, animation
frame, or `ResizeObserver` as a render acknowledgement would mix unrelated
invalidation with actual React commit.

FigJam additionally committed `blur` during an active composition. That could
end the preview and unmount its textarea before the final composition input was
observed.

## Decision

1. `ReactDesignEditorRenderer` is the runtime-owned canonical render surface.
It keeps the existing `ReactDesignRenderer` interface intact and
acknowledges the immutable `DesignDocument.snapshot` and
`EditorEngineSnapshot` identities from a parent layout effect after the
renderer subtree DOM mutation and callback refs have committed. The editor
snapshot identity invalidates old evidence; its revision number is not
itself render evidence. The acknowledgement is a mounted render lease and
is revoked by layout-effect cleanup.
2. `getReactDesignEditorExternalChangeHost(runtime)` exposes the browser-aware
external change host without changing the existing `ReactDesignEditorRuntime`
shape. It delegates publication ownership and the headless preview/mutation
gate to `EditorEngineDocumentHost`, and additionally returns `host_not_ready`
until the one canonical renderer lease acknowledges the current document
and editor snapshot identities. A runtime has exactly one canonical render
surface because its `DomProjection` registration is singular/latest-wins;
a runtime that observes overlapping renderer mounts remains fail closed for
the rest of its lifetime. Runtimes backed by a document without patch
coordination remain usable when this additive capability is not requested.
3. A ready notification is delivered in a generation-checked microtask after
the explicit layout-commit acknowledgement. The microtask is delivery
scheduling, not the evidence of render completion. If the document snapshot
changes while one listener applies an external change, remaining listeners
do not run for the obsolete snapshot.
4. The required ordering is:

```text
input preview owns browser edits
-> local canonical commit
-> local React DOM commit acknowledgement
-> ready external change apply
-> external React DOM commit acknowledgement
-> selection correction
```

5. `ReactDesignTextSelection` stores text-control selection as stable design
node id, UTF-16 anchor/focus offsets, and direction. It does not
retain a DOM `Range` or element reference in the bookmark. Restore resolves
the current element and succeeds only for the active ownership generation,
unchanged focus generation, connected element, and current logical node.
A body-focus gap is accepted only when the captured control was disconnected
and the resolver now returns a different connected replacement. Focusing is
revalidated before the range is written.
6. A React remount may focus the replacement control before its ref callback is
visible to the resolver. That focus transition is reconciled only when the
previously focused element becomes disconnected, no later focus event has
intervened, and the resolver identifies the new target as the same active
logical input.
7. FigJam composition blur is deferred in explicit `composing` and `settling`
phases. `compositionend` starts a generation-checked 30 ms settling window;
a later final input updates the existing `EditorEnginePreviewSession` first
and restarts that window. New composition, preview-session replacement, or
unmount invalidates the timer. This is a conservative browser event window,
not render acknowledgement.
8. The browser causal tracer lives under `e2e/fixtures`. It may compose the
SHA-pinned private causal inbox, but production `src/canvas/**` keeps no
runtime dependency on unpublished json-document labs.

## Boundaries

- A React layout commit acknowledgement does not mean browser paint, font load,
image decode, network completion, or asynchronous layout has settled.
- `DesignDocument` remains independent from DOM, React, focus, composition, and
selection.
- `DomProjection` remains measurement and node-to-element runtime state. Its
revision and subscription channel are not reused for render acknowledgement.
- The external change host does not choose CRDT, OT, transport, retry backoff,
acknowledgement, persistence, or conflict policy.
- Text selection bookmarks currently cover native text controls. They preserve
and clamp UTF-16 offsets across remount; they do not rebase offsets through a
same-node text patch. A contenteditable adapter or patch-aware correction
layer must define its own position mapping and boundary affinity semantics.
- Synthetic Chromium composition evidence verifies browser event-handler
ordering, not real Korean/Japanese IME behavior across operating systems or
Safari. Native IME verification remains required before an IME-safe claim.

## Consequences

- Figma and FigJam canonical routes use the runtime-owned renderer, so future
external-change adapters can consume one render-aware host instead of wiring
document, engine, projection, and React timing independently.
- A delayed change cannot run merely because a preview ended; the locally
committed or reverted DOM must first be observed in a React commit.
- Selection correction cannot steal focus after the user moves to another
control, deliberately blurs a still-connected editor, or a newer edit
generation takes ownership.
- The headless causal tracer remains useful for deterministic document
coordination, while the Chromium tracer provides separate evidence for DOM
focus, remount, and selection behavior.
39 changes: 39 additions & 0 deletions e2e/figjam-react-cutover.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,45 @@ test('previews, cancels, commits, and restores direct text edits', async ({
await expect(sticky).toContainText('Ship the canonical DOM board')
})

test('keeps a blurred composition preview until its final input commits', async ({
page,
}) => {
await page.goto('/figjam')

const app = figJamApp(page)
const sticky = designNode(page, 'figjam-sticky')

await sticky.dblclick({ force: true })
const editor = page.getByRole('textbox', {
name: 'Edit sticky note text',
})

await editor.focus()
await editor.dispatchEvent('compositionstart', { data: '한' })
await editor.evaluate((element) => element.blur())

await expect(editor).toBeVisible()
await expect(app).toHaveAttribute('data-preview-node-id', 'figjam-sticky')

await editor.evaluate((element) => {
element.dispatchEvent(new CompositionEvent('compositionend', {
bubbles: true,
data: '한',
}))
const setter = Object.getOwnPropertyDescriptor(
HTMLTextAreaElement.prototype,
'value',
)?.set

setter?.call(element, '한글 입력')
element.dispatchEvent(new Event('input', { bubbles: true }))
})

await expect(editor).toHaveCount(0)
await expect(sticky).toContainText('한글 입력')
await expect.poll(() => app.getAttribute('data-preview-node-id')).toBeNull()
})

test('honors widget move and resize capabilities in DOM controls', async ({
page,
}) => {
Expand Down
12 changes: 12 additions & 0 deletions e2e/fixtures/react-design-dom-settle.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React design DOM settle tracer</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/e2e/fixtures/react-design-dom-settle.tsx"></script>
</body>
</html>
Loading
Loading