Skip to content

Add @wafflebase/slides package - Phase 1-5a + UI chrome#190

Merged
hackerwins merged 95 commits into
mainfrom
feat/slides-phase1
May 7, 2026
Merged

Add @wafflebase/slides package - Phase 1-5a + UI chrome#190
hackerwins merged 95 commits into
mainfrom
feat/slides-phase1

Conversation

@hackerwins

@hackerwins hackerwins commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the new @wafflebase/slides package — a Google Slides-style
presentation editor that sits alongside @wafflebase/sheets and
@wafflebase/docs. Each presentation is one Yorkie document; a slide
is 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/slides scaffold (package.json, vite config, tsconfig, README).
  • model/SlidesDocument, Slide (incl. notes), Layout, Element discriminated union (text/image/shape), frame math + rotation matrices with property tests.
  • store/SlidesStore interface + MemSlidesStore with batch() 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).
  • Standalone HTML harness with sample fixtures.

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 with application/x-wafflebase-slides+json, z-order shortcuts, right-click context menus, speaker notes panel, undo/redo via store batches.
  • Vitest interaction tests cover drag/resize/rotate matrix, undo, clipboard.

Phase 4 — Yorkie + multi-user

  • packages/frontend/src/app/slides/yorkie-slides-store.ts — Yorkie ↔ Store adapter.
  • Equivalence tests: MemSlidesStore vs YorkieSlidesStore for identical op sequences.
  • SlidesPresence schema, peer cursors / selection rings (reuse sheets/docs visuals).
  • Backend SlidesDocument Yorkie type + frontend DocumentType extension + /p/:id route.
  • two-user-slides-yorkie.ts integration helper + concurrent add/move/delete suite (gated by RUN_YORKIE_INTEGRATION_TESTS).

Phase 5a — Text editing + CJK

  • Docs-side refactor: extracted paintLayout and findPositionAtPixel from DocCanvas so they can be called outside a paginated full-page context. Added initializeTextBox factory that mounts a single-page rich-text editor inside a slides text-box.
  • Slides-side: text-box-editor.ts mounts the docs initializeTextBox on double-click; commit on blur, Esc cancels.
  • Slide canvas calls docs paintLayout directly for committed text so editor and slide canvas produce pixel-identical output (same baseline math, same font path).
  • Block style normalisation in drawText so sparse style: {} blocks (created by slides) don't NaN the cumulative y in computeLayout.
  • DPR-aware text-box editor canvas; canvas size matches host CSS pixels times slide scale times browser DPR so text is sharp and matches the slide canvas's font sizing.

Note (Phase 5a-2 carry-over): Yorkie.Tree migration for text bodies and notes was attempted and reverted — nested Tree inside an array element gets JSON-serialised by Yorkie (no CRDT). Bodies and notes stay as plain Block[] with last-write-wins on blur. Per-keystroke convergence will revisit this with a root-level textTrees: { [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 to null after a placement.
  • SlidesFormattingToolbar.tsx — React component on shadcn Toolbar / Toggle / Tooltip + tabler icons, replacing the raw-DOM toolbar.
  • SlidesView slimmed to mount canvas / overlay / thumbnail panel / notes panel only.
  • SlidesDetail wrapped with SidebarProvider + AppSidebar + SiteHeader (with ShareDialog + UserPresence) — same chrome as DocsDetail.

What's NOT in this PR (Phase 5b / 5c)

  • 5b-1 Image input paths (toolbar / drag-drop / paste) — plan committed as docs/tasks/active/20260507-slides-phase5b-1-image-plan.md.
  • 5b-2 Presentation mode (requestFullscreen, key nav) — not yet planned.
  • 5b-3 PDF export reusing the docs pdf-lib + Noto KR pipeline — not yet planned.
  • 5c CLI commands, visual harness scenario, verify:full — not yet planned.

Test plan

  • pnpm verify:fast (already green at HEAD).
  • Open a slides document — sidebar + header + share + presence chrome matches docs.
  • Toolbar 5 toggles (rect/ellipse/line/arrow/text) — click → cursor changes → place on canvas → toggle un-presses automatically.
  • Double-click a text element → in-place edit, Korean / Latin typing works, Esc cancels, blur commits.
  • Committed text on the slide canvas matches the in-editor text in size and baseline position.
  • Drag / resize / rotate / undo / redo / copy / cut / paste / duplicate / delete still work.
  • Open the same document in two browser windows — element add / move / delete converge.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Slides presentation support with a new document type for creating and editing presentations
    • Added slides editor with drawing tools (shapes, text, lines, arrows) and a formatting toolbar
    • Added thumbnail navigation panel for slide browsing and reordering
    • Added speaker notes panel for each slide
    • Added keyboard shortcuts for common editing tasks and undo/redo support
    • Added context menus for interactive operations
    • Added image insertion capability for slides

hackerwins added 30 commits May 7, 2026 19:17
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.
hackerwins and others added 8 commits May 7, 2026 20:45
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
hackerwins merged commit eca7ba9 into main May 7, 2026
1 check passed
@hackerwins
hackerwins deleted the feat/slides-phase1 branch May 7, 2026 12:56
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]>
@hackerwins hackerwins mentioned this pull request May 11, 2026
6 tasks
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
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