Skip to content

Add slides Format options right panel (v1)#309

Merged
hackerwins merged 23 commits into
mainfrom
slides-format-options-panel
May 29, 2026
Merged

Add slides Format options right panel (v1)#309
hackerwins merged 23 commits into
mainfrom
slides-format-options-panel

Conversation

@hackerwins

@hackerwins hackerwins commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a right-side Format options panel to the slides editor, mirroring Google Slides: Size & Position (W/H/X/Y/Rotation, lock aspect, 90° rotate, in/cm), Text fitting (autofit 3-mode), Image opacity, Alt text. Theme and Format share one right slot via a rightPanel: 'theme' | 'format' | null union.
  • Extends the in-canvas autofit toggle (bottom-left of selected text box) from a 2-state shrink/grow flip to a 3-state cycle (none → shrink → grow → none) matching the Format panel order, with a new ICON_NONE for the off state.
  • Grows the in-editor text-box canvas in do not autofit mode so text typed past the saved box height stays visible while editing; the outer frame.h write is still gated to grow only, so the post-commit render matches the pre-edit frame.

Data model change is one optional Meta.unit?: 'in' | 'cm' field plus SlidesStore.setUnit. All other work is additive UI.

Design: docs/design/slides/slides-format-options-panel.md
Plan: docs/tasks/archive/2026/05/20260529-slides-format-options-panel-todo.md

Test plan

  • pnpm verify:fast — 803 unit tests green, lint clean (--max-warnings 0).
  • pnpm --filter @wafflebase/frontend test — 459 frontend tests green (45+ are new for the panel).
  • Pre-PR multi-agent review (CLAUDE.md compliance, shallow bug scan, git-history context, prior PR comments, code-comment compliance) ran on the full branch; two findings addressed inline:
    • MemSlidesStore.setUnit now calls this.requireBatch() like every other mutator in the file.
    • ImageAdjustmentsSection re-syncs draft via useEffect so selection swaps don't commit stale transparency.
  • Manual smoke in pnpm dev:
    • Toolbar Format toggle opens/closes the panel; mutually exclusive with Theme.
    • Single-shape edit: W/H/X/Y/Rotation, lock aspect, 90° rotate, in/cm toggle round-trip.
    • Multi-select: mixed values render blank input, edit applies to every selected element in one undo step.
    • Image selection: opacity slider, alt-text textarea; toolbar alt-text dropdown is gone.
    • Text element selection: autofit 3-mode; grow disables H input with tooltip.
    • In-canvas autofit toggle cycles none → shrink → grow → none; none state shows new X-in-box icon.
    • Type past the saved box height in none mode — text stays visible while editing; on commit, frame keeps its saved height and overflow renders below as before.
  • Browser smoke (T13) deferred — slides has no interaction-browser harness today. Follow-up tracked.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements a comprehensive Format options panel for the slides editor, enabling right-side editing of slide element properties (size, position, text fitting, image adjustments, alt text). It introduces a unified right-panel state, adds unit preference persistence, enhances text autofit to 3-state behavior, and includes full test coverage and documentation.

Changes

Format Options Panel Feature

Layer / File(s) Summary
Design specification and planning
docs/design/slides/slides-format-options-panel.md, docs/design/README.md, docs/tasks/archive/2026/05/slides-format-options-panel-*.md, docs/tasks/README.md, docs/tasks/archive/README.md
Design doc specifies Format panel v1 scope (size/position, text fitting, image adjustments, alt text), architecture replacing separate theme state with unified right-panel union, detailed section behaviors, minimal data-model change (Meta.unit), testing/rollout strategy, and risk mitigation. Task documents track 14 implementation steps and capture retrospective lessons learned.
Data model and migration
packages/slides/src/model/presentation.ts, packages/slides/src/model/migrate.ts
Adds optional Meta.unit?: 'in' | 'cm' field to control display units for format inputs (defaults to inches when absent). Migration logic preserves unit during document migration when set to valid values.
Store interface and implementations
packages/slides/src/store/store.ts, packages/slides/src/store/memory.ts, packages/frontend/src/app/slides/yorkie-slides-store.ts, packages/slides/test/store/mem-set-unit.test.ts
Adds setUnit(unit: 'in' | 'cm') method to SlidesStore interface. Implements in both MemSlidesStore and YorkieSlidesStore with input validation, batch requirement, and metadata persistence. Unit test verifies valid units persist and invalid units throw.
Frontend conversion and routing utilities
packages/frontend/src/app/slides/format-panel/units.ts, packages/frontend/src/app/slides/format-panel/pick-sections.ts, packages/frontend/tests/app/slides/format-panel/units.test.ts, packages/frontend/tests/app/slides/format-panel/pick-sections.test.ts
Pure utility modules: units.ts provides pixel↔inch/cm conversions, angle conversions, display formatting, and common-value derivation for multi-select. pick-sections.ts maps selection types (shape/image/text/connector/mixed) to visible section IDs. Comprehensive tests validate conversion accuracy, rounding, and routing logic.
Test infrastructure and dependencies
packages/frontend/package.json, packages/frontend/tests/setup.ts, packages/frontend/vite.config.ts
Adds @testing-library/react and @testing-library/user-event to dev dependencies. Registers global test setup hook (cleanup after each test). Expands Vitest to discover both .test.ts and .test.tsx files and runs setup before tests.
Format panel section components (alt text, image, text fitting)
packages/frontend/src/app/slides/format-panel/alt-text-section.tsx, packages/frontend/src/app/slides/format-panel/image-adjustments-section.tsx, packages/frontend/src/app/slides/format-panel/text-fitting-section.tsx, packages/frontend/tests/app/slides/format-panel/alt-text-section.test.tsx, packages/frontend/tests/app/slides/format-panel/image-adjustments-section.test.tsx, packages/frontend/tests/app/slides/format-panel/text-fitting-section.test.tsx
Three focused section components for format panel: AltTextSection manages image alt via textarea with blur-to-commit; ImageAdjustmentsSection controls opacity via transparency slider with pointerup commit; TextFittingSection selects autofit mode via radio. All handle single/multi-select with draft state and validation. Tests verify read/mixed/commit/no-op paths.
Format panel size/position section
packages/frontend/src/app/slides/format-panel/size-position-section.tsx, packages/frontend/tests/app/slides/format-panel/size-position-section.test.tsx
Complex section for W/H/X/Y/Rotation editing with unit toggle (in↔cm), aspect-ratio lock, and selection-type gating. Includes internal UnitInput and RotationInput helpers. Extensive test suite covers shape/connector/mixed/text-element variants, unit formatting, lock semantics, rotation buttons, and autofit-dependent height disabling.
Format panel shell and selection derivation
packages/frontend/src/app/slides/format-panel/index.tsx
Core FormatPanel component derives current selection from editor/store, computes visible sections, owns mutation callbacks (frame commits, element data patches, locked resize with aspect-ratio preservation, 90° rotation, unit updates), and conditionally renders wired section components based on selection state.
Slides detail integration and right-panel state
packages/frontend/src/app/slides/slides-detail.tsx
Replaces separate themePanel boolean state with unified rightPanel union ('theme' | 'format' | null) for mutual exclusion. Updates toolbar to toggle between panels, conditionally renders ThemePanel or FormatPanel based on right-panel state, preserves mobile/read-only behavior.
Toolbar format button and alt-text migration
packages/frontend/src/app/slides/toolbar/global-controls.tsx, packages/frontend/src/app/slides/toolbar/index.tsx, packages/frontend/src/app/slides/toolbar/image-controls.tsx
Adds "Format options" toggle button to global toolbar controls (right zone, next to Theme) with icon and tooltip. Threads format-panel callbacks through toolbar prop hierarchy. Removes image alt-text dropdown from toolbar (now exclusively in Format panel), including cleanup of state/imports/wiring.
Autofit behavior enhancements
packages/slides/src/view/editor/overlay.ts, packages/slides/src/view/editor/text-box-editor.ts, packages/slides/test/view/editor/autofit-toggle.test.ts, packages/slides/test/view/text-box-autofit-gating.test.ts
Evolves autofit from 2-state toggle (grow↔shrink) to 3-state cycle (none→shrink→grow→none) with new icon. Distinguishes between allowing editor surface growth vs. persisting grown height: none mode allows surface resize without persisting, grow persists. Test updates verify cycle transitions and gating behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • wafflebase/wafflebase#305: Implements the underlying 3-state autofit (none/shrink/grow) behavior and in-editor toggle that this PR's Format panel "Text fitting" section controls
  • wafflebase/wafflebase#244: Earlier toolbar redesign that intersects with this PR's toolbar updates—extends global-controls and slides-detail, removes alt-text UI from image-controls
  • wafflebase/wafflebase#191: Modifies packages/slides/src/model/migrate.ts for theme/master/layout migration; this PR additionally preserves meta.unit during migration

Poem

🐰 A format panel hops into place,
With inches and centimeters embraced,
Size, position, opacity glow,
Alt text and autofit steal the show!
Three modes to fit, one panel to share—
The slides editor's gotten a fair!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add slides Format options right panel (v1)' directly and accurately summarizes the main change: introducing a new Format options panel UI feature for the slides editor.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 265.1s

Lane Status Duration
tokens:build ✅ pass 2.3s
sheets:build ✅ pass 14.7s
docs:build ✅ pass 13.2s
slides:build ✅ pass 15.4s
verify:fast ✅ pass 173.3s
frontend:build ✅ pass 20.2s
verify:frontend:chunks ✅ pass 0.4s
backend:build ✅ pass 5.2s
cli:build ✅ pass 2.3s
verify:entropy ✅ pass 18.1s

Verification: verify:integration

Result: ✅ PASS

@codecov

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
packages/slides/src/model/migrate.ts (1)

18-21: ⚡ Quick win

Consider expanding the comment to explain why explicit preservation is needed.

The current comment states what the code does, but the lessons doc reveals an important why: migrateDocument reconstructs meta from scratch (lines 13-17), so any new optional field will be dropped unless explicitly preserved here. This is a recurring pattern that future contributors adding Meta fields should understand.

📚 Suggested enhancement
- // Preserve the optional unit field if present and valid.
+ // migrateDocument reconstructs meta from a fixed literal (lines 13-17),
+ // so optional fields like unit are dropped unless explicitly preserved.
+ // Every new Meta field needs a similar preservation block.
  if (raw?.meta?.unit === 'in' || raw?.meta?.unit === 'cm') {
    meta.unit = raw.meta.unit;
  }

As per coding guidelines, "Write clear comments explaining the 'why' behind complex logic, not just the 'what'". The lesson doc at docs/tasks/archive/2026/05/20260529-slides-format-options-panel-lessons.md lines 5-16 identifies this as a recurring pattern that surprised the implementer.

🤖 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 18 - 21, The comment above
the unit-preservation branch is too terse; update it to explain why explicit
preservation is required by noting that migrateDocument reconstructs meta from
scratch (see migrateDocument) so any optional fields like meta.unit would be
dropped unless explicitly carried over; modify the comment near the if
(raw?.meta?.unit === 'in' || raw?.meta?.unit === 'cm') block to state that this
preserves optional Meta fields across migration because migrateDocument rebuilds
meta and would otherwise omit newly added optional properties.
packages/slides/test/store/mem-set-unit.test.ts (2)

4-28: ⚖️ Poor tradeoff

Consider adding test coverage for YorkieSlidesStore.setUnit.

The test suite only covers MemSlidesStore, but YorkieSlidesStore is the production implementation used in the collaborative editor. Test coverage for both implementations helps ensure consistency and catch implementation-specific bugs. As per coding guidelines, critical business logic should have test coverage.

🤖 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/test/store/mem-set-unit.test.ts` around lines 4 - 28, Add
equivalent tests for YorkieSlidesStore.setUnit alongside the existing
MemSlidesStore cases: create a YorkieSlidesStore instance and mirror the four
tests (default meta.unit undefined, setUnit('cm') writes 'cm', setUnit('in')
writes 'in', and setUnit rejects invalid values) in the same test file (or a new
parallel test file) to ensure parity; reference the YorkieSlidesStore class and
its setUnit method, use its batch API like the MemSlidesStore tests, and assert
via yorkieStore.read().meta.unit and expect(...).toThrow(/invalid unit/i) for
the invalid case.

4-28: ⚡ Quick win

Add test coverage for batch requirement enforcement.

The test suite should verify that calling setUnit outside of a batch() throws an error, as this is a critical contract of the store interface. As per coding guidelines, tests should cover critical business logic and edge cases.

🧪 Suggested test case
it('setUnit throws when called outside batch', () => {
  const store = new MemSlidesStore();
  expect(() => store.setUnit('cm')).toThrow(/batch/i);
});
🤖 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/test/store/mem-set-unit.test.ts` around lines 4 - 28, Add a
test to enforce the batch requirement for MemSlidesStore.setUnit: in the
MemSlidesStore.setUnit test suite, add a test that instantiates MemSlidesStore
and asserts that calling store.setUnit('cm') outside of store.batch(...) throws
an error matching /batch/i; reference the MemSlidesStore class and setUnit
method to locate where to add the new test.
packages/frontend/tests/app/slides/format-panel/image-adjustments-section.test.tsx (1)

50-62: ⚡ Quick win

Add coverage for the keyboard commit path.

This suite only verifies pointerUp. Once the keyboard commit gap in image-adjustments-section.tsx is addressed, add a case that fires keyUp after a value change and asserts onCommit is invoked, to lock in the accessibility fix.

🤖 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/format-panel/image-adjustments-section.test.tsx`
around lines 50 - 62, Add a new test in image-adjustments-section.test.tsx that
mirrors the existing pointerUp case but exercises the keyboard commit path:
render ImageAdjustmentsSection with elements like img('a', 1) and img('b', 1)
and a mock onCommit, change the transparency slider
(screen.getByLabelText(/transparency/i)) to a new value, then fire a keyUp event
on the slider and assert onCommit was called with ['a','b'] and the expected
opacity (e.g., 0.6); this ensures ImageAdjustmentsSection's keyboard commit path
invokes the same onCommit handler as pointerUp.
packages/frontend/src/app/slides/format-panel/size-position-section.tsx (1)

69-73: ⚡ Quick win

Lock reset depends on elements array identity, which may be fragile.

The effect resets locked whenever the elements reference changes. If the parent (FormatPanel) derives elements by recreating the array each render (e.g. selection.map(...)/filter(...)), any unrelated re-render that preserves the same selection will produce a new array reference and silently flip locked back to false mid-edit. Keying on a stable selection identity avoids this.

♻️ Key the reset on selection ids
   const [locked, setLocked] = useState(false);
   // Reset lock state when the selection changes.
-  useEffect(() => {
-    setLocked(false);
-  }, [elements]);
+  const selectionKey = ids.join('\u0000');
+  useEffect(() => {
+    setLocked(false);
+  }, [selectionKey]);

Confirm whether FormatPanel passes a referentially stable elements array:

#!/bin/bash
# Inspect how `elements` is built/passed to SizePositionSection in the panel shell.
fd -e tsx -e ts . --full-path 'format-panel/index' --exec cat {}
rg -nP -C4 '<SizePositionSection' --type=tsx
🤖 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/format-panel/size-position-section.tsx`
around lines 69 - 73, The effect resetting locked currently depends on the
referential identity of elements (useEffect(() => setLocked(false),
[elements])), which is fragile; change it to depend on a stable selection
identity such as the set of element ids instead: derive a stable array or string
of ids (e.g. selectionIds = elements.map(e => e.id).join(','), or useMemo(() =>
elements.map(e => e.id), [elements]) and then useEffect(() => setLocked(false),
[selectionIds]); alternatively, make FormatPanel pass a referentially stable
elements prop to SizePositionSection; update the dependency array and the
derivation of selectionIds near the locked/setLocked and useEffect code
accordingly (referencing locked, setLocked, useEffect, elements,
SizePositionSection, FormatPanel).
🤖 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/slides/format-panel/image-adjustments-section.tsx`:
- Around line 46-56: The range input currently only calls onCommit inside the
onPointerUp handler so keyboard users never trigger commits; add a shared commit
function (e.g., commit) inside the component that computes opacity as 1 - draft
/ 100 and calls onCommit(elements.map(el => el.id), opacity), then invoke that
commit from both onPointerUp and from a new onKeyUp (and/or onBlur) handler;
keep existing onChange to update draft via setDraft so keyboard adjustments
update state and are committed via the new handlers.

In `@packages/frontend/src/app/slides/yorkie-slides-store.ts`:
- Around line 785-793: Move the validation inside setUnit to after the call to
requireBatch so it matches other mutators: call this.requireBatch() first, then
validate the unit and throw if invalid, and finally call this.doc.update((r) =>
{ r.meta.unit = unit; }); reference functions: setUnit, requireBatch, and
this.doc.update / r.meta.unit to locate and update the code.

---

Nitpick comments:
In `@packages/frontend/src/app/slides/format-panel/size-position-section.tsx`:
- Around line 69-73: The effect resetting locked currently depends on the
referential identity of elements (useEffect(() => setLocked(false),
[elements])), which is fragile; change it to depend on a stable selection
identity such as the set of element ids instead: derive a stable array or string
of ids (e.g. selectionIds = elements.map(e => e.id).join(','), or useMemo(() =>
elements.map(e => e.id), [elements]) and then useEffect(() => setLocked(false),
[selectionIds]); alternatively, make FormatPanel pass a referentially stable
elements prop to SizePositionSection; update the dependency array and the
derivation of selectionIds near the locked/setLocked and useEffect code
accordingly (referencing locked, setLocked, useEffect, elements,
SizePositionSection, FormatPanel).

In
`@packages/frontend/tests/app/slides/format-panel/image-adjustments-section.test.tsx`:
- Around line 50-62: Add a new test in image-adjustments-section.test.tsx that
mirrors the existing pointerUp case but exercises the keyboard commit path:
render ImageAdjustmentsSection with elements like img('a', 1) and img('b', 1)
and a mock onCommit, change the transparency slider
(screen.getByLabelText(/transparency/i)) to a new value, then fire a keyUp event
on the slider and assert onCommit was called with ['a','b'] and the expected
opacity (e.g., 0.6); this ensures ImageAdjustmentsSection's keyboard commit path
invokes the same onCommit handler as pointerUp.

In `@packages/slides/src/model/migrate.ts`:
- Around line 18-21: The comment above the unit-preservation branch is too
terse; update it to explain why explicit preservation is required by noting that
migrateDocument reconstructs meta from scratch (see migrateDocument) so any
optional fields like meta.unit would be dropped unless explicitly carried over;
modify the comment near the if (raw?.meta?.unit === 'in' || raw?.meta?.unit ===
'cm') block to state that this preserves optional Meta fields across migration
because migrateDocument rebuilds meta and would otherwise omit newly added
optional properties.

In `@packages/slides/test/store/mem-set-unit.test.ts`:
- Around line 4-28: Add equivalent tests for YorkieSlidesStore.setUnit alongside
the existing MemSlidesStore cases: create a YorkieSlidesStore instance and
mirror the four tests (default meta.unit undefined, setUnit('cm') writes 'cm',
setUnit('in') writes 'in', and setUnit rejects invalid values) in the same test
file (or a new parallel test file) to ensure parity; reference the
YorkieSlidesStore class and its setUnit method, use its batch API like the
MemSlidesStore tests, and assert via yorkieStore.read().meta.unit and
expect(...).toThrow(/invalid unit/i) for the invalid case.
- Around line 4-28: Add a test to enforce the batch requirement for
MemSlidesStore.setUnit: in the MemSlidesStore.setUnit test suite, add a test
that instantiates MemSlidesStore and asserts that calling store.setUnit('cm')
outside of store.batch(...) throws an error matching /batch/i; reference the
MemSlidesStore class and setUnit method to locate where to add the new test.
🪄 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: 3002d7eb-ae09-4d92-bf6d-c7dfb777c1bb

📥 Commits

Reviewing files that changed from the base of the PR and between 9708b6e and 9c9f9cc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (36)
  • docs/design/README.md
  • docs/design/slides/slides-format-options-panel.md
  • docs/tasks/README.md
  • docs/tasks/archive/2026/05/20260529-slides-format-options-panel-lessons.md
  • docs/tasks/archive/2026/05/20260529-slides-format-options-panel-todo.md
  • docs/tasks/archive/README.md
  • packages/frontend/package.json
  • packages/frontend/src/app/slides/format-panel/alt-text-section.tsx
  • packages/frontend/src/app/slides/format-panel/image-adjustments-section.tsx
  • packages/frontend/src/app/slides/format-panel/index.tsx
  • packages/frontend/src/app/slides/format-panel/pick-sections.ts
  • packages/frontend/src/app/slides/format-panel/size-position-section.tsx
  • packages/frontend/src/app/slides/format-panel/text-fitting-section.tsx
  • packages/frontend/src/app/slides/format-panel/units.ts
  • packages/frontend/src/app/slides/slides-detail.tsx
  • packages/frontend/src/app/slides/toolbar/global-controls.tsx
  • packages/frontend/src/app/slides/toolbar/image-controls.tsx
  • packages/frontend/src/app/slides/toolbar/index.tsx
  • packages/frontend/src/app/slides/yorkie-slides-store.ts
  • packages/frontend/tests/app/slides/format-panel/alt-text-section.test.tsx
  • packages/frontend/tests/app/slides/format-panel/image-adjustments-section.test.tsx
  • packages/frontend/tests/app/slides/format-panel/pick-sections.test.ts
  • packages/frontend/tests/app/slides/format-panel/size-position-section.test.tsx
  • packages/frontend/tests/app/slides/format-panel/text-fitting-section.test.tsx
  • packages/frontend/tests/app/slides/format-panel/units.test.ts
  • packages/frontend/tests/setup.ts
  • packages/frontend/vite.config.ts
  • packages/slides/src/model/migrate.ts
  • packages/slides/src/model/presentation.ts
  • packages/slides/src/store/memory.ts
  • packages/slides/src/store/store.ts
  • packages/slides/src/view/editor/overlay.ts
  • packages/slides/src/view/editor/text-box-editor.ts
  • packages/slides/test/store/mem-set-unit.test.ts
  • packages/slides/test/view/editor/autofit-toggle.test.ts
  • packages/slides/test/view/text-box-autofit-gating.test.ts

Comment thread packages/frontend/src/app/slides/yorkie-slides-store.ts
hackerwins and others added 23 commits May 30, 2026 01:24
Slides v1 deferred the right-side Format options panel — users have
nowhere to enter precise numeric sizes, positions, or rotations and
can only reach autofit and image alt text through ad-hoc spots in
the toolbar. The contextual toolbar is the wrong place for precise
inputs; a Google-Slides-style right panel is the established
solution.

v1 of the panel covers only properties already in the data model
(Size & Position, Text fitting, Image opacity, Alt text), so the
change is a single optional `Meta.unit` field and additive UI.
Drop shadow, reflection, recolor, brightness/contrast, text
padding, and numeric shape adjustments stay deferred — each needs
new model fields and renderer/PPTX work that does not belong with
the panel shell.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
14-task plan covering Meta.unit field + setUnit on both stores,
pure helpers (units.ts, pick-sections.ts), four section components
with TDD, FormatPanel shell, toolbar Format button, rightPanel
union in slides-detail, browser smoke, and final verification.
The right-side Format options panel needs a persisted in/cm
preference so each peer sees the same setting and so the ruler
(separate spec) can pick up the same field later. Adding it as
optional on Meta keeps existing serialized decks unchanged.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Mirrors MemSlidesStore.setUnit so the Format panel writes the
in/cm preference through Yorkie. Peers receive the change via
the normal store onChange subscription.
Pure helpers backing the Format options panel — px↔inch/cm at
192 px/in (canvas-derived), rad↔deg, and a multi-select common-
value reducer. Kept in their own module so they unit-test without
React mounting.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Pure mapping from PanelSelection to the ordered list of sections
the shell should render. Idle returns empty so the shell renders
the empty-state hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Textarea bound to image.alt with draft state, onBlur commit, and
mixed-value placeholder. Also installs @testing-library/react,
adds tests/setup.ts with afterEach cleanup, and extends the vitest
include pattern to cover .tsx test files.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Maps the existing image.opacity field to a 0..100% transparency
slider. pointerUp commit so a drag produces one undo entry per
adjustment session.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Direct 3-radio control for none/shrink/grow. Same autofit field
the existing in-canvas toggle writes — both UI surfaces stay in
sync via the store onChange path.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Numeric inputs with onBlur/Enter commit, Escape revert, mixed-
value blank placeholder, in/cm radio, 90 deg rotation buttons,
and a local-state lock-aspect toggle that hands off per-element
proportional resize to the parent. Connector W/H/rotation hidden,
X/Y disabled when an endpoint is attached. text-element + autofit=
grow disables H with tooltip.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Derives PanelSelection from editor + store, routes to sections via
pickSections, owns the commit callbacks that wrap store.batch.
Subscribes to both selection and store changes so the panel
follows remote edits and selection swaps.
The Format panel's Alt text section is the single source. Removing
the toolbar dropdown avoids two-surface drift.
Right global zone gains a Format button next to Theme. Wired via
two optional props so mobile and read-only call sites can keep
ignoring them.
themePanelOpen boolean is generalized to a single rightPanel union
so Theme and Format are mutually exclusive in the slot. Opening
one closes the other. Mobile and the read-only viewer are
untouched — they never pass the format toggle props, so the
toolbar button stays hidden there.
Match the existing Theme toggle pattern so both right-zone buttons
share visual treatment, focus ring, and pressed state styling.
Plan and lessons for the 13-task implementation. T13 browser smoke
deferred — slides has no interaction harness today; coverage gap
closed by 45+ unit tests across T3–T8.
The panel is w-72 (288px). Rotation label + input + ° unit takes
~216px on its own; adding two 90° buttons inline overflows.
Splitting the row keeps the input visible at full width and lets
the buttons sit right-aligned underneath.
W/H/X/Y/Rotation labels share an 80px left column; inputs and the
unit suffix share the right column. Lock-aspect, rotate-90 buttons,
and the Units radio row now indent under the input column instead
of floating left or right-aligned, producing one consistent grid
across the section.
W/H/X/Y/Rotation now use shadcn <Input> (h-7 compact); lock and
rotate buttons use shadcn <Button> with Tabler icons (IconLock/
IconLockOpen, IconRotate/IconRotateClockwise) replacing emoji
and unicode arrows. Alt-text textarea inherits the same border
and focus-ring styling. Text-fitting radios share the text-xs
type scale with the rest of the panel.
The bottom-left in-context toggle on a selected text box now
advances none → shrink → grow → none, matching the order the
Format panel's Text fitting section lists them in. Previously
the inline icon was a 2-state shrink/grow toggle and 'do not
autofit' was reachable only via the Format panel or API.

A new ICON_NONE (rectangle with X) renders for the 'none' state.
Editing a text box in 'do not autofit' mode clipped any text past
the saved box height — the canvas was sized to frame.h with no
growth hook, so what the user typed disappeared until commit (the
slide renderer then drew the overflow below the frame, so the
text reappeared on exit).

The fix wires onContentHeightChange for 'none' the same way 'grow'
does so the editing canvas tracks content height. The outer
callback that writes back to frame.h still only fires for 'grow' —
'none' keeps the saved box, so post-commit rendering matches the
pre-edit frame and the overflow paints below as before.
Two findings from the pre-PR review:

- MemSlidesStore.setUnit was the only mutator in the file that
  skipped requireBatch(). Calling it outside a batch would corrupt
  undo state silently rather than throwing like every other writer.
- ImageAdjustmentsSection captured the transparency draft only at
  mount, so swapping to a different image (or receiving a remote
  opacity change) would leave the slider on the stale value and
  the next pointerUp would commit it back.
Two follow-ups from the bot review:

- ImageAdjustmentsSection committed only on pointerUp, so keyboard
  adjustments (Arrow / Home / End / PgUp / PgDn) updated the slider
  thumb but never reached the store. Add an onKeyUp commit using
  the same handler.
- YorkieSlidesStore.setUnit ran the unit validation before
  requireBatch(), so callers outside a batch with an invalid value
  saw 'invalid unit' instead of the 'Mutations must be wrapped in
  batch()' error every other mutator throws. Reorder to match.
@hackerwins
hackerwins force-pushed the slides-format-options-panel branch from 84ba917 to bfda4cd Compare May 29, 2026 16:34
@hackerwins
hackerwins merged commit abd33e3 into main May 29, 2026
4 checks passed
@hackerwins
hackerwins deleted the slides-format-options-panel branch May 29, 2026 16:43
hackerwins added a commit that referenced this pull request May 29, 2026
Pull in PR #309 (slides format options right panel) and #311 (Korean
NFC normalize in document-list filter). Only conflict was the archived
tasks index, regenerated via pnpm tasks:index.
hackerwins added a commit that referenced this pull request May 31, 2026
PRs #257/#301/#303/#304/#305/#306/#307/#309/#315/#316/#317/#321/#322
shipped between v0.4.2 and the v0.4.3 cut, leaving their paired todo
files in active with the process checkboxes still open. Flip the
remaining boxes for the work that landed, then move the paired
todo/lessons into docs/tasks/archive/2026/05/ via pnpm tasks:archive.
Active count drops from 17 to 6; archive count grows by 21 files.

Patch release covering 29 commits since v0.4.2. Headline theme is
Slides editing & UX polish toward Google Slides / PowerPoint parity:
smart guides (equal-spacing / distance / size), Shift drag modifiers,
text inside shapes, a Format options right panel, tier-1 universal
toolbar controls, text autofit (shrink + grow), group-selection
visuals, and several smaller text-box / selection fixes. PPTX import
gained paragraph-level bullet styles, body anchor (vertical
alignment), and stretched blipFill cover-crop.

Docs added a pending-inline-style at collapsed caret, a caret that
tracks the resolved text color, bullet indent on Tab / Shift+Tab,
font / size / line-spacing / clear formatting toolbar controls, an
inline-style-attribute clear that actually lands in the CRDT, and a
polished toolbar trigger / color swatch pass shared across docs /
sheets / slides. Sheets gained .xlsx import.

Infrastructure: Docker images now build on native arm64 runners,
fixing the release hang seen on v0.4.2. No Prisma migration, no new
backend env vars — the devops bump is a routine image-tag change.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant