Add mobile shell and morphing toolbar for slides editor#268
Conversation
Replace the bespoke 44px header in MobileSlidesView with the docs/sheets shell (SidebarProvider + SiteHeader + SlidesToolbar) and make the toolbar morph between idle/object/text-edit states with bottom sheets for contextual editing. Adds a horizontal thumbnail strip for slide nav with an inline + cell that appends a blank slide and jumps to it. The previous mobile slides UI was read-mostly: no insert, no format, no arrange — just swipe navigation. This brings mobile up to feature parity with desktop for the common edit paths while keeping the desktop code path byte-identical (state machine + controls all reused). ShapePicker and LinePicker gain an optional `trigger` prop so the mobile Insert sheet's Shape/Line entries match the Text box and Image entries visually. SlideGroup drops the "Slide" text label (icon-only `+`), with add-slide now living in the thumbnail strip. Tracked in: - docs/tasks/active/20260519-slides-mobile-shell-todo.md - docs/tasks/active/20260519-slides-mobile-toolbar-morph-todo.md Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis PR implements the mobile slides shell (Phase B-0) by refactoring mobile slides view to lift state upward, introducing a new mobile-specific toolbar renderer that morphs between contextual modes via bottom sheets, and orchestrating a new MobileSlidesLayout that reuses desktop shell elements for mobile. ChangesMobile Slides Shell & Toolbar
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Verification: verify:selfResult: ✅ PASS in 219.3s
Verification: verify:integrationResult: ✅ PASS |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Regenerated via `pnpm verify:browser:docker:update`: Intended changes (28 baselines × 7 scenarios × 4 profiles): - slides-toolbar, slides-toolbar-idle, slides-toolbar-shape-selected, slides-toolbar-image-selected, slides-toolbar-text-element-selected, slides-toolbar-text-editing, slides-toolbar-multi-select - Desktop: SlideGroup label "Slide" removed → icon-only `+` - Mobile: completely new MobileSlidesToolbar morph renderer (was desktop renderer clipped at 430px before) Incidental drift (43 baselines): - 4 harness landing page captures (aggregate of all scenarios) - 39 mobile-only slides-canvas/shapes baselines — small antialiasing shifts (300-500 byte deltas) from Docker browser re-pull. Desktop variants of these scenarios are byte-identical, confirming canvas rendering logic is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The mobile toolbar's trash button called store.removeElements + editor.setSelection([]) directly, but neither path triggers a canvas repaint. The thumbnail strip subscribes to store.onChange and refreshed instantly, so the deleted shape vanished from the bottom strip while remaining visible on the main slide until the next interaction. Add a public editor.deleteSelected() that wraps the desktop context menu's existing batch + selection.clear + requestRender pattern, and have both the context menu and the mobile trash button call it. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
packages/frontend/src/app/slides/line-picker.tsx (1)
121-123: ⚡ Quick winNarrow
triggerto a single element compatible withasChild.
DropdownMenuTrigger asChildexpects one element that can receive forwarded props/ref;React.ReactNodeallows invalid values and weakens type safety. Tighten this to an element type.Proposed fix
-import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, type ReactElement } from "react"; @@ - trigger?: React.ReactNode; + trigger?: ReactElement;As per coding guidelines, "Use type annotations in TypeScript to improve code clarity and catch errors early".
Also applies to: 159-161
🤖 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/line-picker.tsx` around lines 121 - 123, The prop type for "trigger" is too broad—change its type from React.ReactNode to a single element type compatible with DropdownMenuTrigger's asChild (e.g. React.ReactElement or React.ReactElement<any, any>) so the component only accepts one element that can receive forwarded props/refs; update the "trigger?: React.ReactNode" declaration (and the identical declaration around lines 159-161) to "trigger?: React.ReactElement" (or React.ReactElement<any, any>) and ensure any call sites pass a single element (like SheetActionButton) rather than fragments/strings.packages/frontend/src/app/slides/shape-picker.tsx (1)
73-77: ⚡ Quick winUse a stricter
triggertype for RadixasChildcomposition.Because this is passed directly to
DropdownMenuTrigger asChild, the prop should be a single element type instead ofReactNodeto prevent invalid children at call sites.Proposed fix
-import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, type ReactElement } from "react"; @@ - trigger?: React.ReactNode; + trigger?: ReactElement;As per coding guidelines, "Use type annotations in TypeScript to improve code clarity and catch errors early".
Also applies to: 116-118
🤖 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/shape-picker.tsx` around lines 73 - 77, The prop type for trigger is too permissive (React.ReactNode) for Radix's DropdownMenuTrigger asChild — change the trigger prop declaration(s) in shape-picker.tsx (the `trigger` prop at the earlier definition and the similar one at lines ~116-118) to accept a single React element (e.g., React.ReactElement or React.ReactElement<any>) instead of React.ReactNode so callers cannot pass invalid/multiple children; update the prop type annotations and any related usages to accept/forward a single React element compatible with asChild.packages/frontend/src/app/slides/mobile-slides-view.tsx (1)
133-141: ⚡ Quick winAvoid re-rendering the whole mobile shell on every store change.
refresh()always allocates a newslideIdsarray, so typing into a text box still rerendersMobileSlidesViewand the full thumbnail strip even when the slide list did not change. Guard the state update so only structural slide-list changes trigger React work.♻️ Suggested change
useEffect(() => { if (!store) return; const refresh = () => { - const r = store.read(); - setSnapshot({ slideIds: r.slides.map((s) => s.id) }); + const nextSlideIds = store.read().slides.map((slide) => slide.id); + setSnapshot((prev) => + prev.slideIds.length === nextSlideIds.length && + prev.slideIds.every((id, index) => id === nextSlideIds[index]) + ? prev + : { slideIds: nextSlideIds }, + ); }; refresh(); return store.onChange(refresh); }, [store]);🤖 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/mobile-slides-view.tsx` around lines 133 - 141, The effect's refresh callback always allocates a new slideIds array and calls setSnapshot even when the logical slide list hasn't changed; modify the refresh function (the one created inside the useEffect that calls store.read() and setSnapshot) to compute the new slideIds (r.slides.map(s => s.id)) and compare it to the current snapshot.slideIds (or previous value) for equality (same length and same ids in order) and only call setSnapshot when they differ, so typing or other non-structural store changes won't trigger a re-render of MobileSlidesView or the thumbnail strip.
🤖 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/mobile-slides-view.tsx`:
- Around line 462-468: The ThumbnailStrip is being given
onAddSlide={addBlankSlide} even when the surrounding component is in read-only
mode (mode="view"), allowing mutations; update the render in
mobile-slides-view.tsx so that ThumbnailStrip only receives the onAddSlide prop
when the component is not in view/read-only mode (e.g., conditionally pass
onAddSlide={addBlankSlide} or omit the prop based on mode), leaving other props
(store, slideIds, currentSlideId, onSelectSlide) unchanged.
In `@packages/frontend/src/app/slides/slides-detail.tsx`:
- Around line 527-545: handleImagePick currently captures slideId before opening
the OS file picker which lets the user change slides while the picker is open;
fix by resolving the target slide inside the input.onchange handler (call
editor.getCurrentSlideId() there and validate it) and then call
insertImageOnSlide with that fresh slideId; keep the existing checks for
store/workspaceId and the existing error handling (console.error/toast.error)
and ensure uploadFn is still passed to insertImageOnSlide.
---
Nitpick comments:
In `@packages/frontend/src/app/slides/line-picker.tsx`:
- Around line 121-123: The prop type for "trigger" is too broad—change its type
from React.ReactNode to a single element type compatible with
DropdownMenuTrigger's asChild (e.g. React.ReactElement or
React.ReactElement<any, any>) so the component only accepts one element that can
receive forwarded props/refs; update the "trigger?: React.ReactNode" declaration
(and the identical declaration around lines 159-161) to "trigger?:
React.ReactElement" (or React.ReactElement<any, any>) and ensure any call sites
pass a single element (like SheetActionButton) rather than fragments/strings.
In `@packages/frontend/src/app/slides/mobile-slides-view.tsx`:
- Around line 133-141: The effect's refresh callback always allocates a new
slideIds array and calls setSnapshot even when the logical slide list hasn't
changed; modify the refresh function (the one created inside the useEffect that
calls store.read() and setSnapshot) to compute the new slideIds (r.slides.map(s
=> s.id)) and compare it to the current snapshot.slideIds (or previous value)
for equality (same length and same ids in order) and only call setSnapshot when
they differ, so typing or other non-structural store changes won't trigger a
re-render of MobileSlidesView or the thumbnail strip.
In `@packages/frontend/src/app/slides/shape-picker.tsx`:
- Around line 73-77: The prop type for trigger is too permissive
(React.ReactNode) for Radix's DropdownMenuTrigger asChild — change the trigger
prop declaration(s) in shape-picker.tsx (the `trigger` prop at the earlier
definition and the similar one at lines ~116-118) to accept a single React
element (e.g., React.ReactElement or React.ReactElement<any>) instead of
React.ReactNode so callers cannot pass invalid/multiple children; update the
prop type annotations and any related usages to accept/forward a single React
element compatible with asChild.
🪄 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: c408b67a-4a7c-4e90-aaea-9f6f33ac70fd
⛔ Files ignored due to path filters (72)
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.shapes-action-buttons.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-action-buttons.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-p3b-arrows.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-p3b-arrows.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-p3b-basics.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-p3b-basics.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-pilot.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-pilot.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-sweep.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.shapes-adjustments-sweep.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-callout-tail.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-callout-tail.mobile.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-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-donut-evenodd.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-donut-evenodd.mobile.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-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-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-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-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-shapes-catalog-dark.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-shapes-catalog-dark.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-shapes-catalog-light.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-shapes-catalog-light.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-shapes-catalog-material.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-canvas-shapes-catalog-material.mobile.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-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-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-toolbar-idle.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-idle.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-idle.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-idle.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-image-selected.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-image-selected.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-image-selected.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-image-selected.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-multi-select.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-multi-select.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-multi-select.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-multi-select.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-shape-selected.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-shape-selected.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-shape-selected.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-shape-selected.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-text-editing.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-text-editing.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-text-editing.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-text-editing.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-text-element-selected.desktop.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-text-element-selected.mobile.dark.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-text-element-selected.mobile.pngis excluded by!**/*.pngpackages/frontend/tests/visual/baselines/harness-visual.browser.slides-toolbar-text-element-selected.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 (12)
docs/tasks/README.mddocs/tasks/active/20260519-slides-mobile-shell-todo.mddocs/tasks/active/20260519-slides-mobile-toolbar-morph-todo.mddocs/tasks/archive/README.mdpackages/frontend/src/app/slides/line-picker.tsxpackages/frontend/src/app/slides/mobile-slides-view.tsxpackages/frontend/src/app/slides/shape-picker.tsxpackages/frontend/src/app/slides/slides-detail.tsxpackages/frontend/src/app/slides/toolbar/index.tsxpackages/frontend/src/app/slides/toolbar/mobile-toolbar.tsxpackages/frontend/src/app/slides/toolbar/slide-group.tsxpackages/slides/src/view/editor/editor.ts
Two correctness fixes + three typing/perf nitpicks raised on #268: - Pass `onAddSlide` to ThumbnailStrip only when `mode === "edit"` so the documented read-only `mode="view"` shell can't mutate the deck by appending a slide. - Resolve the target slide inside the file picker's `onchange` handler instead of capturing it before `input.click()`. A user swiping to a different slide while the native picker is open would otherwise drop the image into the slide that was current at open-time. - Tighten `ShapePicker`/`LinePicker` `trigger` prop from `ReactNode` to `ReactElement`. Radix's `asChild` slot needs a single element that can receive forwarded refs and handlers, and `ReactNode` would let callers pass fragments or text the slot can't bind to. - Guard the mobile-shell `setSnapshot` call with a shallow slide-id array equality check. `store.onChange` fires on every keystroke during text editing; the previous `refresh` always allocated a fresh array and triggered a React rerender of `MobileSlidesView` even when the slide list was unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Captures lessons from PR #269 (diagnosis-by-instrumentation, rotation-preserving refit math, ghost-pattern reuse, DOM ownership across renderOverlay rebuilds) and archives the three May 19 slides tasks (mobile-shell, mobile-toolbar-morph, precise-shape-hit-test) that shipped in PRs #266 / #268 but stayed in `active/` until now.
Summary
MobileSlidesViewwith the docs/sheets shell (SidebarProvider + SiteHeader + SlidesToolbar); addMobileSlidesLayoutinslides-detail.tsxparallel to the existingDesktopSlidesLayout.ThumbnailStripwithrenderThumbnailmini-canvases (120 ms debounced repaint, auto-scrollIntoViewon active,+cell at end appends a blank slide and jumps to it).idle/object/text-editvia the existinggetToolbarStatestate machine (hoisted inSlidesToolbar). Bottom sheets surface contextualShapeControls/ImageControls/TextElementControls/TextStyleGroup/TextFormatGroup/TextParagraphGroup— no parallel mobile API.ShapePickerandLinePickergain an optionaltrigger?: ReactNodeprop so the mobile Insert sheet's Shape/Line entries match Text box and Image visually. Desktop trigger path is byte-identical.SlideGroup's primary button (icon-only+) and remove the entireSlideGroupfrom the mobile toolbar — add-slide now lives in the thumbnail strip.activeThemesubscription intoMobileSlidesLayoutso the Format sheet's theme-bound pickers (shape fill, text color, font family) actually render swatches.Why
The previous mobile slides UI was read-mostly: no insert, no format, no arrange, no thumbnails — just left/right swipe nav. Phone users couldn't add a text box, change a shape's fill, undo a change, or even see which slide they were on relative to the deck. This brings mobile up to feature parity with desktop for the common edit paths while keeping the desktop code path byte-identical (state machine, controls, and ShapePicker/LinePicker defaults all unchanged).
Out of scope (tracked separately)
ThemePanelcontainer (overflow Theme item still toggles the desktop side panel).editor.deleteSelected()/editor.duplicateSelected()— mobile trash button callsstore.removeElementsdirectly.useSlidesShellStatehook to dedupeMobileSlidesLayoutvsDesktopSlidesLayout(~200 lines of parallel state plumbing).Tracked in:
docs/tasks/active/20260519-slides-mobile-shell-todo.mddocs/tasks/active/20260519-slides-mobile-toolbar-morph-todo.mdTest plan
+cell at end appends a blank slide and jumps to it⋮→ Theme… opens panel (currently desktop side-panel — acceptable per scope notes)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
UI Changes