refactor(views): extract layout state + decompose ManuscriptView#179
Conversation
β¦ering P3 of the architecture refactor. ManuscriptView: split ManuscriptViewUI into ManuscriptMobileLayout (header + full-bleed editor + drawers) and ManuscriptDesktopLayout (3-pane navigator Β· editor+research Β· inspector). Shared UI state (nav tab, mobile drawers, focus mode, resizable panels, research-split Escape handler) moves into hooks/useManuscriptLayout.ts; the view is now a thin orchestrator that renders both responsive layouts. WriterViewUI: extract local state (mobile tab, panel collapse, focus mode, flow-mode Escape handler, swipe-to-switch wiring) into hooks/useWriterLayout.ts; rendering unchanged. No behavior change β responsive show/hide, ARIA tablists, drawers, and keyboard handlers are preserved. Verified: typecheck, lint (0 warnings), 38 tests pass (ManuscriptView, WriterView, useWriterView). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
ManuscriptViewUI mounted both ManuscriptMobileLayout and ManuscriptDesktopLayout unconditionally, so two ManuscriptEditor trees ran at once (duplicate voice/window effects) and a mobile drawer could stay open over the desktop layout on resize. Add `hooks/useMediaQuery.ts` and render only the layout matching `(min-width: 768px)` (the `md` breakpoint the layouts already use) β one editor mounts; switching the breakpoint unmounts the other layout (and its drawers). Manuscript content lives in Redux so the remount is state-safe. Tests: useMediaQuery (initial + change); ManuscriptView suite still green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
|
@CodeAnt-AI review |
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
Sequence DiagramThis PR refactors ManuscriptView so shared layout state is managed by a hook and only the layout matching the current breakpoint is rendered, ensuring a single ManuscriptEditor tree mounts at any time. sequenceDiagram
participant User
participant ManuscriptView
participant LayoutState
participant MediaQuery
participant DesktopLayout
participant MobileLayout
participant Editor
User->>ManuscriptView: Navigate to manuscript view
ManuscriptView->>LayoutState: Initialize shared layout state
ManuscriptView->>MediaQuery: Evaluate desktop breakpoint
alt Desktop viewport
ManuscriptView->>DesktopLayout: Render desktop layout with project and layout
DesktopLayout->>Editor: Mount editor and research split
else Mobile viewport
ManuscriptView->>MobileLayout: Render mobile layout with project and layout
MobileLayout->>Editor: Mount editor and drawers
end
MediaQuery-->>ManuscriptView: Notify breakpoint change
ManuscriptView->>ManuscriptView: Switch layout and unmount previous editor tree
Generated by CodeAnt AI |
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
|
@CodeAnt-AI review |
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
β¦st fallback (CodeAnt #179) - ManuscriptDesktopLayout focus toggle and useWriterLayout swipe handlers now use functional setState updaters, so rapid clicks/swipes processed before a commit derive from the latest value instead of a stale captured one (no lost/collapsed toggles). - useMediaQuery now falls back to the deprecated addListener/removeListener when MediaQueryList.addEventListener is unavailable (older Safari/WebView <14), where the previous code would throw a TypeError and break layout rendering entirely. New test covers the legacy path (subscribe + react + cleanup). Typecheck + lint clean; useMediaQuery suite 3/3 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
|
@CodeAnt-AI review |
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
Sequence DiagramThis PR refactors Manuscript and Writer views so layout state lives in dedicated hooks and ManuscriptView mounts only the layout that matches the current viewport, ensuring a single editor tree and consistent drawers and focus behavior across mobile and desktop. sequenceDiagram
participant User
participant ManuscriptView
participant MediaQueryHook
participant ManuscriptLayoutHook
participant DesktopLayout
participant MobileLayout
participant WriterViewUI
participant WriterLayoutHook
User->>ManuscriptView: Open manuscript view
ManuscriptView->>ManuscriptLayoutHook: Initialize shared layout state
ManuscriptView->>MediaQueryHook: Evaluate min width 768 query
MediaQueryHook-->>ManuscriptView: Return isDesktop flag
alt Desktop viewport
ManuscriptView->>DesktopLayout: Render with shared layout state
DesktopLayout->>DesktopLayout: Show navigator editor inspector and research split
else Mobile viewport
ManuscriptView->>MobileLayout: Render with shared layout state
MobileLayout->>MobileLayout: Show mobile header editor and drawers
end
User->>WriterViewUI: Open writer view
WriterViewUI->>WriterLayoutHook: Initialize mobile tabs focus and swipe handlers
alt Flow mode active and Escape pressed
WriterLayoutHook-->>WriterViewUI: Toggle flow mode off
end
Generated by CodeAnt AI |
β¦Media fallback (CodeAnt #179 wave-2) - ManuscriptDesktopLayout: the left/right resizers clamped each side to 50% independently, so both could reach 50% and collapse the center editor. Added a combined constraint (each side β€ 80 minus the other's current width) so the editor keeps β₯ 20%. - useWriterLayout: the swipe handlers were inline closures recreated every render, so useSwipeGesture (effect deps include the callbacks) tore down + reattached DOM touch listeners on each rerender (e.g. while writer state streams). Wrapped both in useCallback (functional setState β no deps). - useMediaQuery: when matchMedia is unavailable it returned a permanent false, trapping callers in the mobile layout forever. Now evaluates (min/max-width) against window.innerWidth and updates on resize. New test covers the fallback (initial eval + resize update). Typecheck clean; useMediaQuery 4/4 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
|
@CodeAnt-AI review |
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
Sequence DiagramThis PR refactors ManuscriptView to centralize layout state in a hook and use a media query hook so only the mobile or desktop layout mounts at a time, ensuring a single ManuscriptEditor tree and correct behavior on resize. sequenceDiagram
participant User
participant Browser
participant ManuscriptView
participant MediaQueryHook
participant ManuscriptLayoutHook
participant DesktopLayout
participant MobileLayout
participant ManuscriptEditor
User->>Browser: Open Manuscript view
Browser->>ManuscriptView: Mount ManuscriptView component
ManuscriptView->>MediaQueryHook: Evaluate desktop breakpoint query
MediaQueryHook-->>ManuscriptView: Return isDesktop flag
ManuscriptView->>ManuscriptLayoutHook: Initialize shared layout state
ManuscriptLayoutHook-->>ManuscriptView: Layout state and handlers
alt Desktop viewport
ManuscriptView->>DesktopLayout: Render with project and layout state
DesktopLayout->>ManuscriptEditor: Render editor with panels and research split
else Mobile viewport
ManuscriptView->>MobileLayout: Render with project and layout state
MobileLayout->>ManuscriptEditor: Render editor with mobile header and drawers
end
Generated by CodeAnt AI |
β¦p reset (CodeAnt #179 wave-4) - useResizablePanels (HIGH): the pointer-DRAG handlers clamped each side independently to 15β50%, so a drag could still collapse the center editor (my earlier combined constraint only covered keyboard resize). Track both widths in a ref and clamp left to min(50, 80 - right) / right to min(50, 80 - left), keeping the editor β₯ 20%. - useManuscriptLayout: the two mobile drawers are now mutually exclusive (opening one closes the other) β the shared Drawer applies global body-overflow + focus side effects, so two open at once let a close re-enable scrolling / restore focus while the other was still open. - Reset both mobile drawers when the viewport crosses to desktop, so a drawer opened on mobile doesn't stay logically open and reappear on return. Typecheck clean; useMediaQuery suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
|
@CodeAnt-AI review |
Thanks for using CodeAnt! πWe're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X Β· |
Sequence DiagramThis PR refactors ManuscriptView to centralize layout state and use a media query hook so only the matching mobile or desktop layout renders at a time, ensuring a single ManuscriptEditor tree and preventing mobile drawers from lingering after resize. sequenceDiagram
participant User
participant Browser
participant ManuscriptView
participant LayoutState
participant MediaQuery
participant Editor
User->>ManuscriptView: Open manuscript screen
ManuscriptView->>LayoutState: Initialize manuscript layout state
ManuscriptView->>MediaQuery: Evaluate min width 768 query
MediaQuery-->>ManuscriptView: Return isDesktop flag
ManuscriptView->>Editor: Render matching desktop or mobile layout with one editor tree
User->>Browser: Resize viewport across breakpoint
Browser->>MediaQuery: Notify media query change
MediaQuery-->>ManuscriptView: Return updated isDesktop flag
ManuscriptView->>Editor: Unmount old layout and mount new matching layout
Generated by CodeAnt AI |
| Object.defineProperty(window, 'innerWidth', { value: 1024, configurable: true }); | ||
| const { result } = renderHook(() => useMediaQuery('(min-width: 768px)')); | ||
| expect(result.current).toBe(true); // 1024 >= 768 | ||
| act(() => { | ||
| Object.defineProperty(window, 'innerWidth', { value: 500, configurable: true }); | ||
| window.dispatchEvent(new Event('resize')); | ||
| }); |
There was a problem hiding this comment.
Suggestion: This test mutates the global window.innerWidth value but never restores it, so later tests in the same worker can inherit a stale viewport width and become order-dependent/flaky. Save the original width and restore it at the end of the test (or in afterEach). [code quality]
Severity Level: Major β οΈ
- β οΈ Vitest workers retain mutated window.innerWidth across suites.
- β οΈ Components using innerWidth see unexpected viewport in tests.
- β οΈ Future layout-sensitive tests can become order-dependent and flaky.Steps of Reproduction β
1. In `tests/unit/useMediaQuery.test.ts:92-103`, the `"evaluates innerWidth + tracks
resize when matchMedia is unavailable"` test sets `window.innerWidth` to `1024` and later
to `500` via `Object.defineProperty(window, 'innerWidth', { value: ..., configurable: true
})` at lines 95 and 99, and never restores the original value.
2. The `afterEach` hook for this suite at `tests/unit/useMediaQuery.test.ts:65-66` only
calls `vi.unstubAllGlobals()`, which clears the `matchMedia` stub but leaves the mutated
`window.innerWidth` value (`500`) in place for the rest of the Vitest worker.
3. In the same worker, subsequent tests that render components depending on
`window.innerWidth`βfor example `ManuscriptEditor` at
`components/manuscript/ManuscriptEditor.tsx:12-23` (which computes `isMobile` from
`window.innerWidth < 768`) or logic under test in `hooks/useResizablePanels.ts:31-40`
(which computes panel widths from `window.innerWidth`)βwill run against this stale 500px
viewport unless they explicitly override it themselves.
4. When Vitest schedules `tests/unit/useMediaQuery.test.ts` before suites like
`tests/unit/manuscript/ManuscriptEditor.test.tsx` or other future tests that implicitly
rely on jsdom's default viewport size, those tests will observe different layout decisions
and DOM structures depending on test ordering (isolated vs full run), making the suite
order-dependent and potentially flaky.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent π€
This is a comment left during a code review.
**Path:** tests/unit/useMediaQuery.test.ts
**Line:** 95:101
**Comment:**
*Code Quality: This test mutates the global `window.innerWidth` value but never restores it, so later tests in the same worker can inherit a stale viewport width and become order-dependent/flaky. Save the original width and restore it at the end of the test (or in `afterEach`).
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| const maxLeft = Math.min(50, 80 - widthsRef.current.right); | ||
| if (newWidth > 15 && newWidth < maxLeft) { | ||
| setLeftPanelWidth(newWidth); | ||
| } |
There was a problem hiding this comment.
Suggestion: The left resize handler drops out-of-range pointer positions instead of clamping them, so a fast drag that overshoots the allowed range leaves the panel width stuck at its old value and makes edge resizing feel broken. Clamp newWidth into the valid interval and always apply it while resizing. [logic error]
Severity Level: Major β οΈ
- β οΈ Manuscript desktop navigator resizer ignores fast drags past limit.
- β οΈ Editor layout feels stuck when expanding left panel quickly.Steps of Reproduction β
1. Render the Manuscript view by mounting `ManuscriptView` from
`components/ManuscriptView.tsx:63`, which internally renders `ManuscriptViewUI` and, on
desktop (`useMediaQuery` at `components/ManuscriptView.tsx:22β23`), shows
`ManuscriptDesktopLayout` at `components/ManuscriptView.tsx:55`.
2. In `ManuscriptDesktopLayout`
(`components/manuscript/ManuscriptDesktopLayout.tsx:25β45`), the layout prop comes from
`useManuscriptLayout` (`hooks/useManuscriptLayout.ts:9β65`), which in turn calls
`useResizablePanels(20, 20)` at `hooks/useManuscriptLayout.ts:42`, initializing
`leftPanelWidth` and `rightPanelWidth` to 20% (`hooks/useResizablePanels.ts:15β17`).
3. On the desktop manuscript screen, start dragging the left resizer handle between the
navigator and editor, which is the `Resizer` wired with `onPointerDown={startLeftResize}`
at `components/manuscript/ManuscriptDesktopLayout.tsx:129β140`. This calls
`startLeftResize` (`hooks/useResizablePanels.ts:62β70`), which sets
`isResizingLeft.current = true` and installs a throttled `pointermove` listener that
delegates to `handleLeftResize` in the `useEffect` at
`hooks/useResizablePanels.ts:81β103`.
4. While holding the drag, move the pointer quickly to the far right so that the first
processed `pointermove` has `clientX` corresponding to `newWidth` greater than the allowed
maximum (e.g., >50% when `rightPanelWidth` is 20%, per `maxLeft = Math.min(50, 80 -
widthsRef.current.right)` at `hooks/useResizablePanels.ts:31β32`). In `handleLeftResize`
(`hooks/useResizablePanels.ts:29β35`), the condition `if (newWidth > 15 && newWidth <
maxLeft)` fails for this overshoot value, so `setLeftPanelWidth(newWidth)` is not called
and `leftPanelWidth` remains at the old value. Visually, the left panel does not resize
despite the drag, and only updates again if a later `pointermove` happens to fall back
into the 15βmaxLeft interval, creating the "stuck/jumpy near the edge" behavior the
suggestion describes.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent π€
This is a comment left during a code review.
**Path:** hooks/useResizablePanels.ts
**Line:** 32:35
**Comment:**
*Logic Error: The left resize handler drops out-of-range pointer positions instead of clamping them, so a fast drag that overshoots the allowed range leaves the panel width stuck at its old value and makes edge resizing feel broken. Clamp `newWidth` into the valid interval and always apply it while resizing.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| const maxRight = Math.min(50, 80 - widthsRef.current.left); | ||
| if (newWidth > 15 && newWidth < maxRight) { | ||
| setRightPanelWidth(newWidth); | ||
| } |
There was a problem hiding this comment.
Suggestion: The right resize handler has the same boundary bug: it ignores pointer positions outside the valid range rather than clamping, so dragging past the limit can fail to update the width and makes it hard to reach the constraint edge reliably. Clamp and apply the bounded value instead of skipping the update. [logic error]
Severity Level: Major β οΈ
- β οΈ Manuscript inspector resizer unresponsive when dragging handle past limit.
- β οΈ Users struggle sizing inspector while preserving central editor space.Steps of Reproduction β
1. Render the Manuscript view by mounting `ManuscriptView` from
`components/ManuscriptView.tsx:63`; on desktop (`components/ManuscriptView.tsx:22β23`),
this renders `ManuscriptDesktopLayout` at `components/ManuscriptView.tsx:55` with a
`layout` prop from `useManuscriptLayout` (`hooks/useManuscriptLayout.ts:9β65`), which
internally uses `useResizablePanels(20, 20)` at `hooks/useManuscriptLayout.ts:42`.
2. In `ManuscriptDesktopLayout`, the right inspector pane width is bound to
`rightPanelWidth` in the style prop at
`components/manuscript/ManuscriptDesktopLayout.tsx:179β183`, and the right resize handle
is the `Resizer` with `onPointerDown={startRightResize}` at
`components/manuscript/ManuscriptDesktopLayout.tsx:165β175`, using the same `layout`
object that exposes `startRightResize` and `rightPanelWidth` from `useResizablePanels`
(`hooks/useResizablePanels.ts:111β118`).
3. Begin a drag on the right resizer handle; `onPointerDown` calls `startRightResize`
(`hooks/useResizablePanels.ts:72β79`), which sets `isResizingRight.current = true`,
`activeResize = 'right'`, and registers a throttled `pointermove` listener that routes to
`handleRightResize` via the `useEffect` at `hooks/useResizablePanels.ts:81β103`.
4. While holding the drag, move the pointer quickly towards the left so that the first
processed `pointermove` places the handle beyond the allowed maximum width for the right
panel (computed as `maxRight = Math.min(50, 80 - widthsRef.current.left)` at
`hooks/useResizablePanels.ts:40β41`). In `handleRightResize`
(`hooks/useResizablePanels.ts:38β44`), this makes `newWidth` either below 15 or above
`maxRight`, so the condition `if (newWidth > 15 && newWidth < maxRight)` fails and
`setRightPanelWidth(newWidth)` is skipped, leaving `rightPanelWidth` unchanged and the
inspector pane visually stuck despite the drag. Only when a subsequent `pointermove`
happens to fall back inside the valid interval does the width update, causing inconsistent
and hard-to-hit behavior near the constraint edge as described in the suggestion.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent π€
This is a comment left during a code review.
**Path:** hooks/useResizablePanels.ts
**Line:** 41:44
**Comment:**
*Logic Error: The right resize handler has the same boundary bug: it ignores pointer positions outside the valid range rather than clamping, so dragging past the limit can fail to update the width and makes it hard to reach the constraint edge reliably. Clamp and apply the bounded value instead of skipping the update.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fixβ¦extraction # Conflicts: # graphify-out/GRAPH_REPORT.md
User description
Context
Phase 3 of the architecture refactor β separate layout state from rendering in the two largest view orchestrators, and split ManuscriptView's responsive markup into focused components.
What changed
ManuscriptView
hooks/useManuscriptLayout.ts(new) β owns nav tab, mobile drawers, focus mode, theuseResizablePanelsstate, and the Escape-closes-research-split effect.components/manuscript/ManuscriptMobileLayout.tsx(new) β mobile header + full-bleed editor + nav/inspector drawers.components/manuscript/ManuscriptDesktopLayout.tsx(new) β 3-pane navigator Β· editor+research Β· inspector + focus toggle.ManuscriptView.tsxβ now a thin orchestrator: guards (spinner/empty), then renders both responsive layouts.WriterViewUI
hooks/useWriterLayout.ts(new) β mobile tab, panel collapse, focus mode, flow-mode Escape handler, swipe-to-switch wiring. Component rendering unchanged.Invariants preserved (no behavior change)
Responsive show/hide (
md:classes), ARIA tablists/aria-selected, mobile drawers, container-querycontainerType, min touch targets, and all keyboard handlers are identical. Per-viewport DOM order is unchanged (only one layout is visible at a time).Verification
pnpm run typecheckβpnpm run lint(--error-on-warnings) β β 0 warningspnpm exec vitest runβ β 38 tests pass (ManuscriptView6,WriterView,useWriterView20)π€ Generated with Claude Code
CodeAnt-AI Description
Make manuscript resizing and view switching stay in sync across screen sizes
What Changed
Impact
β Fewer layout glitches after resizingβ Fewer duplicate editor side effectsβ Wider browser support for responsive layoutsπ‘ Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.