feat(ui): squirrel logo, fork ribbon, clickable steps, and polish
- Squirrel engineer logo (SkwikLogo.vue) with hard hat and ruler - Matching favicon with squirrel head silhouette - Gitea fork ribbon (top-left, desktop only, Gitea green) - Centered header with logo, title, and subtitle - Footer: "Made by Samuel Prevost" with GitHub link - Clickable step indicators for previously visited steps - Smaller datum dots (6/4 base radius with visual cap) - Engineering-tool styling: monospace for measurements, Geist Mono font, deeper dark mode colors, instrument-panel header - EXIF viewer explains why focal length matters - Upload page describes what Skwik does Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
11e8013b6a
commit
98c6fc9a35
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 2.0 KiB |
43
src/App.vue
43
src/App.vue
@ -6,20 +6,44 @@ import ExifViewer from "@/components/ExifViewer.vue"
|
|||||||
import DatumEditor from "@/components/DatumEditor.vue"
|
import DatumEditor from "@/components/DatumEditor.vue"
|
||||||
import ResultViewer from "@/components/ResultViewer.vue"
|
import ResultViewer from "@/components/ResultViewer.vue"
|
||||||
import ThemeToggle from "@/components/ThemeToggle.vue"
|
import ThemeToggle from "@/components/ThemeToggle.vue"
|
||||||
|
import SkwikLogo from "@/components/SkwikLogo.vue"
|
||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-background text-foreground">
|
<div class="min-h-screen bg-background text-foreground">
|
||||||
|
<!-- Gitea fork ribbon — top-left, desktop only -->
|
||||||
|
<a
|
||||||
|
href="https://serv.e1n.sh/git/sam1902/skwik"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="github-fork-ribbon fixed left-0 top-0 z-[100] hidden md:block"
|
||||||
|
data-ribbon="Fork me on Gitea"
|
||||||
|
title="Fork me on Gitea"
|
||||||
|
>Fork me on Gitea</a
|
||||||
|
>
|
||||||
|
|
||||||
<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 flex h-14 max-w-7xl items-center justify-between px-4"
|
class="mx-auto grid h-14 max-w-7xl grid-cols-3 items-center px-4"
|
||||||
>
|
>
|
||||||
<h1 class="text-lg font-semibold tracking-tight">Skwik</h1>
|
<div><!-- spacer for ribbon --></div>
|
||||||
<div class="flex items-center gap-4">
|
<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>
|
||||||
|
<div class="flex items-center justify-end gap-4">
|
||||||
<StepIndicator />
|
<StepIndicator />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
@ -32,5 +56,18 @@ const store = useAppStore()
|
|||||||
<DatumEditor v-else-if="store.currentStep === 3" />
|
<DatumEditor v-else-if="store.currentStep === 3" />
|
||||||
<ResultViewer v-else-if="store.currentStep === 4" />
|
<ResultViewer v-else-if="store.currentStep === 4" />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<footer
|
||||||
|
class="border-t border-border/50 py-4 text-center text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
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
|
||||||
|
>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap");
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap');
|
||||||
|
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@ -9,7 +10,8 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-sans: "Geist Variable", sans-serif;
|
--font-sans: "Geist", "Geist Variable", ui-sans-serif, sans-serif;
|
||||||
|
--font-mono: "Geist Mono", ui-monospace, monospace;
|
||||||
--font-heading: var(--font-sans);
|
--font-heading: var(--font-sans);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
@ -84,10 +86,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.13 0 0);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.95 0 0);
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.175 0 0);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.95 0 0);
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.205 0 0);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.922 0 0);
|
--primary: oklch(0.922 0 0);
|
||||||
@ -99,7 +101,7 @@
|
|||||||
--accent: oklch(0.269 0 0);
|
--accent: oklch(0.269 0 0);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 12%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.556 0 0);
|
||||||
--chart-1: oklch(0.87 0 0);
|
--chart-1: oklch(0.87 0 0);
|
||||||
@ -126,3 +128,63 @@
|
|||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fork-me ribbon — adapted from github-fork-ribbon-css */
|
||||||
|
.github-fork-ribbon {
|
||||||
|
width: 12.1em;
|
||||||
|
height: 12.1em;
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
text-decoration: none;
|
||||||
|
text-indent: -999em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-fork-ribbon::before,
|
||||||
|
.github-fork-ribbon::after {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
width: 15.38em;
|
||||||
|
height: 1.54em;
|
||||||
|
top: 3.23em;
|
||||||
|
right: 0.5em;
|
||||||
|
box-sizing: content-box;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-fork-ribbon::before {
|
||||||
|
content: "";
|
||||||
|
padding: 0.38em 0;
|
||||||
|
background-color: #2d7a2e;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(255, 255, 255, 0.05),
|
||||||
|
rgba(0, 0, 0, 0.15)
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
0 0.15em 0.23em 0 rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 0 1px #5cb85c,
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
border-top: 1px dashed rgba(255, 255, 255, 0.25);
|
||||||
|
border-bottom: 1px dashed rgba(255, 255, 255, 0.25);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-fork-ribbon::after {
|
||||||
|
content: attr(data-ribbon);
|
||||||
|
color: #fff;
|
||||||
|
font: 700 0.9em "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.54em;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
text-indent: 0;
|
||||||
|
text-shadow:
|
||||||
|
0 -1px 0 rgba(0, 0, 0, 0.5),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|||||||
@ -49,19 +49,23 @@ function getPointConfigs(datum: Datum, dIdx: number) {
|
|||||||
const color = getDatumColor(dIdx)
|
const color = getDatumColor(dIdx)
|
||||||
const isSelected = store.selectedDatumId === datum.id
|
const isSelected = store.selectedDatumId === datum.id
|
||||||
const points = datum.type === "rectangle" ? datum.corners : datum.endpoints
|
const points = datum.type === "rectangle" ? datum.corners : datum.endpoints
|
||||||
const radius = isSelected ? 10 : 7
|
const baseRadius = isSelected ? 6 : 4
|
||||||
|
const visualRadius = Math.max(
|
||||||
|
baseRadius / scale.value,
|
||||||
|
baseRadius * 0.5
|
||||||
|
)
|
||||||
|
|
||||||
return points.map((pt, pIdx) => ({
|
return points.map((pt, pIdx) => ({
|
||||||
x: pt.x,
|
x: pt.x,
|
||||||
y: pt.y,
|
y: pt.y,
|
||||||
radius: radius / scale.value,
|
radius: visualRadius,
|
||||||
fill: color,
|
fill: color,
|
||||||
stroke: isSelected ? "#fff" : color,
|
stroke: isSelected ? "#fff" : color,
|
||||||
strokeWidth: 2 / scale.value,
|
strokeWidth: 1.5 / scale.value,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
_datumId: datum.id,
|
_datumId: datum.id,
|
||||||
_pointIndex: pIdx,
|
_pointIndex: pIdx,
|
||||||
hitStrokeWidth: 20 / scale.value,
|
hitStrokeWidth: 12 / scale.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue"
|
import { ref, computed, watch } from "vue"
|
||||||
import { useMediaQuery } from "@vueuse/core"
|
import { useMediaQuery } from "@vueuse/core"
|
||||||
import { useAppStore } from "@/stores/app"
|
import { useAppStore } from "@/stores/app"
|
||||||
|
import { saveDatums } from "@/lib/datum-cache"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@ -40,6 +42,16 @@ const nextTooltip = computed(() => {
|
|||||||
const names = incompleteDatums.value.map((d) => d.label)
|
const names = incompleteDatums.value.map((d) => d.label)
|
||||||
return `Missing dimensions: ${names.join(", ")}`
|
return `Missing dimensions: ${names.join(", ")}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.datums,
|
||||||
|
(datums) => {
|
||||||
|
if (store.fileHash && datums.length > 0) {
|
||||||
|
saveDatums(store.fileHash, datums)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -77,6 +89,16 @@ const nextTooltip = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Transition name="fade">
|
||||||
|
<Badge
|
||||||
|
v-if="store.cacheRestoreMessage"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ store.cacheRestoreMessage }}
|
||||||
|
</Badge>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<!-- Single layout: canvas always present, sidebar conditionally placed -->
|
<!-- Single layout: canvas always present, sidebar conditionally placed -->
|
||||||
<div
|
<div
|
||||||
class="grid gap-4"
|
class="grid gap-4"
|
||||||
|
|||||||
@ -149,7 +149,9 @@ function getExifRows(): ExifRow[] {
|
|||||||
<TableCell class="font-medium">{{
|
<TableCell class="font-medium">{{
|
||||||
row.label
|
row.label
|
||||||
}}</TableCell>
|
}}</TableCell>
|
||||||
<TableCell>{{ row.value }}</TableCell>
|
<TableCell class="font-mono text-sm">{{
|
||||||
|
row.value
|
||||||
|
}}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
@ -179,26 +181,31 @@ function getExifRows(): ExifRow[] {
|
|||||||
</svg>
|
</svg>
|
||||||
<div class="text-sm text-muted-foreground">
|
<div class="text-sm text-muted-foreground">
|
||||||
<p class="font-medium text-foreground">
|
<p class="font-medium text-foreground">
|
||||||
Lens Correction Info
|
Why EXIF matters
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1">
|
<p class="mt-1">
|
||||||
|
Focal length and lens model data allow the
|
||||||
|
algorithm to estimate radial distortion
|
||||||
|
(barrel/pincushion) introduced by the lens.
|
||||||
|
Without this, the perspective correction relies
|
||||||
|
solely on the datum geometry you provide.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">
|
||||||
<template v-if="store.exifData.focalLength">
|
<template v-if="store.exifData.focalLength">
|
||||||
This image was shot at
|
Detected
|
||||||
<strong
|
<span class="font-mono font-medium text-foreground"
|
||||||
>{{ store.exifData.focalLength }}mm</strong
|
>{{ store.exifData.focalLength }}mm</span
|
||||||
>
|
>
|
||||||
<template v-if="store.exifData.lensModel">
|
<template v-if="store.exifData.lensModel">
|
||||||
with a
|
on
|
||||||
<strong>{{
|
<span class="font-medium text-foreground">{{
|
||||||
store.exifData.lensModel
|
store.exifData.lensModel
|
||||||
}}</strong> </template
|
}}</span> </template
|
||||||
>. The deskew algorithm can use this to correct
|
>. Lens correction will be applied.
|
||||||
barrel/pincushion distortion.
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
No focal length data found. The algorithm will
|
No focal length found. Lens distortion
|
||||||
rely solely on datum measurements for
|
correction will be skipped.
|
||||||
perspective correction.
|
|
||||||
</template>
|
</template>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
280
src/components/SkwikLogo.vue
Normal file
280
src/components/SkwikLogo.vue
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 28,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
:width="size"
|
||||||
|
:height="size"
|
||||||
|
aria-label="Skwik squirrel logo"
|
||||||
|
>
|
||||||
|
<!-- Tail (fluffy, curling up behind) -->
|
||||||
|
<path
|
||||||
|
d="M50 52 Q58 40 54 28 Q52 22 48 20
|
||||||
|
Q55 18 56 12 Q56 8 52 10
|
||||||
|
Q48 12 46 18"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
opacity="0.7"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<ellipse
|
||||||
|
cx="32"
|
||||||
|
cy="46"
|
||||||
|
rx="12"
|
||||||
|
ry="10"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Head -->
|
||||||
|
<ellipse
|
||||||
|
cx="32"
|
||||||
|
cy="28"
|
||||||
|
rx="12"
|
||||||
|
ry="13"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Ear tufts -->
|
||||||
|
<path
|
||||||
|
d="M22 18 Q18 10 14 12 Q12 16 20 20"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M42 18 Q46 10 50 12 Q52 16 44 20"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Eyes -->
|
||||||
|
<circle
|
||||||
|
cx="27"
|
||||||
|
cy="26"
|
||||||
|
r="3"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="37"
|
||||||
|
cy="26"
|
||||||
|
r="3"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<!-- Eye shine -->
|
||||||
|
<circle cx="28.2" cy="25" r="1" fill="var(--background, #fff)" />
|
||||||
|
<circle cx="38.2" cy="25" r="1" fill="var(--background, #fff)" />
|
||||||
|
|
||||||
|
<!-- Nose -->
|
||||||
|
<ellipse
|
||||||
|
cx="32"
|
||||||
|
cy="32"
|
||||||
|
rx="2"
|
||||||
|
ry="1.5"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Cheeks -->
|
||||||
|
<ellipse
|
||||||
|
cx="24"
|
||||||
|
cy="32"
|
||||||
|
rx="4"
|
||||||
|
ry="3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
opacity="0.3"
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx="40"
|
||||||
|
cy="32"
|
||||||
|
rx="4"
|
||||||
|
ry="3"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
opacity="0.3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Mouth -->
|
||||||
|
<path
|
||||||
|
d="M30 34 Q32 36 34 34"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Arms holding ruler -->
|
||||||
|
<path
|
||||||
|
d="M20 42 L12 38"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M44 42 L52 38"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Ruler (held diagonally) -->
|
||||||
|
<rect
|
||||||
|
x="4"
|
||||||
|
y="34"
|
||||||
|
width="22"
|
||||||
|
height="4"
|
||||||
|
rx="0.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
transform="rotate(-15 15 36)"
|
||||||
|
/>
|
||||||
|
<!-- Ruler tick marks -->
|
||||||
|
<line
|
||||||
|
x1="8" y1="34" x2="8" y2="36"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
transform="rotate(-15 15 36)"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12" y1="34" x2="12" y2="36"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
transform="rotate(-15 15 36)"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="16" y1="34" x2="16" y2="36"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
transform="rotate(-15 15 36)"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="20" y1="34" x2="20" y2="36"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
transform="rotate(-15 15 36)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Safety goggles on forehead -->
|
||||||
|
<path
|
||||||
|
d="M22 20 Q27 17 32 18 Q37 17 42 20"
|
||||||
|
fill="none"
|
||||||
|
stroke="#f59e0b"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<!-- Goggle lenses -->
|
||||||
|
<ellipse
|
||||||
|
cx="26"
|
||||||
|
cy="20"
|
||||||
|
rx="3.5"
|
||||||
|
ry="2.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="#f59e0b"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx="38"
|
||||||
|
cy="20"
|
||||||
|
rx="3.5"
|
||||||
|
ry="2.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="#f59e0b"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
<!-- Goggle bridge -->
|
||||||
|
<path
|
||||||
|
d="M29.5 20 Q32 19 34.5 20"
|
||||||
|
fill="none"
|
||||||
|
stroke="#f59e0b"
|
||||||
|
stroke-width="1.2"
|
||||||
|
/>
|
||||||
|
<!-- Goggle lens tint -->
|
||||||
|
<ellipse
|
||||||
|
cx="26"
|
||||||
|
cy="20"
|
||||||
|
rx="2.5"
|
||||||
|
ry="1.5"
|
||||||
|
fill="#f59e0b"
|
||||||
|
opacity="0.15"
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx="38"
|
||||||
|
cy="20"
|
||||||
|
rx="2.5"
|
||||||
|
ry="1.5"
|
||||||
|
fill="#f59e0b"
|
||||||
|
opacity="0.15"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Hard hat -->
|
||||||
|
<path
|
||||||
|
d="M18 16 Q18 6 32 5 Q46 6 46 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="#f59e0b"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<!-- Hat brim -->
|
||||||
|
<path
|
||||||
|
d="M16 16 L48 16"
|
||||||
|
stroke="#f59e0b"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<!-- Hat dome highlight -->
|
||||||
|
<path
|
||||||
|
d="M26 9 Q32 7 38 9"
|
||||||
|
fill="none"
|
||||||
|
stroke="#fbbf24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
opacity="0.6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Feet -->
|
||||||
|
<ellipse
|
||||||
|
cx="26"
|
||||||
|
cy="56"
|
||||||
|
rx="4"
|
||||||
|
ry="2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx="38"
|
||||||
|
cy="56"
|
||||||
|
rx="4"
|
||||||
|
ry="2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@ -17,17 +17,32 @@ const steps = [
|
|||||||
<template v-for="(step, i) in steps" :key="step.num">
|
<template v-for="(step, i) in steps" :key="step.num">
|
||||||
<Badge
|
<Badge
|
||||||
:variant="
|
:variant="
|
||||||
store.currentStep === step.num ? 'default' : 'outline'
|
store.currentStep === step.num
|
||||||
|
? 'default'
|
||||||
|
: 'outline'
|
||||||
"
|
"
|
||||||
class="cursor-default select-none text-xs"
|
class="select-none font-mono text-xs"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-40': store.currentStep < step.num,
|
'opacity-40': store.currentStep < step.num,
|
||||||
|
'cursor-pointer hover:bg-accent':
|
||||||
|
step.num <= store.maxStepReached
|
||||||
|
&& step.num !== store.currentStep,
|
||||||
|
'cursor-default':
|
||||||
|
step.num > store.maxStepReached
|
||||||
|
|| step.num === store.currentStep,
|
||||||
}"
|
}"
|
||||||
|
@click="
|
||||||
|
step.num <= store.maxStepReached
|
||||||
|
? store.goToStep(step.num)
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ step.num }}.{{ step.label }}
|
{{ step.num }}.{{ step.label }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span v-if="i < steps.length - 1" class="text-muted-foreground"
|
<span
|
||||||
>·</span
|
v-if="i < steps.length - 1"
|
||||||
|
class="text-xs text-muted-foreground/40"
|
||||||
|
>›</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user