Add @wafflebase/slides package - Phase 1-5a + UI chrome#190
Merged
Conversation
Establishes the design baseline for @wafflebase/slides v1 (Google Slides-style presentation engine) before any implementation lands. The spec is the canonical reference; the Phase 1 plan unfolds the foundation work (package scaffold, model types, MemSlidesStore) into TDD-shaped tasks that subsequent commits on this branch implement. This commit only adds documentation — no code yet, so verify:fast is unchanged at this revision. Refs docs/design/slides/slides.md.
Adds package.json, tsconfig, vite configs (test + build), README, and empty src/index.ts so subsequent tasks can land model/store/view in isolation. Wires the package into the root build, test, verify:fast, and pnpm filter aliases. Mirrors the @wafflebase/sheets and @wafflebase/docs scaffolding so future contributors find no surprises. Refs docs/design/slides/slides.md.
TextElement reuses @wafflebase/docs Block[] for rich text so Phase 5 can wire the docs IME bridge without translating types. Background is a discriminated object so v2 image-fill backgrounds extend cleanly. Slide.notes is in v1 even though the presenter-view that displays notes ships in v2 (per spec Non-Goals). Refs docs/design/slides/slides.md sections 'Data model' and 'Future parity with Google Slides'.
Aligns the model with the design spec at docs/design/slides/slides.md which defines background.image as ImageRef. Also resolves the 'unused export' warning the reviewer surfaced — ImageRef now has the single intended consumer in v1. Refs Task 2 of docs/tasks/active/20260505-slides-phase1-plan.md.
containsPoint converts world coordinates into the frame's local space by rotating around the frame center, which is the same convention the Phase 3 resize handles will use. boundingBox and combinedBoundingBox give the editor enough to draw selection rectangles around rotated single and multi-selections without re-implementing rotation math. Tests cover axis-aligned, 90°, 360° round-trip, and 45° bounding-box growth — the stepping stones to the larger resize-handle matrix in Phase 3. Refs docs/design/slides/slides.md section 'Rendering pipeline' (coordinate system / hit-testing) and the Phase 3 todo entry.
Layouts in v1 are a fixed code-defined set, per the spec 'Non-Goals' — user-editable master slides arrive in v2. Defining them here as plain data lets store.addSlide and store.applyLayout consume them uniformly with no special cases. Coordinates use the SLIDE_WIDTH/HEIGHT constants from presentation.ts so the layouts stay consistent if we later switch the default canvas size. Refs docs/design/slides/slides.md sections 'Data model' (Layout) and 'Non-Goals'.
Single mutation entry point for the slides engine, modelled on the DocStore interface in @wafflebase/docs. The text and notes bridges take a plain Block[] callback so MemSlidesStore can implement them trivially in Phase 1; the Yorkie store in Phase 4 swaps in real Yorkie.Tree access without changing the contract. Refs docs/design/slides/slides.md section 'Store interface'.
Adds add/remove/move/duplicate slide and updateSlideBackground with deep cloning on every read/write — same JSON.parse(JSON.stringify()) strategy as @wafflebase/docs MemDocStore. Element/text/batch ops are stubbed with explicit 'not implemented yet' throws so a missed call surfaces immediately while the next tasks land them. Tests cover insert position, multi-remove, deep duplicate (regenerated element ids), and reference-vs-clone safety on background updates. Refs docs/design/slides/slides.md section 'Store interface'.
Element add/remove/update/reorder with the same clone-on-write discipline as the slide ops. updateElementData takes a plain partial object and shallow-merges it into the element's discriminated data, which is enough for v1 — Yorkie store will do the equivalent at the CRDT level in Phase 4. Tests cover happy paths plus the not-found error messages so the editor surfaces real problems instead of silent no-ops. Refs docs/design/slides/slides.md section 'Store interface'.
withTextElement / withNotes accept either a void in-place mutation or a returned Block[] replacement, matching how the Phase 5 docs IME bridge will hand the live Yorkie.Tree back to the caller. applyLayout adds missing placeholders without ever deleting user-edited elements, per the spec's 'Layout reapplication' policy — destructive resets are v2 master-slide work. Refs docs/design/slides/slides.md sections 'Store interface' and 'Data model > Layout reapplication'.
Snapshot the document at the start of each top-level batch and clear the redo stack on any new mutation, matching MemDocStore. Mutations outside a batch now throw — this surfaces editor bugs (forgetting to group an action) at the moment they happen instead of at undo time. Nested batches collapse into the outer one so compound editor flows like 'paste N elements' undo as a single user-visible step. Refs docs/design/slides/slides.md section 'Yorkie schema > Undo grouping'.
Curates the package's public surface so frontend / cli / tests import from '@wafflebase/slides' (never deep paths). Marks Phase 1 checklist items complete in the task tracker. verify:fast green at this commit. Refs docs/design/slides/slides.md section 'Package layout' and the Phase 1 entries in docs/tasks/active/20260505-slides-package-mvp-todo.md.
Aligns addSlide with the clamp-on-bounds policy already used by moveSlide / moveSlides / reorderElement so an out-of-range atIndex saturates to the closest valid position instead of going through splice's end-relative semantics on negatives. New test covers negative, > length, and middle insert. Also back-ports three small fixes into the Phase 1 plan that surfaced during implementation: --passWithNoTests on the vitest scripts (Vitest 3 exits 1 with no tests by default), defineConfig from 'vitest/config' in the empty-source scaffold (so tsc --noEmit can resolve the test field), and the corrected boundingBox-grows assertion (the original test asserted box.w > 100 for a 100×40 frame at 45°, but the width actually shrinks to ~99 because the long edge tilts onto the diagonal). Future re-runs of Phase 1 from the plan no longer hit these snags.
Stands up the Phase 2 directory structure: a tiny ctx-spy helper that backs every renderer test (jsdom has no Canvas 2D), an index.html serving a 960x540 placeholder canvas at pnpm slides dev, and a demo.ts that clears to a flat colour so the Vite dev server boots end-to-end before any renderer lands. Subsequent tasks add shape/image/text/ element/slide renderers in isolation; T8 swaps demo.ts for a real fixture. Refs docs/tasks/active/20260506-slides-phase2-plan.md Task 1.
Pure ctx-driven painter that draws each shape in element-local coordinates. The frame transform (position + rotation) is the element-renderer's job in T5, so this module never sees frame.x or frame.rotation. Each shape paints its fill (if set) and stroke (if set) independently — a stroke-only rectangle and a fill-only line are both legitimate inputs. The arrow head is a filled triangle scaled to the frame's shorter dimension so a long thin frame still gets a recognisable arrowhead. Tests cover fill, stroke, and the no-paint case for each shape. Refs docs/design/slides/slides.md section 'Rendering pipeline'.
image-cache mirrors docs/src/view/image-cache.ts byte-for-byte (modulo the test-only clear helper) so slides and docs share the same lazy load + multi-subscriber semantics. We copy rather than import because docs does not export this helper publicly and the function is small enough that the duplication cost is lower than the coupling cost. drawImage returns true|false so the slide-renderer in T6 can decide whether to schedule a re-render after the bitmap arrives. The crop path maps the Crop rectangle (image-relative 0..1 coords) onto the source rectangle of drawImage's 9-arg form. Loaded-path coverage in vitest is structural only — jsdom never fires onload for network URLs. The real verification is the T8 demo running in a browser. Test file flags this trade-off inline. Refs docs/design/slides/slides.md section 'Rendering pipeline'.
@wafflebase/docs ships separate browser and node bundles via package exports conditions. The node bundle (which vitest resolves to in slides tests) intentionally omits browser-only symbols like CanvasTextMeasurer. Aliasing to source — the same pattern packages/frontend/vite.config.ts already uses — lets slides tests import the full surface without a TypeError at load time. Unblocks Phase 2 Task 4 (text-renderer).
Calls computeLayout from @wafflebase/docs with the text element's blocks and the frame width, then walks the resulting block-line-run tree and emits one fillText per run. Reuses a module-scope CanvasTextMeasurer so the per-measurer width cache survives across renders — a fresh measurer per call would thrash the cache and effectively double the measurement cost. Image runs (LayoutRun.imageHeight != undefined) are skipped — slides text boxes hold no inline images in v1, top-level image elements are their own renderer (T3). Baseline placement is approximate; Phase 5 will align it with the docs IME bridge. Tests run under @vitest-environment jsdom with a small OffscreenCanvas shim — jsdom does not implement Canvas 2D, so the docs measurer's lazy ctx acquisition needs a fake offscreen canvas to fall through to. Width returns 8 px per character so layout assertions stay deterministic. Refs docs/design/slides/slides.md section 'Rendering pipeline > Text rendering'.
One module owns the world<->local coordinate transform so each per-type painter (shape/text/image) sees only (w, h). Rotation pivots around the frame centre via translate-rotate-translate, matching the hit-testing convention in model/frame.ts so click and paint stay in agreement. The dispatcher passes an onAssetLoad callback through to drawImage so the slide-renderer in T6 can request a re-render once a bitmap finishes loading. Save/restore wraps the entire dispatch so a per-type painter that forgets to reset state cannot leak ctx mutations to the next element. Extract the OffscreenCanvas shim that text-renderer.test.ts had inline into a shared test-canvas-env helper, since element-renderer.test.ts also needs it (the text-dispatch test pulls in text-renderer transitively, and that module instantiates CanvasTextMeasurer at load). Refs docs/design/slides/slides.md section 'Rendering pipeline > Coordinate system'.
Owns the logical-to-host coordinate transform: a 1920x1080 logical canvas maps to the host <canvas>'s CSS pixels times the device pixel ratio. setTransform(1,0,0,1,0,0) at the top of every render resets any leftover state so the renderer is robust to ctx mutations from neighbouring code (e.g. a future selection overlay sharing the same canvas). Elements paint in array order (= z-order, per the spec), and an async image load schedules a single repaint via markDirty so the slide re-renders exactly once after the bitmap arrives. Image-fill backgrounds are documented as v2 work; v1 honours only background.fill. Refs docs/design/slides/slides.md sections 'Rendering pipeline > Dirty tracking' and 'Yorkie schema > z-order'.
renderThumbnail is a thin reuse of SlideRenderer at a smaller host size — no dirty tracking, since the caller already decided the thumbnail needs to refresh. ThumbnailScheduler debounces rapid edits into one onFlush call per quiet window, which keeps the thumbnail panel responsive during typing and dragging without redrawing every keystroke. flushNow() gives the editor an escape hatch (e.g. on slide-switch or blur) where waiting another 200 ms for the previously-edited slide's thumbnail would feel laggy. Refs docs/design/slides/slides.md section 'Rendering pipeline > Dirty tracking' (thumbnail re-render debounce ~200ms).
demo.ts now builds a fixture deck through the public MemSlidesStore + SlideRenderer API and exercises every renderer landed in T2-T7: each shape kind, multi-style text via docs computeLayout, a rotated arrow to confirm the frame transform, and an SVG-encoded image placeholder (loads synchronously in the browser; real workspace images come in later phases). Renderer entry points are added to packages/slides/src/index.ts so the editor in Phase 3 imports through the package boundary, never through deep paths. The demo also marks pnpm slides dev as a self-contained smoke check for future contributors. Phase 2 checklist items 2.1-2.6 are ticked in the Phase 1+2 todo. verify:fast green at this commit.
Captures the implementation plan that drove the Phase 2 commits (2307474..563dd81), with corrections back-ported from issues discovered during execution: shape test count off by one, jsdom directive needed for image-renderer tests, vite alias required for text-renderer (CanvasTextMeasurer is omitted from the docs node bundle), OffscreenCanvas shim needed for text-renderer tests, and the 'Hello world' assertion adjusted for computeLayout's word-level segmentation. Future re-runs of Phase 2 from this plan no longer hit these snags.
Stands up the Phase 3 controller surface: a vanilla-TypeScript
SlidesEditor with render/getSelection/onSelectionChange/setInsertMode/
detach, mirroring the initialize() pattern from packages/docs and
packages/sheets so frontend can wrap it in React the same way it
wraps the existing engines.
Selection is editor-local with a small subscribe/notify API; no
selection state lives in the SlidesDocument (Phase 4 presence covers
peer awareness). Includes a suppress-no-op guard so set([a,b]) with
the same selection does not fire subscribers — relied on by T2's
overlay re-render gate.
keymap.ts is a verbatim copy of packages/sheets/src/view/keymap.ts;
copying avoids a slides → sheets runtime dependency (sheets brings
antlr4ts and the formula engine, neither wanted here). The two copies
must be hand-synced.
test-canvas-env.ts also patches HTMLCanvasElement.prototype.getContext
('2d') so jsdom-backed editor tests can construct a SlidesEditor
without a real canvas backing — companion to the existing
OffscreenCanvas shim.
Refs docs/design/slides/slides.md section 'Editor UI'.
Selection now paints a frame outline + 8 resize handles + 1 rotate handle into the host overlay div on every change. handle-hit-test finds the kind under a pointer position by reading data-handle attributes; T3 will use it from the canvas mousedown dispatcher. For Phase 3a we accept the deliberate simplification that handles sit on the combined axis-aligned bbox even for a single rotated element - resize math (T5) follows the same convention so click and drag stay in agreement. Per-rotated-element handles are a v2 polish item. Refs docs/design/slides/slides.md sections 'Editor UI' and 'Interactions' (Select / Multi-select rows).
selectAt is the pure decision: hit-test topmost element under the pointer, optionally shift-toggle. selectInRect is the lasso payload: bbox intersection, edge contact counts as a hit (matches Google Slides). The editor wires both via canvas mousedown — handle / insert hits short-circuit out so T4-T7 can take over, and shift-click on empty canvas is a no-op so it does not accidentally clear an additive multi-select. A click-without-drag on empty canvas clears selection. The 2-pixel threshold distinguishes 'click' from 'lasso' so a careless mousedown does not produce a tiny rubber-band. Refs docs/design/slides/slides.md 'Interactions' table rows Select / Multi-select / Lasso select.
mousedown on a selected element starts a drag; mousemove updates an in-memory frame map and forceRender's the canvas + overlay every frame; mouseup commits one updateElementFrame per dragged element inside a single store.batch — exactly one undo entry per drag. snapDelta proposes the smallest adjustment that lands the dragged group's centre or any of its edges on the slide centre or an non-selected element's edge, within an 8 px threshold. v1 leaves guideline visualisation out — the snap effect alone is enough to feel responsive; visible guidelines arrive in 3b polish. SlideRenderer gains forceRender as the 'skip the dirty check' entry point for interaction live-paint. Tests cover both the pure delta math and an end-to-end drag through the editor. Refs docs/design/slides/slides.md 'Interactions' table row Drag move, and 'Yorkie schema > Undo grouping' for the one-batch-per-drag rule.
resizeFrame is the pure decision: each handle anchors at the opposite corner or edge, dragging moves the active edges, and shift preserves aspect by adopting the larger relative axis change. A 1 px minimum clamps both dimensions so frames never invert. Editor wiring is single-element only in v1 — multi-element proportional resize is a v2 polish item. Per the T2 simplification, handles operate on the axis-aligned bbox even for rotated frames; the rotated-resize-around-element-axis case lands later when overlay gains per-rotated-frame handles. Refs docs/design/slides/slides.md 'Interactions' table row Resize.
Dragging the rotate handle (positioned 24 px above the bbox top-centre) spins the single selected element around its frame centre. shift snaps to 15° steps (the same snap docs uses for line angle); release commits one updateElementFrame batch with the final rotation. Multi-element rotate is deferred to v2 — the question of 'rotate around what?' (each element's own centre vs the group bbox centre) is a UX decision worth dedicated brainstorming, and v1 doesn't need it. Refs docs/design/slides/slides.md 'Interactions' table row Rotate.
setInsertMode(kind) puts the editor into 'place a new element' mode. The next canvas mousedown anchors the start; mousemove drags out a live preview; mouseup commits one addElement inside a single batch and selects the new element. Tiny drags (< 4 px on both axes) fall back to a default 200x100 frame so a careless click still gives the user something usable. Text insert is single-click — drag-to-size is rare for text and the default 400x80 frame gives a useful starting size for typing. Insert mode auto-exits to idle after a successful place; the toolbar is responsible for re-entering it if the user wants to insert another. Refs docs/design/slides/slides.md 'Interactions' table row Add shape/text/image.
drawElement called ctx.save() then ran the per-type painter then ctx.restore() on the happy path. If any painter (drawShape / drawText / drawImage) throws, the restore is skipped and the translate / rotate transform leaks into every subsequent element on the slide — one corrupted element corrupts the whole rendering pass. Wrap the painter dispatch in try/finally so the restore always runs. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Two existing tests hand-waved the painted path: one only checked that onLoad wasn't fired (without verifying the painted call), the other only checked that crop didn't throw (without verifying the crop rect was passed through to ctx.drawImage). Stub the global Image constructor with a FakeImage that auto-completes on the next microtask, then drive the load lifecycle synchronously. Now both tests verify ctx.drawImage call shape: dx/dy/dw/dh for the uncropped path, sx/sy/sw/sh + dx/dy/dw/dh for the crop path. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
scale derived from hostWidth alone distorts the slide when the host canvas's aspect ratio differs from SLIDE_WIDTH:SLIDE_HEIGHT (16:9). The current SlidesView uses a fixed 960x540 host, so both axes give the same value today, but a future responsive layout or non-16:9 host would stretch the rendering horizontally. Compute scaleX and scaleY independently and take Math.min so the slide always fits inside the host without distortion. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Phase 1-5a (foundation, rendering, editor, multi-user collab, text editing) and the UI chrome refactor are all landed on PR #190. Their plan files move from docs/tasks/active to docs/tasks/archive/2026/05/. Stays in active: - 20260505-slides-package-mvp-{todo,lessons}.md (master tracking; Phase 5b image input / 5b present / 5b PDF / 5c CLI items still unchecked). - 20260507-slides-phase5b-1-image-plan.md (not started yet). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Pressing Escape inside a slides text-box now discards the user's in-flight typing, matching the Word / Google Docs convention. Until now Escape behaved like blur — the docs editor fires onCancel, then the subsequent blur cascades through onCommit, and the slides editor wrote those edits via withTextElement regardless. Implementation: a flag in enterEditMode that onCancel flips, which onCommit consults before calling withTextElement. The finishEditMode teardown still runs in both branches so the editor cleanly exits regardless. The CR/PR comment that flagged this as a v1.1 follow-up is now unblocked — the discard story is in v1. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Spec: 'Image load failure - render a placeholder box with the alt text and a retry affordance.' We had nothing - the image element just stayed blank forever. Now image-cache tracks failed src URLs in a Set + exposes isImageFailed(src), and drawImage paints a light-grey dashed placeholder with 'Image unavailable' + the alt text when the load failed. The retry affordance is deferred - the placeholder is the non-trivial part. Adding a click-to-retry button on top is a follow up that needs UI design (where does the button live, how does it look on a tiny image, etc.). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Two UI completeness gaps the user flagged after the chrome refactor: 1. Left thumbnail panel did not scroll when the deck grew past the visible viewport. The grid had height: 100% on the layout but no minmax(0, 1fr) on the row, so CSS-grid let the row expand to the panel's intrinsic content height. Added a row template + min-height: 0 on the column so overflowY: auto actually clips and scrolls. 2. Right canvas was hard-coded at 960x540 host pixels regardless of the available width. Replaced with a ResizeObserver-driven fit: the canvas (and overlay) get sized each frame to the right column's content rect minus the notes panel reservation, capped at 1600px host width. SlidesEditor.setHostSize feeds the new dimensions into the renderer's scale and triggers a repaint. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Splits the slides 2-pane grid into 3 columns - thumbs / 6px handle / canvas - and wires mouse drag on the handle to update the left column's width. Width persists to localStorage (wfb-slides-left-width) clamped to 120-480px. The handle is functionally a 6px hit area with a 1px center line that grows to 2px primary-coloured on hover, matching the resizer style elsewhere in the app. role=separator + aria-orientation are set so screen readers announce the affordance. The right column's ResizeObserver fires automatically when leftWidth changes (the right column shrinks/grows in response), so the canvas auto-fit picks up the new width without any extra wiring. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
hackerwins
added a commit
that referenced
this pull request
May 7, 2026
Introduces a new presentation editor sibling to @wafflebase/sheets and @wafflebase/docs. A presentation is one Yorkie document; each slide is a free-position canvas with text boxes, images, and shapes. Text rendering reuses docs' computeLayout + paintLayout through a new initializeTextBox factory, so font, size, alignment, and list rendering are pixel-identical between an in-place slide text-box and a standalone document. What's in: - Phase 1 — Foundation: package scaffold, model (SlidesDocument / Slide / TextElement / ImageElement / ShapeElement / Layout, frame math + rotation matrices), SlidesStore interface, MemSlidesStore reference impl with batch()-based undo/redo grouping. - Phase 2 — Static rendering: element / slide / thumbnail Canvas renderers, dirty-tracked SlideRenderer, demo HTML harness. - Phase 3 — Single-user editor: selection (single + multi + lasso), drag / 8-handle resize / rotate (incl. rotated-frame math), toolbar insert, nudge, clipboard via application/x-wafflebase-slides+json, multi-slide thumbnail panel, context menus, speaker notes panel, undo/redo via store.batch. - Phase 4 — Yorkie + multi-user: YorkieSlidesStore adapter, equivalence tests vs MemSlidesStore, SlidesPresence schema, peer cursors, backend SlidesDocument type, /p/:id route, two-user concurrency integration tests. - Phase 5a — Text editing: in-place text-box on double-click, Korean / Latin / IME typing, Escape-as-discard, blur commits via store.withTextElement, slide-canvas paintLayout reuse so committed text matches the editor's rendering, sparse-style block normalisation. - UI chrome refactor: SlidesDetail wraps in SidebarProvider + AppSidebar + SiteHeader (with ShareDialog + UserPresence), new React SlidesFormattingToolbar replaces the raw-DOM toolbar, draggable resizer between thumbnails and canvas with localStorage persistence, ResizeObserver-driven canvas auto-fit (16:9), scrollable thumbnail panel, image load-failure placeholder with alt text. What's NOT in (follow-up PRs): - 5.3 Image input (toolbar / drag-drop / paste) — plan committed at docs/tasks/active/20260507-slides-phase5b-1-image-plan.md. - 5.5 Presentation mode (fullscreen / key nav). - 5.6 PDF export (reusing docs pdf-lib + Noto KR pipeline). - 5.7–5.10 CLI commands, visual harness scenario, verify:full. - Phase 5a-2: per-keystroke text convergence (root-level Tree map). Phase 5a uses last-write-wins on blur after a Yorkie.Tree migration was reverted — nested Tree inside an array element gets JSON-serialised by Yorkie and loses CRDT semantics. Verification: pnpm verify:fast green (748 tests). verify-self green on CI. CodeRabbit: 9 actionable + 1 outside-diff comments all addressed. Manual browser verification: thumbnail panel scroll, canvas auto-fit, drag resizer, Escape-as-discard, image load failure placeholder. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
This was referenced May 8, 2026
hackerwins
added a commit
that referenced
this pull request
May 11, 2026
Adds @wafflebase/slides as a third surface alongside Sheets and Docs, plus 53-shape library (Phase 1+2), adjustment handles for 9 pilot shapes (P3-A.1), live snap guides, align/distribute toolbar, themed authoring, and layout-change UI. Also ships Sheets cell comments (Phase B), docs peer-avatar caret jump, yorkie-js-sdk 0.7.8 upgrade, and CLI/REST API improvements (docs export imageFetcher, expired session refresh, REST docs split). Minor bump because slides is a new top-level package. Highlights: #184 #185 #186 #187 #188 #189 #190 #191 #192 #197 #198 #201 #202 #203 #204 #205 #206 #207 #209 #210
This was referenced May 13, 2026
This was referenced May 24, 2026
This was referenced Jul 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the new
@wafflebase/slidespackage — a Google Slides-stylepresentation editor that sits alongside
@wafflebase/sheetsand@wafflebase/docs. Each presentation is one Yorkie document; a slideis a free-position canvas containing text boxes, images, and shapes.
Text bodies and notes reuse the docs rich-text engine
(
computeLayout/paintLayout/initializeTextBox) so font /size / alignment / list rendering is identical between a slide
text-box and a standalone document.
The branch covers Phase 1 through Phase 5a of the MVP plan plus a
final UI-chrome refactor to bring the slides app's look-and-feel in
line with docs.
What's in this PR
Phase 1 — Foundation
packages/slidesscaffold (package.json, vite config, tsconfig, README).model/—SlidesDocument,Slide(incl. notes),Layout,Elementdiscriminated union (text/image/shape), frame math + rotation matrices with property tests.store/—SlidesStoreinterface +MemSlidesStorewithbatch()for undo/redo grouping.Phase 2 — Static rendering
view/canvas/— element-renderer (rect/ellipse/line/arrow/image/text), slide-renderer (background + element loop, dirty tracking), thumbnail (debounced re-render).Phase 3 — Editor (single user)
view/editor/— controller, selection (single + multi + lasso), drag (with snap guidelines), 8-handle resize (incl. rotated case), free + 15° snap rotate, insert via toolbar, nudge, clipboard withapplication/x-wafflebase-slides+json, z-order shortcuts, right-click context menus, speaker notes panel, undo/redo via store batches.Phase 4 — Yorkie + multi-user
packages/frontend/src/app/slides/yorkie-slides-store.ts— Yorkie ↔ Store adapter.MemSlidesStorevsYorkieSlidesStorefor identical op sequences.SlidesPresenceschema, peer cursors / selection rings (reuse sheets/docs visuals).SlidesDocumentYorkie type + frontendDocumentTypeextension +/p/:idroute.two-user-slides-yorkie.tsintegration helper + concurrent add/move/delete suite (gated byRUN_YORKIE_INTEGRATION_TESTS).Phase 5a — Text editing + CJK
paintLayoutandfindPositionAtPixelfromDocCanvasso they can be called outside a paginated full-page context. AddedinitializeTextBoxfactory that mounts a single-page rich-text editor inside a slides text-box.text-box-editor.tsmounts the docsinitializeTextBoxon double-click; commit on blur, Esc cancels.paintLayoutdirectly for committed text so editor and slide canvas produce pixel-identical output (same baseline math, same font path).drawTextso sparsestyle: {}blocks (created by slides) don'tNaNthe cumulative y incomputeLayout.Note (Phase 5a-2 carry-over): Yorkie.Tree migration for text bodies and notes was attempted and reverted — nested
Treeinside an array element gets JSON-serialised by Yorkie (no CRDT). Bodies and notes stay as plainBlock[]with last-write-wins on blur. Per-keystroke convergence will revisit this with a root-leveltextTrees: { [elementId]: Tree }map.UI chrome refactor (final 5 commits)
Aligns the slides app's visual language with docs:
SlidesEditor.getInsertMode()+onInsertModeChange()so the React toolbar reflects the editor's auto-reset tonullafter a placement.SlidesFormattingToolbar.tsx— React component on shadcnToolbar/Toggle/Tooltip+ tabler icons, replacing the raw-DOM toolbar.SlidesViewslimmed to mount canvas / overlay / thumbnail panel / notes panel only.SlidesDetailwrapped withSidebarProvider + AppSidebar + SiteHeader (with ShareDialog + UserPresence)— same chrome asDocsDetail.What's NOT in this PR (Phase 5b / 5c)
docs/tasks/active/20260507-slides-phase5b-1-image-plan.md.requestFullscreen, key nav) — not yet planned.pdf-lib+ Noto KR pipeline — not yet planned.verify:full— not yet planned.Test plan
pnpm verify:fast(already green at HEAD).🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes