fix(mobile): stack header, declutter stepper, and reflow chrome
The header now stacks on mobile: title row up top, stepper on its own row below, and the theme toggle moves into the footer's right edge so it isn't competing for space with the title or stepper. Stepper labels drop the leading "1." prefix (saves ~8ch across five steps) and the mobile container has overflow-x-auto so very narrow screens scroll instead of breaking layout. Clear-cache button on the upload card collapses to icon-only on mobile so the centered "Load Source Image" title doesn't sit underneath it; the confirm state still reads "Sure?" so the destructive prompt is visible at a glance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9032af426e
commit
e56ee9611d
76
src/App.vue
76
src/App.vue
@ -25,28 +25,45 @@ const store = useAppStore()
|
||||
>Fork me on Gitea</a
|
||||
>
|
||||
|
||||
<!-- Header lays out as a single h-14 row on desktop and stacks
|
||||
into title-row + stepper-row on mobile so the title doesn't
|
||||
collide with the stepper at narrow widths. The theme toggle
|
||||
stays in the desktop header but moves into the footer on
|
||||
mobile (see below). -->
|
||||
<header
|
||||
class="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
||||
>
|
||||
<div
|
||||
class="mx-auto grid h-14 max-w-7xl grid-cols-3 items-center px-4"
|
||||
>
|
||||
<div><!-- spacer for ribbon --></div>
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<SkwikLogo :size="28" />
|
||||
<h1
|
||||
class="font-mono text-lg font-semibold tracking-tight"
|
||||
>
|
||||
Skwik
|
||||
</h1>
|
||||
<span
|
||||
class="hidden text-[10px] font-medium uppercase tracking-widest text-muted-foreground sm:inline"
|
||||
>Perspective Correction</span
|
||||
>
|
||||
<div class="mx-auto max-w-7xl px-4">
|
||||
<div
|
||||
class="flex items-center justify-between gap-3 py-2 sm:grid sm:h-14 sm:grid-cols-3 sm:py-0"
|
||||
>
|
||||
<div class="hidden sm:block">
|
||||
<!-- spacer for the fork-me ribbon -->
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:justify-center">
|
||||
<SkwikLogo :size="28" />
|
||||
<h1
|
||||
class="font-mono text-lg font-semibold tracking-tight"
|
||||
>
|
||||
Skwik
|
||||
</h1>
|
||||
<span
|
||||
class="text-[10px] font-medium uppercase tracking-widest text-muted-foreground"
|
||||
>Perspective Correction</span
|
||||
>
|
||||
</div>
|
||||
<div class="hidden items-center justify-end gap-4 sm:flex">
|
||||
<StepIndicator />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<!-- Mobile-only stepper row. overflow-x-auto keeps the
|
||||
page width sane if the labels still don't fit on
|
||||
very narrow screens. -->
|
||||
<div
|
||||
class="-mx-4 flex justify-center overflow-x-auto px-4 pb-2 sm:hidden"
|
||||
>
|
||||
<StepIndicator />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -62,17 +79,28 @@ const store = useAppStore()
|
||||
<MeasureViewer v-else-if="store.currentStep === 5" />
|
||||
</main>
|
||||
|
||||
<!-- Footer is fixed to the bottom. On mobile we tuck the theme
|
||||
toggle into the right edge so it's reachable without
|
||||
eating into the header chrome; the toggle is absolutely
|
||||
positioned so it doesn't push the centered byline around. -->
|
||||
<footer
|
||||
class="fixed inset-x-0 bottom-0 z-40 border-t border-border/50 bg-background/95 py-3 text-center text-xs text-muted-foreground backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
||||
>
|
||||
Made by
|
||||
<a
|
||||
href="https://github.com/usr-ein"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="underline underline-offset-2 transition-colors hover:text-foreground"
|
||||
>Samuel Prevost</a
|
||||
<span>
|
||||
Made by
|
||||
<a
|
||||
href="https://github.com/usr-ein"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="underline underline-offset-2 transition-colors hover:text-foreground"
|
||||
>Samuel Prevost</a
|
||||
>
|
||||
</span>
|
||||
<div
|
||||
class="absolute bottom-1/2 right-2 translate-y-1/2 sm:hidden"
|
||||
>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -296,16 +296,25 @@ function onFileSelect(e: Event) {
|
||||
primary drop target. Two-step confirm: first click
|
||||
arms it; second click within
|
||||
CLEAR_CONFIRM_TIMEOUT_MS commits. -->
|
||||
<!-- Mobile: icon-only when idle, icon + "Sure?" while
|
||||
confirming so the destructive prompt is unmistakable.
|
||||
Desktop: full label always (the centered card title
|
||||
has room beside it). -->
|
||||
<Button
|
||||
v-if="cacheCount > 0"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="absolute right-2 top-2 h-7 gap-1.5 text-xs"
|
||||
class="absolute right-2 top-2 h-7 gap-1.5 px-2 text-xs"
|
||||
:class="
|
||||
confirmingClear
|
||||
? 'text-destructive hover:text-destructive'
|
||||
: 'text-muted-foreground/60 hover:text-destructive'
|
||||
"
|
||||
:aria-label="
|
||||
confirmingClear
|
||||
? 'Confirm clear cache'
|
||||
: `Clear cache (${String(cacheCount)})`
|
||||
"
|
||||
@click="handleClearCacheClick"
|
||||
>
|
||||
<svg
|
||||
@ -325,11 +334,16 @@ function onFileSelect(e: Event) {
|
||||
/>
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||
</svg>
|
||||
{{
|
||||
confirmingClear
|
||||
? "Are you sure?"
|
||||
: `Clear cache (${String(cacheCount)})`
|
||||
}}
|
||||
<span
|
||||
v-if="confirmingClear"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
<span class="hidden sm:inline">Are you sure?</span>
|
||||
<span class="sm:hidden">Sure?</span>
|
||||
</span>
|
||||
<span v-else class="hidden whitespace-nowrap sm:inline">
|
||||
Clear cache ({{ cacheCount }})
|
||||
</span>
|
||||
</Button>
|
||||
<CardHeader class="text-center">
|
||||
<CardTitle class="text-lg">Load Source Image</CardTitle>
|
||||
|
||||
@ -29,10 +29,10 @@ function handleClick(num: AppStep) {
|
||||
<template v-for="(step, i) in steps" :key="step.num">
|
||||
<button
|
||||
v-if="isReachable(step.num)"
|
||||
class="inline-flex items-center rounded-md border border-border px-2 py-0.5 font-mono text-xs font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
class="inline-flex shrink-0 items-center rounded-md border border-border px-2 py-0.5 font-mono text-xs font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
@click="handleClick(step.num)"
|
||||
>
|
||||
{{ step.num }}.{{ step.label }}
|
||||
{{ step.label }}
|
||||
</button>
|
||||
<Badge
|
||||
v-else
|
||||
@ -41,13 +41,13 @@ function handleClick(num: AppStep) {
|
||||
? 'default'
|
||||
: 'outline'
|
||||
"
|
||||
class="cursor-default select-none font-mono text-xs"
|
||||
class="shrink-0 cursor-default select-none font-mono text-xs"
|
||||
:class="{
|
||||
'opacity-40':
|
||||
step.num > store.maxStepReached,
|
||||
}"
|
||||
>
|
||||
{{ step.num }}.{{ step.label }}
|
||||
{{ step.label }}
|
||||
</Badge>
|
||||
<span
|
||||
v-if="i < steps.length - 1"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user