fix(mobile): stack header, declutter stepper, and reflow chrome
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled

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:
Samuel Prevost 2026-04-26 17:57:25 +02:00
parent 9032af426e
commit e56ee9611d
3 changed files with 76 additions and 34 deletions

View File

@ -25,28 +25,45 @@ const store = useAppStore()
>Fork me on Gitea</a >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 <header
class="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60" class="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
> >
<div <div class="mx-auto max-w-7xl px-4">
class="mx-auto grid h-14 max-w-7xl grid-cols-3 items-center 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><!-- spacer for ribbon --></div> >
<div class="flex items-center justify-center gap-2"> <div class="hidden sm:block">
<SkwikLogo :size="28" /> <!-- spacer for the fork-me ribbon -->
<h1 </div>
class="font-mono text-lg font-semibold tracking-tight" <div class="flex items-center gap-2 sm:justify-center">
> <SkwikLogo :size="28" />
Skwik <h1
</h1> class="font-mono text-lg font-semibold tracking-tight"
<span >
class="hidden text-[10px] font-medium uppercase tracking-widest text-muted-foreground sm:inline" Skwik
>Perspective Correction</span </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>
<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 /> <StepIndicator />
<ThemeToggle />
</div> </div>
</div> </div>
</header> </header>
@ -62,17 +79,28 @@ const store = useAppStore()
<MeasureViewer v-else-if="store.currentStep === 5" /> <MeasureViewer v-else-if="store.currentStep === 5" />
</main> </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 <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" 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 <span>
<a Made by
href="https://github.com/usr-ein" <a
target="_blank" href="https://github.com/usr-ein"
rel="noopener" target="_blank"
class="underline underline-offset-2 transition-colors hover:text-foreground" rel="noopener"
>Samuel Prevost</a 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> </footer>
</div> </div>
</template> </template>

View File

@ -296,16 +296,25 @@ function onFileSelect(e: Event) {
primary drop target. Two-step confirm: first click primary drop target. Two-step confirm: first click
arms it; second click within arms it; second click within
CLEAR_CONFIRM_TIMEOUT_MS commits. --> 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 <Button
v-if="cacheCount > 0" v-if="cacheCount > 0"
variant="ghost" variant="ghost"
size="sm" 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=" :class="
confirmingClear confirmingClear
? 'text-destructive hover:text-destructive' ? 'text-destructive hover:text-destructive'
: 'text-muted-foreground/60 hover:text-destructive' : 'text-muted-foreground/60 hover:text-destructive'
" "
:aria-label="
confirmingClear
? 'Confirm clear cache'
: `Clear cache (${String(cacheCount)})`
"
@click="handleClearCacheClick" @click="handleClearCacheClick"
> >
<svg <svg
@ -325,11 +334,16 @@ function onFileSelect(e: Event) {
/> />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" /> <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg> </svg>
{{ <span
confirmingClear v-if="confirmingClear"
? "Are you sure?" class="whitespace-nowrap"
: `Clear cache (${String(cacheCount)})` >
}} <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> </Button>
<CardHeader class="text-center"> <CardHeader class="text-center">
<CardTitle class="text-lg">Load Source Image</CardTitle> <CardTitle class="text-lg">Load Source Image</CardTitle>

View File

@ -29,10 +29,10 @@ function handleClick(num: AppStep) {
<template v-for="(step, i) in steps" :key="step.num"> <template v-for="(step, i) in steps" :key="step.num">
<button <button
v-if="isReachable(step.num)" 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)" @click="handleClick(step.num)"
> >
{{ step.num }}.{{ step.label }} {{ step.label }}
</button> </button>
<Badge <Badge
v-else v-else
@ -41,13 +41,13 @@ function handleClick(num: AppStep) {
? 'default' ? 'default'
: 'outline' : 'outline'
" "
class="cursor-default select-none font-mono text-xs" class="shrink-0 cursor-default select-none font-mono text-xs"
:class="{ :class="{
'opacity-40': 'opacity-40':
step.num > store.maxStepReached, step.num > store.maxStepReached,
}" }"
> >
{{ step.num }}.{{ step.label }} {{ step.label }}
</Badge> </Badge>
<span <span
v-if="i < steps.length - 1" v-if="i < steps.length - 1"