Add themed authoring for slides (5 themes, 11 layouts, pickers)#191
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (20)
📝 WalkthroughWalkthroughAdds 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. ChangesSlides Themed Authoring and Rendering
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
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.
75b1ab0 to
3db71fe
Compare
Verification: verify:selfResult: ✅ PASS in 129.7s
Verification: verify:integrationResult: ✅ PASS |
There was a problem hiding this comment.
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
inlineStylesEqualnow does reference checks for object colors (false negatives).Line 345 and Line 346 compare
StoredColorvalues 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 winMigrate at construction, not at every
read().
read()runsmigrateDocument(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:
- Internal mutations on a legacy-shaped doc crash before
read()is ever called.addThemeimmediately accessesthis.doc.themes.find(...)on line 125 andthis.doc.themes.push(...)on line 126;applyThemedereferencesthis.doc.themes(line 131) andthis.doc.meta(line 134). If a caller passes a pre-v0.5 document missingthemes/masters/meta.themeId, both methods throwTypeError: Cannot read properties of undefinedeven thoughread()is documented to handle exactly that legacy shape.read()is a hot path. The slides view's RAF tick andstore.onChangere-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 winCoverage gap:
wrapLegacyColoronly 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
ThemeColorvalues — 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 valueOptional: tighten the
visual.testselector.
vitest run visual.testmatches any file whose path contains the substringvisual.test(e.g. a hypotheticalvisual.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 winConsider
canvas@^3instead of^2.11.2for the visual-test dependency.
canvas3.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 valueLGTM — solid immutability check for the new
ThemeColorfill shape.Mutating
bg.fill.valueafter the write and asserting via the discriminatedkindnarrowing is exactly the right test for the deep-clone contract. One small enhancement worth considering: also reassignbg.fill = { kind: 'srgb', value: '#00ff00' }(or pass throughJSON.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 valueConsider whether the all-blue accent palette gives enough perceptual differentiation.
accent1–accent6are 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. Thedefault-darktheme by contrast uses six visibly distinct hues.Additionally,
accent6(#E3F2FD) on thebackground(#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 winUse
configDefaults.excludeto avoid silently dropping Vitest's built-in exclusions.Setting
excludein config replaces the default value entirely. To extend the default exclude patterns,configDefaultsfromvitest/configshould 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 ofnode_modules/distacross 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 valueCreate the paired lessons file alongside the todo.
The coding guidelines require a
YYYYMMDD-<slug>-lessons.mdalongside every*-todo.md. Line 57 tracks this as a future task, but the file is expected to exist now (even if empty). Consider addingdocs/tasks/active/20260507-slides-themes-layouts-import-lessons.mdin 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.mdandYYYYMMDD-<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
paintLiveandcurrentSlide()each callstore.read()independently fromrender()'s snapshot.
paintLive(line 745) andcurrentSlide()(line 754) each issue a freshstore.read(), whilerender()already has a snapshot in scope. On everymousemoveduring drag/resize/rotate, this results in two store reads: one for the live doc (theme colors inforceRender) and one insidecurrentSlide()called byrepaintOverlay. Passing the already-readdocinto 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
docis on the public surface but unused; either wire it or drop it.The JSDoc on lines 14–17 says
docis threaded in so shape/text painters can resolve theme bindings, but onlythemeis forwarded todrawShape/drawText;docis silenced viavoid doc;. Every caller (and now every test) has to construct aSlidesDocumentfor an argument the implementation discards. Since theme resolution is already covered by thethemeargument, consider droppingdocuntil 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 valueStrengthen the fallback assertion.
expect(fillStyles.some((s) => s.startsWith('#'))).toBe(true)is satisfied as long as any hex value reachedfillStyleat 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 onundefined, this check still passes. Consider asserting the specific theme default the renderer is expected to fall back to (e.g.'#000000'fromdefaultLight.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 valueConsider naming the
'#000000'sentinel constant.The magic literal ties this function to
DEFAULT_INLINE_STYLE.colorin 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 valueCell-level
backgroundColorbypassesdefaultColorResolverwhile inline run backgrounds do not.Line 173 sets
ctx.fillStyle = cell.style.backgroundColordirectly (raw string), while lines 220–222 route the inline run'sstyle.backgroundColorthroughdefaultColorResolver. If the model ever evolves to allowThemeColor-shaped objects at the cell level, this path would silently render the object'stoString()representation instead of the resolved color.This is currently safe since
cell.style.backgroundColoris 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 valueTest 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 valueFont picker
valueis hardcoded toundefined, so the active font is never highlighted.The shape-fill picker reads the live value via
readShapeFilland reflects the current selection in its swatch state, but the font picker passesvalue={undefined}regardless — so even when a text element has a uniform font (the common case afteronTextFontChangeruns), 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
parseHexsilently producesNaNfor 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 throughMath.max(0, Math.min(255, NaN)) = NaNand 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 winReuse the shared
DEFAULT_BACKGROUNDhere.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
⛔ Files ignored due to path filters (27)
packages/slides/test-fixtures/visual/goldens/default-dark__empty.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/default-dark__three-slides.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/default-dark__title-only.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/default-light__empty.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/default-light__three-slides.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/default-light__title-only.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/focus__empty.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/focus__three-slides.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/focus__title-only.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/big-number.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/blank.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/caption.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/main-point.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/one-column-text.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/section-header.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/section-title-description.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/title-body.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/title-only.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/title-slide.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/layouts/title-two-columns.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/material__empty.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/material__three-slides.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/material__title-only.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/streamline__empty.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/streamline__three-slides.pngis excluded by!**/*.pngpackages/slides/test-fixtures/visual/goldens/streamline__title-only.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (91)
CLAUDE.mddocs/design/README.mddocs/design/slides/slides-themes-layouts-import.mddocs/tasks/README.mddocs/tasks/active/20260507-slides-pr1-themed-authoring-todo.mddocs/tasks/active/20260507-slides-themes-layouts-import-lessons.mddocs/tasks/active/20260507-slides-themes-layouts-import-todo.mdpackage.jsonpackages/backend/src/yorkie/docs-tree.tspackages/docs/src/export/docx-style-map.tspackages/docs/src/export/pdf-style-map.tspackages/docs/src/index.tspackages/docs/src/model/color.tspackages/docs/src/model/types.tspackages/docs/src/view/paint-layout.tspackages/docs/src/view/table-renderer.tspackages/docs/test/model/color.test.tspackages/docs/test/view/themed-color.test.tspackages/frontend/src/app/docs/yorkie-doc-store.tspackages/frontend/src/app/slides/slides-detail.tsxpackages/frontend/src/app/slides/slides-formatting-toolbar.tsxpackages/frontend/src/app/slides/slides-view.tsxpackages/frontend/src/app/slides/theme-panel-helpers.tspackages/frontend/src/app/slides/theme-panel.tsxpackages/frontend/src/app/slides/theme-thumbnail.tsxpackages/frontend/src/app/slides/themed-color-picker-helpers.tspackages/frontend/src/app/slides/themed-color-picker.tsxpackages/frontend/src/app/slides/themed-font-picker-helpers.tspackages/frontend/src/app/slides/themed-font-picker.tsxpackages/frontend/src/app/slides/yorkie-slides-store.tspackages/frontend/src/types/slides-document.tspackages/frontend/tests/app/slides/theme-panel.test.tspackages/frontend/tests/app/slides/themed-color-picker.test.tspackages/frontend/tests/app/slides/themed-font-picker.test.tspackages/frontend/tests/app/slides/two-user-slides-yorkie.test.tspackages/frontend/tests/app/slides/yorkie-slides-equivalence.test.tspackages/slides/demo.tspackages/slides/package.jsonpackages/slides/src/index.tspackages/slides/src/model/element.tspackages/slides/src/model/layout.test.tspackages/slides/src/model/layout.tspackages/slides/src/model/layouts.visual.test.tspackages/slides/src/model/master.test.tspackages/slides/src/model/master.tspackages/slides/src/model/migrate.test.tspackages/slides/src/model/migrate.tspackages/slides/src/model/presentation.tspackages/slides/src/model/theme.test.tspackages/slides/src/model/theme.tspackages/slides/src/store/memory.test.tspackages/slides/src/store/memory.tspackages/slides/src/store/store.tspackages/slides/src/test-utils/load-fixture.tspackages/slides/src/test-utils/render-snapshot.tspackages/slides/src/themes/default-dark.tspackages/slides/src/themes/default-light.tspackages/slides/src/themes/focus.tspackages/slides/src/themes/index.tspackages/slides/src/themes/material.tspackages/slides/src/themes/streamline.tspackages/slides/src/themes/themes.test.tspackages/slides/src/themes/themes.visual.test.tspackages/slides/src/view/canvas/element-renderer.test.tspackages/slides/src/view/canvas/element-renderer.tspackages/slides/src/view/canvas/image-renderer.tspackages/slides/src/view/canvas/render-context.tspackages/slides/src/view/canvas/shape-renderer.test.tspackages/slides/src/view/canvas/shape-renderer.tspackages/slides/src/view/canvas/slide-renderer.test.tspackages/slides/src/view/canvas/slide-renderer.tspackages/slides/src/view/canvas/text-renderer.test.tspackages/slides/src/view/canvas/text-renderer.tspackages/slides/src/view/canvas/thumbnail.test.tspackages/slides/src/view/canvas/thumbnail.tspackages/slides/src/view/editor/editor.test.tspackages/slides/src/view/editor/editor.tspackages/slides/src/view/editor/interactions/clipboard.test.tspackages/slides/src/view/editor/interactions/drag.test.tspackages/slides/src/view/editor/interactions/insert.test.tspackages/slides/src/view/editor/interactions/insert.tspackages/slides/src/view/editor/interactions/keyboard.test.tspackages/slides/src/view/editor/interactions/lasso.test.tspackages/slides/src/view/editor/interactions/select.test.tspackages/slides/src/view/editor/overlay.test.tspackages/slides/src/view/editor/text-box-editor.test.tspackages/slides/src/view/editor/thumbnail-panel.tspackages/slides/test-fixtures/decks/empty.jsonpackages/slides/test-fixtures/decks/three-slides.jsonpackages/slides/test-fixtures/decks/title-only.jsonpackages/slides/vite.config.ts
| # 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. |
There was a problem hiding this comment.
🧩 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.
| 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 ?? [], | ||
| }; | ||
| } |
There was a problem hiding this comment.
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 Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
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 paintedThe
data-visual-scenario-readyattribute is hardcoded unconditionally on theCardelement, so it is present in the DOM from the very first React commit — before theSlideCanvasuseEffecthas run and beforeSlideRenderer.render()has drawn anything to the canvas.
captureScreenshotsin the script resolves per-target vialocator.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 fiveslides-canvas-*scenarios.A robust fix is to have
SlideCanvasset adata-canvas-readyattribute 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 winMissing section-level readiness wait for slides — inconsistent with other sections
The harness explicitly waits for
sheet,format, andchartsections withdata-visual-*-readyguards (lines 204–217) before starting per-target captures. There is no corresponding wait for theslidessection. While the staticdata-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 onnetworkidle+document.fonts.readyfor timing — which, as noted above inslides-scenarios.tsx, does not guaranteeuseEffectcompletion on the canvas.The concrete fix for canvas readiness belongs in
slides-scenarios.tsx(adding adata-canvas-readysignal), 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
⛔ Files ignored due to path filters (48)
packages/frontend/tests/visual/baselines/harness-visual.browser.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-dark.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-dark.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-dark.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-light.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-light.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-light.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-default-light.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-focus.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-focus.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-focus.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-focus.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-big-number.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-big-number.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-big-number.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-big-number.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-section-header.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-section-header.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-section-header.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-section-header.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-title-body.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-title-body.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-title-body.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-layout-title-body.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-material.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-material.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-material.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-material.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-streamline.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-streamline.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-streamline.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-streamline.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-pickers.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-pickers.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-pickers.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-pickers.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-theme-panel.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-theme-panel.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-theme-panel.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-theme-panel.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar.pngis excluded by!**/*.png
📒 Files selected for processing (6)
docs/design/slides/slides-themes-layouts-import.mddocs/tasks/active/20260507-slides-themes-layouts-import-lessons.mddocs/tasks/active/20260508-slides-visual-harness-scenarios-todo.mdpackages/frontend/scripts/verify-visual-browser.mjspackages/frontend/src/app/harness/visual/page.tsxpackages/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
| const elements: Element[] = layout.placeholders.map((p, i) => ({ | ||
| id: `e${i}`, | ||
| type: p.type, | ||
| frame: p.frame, | ||
| data: p.data, | ||
| })) as Element[]; |
There was a problem hiding this comment.
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.
| 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[]".
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]>
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
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.
OOXML / Google Slides. New types in
@wafflebase/slideswithresolveColor/resolveFontas the single seam every paint pathgoes through.
ThemeColor: stored as{ kind: 'role', role, tint?, shade? }or
{ kind: 'srgb', value }. Role-bound colors automatically followtheme switches with no model rewrite.
focus, material. Side panel batches
addTheme + applyThemeintoone undo step.
section-header, title-body, title-two-columns, title-only,
one-column-text, main-point, section-title-description, caption,
big-number.
and text Font (theme heading+body / system list). Built on Radix
DropdownMenuso they portal to body and don't get clipped by thetoolbar's overflow context. Look and feel matches the docs / sheets
toolbars.
migrateDocument: idempotent. Legacy v1 decks gainthemes,masters,meta.themeId='default-light', layouts; legacystring color fills wrap to
{ kind: 'srgb' }; legacylayoutId: 'title'remaps to
'title-slide'.11 layout) gated behind
INCLUDE_VISUAL_TESTS=1to avoid CIfont/Cairo divergence on Linux.
Sixteen commits (eight
feat, threefixup, three lessons / planning,two refactor / picker UX). Each
featcommit is a coherent unit; thefixupcommits surface bugs caught during browser smoke and thepicker-look refactor. The first
feat(Theme/Master types) deliberatelyends 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 testconfirms
applyThemeconverges across attached clients.Test plan
pnpm verify:fast— green (754 tests)pnpm --filter @wafflebase/slides test:visual— 26 goldens matchfollow theme, contextual Fill / Font pickers open and apply
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
Documentation
Tests