Add mobile view-only support
Below md width, the app now forces view-only (same canEdit mechanism as a shared view-only diagram) rather than trying to support touch editing: palette and inspector panels hide, editing controls disable, and the top bar collapses to just Diagrams + Account. A bottom tab bar swaps between a full-screen canvas and a full-screen Bill of Materials, since there's no room for both side by side. Canvas gets a few mobile-specific trims too: MiniMap and the interactivity lock button hide whenever canEdit is false (the lock button doesn't actually grant editing either way — nodesDraggable/ nodesConnectable already override it — so leaving it visible would just be misleading), and h-screen is replaced with h-dvh throughout so the layout doesn't get clipped by a mobile browser's collapsing address bar. Also fixes quick-add's anti-stack offset (24px — far smaller than a device node) to actually tile devices apart instead of leaving them nearly overlapping, and logs a deferred "Auto-arrange" layout feature in organized-ideas.md, surfaced by mobile visitors having no way to fix an overlapping diagram themselves. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
@@ -5,6 +5,7 @@ import { useAdminReviewStore } from '../../state/adminReviewStore'
|
||||
import { useAuthStore } from '../../state/authStore'
|
||||
import { useDiagramStore } from '../../state/diagramStore'
|
||||
import { useSubmissionStore } from '../../state/submissionStore'
|
||||
import { useCanEdit } from '../../hooks/useCanEdit'
|
||||
import AdminUsersModal from '../admin/AdminUsersModal'
|
||||
import ProfileModal from '../account/ProfileModal'
|
||||
import AnnouncementComposerModal from '../announcements/AnnouncementComposerModal'
|
||||
@@ -37,8 +38,7 @@ function setLastSeenSubmissionsAt(iso: string): void {
|
||||
|
||||
export default function TopBar() {
|
||||
const diagram = useDiagramStore((s) => s.diagram)
|
||||
const access = useDiagramStore((s) => s.access)
|
||||
const canEdit = access?.myPermission !== 'view'
|
||||
const canEdit = useCanEdit()
|
||||
const renameDiagram = useDiagramStore((s) => s.renameDiagram)
|
||||
const importDiagram = useDiagramStore((s) => s.importDiagram)
|
||||
const mySubmissions = useSubmissionStore((s) => s.mySubmissions)
|
||||
@@ -92,13 +92,13 @@ export default function TopBar() {
|
||||
|
||||
return (
|
||||
<header className="flex h-12 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-indigo-700">Diagrav</span>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="shrink-0 text-sm font-semibold text-indigo-700">Diagrav</span>
|
||||
<input
|
||||
value={diagram.name}
|
||||
onChange={(e) => renameDiagram(e.target.value)}
|
||||
disabled={!canEdit}
|
||||
className="rounded border border-transparent px-2 py-1 text-sm text-slate-700 hover:border-slate-200 focus:border-slate-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className="w-28 min-w-0 rounded border border-transparent px-2 py-1 text-sm text-slate-700 hover:border-slate-200 focus:border-slate-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60 sm:w-48 md:w-64"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -108,64 +108,72 @@ export default function TopBar() {
|
||||
>
|
||||
Diagrams
|
||||
</button>
|
||||
<DropdownMenu label="Diagram">
|
||||
<DropdownMenuItem onClick={() => setVersionHistoryOpen(true)}>History</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setSharingOpen(true)}>Share</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleImportClick}>Import</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleExport}>Export</DropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
<input ref={fileInputRef} type="file" accept="application/json,.json" className="hidden" onChange={handleFileChange} />
|
||||
{/* Editing/reviewing actions below aren't useful on a mobile-width
|
||||
viewport (view-only there via useCanEdit) — hidden rather than
|
||||
removed so they come straight back if the window's just narrow,
|
||||
not actually a phone (see useIsMobile). "Diagrams" above and
|
||||
"Account" below stay: picking which diagram to look at, and
|
||||
signing out, are both still things a mobile visitor might want. */}
|
||||
<div className="hidden md:contents">
|
||||
<DropdownMenu label="Diagram">
|
||||
<DropdownMenuItem onClick={() => setVersionHistoryOpen(true)}>History</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setSharingOpen(true)}>Share</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleImportClick}>Import</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleExport}>Export</DropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
<input ref={fileInputRef} type="file" accept="application/json,.json" className="hidden" onChange={handleFileChange} />
|
||||
|
||||
<button onClick={openMySubmissions} className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100">
|
||||
My Submissions
|
||||
{unseenOutcomeCount > 0 ? (
|
||||
<span
|
||||
className="ml-1 rounded-full bg-indigo-100 px-1.5 py-0.5 text-[10px] font-semibold text-indigo-700"
|
||||
title="Submission(s) reviewed since you last checked"
|
||||
>
|
||||
{unseenOutcomeCount}
|
||||
</span>
|
||||
) : (
|
||||
pendingSubmissionCount > 0 && (
|
||||
<span className="ml-1 rounded-full bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700">
|
||||
{pendingSubmissionCount}
|
||||
<button onClick={openMySubmissions} className="rounded px-2.5 py-1.5 text-xs text-slate-600 hover:bg-slate-100">
|
||||
My Submissions
|
||||
{unseenOutcomeCount > 0 ? (
|
||||
<span
|
||||
className="ml-1 rounded-full bg-indigo-100 px-1.5 py-0.5 text-[10px] font-semibold text-indigo-700"
|
||||
title="Submission(s) reviewed since you last checked"
|
||||
>
|
||||
{unseenOutcomeCount}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
pendingSubmissionCount > 0 && (
|
||||
<span className="ml-1 rounded-full bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700">
|
||||
{pendingSubmissionCount}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isAdmin && (
|
||||
<DropdownMenu
|
||||
align="right"
|
||||
label={
|
||||
<>
|
||||
Admin
|
||||
{adminPendingCount > 0 && (
|
||||
<span className="rounded-full bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700">
|
||||
{adminPendingCount}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setAdminReviewOpen(true)}
|
||||
badge={
|
||||
adminPendingCount > 0 && (
|
||||
<span className="rounded-full bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700">
|
||||
{adminPendingCount}
|
||||
</span>
|
||||
)
|
||||
{isAdmin && (
|
||||
<DropdownMenu
|
||||
align="right"
|
||||
label={
|
||||
<>
|
||||
Admin
|
||||
{adminPendingCount > 0 && (
|
||||
<span className="rounded-full bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700">
|
||||
{adminPendingCount}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
Review Queue
|
||||
</DropdownMenuItem>
|
||||
{isSuperAdmin && <DropdownMenuItem onClick={() => setAdminUsersOpen(true)}>Manage Users</DropdownMenuItem>}
|
||||
{canManageAnnouncements && (
|
||||
<DropdownMenuItem onClick={() => setAnnouncementComposerOpen(true)}>Announcement</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => setAdminReviewOpen(true)}
|
||||
badge={
|
||||
adminPendingCount > 0 && (
|
||||
<span className="rounded-full bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700">
|
||||
{adminPendingCount}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
>
|
||||
Review Queue
|
||||
</DropdownMenuItem>
|
||||
{isSuperAdmin && <DropdownMenuItem onClick={() => setAdminUsersOpen(true)}>Manage Users</DropdownMenuItem>}
|
||||
{canManageAnnouncements && (
|
||||
<DropdownMenuItem onClick={() => setAnnouncementComposerOpen(true)}>Announcement</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DropdownMenu label={username ?? 'Account'} align="right">
|
||||
<DropdownMenuItem onClick={() => setProfileOpen(true)}>Profile</DropdownMenuItem>
|
||||
|
||||
Reference in New Issue
Block a user