Add flair badge gamification system with custom SVG badges

- Create 12 Office Space themed SVG badges (extraction, basement, printer,
  coffee, tps, bobs, memo, oface, stapler, conclusions, spreadsheet, flair-badge)
- Implement FlairBadge.astro component with localStorage persistence
- Add 15-second timer per page to earn flair
- Create floating counter, modal flair board, toast notifications
- Override Starlight Footer to inject flair system globally
- Add name capture dialog on first flair earned

Features:
- Badges glow golden when earned, grayscale when locked
- Progress persists across browser sessions
- Stan quote: "We need to talk about your document processing..."
This commit is contained in:
Ryan Malloy 2026-01-11 14:24:39 -07:00
parent 32b41f79d9
commit 5ae7040496
28 changed files with 1410 additions and 11 deletions

View File

@ -11,6 +11,9 @@ export default defineConfig({
integrations: [ integrations: [
starlight({ starlight({
title: 'mcwaddams', title: 'mcwaddams',
components: {
Footer: './src/components/Footer.astro',
},
tagline: 'I was told there would be document extraction.', tagline: 'I was told there would be document extraction.',
logo: { logo: {
src: './src/assets/stapler.svg', src: './src/assets/stapler.svg',

12
public/flair/basement.svg Normal file
View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "Basement Dweller" - Basement window with stapler silhouette -->
<rect x="8" y="8" width="32" height="32" rx="2" fill="#1e293b"/>
<!-- Basement window bars -->
<rect x="12" y="12" width="24" height="18" rx="1" fill="#334155"/>
<rect x="12" y="12" width="24" height="18" rx="1" stroke="#64748b" stroke-width="1"/>
<rect x="23" y="12" width="2" height="18" fill="#64748b"/>
<rect x="12" y="20" width="24" height="2" fill="#64748b"/>
<!-- Red stapler silhouette in darkness -->
<rect x="16" y="34" width="16" height="6" rx="1" fill="#dc2626"/>
<rect x="14" y="36" width="4" height="4" rx="1" fill="#b91c1c"/>
</svg>

After

Width:  |  Height:  |  Size: 711 B

17
public/flair/bobs.svg Normal file
View File

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "The Bobs Approved" - Business tie with checkmark -->
<!-- Shirt collar -->
<path d="M8 6 L24 16 L40 6" stroke="#e2e8f0" stroke-width="4" fill="none"/>
<!-- Tie knot -->
<polygon points="20,14 28,14 26,20 22,20" fill="#1e40af"/>
<!-- Tie body -->
<polygon points="22,20 26,20 28,42 24,46 20,42" fill="#1e40af"/>
<!-- Diagonal stripes on tie -->
<path d="M22 24 L26 22" stroke="#3b82f6" stroke-width="1.5"/>
<path d="M21 28 L27 25" stroke="#3b82f6" stroke-width="1.5"/>
<path d="M21 32 L27 29" stroke="#3b82f6" stroke-width="1.5"/>
<path d="M21 36 L27 33" stroke="#3b82f6" stroke-width="1.5"/>
<!-- Checkmark badge -->
<circle cx="36" cy="12" r="8" fill="#16a34a"/>
<path d="M32 12 L35 15 L40 9" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 905 B

14
public/flair/coffee.svg Normal file
View File

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "Case of the Mondays" - Coffee mug with steam -->
<!-- Mug body -->
<rect x="10" y="18" width="22" height="22" rx="3" fill="#78350f"/>
<rect x="12" y="20" width="18" height="18" rx="2" fill="#92400e"/>
<!-- Coffee surface -->
<ellipse cx="21" cy="22" rx="8" ry="2" fill="#451a03"/>
<!-- Mug handle -->
<path d="M32 22 Q40 22 40 30 Q40 38 32 38" stroke="#78350f" stroke-width="4" fill="none"/>
<!-- Steam wisps -->
<path d="M14 14 Q16 10 14 6" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M21 12 Q23 8 21 4" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M28 14 Q30 10 28 6" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 822 B

View File

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "Jump to Conclusions" - The Jump to Conclusions mat -->
<!-- Mat base -->
<rect x="4" y="28" width="40" height="16" rx="2" fill="#16a34a" transform="skewX(-5)"/>
<rect x="6" y="30" width="36" height="12" rx="1" fill="#22c55e" transform="skewX(-5)"/>
<!-- Conclusion squares -->
<rect x="8" y="32" width="8" height="8" rx="1" fill="#f8fafc" transform="skewX(-5)"/>
<rect x="18" y="32" width="8" height="8" rx="1" fill="#fef08a" transform="skewX(-5)"/>
<rect x="28" y="32" width="8" height="8" rx="1" fill="#fca5a5" transform="skewX(-5)"/>
<!-- Jumping figure -->
<circle cx="24" cy="12" r="5" fill="#1e293b"/>
<path d="M24 17 L24 24" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
<path d="M24 19 L18 22" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
<path d="M24 19 L30 22" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
<path d="M24 24 L20 30" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
<path d="M24 24 L28 30" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "I Was Told There Would Be Extraction" - Document with extraction arrow -->
<rect x="10" y="6" width="22" height="28" rx="2" fill="#f8fafc" stroke="#475569" stroke-width="2"/>
<rect x="14" y="12" width="14" height="2" rx="1" fill="#94a3b8"/>
<rect x="14" y="17" width="14" height="2" rx="1" fill="#94a3b8"/>
<rect x="14" y="22" width="10" height="2" rx="1" fill="#94a3b8"/>
<!-- Extraction arrow coming out -->
<path d="M28 26 L38 26 L38 22 L46 28 L38 34 L38 30 L28 30 Z" fill="#dc2626"/>
<!-- Small glow effect -->
<ellipse cx="46" cy="28" rx="4" ry="6" fill="#dc2626" opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "37 Pieces of Flair" - Championship medal -->
<!-- Ribbon -->
<polygon points="16,4 20,4 24,16 28,4 32,4 26,20 22,20" fill="#dc2626"/>
<polygon points="16,4 20,4 22,12" fill="#b91c1c"/>
<polygon points="32,4 28,4 26,12" fill="#b91c1c"/>
<!-- Medal circle -->
<circle cx="24" cy="30" r="14" fill="#f59e0b"/>
<circle cx="24" cy="30" r="14" stroke="#b45309" stroke-width="2"/>
<circle cx="24" cy="30" r="11" fill="#fbbf24"/>
<circle cx="24" cy="30" r="11" stroke="#f59e0b" stroke-width="1"/>
<!-- "37" on medal -->
<text x="24" y="34" font-size="12" fill="#78350f" font-family="sans-serif" font-weight="bold" text-anchor="middle">37</text>
<!-- Star accent -->
<polygon points="24,18 25,21 28,21 25.5,23 26.5,26 24,24 21.5,26 22.5,23 20,21 23,21" fill="#fef3c7"/>
</svg>

After

Width:  |  Height:  |  Size: 874 B

18
public/flair/memo.svg Normal file
View File

@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "Did You Get The Memo?" - Memo/sticky note with pushpin -->
<!-- Shadow -->
<rect x="10" y="12" width="30" height="30" rx="1" fill="#1e293b" opacity="0.2" transform="translate(2,2)"/>
<!-- Yellow memo paper -->
<rect x="8" y="10" width="30" height="30" rx="1" fill="#fef08a"/>
<!-- Corner fold -->
<path d="M38 10 L38 18 L30 10 Z" fill="#fde047"/>
<path d="M30 10 L38 18" stroke="#eab308" stroke-width="1"/>
<!-- Pushpin -->
<circle cx="24" cy="6" r="4" fill="#dc2626"/>
<rect x="23" y="8" width="2" height="6" fill="#94a3b8"/>
<!-- Text lines -->
<text x="12" y="22" font-size="6" fill="#713f12" font-family="sans-serif" font-weight="bold">MEMO</text>
<rect x="12" y="26" width="22" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
<rect x="12" y="30" width="18" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
<rect x="12" y="34" width="20" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1010 B

20
public/flair/oface.svg Normal file
View File

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "O Face" - Surprised/amazed face -->
<!-- Face circle -->
<circle cx="24" cy="24" r="20" fill="#fef3c7"/>
<circle cx="24" cy="24" r="20" stroke="#f59e0b" stroke-width="2"/>
<!-- Eyebrows raised -->
<path d="M12 14 Q16 10 20 14" stroke="#78350f" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M28 14 Q32 10 36 14" stroke="#78350f" stroke-width="2" fill="none" stroke-linecap="round"/>
<!-- Wide eyes -->
<ellipse cx="16" cy="20" rx="4" ry="5" fill="white"/>
<circle cx="16" cy="20" r="2.5" fill="#1e293b"/>
<ellipse cx="32" cy="20" rx="4" ry="5" fill="white"/>
<circle cx="32" cy="20" r="2.5" fill="#1e293b"/>
<!-- "O" mouth -->
<ellipse cx="24" cy="34" rx="6" ry="8" fill="#dc2626"/>
<ellipse cx="24" cy="33" rx="4" ry="5" fill="#991b1b"/>
<!-- Cheek blush -->
<ellipse cx="10" cy="26" rx="3" ry="2" fill="#fca5a5" opacity="0.6"/>
<ellipse cx="38" cy="26" rx="3" ry="2" fill="#fca5a5" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

15
public/flair/printer.svg Normal file
View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "PC Load Letter" - Printer with error -->
<rect x="6" y="18" width="36" height="18" rx="3" fill="#64748b"/>
<rect x="8" y="20" width="32" height="14" rx="2" fill="#475569"/>
<!-- Paper tray top -->
<rect x="12" y="8" width="24" height="12" rx="1" fill="#e2e8f0"/>
<rect x="14" y="10" width="20" height="8" rx="1" fill="#f8fafc"/>
<!-- Paper output slot -->
<rect x="14" y="34" width="20" height="8" rx="1" fill="#f8fafc"/>
<!-- Error light -->
<circle cx="36" cy="24" r="3" fill="#dc2626"/>
<!-- Error LCD display -->
<rect x="12" y="22" width="18" height="6" rx="1" fill="#1e293b"/>
<text x="14" y="27" font-size="5" fill="#dc2626" font-family="monospace">PC LOAD</text>
</svg>

After

Width:  |  Height:  |  Size: 783 B

View File

@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "Spreadsheet Survivor" - Excel grid with survivor badge -->
<!-- Sheet background -->
<rect x="6" y="6" width="36" height="36" rx="2" fill="#16a34a"/>
<rect x="8" y="8" width="32" height="32" rx="1" fill="#f8fafc"/>
<!-- Grid lines -->
<line x1="8" y1="16" x2="40" y2="16" stroke="#94a3b8" stroke-width="1"/>
<line x1="8" y1="24" x2="40" y2="24" stroke="#94a3b8" stroke-width="1"/>
<line x1="8" y1="32" x2="40" y2="32" stroke="#94a3b8" stroke-width="1"/>
<line x1="18" y1="8" x2="18" y2="40" stroke="#94a3b8" stroke-width="1"/>
<line x1="28" y1="8" x2="28" y2="40" stroke="#94a3b8" stroke-width="1"/>
<!-- Header row (green) -->
<rect x="8" y="8" width="32" height="8" fill="#16a34a"/>
<!-- Header column (green) -->
<rect x="8" y="8" width="10" height="32" fill="#16a34a" opacity="0.7"/>
<!-- Cell labels -->
<text x="13" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">A</text>
<text x="23" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">B</text>
<text x="34" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">C</text>
<!-- Star survivor badge -->
<circle cx="38" cy="38" r="8" fill="#f59e0b"/>
<polygon points="38,32 39.5,36 44,36.5 40.5,39 42,44 38,41 34,44 35.5,39 32,36.5 36.5,36" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

17
public/flair/stapler.svg Normal file
View File

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "I Have Your Stapler" - Milton's famous red Swingline stapler -->
<!-- Stapler base -->
<rect x="4" y="32" width="40" height="8" rx="2" fill="#1e293b"/>
<rect x="6" y="34" width="36" height="4" rx="1" fill="#334155"/>
<!-- Stapler top (red!) -->
<path d="M6 32 L6 24 Q6 20 10 20 L38 20 Q42 20 42 24 L42 32 Z" fill="#dc2626"/>
<path d="M8 30 L8 25 Q8 22 11 22 L37 22 Q40 22 40 25 L40 30 Z" fill="#ef4444"/>
<!-- Swingline branding area -->
<rect x="14" y="24" width="20" height="4" rx="1" fill="#b91c1c"/>
<!-- Chrome accent -->
<rect x="6" y="30" width="36" height="2" fill="#94a3b8"/>
<!-- Staple loading slot -->
<rect x="16" y="20" width="16" height="2" fill="#991b1b"/>
<!-- Shadow/depth -->
<rect x="4" y="38" width="40" height="2" rx="1" fill="#0f172a" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 890 B

15
public/flair/tps.svg Normal file
View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "TPS Report Expert" - Clipboard with TPS -->
<!-- Clipboard board -->
<rect x="8" y="8" width="32" height="36" rx="2" fill="#78350f"/>
<!-- Clip -->
<rect x="16" y="4" width="16" height="8" rx="2" fill="#94a3b8"/>
<rect x="18" y="6" width="12" height="4" rx="1" fill="#cbd5e1"/>
<!-- Paper -->
<rect x="12" y="12" width="24" height="28" rx="1" fill="#f8fafc"/>
<!-- TPS text -->
<text x="24" y="24" font-size="10" fill="#1e293b" font-family="sans-serif" font-weight="bold" text-anchor="middle">TPS</text>
<rect x="14" y="28" width="20" height="2" rx="1" fill="#94a3b8"/>
<rect x="14" y="32" width="16" height="2" rx="1" fill="#94a3b8"/>
<rect x="14" y="36" width="18" height="2" rx="1" fill="#94a3b8"/>
</svg>

After

Width:  |  Height:  |  Size: 814 B

View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "Basement Dweller" - Basement window with stapler silhouette -->
<rect x="8" y="8" width="32" height="32" rx="2" fill="#1e293b"/>
<!-- Basement window bars -->
<rect x="12" y="12" width="24" height="18" rx="1" fill="#334155"/>
<rect x="12" y="12" width="24" height="18" rx="1" stroke="#64748b" stroke-width="1"/>
<rect x="23" y="12" width="2" height="18" fill="#64748b"/>
<rect x="12" y="20" width="24" height="2" fill="#64748b"/>
<!-- Red stapler silhouette in darkness -->
<rect x="16" y="34" width="16" height="6" rx="1" fill="#dc2626"/>
<rect x="14" y="36" width="4" height="4" rx="1" fill="#b91c1c"/>
</svg>

After

Width:  |  Height:  |  Size: 711 B

17
src/assets/flair/bobs.svg Normal file
View File

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "The Bobs Approved" - Business tie with checkmark -->
<!-- Shirt collar -->
<path d="M8 6 L24 16 L40 6" stroke="#e2e8f0" stroke-width="4" fill="none"/>
<!-- Tie knot -->
<polygon points="20,14 28,14 26,20 22,20" fill="#1e40af"/>
<!-- Tie body -->
<polygon points="22,20 26,20 28,42 24,46 20,42" fill="#1e40af"/>
<!-- Diagonal stripes on tie -->
<path d="M22 24 L26 22" stroke="#3b82f6" stroke-width="1.5"/>
<path d="M21 28 L27 25" stroke="#3b82f6" stroke-width="1.5"/>
<path d="M21 32 L27 29" stroke="#3b82f6" stroke-width="1.5"/>
<path d="M21 36 L27 33" stroke="#3b82f6" stroke-width="1.5"/>
<!-- Checkmark badge -->
<circle cx="36" cy="12" r="8" fill="#16a34a"/>
<path d="M32 12 L35 15 L40 9" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 905 B

View File

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "Case of the Mondays" - Coffee mug with steam -->
<!-- Mug body -->
<rect x="10" y="18" width="22" height="22" rx="3" fill="#78350f"/>
<rect x="12" y="20" width="18" height="18" rx="2" fill="#92400e"/>
<!-- Coffee surface -->
<ellipse cx="21" cy="22" rx="8" ry="2" fill="#451a03"/>
<!-- Mug handle -->
<path d="M32 22 Q40 22 40 30 Q40 38 32 38" stroke="#78350f" stroke-width="4" fill="none"/>
<!-- Steam wisps -->
<path d="M14 14 Q16 10 14 6" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M21 12 Q23 8 21 4" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M28 14 Q30 10 28 6" stroke="#94a3b8" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 822 B

View File

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "Jump to Conclusions" - The Jump to Conclusions mat -->
<!-- Mat base -->
<rect x="4" y="28" width="40" height="16" rx="2" fill="#16a34a" transform="skewX(-5)"/>
<rect x="6" y="30" width="36" height="12" rx="1" fill="#22c55e" transform="skewX(-5)"/>
<!-- Conclusion squares -->
<rect x="8" y="32" width="8" height="8" rx="1" fill="#f8fafc" transform="skewX(-5)"/>
<rect x="18" y="32" width="8" height="8" rx="1" fill="#fef08a" transform="skewX(-5)"/>
<rect x="28" y="32" width="8" height="8" rx="1" fill="#fca5a5" transform="skewX(-5)"/>
<!-- Jumping figure -->
<circle cx="24" cy="12" r="5" fill="#1e293b"/>
<path d="M24 17 L24 24" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
<path d="M24 19 L18 22" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
<path d="M24 19 L30 22" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
<path d="M24 24 L20 30" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
<path d="M24 24 L28 30" stroke="#1e293b" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "I Was Told There Would Be Extraction" - Document with extraction arrow -->
<rect x="10" y="6" width="22" height="28" rx="2" fill="#f8fafc" stroke="#475569" stroke-width="2"/>
<rect x="14" y="12" width="14" height="2" rx="1" fill="#94a3b8"/>
<rect x="14" y="17" width="14" height="2" rx="1" fill="#94a3b8"/>
<rect x="14" y="22" width="10" height="2" rx="1" fill="#94a3b8"/>
<!-- Extraction arrow coming out -->
<path d="M28 26 L38 26 L38 22 L46 28 L38 34 L38 30 L28 30 Z" fill="#dc2626"/>
<!-- Small glow effect -->
<ellipse cx="46" cy="28" rx="4" ry="6" fill="#dc2626" opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "37 Pieces of Flair" - Championship medal -->
<!-- Ribbon -->
<polygon points="16,4 20,4 24,16 28,4 32,4 26,20 22,20" fill="#dc2626"/>
<polygon points="16,4 20,4 22,12" fill="#b91c1c"/>
<polygon points="32,4 28,4 26,12" fill="#b91c1c"/>
<!-- Medal circle -->
<circle cx="24" cy="30" r="14" fill="#f59e0b"/>
<circle cx="24" cy="30" r="14" stroke="#b45309" stroke-width="2"/>
<circle cx="24" cy="30" r="11" fill="#fbbf24"/>
<circle cx="24" cy="30" r="11" stroke="#f59e0b" stroke-width="1"/>
<!-- "37" on medal -->
<text x="24" y="34" font-size="12" fill="#78350f" font-family="sans-serif" font-weight="bold" text-anchor="middle">37</text>
<!-- Star accent -->
<polygon points="24,18 25,21 28,21 25.5,23 26.5,26 24,24 21.5,26 22.5,23 20,21 23,21" fill="#fef3c7"/>
</svg>

After

Width:  |  Height:  |  Size: 874 B

18
src/assets/flair/memo.svg Normal file
View File

@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "Did You Get The Memo?" - Memo/sticky note with pushpin -->
<!-- Shadow -->
<rect x="10" y="12" width="30" height="30" rx="1" fill="#1e293b" opacity="0.2" transform="translate(2,2)"/>
<!-- Yellow memo paper -->
<rect x="8" y="10" width="30" height="30" rx="1" fill="#fef08a"/>
<!-- Corner fold -->
<path d="M38 10 L38 18 L30 10 Z" fill="#fde047"/>
<path d="M30 10 L38 18" stroke="#eab308" stroke-width="1"/>
<!-- Pushpin -->
<circle cx="24" cy="6" r="4" fill="#dc2626"/>
<rect x="23" y="8" width="2" height="6" fill="#94a3b8"/>
<!-- Text lines -->
<text x="12" y="22" font-size="6" fill="#713f12" font-family="sans-serif" font-weight="bold">MEMO</text>
<rect x="12" y="26" width="22" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
<rect x="12" y="30" width="18" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
<rect x="12" y="34" width="20" height="2" rx="1" fill="#ca8a04" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1010 B

View File

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "O Face" - Surprised/amazed face -->
<!-- Face circle -->
<circle cx="24" cy="24" r="20" fill="#fef3c7"/>
<circle cx="24" cy="24" r="20" stroke="#f59e0b" stroke-width="2"/>
<!-- Eyebrows raised -->
<path d="M12 14 Q16 10 20 14" stroke="#78350f" stroke-width="2" fill="none" stroke-linecap="round"/>
<path d="M28 14 Q32 10 36 14" stroke="#78350f" stroke-width="2" fill="none" stroke-linecap="round"/>
<!-- Wide eyes -->
<ellipse cx="16" cy="20" rx="4" ry="5" fill="white"/>
<circle cx="16" cy="20" r="2.5" fill="#1e293b"/>
<ellipse cx="32" cy="20" rx="4" ry="5" fill="white"/>
<circle cx="32" cy="20" r="2.5" fill="#1e293b"/>
<!-- "O" mouth -->
<ellipse cx="24" cy="34" rx="6" ry="8" fill="#dc2626"/>
<ellipse cx="24" cy="33" rx="4" ry="5" fill="#991b1b"/>
<!-- Cheek blush -->
<ellipse cx="10" cy="26" rx="3" ry="2" fill="#fca5a5" opacity="0.6"/>
<ellipse cx="38" cy="26" rx="3" ry="2" fill="#fca5a5" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "PC Load Letter" - Printer with error -->
<rect x="6" y="18" width="36" height="18" rx="3" fill="#64748b"/>
<rect x="8" y="20" width="32" height="14" rx="2" fill="#475569"/>
<!-- Paper tray top -->
<rect x="12" y="8" width="24" height="12" rx="1" fill="#e2e8f0"/>
<rect x="14" y="10" width="20" height="8" rx="1" fill="#f8fafc"/>
<!-- Paper output slot -->
<rect x="14" y="34" width="20" height="8" rx="1" fill="#f8fafc"/>
<!-- Error light -->
<circle cx="36" cy="24" r="3" fill="#dc2626"/>
<!-- Error LCD display -->
<rect x="12" y="22" width="18" height="6" rx="1" fill="#1e293b"/>
<text x="14" y="27" font-size="5" fill="#dc2626" font-family="monospace">PC LOAD</text>
</svg>

After

Width:  |  Height:  |  Size: 783 B

View File

@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "Spreadsheet Survivor" - Excel grid with survivor badge -->
<!-- Sheet background -->
<rect x="6" y="6" width="36" height="36" rx="2" fill="#16a34a"/>
<rect x="8" y="8" width="32" height="32" rx="1" fill="#f8fafc"/>
<!-- Grid lines -->
<line x1="8" y1="16" x2="40" y2="16" stroke="#94a3b8" stroke-width="1"/>
<line x1="8" y1="24" x2="40" y2="24" stroke="#94a3b8" stroke-width="1"/>
<line x1="8" y1="32" x2="40" y2="32" stroke="#94a3b8" stroke-width="1"/>
<line x1="18" y1="8" x2="18" y2="40" stroke="#94a3b8" stroke-width="1"/>
<line x1="28" y1="8" x2="28" y2="40" stroke="#94a3b8" stroke-width="1"/>
<!-- Header row (green) -->
<rect x="8" y="8" width="32" height="8" fill="#16a34a"/>
<!-- Header column (green) -->
<rect x="8" y="8" width="10" height="32" fill="#16a34a" opacity="0.7"/>
<!-- Cell labels -->
<text x="13" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">A</text>
<text x="23" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">B</text>
<text x="34" y="14" font-size="5" fill="white" font-family="sans-serif" text-anchor="middle">C</text>
<!-- Star survivor badge -->
<circle cx="38" cy="38" r="8" fill="#f59e0b"/>
<polygon points="38,32 39.5,36 44,36.5 40.5,39 42,44 38,41 34,44 35.5,39 32,36.5 36.5,36" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "I Have Your Stapler" - Milton's famous red Swingline stapler -->
<!-- Stapler base -->
<rect x="4" y="32" width="40" height="8" rx="2" fill="#1e293b"/>
<rect x="6" y="34" width="36" height="4" rx="1" fill="#334155"/>
<!-- Stapler top (red!) -->
<path d="M6 32 L6 24 Q6 20 10 20 L38 20 Q42 20 42 24 L42 32 Z" fill="#dc2626"/>
<path d="M8 30 L8 25 Q8 22 11 22 L37 22 Q40 22 40 25 L40 30 Z" fill="#ef4444"/>
<!-- Swingline branding area -->
<rect x="14" y="24" width="20" height="4" rx="1" fill="#b91c1c"/>
<!-- Chrome accent -->
<rect x="6" y="30" width="36" height="2" fill="#94a3b8"/>
<!-- Staple loading slot -->
<rect x="16" y="20" width="16" height="2" fill="#991b1b"/>
<!-- Shadow/depth -->
<rect x="4" y="38" width="40" height="2" rx="1" fill="#0f172a" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 890 B

15
src/assets/flair/tps.svg Normal file
View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<!-- "TPS Report Expert" - Clipboard with TPS -->
<!-- Clipboard board -->
<rect x="8" y="8" width="32" height="36" rx="2" fill="#78350f"/>
<!-- Clip -->
<rect x="16" y="4" width="16" height="8" rx="2" fill="#94a3b8"/>
<rect x="18" y="6" width="12" height="4" rx="1" fill="#cbd5e1"/>
<!-- Paper -->
<rect x="12" y="12" width="24" height="28" rx="1" fill="#f8fafc"/>
<!-- TPS text -->
<text x="24" y="24" font-size="10" fill="#1e293b" font-family="sans-serif" font-weight="bold" text-anchor="middle">TPS</text>
<rect x="14" y="28" width="20" height="2" rx="1" fill="#94a3b8"/>
<rect x="14" y="32" width="16" height="2" rx="1" fill="#94a3b8"/>
<rect x="14" y="36" width="18" height="2" rx="1" fill="#94a3b8"/>
</svg>

After

Width:  |  Height:  |  Size: 814 B

View File

@ -0,0 +1,979 @@
---
/**
* FlairBadge - Collect pieces of flair by spending time on key pages
* Adapted for Starlight documentation site
*
* "We need to talk about your flair..." - Stan, Manager
*
* Features:
* - Floating counter in bottom-right corner
* - Opens modal "flair board" showing collection
* - 15-second timer per page to earn flair
* - localStorage persistence across sessions
* - Astro view transition support
*/
import flairConfig from '../data/flair-config.json';
interface Props {
currentPath: string;
}
const { currentPath } = Astro.props;
// Normalize path for matching (handle trailing slashes)
const normalizedPath = currentPath === '/' ? '/' : currentPath.replace(/\/$/, '') + '/';
const altPath = currentPath === '/' ? '/' : currentPath.replace(/\/$/, '');
// Check if current page has flair (try both with and without trailing slash)
const currentFlair = flairConfig.flairs.find(f =>
f.path === normalizedPath || f.path === altPath || f.path === currentPath
);
---
<!-- Floating Flair Counter -->
<button
id="flair-counter"
type="button"
class="flair-counter-btn"
aria-label="Open flair collection"
>
<span class="flair-icon">🎖️</span>
<span class="flair-count">0</span>
<span class="flair-divider">/</span>
<span class="flair-total">{flairConfig.flairs.length}</span>
<!-- Progress ring indicator -->
<span id="flair-progress-indicator" class="progress-indicator hidden"></span>
</button>
<!-- Flair Earned Toast -->
<div id="flair-toast" class="flair-toast hidden-right">
<div class="toast-icon-container">
<img id="toast-image" src="" alt="" class="toast-image hidden" />
<span class="toast-emoji" id="toast-icon">🎉</span>
</div>
<div class="toast-content">
<p class="toast-title">Flair Earned!</p>
<p class="toast-name" id="toast-name">Badge Name</p>
</div>
</div>
<!-- Name Capture Dialog -->
<dialog id="name-dialog" class="name-dialog">
<div class="dialog-content">
<div class="dialog-header">
<span id="name-dialog-icon" class="dialog-icon">🎖️</span>
<h2 id="name-dialog-title" class="dialog-title">You earned your first flair!</h2>
<p id="name-dialog-subtitle" class="dialog-subtitle">What should we call you?</p>
</div>
<form id="name-form" class="name-form">
<input
type="text"
id="visitor-name-input"
placeholder="Your name"
maxlength="30"
class="name-input"
/>
<div class="dialog-buttons">
<button type="button" id="skip-name-btn" class="btn-secondary">Skip</button>
<button type="submit" class="btn-primary">Save</button>
</div>
</form>
</div>
</dialog>
<!-- Flair Board Modal - Stan's Chotchkie's Vest -->
<dialog id="flair-modal" class="flair-modal">
<div class="modal-container">
<!-- Close button -->
<button
type="button"
id="flair-modal-close"
class="modal-close"
aria-label="Close flair board"
>
<svg class="close-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- Vest Container -->
<div class="vest-container">
<!-- Header -->
<div class="vest-header">
<h2 class="vest-title" id="modal-title">Your Flair Collection</h2>
<div class="name-tag-wrapper">
<div class="name-tag">
<span id="vest-name-tag">VISITOR</span>
<button type="button" id="edit-name-btn" class="edit-name-btn" aria-label="Edit your name" title="Edit your name">
<svg class="edit-icon" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931z" />
</svg>
</button>
</div>
</div>
</div>
<!-- Flair Grid -->
<div class="flair-grid">
{flairConfig.flairs.map((flair, index) => {
const rotations = [-5, 3, -2, 6, -4, 2, 5, -3, 4, -6, 3, -2];
const rotation = rotations[index % rotations.length];
return (
<div
class="flair-item"
data-flair-id={flair.id}
data-flair-image={flair.image || ''}
data-flair-placeholder={flair.placeholder}
data-earned="false"
style={`--rotation: ${rotation}deg`}
>
<div class="flair-badge">
{flair.image ? (
<img src={flair.image} alt={flair.name} class="badge-image" />
) : (
<span class="badge-emoji">{flair.placeholder}</span>
)}
</div>
<div class="flair-tooltip">
<span class="tooltip-name">{flair.name}</span>
<span class="tooltip-desc">{flair.description}</span>
</div>
</div>
);
})}
</div>
<!-- Progress Footer -->
<div class="vest-footer">
<div class="progress-counter">
<span class="earned-count" id="modal-earned-count">0</span>
<span class="count-divider">/</span>
<span class="total-count">{flairConfig.flairs.length}</span>
<span class="count-label">pieces of flair</span>
</div>
<div class="stan-quote">
<p class="quote-text">"{flairConfig.stanQuote}"</p>
<p class="quote-attribution">— Stan, Manager</p>
</div>
<!-- Completion message -->
<div id="completion-message" class="completion-message hidden">
<p>{flairConfig.completionMessage}</p>
</div>
<!-- Current page hint -->
{currentFlair && (
<div class="current-page-hint">
<span class="hint-label">Current page:</span>
<span class="hint-name">{currentFlair.name}</span>
<span id="timer-display" class="timer-display"></span>
</div>
)}
</div>
</div>
</div>
</dialog>
<script define:vars={{ flairConfig, currentFlair }}>
// Flair Collection System
const STORAGE_KEY = flairConfig.storageKey;
const REQUIRED_TIME = flairConfig.requiredTime;
let pageTimer = null;
let timeSpent = 0;
let isTimerRunning = false;
// Get/set flair data from localStorage
function getFlairData() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : { earned: [], timestamps: {}, visitorName: null, namePrompted: false };
} catch {
return { earned: [], timestamps: {}, visitorName: null, namePrompted: false };
}
}
function saveFlairData(data) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (e) {
console.warn('Could not save flair data:', e);
}
}
// Check if flair is earned
function hasEarnedFlair(flairId) {
const data = getFlairData();
return data.earned.includes(flairId);
}
// Award flair
function awardFlair(flairId) {
const data = getFlairData();
if (!data.earned.includes(flairId)) {
const isFirstFlair = data.earned.length === 0;
data.earned.push(flairId);
data.timestamps[flairId] = Date.now();
saveFlairData(data);
updateUI();
showToast(flairId);
// Prompt for name on first flair
if (isFirstFlair && !data.namePrompted) {
setTimeout(() => showNamePrompt(), 3500);
}
return true;
}
return false;
}
// Show name prompt dialog
function showNamePrompt(mode = 'first-flair') {
const nameDialog = document.getElementById('name-dialog');
const iconEl = document.getElementById('name-dialog-icon');
const titleEl = document.getElementById('name-dialog-title');
const subtitleEl = document.getElementById('name-dialog-subtitle');
const input = document.getElementById('visitor-name-input');
const skipBtn = document.getElementById('skip-name-btn');
if (!nameDialog) return;
if (mode === 'edit') {
if (iconEl) iconEl.textContent = '✏️';
if (titleEl) titleEl.textContent = 'Update Your Name';
if (subtitleEl) subtitleEl.textContent = 'Change how your flair collection is personalized';
if (skipBtn) skipBtn.textContent = 'Cancel';
const data = getFlairData();
if (input && data.visitorName) {
input.value = data.visitorName;
}
} else {
if (iconEl) iconEl.textContent = '🎖️';
if (titleEl) titleEl.textContent = 'You earned your first flair!';
if (subtitleEl) subtitleEl.textContent = 'What should we call you?';
if (skipBtn) skipBtn.textContent = 'Skip';
if (input) input.value = '';
}
nameDialog.showModal();
if (input) {
input.focus();
if (mode === 'edit') input.select();
}
}
// Save visitor name
function saveVisitorName(name) {
const data = getFlairData();
data.visitorName = name || null;
data.namePrompted = true;
saveFlairData(data);
updateModalTitle();
}
// Update modal title and vest name tag
function updateModalTitle() {
const data = getFlairData();
const titleEl = document.getElementById('modal-title');
const vestNameTag = document.getElementById('vest-name-tag');
if (titleEl && data.visitorName) {
titleEl.textContent = `${data.visitorName}'s Flair Collection`;
} else if (titleEl) {
titleEl.textContent = 'Your Flair Collection';
}
if (vestNameTag) {
vestNameTag.textContent = data.visitorName || 'VISITOR';
}
}
// Show toast notification
function showToast(flairId) {
const flair = flairConfig.flairs.find(f => f.id === flairId);
if (!flair) return;
const toast = document.getElementById('flair-toast');
const toastIcon = document.getElementById('toast-icon');
const toastImage = document.getElementById('toast-image');
const toastName = document.getElementById('toast-name');
if (toast && toastIcon && toastImage && toastName) {
if (flair.image) {
toastImage.src = flair.image;
toastImage.alt = flair.name;
toastImage.classList.remove('hidden');
toastIcon.classList.add('hidden');
} else {
toastIcon.textContent = flair.placeholder;
toastIcon.classList.remove('hidden');
toastImage.classList.add('hidden');
}
toastName.textContent = flair.name;
toast.classList.remove('hidden-right');
setTimeout(() => {
toast.classList.add('hidden-right');
}, 3000);
}
}
// Update all UI elements
function updateUI() {
const data = getFlairData();
const earnedCount = data.earned.length;
const totalCount = flairConfig.flairs.length;
// Update counter
const countEl = document.querySelector('.flair-count');
if (countEl) countEl.textContent = earnedCount.toString();
// Update modal
const modalCount = document.getElementById('modal-earned-count');
if (modalCount) modalCount.textContent = earnedCount.toString();
// Update flair grid
document.querySelectorAll('.flair-item').forEach((item) => {
const flairId = item.getAttribute('data-flair-id');
const isEarned = data.earned.includes(flairId);
item.setAttribute('data-earned', isEarned.toString());
});
// Show completion message if all earned
const completionMsg = document.getElementById('completion-message');
if (completionMsg) {
completionMsg.classList.toggle('hidden', earnedCount < totalCount);
}
}
// Timer display update
function updateTimerDisplay() {
const timerEl = document.getElementById('timer-display');
if (!timerEl || !currentFlair) return;
if (hasEarnedFlair(currentFlair.id)) {
timerEl.textContent = '✓ Earned!';
timerEl.classList.add('earned');
} else if (isTimerRunning) {
const remaining = Math.max(0, REQUIRED_TIME - timeSpent);
timerEl.textContent = `(${remaining}s remaining)`;
timerEl.classList.remove('earned');
}
}
// Start page timer
function startTimer() {
if (!currentFlair || hasEarnedFlair(currentFlair.id)) {
updateTimerDisplay();
return;
}
isTimerRunning = true;
timeSpent = 0;
const indicator = document.getElementById('flair-progress-indicator');
if (indicator) indicator.classList.remove('hidden');
pageTimer = setInterval(() => {
timeSpent++;
updateTimerDisplay();
if (timeSpent >= REQUIRED_TIME) {
stopTimer();
awardFlair(currentFlair.id);
if (indicator) indicator.classList.add('hidden');
}
}, 1000);
updateTimerDisplay();
}
// Stop timer
function stopTimer() {
isTimerRunning = false;
if (pageTimer) {
clearInterval(pageTimer);
pageTimer = null;
}
}
// Initialize
function init() {
updateUI();
updateModalTitle();
// Counter click opens modal
const counter = document.getElementById('flair-counter');
const modal = document.getElementById('flair-modal');
const closeBtn = document.getElementById('flair-modal-close');
if (counter && modal) {
counter.addEventListener('click', () => modal.showModal());
}
if (closeBtn && modal) {
closeBtn.addEventListener('click', () => modal.close());
}
// Edit name button
const editNameBtn = document.getElementById('edit-name-btn');
if (editNameBtn && modal) {
editNameBtn.addEventListener('click', () => {
modal.close();
showNamePrompt('edit');
});
}
// Close modal on backdrop click
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.close();
});
}
// Name form handlers
const nameDialog = document.getElementById('name-dialog');
const nameForm = document.getElementById('name-form');
const skipBtn = document.getElementById('skip-name-btn');
const nameInput = document.getElementById('visitor-name-input');
if (nameForm && nameDialog) {
nameForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = nameInput?.value.trim();
saveVisitorName(name);
nameDialog.close();
});
}
if (skipBtn && nameDialog) {
skipBtn.addEventListener('click', () => {
saveVisitorName(null);
nameDialog.close();
});
}
if (nameDialog) {
nameDialog.addEventListener('click', (e) => {
if (e.target === nameDialog) {
saveVisitorName(null);
nameDialog.close();
}
});
}
// Start timer for current page
startTimer();
}
// Cleanup on page transition
function cleanup() {
stopTimer();
}
// Handle Astro view transitions
document.addEventListener('astro:before-swap', cleanup);
document.addEventListener('astro:page-load', init);
// Initial load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
</script>
<style>
/* Reset and base styles */
.hidden { display: none !important; }
.hidden-right { transform: translateX(150%); }
/* Floating Counter Button */
.flair-counter-btn {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 100;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(8px);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 9999px;
color: white;
font-family: system-ui, sans-serif;
font-size: 0.875rem;
cursor: pointer;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
}
.flair-counter-btn:hover {
background: rgba(30, 41, 59, 0.95);
border-color: rgba(245, 158, 11, 0.5);
transform: scale(1.02);
}
.flair-icon { font-size: 1.125rem; }
.flair-count { font-weight: 600; color: #fbbf24; }
.flair-divider { color: #64748b; }
.flair-total { color: #cbd5e1; }
.progress-indicator {
position: absolute;
top: -0.25rem;
right: -0.25rem;
width: 0.75rem;
height: 0.75rem;
background: #f59e0b;
border-radius: 9999px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Toast Notification */
.flair-toast {
position: fixed;
bottom: 5rem;
right: 1.5rem;
z-index: 100;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: rgba(6, 78, 59, 0.95);
backdrop-filter: blur(8px);
border: 1px solid rgba(16, 185, 129, 0.5);
border-radius: 0.75rem;
color: white;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
transition: transform 0.3s ease;
}
.toast-icon-container {
width: 3rem;
height: 3rem;
display: flex;
align-items: center;
justify-content: center;
}
.toast-image {
width: 3rem;
height: 3rem;
object-fit: contain;
}
.toast-emoji { font-size: 1.5rem; }
.toast-title { font-weight: 600; color: #6ee7b7; margin: 0; }
.toast-name { font-size: 0.875rem; color: #d1fae5; margin: 0; }
/* Dialogs */
.name-dialog,
.flair-modal {
padding: 0;
border: none;
border-radius: 1rem;
background: transparent;
max-width: 90vw;
}
.name-dialog::backdrop,
.flair-modal::backdrop {
background: rgba(0, 0, 0, 0.85);
}
.name-dialog[open],
.flair-modal[open] {
animation: modal-appear 0.3s ease-out;
}
@keyframes modal-appear {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* Name Dialog */
.name-dialog {
max-width: 24rem;
width: 100%;
background: #0f172a;
border: 1px solid rgba(245, 158, 11, 0.5);
}
.dialog-content { padding: 1.5rem; }
.dialog-header {
text-align: center;
margin-bottom: 1.5rem;
}
.dialog-icon {
font-size: 2.5rem;
display: block;
margin-bottom: 1rem;
}
.dialog-title {
font-size: 1.25rem;
font-weight: 700;
color: white;
margin: 0 0 0.5rem;
}
.dialog-subtitle {
font-size: 0.875rem;
color: #94a3b8;
margin: 0;
}
.name-form { display: flex; flex-direction: column; gap: 1rem; }
.name-input {
width: 100%;
padding: 0.75rem 1rem;
background: #1e293b;
border: 1px solid #475569;
border-radius: 0.75rem;
color: white;
font-size: 1rem;
transition: all 0.2s;
}
.name-input:focus {
outline: none;
border-color: #f59e0b;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
}
.name-input::placeholder { color: #64748b; }
.dialog-buttons {
display: flex;
gap: 0.75rem;
}
.btn-secondary,
.btn-primary {
flex: 1;
padding: 0.625rem 1rem;
border-radius: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-secondary {
background: #334155;
color: #cbd5e1;
}
.btn-secondary:hover { background: #475569; }
.btn-primary {
background: #f59e0b;
color: white;
}
.btn-primary:hover { background: #d97706; }
/* Flair Modal */
.flair-modal {
max-width: 32rem;
width: 100%;
}
.modal-container { position: relative; }
.modal-close {
position: absolute;
top: -0.5rem;
right: -0.5rem;
z-index: 10;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
background: #1e293b;
border: 1px solid #475569;
border-radius: 9999px;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
transition: all 0.2s;
}
.modal-close:hover {
background: #dc2626;
color: white;
}
.close-icon { width: 1rem; height: 1rem; }
/* Vest Container */
.vest-container {
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.vest-header {
padding: 1.5rem;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.vest-title {
font-size: 1.125rem;
font-weight: 700;
color: white;
margin: 0;
}
.name-tag-wrapper { display: flex; align-items: center; }
.name-tag {
background: #16a34a;
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 700;
color: white;
letter-spacing: 0.05em;
position: relative;
display: flex;
align-items: center;
gap: 0.5rem;
}
.edit-name-btn {
width: 1.25rem;
height: 1.25rem;
background: rgba(0, 0, 0, 0.3);
border: none;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.edit-name-btn:hover { background: #f59e0b; }
.edit-icon { width: 0.625rem; height: 0.625rem; color: white; }
/* Flair Grid */
.flair-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
padding: 1.5rem;
background: linear-gradient(180deg, #334155 0%, #1e293b 100%);
}
.flair-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
transform: rotate(var(--rotation, 0deg));
transition: transform 0.2s;
}
.flair-item:hover {
transform: rotate(var(--rotation, 0deg)) scale(1.1);
z-index: 5;
}
.flair-badge {
width: 3rem;
height: 3rem;
display: flex;
align-items: center;
justify-content: center;
background: #475569;
border: 2px solid #64748b;
border-radius: 9999px;
overflow: hidden;
transition: all 0.3s;
cursor: pointer;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
}
.flair-item[data-earned="false"] .flair-badge {
filter: grayscale(100%);
opacity: 0.4;
}
.flair-item[data-earned="true"] .flair-badge {
filter: none;
opacity: 1;
background: linear-gradient(145deg, #fcd34d 0%, #f59e0b 50%, #d97706 100%);
border-color: #b45309;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.3),
0 4px 8px rgba(251, 191, 36, 0.3),
inset 0 1px 2px rgba(255, 255, 255, 0.4);
}
.badge-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.badge-emoji { font-size: 1.25rem; }
/* Flair Tooltip */
.flair-tooltip {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 0.5rem;
padding: 0.5rem 0.75rem;
background: rgba(0, 0, 0, 0.95);
border-radius: 0.375rem;
opacity: 0;
visibility: hidden;
transition: all 0.2s;
pointer-events: none;
white-space: nowrap;
z-index: 10;
text-align: center;
}
.flair-item:hover .flair-tooltip {
opacity: 1;
visibility: visible;
}
.tooltip-name {
display: block;
font-size: 0.6875rem;
font-weight: 600;
color: white;
}
.tooltip-desc {
display: block;
font-size: 0.625rem;
color: #94a3b8;
margin-top: 0.125rem;
}
/* Vest Footer */
.vest-footer {
padding: 1.25rem 1.5rem;
text-align: center;
background: #0f172a;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.progress-counter {
display: inline-flex;
align-items: baseline;
gap: 0.25rem;
background: rgba(0, 0, 0, 0.3);
padding: 0.5rem 1rem;
border-radius: 9999px;
margin-bottom: 0.75rem;
}
.earned-count {
font-size: 1.5rem;
font-weight: 700;
color: #fbbf24;
}
.count-divider { color: #64748b; }
.total-count { font-size: 1.125rem; color: #cbd5e1; }
.count-label { font-size: 0.75rem; color: #64748b; margin-left: 0.25rem; }
.stan-quote {
max-width: 16rem;
margin: 0 auto 0.75rem;
}
.quote-text {
font-size: 0.6875rem;
font-style: italic;
color: rgba(251, 191, 36, 0.9);
margin: 0;
line-height: 1.4;
}
.quote-attribution {
font-size: 0.625rem;
color: #64748b;
margin: 0.25rem 0 0;
}
.completion-message {
margin-top: 0.75rem;
padding: 0.5rem 1rem;
background: rgba(16, 185, 129, 0.9);
border: 1px solid #10b981;
border-radius: 0.5rem;
display: inline-block;
}
.completion-message p {
color: white;
font-weight: 600;
font-size: 0.875rem;
margin: 0;
}
.current-page-hint {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.8125rem;
color: #94a3b8;
}
.hint-label { color: #fbbf24; }
.hint-name { color: white; margin-left: 0.25rem; }
.timer-display { margin-left: 0.5rem; color: #64748b; }
.timer-display.earned { color: #10b981; }
/* Responsive */
@media (max-width: 480px) {
.flair-grid {
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
padding: 1rem;
}
.flair-badge {
width: 2.5rem;
height: 2.5rem;
}
.vest-header { padding: 1rem; }
.vest-footer { padding: 1rem; }
}
</style>

View File

@ -0,0 +1,15 @@
---
/**
* Custom Footer component that includes the FlairBadge system
* Wraps Starlight's default Footer
*/
import type { Props } from '@astrojs/starlight/props';
import Default from '@astrojs/starlight/components/Footer.astro';
import FlairBadge from './FlairBadge.astro';
// Get current path for flair matching
const currentPath = Astro.url.pathname;
---
<Default {...Astro.props}><slot /></Default>
<FlairBadge currentPath={currentPath} />

View File

@ -7,83 +7,95 @@
"path": "/", "path": "/",
"name": "I Was Told There Would Be Extraction", "name": "I Was Told There Would Be Extraction",
"placeholder": "📄", "placeholder": "📄",
"image": "/flair/extraction.svg",
"description": "Started your mcwaddams journey" "description": "Started your mcwaddams journey"
}, },
{ {
"id": "backstory", "id": "backstory",
"path": "/backstory", "path": "/backstory/",
"name": "Basement Dweller", "name": "Basement Dweller",
"placeholder": "🔴", "placeholder": "🔴",
"image": "/flair/basement.svg",
"description": "Learned about Milton and the legacy documents" "description": "Learned about Milton and the legacy documents"
}, },
{ {
"id": "installation", "id": "installation",
"path": "/installation", "path": "/installation/",
"name": "PC Load Letter", "name": "PC Load Letter",
"placeholder": "🖨️", "placeholder": "🖨️",
"image": "/flair/printer.svg",
"description": "Successfully installed mcwaddams" "description": "Successfully installed mcwaddams"
}, },
{ {
"id": "quickstart", "id": "quickstart",
"path": "/quickstart", "path": "/quickstart/",
"name": "Case of the Mondays", "name": "Case of the Mondays",
"placeholder": "☕", "placeholder": "☕",
"image": "/flair/coffee.svg",
"description": "Completed the quick start guide" "description": "Completed the quick start guide"
}, },
{ {
"id": "reference", "id": "reference",
"path": "/reference/tools", "path": "/reference/tools/",
"name": "TPS Report Expert", "name": "TPS Report Expert",
"placeholder": "📋", "placeholder": "📋",
"image": "/flair/tps.svg",
"description": "Read the complete tools reference" "description": "Read the complete tools reference"
}, },
{ {
"id": "architecture", "id": "architecture",
"path": "/explanation/architecture", "path": "/explanation/architecture/",
"name": "The Bobs Approved", "name": "The Bobs Approved",
"placeholder": "👔", "placeholder": "👔",
"image": "/flair/bobs.svg",
"description": "Understood the architecture" "description": "Understood the architecture"
}, },
{ {
"id": "dashboard", "id": "dashboard",
"path": "/tps/dashboard", "path": "/tps/dashboard/",
"name": "Did You Get The Memo?", "name": "Did You Get The Memo?",
"placeholder": "📝", "placeholder": "📝",
"image": "/flair/memo.svg",
"description": "Checked the test dashboard" "description": "Checked the test dashboard"
}, },
{ {
"id": "torture", "id": "torture",
"path": "/tps/torture", "path": "/tps/torture/",
"name": "O Face", "name": "O Face",
"placeholder": "😮", "placeholder": "😮",
"image": "/flair/oface.svg",
"description": "Witnessed the torture test results" "description": "Witnessed the torture test results"
}, },
{ {
"id": "credits", "id": "credits",
"path": "/community/credits", "path": "/community/credits/",
"name": "I Have Your Stapler", "name": "I Have Your Stapler",
"placeholder": "🔴", "placeholder": "🔴",
"image": "/flair/stapler.svg",
"description": "Found the credits and attributions" "description": "Found the credits and attributions"
}, },
{ {
"id": "tutorial", "id": "tutorial",
"path": "/tutorials/first-extraction", "path": "/tutorials/first-extraction/",
"name": "Jump to Conclusions", "name": "Jump to Conclusions",
"placeholder": "🎲", "placeholder": "🎲",
"image": "/flair/conclusions.svg",
"description": "Completed your first extraction tutorial" "description": "Completed your first extraction tutorial"
}, },
{ {
"id": "tables", "id": "tables",
"path": "/how-to/extract-tables", "path": "/how-to/extract-tables/",
"name": "Spreadsheet Survivor", "name": "Spreadsheet Survivor",
"placeholder": "📊", "placeholder": "📊",
"image": "/flair/spreadsheet.svg",
"description": "Mastered table extraction" "description": "Mastered table extraction"
}, },
{ {
"id": "collector", "id": "collector",
"path": "/community/leaderboard", "path": "/community/leaderboard/",
"name": "37 Pieces of Flair", "name": "37 Pieces of Flair",
"placeholder": "🎖️", "placeholder": "🎖️",
"image": "/flair/flair-badge.svg",
"description": "Discovered the flair leaderboard" "description": "Discovered the flair leaderboard"
} }
], ],