Slides: edit text inside shapes (PowerPoint / Google Slides parity)#321
Conversation
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]>
|
Warning Review limit reached
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (22)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Verification: verify:selfResult: ✅ PASS in 257.9s
Verification: verify:integrationResult: ✅ PASS |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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]>
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]>
Summary
TextBodytype fromTextElement.data, addsShapeElement.data.text?: TextBody. Backward-compatible (optional field, no migration).SlidesStore.withShapeText(slideId, elementId, cb)mirrorswithTextElement. Concurrency-safe: only ever writesdata.text.blocks, never deletesdata.text(adeletewould race-wipe peer typing).data.texton top of shape fill/stroke via a sharedpaintTextBodyhelper (intext-renderer.ts) with PowerPoint default insets (SHAPE_TEXT_PADDING = { x: 14.4, y: 7.2 }) and'middle'vertical anchor.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 atoriginY=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.<p:sp>with bothprstGeomand<p:txBody>now folds into oneShapeElement.data.textinstead of the prior two-element layered form. Stand-alonetxBox-preset text boxes keep producingTextElement.slides-shapes.mdupdated — 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)
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 aninitialTextthroughmountSlidesTextBoxinto the docs editor — deferred follow-up.data.text).Test plan
pnpm verify:fastgreen (frontend + backend + slides + sheets + docs lint/typecheck/tests).pnpm verify:selfgreen (adds the package builds).packages/slides/test/store/memory.test.ts—withShapeTextround-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 indata.text; mount frame inset bySHAPE_TEXT_PADDING; image (non-text/non-shape) doesn't enter edit; clean shape committed empty doesn't materializedata.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 withdata.text; empty<p:txBody>does NOT seeddata.text.packages/frontend/tests/app/slides/yorkie-slides-store.test.ts—withShapeTextround-trip through a real Yorkie doc; no-destructive-delete confirmed; no-op short-circuit; non-shape throws.pnpm dev:data.textper shape (not the layered form).🤖 Generated with Claude Code