Skip to content

Commit 8bcff38

Browse files
ectoclaude
andcommitted
feat(app): add mobile-first responsive layout
Make the UI usable on mobile devices (< 640px): - Add viewport-fit=cover and safe area CSS variables - PropertyPanel: bottom sheet with drag handle on mobile - BottomToolbar: full-width scrollable, 44px touch targets - CornerIcons: hide secondary icons, increase touch targets - FeatureTree: slide-out drawer with backdrop on mobile - New useIsMobile hook for reactive breakpoint detection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d042313 commit 8bcff38

7 files changed

Lines changed: 236 additions & 135 deletions

File tree

packages/app/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en" class="dark">
33
<head>
44
<meta charset="UTF-8" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
66
<title>vcad</title>
77
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⬡</text></svg>" />
88
</head>

packages/app/src/components/BottomToolbar.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ function ToolbarButton({
8989
<Tooltip content={tooltip}>
9090
<button
9191
className={cn(
92-
"flex h-10 w-10 items-center justify-center relative",
92+
// Mobile: 44px touch targets (iOS minimum)
93+
"flex h-11 w-11 min-w-[44px] items-center justify-center relative",
94+
// Desktop: 40px
95+
"sm:h-10 sm:w-10 sm:min-w-0",
9396
"disabled:opacity-40 disabled:cursor-not-allowed",
9497
active
9598
? "bg-accent text-white"
@@ -238,13 +241,17 @@ export function BottomToolbar() {
238241
open={jointDialogOpen}
239242
onOpenChange={setJointDialogOpen}
240243
/>
241-
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-20">
244+
{/* Mobile: full-width fixed at bottom; Desktop: centered floating */}
245+
<div className="fixed bottom-0 inset-x-0 sm:absolute sm:bottom-4 sm:left-1/2 sm:-translate-x-1/2 sm:inset-auto z-20 pb-[var(--safe-bottom)]">
242246
<div
243247
className={cn(
244248
"flex items-center gap-1 px-2 py-1.5",
245249
"bg-surface",
246-
"border border-border",
250+
// Mobile: border only on top, full width; Desktop: full border
251+
"border-t sm:border border-border",
247252
"shadow-lg shadow-black/30",
253+
// Mobile: horizontal scroll
254+
"overflow-x-auto scrollbar-thin",
248255
)}
249256
>
250257
{/* Primitives */}

packages/app/src/components/CornerIcons.tsx

Lines changed: 63 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,23 @@ function IconButton({
4343
onClick,
4444
tooltip,
4545
active,
46+
className,
4647
}: {
4748
children: React.ReactNode;
4849
onClick?: () => void;
4950
tooltip: string;
5051
active?: boolean;
52+
className?: string;
5153
}) {
5254
return (
5355
<Tooltip content={tooltip}>
5456
<button
5557
className={cn(
56-
"flex h-8 w-8 items-center justify-center",
58+
// Mobile: 44px touch targets; Desktop: 32px
59+
"flex h-11 w-11 sm:h-8 sm:w-8 items-center justify-center",
5760
"text-text-muted/70 hover:text-text hover:bg-hover",
5861
active && "text-accent",
62+
className,
5963
)}
6064
onClick={onClick}
6165
>
@@ -364,8 +368,8 @@ export function CornerIcons({ onAboutOpen, onSave, onOpen }: CornerIconsProps) {
364368

365369
return (
366370
<>
367-
{/* Top-left: hamburger + logo */}
368-
<div className="absolute top-3 left-3 z-20 flex items-center gap-2">
371+
{/* Top-left: hamburger + logo - with safe area padding */}
372+
<div className="absolute z-20 flex items-center gap-2 top-[max(0.75rem,var(--safe-top))] left-[max(0.75rem,var(--safe-left))]">
369373
<IconButton
370374
tooltip="Toggle sidebar (`)"
371375
onClick={toggleFeatureTree}
@@ -381,68 +385,71 @@ export function CornerIcons({ onAboutOpen, onSave, onOpen }: CornerIconsProps) {
381385
</div>
382386
</div>
383387

384-
{/* Top-right: file actions, utilities, settings */}
385-
<div className="absolute top-3 right-3 z-20 flex items-center gap-1">
386-
{/* File actions */}
388+
{/* Top-right: file actions, utilities, settings - with safe area padding */}
389+
<div className="absolute z-20 flex items-center gap-1 top-[max(0.75rem,var(--safe-top))] right-[max(0.75rem,var(--safe-right))]">
390+
{/* File actions - always visible */}
387391
<IconButton tooltip="Save (Cmd+S)" onClick={onSave}>
388392
<FloppyDisk size={18} />
389393
</IconButton>
390394
<IconButton tooltip="Open (Cmd+O)" onClick={onOpen}>
391395
<FolderOpen size={18} />
392396
</IconButton>
393397

394-
<IconButton
395-
tooltip="Command palette (Cmd+K)"
396-
onClick={toggleCommandPalette}
397-
>
398-
<Command size={18} />
399-
</IconButton>
400-
<IconButton
401-
tooltip={
402-
theme === "system"
403-
? "Theme: System (click for Light)"
404-
: theme === "light"
405-
? "Theme: Light (click for Dark)"
406-
: "Theme: Dark (click for System)"
407-
}
408-
onClick={toggleTheme}
409-
>
410-
{theme === "system" ? (
411-
<Desktop size={18} />
412-
) : theme === "light" ? (
413-
<Sun size={18} />
414-
) : (
415-
<Moon size={18} />
416-
)}
417-
</IconButton>
418-
419-
{/* External links */}
420-
<Tooltip content="GitHub">
421-
<a
422-
href="https://github.com/ecto/vcad"
423-
target="_blank"
424-
rel="noopener noreferrer"
425-
className={cn(
426-
"flex h-8 w-8 items-center justify-center",
427-
"text-text-muted/70 hover:text-text hover:bg-hover",
428-
)}
398+
{/* Desktop-only icons */}
399+
<div className="hidden sm:flex items-center gap-1">
400+
<IconButton
401+
tooltip="Command palette (Cmd+K)"
402+
onClick={toggleCommandPalette}
429403
>
430-
<GithubLogo size={18} />
431-
</a>
432-
</Tooltip>
433-
<Tooltip content="Discord">
434-
<a
435-
href="https://discord.gg/ZU8QHnFAc2"
436-
target="_blank"
437-
rel="noopener noreferrer"
438-
className={cn(
439-
"flex h-8 w-8 items-center justify-center",
440-
"text-text-muted/70 hover:text-text hover:bg-hover",
441-
)}
404+
<Command size={18} />
405+
</IconButton>
406+
<IconButton
407+
tooltip={
408+
theme === "system"
409+
? "Theme: System (click for Light)"
410+
: theme === "light"
411+
? "Theme: Light (click for Dark)"
412+
: "Theme: Dark (click for System)"
413+
}
414+
onClick={toggleTheme}
442415
>
443-
<DiscordLogo size={18} />
444-
</a>
445-
</Tooltip>
416+
{theme === "system" ? (
417+
<Desktop size={18} />
418+
) : theme === "light" ? (
419+
<Sun size={18} />
420+
) : (
421+
<Moon size={18} />
422+
)}
423+
</IconButton>
424+
425+
{/* External links */}
426+
<Tooltip content="GitHub">
427+
<a
428+
href="https://github.com/ecto/vcad"
429+
target="_blank"
430+
rel="noopener noreferrer"
431+
className={cn(
432+
"flex h-8 w-8 items-center justify-center",
433+
"text-text-muted/70 hover:text-text hover:bg-hover",
434+
)}
435+
>
436+
<GithubLogo size={18} />
437+
</a>
438+
</Tooltip>
439+
<Tooltip content="Discord">
440+
<a
441+
href="https://discord.gg/ZU8QHnFAc2"
442+
target="_blank"
443+
rel="noopener noreferrer"
444+
className={cn(
445+
"flex h-8 w-8 items-center justify-center",
446+
"text-text-muted/70 hover:text-text hover:bg-hover",
447+
)}
448+
>
449+
<DiscordLogo size={18} />
450+
</a>
451+
</Tooltip>
452+
</div>
446453

447454
<SettingsMenu onAboutOpen={onAboutOpen} />
448455
</div>

packages/app/src/components/FeatureTree.tsx

Lines changed: 81 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useState, useRef, useEffect, useMemo } from "react";
1+
import { useState, useRef, useEffect, useMemo, useCallback } from "react";
2+
import { useIsMobile } from "@/hooks/useIsMobile";
23
import * as RadixContextMenu from "@radix-ui/react-context-menu";
34
import {
45
Cube,
@@ -533,6 +534,7 @@ export function FeatureTree() {
533534
const setFeatureTreeOpen = useUiStore((s) => s.setFeatureTreeOpen);
534535
const [renamingId, setRenamingId] = useState<string | null>(null);
535536
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
537+
const isMobile = useIsMobile();
536538

537539
// Check if this is an assembly document
538540
const hasInstances = document.instances && document.instances.length > 0;
@@ -573,70 +575,90 @@ export function FeatureTree() {
573575
}
574576
}, [hasContent, setFeatureTreeOpen]);
575577

578+
const handleBackdropClick = useCallback(() => {
579+
setFeatureTreeOpen(false);
580+
}, [setFeatureTreeOpen]);
581+
576582
if (!featureTreeOpen) return null;
577583

578584
return (
579-
<div
580-
className={cn(
581-
"absolute top-14 left-3 z-20 w-56",
582-
"border border-border",
583-
"bg-surface",
584-
"shadow-lg shadow-black/30",
585-
"max-h-[calc(100vh-120px)] flex flex-col",
585+
<>
586+
{/* Mobile backdrop */}
587+
{isMobile && (
588+
<div
589+
className="fixed inset-0 z-10 bg-black/50 sm:hidden"
590+
onClick={handleBackdropClick}
591+
/>
586592
)}
587-
>
588-
{/* Header */}
589-
<div className="flex h-10 shrink-0 items-center justify-between gap-2 border-b border-border px-3">
590-
<span className="text-xs font-bold uppercase tracking-wider text-text-muted">
591-
Features
592-
</span>
593-
<button
594-
onClick={() => setFeatureTreeOpen(false)}
595-
className="flex h-6 w-6 items-center justify-center text-text-muted hover:text-text hover:bg-hover"
596-
>
597-
<X size={14} />
598-
</button>
599-
</div>
600593

601-
{/* Body */}
602-
<div className="flex-1 overflow-y-auto p-2 scrollbar-thin">
603-
{!hasContent ? (
604-
<div className="px-2 py-4 text-center text-xs text-text-muted">
605-
No features yet.
606-
<br />
607-
Use the command bar to create a part.
608-
</div>
609-
) : (
610-
<ContextMenu>
611-
<div>
612-
{/* Assembly mode: show instances and joints */}
613-
{hasInstances ? (
614-
<AssemblyTree
615-
instances={document.instances!}
616-
joints={document.joints ?? []}
617-
groundInstanceId={document.groundInstanceId}
618-
/>
619-
) : (
620-
<>
621-
{/* Legacy mode: show parts */}
622-
{parts.map((part) => (
623-
<TreeNode
624-
key={part.id}
625-
part={part}
626-
depth={0}
627-
expandedIds={expandedIds}
628-
toggleExpanded={toggleExpanded}
629-
consumedParts={consumedParts}
630-
renamingId={renamingId}
631-
setRenamingId={setRenamingId}
632-
/>
633-
))}
634-
</>
635-
)}
636-
</div>
637-
</ContextMenu>
594+
<div
595+
className={cn(
596+
// Mobile: full-height drawer from left
597+
"fixed inset-y-0 left-0 z-20 w-72",
598+
"pt-[var(--safe-top)] pb-[var(--safe-bottom)] pl-[var(--safe-left)]",
599+
// Desktop: floating panel
600+
"sm:absolute sm:top-14 sm:left-3 sm:inset-y-auto sm:w-56",
601+
"sm:pt-0 sm:pb-0 sm:pl-0",
602+
"border-r sm:border border-border",
603+
"bg-surface",
604+
"shadow-lg shadow-black/30",
605+
isMobile ? "h-full" : "max-h-[calc(100vh-120px)]",
606+
"flex flex-col",
638607
)}
608+
>
609+
{/* Header */}
610+
<div className="flex h-10 shrink-0 items-center justify-between gap-2 border-b border-border px-3">
611+
<span className="text-xs font-bold uppercase tracking-wider text-text-muted">
612+
Features
613+
</span>
614+
<button
615+
onClick={() => setFeatureTreeOpen(false)}
616+
className="flex h-8 w-8 sm:h-6 sm:w-6 items-center justify-center text-text-muted hover:text-text hover:bg-hover"
617+
>
618+
<X size={14} />
619+
</button>
620+
</div>
621+
622+
{/* Body */}
623+
<div className="flex-1 overflow-y-auto p-2 scrollbar-thin">
624+
{!hasContent ? (
625+
<div className="px-2 py-4 text-center text-xs text-text-muted">
626+
No features yet.
627+
<br />
628+
Use the command bar to create a part.
629+
</div>
630+
) : (
631+
<ContextMenu>
632+
<div>
633+
{/* Assembly mode: show instances and joints */}
634+
{hasInstances ? (
635+
<AssemblyTree
636+
instances={document.instances!}
637+
joints={document.joints ?? []}
638+
groundInstanceId={document.groundInstanceId}
639+
/>
640+
) : (
641+
<>
642+
{/* Legacy mode: show parts */}
643+
{parts.map((part) => (
644+
<TreeNode
645+
key={part.id}
646+
part={part}
647+
depth={0}
648+
expandedIds={expandedIds}
649+
toggleExpanded={toggleExpanded}
650+
consumedParts={consumedParts}
651+
renamingId={renamingId}
652+
setRenamingId={setRenamingId}
653+
/>
654+
))}
655+
</>
656+
)}
657+
</div>
658+
</ContextMenu>
659+
)}
660+
</div>
639661
</div>
640-
</div>
662+
</>
641663
);
642664
}

0 commit comments

Comments
 (0)