Skip to content

Slides: edit text inside shapes (PowerPoint / Google Slides parity)#321

Merged
hackerwins merged 1 commit into
mainfrom
slides-shape-inline-text
May 31, 2026
Merged

Slides: edit text inside shapes (PowerPoint / Google Slides parity)#321
hackerwins merged 1 commit into
mainfrom
slides-shape-inline-text

Conversation

@hackerwins

@hackerwins hackerwins commented May 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Shapes now carry an optional inline text body painted on top of fill/stroke and editable via the existing docs text-box editor — double-click, F2/Enter, or typing a printable character on a selected shape all enter text edit. Matches PowerPoint / Google Slides behavior where every autoshape is a text container.
  • Hoists a shared TextBody type from TextElement.data, adds ShapeElement.data.text?: TextBody. Backward-compatible (optional field, no migration).
  • New SlidesStore.withShapeText(slideId, elementId, cb) mirrors withTextElement. Concurrency-safe: only ever writes data.text.blocks, never deletes data.text (a delete would race-wipe peer typing).
  • Renderer paints data.text on top of shape fill/stroke via a shared paintTextBody helper (in text-renderer.ts) with PowerPoint default insets (SHAPE_TEXT_PADDING = { x: 14.4, y: 7.2 }) and 'middle' vertical anchor.
  • Editor mounts the in-place text-box on the inner padded frame so the caret aligns with the committed paint. New growMode: 'never' opt keeps the editor canvas at the inner-frame height so the middle anchor matches the renderer (without it the canvas shrinks to text height + anchors at originY=0, producing a visible jump on commit). The shape stays visible during edit — only the text body is hidden from the slide-canvas pass, not the whole element.
  • PPTX import: <p:sp> with both prstGeom and <p:txBody> now folds into one ShapeElement.data.text instead of the prior two-element layered form. Stand-alone txBox-preset text boxes keep producing TextElement.
  • Design doc slides-shapes.md updated — removes "Shape-internal text" from non-goals; adds a "Shape text body" section covering the data-model contract, render order, editor entry table, store API, PPTX mapping, and v1 limitations.

v1 limitations (called out in design doc)

  • Type-to-edit consumes the first keystroke (preventDefault) but doesn't yet insert it into the freshly-mounted text-box; the user has to type it again. Forwarding the initial character requires threading an initialText through mountSlidesTextBox into the docs editor — deferred follow-up.
  • Two import formats coexist in Yorkie storage: pre-feature decks keep their layered (ShapeElement + paired TextElement) form indefinitely (no migration); post-feature imports use the folded form (one ShapeElement with data.text).

Test plan

  • pnpm verify:fast green (frontend + backend + slides + sheets + docs lint/typecheck/tests).
  • pnpm verify:self green (adds the package builds).
  • New unit tests:
    • packages/slides/test/store/memory.test.tswithShapeText round-trip, seed-on-entry, no-delete-on-empty (preserves empty body if user typed and cleared), no-op when entered with no text and committed empty.
    • packages/slides/test/view/canvas/shape-renderer-text.test.ts — text paints on top of fill, skips when absent/empty, paints on placeholder-rect fallback, applies the PowerPoint-default 14.4 / 7.2 px insets.
    • packages/slides/test/view/editor/text-box-editor.test.ts — double-click on shape enters edit; commit lands in data.text; mount frame inset by SHAPE_TEXT_PADDING; image (non-text/non-shape) doesn't enter edit; clean shape committed empty doesn't materialize data.text.
    • packages/slides/test/view/editor/interactions/keyboard.test.ts — Enter on selected shape enters edit; type-to-edit on text + shape; arrow keys / Cmd+S don't fire (printable + modifier gates); image doesn't fire.
    • packages/slides/test/import/pptx/shape.test.ts<p:sp> with prstGeom + txBody folds into one ShapeElement with data.text; empty <p:txBody> does NOT seed data.text.
    • packages/frontend/tests/app/slides/yorkie-slides-store.test.tswithShapeText round-trip through a real Yorkie doc; no-destructive-delete confirmed; no-op short-circuit; non-shape throws.
  • Manual smoke in pnpm dev:
    • Insert a roundRect, double-click, type "Hello", ESC — shape stays visible during edit; text persists at the same vertical position before/after commit.
    • Insert a shape, type 'x' while it's selected (no double-click) — enters edit mode (note v1 caveat: 'x' itself not yet inserted).
    • Two-client Yorkie smoke: both clients edit the same shape's text; no divergence; B's blur on an empty body does not wipe A's typed content.
    • Import a PPTX deck with labelled shapes; verify single ShapeElement with data.text per shape (not the layered form).

🤖 Generated with Claude Code

Shapes now carry an optional inline text body painted on top of
fill/stroke, edited via the existing docs text-box editor. Matches
PowerPoint and Google Slides where every autoshape is a text
container — double-click, F2/Enter, or typing a printable character
on a selected shape enters text edit; ESC/blur commits.

Pre-feature, text inside a shape was a separately-positioned
TextElement overlapping the shape (a workaround documented as a
non-goal in slides-shapes.md). That model diverged from OOXML
(`<p:sp>` always pairs `<p:spPr>` with `<p:txBody>`), broke PPTX
round-trip fidelity, and forced users to group / move / resize two
elements they thought of as one.

Surfaces:
- Data: ShapeElement.data.text?: TextBody (shared with TextElement via
  intersection); isBlocksEmpty hoisted.
- Renderer: paintTextBody shared between shapes and text elements with
  padding + defaultVerticalAnchor opts; shapes use the PowerPoint
  default 91440 / 45720 EMU insets (14.4 / 7.2 px at 1920×1080) and
  middle anchor.
- Store: withShapeText mirrors withTextElement. Writes are per-field
  (never `delete data.text`) so a concurrent peer's typing can't be
  wiped by a wholesale-field LWW op on blur.
- Editor: onDoubleClick / F2 / Enter / type-to-edit accept 'shape' in
  addition to 'text'. Shape edit mounts the text-box on the inner
  padded frame so caret aligns with the committed paint. New
  growMode: 'never' opt keeps the editor canvas at the inner-frame
  height so the middle vertical anchor matches the renderer (without
  it the editor shrinks to text height + originY=0 and the renderer
  middle-anchors → visible jump on commit). The shape stays visible
  during edit (only the text body is hidden, not the whole element).
- PPTX import: <p:sp> with prstGeom + <p:txBody> folds into one
  ShapeElement.data.text instead of the prior two-element layered
  form. Stand-alone txBox-preset text boxes keep producing TextElement.
- Docs: slides-shapes.md "Shape text body" section + task docs.

v1 limitations called out: type-to-edit consumes the first keystroke
but doesn't insert it (requires plumbing initialText through
mountSlidesTextBox); two import formats coexist in Yorkie storage
(pre-feature layered + post-feature folded — no migration).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hackerwins, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 8 minutes and 50 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40f5ea39-699e-4dab-aeb8-6f1780455ba1

📥 Commits

Reviewing files that changed from the base of the PR and between 41c6341 and 0bd6d82.

📒 Files selected for processing (22)
  • docs/design/slides/slides-shapes.md
  • docs/tasks/README.md
  • docs/tasks/active/20260531-slides-shape-inline-text-lessons.md
  • docs/tasks/active/20260531-slides-shape-inline-text-todo.md
  • packages/frontend/src/app/slides/yorkie-slides-store.ts
  • packages/frontend/tests/app/slides/yorkie-slides-store.test.ts
  • packages/slides/src/import/pptx/shape.ts
  • packages/slides/src/index.ts
  • packages/slides/src/model/element.ts
  • packages/slides/src/node.ts
  • packages/slides/src/store/memory.ts
  • packages/slides/src/store/store.ts
  • packages/slides/src/view/canvas/shape-renderer.ts
  • packages/slides/src/view/canvas/text-renderer.ts
  • packages/slides/src/view/editor/editor.ts
  • packages/slides/src/view/editor/interactions/keyboard.ts
  • packages/slides/src/view/editor/text-box-editor.ts
  • packages/slides/test/import/pptx/shape.test.ts
  • packages/slides/test/store/memory.test.ts
  • packages/slides/test/view/canvas/shape-renderer-text.test.ts
  • packages/slides/test/view/editor/interactions/keyboard.test.ts
  • packages/slides/test/view/editor/text-box-editor.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slides-shape-inline-text

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 257.9s

Lane Status Duration
tokens:build ✅ pass 2.4s
sheets:build ✅ pass 13.3s
docs:build ✅ pass 12.7s
slides:build ✅ pass 14.6s
verify:fast ✅ pass 171.7s
frontend:build ✅ pass 18.5s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 5.1s
cli:build ✅ pass 2.2s
verify:entropy ✅ pass 17.1s

Verification: verify:integration

Result: ✅ PASS

@codecov

codecov Bot commented May 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@hackerwins
hackerwins merged commit 15f84a8 into main May 31, 2026
4 checks passed
@hackerwins
hackerwins deleted the slides-shape-inline-text branch May 31, 2026 07:58
hackerwins added a commit that referenced this pull request May 31, 2026
PRs #257/#301/#303/#304/#305/#306/#307/#309/#315/#316/#317/#321/#322
shipped between v0.4.2 and the v0.4.3 cut, leaving their paired todo
files in active with the process checkboxes still open. Flip the
remaining boxes for the work that landed, then move the paired
todo/lessons into docs/tasks/archive/2026/05/ via pnpm tasks:archive.
Active count drops from 17 to 6; archive count grows by 21 files.

Patch release covering 29 commits since v0.4.2. Headline theme is
Slides editing & UX polish toward Google Slides / PowerPoint parity:
smart guides (equal-spacing / distance / size), Shift drag modifiers,
text inside shapes, a Format options right panel, tier-1 universal
toolbar controls, text autofit (shrink + grow), group-selection
visuals, and several smaller text-box / selection fixes. PPTX import
gained paragraph-level bullet styles, body anchor (vertical
alignment), and stretched blipFill cover-crop.

Docs added a pending-inline-style at collapsed caret, a caret that
tracks the resolved text color, bullet indent on Tab / Shift+Tab,
font / size / line-spacing / clear formatting toolbar controls, an
inline-style-attribute clear that actually lands in the CRDT, and a
polished toolbar trigger / color swatch pass shared across docs /
sheets / slides. Sheets gained .xlsx import.

Infrastructure: Docker images now build on native arm64 runners,
fixing the release hang seen on v0.4.2. No Prisma migration, no new
backend env vars — the devops bump is a routine image-tag change.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
hackerwins added a commit that referenced this pull request Jun 1, 2026
Two coupled bugs prevented selecting PPTX-imported text placeholder
shapes (type:'shape', kind:'rect', data.text set, no fill / no stroke).
On the originally-reported deck every text click landed on the back-
most full-bleed background image instead.

1. hitShape's visibility gate ignored data.text. PR #321 folded inline
   text into ShapeElement.data.text but didn't update the hit-test
   side, so text-only shapes were silently unclickable. Extend the
   gate to include hasText, with the same OPEN_PATH_KINDS exclusion
   the fill branch carries so bracket / brace shapes with text fall
   through to the stroke-band test rather than false-positive-ing
   across an auto-closed C / U interior.

2. SlideRenderer leaves the ctx with scale((hostWidth / SLIDE_WIDTH)
   * dpr) after each paint. Path2D variants of isPointInPath /
   isPointInStroke interpret the path through the active transform
   while the query (lx, ly) stays in canvas-pixel space, so interior
   clicks in logical coords landed outside every shape's scaled-down
   footprint. Route all four hitTestSlide call sites through a new
   SlidesEditorImpl.hitTestAt helper that pins the ctx to identity
   around the hit-test and restores on exit. The unit-test canvas
   stub ignores transforms, which is why this latent bug went
   unnoticed; filled non-rect shapes were silently affected too and
   were partially masked by the stroke-band fallback.

Bundled with this PR: vitest 3 → ^4.1.8 across the workspace to
clear GHSA-5xrq-8626-4rwp (vitest UI critical advisory, 22
transitive paths). scripts/verify-entropy.mjs:267 fails the pre-push
lane on any critical, so the bug fix and the dep bump have to ship
together. v4 type-side carry-overs: packages/{sheets,docs}/vite.config
.ts import defineConfig from vitest/config (Vite's UserConfigExport
no longer carries the test key in v4), and slides ctx-spy types
swap ReturnType<typeof vi.fn> for Mock<(...args: any[]) => any> so
TypeScript can invoke the spies after v4 widened the default return
to Mock<Procedure | Constructable>.

Adds three regression tests in hit-test-elements.test.ts: text-only
shape hits via bbox interior, empty shape misses, and OPEN_PATH_KINDS
shape with text still does not hit on its auto-closed interior.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant