Skip to content

Add themed authoring for slides (5 themes, 11 layouts, pickers)#191

Merged
hackerwins merged 22 commits into
mainfrom
slides-themed-authoring
May 8, 2026
Merged

Add themed authoring for slides (5 themes, 11 layouts, pickers)#191
hackerwins merged 22 commits into
mainfrom
slides-themed-authoring

Conversation

@hackerwins

@hackerwins hackerwins commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR1 of three for slides v0.5 themes + layouts + PPTX import (design:
docs/design/slides/slides-themes-layouts-import.md).
Lands the Themed authoring value unit.

  • 4-tier model: Theme → Master → Layout → Slide hierarchy mirroring
    OOXML / Google Slides. New types in @wafflebase/slides with
    resolveColor / resolveFont as the single seam every paint path
    goes through.
  • Hybrid ThemeColor: stored as { kind: 'role', role, tint?, shade? }
    or { kind: 'srgb', value }. Role-bound colors automatically follow
    theme switches with no model rewrite.
  • Five built-in themes: default-light, default-dark, streamline,
    focus, material. Side panel batches addTheme + applyTheme into
    one undo step.
  • Eleven Google-Slides-parity layouts: blank, title-slide,
    section-header, title-body, title-two-columns, title-only,
    one-column-text, main-point, section-title-description, caption,
    big-number.
  • Themed contextual pickers: shape Fill (Theme / Standard / Custom)
    and text Font (theme heading+body / system list). Built on Radix
    DropdownMenu so they portal to body and don't get clipped by the
    toolbar's overflow context. Look and feel matches the docs / sheets
    toolbars.
  • Read-time migrateDocument: idempotent. Legacy v1 decks gain
    themes, masters, meta.themeId='default-light', layouts; legacy
    string color fills wrap to { kind: 'srgb' }; legacy layoutId: 'title'
    remaps to 'title-slide'.
  • Visual snapshot infrastructure: 26 PNG goldens (15 theme × deck +
    11 layout) gated behind INCLUDE_VISUAL_TESTS=1 to avoid CI
    font/Cairo divergence on Linux.

Sixteen commits (eight feat, three fixup, three lessons / planning,
two refactor / picker UX). Each feat commit is a coherent unit; the
fixup commits surface bugs caught during browser smoke and the
picker-look refactor. The first feat (Theme/Master types) deliberately
ends with type errors elsewhere in the package which the second commit
closes — bisect to commit 2 if you need a green starting point.

Backward compatibility verified end-to-end: pre-PR1 decks render
byte-for-byte identical under default-light. Two-user Yorkie test
confirms applyTheme converges across attached clients.

Test plan

  • pnpm verify:fast — green (754 tests)
  • pnpm --filter @wafflebase/slides test:visual — 26 goldens match
  • Browser smoke: theme switch, theme-aware shape + text colors
    follow theme, contextual Fill / Font pickers open and apply
  • Reviewer: open a pre-PR1 deck, confirm renders identical
  • Reviewer: verify two-user theme convergence in dev
  • Picker palette polish (spacing / standard color set) is deferred
    to a follow-up — see lessons file

Follow-up backlog: docs/tasks/active/20260507-slides-themes-layouts-import-lessons.md

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added slide theme system with five built-in themes to customize deck appearance globally.
    • Added theme picker panel for selecting themes and applying them to presentations.
    • Added themed color picker for shape fills with color role and custom sRGB options.
    • Added themed font picker for text with theme and system font selections.
    • Expanded built-in slide layouts from three to eleven, improving Google Slides compatibility.
  • Documentation

    • Updated design documentation with comprehensive theme/layout/import specifications.
    • Added task planning and lessons documents for theme system rollout.
  • Tests

    • Added visual regression scenarios for slides canvas, toolbar, theme panel, and color/font pickers.

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 65a5c84b-329e-4be8-ac73-d87be82f9536

📥 Commits

Reviewing files that changed from the base of the PR and between eba9c11 and eda1978.

📒 Files selected for processing (20)
  • CLAUDE.md
  • docs/design/slides/slides-themes-layouts-import.md
  • docs/tasks/README.md
  • docs/tasks/active/20260507-slides-themes-layouts-import-todo.md
  • docs/tasks/archive/2026/05/20260507-slides-pr1-themed-authoring-todo.md
  • docs/tasks/archive/2026/05/20260508-slides-visual-harness-scenarios-todo.md
  • docs/tasks/archive/README.md
  • packages/docs/src/index.ts
  • packages/docs/src/model/color.ts
  • packages/docs/src/model/types.ts
  • packages/docs/src/view/table-renderer.ts
  • packages/docs/test/model/color.test.ts
  • packages/frontend/src/app/slides/themed-color-picker.tsx
  • packages/frontend/src/app/slides/themed-font-picker.tsx
  • packages/frontend/src/app/slides/yorkie-slides-store.ts
  • packages/frontend/src/types/slides-document.ts
  • packages/frontend/tests/app/slides/themed-color-picker.test.ts
  • packages/slides/README.md
  • packages/slides/src/model/layout.test.ts
  • packages/slides/src/store/memory.ts

📝 Walkthrough

Walkthrough

Adds theme-aware slide models, migration utilities, built-in themes/layouts, theme-resolved canvas/text rendering, Yorkie/Mem store theme ops, Theme Panel and color/font pickers, docs color model/export updates, tests, visual scenarios, and design/task docs.

Changes

Slides Themed Authoring and Rendering

Layer / File(s) Summary
Models, Layouts, Themes, Migration
packages/slides/src/model/*, packages/slides/src/index.ts, packages/slides/src/themes/*, packages/slides/demo.ts
Adds Theme/Master/Presentation types, BUILT_IN_LAYOUTS, five built-in themes/registry, and migrateDocument with tests.
Docs Color Model & Exporters
packages/docs/src/model/*, packages/docs/src/export/*, packages/docs/src/view/*, tests
Introduces StoredColor/ColorResolver; routes table/paint/DOCX/PDF colors via defaultColorResolver with tests.
Canvas Rendering
packages/slides/src/view/canvas/*
Threads SlidesDocument/Theme through renderers; resolves ThemeColor in shape/text/slide/thumbnail; adds render-context; updates tests.
Stores, Types, Backend
packages/frontend/src/app/slides/yorkie-slides-store.ts, packages/slides/src/store/*, packages/frontend/src/types/*, packages/backend/src/yorkie/docs-tree.ts
Adds addTheme/applyTheme, migrate-on-read, ThemeColor typings, and string-only inline color serialization.
Frontend UI
packages/frontend/src/app/slides/*
Adds ThemePanel/Thumbnail, ThemedColorPicker/ThemedFontPicker, selection-aware toolbar, SlidesView onStoreReady, shell wiring.
Editor Interactions & Tests
packages/slides/src/view/editor/*
Renders with doc snapshot; defaults insert to role colors; updates fixtures/tests for ThemeColor and new signatures.
Visual Harness
packages/frontend/src/app/harness/visual/*, packages/frontend/scripts/verify-visual-browser.mjs
Adds slides visual scenarios and verification wiring.
Design/Docs/Tasks
docs/design/*, docs/tasks/*, CLAUDE.md, packages/slides/README.md
Adds slides design doc/index entry; task updates; PR planning; CLAUDE workflow; expanded slides README.

Sequence Diagram(s)

sequenceDiagram
  participant Toolbar
  participant ThemePanel
  participant Store
  participant Renderer
  participant Exporters
  Toolbar->>ThemePanel: Open picker
  ThemePanel->>Store: addTheme + applyTheme (batched)
  Store-->>Renderer: read() doc with themeId
  Renderer->>Renderer: resolve ThemeColor/ThemeFont
  Renderer-->>Toolbar: canvas updated
  Store-->>Exporters: docs model with StoredColor
  Exporters->>Exporters: defaultColorResolver -> emit styles
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

I swap my coats of color bright,
From text to fills, they match the light. 🌈
I tap the theme, I hop and draw—
The canvas sings without a flaw. 🐇
With tests and docs, I take a bow—
New decks are themed and ready now. ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slides-themed-authoring

hackerwins and others added 18 commits May 8, 2026 16:59
Brings @wafflebase/slides closer to Google Slides parity in three
pillars: a Theme/Master/Layout/Slide 4-tier model with hybrid color
binding (theme role or concrete hex), eleven Google-Slides-parity
built-in layouts, and a best-effort PPTX importer.

Forcing function: a 36-slide real-world PPTX (Yorkie 캐즘 deck) that
exercises themes, masters, eleven layouts, tables, groups, and
unsupported shapes — none of which the v1 MVP supports today.

Rolled out as three user-value PR groups instead of implementation
phases, so each merged PR is something the user can notice:

  PR1  Themed authoring   — theme + 11 layouts + themed pickers
  PR2  Import existing    — PPTX upload + CLI import
  PR3  Customize          — Theme builder mode (deferrable to v1.5)

PR1 is the foundation: model widening, renderer routing through
resolvers, Yorkie schema additions, and a small additive ripple to
@wafflebase/docs (Block.style.color: string | ThemeColor). Migration
is read-time and idempotent so existing v1 decks keep their byte-for-
byte appearance under the default-light theme.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Eight-commit implementation plan that lands the first user-value
unit of the slides v0.5 work: theme switching, eleven Google-Slides-
parity layouts, and themed color/font pickers — together as one
mental model.

Includes Task 0 (visual snapshot infrastructure: node-canvas + 320×180
PNG goldens, 26 baselines after the PR), per-task TDD steps with
exact code, dependency-cycle handling for the inline-default-theme
placeholder swap, and end-of-PR verification gates.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Introduce the type vocabulary for the v0.5 theme system:
- Theme = ColorScheme (12 slots) + FontScheme (heading + body)
- Master = themed background + placeholder styles
- ThemeColor = role | srgb (hybrid binding)
- ThemeFont = role | family
- resolveColor/resolveFont with tint/shade math

Background.fill becomes ThemeColor; SlidesDocument gains themes,
masters arrays and meta.themeId/masterId.

Renderer and stores will be wired through resolvers in the next
commit; intermediate type errors are expected until then.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Every ctx.fillStyle, ctx.strokeStyle, and (text path) ctx.font goes
through resolveColor / resolveFont. ShapeElement and Background colors
widened to ThemeColor. drawSlide now takes (ctx, slide, doc, theme).

Image placeholder colors stay hard-coded — they're a fallback UI, not
themed content.

Closes the type-cascade left open by the previous commit. layout.ts
existing 3 entries are minimally patched (masterId, staticElements);
Task 7 will replace the file with the eleven-layout set. MemSlidesStore
and yorkie-slides-store gain placeholder default-light theme so
SlidesDocument is well-formed; Task 5 swaps the placeholder for the
real theme module.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Extend YorkieSlidesRoot with themes[], masters[], meta.themeId,
meta.masterId — all optional in storage, populated by ensureSlidesRoot
on first attach.

read() runs migrateDocument over the raw root so legacy documents
appear with the default-light theme attached and any string-typed
colors wrapped to { kind: 'srgb' }. Migration is idempotent and
read-time; the first write afterwards persists the new shape.

Adds addTheme(theme) + applyTheme(themeId) on SlidesStore for the
upcoming theme picker.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Inline.style.color and Inline.style.backgroundColor now accept
StoredColor = string | { kind: 'role', role, tint?, shade? } |
{ kind: 'srgb', value }. The change is strictly additive; existing
string callers in sheets and docs are unaffected.

computeLayout and paintLayout take an optional ColorResolver to
translate StoredColor at paint time. defaultColorResolver returns
strings verbatim and srgb values verbatim; role colors require the
caller (slides) to supply a theme-aware resolver.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Ship default-light, default-dark, streamline, focus, material.
default-light reproduces the v1 visual baseline byte-for-byte
(white background, near-black text, blue accent1).

Adds visual snapshot infrastructure under test-fixtures/visual/
with 15 PNG goldens (5 themes x 3 reference decks). Run
'UPDATE_SNAPSHOTS=1 pnpm slides test:visual' to regenerate when
intentionally changing renderer output.

Replaces the three inline PLACEHOLDER_DEFAULT_LIGHT literals
introduced in Tasks 2 and 3 with imports from the real theme
module.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Visual snapshot byte-compare is not cross-platform stable (node-canvas
font stack differs between macOS and Linux). The plan intended visual
tests to be an explicit gate (pnpm slides test:visual) not part of the
default verify:fast lane. Updates the test script glob accordingly.

Also:
- load-fixture.ts validates parsed shape so a malformed JSON gives a
  clear error
- visual snapshot mismatch points users at UPDATE_SNAPSHOTS=1
- CLAUDE.md notes the canvas onlyBuiltDependencies allowlist

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Right-docked panel listing the five built-in themes with live
swatches. Clicking a thumbnail batches addTheme + applyTheme so
the change is one undo step. Wired into the slides editor shell
behind a 'Theme' button on the top toolbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Expand BUILT_IN_LAYOUTS from three to eleven entries:
blank, title-slide, section-header, title-body, title-two-columns,
title-only, one-column-text, main-point, section-title-description,
caption, big-number. Each carries masterId: 'default' and an empty
staticElements array (v1.5 populates).

Existing decks using layoutId 'title' migrate to 'title-slide' on
read (already wired in Task 3). 11 visual goldens added under
test-fixtures/visual/goldens/layouts/.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Color picker top row shows the active theme's twelve swatches; click
emits { kind: 'role' }. Custom input emits { kind: 'srgb' }.
Selected role swatches carry a small marker so users can tell role-
bound colors from concrete ones at a glance.

Font picker has a 'Theme fonts' section with the heading and body
families rendered in their own typeface, plus a system font dropdown
for { kind: 'family' }.

Wired into the slides contextual toolbar:
- Fill button (visible when one shape is selected) writes data.fill
  via store.batch + updateElementData, single undo step.
- Font button (visible when one text element is selected) resolves
  the chosen ThemeFont to a family string and rewrites every inline
  run's fontFamily. The role-tracking is lost on write because docs
  InlineStyle still stores a string family — extending docs to carry
  ThemeFont is a follow-up commit, mirroring Task 6's ThemeColor
  extension.

Helper modules (.ts) own the constants, builders, and store-write
logic so node:test can exercise them — the .tsx components are
stubbed at test load by tests/resolve-hooks.mjs since
--experimental-strip-types can't parse JSX.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Nine non-blocking items surfaced during per-task code reviews:
makeColorResolver unknown-role guard, wrapLegacyColor wiring,
inline-styles → Tailwind on theme-panel, currentThemeId initial
seeding, font picker active-marker, popover keyboard accessibility,
toolbar popover duplication, visual snapshot shim extraction,
layout placeholder hint text, and the docs InlineStyle.fontFamily
extension to ThemeFont.

These belong in a follow-up cleanup commit on this branch or rolled
into PR2 (PPTX import).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Two issues surfaced in browser smoke after Task 8:

1. Insert defaults (DEFAULT_FILL, DEFAULT_STROKE_COLOR) wrote concrete
   #cccccc / #222222 hex into new shapes, so they ignored the active
   theme. Switch them to theme roles ({ kind: 'role', role: 'accent1' }
   for fill, 'text' for stroke) so newly-inserted shapes follow theme
   switches. Users can still override per-shape via the Fill picker.

2. The Fill / Font popover Toggles were disabled when nothing was
   selected, so clicking them did nothing visible. Drop the !isShape /
   !isText guards from `disabled` so the buttons are always clickable
   when theme + store are present. The popovers open and show the theme
   palette / fonts; a small hint inside the popover ("Select a shape
   to apply the fill.") explains why the pick won't apply if no element
   is selected. Apply still no-ops gracefully on no-selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Text was rendering concrete black regardless of theme switches because
the docs editor seeds every newly typed run with DEFAULT_INLINE_STYLE
which sets color to '#000000'. The slides text-renderer's resolver
returned strings verbatim, so the literal '#000000' rendered no matter
the theme.

Two changes close the gap:

1. New text boxes inserted via `buildInsertElement` now carry an
   explicit `color: { kind: 'role', role: 'text' }` on their initial
   inline run, so the role binding is the *intended* default.

2. The text-renderer's `makeColorResolver` remaps the bare '#000000'
   string to the deck's `text` role color. This covers runs that
   inherit DEFAULT_INLINE_STYLE through the docs editor's normal
   "stick to left" insertion path (which the slides text-box bridge
   has no hook to override). Users who deliberately pick black via
   the picker write `{ kind: 'srgb', value: '#000000' }` — an object,
   not a string — so their concrete intent is preserved.

A clean fix would extend `docs/InlineStyle.color` to allow
ThemeColor and have the docs editor seed `{ kind: 'role' }` runs.
That ripple is captured in the PR1 lessons file.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The shadcn Toolbar component sets `overflow-x-auto` on its root for
horizontal scroll on dense toolbars. Per the CSS spec, when one axis
is non-visible, the OTHER axis is reinterpreted as auto — so the
toolbar effectively clips both directions even though we never asked
to clip the y axis.

The contextual Fill / Font popovers (Task 8) are absolute-positioned
below their trigger buttons via `top: calc(100% + 4px)`, which puts
them entirely outside the toolbar's ~40 px height. The clipped
popovers rendered correctly into the DOM but were invisible — the
browser snipped them at the toolbar boundary.

Override on the slides toolbar usage with `!overflow-visible` to set
both axes to visible. Slides has only ~10 buttons so we don't need
horizontal scroll. Sheets / docs toolbars retain their default
clipping behaviour (they don't host overflowing popovers).

A more durable fix is to render the popovers via React portal to
document.body so no ancestor's overflow can clip them — captured in
the PR1 lessons backlog.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The Task 8 contextual pickers shipped with manual click-outside
popovers and inline styles, which felt foreign next to the docs and
sheets toolbars. Two changes bring them into the project's idiom:

1. Replace the manual `position: relative + absolute popover + global
   pointerdown` plumbing with `DropdownMenu` (Radix). Same primitive
   docs uses for its color picker. Auto-portals to body so popovers
   are no longer clipped by the toolbar's overflow context — the
   `!overflow-visible` workaround is gone, the toolbar uses the same
   default chrome as docs/sheets.

2. Rewrite `ThemedColorPicker` and `ThemedFontPicker` with Tailwind
   utility classes mirroring `ColorPickerGrid`. The color picker now
   has three sections (Theme / Standard / Custom) — theme swatches
   stay theme-bound, the Standard row reuses the docs `TEXT_COLORS`
   palette so users have a familiar set of concrete colors, and the
   Custom input shows the chosen hex value. The font picker shows
   Heading + Body buttons rendered in their own family then a list
   of system fonts each rendered in its own family, matching the
   Google Slides font picker pattern.

The `hint` prop replaces the previous "Select a shape to apply"
warning UI; the picker still renders so users can browse the theme
palette regardless of selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
DropdownMenu refactor closed the popover-clipping issue; spacing /
sizing / standard-color selection polish is captured for a later pass.
@hackerwins
hackerwins force-pushed the slides-themed-authoring branch from 75b1ab0 to 3db71fe Compare May 8, 2026 08:12
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 129.7s

Lane Status Duration
sheets:build ✅ pass 10.1s
docs:build ✅ pass 9.8s
slides:build ✅ pass 7.0s
verify:fast ✅ pass 67.9s
frontend:build ✅ pass 14.7s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 3.8s
cli:build ✅ pass 1.6s
verify:entropy ✅ pass 14.6s

Verification: verify:integration

Result: ✅ PASS

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/docs/src/model/types.ts (1)

345-346: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

inlineStylesEqual now does reference checks for object colors (false negatives).

Line 345 and Line 346 compare StoredColor values with ===. After moving to object-capable color values, two equivalent colors from different objects will be treated as unequal, which can break inline style dedupe/merge behavior.

Suggested fix
+function storedColorEqual(a: StoredColor | undefined, b: StoredColor | undefined): boolean {
+  if (a === b) return true;
+  if (!a || !b) return false;
+  if (a.kind !== b.kind) return false;
+  if (a.kind === 'srgb' && b.kind === 'srgb') return a.value === b.value;
+  if (a.kind === 'role' && b.kind === 'role') {
+    return a.role === b.role && a.tint === b.tint && a.shade === b.shade;
+  }
+  return false;
+}
+
 export function inlineStylesEqual(a: InlineStyle, b: InlineStyle): boolean {
   return (
@@
-    a.color === b.color &&
-    a.backgroundColor === b.backgroundColor &&
+    storedColorEqual(a.color, b.color) &&
+    storedColorEqual(a.backgroundColor, b.backgroundColor) &&
@@
   );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs/src/model/types.ts` around lines 345 - 346, inlineStylesEqual
currently uses reference equality for StoredColor fields (a.color === b.color
and a.backgroundColor === b.backgroundColor), causing false negatives when
equivalent color objects are distinct instances; change it to perform
value-based comparison for color and backgroundColor (add or use a helper like
colorEquals that compares the relevant color properties/representation such as
hex/rgb values or serialized form) and update inlineStylesEqual to call that
helper for both color and backgroundColor while keeping the rest of the field
comparisons unchanged.
packages/slides/src/store/memory.ts (1)

41-51: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Migrate at construction, not at every read().

read() runs migrateDocument(clone(this.doc)) on every call (line 50), but the constructor stores the input as-is: this.doc = doc ? clone(doc) : emptyDocument() (line 42). Two consequences:

  1. Internal mutations on a legacy-shaped doc crash before read() is ever called. addTheme immediately accesses this.doc.themes.find(...) on line 125 and this.doc.themes.push(...) on line 126; applyTheme dereferences this.doc.themes (line 131) and this.doc.meta (line 134). If a caller passes a pre-v0.5 document missing themes/masters/meta.themeId, both methods throw TypeError: Cannot read properties of undefined even though read() is documented to handle exactly that legacy shape.
  2. read() is a hot path. The slides view's RAF tick and store.onChange re-render route through reads; cloning + re-migrating the entire deck on each call scales with deck size and is wasted work because the result is byte-identical after the first migration.

Both go away if you migrate once at construction and let read() just clone:

♻️ Proposed change
   constructor(doc?: SlidesDocument) {
-    this.doc = doc ? clone(doc) : emptyDocument();
+    this.doc = doc ? migrateDocument(clone(doc)) : emptyDocument();
   }

   read(): SlidesDocument {
-    // Pipe through migrateDocument so a constructor-supplied legacy
-    // SlidesDocument (string-typed background fills, layoutId 'title',
-    // missing themes/masters/meta.themeId) is reconciled to the v0.5
-    // shape — keeping Mem reads aligned with YorkieSlidesStore.read().
-    return migrateDocument(clone(this.doc));
+    return clone(this.doc);
   }

Also applies to: 123-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/store/memory.ts` around lines 41 - 51, The constructor
currently stores the raw input doc but read() runs
migrateDocument(clone(this.doc)) on every call; change this so the constructor
immediately sets this.doc = migrateDocument(clone(doc || emptyDocument())) (or
migrate the cloned empty doc) and then make read() simply return clone(this.doc)
without calling migrateDocument; this ensures methods like addTheme and
applyTheme can safely access this.doc.themes / this.doc.meta and avoids repeated
clone+migration on each read() hot-path.
🧹 Nitpick comments (16)
packages/docs/test/model/color.test.ts (1)

16-20: ⚡ Quick win

Coverage gap: wrapLegacyColor only exercises the passthrough branch.

The single test only asserts string passthrough. Given the function is named wrapLegacy, the more interesting paths — what it produces for legacy-shaped inputs and idempotency on already-wrapped ThemeColor values — are not covered. Since legacy migration is one of the headline behaviors of this PR, a thin extra test or two here would meaningfully harden the contract.

🧪 Suggested additions
it('passes through an already-wrapped srgb value (idempotent)', () => {
  const wrapped = { kind: 'srgb' as const, value: '#abc' };
  expect(wrapLegacyColor(wrapped)).toEqual(wrapped);
});

it('passes through a role-bound color unchanged', () => {
  const role = { kind: 'role' as const, role: 'accent1' as const };
  expect(wrapLegacyColor(role)).toEqual(role);
});

Adjust to match the actual signature/return contract of wrapLegacyColor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs/test/model/color.test.ts` around lines 16 - 20, Add tests to
cover non-passthrough branches of wrapLegacyColor: add one test that passes an
already-wrapped ThemeColor object (e.g., { kind: 'srgb', value: '#abc' }) and
asserts wrapLegacyColor(...) returns the same object (idempotency), and another
test that passes a role-shaped color (e.g., { kind: 'role', role: 'accent1' })
and asserts it is returned unchanged; place these in the same
describe('wrapLegacyColor', ...) block in packages/docs/test/model/color.test.ts
and ensure expectations use toEqual to compare objects and match the function's
actual return shape.
packages/slides/package.json (2)

23-24: 💤 Low value

Optional: tighten the visual.test selector.

vitest run visual.test matches any file whose path contains the substring visual.test (e.g. a hypothetical visual.test.helpers.ts). If you intend to run only the two visual suites, a more specific glob like '**/*.visual.test.ts' is less prone to drift as the test tree grows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/package.json` around lines 23 - 24, The package.json visual
test scripts ("test:visual" and "test:visual:update") use the loose selector
"visual.test" which matches any path containing that substring; tighten both
script commands to use a more specific glob (for example
"'**/*.visual.test.ts'") so only intended visual suites run and avoid accidental
matches as the test tree grows.

34-34: ⚡ Quick win

Consider canvas@^3 instead of ^2.11.2 for the visual-test dependency.

canvas 3.x is the current major (latest is 3.2.3) and brings the N-API migration, fixed Node 24 crash via external memory API change, and better long-term compatibility. v2.11.2 is the last 2.x and is increasingly fragile on newer Node versions.

Since this is a devDependency for INCLUDE_VISUAL_TESTS=1, the upgrade is low-risk; the API surface used by node-canvas in tests (createCanvas, 2D context) is compatible across 2.x → 3.x. The codebase requires Node.js >=18.12.0 for canvas 3.x, which is not constrained elsewhere in the project.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/package.json` at line 34, The package.json currently pins
"canvas": "^2.11.2" for visual tests; update that dependency to a 3.x range
(e.g. "canvas": "^3") in packages/slides/package.json, then reinstall dev deps
(npm/yarn install or pnpm install) and run the visual tests with
INCLUDE_VISUAL_TESTS=1 to ensure createCanvas/2D context usage remains
compatible; also update any lockfile (package-lock.json / pnpm-lock.yaml /
yarn.lock) and CI dev matrix if it pins older Node versions to ensure Node
>=18.12.0 is used for canvas@^3.
packages/slides/src/store/memory.test.ts (1)

132-143: 💤 Low value

LGTM — solid immutability check for the new ThemeColor fill shape.

Mutating bg.fill.value after the write and asserting via the discriminated kind narrowing is exactly the right test for the deep-clone contract. One small enhancement worth considering: also reassign bg.fill = { kind: 'srgb', value: '#00ff00' } (or pass through JSON.parse(JSON.stringify(...)) round-trip) to catch a future regression where only top-level fields are cloned. Optional — the current assertion already covers the realistic caller-mutation scenario.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/store/memory.test.ts` around lines 132 - 143, Add a
stronger immutability check to the test "updateSlideBackground stores a clone,
not a reference": after calling store.updateSlideBackground(id, bg) keep the
existing mutation of bg.fill.value and also reassign bg.fill = { kind: 'srgb',
value: '#00ff00' } (or equivalent JSON round-trip) to ensure only a deep clone
of the entire fill object was stored; then read the store via
store.read().slides[0].background.fill and assert the stored fill still has kind
'srgb' and value '#ff0000'. Reference MemSlidesStore, updateSlideBackground,
read(), slides[0].background.fill and the local bg variable when making the
change.
packages/slides/src/themes/streamline.ts (1)

11-16: 💤 Low value

Consider whether the all-blue accent palette gives enough perceptual differentiation.

accent1accent6 are all sequential shades of the same blue (#1976D2#E3F2FD). For chart series, callouts, or any UI that relies on accent index to convey distinct categories, neighboring accents (e.g. accent1/accent3, accent4/accent5) will be hard to tell apart. The default-dark theme by contrast uses six visibly distinct hues.

Additionally, accent6 (#E3F2FD) on the background (#FAFAFA) has very low contrast and will read as a near-invisible fill on light surfaces — fine for tints, but worth confirming this is the intended role.

If "Streamline" is deliberately monochromatic, consider a brief code comment noting that, so future authors don't "fix" it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/themes/streamline.ts` around lines 11 - 16, The current
Streamline theme uses a sequential monochromatic blue for accent1..accent6 which
reduces perceptual differentiation and makes accent6 (`#E3F2FD`) low-contrast
against background (`#FAFAFA`); update the accent1–accent6 values in the theme
object to provide more distinct hues (or at least increase contrast for accent6)
so series/callouts are visually separable, or if the monochrome palette is
intentional add a code comment next to the theme object (e.g., near
accent1..accent6) stating that Streamline is deliberately monochromatic and
noting accent6 is a tint for subtle UI elements only. Ensure any changed colors
preserve overall theme intent and that accent names (accent1..accent6) and
background are the targeted symbols.
packages/slides/vite.config.ts (1)

26-29: ⚡ Quick win

Use configDefaults.exclude to avoid silently dropping Vitest's built-in exclusions.

Setting exclude in config replaces the default value entirely. To extend the default exclude patterns, configDefaults from vitest/config should be used. The current code drops Vitest's built-in exclusions for hidden directories (**/.{idea,git,cache,output,temp}/**) and tool config files (**/{vitest,vite,...}.config.*). While harmless for this bounded package today, it's a latent footgun and the duplication of node_modules/dist across both branches can be eliminated for free.

♻️ Proposed refactor using `configDefaults`
-import { defineConfig } from 'vitest/config';
+import { configDefaults, defineConfig } from 'vitest/config';

 ...

     exclude:
       process.env.INCLUDE_VISUAL_TESTS === '1'
-        ? ['**/node_modules/**', '**/dist/**']
-        : ['**/node_modules/**', '**/dist/**', '**/*.visual.test.ts'],
+        ? [...configDefaults.exclude]
+        : [...configDefaults.exclude, '**/*.visual.test.ts'],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/vite.config.ts` around lines 26 - 29, The Vitest config
currently sets exclude directly (based on process.env.INCLUDE_VISUAL_TESTS)
which replaces Vitest's built-in exclusions; instead import configDefaults from
'vitest/config' and set exclude = [...configDefaults.exclude,
'**/node_modules/**', '**/dist/**', ...(process.env.INCLUDE_VISUAL_TESTS === '1'
? [] : ['**/*.visual.test.ts'])] so you preserve default exclusions, remove
duplicate '**/node_modules/**' and '**/dist/**' across branches, and only add
the visual test pattern when visual tests are disabled; update the config export
where "exclude" is defined to use this merged array.
docs/tasks/active/20260507-slides-themes-layouts-import-todo.md (1)

54-58: 💤 Low value

Create the paired lessons file alongside the todo.

The coding guidelines require a YYYYMMDD-<slug>-lessons.md alongside every *-todo.md. Line 57 tracks this as a future task, but the file is expected to exist now (even if empty). Consider adding docs/tasks/active/20260507-slides-themes-layouts-import-lessons.md in this PR so the pair is complete from the start.

Based on coding guidelines: "Non-trivial tasks use paired files in docs/tasks/active/: YYYYMMDD-<slug>-todo.md and YYYYMMDD-<slug>-lessons.md."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/tasks/active/20260507-slides-themes-layouts-import-todo.md` around lines
54 - 58, Add the paired lessons file for the task by creating
docs/tasks/active/20260507-slides-themes-layouts-import-lessons.md (matching the
todo slug) even if empty; ensure the new file follows the
YYYYMMDD-<slug>-lessons.md naming convention and is committed in this PR so the
todo and lessons pair exist together as required by the coding guidelines.
packages/slides/src/view/editor/editor.ts (1)

733-755: ⚖️ Poor tradeoff

paintLive and currentSlide() each call store.read() independently from render()'s snapshot.

paintLive (line 745) and currentSlide() (line 754) each issue a fresh store.read(), while render() already has a snapshot in scope. On every mousemove during drag/resize/rotate, this results in two store reads: one for the live doc (theme colors in forceRender) and one inside currentSlide() called by repaintOverlay. Passing the already-read doc into these helpers would halve store reads on hot paths.

♻️ Sketch: thread `doc` through `paintLive` to avoid the second read
-  private paintLive(live: Map<string, Frame>): void {
-    const slide = this.currentSlide();
+  private paintLive(live: Map<string, Frame>, doc?: SlidesDocument): void {
+    const d = doc ?? this.options.store.read();
+    const id = this.currentId;
+    const slide = id ? d.slides.find((s) => s.id === id) : undefined;
     if (!slide) return;
     const synthetic = { ... };
-    this.renderer.forceRender(synthetic, this.options.store.read());
+    this.renderer.forceRender(synthetic, d);
     ...
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/view/editor/editor.ts` around lines 733 - 755, The
paintLive and currentSlide helpers each call options.store.read() causing two
reads per mousemove; change currentSlide to accept a doc parameter (e.g.,
currentSlide(doc)) and change paintLive to accept the already-read doc (e.g.,
paintLive(live, doc)), then update render to pass its snapshot/doc into
paintLive instead of letting those methods call options.store.read(); inside
paintLive use the passed doc when constructing synthetic and when selecting
elements for renderOverlay and call this.renderer.forceRender(synthetic, doc) so
forceRender and renderOverlay use the single snapshot rather than performing a
second store.read().
packages/slides/src/view/canvas/element-renderer.ts (1)

14-22: 💤 Low value

doc is on the public surface but unused; either wire it or drop it.

The JSDoc on lines 14–17 says doc is threaded in so shape/text painters can resolve theme bindings, but only theme is forwarded to drawShape/drawText; doc is silenced via void doc;. Every caller (and now every test) has to construct a SlidesDocument for an argument the implementation discards. Since theme resolution is already covered by the theme argument, consider dropping doc until the planned colorResolver wiring actually consumes it, or at least correct the JSDoc so it doesn't promise behavior the painters don't currently rely on.

Also applies to: 26-28, 59-62

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/view/canvas/element-renderer.ts` around lines 14 - 22,
The public parameter doc (SlidesDocument) is declared but unused (silenced via
`void doc;`) while only `theme` is forwarded to drawShape/drawText; either
remove `doc` from the public API and update the JSDoc, or actually thread it
through to the painters: stop silencing `doc`, pass it into `drawShape` and
`drawText` (and any other painter calls that currently receive `theme`), and
ensure the JSDoc comment reflects that painters can resolve ThemeColor/ThemeFont
via the provided `doc` and `theme`; also apply the same change pattern where
`doc` is similarly ignored (the other occurrences noted near the
drawShape/drawText usages and the onAssetLoad handling).
packages/docs/test/view/themed-color.test.ts (1)

86-92: 💤 Low value

Strengthen the fallback assertion.

expect(fillStyles.some((s) => s.startsWith('#'))).toBe(true) is satisfied as long as any hex value reached fillStyle at any point during paint — including an unrelated background or stroke color, or a default that the test setup itself wrote. If the text run silently no-ops on undefined, this check still passes. Consider asserting the specific theme default the renderer is expected to fall back to (e.g. '#000000' from defaultLight.colors.text) so a regression in the fallback path is actually caught.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs/test/view/themed-color.test.ts` around lines 86 - 92, The test
currently only checks that some fillStyle starts with '#' which can pass for
unrelated colors; update the assertion to verify the renderer fell back to the
theme default by asserting that fillStyles includes the expected theme text
color (use the known value like defaultLight.colors.text or its literal
'#000000') instead of the generic startsWith check; locate the test around
paintLayout(ctx, layout, 0, 0) and replace or add an expectation referencing
fillStyles and defaultLight.colors.text (or the literal) so the fallback from
defaultColorResolver is explicitly asserted.
packages/slides/src/view/canvas/text-renderer.ts (1)

43-55: 💤 Low value

Consider naming the '#000000' sentinel constant.

The magic literal ties this function to DEFAULT_INLINE_STYLE.color in the docs package. If that default ever changes, the remapping silently breaks. The comment is thorough, but a named constant (const DOCS_DEFAULT_TEXT_COLOR = '#000000') would make the coupling explicit and searchable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/view/canvas/text-renderer.ts` around lines 43 - 55, The
function makeColorResolver uses the magic literal '#000000' to detect the docs
package default text color; extract that string into a named constant (e.g.,
DOCS_DEFAULT_TEXT_COLOR = '#000000') and replace the inline literal in the
conditional with this constant so the coupling to DEFAULT_INLINE_STYLE.color is
explicit and searchable; update the comment to reference the new constant and
keep the behavior in makeColorResolver, which handles StoredColor/ThemeColor
resolution, unchanged otherwise.
packages/docs/src/view/table-renderer.ts (1)

171-180: 💤 Low value

Cell-level backgroundColor bypasses defaultColorResolver while inline run backgrounds do not.

Line 173 sets ctx.fillStyle = cell.style.backgroundColor directly (raw string), while lines 220–222 route the inline run's style.backgroundColor through defaultColorResolver. If the model ever evolves to allow ThemeColor-shaped objects at the cell level, this path would silently render the object's toString() representation instead of the resolved color.

This is currently safe since cell.style.backgroundColor is always a plain string, but flagging it for awareness if the model is extended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs/src/view/table-renderer.ts` around lines 171 - 180, The cell
background path in table-renderer.ts assigns ctx.fillStyle directly from
cell.style.backgroundColor which bypasses defaultColorResolver; update the cell
background branch in the renderer (the code that currently sets ctx.fillStyle =
cell.style.backgroundColor and calls ctx.fillRect) to pass
cell.style.backgroundColor through defaultColorResolver (same resolver used for
inline runs) and use the resolved value for ctx.fillStyle, guarding for
undefined/null so you only set fillStyle and call ctx.fillRect when a resolved
color string is returned.
packages/frontend/tests/app/slides/themed-color-picker.test.ts (1)

23-37: 💤 Low value

Test name claims OOXML ordering but assertions only check membership.

THEME_ROLES.includes(...) does not verify the slot order, so the "in OOXML order" part of the description is unverified. Either rename to "has all 12 ColorScheme slots" or add an order-sensitive assertion (e.g., assert.deepEqual(THEME_ROLES, ['text','background', ...])).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/frontend/tests/app/slides/themed-color-picker.test.ts` around lines
23 - 37, The test name claims OOXML ordering but currently only checks
membership; update the test for THEME_ROLES to either rename the test to "has
all 12 ColorScheme slots" or perform an order-sensitive assertion: replace the
individual includes checks with a single equality check asserting THEME_ROLES
exactly equals the expected OOXML-ordered array (e.g.,
assert.deepEqual(THEME_ROLES,
['text','background','textSecondary','backgroundAlt','accent1','accent2','accent3','accent4','accent5','accent6','hyperlink','visitedHyperlink']));
keep the length check if desired and remove the redundant includes assertions
when using deepEqual.
packages/frontend/src/app/slides/slides-formatting-toolbar.tsx (1)

256-266: 💤 Low value

Font picker value is hardcoded to undefined, so the active font is never highlighted.

The shape-fill picker reads the live value via readShapeFill and reflects the current selection in its swatch state, but the font picker passes value={undefined} regardless — so even when a text element has a uniform font (the common case after onTextFontChange runs), the picker can't show which row is "active". Cheap symmetry win with the fill picker.

A reasonable v0.5 cut is to sample the first run of the first block (matching the write path which sets every run uniformly) and pass that as value.

♻️ Sketch — derive font from first run
+  const textFontValue: ThemeFont | undefined = (() => {
+    if (!isText || !theme || !store) return undefined;
+    const first = store
+      .read()
+      .slides.find((s) => s.id === selected!.slideId)
+      ?.elements.find((e) => e.id === selected!.element.id);
+    // Read fontFamily off the first run of the first block; matches the
+    // uniform-write path in onTextFontChange.
+    const family = first && first.type === "text"
+      ? first.data.blocks[0]?.inlines[0]?.style.fontFamily
+      : undefined;
+    return family ? { kind: "family", family } : undefined;
+  })();
   ...
-          <ThemedFontPicker
-            value={undefined}
+          <ThemedFontPicker
+            value={textFontValue}
             theme={theme}
             onChange={onTextFontChange}
             hint={fontHint}
           />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/frontend/src/app/slides/slides-formatting-toolbar.tsx` around lines
256 - 266, The ThemedFontPicker is always passed value={undefined}, so the
active font never highlights; update the DropdownMenuContent usage to compute
the current font from the selected text element (same approach as your write
path): read the first block's first run font (e.g. sample
selection.blocks[0].runs[0].font or via the existing helper used when applying
fonts) and pass that string as the value prop to ThemedFontPicker (keep theme,
onTextFontChange, and hint as-is) so the picker reflects the current selection.
packages/slides/src/model/theme.ts (1)

54-57: ⚡ Quick win

parseHex silently produces NaN for 3-char shorthand hex.

parseHex('#fff') returns [255, 15, NaN] because the slices assume a 6-digit form. Any tint/shade applied to such a color cascades through Math.max(0, Math.min(255, NaN)) = NaN and emits a '#NaNNaNNaN'-shaped string, silently corrupting the paint.

Built-in theme files most likely use 6-char hex so production is unaffected today, but the type signature accepts any string and authored themes (theme builder is PR3 of this rollout) commonly use shorthand. Cheap to harden now.

♻️ Proposed handling for shorthand + invalid hex
-function parseHex(hex: string): [number, number, number] {
-  const h = hex.startsWith('#') ? hex.slice(1) : hex;
-  return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
-}
+function parseHex(hex: string): [number, number, number] {
+  const raw = hex.startsWith('#') ? hex.slice(1) : hex;
+  // Expand shorthand `abc` → `aabbcc` so theme authors can use either form.
+  const h = raw.length === 3
+    ? raw.split('').map((c) => c + c).join('')
+    : raw;
+  if (h.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(h)) {
+    throw new Error(`Invalid hex color: ${hex}`);
+  }
+  return [
+    parseInt(h.slice(0, 2), 16),
+    parseInt(h.slice(2, 4), 16),
+    parseInt(h.slice(4, 6), 16),
+  ];
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/model/theme.ts` around lines 54 - 57, parseHex currently
assumes a 6-digit hex and yields NaN for 3-char shorthand; update parseHex to
first strip a leading '#', detect 3-character shorthand (e.g. 'fff') and expand
it to 6 characters ('ffffff'), then validate the resulting string with a hex
regex /^[0-9A-Fa-f]{6}$/; if validation fails, throw a descriptive Error
including the original input so callers can surface bad theme values. Ensure the
function still returns a [number, number, number] tuple after parsing the
validated 6-char string.
packages/frontend/src/app/slides/yorkie-slides-store.ts (1)

2-17: ⚡ Quick win

Reuse the shared DEFAULT_BACKGROUND here.

This now duplicates the model default from packages/slides/src/model/presentation.ts. If the shared default changes again, Yorkie-backed new slides and memory/migrated slides will silently diverge.

♻️ Suggested change
 import {
+  DEFAULT_BACKGROUND as MODEL_DEFAULT_BACKGROUND,
   DEFAULT_MASTER,
   type Background,
   type ElementInit,
   type Frame,
   type Layout,
   type Master,
   type SlidesDocument,
   type SlidesStore,
   type Theme,
   BUILT_IN_LAYOUTS,
   defaultLight,
   generateId,
   getLayout,
   migrateDocument,
 } from '@wafflebase/slides';
@@
-const DEFAULT_BACKGROUND: YorkieSlide['background'] = {
-  fill: { kind: 'role', role: 'background' },
-};

Then use the shared constant at slide creation time:

background: clone(MODEL_DEFAULT_BACKGROUND) as YorkieSlide['background'],

Also applies to: 27-29

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/frontend/src/app/slides/yorkie-slides-store.ts` around lines 2 - 17,
The slide default background is duplicated — import and reuse the shared
DEFAULT_BACKGROUND (or MODEL_DEFAULT_BACKGROUND from the slides model) instead
of re-defining it, and when constructing Yorkie-backed slides assign a cloned
copy (e.g. clone(DEFAULT_BACKGROUND) cast to YorkieSlide['background']) wherever
the current hardcoded/default background is used (also update the other
occurrences mentioned around the slide creation code that set background).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/tasks/active/20260507-slides-pr1-themed-authoring-todo.md`:
- Around line 1-5: Create the missing paired lessons file named
20260507-slides-pr1-themed-authoring-lessons.md to accompany the todo file;
populate it with the standard lessons frontmatter (title, date, authors), a
short summary referencing the todo task, a "Lessons learned" section listing
outcomes/decisions for Theme/Master/Layout/Slide model, hybrid color binding,
built-in themes, layout parity, theme picker and pickers, plus a "Retrospective
/ Next steps" section; save, commit as a separate change in the same stacked PR,
and ensure the new file is included in the PR discussion so reviewers can
cross-check against 20260507-slides-pr1-themed-authoring-todo.md.

In `@packages/docs/src/model/color.ts`:
- Around line 14-20: The comment inside defaultColorResolver is misleading:
update the comment to state that when a StoredColor with a role (non-srgb) is
encountered the function intentionally returns undefined to signal
"unresolvable" to callers rather than providing a fallback; reference the
StoredColor type and defaultColorResolver function so callers know they must
apply their own fallback (e.g., resolve(color) ?? '#000') when undefined is
returned.

In `@packages/frontend/src/app/slides/themed-color-picker.tsx`:
- Around line 107-125: The label/input use a hardcoded id
("themed-custom-color") which causes duplicate IDs when multiple
ThemedColorPicker instances render; update the component to call React's useId()
(import useId from React) and use the generated id (e.g.,
`${id}-themed-custom-color` or similar) for both the label's htmlFor and the
input's id, keeping the rest of the logic (value, onChange, classes) unchanged
so each ThemedColorPicker instance has a unique stable id.

In `@packages/frontend/src/app/slides/themed-font-picker.tsx`:
- Around line 96-117: The list uses role="listbox" with buttons that have
role="option", which breaks AT keyboard semantics; change the System fonts
section to a plain list of buttons: remove role="listbox" from the <ul> and
remove role="option" from the <button>, add aria-pressed={selected} (matching
the Theme fonts pattern) and keep aria-label={family}, onClick={() =>
onChange(makeFamilyFont(family))}, className and style as-is; use the existing
helpers isFamilySelected, makeFamilyFont and onChange to preserve selection
behavior (alternatively implement full listbox keyboard handling if you truly
need listbox semantics).

In `@packages/frontend/src/app/slides/yorkie-slides-store.ts`:
- Around line 107-117: The code currently unconditionally sets meta.themeId =
'default-light' and meta.masterId = 'default' which can conflict with existing
rootAny.themes/rootAny.masters; update the doc.update block to backfill
meta.themeId/meta.masterId from existing arrays instead of hard-coded strings —
if rootAny.themes exists and has at least one entry use rootAny.themes[0].id (or
the first theme's id) as the default, and similarly use rootAny.masters[0].id
for meta.masterId; keep the clone(defaultLight)/clone(DEFAULT_MASTER) population
only when the arrays are absent, and reference the symbols doc.update,
meta.themeId, meta.masterId, rootAny.themes, rootAny.masters, defaultLight,
DEFAULT_MASTER, and clone to locate and change the logic.

In `@packages/frontend/src/types/slides-document.ts`:
- Around line 33-39: YorkieSlide's background.fill is currently required but can
be undefined in pre-migration documents; update the YorkieSlide interface so
background.fill is optional (e.g., make fill?: ThemeColor or ThemeColor |
undefined) to reflect pre-v0.5 data and avoid masked runtime errors, and then
adjust any callers that assume a guaranteed ThemeColor to either guard for
undefined or narrow to a post-migration type; reference the YorkieSlide type and
the background.fill property when making the change.

In `@packages/slides/src/model/layout.test.ts`:
- Around line 27-34: The test 'placeholder frames are inside the 1920×1080
canvas' currently only checks bounds and can allow negative widths/heights;
update the validation over BUILT_IN_LAYOUTS by adding explicit assertions on
each placeholder's dimensions — assert p.frame.w and p.frame.h are positive
(e.g., use toBeGreaterThan(0)) in the loop that iterates placeholders, keeping
the existing x/y and bounds checks in place.

In `@packages/slides/src/model/migrate.ts`:
- Around line 40-49: migrateSlide currently preserves slide?.id verbatim,
causing missing ids to become undefined and collide downstream; update
migrateSlide (and migrateElement) to ensure a stable id is provided when missing
by generating or deriving one (e.g., call the project's id utility or add a
generateStableId function) and assign that id instead of leaving undefined, or
alternatively throw/return an error to reject documents without ids so callers
can handle it; make sure the changes touch migrateSlide and migrateElement and
that downstream lookups like doc.slides.find((s) => s.id === slideId) will
receive unique, non-undefined ids.

---

Outside diff comments:
In `@packages/docs/src/model/types.ts`:
- Around line 345-346: inlineStylesEqual currently uses reference equality for
StoredColor fields (a.color === b.color and a.backgroundColor ===
b.backgroundColor), causing false negatives when equivalent color objects are
distinct instances; change it to perform value-based comparison for color and
backgroundColor (add or use a helper like colorEquals that compares the relevant
color properties/representation such as hex/rgb values or serialized form) and
update inlineStylesEqual to call that helper for both color and backgroundColor
while keeping the rest of the field comparisons unchanged.

In `@packages/slides/src/store/memory.ts`:
- Around line 41-51: The constructor currently stores the raw input doc but
read() runs migrateDocument(clone(this.doc)) on every call; change this so the
constructor immediately sets this.doc = migrateDocument(clone(doc ||
emptyDocument())) (or migrate the cloned empty doc) and then make read() simply
return clone(this.doc) without calling migrateDocument; this ensures methods
like addTheme and applyTheme can safely access this.doc.themes / this.doc.meta
and avoids repeated clone+migration on each read() hot-path.

---

Nitpick comments:
In `@docs/tasks/active/20260507-slides-themes-layouts-import-todo.md`:
- Around line 54-58: Add the paired lessons file for the task by creating
docs/tasks/active/20260507-slides-themes-layouts-import-lessons.md (matching the
todo slug) even if empty; ensure the new file follows the
YYYYMMDD-<slug>-lessons.md naming convention and is committed in this PR so the
todo and lessons pair exist together as required by the coding guidelines.

In `@packages/docs/src/view/table-renderer.ts`:
- Around line 171-180: The cell background path in table-renderer.ts assigns
ctx.fillStyle directly from cell.style.backgroundColor which bypasses
defaultColorResolver; update the cell background branch in the renderer (the
code that currently sets ctx.fillStyle = cell.style.backgroundColor and calls
ctx.fillRect) to pass cell.style.backgroundColor through defaultColorResolver
(same resolver used for inline runs) and use the resolved value for
ctx.fillStyle, guarding for undefined/null so you only set fillStyle and call
ctx.fillRect when a resolved color string is returned.

In `@packages/docs/test/model/color.test.ts`:
- Around line 16-20: Add tests to cover non-passthrough branches of
wrapLegacyColor: add one test that passes an already-wrapped ThemeColor object
(e.g., { kind: 'srgb', value: '#abc' }) and asserts wrapLegacyColor(...) returns
the same object (idempotency), and another test that passes a role-shaped color
(e.g., { kind: 'role', role: 'accent1' }) and asserts it is returned unchanged;
place these in the same describe('wrapLegacyColor', ...) block in
packages/docs/test/model/color.test.ts and ensure expectations use toEqual to
compare objects and match the function's actual return shape.

In `@packages/docs/test/view/themed-color.test.ts`:
- Around line 86-92: The test currently only checks that some fillStyle starts
with '#' which can pass for unrelated colors; update the assertion to verify the
renderer fell back to the theme default by asserting that fillStyles includes
the expected theme text color (use the known value like defaultLight.colors.text
or its literal '#000000') instead of the generic startsWith check; locate the
test around paintLayout(ctx, layout, 0, 0) and replace or add an expectation
referencing fillStyles and defaultLight.colors.text (or the literal) so the
fallback from defaultColorResolver is explicitly asserted.

In `@packages/frontend/src/app/slides/slides-formatting-toolbar.tsx`:
- Around line 256-266: The ThemedFontPicker is always passed value={undefined},
so the active font never highlights; update the DropdownMenuContent usage to
compute the current font from the selected text element (same approach as your
write path): read the first block's first run font (e.g. sample
selection.blocks[0].runs[0].font or via the existing helper used when applying
fonts) and pass that string as the value prop to ThemedFontPicker (keep theme,
onTextFontChange, and hint as-is) so the picker reflects the current selection.

In `@packages/frontend/src/app/slides/yorkie-slides-store.ts`:
- Around line 2-17: The slide default background is duplicated — import and
reuse the shared DEFAULT_BACKGROUND (or MODEL_DEFAULT_BACKGROUND from the slides
model) instead of re-defining it, and when constructing Yorkie-backed slides
assign a cloned copy (e.g. clone(DEFAULT_BACKGROUND) cast to
YorkieSlide['background']) wherever the current hardcoded/default background is
used (also update the other occurrences mentioned around the slide creation code
that set background).

In `@packages/frontend/tests/app/slides/themed-color-picker.test.ts`:
- Around line 23-37: The test name claims OOXML ordering but currently only
checks membership; update the test for THEME_ROLES to either rename the test to
"has all 12 ColorScheme slots" or perform an order-sensitive assertion: replace
the individual includes checks with a single equality check asserting
THEME_ROLES exactly equals the expected OOXML-ordered array (e.g.,
assert.deepEqual(THEME_ROLES,
['text','background','textSecondary','backgroundAlt','accent1','accent2','accent3','accent4','accent5','accent6','hyperlink','visitedHyperlink']));
keep the length check if desired and remove the redundant includes assertions
when using deepEqual.

In `@packages/slides/package.json`:
- Around line 23-24: The package.json visual test scripts ("test:visual" and
"test:visual:update") use the loose selector "visual.test" which matches any
path containing that substring; tighten both script commands to use a more
specific glob (for example "'**/*.visual.test.ts'") so only intended visual
suites run and avoid accidental matches as the test tree grows.
- Line 34: The package.json currently pins "canvas": "^2.11.2" for visual tests;
update that dependency to a 3.x range (e.g. "canvas": "^3") in
packages/slides/package.json, then reinstall dev deps (npm/yarn install or pnpm
install) and run the visual tests with INCLUDE_VISUAL_TESTS=1 to ensure
createCanvas/2D context usage remains compatible; also update any lockfile
(package-lock.json / pnpm-lock.yaml / yarn.lock) and CI dev matrix if it pins
older Node versions to ensure Node >=18.12.0 is used for canvas@^3.

In `@packages/slides/src/model/theme.ts`:
- Around line 54-57: parseHex currently assumes a 6-digit hex and yields NaN for
3-char shorthand; update parseHex to first strip a leading '#', detect
3-character shorthand (e.g. 'fff') and expand it to 6 characters ('ffffff'),
then validate the resulting string with a hex regex /^[0-9A-Fa-f]{6}$/; if
validation fails, throw a descriptive Error including the original input so
callers can surface bad theme values. Ensure the function still returns a
[number, number, number] tuple after parsing the validated 6-char string.

In `@packages/slides/src/store/memory.test.ts`:
- Around line 132-143: Add a stronger immutability check to the test
"updateSlideBackground stores a clone, not a reference": after calling
store.updateSlideBackground(id, bg) keep the existing mutation of bg.fill.value
and also reassign bg.fill = { kind: 'srgb', value: '#00ff00' } (or equivalent
JSON round-trip) to ensure only a deep clone of the entire fill object was
stored; then read the store via store.read().slides[0].background.fill and
assert the stored fill still has kind 'srgb' and value '#ff0000'. Reference
MemSlidesStore, updateSlideBackground, read(), slides[0].background.fill and the
local bg variable when making the change.

In `@packages/slides/src/themes/streamline.ts`:
- Around line 11-16: The current Streamline theme uses a sequential
monochromatic blue for accent1..accent6 which reduces perceptual differentiation
and makes accent6 (`#E3F2FD`) low-contrast against background (`#FAFAFA`); update
the accent1–accent6 values in the theme object to provide more distinct hues (or
at least increase contrast for accent6) so series/callouts are visually
separable, or if the monochrome palette is intentional add a code comment next
to the theme object (e.g., near accent1..accent6) stating that Streamline is
deliberately monochromatic and noting accent6 is a tint for subtle UI elements
only. Ensure any changed colors preserve overall theme intent and that accent
names (accent1..accent6) and background are the targeted symbols.

In `@packages/slides/src/view/canvas/element-renderer.ts`:
- Around line 14-22: The public parameter doc (SlidesDocument) is declared but
unused (silenced via `void doc;`) while only `theme` is forwarded to
drawShape/drawText; either remove `doc` from the public API and update the
JSDoc, or actually thread it through to the painters: stop silencing `doc`, pass
it into `drawShape` and `drawText` (and any other painter calls that currently
receive `theme`), and ensure the JSDoc comment reflects that painters can
resolve ThemeColor/ThemeFont via the provided `doc` and `theme`; also apply the
same change pattern where `doc` is similarly ignored (the other occurrences
noted near the drawShape/drawText usages and the onAssetLoad handling).

In `@packages/slides/src/view/canvas/text-renderer.ts`:
- Around line 43-55: The function makeColorResolver uses the magic literal
'#000000' to detect the docs package default text color; extract that string
into a named constant (e.g., DOCS_DEFAULT_TEXT_COLOR = '#000000') and replace
the inline literal in the conditional with this constant so the coupling to
DEFAULT_INLINE_STYLE.color is explicit and searchable; update the comment to
reference the new constant and keep the behavior in makeColorResolver, which
handles StoredColor/ThemeColor resolution, unchanged otherwise.

In `@packages/slides/src/view/editor/editor.ts`:
- Around line 733-755: The paintLive and currentSlide helpers each call
options.store.read() causing two reads per mousemove; change currentSlide to
accept a doc parameter (e.g., currentSlide(doc)) and change paintLive to accept
the already-read doc (e.g., paintLive(live, doc)), then update render to pass
its snapshot/doc into paintLive instead of letting those methods call
options.store.read(); inside paintLive use the passed doc when constructing
synthetic and when selecting elements for renderOverlay and call
this.renderer.forceRender(synthetic, doc) so forceRender and renderOverlay use
the single snapshot rather than performing a second store.read().

In `@packages/slides/vite.config.ts`:
- Around line 26-29: The Vitest config currently sets exclude directly (based on
process.env.INCLUDE_VISUAL_TESTS) which replaces Vitest's built-in exclusions;
instead import configDefaults from 'vitest/config' and set exclude =
[...configDefaults.exclude, '**/node_modules/**', '**/dist/**',
...(process.env.INCLUDE_VISUAL_TESTS === '1' ? [] : ['**/*.visual.test.ts'])] so
you preserve default exclusions, remove duplicate '**/node_modules/**' and
'**/dist/**' across branches, and only add the visual test pattern when visual
tests are disabled; update the config export where "exclude" is defined to use
this merged array.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f18a404d-b411-42d5-bf13-d1f16698d2ea

📥 Commits

Reviewing files that changed from the base of the PR and between 04e7750 and 3db71fe.

⛔ Files ignored due to path filters (27)
  • packages/slides/test-fixtures/visual/goldens/default-dark__empty.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/default-dark__three-slides.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/default-dark__title-only.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/default-light__empty.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/default-light__three-slides.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/default-light__title-only.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/focus__empty.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/focus__three-slides.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/focus__title-only.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/big-number.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/blank.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/caption.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/main-point.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/one-column-text.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/section-header.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/section-title-description.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/title-body.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/title-only.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/title-slide.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/layouts/title-two-columns.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/material__empty.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/material__three-slides.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/material__title-only.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/streamline__empty.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/streamline__three-slides.png is excluded by !**/*.png
  • packages/slides/test-fixtures/visual/goldens/streamline__title-only.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (91)
  • CLAUDE.md
  • docs/design/README.md
  • docs/design/slides/slides-themes-layouts-import.md
  • docs/tasks/README.md
  • docs/tasks/active/20260507-slides-pr1-themed-authoring-todo.md
  • docs/tasks/active/20260507-slides-themes-layouts-import-lessons.md
  • docs/tasks/active/20260507-slides-themes-layouts-import-todo.md
  • package.json
  • packages/backend/src/yorkie/docs-tree.ts
  • packages/docs/src/export/docx-style-map.ts
  • packages/docs/src/export/pdf-style-map.ts
  • packages/docs/src/index.ts
  • packages/docs/src/model/color.ts
  • packages/docs/src/model/types.ts
  • packages/docs/src/view/paint-layout.ts
  • packages/docs/src/view/table-renderer.ts
  • packages/docs/test/model/color.test.ts
  • packages/docs/test/view/themed-color.test.ts
  • packages/frontend/src/app/docs/yorkie-doc-store.ts
  • packages/frontend/src/app/slides/slides-detail.tsx
  • packages/frontend/src/app/slides/slides-formatting-toolbar.tsx
  • packages/frontend/src/app/slides/slides-view.tsx
  • packages/frontend/src/app/slides/theme-panel-helpers.ts
  • packages/frontend/src/app/slides/theme-panel.tsx
  • packages/frontend/src/app/slides/theme-thumbnail.tsx
  • packages/frontend/src/app/slides/themed-color-picker-helpers.ts
  • packages/frontend/src/app/slides/themed-color-picker.tsx
  • packages/frontend/src/app/slides/themed-font-picker-helpers.ts
  • packages/frontend/src/app/slides/themed-font-picker.tsx
  • packages/frontend/src/app/slides/yorkie-slides-store.ts
  • packages/frontend/src/types/slides-document.ts
  • packages/frontend/tests/app/slides/theme-panel.test.ts
  • packages/frontend/tests/app/slides/themed-color-picker.test.ts
  • packages/frontend/tests/app/slides/themed-font-picker.test.ts
  • packages/frontend/tests/app/slides/two-user-slides-yorkie.test.ts
  • packages/frontend/tests/app/slides/yorkie-slides-equivalence.test.ts
  • packages/slides/demo.ts
  • packages/slides/package.json
  • packages/slides/src/index.ts
  • packages/slides/src/model/element.ts
  • packages/slides/src/model/layout.test.ts
  • packages/slides/src/model/layout.ts
  • packages/slides/src/model/layouts.visual.test.ts
  • packages/slides/src/model/master.test.ts
  • packages/slides/src/model/master.ts
  • packages/slides/src/model/migrate.test.ts
  • packages/slides/src/model/migrate.ts
  • packages/slides/src/model/presentation.ts
  • packages/slides/src/model/theme.test.ts
  • packages/slides/src/model/theme.ts
  • packages/slides/src/store/memory.test.ts
  • packages/slides/src/store/memory.ts
  • packages/slides/src/store/store.ts
  • packages/slides/src/test-utils/load-fixture.ts
  • packages/slides/src/test-utils/render-snapshot.ts
  • packages/slides/src/themes/default-dark.ts
  • packages/slides/src/themes/default-light.ts
  • packages/slides/src/themes/focus.ts
  • packages/slides/src/themes/index.ts
  • packages/slides/src/themes/material.ts
  • packages/slides/src/themes/streamline.ts
  • packages/slides/src/themes/themes.test.ts
  • packages/slides/src/themes/themes.visual.test.ts
  • packages/slides/src/view/canvas/element-renderer.test.ts
  • packages/slides/src/view/canvas/element-renderer.ts
  • packages/slides/src/view/canvas/image-renderer.ts
  • packages/slides/src/view/canvas/render-context.ts
  • packages/slides/src/view/canvas/shape-renderer.test.ts
  • packages/slides/src/view/canvas/shape-renderer.ts
  • packages/slides/src/view/canvas/slide-renderer.test.ts
  • packages/slides/src/view/canvas/slide-renderer.ts
  • packages/slides/src/view/canvas/text-renderer.test.ts
  • packages/slides/src/view/canvas/text-renderer.ts
  • packages/slides/src/view/canvas/thumbnail.test.ts
  • packages/slides/src/view/canvas/thumbnail.ts
  • packages/slides/src/view/editor/editor.test.ts
  • packages/slides/src/view/editor/editor.ts
  • packages/slides/src/view/editor/interactions/clipboard.test.ts
  • packages/slides/src/view/editor/interactions/drag.test.ts
  • packages/slides/src/view/editor/interactions/insert.test.ts
  • packages/slides/src/view/editor/interactions/insert.ts
  • packages/slides/src/view/editor/interactions/keyboard.test.ts
  • packages/slides/src/view/editor/interactions/lasso.test.ts
  • packages/slides/src/view/editor/interactions/select.test.ts
  • packages/slides/src/view/editor/overlay.test.ts
  • packages/slides/src/view/editor/text-box-editor.test.ts
  • packages/slides/src/view/editor/thumbnail-panel.ts
  • packages/slides/test-fixtures/decks/empty.json
  • packages/slides/test-fixtures/decks/three-slides.json
  • packages/slides/test-fixtures/decks/title-only.json
  • packages/slides/vite.config.ts

Comment on lines +1 to +5
# Slides PR1 — Themed Authoring Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Land PR1 of the slides themes/layouts work — Theme/Master/Layout/Slide 4-tier model, hybrid color binding, five built-in themes, eleven Google-Slides-parity layouts, theme picker side panel, themed color and font pickers — as eight commits in one stacked PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for the paired lessons file alongside the todo.md
fd --type f '20260507-slides-pr1-themed-authoring-lessons.md' docs/tasks/

Repository: wafflebase/wafflebase

Length of output: 47


Create the paired lessons file.

Per coding guidelines, non-trivial tasks in docs/tasks/active/ require both YYYYMMDD-<slug>-todo.md and YYYYMMDD-<slug>-lessons.md. The paired file 20260507-slides-pr1-themed-authoring-lessons.md is missing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/tasks/active/20260507-slides-pr1-themed-authoring-todo.md` around lines
1 - 5, Create the missing paired lessons file named
20260507-slides-pr1-themed-authoring-lessons.md to accompany the todo file;
populate it with the standard lessons frontmatter (title, date, authors), a
short summary referencing the todo task, a "Lessons learned" section listing
outcomes/decisions for Theme/Master/Layout/Slide model, hybrid color binding,
built-in themes, layout parity, theme picker and pickers, plus a "Retrospective
/ Next steps" section; save, commit as a separate change in the same stacked PR,
and ensure the new file is included in the PR discussion so reviewers can
cross-check against 20260507-slides-pr1-themed-authoring-todo.md.

Comment thread packages/docs/src/model/color.ts
Comment thread packages/frontend/src/app/slides/themed-color-picker.tsx
Comment thread packages/frontend/src/app/slides/themed-font-picker.tsx Outdated
Comment thread packages/frontend/src/app/slides/yorkie-slides-store.ts
Comment thread packages/frontend/src/types/slides-document.ts
Comment thread packages/slides/src/model/layout.test.ts Outdated
Comment on lines +40 to +49
function migrateSlide(slide: any): any {
const layoutId = LAYOUT_ID_MIGRATIONS[slide?.layoutId] ?? slide?.layoutId ?? 'blank';
return {
id: slide?.id,
layoutId,
background: migrateBackground(slide?.background ?? { fill: '#ffffff' }),
elements: Array.isArray(slide?.elements) ? slide.elements.map(migrateElement) : [],
notes: slide?.notes ?? [],
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Slides without an id are silently migrated to id: undefined.

migrateSlide preserves slide?.id verbatim, so a legacy slide missing id ends up with id: undefined in the migrated document. Downstream code paths like doc.slides.find((s) => s.id === slideId) will conflate every such slide, and selection / editor lookups by id will fail or collide if more than one slide is in this state. Same caveat applies to migrateElement (line 59-67) which preserves el as-is — element ids are not validated either.

Consider either generating a stable id when one is missing or rejecting the document so the caller can decide.

🛡️ Possible defensive fix
 function migrateSlide(slide: any): any {
   const layoutId = LAYOUT_ID_MIGRATIONS[slide?.layoutId] ?? slide?.layoutId ?? 'blank';
   return {
-    id: slide?.id,
+    id: typeof slide?.id === 'string' && slide.id.length > 0
+      ? slide.id
+      : `slide-${Math.random().toString(36).slice(2, 10)}`,
     layoutId,
     background: migrateBackground(slide?.background ?? { fill: '#ffffff' }),
     elements: Array.isArray(slide?.elements) ? slide.elements.map(migrateElement) : [],
     notes: slide?.notes ?? [],
   };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/slides/src/model/migrate.ts` around lines 40 - 49, migrateSlide
currently preserves slide?.id verbatim, causing missing ids to become undefined
and collide downstream; update migrateSlide (and migrateElement) to ensure a
stable id is provided when missing by generating or deriving one (e.g., call the
project's id utility or add a generateStableId function) and assign that id
instead of leaving undefined, or alternatively throw/return an error to reject
documents without ids so callers can handle it; make sure the changes touch
migrateSlide and migrateElement and that downstream lookups like
doc.slides.find((s) => s.id === slideId) will receive unique, non-undefined ids.

PR1 originally added a node-canvas golden lane in the slides package
(26 PNGs + canvas devDep + INCLUDE_VISUAL_TESTS env split). That
duplicated the work the project's harness visual lane already does
through Docker, and forced the slides package to carry a native
dependency just for unit-level renderer snapshots.

Drop the goldens entirely and surface the renderer through the
existing harness instead:

- Add 11 slides scenarios in
  packages/frontend/src/app/harness/visual/slides-scenarios.tsx:
  five themes × shared composition, three structurally diverse
  layouts under default-light, and three UI surfaces (toolbar,
  theme panel, contextual color + font pickers). Mounted on a
  MemSlidesStore where the component needs one.
- Wire the section into the visual harness page tabs.
- Register all eleven scenario ids in verify-visual-browser.mjs so
  pnpm verify:browser:docker captures + diffs them across the four
  desktop / mobile × light / dark profiles.
- Generate 44 new slides baselines under
  packages/frontend/tests/visual/baselines/.
- Delete the slides goldens infrastructure: themes.visual.test.ts,
  layouts.visual.test.ts, src/test-utils, test-fixtures, and the 26
  PNG goldens.
- Remove the canvas devDep from packages/slides/package.json, drop
  the pnpm.onlyBuiltDependencies allowlist from root package.json,
  remove the INCLUDE_VISUAL_TESTS env-var gate from
  packages/slides/vite.config.ts, drop the test:visual /
  test:visual:update scripts, and remove the related CLAUDE.md
  pitfall.
- Update the design doc and lessons to reflect a single visual
  regression lane (harness via Docker) instead of two.

Net: -26 PNG goldens + 5 infra files removed; +44 PNG baselines for
the new harness scenarios; +1 scenarios file. Slides now has zero
native dependencies and relies on the same Docker-based visual
regression suite as the rest of the frontend.

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

codecov Bot commented May 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.19753% with 29 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/docs/src/view/paint-layout.ts 31.03% 20 Missing ⚠️
packages/docs/src/view/table-renderer.ts 66.66% 3 Missing and 1 partial ⚠️
packages/docs/src/model/color.ts 86.95% 3 Missing ⚠️
packages/docs/src/export/docx-style-map.ts 71.42% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/frontend/src/app/harness/visual/slides-scenarios.tsx (1)

305-319: ⚡ Quick win

data-visual-scenario-ready="true" is set before the canvas is painted

The data-visual-scenario-ready attribute is hardcoded unconditionally on the Card element, so it is present in the DOM from the very first React commit — before the SlideCanvas useEffect has run and before SlideRenderer.render() has drawn anything to the canvas.

captureScreenshots in the script resolves per-target via locator.waitFor({ state: "visible" }) (script line 221), which unblocks as soon as the card element is visible — not after the canvas effect fires. On a slow CI runner or a loaded headless browser process, load states only reflect document and network activity; they do not guarantee that dynamic components or client-side rendering are complete, making this a real flakiness risk for the five slides-canvas-* scenarios.

A robust fix is to have SlideCanvas set a data-canvas-ready attribute after the effect completes, and have the harness script wait for it:

💡 Proposed fix

In SlideCanvas:

 function SlideCanvas({ doc, width = 480, height = 270 }: ...) {
   const ref = useRef<HTMLCanvasElement | null>(null);
+  const [ready, setReady] = useState(false);

   useEffect(() => {
     const canvas = ref.current;
     if (!canvas) return;
     canvas.width = width;
     canvas.height = height;
     const ctx = canvas.getContext("2d");
     if (!ctx) return;
     const renderer = new SlideRenderer(ctx, { hostWidth: width, hostHeight: height, dpr: 1 });
     renderer.render(doc.slides[0], doc);
+    setReady(true);
   }, [doc, width, height]);

   return (
     <canvas
       ref={ref}
+      data-canvas-ready={ready ? "true" : undefined}
       className="rounded-md border bg-background"
       style={{ width, height }}
     />
   );
 }

In SlidesScenarioCard, replace the hardcoded attribute with one derived from the scenario type:

 <Card
   data-visual-scenario-id={scenario.id}
-  data-visual-scenario-ready="true"
-  data-visual-scenario-state="ready"
+  data-visual-scenario-ready={scenario.id.startsWith("slides-canvas-") ? undefined : "true"}

And in verify-visual-browser.mjs, add a canvas-specific wait after the section wait:

// After the section is visible, wait for each canvas scenario to be painted:
for (const canvasId of scenarioIds.filter((id) => id.startsWith("slides-canvas-"))) {
  await page
    .locator(`[data-visual-scenario-id='${canvasId}'] canvas[data-canvas-ready='true']`)
    .waitFor({ state: "attached", timeout: 10000 });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/frontend/src/app/harness/visual/slides-scenarios.tsx` around lines
305 - 319, The Card currently hardcodes data-visual-scenario-ready="true" which
causes the harness to believe canvas scenarios are ready before SlideCanvas's
effect finishes; remove the unconditional attribute from SlidesScenarioCard and
instead have SlideCanvas set a data-canvas-ready="true" attribute on its
rendered <canvas> after SlideRenderer.render() completes (e.g., in the useEffect
callback). Keep any non-canvas scenarios using a readiness attribute if needed
(use scenario.type or scenario.id to discriminate) and update the visual test
harness (verify-visual-browser.mjs) to, after the section becomes visible, wait
specifically for canvas scenarios by awaiting
page.locator(`[data-visual-scenario-id='${id}']
canvas[data-canvas-ready='true']`). Ensure you reference SlidesScenarioCard and
SlideCanvas when making these changes.
packages/frontend/scripts/verify-visual-browser.mjs (1)

195-226: ⚡ Quick win

Missing section-level readiness wait for slides — inconsistent with other sections

The harness explicitly waits for sheet, format, and chart sections with data-visual-*-ready guards (lines 204–217) before starting per-target captures. There is no corresponding wait for the slides section. While the static data-visual-slides-ready="true" on the section element would resolve immediately and isn't a useful synchronization point on its own, the gap in the pattern means the canvas scenarios rely entirely on networkidle + document.fonts.ready for timing — which, as noted above in slides-scenarios.tsx, does not guarantee useEffect completion on the canvas.

The concrete fix for canvas readiness belongs in slides-scenarios.tsx (adding a data-canvas-ready signal), but this script should also add a wait to consume it before the per-target capture loop starts:

💡 Suggested addition after line 217
const slidesSection = page.locator(
  "[data-testid='visual-harness-slides-section'][data-visual-slides-ready='true']",
);
await slidesSection.waitFor({ state: "visible", timeout: 20000 });

// Wait for all canvas scenarios to finish rendering before capture.
for (const id of scenarioIds.filter((id) => id.startsWith("slides-canvas-"))) {
  await page
    .locator(`[data-visual-scenario-id='${id}'] canvas[data-canvas-ready='true']`)
    .waitFor({ state: "attached", timeout: 10000 });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/frontend/scripts/verify-visual-browser.mjs` around lines 195 - 226,
Add a slides-section readiness wait and consume per-canvas readiness before the
per-target capture loop: insert a wait for the locator
"[data-testid='visual-harness-slides-section'][data-visual-slides-ready='true']"
(e.g. as slidesSection.waitFor(...)) and then, for each id in scenarioIds that
startsWith "slides-canvas-", wait on the page locator
`[data-visual-scenario-id='${id}'] canvas[data-canvas-ready='true']` to be
attached/visible with a reasonable timeout; place this logic immediately before
the for (const target of visualTargets) loop so captures
(captures.set(captureKey(profile.id, target.id), ...)) only run after canvas
scenarios are signalled ready.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/frontend/src/app/harness/visual/slides-scenarios.tsx`:
- Around line 106-111: The current map casts the created objects to Element[]
with "as Element[]" which can silence mismatched discriminated-union errors;
update the mapping to use a type-safe assertion (e.g., use TypeScript's
"satisfies Element[]" or explicitly type the mapped result) so the compiler
validates each object against the Element union instead of bypassing
checks—change the creation of elements (the const elements =
layout.placeholders.map(...) code) to produce items that satisfy Element[]
rather than casting with "as Element[]".

---

Nitpick comments:
In `@packages/frontend/scripts/verify-visual-browser.mjs`:
- Around line 195-226: Add a slides-section readiness wait and consume
per-canvas readiness before the per-target capture loop: insert a wait for the
locator
"[data-testid='visual-harness-slides-section'][data-visual-slides-ready='true']"
(e.g. as slidesSection.waitFor(...)) and then, for each id in scenarioIds that
startsWith "slides-canvas-", wait on the page locator
`[data-visual-scenario-id='${id}'] canvas[data-canvas-ready='true']` to be
attached/visible with a reasonable timeout; place this logic immediately before
the for (const target of visualTargets) loop so captures
(captures.set(captureKey(profile.id, target.id), ...)) only run after canvas
scenarios are signalled ready.

In `@packages/frontend/src/app/harness/visual/slides-scenarios.tsx`:
- Around line 305-319: The Card currently hardcodes
data-visual-scenario-ready="true" which causes the harness to believe canvas
scenarios are ready before SlideCanvas's effect finishes; remove the
unconditional attribute from SlidesScenarioCard and instead have SlideCanvas set
a data-canvas-ready="true" attribute on its rendered <canvas> after
SlideRenderer.render() completes (e.g., in the useEffect callback). Keep any
non-canvas scenarios using a readiness attribute if needed (use scenario.type or
scenario.id to discriminate) and update the visual test harness
(verify-visual-browser.mjs) to, after the section becomes visible, wait
specifically for canvas scenarios by awaiting
page.locator(`[data-visual-scenario-id='${id}']
canvas[data-canvas-ready='true']`). Ensure you reference SlidesScenarioCard and
SlideCanvas when making these changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bddf33c3-4f0b-444f-b4c4-4f8db059745f

📥 Commits

Reviewing files that changed from the base of the PR and between 3db71fe and eba9c11.

⛔ Files ignored due to path filters (48)
  • packages/frontend/tests/visual/baselines/harness-visual.browser.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-dark.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-dark.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-dark.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-light.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-light.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-light.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-light.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-focus.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-focus.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-focus.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-focus.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-big-number.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-big-number.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-big-number.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-big-number.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-section-header.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-section-header.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-section-header.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-section-header.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-title-body.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-title-body.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-title-body.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-title-body.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-material.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-material.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-material.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-material.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-streamline.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-streamline.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-streamline.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-streamline.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-pickers.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-pickers.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-pickers.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-pickers.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-theme-panel.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-theme-panel.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-theme-panel.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-theme-panel.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.desktop.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.mobile.dark.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.mobile.png is excluded by !**/*.png
  • packages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.png is excluded by !**/*.png
📒 Files selected for processing (6)
  • docs/design/slides/slides-themes-layouts-import.md
  • docs/tasks/active/20260507-slides-themes-layouts-import-lessons.md
  • docs/tasks/active/20260508-slides-visual-harness-scenarios-todo.md
  • packages/frontend/scripts/verify-visual-browser.mjs
  • packages/frontend/src/app/harness/visual/page.tsx
  • packages/frontend/src/app/harness/visual/slides-scenarios.tsx
✅ Files skipped from review due to trivial changes (3)
  • docs/tasks/active/20260508-slides-visual-harness-scenarios-todo.md
  • docs/tasks/active/20260507-slides-themes-layouts-import-lessons.md
  • docs/design/slides/slides-themes-layouts-import.md

Comment on lines +106 to +111
const elements: Element[] = layout.placeholders.map((p, i) => ({
id: `e${i}`,
type: p.type,
frame: p.frame,
data: p.data,
})) as Element[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

as Element[] cast may silently bypass the discriminated union

layout.placeholders.map(...) returns objects shaped as { id, type, frame, data }, but Element is presumably a discriminated union where each type variant expects a specific data shape. The as Element[] cast suppresses any TypeScript error here, meaning mismatched placeholder data would only surface at runtime inside renderer.render() (e.g., a missing field, wrong union arm).

🛡️ Safer alternative
-  const elements: Element[] = layout.placeholders.map((p, i) => ({
-    id: `e${i}`,
-    type: p.type,
-    frame: p.frame,
-    data: p.data,
-  })) as Element[];
+  const elements = layout.placeholders.map((p, i) => ({
+    id: `e${i}`,
+    type: p.type,
+    frame: p.frame,
+    data: p.data,
+  })) satisfies Element[];

Using satisfies instead of as will catch type mismatches at compile-time rather than silently swallowing them. If Placeholder and Element are structurally compatible, the change is zero-cost.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const elements: Element[] = layout.placeholders.map((p, i) => ({
id: `e${i}`,
type: p.type,
frame: p.frame,
data: p.data,
})) as Element[];
const elements = layout.placeholders.map((p, i) => ({
id: `e${i}`,
type: p.type,
frame: p.frame,
data: p.data,
})) satisfies Element[];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/frontend/src/app/harness/visual/slides-scenarios.tsx` around lines
106 - 111, The current map casts the created objects to Element[] with "as
Element[]" which can silence mismatched discriminated-union errors; update the
mapping to use a type-safe assertion (e.g., use TypeScript's "satisfies
Element[]" or explicitly type the mapped result) so the compiler validates each
object against the Element union instead of bypassing checks—change the creation
of elements (the const elements = layout.placeholders.map(...) code) to produce
items that satisfy Element[] rather than casting with "as Element[]".

hackerwins and others added 3 commits May 8, 2026 19:35
Apply 8 of 10 actionable findings from CodeRabbit's pass:

Major:
- inlineStylesEqual now value-compares StoredColor via storedColorsEqual
  (packages/docs/src/model/types.ts, color.ts). Previously two equivalent
  color objects from different references — common after Yorkie sync or
  migrate — were treated as unequal, breaking inline merge / dedupe in
  document.ts:864, clipboard.ts:116, and block-helpers.ts:75.
- MemSlidesStore now migrates once at construction instead of on every
  read(): fixes a TypeError in addTheme/applyTheme on legacy-shaped input
  and removes wasted clone+migrate work on the slides view RAF tick.
- Theme / master backfill in yorkie-slides-store reuses the document's
  own first entry instead of pinning 'default-light' / 'default'. A
  customized doc whose themes array doesn't include 'default-light' no
  longer ends up with a meta.themeId that getActiveTheme can't resolve.
- Font picker drops the broken role="listbox" + role="option" pattern
  in favor of plain buttons with aria-pressed (matching the Theme fonts
  section). Listbox semantics declare arrow-key nav as the AT contract,
  but the Tab/Enter buttons there don't honor it.

Minor:
- defaultColorResolver comment now says "return undefined so the caller
  supplies a fallback" instead of the misleading "fall back to a
  sensible default."
- ThemedColorPicker uses useId() for the custom-color input/label so
  two pickers in the DOM (e.g. the harness 'slides-pickers' scenario)
  don't collide on a static id.
- YorkieSlide.background.fill is optional to reflect pre-migration
  Yorkie state where the field may be absent.
- layout.test.ts now asserts placeholder w/h are positive, not just
  bounded.
- table-renderer cell backgroundColor goes through defaultColorResolver
  instead of bypassing it (consistent with inline runs).
- yorkie-slides-store reuses DEFAULT_BACKGROUND from the slides model
  instead of redefining it.
- themed-color-picker.test.ts asserts THEME_ROLES order (was named
  "OOXML order" but only checked membership).

Add:
- storedColorsEqual exported from @wafflebase/docs for reuse.
- 10 unit tests for storedColorsEqual + wrapLegacyColor coverage gaps.

Skip with reason:
- "Missing paired lessons file for pr1-themed-authoring-todo.md": the
  master plan's lessons (20260507-slides-themes-layouts-import-lessons.md)
  already covers PR1; the per-PR plan is a sub-document of that master.
- "Slides without id → undefined in migrate.ts": legacy slides always
  carry an id per pre-PR1 schema; the defensive id-generation would be
  YAGNI.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
CLAUDE.md changes:
- Task Workflow now describes the full pipeline from todo file to merged
  PR (six steps: Plan → Branch+commit → Self review → Sync+open PR →
  Address review → Before merge). The previous version only covered
  cleanup ("before marking done"); the actual flow was reinvented each
  task and learned the hard way during PR1 (skipped self-review,
  archive-after-merge, squash-merge SHA collision, etc.). The new step
  3 explicitly dispatches a code review skill on the full branch diff
  before pushing — catching the same kind of findings external review
  would, but on the author's clock. Step 6 moves lessons + archive
  into the PR itself instead of a follow-up commit on main.
- Drops the standalone "Design Docs" section (one line absorbed into
  the Task Workflow header).
- Project intro adds Slides alongside Sheets and Docs, and references
  packages/slides/README.md.

packages/slides/README.md:
- Expands from a 17-line scaffold to match the structure of
  packages/docs/README.md and packages/sheets/README.md: architecture
  diagram, key concepts table, public API surface, usage example,
  development scripts, tech stack, design doc links.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Per the new Task Workflow, archive completed tasks before merge so
the lessons file and the archived todo land inside the same PR.

PR1 is done:
- 20260507-slides-pr1-themed-authoring-todo.md (100 items checked)
  → docs/tasks/archive/2026/05/
- 20260508-slides-visual-harness-scenarios-todo.md (8 items checked)
  → docs/tasks/archive/2026/05/

The master plan
(20260507-slides-themes-layouts-import-todo.md) keeps PR2 and PR3
sections unchecked so it stays in active/. Only the PR1 section and
the cross-cutting items 1-2 are checked off.

Also align the design doc target-version with the current 0.3.x
package versions: 0.5.0 → 0.4.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@hackerwins
hackerwins merged commit a58aab1 into main May 8, 2026
1 check passed
@hackerwins
hackerwins deleted the slides-themed-authoring branch May 8, 2026 11:21
@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