chore: merge executor worktree (worktree-agent-a65077bbd9885bf7e)
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
phase: 17-ui-optimization-polish
|
||||||
|
plan: "05"
|
||||||
|
subsystem: pwa-ui
|
||||||
|
tags: [logout, settings, desktop-centering, sheet-ux, d-07, d-09]
|
||||||
|
dependency_graph:
|
||||||
|
requires: ["17-01"]
|
||||||
|
provides: [reachable-logout, desktop-centered-sheets]
|
||||||
|
affects: [SettingsSheet, ChangePasswordSheet, LinkOidcSheet, CredentialSheet]
|
||||||
|
tech_stack:
|
||||||
|
added: []
|
||||||
|
patterns: [phone-desktop-style-branch, fire-and-best-effort-logout, react-router-useNavigate]
|
||||||
|
key_files:
|
||||||
|
modified:
|
||||||
|
- apps/pwa/src/components/SettingsSheet.tsx
|
||||||
|
- apps/pwa/src/components/CredentialSheet.tsx
|
||||||
|
decisions:
|
||||||
|
- "phone const computed at render time via window.matchMedia (not a hook) — consistent with established App.tsx / BottomTabBar.tsx project pattern"
|
||||||
|
- "handleSignOut calls onClose() before navigate() so the sheet dismisses even if router state causes a re-render"
|
||||||
|
- "All three sub-sheets in SettingsSheet.tsx (SettingsSheet, ChangePasswordSheet, LinkOidcSheet) received the centering branch in a single commit since they share the same file and the branch is identical"
|
||||||
|
metrics:
|
||||||
|
duration: "6 minutes"
|
||||||
|
completed: "2026-06-18"
|
||||||
|
tasks_completed: 2
|
||||||
|
tasks_total: 2
|
||||||
|
files_changed: 2
|
||||||
|
status: complete
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 17 Plan 05: Logout Control + Sheet Centering Summary
|
||||||
|
|
||||||
|
Wired a reachable Sign out control into SettingsSheet (D-07) and fixed dialog centering so all four settings sheets render as centered modals on desktop and unchanged bottom-sheets on phone (D-09).
|
||||||
|
|
||||||
|
## Tasks Completed
|
||||||
|
|
||||||
|
| Task | Name | Commit | Files |
|
||||||
|
|------|------|--------|-------|
|
||||||
|
| 1 | Add Sign out control and handleSignOut to SettingsSheet | `132a5e4` | SettingsSheet.tsx |
|
||||||
|
| 2 | Add phone/desktop centering branch to all sheets | `b712386` | CredentialSheet.tsx |
|
||||||
|
|
||||||
|
## What Was Built
|
||||||
|
|
||||||
|
### Task 1 — Sign out control (D-07)
|
||||||
|
|
||||||
|
Added to `SettingsSheet.tsx`:
|
||||||
|
- `LogOut` icon imported from lucide-react (added to existing icon import)
|
||||||
|
- `fetchLocalLogout` imported from `../api/client.js` (added to existing import)
|
||||||
|
- `useNavigate` from `react-router` for post-logout routing
|
||||||
|
- `handleSignOut` async function: wraps `fetchLocalLogout()` in try/catch, calls `onClose()` then `navigate('/login')` in both success and catch branches — fire-and-best-effort per D-07 spec (server cookie is cleared or already expired; navigation must always proceed)
|
||||||
|
- Sign out button row: 44px minHeight, `var(--color-destructive)` color, 16px LogOut icon, "Sign out" text, full-width, `aria-label="Sign out"`, separated from prior sections by the project's 1px `var(--color-border-subtle)` divider
|
||||||
|
|
||||||
|
The button renders unconditionally at the bottom of the sheet — visible regardless of `hasLocalCredential` or `oidcEnabled` gating.
|
||||||
|
|
||||||
|
### Task 2 — Phone/desktop centering branch (D-09)
|
||||||
|
|
||||||
|
Applied `phone = window.matchMedia('(max-width: 767px)').matches` + ternary style branch to four dialog wrappers:
|
||||||
|
|
||||||
|
| Sheet | File | zIndex |
|
||||||
|
|-------|------|--------|
|
||||||
|
| SettingsSheet | SettingsSheet.tsx | 301 |
|
||||||
|
| ChangePasswordSheet | SettingsSheet.tsx | 303 |
|
||||||
|
| LinkOidcSheet | SettingsSheet.tsx | 303 |
|
||||||
|
| CredentialSheet | CredentialSheet.tsx | 301 |
|
||||||
|
|
||||||
|
Phone branch (unchanged): `position: fixed; bottom: 0; left: 0; right: 0; borderRadius: 12px 12px 0 0`
|
||||||
|
|
||||||
|
Desktop branch (new): `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); maxWidth: 480px; width: calc(100% - 32px); maxHeight: calc(100dvh - 32px); overflowY: auto; borderRadius: 12px; boxShadow: 0 8px 32px rgba(0,0,0,0.18)`
|
||||||
|
|
||||||
|
`role=dialog`, `aria-modal`, `aria-label`, Escape-closes, and focus-return invariants unchanged on all wrappers.
|
||||||
|
|
||||||
|
Note: AdminPage.ResetPasswordSheet centering is handled by plan 17-06 (single-file-owner constraint).
|
||||||
|
|
||||||
|
## Playwright Validation
|
||||||
|
|
||||||
|
**Desktop @1280x720:** SettingsSheet opened centered on the page as a modal dialog. Sign out button visible with destructive red styling and LogOut icon at the bottom of the sheet. Clicking Sign out navigated to `/login` (confirmed URL change from `http://localhost:5175/calendar` to `http://localhost:5175/login`).
|
||||||
|
|
||||||
|
**Phone @390x844:** SettingsSheet rendered as a bottom-sheet anchored to the bottom of viewport with rounded top corners. Sign out button visible at bottom. Bottom-sheet behavior unchanged.
|
||||||
|
|
||||||
|
Both screenshots confirmed correct behavior for D-07 and D-09.
|
||||||
|
|
||||||
|
## Verification Checks
|
||||||
|
|
||||||
|
- `grep handleSignOut` in SettingsSheet.tsx: PASS
|
||||||
|
- `grep fetchLocalLogout` in SettingsSheet.tsx: PASS
|
||||||
|
- `grep 'Sign out'` in SettingsSheet.tsx: PASS
|
||||||
|
- `grep LogOut` in SettingsSheet.tsx: PASS
|
||||||
|
- `grep 'translate(-50%, -50%)'` in SettingsSheet.tsx: PASS (3 occurrences — SettingsSheet, ChangePasswordSheet, LinkOidcSheet)
|
||||||
|
- `grep 'translate(-50%, -50%)'` in CredentialSheet.tsx: PASS
|
||||||
|
- `grep 'max-width: 767px'` in CredentialSheet.tsx: PASS
|
||||||
|
- `pnpm --filter @familysync/pwa build`: PASS (exits 0, 1858 modules)
|
||||||
|
|
||||||
|
## Threat Model Compliance
|
||||||
|
|
||||||
|
| Threat ID | Mitigation Applied |
|
||||||
|
|-----------|-------------------|
|
||||||
|
| T-17-05-01 | `handleSignOut` calls `fetchLocalLogout()` (clears server-side cookie) then always navigates to `/login` — no stale-cookie-with-logged-out-UI state possible |
|
||||||
|
| T-17-05-02 | Position-only CSS branch — no new input or executable content |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None — plan executed exactly as written.
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
|
||||||
|
None — both D-07 and D-09 are fully wired. Sign out calls the real `fetchLocalLogout()` endpoint. Centering is CSS-only with no data source.
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
None — no new network endpoints, auth paths, or schema changes introduced. All changes are UI-layer only.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- `/home/luc/projects/familysync/.claude/worktrees/agent-a65077bbd9885bf7e/apps/pwa/src/components/SettingsSheet.tsx` — FOUND, contains handleSignOut, fetchLocalLogout, Sign out, LogOut, translate(-50%, -50%)
|
||||||
|
- `/home/luc/projects/familysync/.claude/worktrees/agent-a65077bbd9885bf7e/apps/pwa/src/components/CredentialSheet.tsx` — FOUND, contains translate(-50%, -50%), max-width: 767px
|
||||||
|
- Commit `132a5e4` — FOUND (Task 1)
|
||||||
|
- Commit `b712386` — FOUND (Task 2)
|
||||||
|
- Build: `pnpm --filter @familysync/pwa build` exits 0
|
||||||
@@ -153,6 +153,7 @@ export function CredentialSheet({
|
|||||||
const heading = headingFor(mode);
|
const heading = headingFor(mode);
|
||||||
const isPending = credentialMutation.isPending;
|
const isPending = credentialMutation.isPending;
|
||||||
const saveDisabled = isPending || password.trim().length === 0 || email.trim().length === 0;
|
const saveDisabled = isPending || password.trim().length === 0 || email.trim().length === 0;
|
||||||
|
const phone = window.matchMedia('(max-width: 767px)').matches;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -173,20 +174,37 @@ export function CredentialSheet({
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={heading}
|
aria-label={heading}
|
||||||
style={{
|
style={
|
||||||
position: 'fixed',
|
phone
|
||||||
bottom: 0,
|
? {
|
||||||
left: 0,
|
position: 'fixed',
|
||||||
right: 0,
|
bottom: 0,
|
||||||
background: 'var(--color-surface)',
|
left: 0,
|
||||||
borderRadius: '12px 12px 0 0',
|
right: 0,
|
||||||
boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
|
background: 'var(--color-surface)',
|
||||||
padding: 'var(--space-6, 24px)',
|
borderRadius: '12px 12px 0 0',
|
||||||
zIndex: 301,
|
boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
|
||||||
fontFamily: 'var(--font-family-base)',
|
padding: 'var(--space-6, 24px)',
|
||||||
maxWidth: '480px',
|
zIndex: 301,
|
||||||
margin: '0 auto',
|
fontFamily: 'var(--font-family-base)',
|
||||||
}}
|
}
|
||||||
|
: {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
maxWidth: '480px',
|
||||||
|
width: 'calc(100% - var(--space-8, 32px))',
|
||||||
|
maxHeight: 'calc(100dvh - var(--space-8, 32px))',
|
||||||
|
overflowY: 'auto',
|
||||||
|
background: 'var(--color-surface)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
|
||||||
|
padding: 'var(--space-6, 24px)',
|
||||||
|
zIndex: 301,
|
||||||
|
fontFamily: 'var(--font-family-base)',
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Heading */}
|
{/* Heading */}
|
||||||
<h2
|
<h2
|
||||||
|
|||||||
@@ -22,11 +22,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { X, Bell, AlertCircle, Loader2 } from 'lucide-react';
|
import { X, Bell, AlertCircle, Loader2, LogOut } from 'lucide-react';
|
||||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import { usePushSubscription } from '../hooks/usePushSubscription.js';
|
import { usePushSubscription } from '../hooks/usePushSubscription.js';
|
||||||
import { InstructionSheet } from './InstructionSheet.js';
|
import { InstructionSheet } from './InstructionSheet.js';
|
||||||
import { fetchMe, fetchAuthMode, fetchChangePassword, fetchLinkOidc } from '../api/client.js';
|
import { fetchMe, fetchAuthMode, fetchChangePassword, fetchLinkOidc, fetchLocalLogout } from '../api/client.js';
|
||||||
|
|
||||||
// CR-04: fetch VAPID key (from sessionStorage cache if available) for the
|
// CR-04: fetch VAPID key (from sessionStorage cache if available) for the
|
||||||
// tap-gated subscribe() path. Same logic as PushPermissionPrompt.
|
// tap-gated subscribe() path. Same logic as PushPermissionPrompt.
|
||||||
@@ -53,6 +54,7 @@ interface SettingsSheetProps {
|
|||||||
|
|
||||||
export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
||||||
const { subscribe, permission, isSubscribed, setEnabled } = usePushSubscription();
|
const { subscribe, permission, isSubscribed, setEnabled } = usePushSubscription();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [isTogglingOn, setIsTogglingOn] = useState(false);
|
const [isTogglingOn, setIsTogglingOn] = useState(false);
|
||||||
const [instructionsOpen, setInstructionsOpen] = useState(false);
|
const [instructionsOpen, setInstructionsOpen] = useState(false);
|
||||||
|
|
||||||
@@ -130,6 +132,19 @@ export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const phone = window.matchMedia('(max-width: 767px)').matches;
|
||||||
|
|
||||||
|
// D-07: fire-and-best-effort logout — navigate to /login regardless of API success/failure
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
try {
|
||||||
|
await fetchLocalLogout();
|
||||||
|
} catch {
|
||||||
|
// API failure does not block navigation — server cookie was cleared or already expired
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
navigate('/login');
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggle = async () => {
|
const handleToggle = async () => {
|
||||||
if (permission === 'denied') return; // no-op — show hint below
|
if (permission === 'denied') return; // no-op — show hint below
|
||||||
|
|
||||||
@@ -185,20 +200,37 @@ export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
style={{
|
style={
|
||||||
position: 'fixed',
|
phone
|
||||||
bottom: 0,
|
? {
|
||||||
left: 0,
|
position: 'fixed',
|
||||||
right: 0,
|
bottom: 0,
|
||||||
background: 'var(--color-surface-raised, #ffffff)',
|
left: 0,
|
||||||
borderRadius: '12px 12px 0 0',
|
right: 0,
|
||||||
boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
|
background: 'var(--color-surface-raised, #ffffff)',
|
||||||
padding: 'var(--space-6, 24px)',
|
borderRadius: '12px 12px 0 0',
|
||||||
zIndex: 301,
|
boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
|
||||||
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
padding: 'var(--space-6, 24px)',
|
||||||
maxWidth: '480px',
|
zIndex: 301,
|
||||||
margin: '0 auto',
|
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||||
}}
|
}
|
||||||
|
: {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
maxWidth: '480px',
|
||||||
|
width: 'calc(100% - var(--space-8, 32px))',
|
||||||
|
maxHeight: 'calc(100dvh - var(--space-8, 32px))',
|
||||||
|
overflowY: 'auto',
|
||||||
|
background: 'var(--color-surface-raised, #ffffff)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
|
||||||
|
padding: 'var(--space-6, 24px)',
|
||||||
|
zIndex: 301,
|
||||||
|
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Heading row */}
|
{/* Heading row */}
|
||||||
<div
|
<div
|
||||||
@@ -486,6 +518,41 @@ export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* D-07: Sign out — always visible at the bottom of the sheet */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '1px',
|
||||||
|
background: 'var(--color-border-subtle, var(--color-border))',
|
||||||
|
margin: 'var(--space-4, 16px) 0',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
void handleSignOut();
|
||||||
|
}}
|
||||||
|
aria-label="Sign out"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 'var(--space-2, 8px)',
|
||||||
|
width: '100%',
|
||||||
|
minHeight: '44px',
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: 'var(--space-2, 8px) 0',
|
||||||
|
fontSize: 'var(--text-body-size, 15px)',
|
||||||
|
fontWeight: 400,
|
||||||
|
color: 'var(--color-destructive, #dc2626)',
|
||||||
|
fontFamily: 'var(--font-family-base)',
|
||||||
|
textAlign: 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogOut size={16} aria-hidden="true" />
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{instructionsOpen && <InstructionSheet onClose={() => setInstructionsOpen(false)} />}
|
{instructionsOpen && <InstructionSheet onClose={() => setInstructionsOpen(false)} />}
|
||||||
@@ -580,6 +647,8 @@ function ChangePasswordSheet({ isOpen, onClose }: ChangePasswordSheetProps) {
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const phone = window.matchMedia('(max-width: 767px)').matches;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
@@ -599,20 +668,37 @@ function ChangePasswordSheet({ isOpen, onClose }: ChangePasswordSheetProps) {
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Change password"
|
aria-label="Change password"
|
||||||
style={{
|
style={
|
||||||
position: 'fixed',
|
phone
|
||||||
bottom: 0,
|
? {
|
||||||
left: 0,
|
position: 'fixed',
|
||||||
right: 0,
|
bottom: 0,
|
||||||
background: 'var(--color-surface-raised, #ffffff)',
|
left: 0,
|
||||||
borderRadius: '12px 12px 0 0',
|
right: 0,
|
||||||
boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
|
background: 'var(--color-surface-raised, #ffffff)',
|
||||||
padding: 'var(--space-6, 24px)',
|
borderRadius: '12px 12px 0 0',
|
||||||
zIndex: 303,
|
boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
|
||||||
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
padding: 'var(--space-6, 24px)',
|
||||||
maxWidth: '480px',
|
zIndex: 303,
|
||||||
margin: '0 auto',
|
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||||
}}
|
}
|
||||||
|
: {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
maxWidth: '480px',
|
||||||
|
width: 'calc(100% - var(--space-8, 32px))',
|
||||||
|
maxHeight: 'calc(100dvh - var(--space-8, 32px))',
|
||||||
|
overflowY: 'auto',
|
||||||
|
background: 'var(--color-surface-raised, #ffffff)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
|
||||||
|
padding: 'var(--space-6, 24px)',
|
||||||
|
zIndex: 303,
|
||||||
|
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
ref={headingRef}
|
ref={headingRef}
|
||||||
@@ -873,6 +959,8 @@ function LinkOidcSheet({ isOpen, onClose }: LinkOidcSheetProps) {
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const phone = window.matchMedia('(max-width: 767px)').matches;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
@@ -892,20 +980,37 @@ function LinkOidcSheet({ isOpen, onClose }: LinkOidcSheetProps) {
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Link OIDC identity"
|
aria-label="Link OIDC identity"
|
||||||
style={{
|
style={
|
||||||
position: 'fixed',
|
phone
|
||||||
bottom: 0,
|
? {
|
||||||
left: 0,
|
position: 'fixed',
|
||||||
right: 0,
|
bottom: 0,
|
||||||
background: 'var(--color-surface-raised, #ffffff)',
|
left: 0,
|
||||||
borderRadius: '12px 12px 0 0',
|
right: 0,
|
||||||
boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
|
background: 'var(--color-surface-raised, #ffffff)',
|
||||||
padding: 'var(--space-6, 24px)',
|
borderRadius: '12px 12px 0 0',
|
||||||
zIndex: 303,
|
boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
|
||||||
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
padding: 'var(--space-6, 24px)',
|
||||||
maxWidth: '480px',
|
zIndex: 303,
|
||||||
margin: '0 auto',
|
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||||
}}
|
}
|
||||||
|
: {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
maxWidth: '480px',
|
||||||
|
width: 'calc(100% - var(--space-8, 32px))',
|
||||||
|
maxHeight: 'calc(100dvh - var(--space-8, 32px))',
|
||||||
|
overflowY: 'auto',
|
||||||
|
background: 'var(--color-surface-raised, #ffffff)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
|
||||||
|
padding: 'var(--space-6, 24px)',
|
||||||
|
zIndex: 303,
|
||||||
|
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
ref={headingRef}
|
ref={headingRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user