Skip to content

Add offscreen block skeletons - #37

Merged
srubin merged 16 commits into
main-pojofrom
codex/script-block-skeleton
Aug 28, 2026
Merged

Add offscreen block skeletons#37
srubin merged 16 commits into
main-pojofrom
codex/script-block-skeleton

Conversation

@srubin

@srubin srubin commented Jul 25, 2026

Copy link
Copy Markdown

Summary

This adds an Editor-owned skeleton mode for large documents. Offscreen blocks keep a lightweight, editable text subtree while skipping block renderers, decorator components, and leaf components; selected and near-viewport blocks use the normal renderer.

Visibility uses one IntersectionObserver with 500 px overscan, while structural edits are registered without a render-all measurement frame. Native scroll anchoring preserves position during promotion. Decorators can expose stable DOM IDs for addressable cards and markers, and skeleton text remains content-editable so native selection and editing continue to work without pinned blocks or keyboard special cases.

Draft now reports skeleton-state DOM commits through onBlockSkeletonsRendered. Consumers can refresh application-owned geometry after promotion without maintaining a parallel visibility signal. Draft does not inspect or rewrite application content-visibility styles, keeping those optimizations independent.

  • I used AI

Decisions

  • The callback fires from a layout effect after the promoted DOM commits, so geometry consumers observe the current tree.
  • Unsupported IntersectionObserver or native scroll anchoring falls back to full rendering.

Test Plan

  • Full suite: 44 suites passed; 402 tests passed, 3 skipped; 500 snapshots passed.
  • Latest callback change: lint passed; the 13 focused editor tests passed; the library build exited successfully.
  • Downstream production-mode coverage verifies select-all, distant caret promotion, autoscroll, typing, marker navigation, and timeline seek behavior. The latest downstream integration passed formatting, lint, typecheck, and 3 focused unit tests; its focused UI files could not be rerun because no UI-test server was detectable.

Published as @descript/draft-js@0.11.6-descript.38 for descriptinc/descript#38103.

@srubin srubin self-assigned this Jul 25, 2026
Native browser selection stops at contentEditable boundaries, so skeletonized editors could select only the current rendered block. Build the full-editor selection in Draft while leaving custom key bindings in control.
Comment thread src/component/handlers/edit/editOnKeyDown.ts Outdated
Comment thread src/component/hooks/useDraftEditorBlockSkeleton.ts Outdated
@srubin srubin changed the title feat(editor): add offscreen block skeletons feat: add offscreen block skeletons Aug 3, 2026
@srubin
srubin requested a review from scottcheng August 3, 2026 23:25
@srubin
srubin marked this pull request as ready for review August 3, 2026 23:25
@scottcheng

Copy link
Copy Markdown

design

looking at this PR and https://github.com/descriptinc/descript/pull/38103, my understanding is that this approach exposes getSkeletonAttributesForRange to customize the skeleton styling, in order to:

  • keep height consistency between skeleton and full rendering
  • keep skeleton addressable (card boundary id)

I don't love that the script feature builder has to know when to opt in to declare a parallel getSkeletonAttributes, that needs to stay in sync with the rendered styling.

one alternative is to remove the need to match skeleton height against full rendering height:

  • skeleton has approximate height, by using the same text style (and the existing getMinHeightForBlock)
    • this further reduces # of DOM elements, each block can just be a single text node (in most cases), rather than splitting up into spans based on getSkeletonAttributesForRange
    • for scrollbar size / position fidelity: it's probably fine if the scrollbar isn't 100% accurate, and if we care about its fidelity we can render it based on skeleton height so it doesn't depend on full rendering — I think it's already custom
  • as skeleton blocks scroll in from above the fold becomes fully rendered, or scroll out of view above, measure the difference and compensate in the same render pass
    • we already have useMaintainScriptPosition, I didn't look closely at it, and I think we need to make some changes to it to work for this purpose
  • for addressability: we can replace getSkeletonAttributesForRange with a more explicit / concrete interface to define id of a block — that might already suffice, or if we care about precision within a block, inject DOM elements w/ id

wdyt?

verification

I was going to try some scrolling in the https://github.com/descriptinc/descript/pull/38103 preview build but I suppose that depends on merging and publishing this, so I'll leave the verification to you!

the concern is that if the skeleton height and full render height diverge, since UseTextEditorContentVisibility tracks block heights based on what's actually rendered, for offscreen blocks it'd register the skeleton heights, and when scrolled into screen the height may jump. concretely, decorators that depend on redux state (e.g. card boundary in audio-only comp, as bugbot pointed out), and decorators with custom CSS (e.g. full-line script clips). I made a test project full of script clips, and a lot of scenes in a paragraph.

further notes

more claude-written notes in https://app.notion.com/p/descript/Script-skeleton-height-parity-vs-scroll-compensation-design-notes-3b2abe2e1a50819c969fcd2843131a86

@srubin

srubin commented Aug 4, 2026

Copy link
Copy Markdown
Author

design

looking at this PR and descriptinc/descript#38103, my understanding is that this approach exposes getSkeletonAttributesForRange to customize the skeleton styling, in order to:

* keep height consistency between skeleton and full rendering

* keep skeleton addressable (card boundary id)

I don't love that the script feature builder has to know when to opt in to declare a parallel getSkeletonAttributes, that needs to stay in sync with the rendered styling.

I agree with the principle that this API is too broad!

one alternative is to remove the need to match skeleton height against full rendering height:

* skeleton has approximate height, by using the same text style (and the existing `getMinHeightForBlock`)
  
  * this further reduces # of DOM elements, each block can just be a single text node (in most cases), rather than splitting up into spans based on `getSkeletonAttributesForRange`
  * for scrollbar size / position fidelity: it's probably fine if the scrollbar isn't 100% accurate, and if we care about its fidelity we can render it based on skeleton height so it doesn't depend on full rendering — I think it's already custom

* as skeleton blocks scroll in from above the fold becomes fully rendered, or scroll out of view above, measure the difference and compensate in the same render pass

This sounds more complex and error-prone than requiring a little bit of upfront developer effort and then relying on the browser to handle scrolling entirely on its own. But I'll noodle on it.

  * we already have `useMaintainScriptPosition`, I didn't look closely at it, and I think we need to make some changes to it to work for this purpose

* for addressability: we can replace `getSkeletonAttributesForRange` with a more explicit / concrete interface to define id of a block — that might already suffice, or if we care about precision within a block, inject DOM elements w/ id

Agreed! There's no reason to expose a full suite of attributes when all we need is an id.

wdyt?

verification

I was going to try some scrolling in the descriptinc/descript#38103 preview build but I suppose that depends on merging and publishing this, so I'll leave the verification to you!

I can always publish interim versions of draft-js for testing in the descript PRs. Will do once I take another pass.

the concern is that if the skeleton height and full render height diverge, since UseTextEditorContentVisibility tracks block heights based on what's actually rendered, for offscreen blocks it'd register the skeleton heights, and when scrolled into screen the height may jump. concretely, decorators that depend on redux state (e.g. card boundary in audio-only comp, as bugbot pointed out), and decorators with custom CSS (e.g. full-line script clips). I made a test project full of script clips, and a lot of scenes in a paragraph.

We might not even need the content visibility stuff anymore if we turn on skeletons, since the offscreen nodes will be very simple. We should profile that.

further notes

more claude-written notes in https://app.notion.com/p/descript/Script-skeleton-height-parity-vs-scroll-compensation-design-notes-3b2abe2e1a50819c969fcd2843131a86

@scottcheng

Copy link
Copy Markdown
  • as skeleton blocks scroll in from above the fold becomes fully rendered, or scroll out of view above, measure the difference and compensate in the same render pass

This sounds more complex and error-prone than requiring a little bit of upfront developer effort and then relying on the browser to handle scrolling entirely on its own. But I'll noodle on it.

yeah fair, I'm probably oversimplifying it, maybe worth a quick prototype to see if it's feasible?

also, my claude keeps suggesting that the native overflow-anchor may be the whole solution, and we don't need any custom scroll position compensation, but I haven't verified that.

@srubin

srubin commented Aug 4, 2026

Copy link
Copy Markdown
Author
  • as skeleton blocks scroll in from above the fold becomes fully rendered, or scroll out of view above, measure the difference and compensate in the same render pass

This sounds more complex and error-prone than requiring a little bit of upfront developer effort and then relying on the browser to handle scrolling entirely on its own. But I'll noodle on it.

yeah fair, I'm probably oversimplifying it, maybe worth a quick prototype to see if it's feasible?

also, my claude keeps suggesting that the native overflow-anchor may be the whole solution, and we don't need any custom scroll position compensation, but I haven't verified that.

For sure, will try it out!

@srubin srubin changed the title feat: add offscreen block skeletons Add offscreen block skeletons Aug 4, 2026
@scottcheng

Copy link
Copy Markdown

oops totally forgot about this!! just saw it in my review list in linear 🫠

did another review pass w/ cursor, I think it found a real bug: if the last paragraph is split with a span with id (e.g. card boundary), when that last paragraph is rendered as skeleton, select all puts end cursor in the wrong place and doesn't actually select everything:

Screen.Recording.2026-08-28.at.11.47.53.AM.mov

the simplest fix I can think of (without diving too deep) is to not split paragraph with id spans - put the id spans at the beginning of paragraphs, and keep the entire paragraph as one text node. this only doesn't work if the paragraph is really long, where scrolling to the id would be inaccurate, but long paragraphs should be rare given we split them by default in transcription?

@scottcheng scottcheng left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approving anyway since it's already in prod 🙂

srubin added 2 commits August 28, 2026 10:03
Persistent marker and card anchors split skeleton block text into multiple DOM nodes. Convert selection endpoints inside skeletons to cumulative block offsets so native selections, including Select All, cover the complete Draft content.
Automated fixes applied by autofix-pr:
- install the pinned pnpm toolchain in CI
- run the build on Node 24 with current setup actions

Agent-Harness: Codex
@srubin
srubin merged commit a4c3183 into main-pojo Aug 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants