Files
backnotprop__plannotator/packages/ui/components/PermissionModeSetup.tsx
Michael Ramos 8e88dcec8c fix: v0.27.2 pre-release QA batch (mobile TOC, dialog bounds, seed guard) (#1311)
* fix(plan): make the compact TOC scroll the document again

The compact navigator overlay rendered outside App's ScrollViewportProvider,
so the TableOfContents it hosts resolved a null viewport and every "jump to
heading" tap was a silent no-op on phones. The provider is context-only, so
hoisting it above the overlay fixes the lookup without touching desktop DOM
structure or order.

* fix(plan): scope the permission-mode chooser to plan review and bound its card

The one-time chooser fired in every non-goal-setup Claude Code session, so
annotate, annotate-last, annotate-folder and archive reviewers got a blocking
dialog about what happens after plan approval. Gate it on plan review, which
is the absence of a mode field in the /api/plan payload.

The card itself was hand-rolled with no height cap and no internal scroll, so
on a short landscape phone it overflowed both edges of a modal that has no
dismiss control. Give it the same bounded shell the sibling one-time dialogs
use: safe-area padding, a visible-viewport max height, and the option list as
the only scrolling region. Content and cookie behavior are unchanged.

* fix(review): never seed Tree over a persisted panel view

The first-run initializer gated only on the setup-seen cookie, but sessions
that never reach it (non-git, workspace, PR, no since-base) still let Settings
persist a panel view. A reviewer could hold an explicit Git status choice with
"seen" unset, and the next plain git session seeded Tree over it. Treat a
persisted view as the decision: consume the one-time setup and write nothing.

* fix(comments): give the geometry-forced composer a working Escape

When the anchor has no room the position tracker forces dialog mode. On a
fine-pointer viewport Escape took the collapse branch, the tracker instantly
re-forced the dialog, and the keystroke was eaten; the Collapse button bounced
the same way. Track forced expansion separately from the preferred kind: in
that state Escape closes (draft-preserving) and Collapse is hidden, because
collapsing is geometrically impossible.

* fix(review): stop the compact Editor tab editing the desktop diff style

The dock's Split/Unified control returns null under the compact touch layout,
but the Settings copy of it kept rendering while the phone showed the
session-only unified diff. It looked dead and silently rewrote the persisted
desktop preference. Hide it on compact and state what the session is doing;
the prop defaults to false, so the plan editor and desktop are untouched.

* fix(portal): give the share portal the mobile app shell

The portal mounts the same plan editor App as the hook but kept the pre-mobile
entry document: no viewport-fit=cover (so every safe-area token was inert) and
a min-h-screen body without the shell's scroll ownership. Mirror the hook's
body class, root class, and viewport meta, and extend the entry-asset pin to
cover the portal alongside them.

* fix(plan): keep compact overlays out of the printed document

The compact plan stage and the compact navigator are full-viewport transient
surfaces with no print-hide marker, so printing on a touch device with
Annotations, Ask AI, Versions or Archive open clipped the document behind
them. Mark both with data-print-hide, which print.css already hides. The
desktop rail is untouched.

* fix(plan): give the selection toolbar real touch targets

Copy / Delete / Comment / quick label / looks-good / Cancel measured 28x28
with 2px gaps on a phone because the toolbar never got the touch-target
markers the rest of the stack uses. Stamp them on its buttons and add a
compact-scoped gap so adjacent destructive and comment actions are not a
mis-tap apart. Both are inert outside the compact scope, so desktop geometry
is unchanged.

* docs: keep the new QA-batch comments free of em dashes
2026-08-13 11:35:00 -07:00

112 lines
4.5 KiB
TypeScript

import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import {
PermissionMode,
PERMISSION_MODE_OPTIONS,
savePermissionModeSettings,
} from '../utils/permissionMode';
interface PermissionModeSetupProps {
isOpen: boolean;
onComplete: (mode: PermissionMode) => void;
}
export const PermissionModeSetup: React.FC<PermissionModeSetupProps> = ({
isOpen,
onComplete,
}) => {
const [selectedMode, setSelectedMode] = useState<PermissionMode>('acceptEdits');
if (!isOpen) return null;
const handleConfirm = () => {
savePermissionModeSettings(selectedMode);
onComplete(selectedMode);
};
return createPortal(
// Same bounded shell as the sibling one-time dialogs (see
// LookAndFeelAnnouncementDialog): safe-area padding, a card capped to the
// visible viewport, and the option list as the only scrolling region. The
// hand-rolled unbounded card used to overflow both edges of a short
// landscape phone with nothing left to tap.
<div
className="fixed inset-0 z-[100] flex items-center justify-center overflow-y-auto bg-background/90 backdrop-blur-sm"
style={{
paddingTop: 'max(1rem, var(--pn-safe-top, 0px))',
paddingRight: 'max(1rem, var(--pn-safe-right, 0px))',
paddingBottom: 'max(1rem, var(--pn-safe-bottom, 0px))',
paddingLeft: 'max(1rem, var(--pn-safe-left, 0px))',
}}
>
<div
className="bg-card border border-border rounded-xl w-full max-w-lg shadow-2xl flex flex-col overflow-hidden"
style={{
maxHeight:
'calc(var(--pn-viewport-height, 100vh) - max(1rem, var(--pn-safe-top, 0px)) - max(1rem, var(--pn-safe-bottom, 0px)))',
}}
>
{/* Header */}
<div className="p-5 border-b border-border flex-shrink-0">
<div className="flex items-center gap-2 mb-2">
<div className="p-1.5 rounded-lg bg-primary/15">
<svg className="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</svg>
</div>
<h3 className="font-semibold text-base">New: Permission Mode Preservation</h3>
</div>
<p className="text-sm text-muted-foreground">
Claude Code 2.1.7+ supports preserving your permission mode after plan approval.
Choose your preferred automation level.
</p>
<p className="text-xs text-muted-foreground/70 mt-1">
Requires Claude Code 2.1.7 or later. Run <code className="bg-muted px-1 rounded">claude update</code> to update.
</p>
</div>
{/* Options */}
<div className="min-h-0 flex-1 overflow-y-auto p-4 space-y-2">
{PERMISSION_MODE_OPTIONS.map((option) => (
<label
key={option.value}
className={`flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-all border ${
selectedMode === option.value
? 'border-primary bg-primary/10'
: 'border-transparent bg-muted/50 hover:bg-muted'
}`}
>
<input
type="radio"
name="permissionMode"
value={option.value}
checked={selectedMode === option.value}
onChange={() => setSelectedMode(option.value)}
className="mt-0.5 accent-primary"
/>
<div className="flex-1">
<div className="text-sm font-medium">{option.label}</div>
<div className="text-xs text-muted-foreground">{option.description}</div>
</div>
</label>
))}
</div>
{/* Footer */}
<div className="p-4 border-t border-border flex flex-shrink-0 flex-wrap gap-2 justify-between items-center">
<p className="text-xs text-muted-foreground">
You can change this later in Settings.
</p>
<button
onClick={handleConfirm}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:opacity-90 transition-opacity"
>
Continue
</button>
</div>
</div>
</div>,
document.body
);
};