Skip to content

Instantly share code, notes, and snippets.

@bojanrajkovic
Last active March 9, 2026 02:14
Show Gist options
  • Select an option

  • Save bojanrajkovic/7cbd0d4527e541d77098771232db32a7 to your computer and use it in GitHub Desktop.

Select an option

Save bojanrajkovic/7cbd0d4527e541d77098771232db32a7 to your computer and use it in GitHub Desktop.
Loupe onboarding flow prototype — interactive 5-card TypeForm-style wizard
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loupe — Onboarding Flow Prototype</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0c0a14;
--card-bg: #1a1625;
--text: #f0eef5;
--accent: #a78bfa;
--accent-hover: #c4b5fd;
--muted: #8b8598;
--border: rgba(167, 139, 250, 0.15);
--success: #4ade80;
--dot-inactive: #3d3650;
--card-radius: 20px;
--transition: 350ms cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: background var(--transition);
}
/* ── Onboarding Container ── */
.onboarding {
position: relative;
width: 100%;
max-width: 640px;
padding: 20px;
}
/* Card wrapper provides stable height for absolutely-positioned cards */
.card-stage {
position: relative;
/* Height is set dynamically by JS */
}
/* ── Card Base ── */
.card {
position: absolute;
top: 0; left: 0; right: 0;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--card-radius);
padding: 48px 48px 40px;
text-align: center;
opacity: 0;
pointer-events: none;
will-change: opacity, transform;
transform: translateY(0);
/* Hidden by default — no transition on parked cards */
}
.card.active {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
z-index: 2;
transition: opacity 400ms ease, transform 400ms ease;
}
.card.exit-up {
opacity: 0;
transform: translateY(-30px);
pointer-events: none;
z-index: 1;
transition: opacity 300ms ease, transform 300ms ease;
}
.card.exit-down {
opacity: 0;
transform: translateY(30px);
pointer-events: none;
z-index: 1;
transition: opacity 300ms ease, transform 300ms ease;
}
/* ── Back Button ── */
.back-btn {
position: absolute;
top: 20px; left: 20px;
background: none;
border: none;
color: var(--muted);
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: color 0.2s, background 0.2s;
z-index: 2;
}
.back-btn:hover { color: var(--text); background: rgba(255,255,255,0.05); }
.back-btn svg { display: block; }
/* ── Typography ── */
.card-icon { margin-bottom: 24px; }
.card-icon svg { display: inline-block; }
h1 {
font-size: 30px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 12px;
color: var(--text);
}
.subtitle {
font-size: 16px;
color: var(--muted);
max-width: 480px;
margin: 0 auto 36px;
line-height: 1.6;
}
/* ── Feature Highlights (Card 1) ── */
.features {
display: flex;
gap: 24px;
justify-content: center;
margin-bottom: 40px;
}
.feature {
flex: 1;
max-width: 160px;
text-align: center;
}
.feature-icon {
width: 56px;
height: 56px;
border-radius: 14px;
background: rgba(167, 139, 250, 0.1);
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 12px;
}
.feature-icon svg { opacity: 0.9; }
.feature-label {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
color: var(--text);
}
.feature-desc {
font-size: 13px;
color: var(--muted);
line-height: 1.4;
}
/* ── Buttons ── */
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
background: var(--accent);
color: #0c0a14;
border: none;
border-radius: 12px;
padding: 14px 36px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
font-family: inherit;
}
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:active { transform: scale(0.98); }
.btn-secondary {
background: rgba(167, 139, 250, 0.1);
color: var(--accent);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 20px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
font-family: inherit;
}
.btn-secondary:hover {
background: rgba(167, 139, 250, 0.15);
border-color: rgba(167, 139, 250, 0.3);
}
.skip-link {
display: block;
margin-top: 16px;
color: var(--muted);
font-size: 14px;
text-decoration: none;
cursor: pointer;
transition: color 0.2s;
background: none;
border: none;
font-family: inherit;
width: 100%;
text-align: center;
}
.skip-link:hover { color: var(--text); }
/* ── Input (Card 2) ── */
.input-group {
display: flex;
gap: 10px;
margin-bottom: 16px;
max-width: 420px;
margin-left: auto;
margin-right: auto;
}
.path-input {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 16px;
color: var(--text);
font-size: 15px;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
outline: none;
transition: border-color 0.2s;
}
.path-input::placeholder { color: var(--dot-inactive); }
.path-input:focus { border-color: var(--accent); }
.browse-btn {
background: rgba(167, 139, 250, 0.1);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 18px;
color: var(--accent);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
font-family: inherit;
white-space: nowrap;
}
.browse-btn:hover { background: rgba(167, 139, 250, 0.18); }
.repo-discovery {
max-width: 420px;
margin: 0 auto 28px;
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 14px;
color: var(--muted);
opacity: 0;
transform: translateY(4px);
transition: opacity 0.3s, transform 0.3s;
}
.repo-discovery.visible {
opacity: 1;
transform: translateY(0);
}
.repo-discovery .count { color: var(--success); font-weight: 600; }
.repo-dots {
display: flex;
gap: 6px;
margin-left: 4px;
}
.repo-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
opacity: 0.5;
}
.repo-dot:nth-child(1) { opacity: 0.9; }
.repo-dot:nth-child(2) { opacity: 0.7; }
.repo-dot:nth-child(3) { opacity: 0.5; }
.repo-dot:nth-child(4) { opacity: 0.3; }
/* ── Theme Swatches (Card 3) ── */
.theme-options {
display: flex;
gap: 16px;
justify-content: center;
margin-bottom: 32px;
}
.theme-swatch {
width: 160px;
border-radius: 14px;
border: 2px solid transparent;
cursor: pointer;
transition: border-color 0.2s, transform 0.15s, box-shadow 0.2s;
overflow: hidden;
background: transparent;
padding: 0;
}
.theme-swatch:hover { transform: translateY(-2px); }
.theme-swatch.selected {
border-color: var(--accent);
box-shadow: 0 0 20px rgba(167, 139, 250, 0.25);
}
.swatch-preview {
height: 88px;
display: flex;
border-radius: 12px 12px 0 0;
overflow: hidden;
}
.swatch-sidebar {
width: 36px;
flex-shrink: 0;
}
.swatch-main {
flex: 1;
display: flex;
flex-direction: column;
padding: 8px;
gap: 4px;
}
.swatch-line {
height: 4px;
border-radius: 2px;
opacity: 0.5;
}
.swatch-line:first-child { width: 60%; opacity: 0.7; }
.swatch-line:nth-child(2) { width: 85%; }
.swatch-line:nth-child(3) { width: 45%; }
.swatch-label-area {
padding: 10px 12px;
text-align: center;
}
.swatch-name {
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.swatch-check {
display: none;
margin-left: 4px;
}
.theme-swatch.selected .swatch-check { display: inline; }
/* Void swatch */
.swatch-void .swatch-sidebar { background: #14101f; }
.swatch-void .swatch-main { background: #1a1625; }
.swatch-void .swatch-line { background: #a78bfa; }
.swatch-void .swatch-label-area { background: #14101f; }
/* Light swatch */
.swatch-light .swatch-sidebar { background: #e8e5f0; }
.swatch-light .swatch-main { background: #faf9fc; }
.swatch-light .swatch-line { background: #6366f1; }
.swatch-light .swatch-label-area { background: #f0eef5; }
.swatch-light .swatch-name { color: #1a1625; }
/* Dark swatch */
.swatch-dark .swatch-sidebar { background: #1a1a1f; }
.swatch-dark .swatch-main { background: #26262c; }
.swatch-dark .swatch-line { background: #2dd4bf; }
.swatch-dark .swatch-label-area { background: #1a1a1f; }
/* ── GitHub Benefits (Card 4) ── */
.benefits {
list-style: none;
margin: 0 auto 32px;
max-width: 280px;
text-align: left;
}
.benefits li {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
font-size: 15px;
color: var(--muted);
}
.benefits .check {
color: var(--success);
font-weight: bold;
font-size: 16px;
flex-shrink: 0;
}
.btn-github {
padding: 16px 40px;
font-size: 17px;
}
.btn-github svg { flex-shrink: 0; }
.skip-link-prominent {
display: block;
margin-top: 20px;
color: var(--muted);
font-size: 15px;
text-decoration: underline;
text-decoration-color: rgba(139, 133, 152, 0.3);
text-underline-offset: 3px;
cursor: pointer;
transition: color 0.2s;
background: none;
border: none;
font-family: inherit;
width: 100%;
text-align: center;
}
.skip-link-prominent:hover { color: var(--text); text-decoration-color: rgba(139, 133, 152, 0.6); }
/* ── Repo Picker (Card 5) ── */
.repo-list {
max-width: 440px;
margin: 0 auto 28px;
text-align: left;
}
.repo-item {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border-radius: 12px;
border: 1.5px solid var(--border);
margin-bottom: 10px;
cursor: pointer;
transition: border-color 0.2s, background 0.2s, transform 0.1s;
background: transparent;
width: 100%;
text-align: left;
font-family: inherit;
color: var(--text);
}
.repo-item:hover {
border-color: rgba(167, 139, 250, 0.3);
background: rgba(167, 139, 250, 0.04);
}
.repo-item.selected {
border-color: var(--accent);
background: rgba(167, 139, 250, 0.08);
}
.repo-item:active { transform: scale(0.99); }
.repo-item-icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(167, 139, 250, 0.1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.repo-item-info { flex: 1; min-width: 0; }
.repo-item-name {
font-size: 15px;
font-weight: 600;
margin-bottom: 2px;
}
.repo-item-detail {
font-size: 13px;
color: var(--muted);
}
.repo-item-branch {
font-size: 12px;
color: var(--accent);
background: rgba(167, 139, 250, 0.1);
padding: 2px 8px;
border-radius: 6px;
flex-shrink: 0;
font-weight: 500;
}
.repo-item-check {
color: var(--accent);
font-size: 18px;
flex-shrink: 0;
opacity: 0;
transition: opacity 0.2s;
}
.repo-item.selected .repo-item-check { opacity: 1; }
.card5-divider {
width: 40px;
height: 1px;
background: var(--border);
margin: 0 auto 16px;
}
.card5-hint {
font-size: 13px;
color: var(--dot-inactive);
margin-bottom: 0;
}
/* ── Progress Dots ── */
.progress-dots {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 32px;
position: relative;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--dot-inactive);
transition: background 0.3s, transform 0.3s;
}
.dot.active {
background: var(--accent);
transform: scale(1.2);
}
/* ── Workspace Page (Post-Onboarding) ── */
.workspace-page {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg);
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 80px;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s ease 0.2s;
}
.workspace-page.visible {
opacity: 1;
pointer-events: auto;
}
.workspace-content {
width: 100%;
max-width: 680px;
padding: 0 20px;
}
.ws-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.ws-title {
font-size: 22px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.ws-settings-btn {
background: none;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
color: var(--muted);
cursor: pointer;
font-size: 13px;
font-family: inherit;
}
.ws-search {
width: 100%;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 16px;
color: var(--text);
font-size: 15px;
outline: none;
margin-bottom: 20px;
font-family: inherit;
}
.ws-search::placeholder { color: var(--dot-inactive); }
.ws-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 12px;
padding: 18px 20px;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
transition: border-color 0.2s;
cursor: pointer;
}
.ws-card:hover { border-color: rgba(167, 139, 250, 0.3); }
.ws-card-name {
font-weight: 600;
font-size: 15px;
margin-bottom: 2px;
}
.ws-card-meta {
font-size: 13px;
color: var(--muted);
}
.ws-card-reviews {
font-size: 13px;
color: var(--accent);
font-weight: 500;
}
.ws-badge {
font-size: 12px;
background: rgba(167, 139, 250, 0.12);
color: var(--accent);
padding: 2px 8px;
border-radius: 6px;
margin-left: 8px;
font-weight: 500;
}
/* ── Restart button ── */
.restart-btn {
position: fixed;
bottom: 24px;
right: 24px;
background: rgba(167, 139, 250, 0.1);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 18px;
color: var(--accent);
font-size: 13px;
cursor: pointer;
font-family: inherit;
transition: background 0.2s;
z-index: 100;
display: none;
}
.restart-btn:hover { background: rgba(167, 139, 250, 0.2); }
.restart-btn.visible { display: block; }
/* ── Prototype Label ── */
.proto-label {
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: var(--dot-inactive);
letter-spacing: 0.05em;
text-transform: uppercase;
pointer-events: none;
z-index: 100;
}
/* ── Keyboard hint ── */
.key-hint {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: var(--dot-inactive);
pointer-events: none;
z-index: 100;
transition: opacity 0.3s;
}
.key-hint kbd {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 4px;
padding: 2px 6px;
font-size: 11px;
font-family: inherit;
}
</style>
</head>
<body>
<div class="proto-label">Loupe Onboarding — Prototype</div>
<div class="key-hint">Navigate: <kbd>Enter</kbd> next &middot; <kbd>Backspace</kbd> back &middot; <kbd>Esc</kbd> skip</div>
<div class="onboarding" id="onboarding">
<div class="card-stage" id="card-stage">
<!-- Card 1: Welcome -->
<div class="card active" id="card-0">
<div class="card-icon">
<svg width="56" height="56" viewBox="0 0 56 56" fill="none">
<rect width="56" height="56" rx="16" fill="rgba(167,139,250,0.1)"/>
<circle cx="24" cy="24" r="10" stroke="#a78bfa" stroke-width="2.5" fill="none"/>
<line x1="31" y1="31" x2="40" y2="40" stroke="#a78bfa" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="24" cy="24" r="4" fill="rgba(167,139,250,0.3)"/>
</svg>
</div>
<h1>Review anything.<br>With an agent at your side.</h1>
<p class="subtitle">Loupe gives you a focused space to review and steer AI-generated work — whether it's code, documentation, or visual designs.</p>
<div class="features">
<div class="feature">
<div class="feature-icon">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
<rect x="3" y="6" width="22" height="16" rx="3" stroke="#a78bfa" stroke-width="1.5" fill="none"/>
<line x1="7" y1="11" x2="15" y2="11" stroke="#a78bfa" stroke-width="1.5" stroke-linecap="round" opacity="0.7"/>
<line x1="7" y1="14.5" x2="21" y2="14.5" stroke="#4ade80" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
<line x1="7" y1="18" x2="12" y2="18" stroke="#f87171" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
</svg>
</div>
<div class="feature-label">Code Review</div>
<div class="feature-desc">Syntax-highlighted diffs with inline commenting</div>
</div>
<div class="feature">
<div class="feature-icon">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
<rect x="5" y="3" width="18" height="22" rx="2.5" stroke="#a78bfa" stroke-width="1.5" fill="none"/>
<line x1="9" y1="8" x2="19" y2="8" stroke="#a78bfa" stroke-width="1.5" stroke-linecap="round" opacity="0.5"/>
<line x1="9" y1="12" x2="17" y2="12" stroke="#a78bfa" stroke-width="1.5" stroke-linecap="round" opacity="0.5"/>
<line x1="9" y1="16" x2="19" y2="16" stroke="#a78bfa" stroke-width="1.5" stroke-linecap="round" opacity="0.5"/>
<line x1="9" y1="20" x2="14" y2="20" stroke="#a78bfa" stroke-width="1.5" stroke-linecap="round" opacity="0.5"/>
</svg>
</div>
<div class="feature-label">Doc Review</div>
<div class="feature-desc">Rendered markdown and prose with tracked changes</div>
</div>
<div class="feature">
<div class="feature-icon">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
<rect x="3" y="5" width="22" height="18" rx="3" stroke="#a78bfa" stroke-width="1.5" fill="none"/>
<circle cx="10" cy="17" r="3" fill="rgba(167,139,250,0.3)" stroke="#a78bfa" stroke-width="1"/>
<polygon points="16,10 22,18 10,18" fill="rgba(167,139,250,0.15)" stroke="#a78bfa" stroke-width="1" stroke-linejoin="round"/>
<circle cx="18" cy="9" r="1.5" fill="#a78bfa" opacity="0.5"/>
</svg>
</div>
<div class="feature-label">Visual Review</div>
<div class="feature-desc">Screenshot annotation and live app preview</div>
</div>
</div>
<button class="btn-primary" onclick="goTo(1)">Get Started</button>
<button class="skip-link" onclick="skipToWorkspace()">Skip</button>
</div>
<!-- Card 2: Root Folder -->
<div class="card" id="card-1">
<button class="back-btn" onclick="goTo(0)" title="Back">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M12 4L6 10L12 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="card-icon">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M6 14V38C6 39.6569 7.34315 41 9 41H39C40.6569 41 42 39.6569 42 38V18C42 16.3431 40.6569 15 39 15H24L20 9H9C7.34315 9 6 10.3431 6 12V14Z" stroke="#a78bfa" stroke-width="2" fill="rgba(167,139,250,0.08)"/>
<path d="M6 14H22L24 15" stroke="#a78bfa" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<h1>Where are your projects?</h1>
<p class="subtitle">Point Loupe to a folder and we'll auto-discover your Git repositories.</p>
<div class="input-group">
<input type="text" class="path-input" id="path-input" placeholder="~/Projects" oninput="onPathInput(this.value)">
<button class="browse-btn" onclick="simulateBrowse()">Browse</button>
</div>
<div class="repo-discovery" id="repo-discovery">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7" stroke="#4ade80" stroke-width="1.5" fill="none"/>
<path d="M5 8L7 10L11 6" stroke="#4ade80" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Found <span class="count">4</span> repositories</span>
<div class="repo-dots">
<div class="repo-dot"></div>
<div class="repo-dot"></div>
<div class="repo-dot"></div>
<div class="repo-dot"></div>
</div>
</div>
<button class="btn-primary" onclick="goTo(2)">Continue</button>
<button class="skip-link" onclick="skipToWorkspace()">Skip</button>
</div>
<!-- Card 3: Theme -->
<div class="card" id="card-2">
<button class="back-btn" onclick="goTo(1)" title="Back">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M12 4L6 10L12 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="card-icon">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
<circle cx="24" cy="24" r="18" stroke="#a78bfa" stroke-width="2" fill="none"/>
<circle cx="18" cy="16" r="5" fill="rgba(167,139,250,0.3)" stroke="#a78bfa" stroke-width="1.5"/>
<circle cx="30" cy="16" r="5" fill="rgba(99,102,241,0.3)" stroke="#6366f1" stroke-width="1.5"/>
<circle cx="24" cy="28" r="5" fill="rgba(45,212,191,0.3)" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>
</div>
<h1>Make it yours</h1>
<p class="subtitle">Choose a color theme. You can always change this later in settings.</p>
<div class="theme-options">
<button class="theme-swatch swatch-void selected" onclick="selectTheme('void', this)" data-theme="void">
<div class="swatch-preview">
<div class="swatch-sidebar"></div>
<div class="swatch-main">
<div class="swatch-line"></div>
<div class="swatch-line"></div>
<div class="swatch-line"></div>
</div>
</div>
<div class="swatch-label-area">
<span class="swatch-name">Void <span class="swatch-check">&#10003;</span></span>
</div>
</button>
<button class="theme-swatch swatch-light" onclick="selectTheme('light', this)" data-theme="light">
<div class="swatch-preview">
<div class="swatch-sidebar"></div>
<div class="swatch-main">
<div class="swatch-line"></div>
<div class="swatch-line"></div>
<div class="swatch-line"></div>
</div>
</div>
<div class="swatch-label-area">
<span class="swatch-name">Light <span class="swatch-check">&#10003;</span></span>
</div>
</button>
<button class="theme-swatch swatch-dark" onclick="selectTheme('dark', this)" data-theme="dark">
<div class="swatch-preview">
<div class="swatch-sidebar"></div>
<div class="swatch-main">
<div class="swatch-line"></div>
<div class="swatch-line"></div>
<div class="swatch-line"></div>
</div>
</div>
<div class="swatch-label-area">
<span class="swatch-name">Dark <span class="swatch-check">&#10003;</span></span>
</div>
</button>
</div>
<button class="btn-primary" onclick="goTo(3)">Continue</button>
<button class="skip-link" onclick="skipToWorkspace()">Skip</button>
</div>
<!-- Card 4: GitHub -->
<div class="card" id="card-3">
<button class="back-btn" onclick="goTo(2)" title="Back">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M12 4L6 10L12 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="card-icon">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
<path d="M24 4C12.954 4 4 12.954 4 24C4 32.837 9.731 40.348 17.677 43.048C18.677 43.228 19.042 42.611 19.042 42.083C19.042 41.611 19.024 40.278 19.016 38.894C13.454 40.189 12.278 36.633 12.278 36.633C11.37 34.267 10.068 33.65 10.068 33.65C8.302 32.439 10.2 32.467 10.2 32.467C12.154 32.6 13.183 34.467 13.183 34.467C14.904 37.467 17.631 36.5 19.087 35.983C19.261 34.722 19.78 33.856 20.35 33.339C15.908 32.817 11.237 31.117 11.237 23.45C11.237 21.233 12.012 19.422 13.217 18.011C13.017 17.489 12.325 15.433 13.4 12.667C13.4 12.667 15.017 12.111 18.996 14.706C20.544 14.244 22.178 14.017 23.8 14.006C25.422 14.017 27.056 14.244 28.604 14.706C32.579 12.111 34.192 12.667 34.192 12.667C35.271 15.433 34.579 17.489 34.379 18.011C35.587 19.422 36.358 21.233 36.358 23.45C36.358 31.133 31.679 32.811 27.225 33.322C27.937 33.95 28.571 35.183 28.571 37.067C28.571 39.767 28.546 41.944 28.546 42.083C28.546 42.617 28.904 43.239 29.921 43.044C37.875 40.339 43.6 32.833 43.6 24C43.6 12.954 35.046 4 24 4Z" fill="#a78bfa"/>
</svg>
</div>
<h1>Connect GitHub</h1>
<p class="subtitle">Link your GitHub account to sync pull requests, comments, and CI status.</p>
<ul class="benefits">
<li><span class="check">&#10003;</span> Sync PR comments and reviews</li>
<li><span class="check">&#10003;</span> View CI status at a glance</li>
<li><span class="check">&#10003;</span> Import team reviews</li>
</ul>
<button class="btn-primary btn-github" onclick="connectGitHub()">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 1C5.03 1 1 5.03 1 10C1 13.97 3.67 17.35 7.35 18.54C7.73 18.61 7.87 18.38 7.87 18.18C7.87 18 7.86 17.42 7.86 16.81C5.49 17.31 5.03 15.56 5.03 15.56C4.69 14.57 4.19 14.34 4.19 14.34C3.49 13.87 4.24 13.88 4.24 13.88C5.01 13.93 5.42 14.67 5.42 14.67C6.1 15.85 7.21 15.47 7.59 15.28C7.66 14.78 7.86 14.44 8.08 14.25C6.18 14.06 4.17 13.3 4.17 9.87C4.17 8.94 4.5 8.17 5.04 7.57C4.96 7.37 4.7 6.5 5.12 5.33C5.12 5.33 5.78 5.12 7.85 6.2C8.5 6.03 9.2 5.94 9.9 5.94C10.6 5.94 11.3 6.03 11.95 6.2C14.02 5.12 14.68 5.33 14.68 5.33C15.1 6.5 14.84 7.37 14.76 7.57C15.31 8.17 15.63 8.94 15.63 9.87C15.63 13.31 13.61 14.06 11.7 14.24C11.98 14.49 12.23 14.97 12.23 15.72C12.23 16.82 12.22 17.71 12.22 18.18C12.22 18.39 12.36 18.62 12.75 18.54C16.33 17.34 19 13.97 19 10C19 5.03 14.97 1 10 1Z" fill="#0c0a14"/>
</svg>
Connect with GitHub
</button>
<button class="skip-link-prominent" onclick="goTo(4)">Skip for now</button>
</div>
<!-- Card 5: First Review -->
<div class="card" id="card-4">
<button class="back-btn" onclick="goTo(3)" title="Back">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M12 4L6 10L12 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="card-icon">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
<rect x="4" y="8" width="40" height="32" rx="4" stroke="#a78bfa" stroke-width="2" fill="none"/>
<line x1="18" y1="8" x2="18" y2="40" stroke="#a78bfa" stroke-width="1.5" opacity="0.3"/>
<line x1="22" y1="16" x2="38" y2="16" stroke="#4ade80" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<line x1="22" y1="22" x2="34" y2="22" stroke="#f87171" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<line x1="22" y1="28" x2="40" y2="28" stroke="#4ade80" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<line x1="22" y1="34" x2="30" y2="34" stroke="#a78bfa" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<circle cx="10" cy="16" r="2" fill="rgba(167,139,250,0.3)"/>
<circle cx="10" cy="22" r="2" fill="rgba(167,139,250,0.3)"/>
<circle cx="10" cy="28" r="2" fill="rgba(167,139,250,0.3)"/>
</svg>
</div>
<h1>Start your first review</h1>
<p class="subtitle">Pick a repo with active branches and jump straight into reviewing.</p>
<div class="repo-list" id="repo-list">
<button class="repo-item" onclick="selectRepo(this, 'loupe')">
<div class="repo-item-icon">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="7" stroke="#a78bfa" stroke-width="1.5" fill="none"/>
<circle cx="9" cy="9" r="3" fill="rgba(167,139,250,0.3)"/>
</svg>
</div>
<div class="repo-item-info">
<div class="repo-item-name">loupe</div>
<div class="repo-item-detail">3 active branches</div>
</div>
<span class="repo-item-branch">feat/onboarding</span>
<span class="repo-item-check">&#10003;</span>
</button>
<button class="repo-item" onclick="selectRepo(this, 'acme-app')">
<div class="repo-item-icon">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<rect x="3" y="3" width="12" height="12" rx="3" stroke="#a78bfa" stroke-width="1.5" fill="none"/>
</svg>
</div>
<div class="repo-item-info">
<div class="repo-item-name">acme-app</div>
<div class="repo-item-detail">1 active branch</div>
</div>
<span class="repo-item-branch">fix/auth-bug</span>
<span class="repo-item-check">&#10003;</span>
</button>
<button class="repo-item" onclick="selectRepo(this, 'design-system')">
<div class="repo-item-icon">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<polygon points="9,2 16,16 2,16" stroke="#a78bfa" stroke-width="1.5" fill="none" stroke-linejoin="round"/>
</svg>
</div>
<div class="repo-item-info">
<div class="repo-item-name">design-system</div>
<div class="repo-item-detail">No active branches</div>
</div>
<span class="repo-item-check">&#10003;</span>
</button>
</div>
<button class="btn-primary" id="review-btn" onclick="launchReview()" disabled style="opacity: 0.5; cursor: default;">Open Review</button>
<div class="card5-divider" style="margin-top: 20px;"></div>
<p class="card5-hint">or go to <button class="skip-link" onclick="skipToWorkspace()" style="display: inline; width: auto; margin: 0; font-size: 13px; text-decoration: underline; text-decoration-color: rgba(139, 133, 152, 0.3); text-underline-offset: 2px;">workspaces</button> to explore on your own</p>
</div>
</div><!-- end card-stage -->
<!-- Progress Dots -->
<div class="progress-dots" id="progress-dots">
<div class="dot active"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</div>
<!-- Workspace Page -->
<div class="workspace-page" id="workspace-page">
<div class="workspace-content">
<div class="ws-header">
<div class="ws-title">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none">
<circle cx="9.5" cy="9.5" r="7" stroke="#a78bfa" stroke-width="2" fill="none"/>
<line x1="14.5" y1="14.5" x2="20" y2="20" stroke="#a78bfa" stroke-width="2" stroke-linecap="round"/>
</svg>
Workspaces
</div>
<button class="ws-settings-btn">Settings</button>
</div>
<input type="text" class="ws-search" placeholder="Search workspaces..." disabled>
<div class="ws-card">
<div>
<div class="ws-card-name">loupe <span class="ws-badge">3 open</span></div>
<div class="ws-card-meta">api-service, frontend &middot; opened today</div>
</div>
<div class="ws-card-reviews">3 open reviews</div>
</div>
<div class="ws-card">
<div>
<div class="ws-card-name">acme-app</div>
<div class="ws-card-meta">backend, mobile, web &middot; 2 days ago</div>
</div>
<div class="ws-card-reviews" style="color: var(--muted);">No reviews</div>
</div>
<div class="ws-card">
<div>
<div class="ws-card-name">design-system</div>
<div class="ws-card-meta">components &middot; 5 days ago</div>
</div>
<div class="ws-card-reviews" style="color: var(--muted);">No reviews</div>
</div>
</div>
</div>
<button class="restart-btn" id="restart-btn" onclick="restart()">&#8634; Restart onboarding</button>
<script>
let current = 0;
const totalCards = 5;
let selectedTheme = 'void';
let selectedRepo = null;
const themes = {
void: { bg: '#0c0a14', cardBg: '#1a1625', text: '#f0eef5', accent: '#a78bfa', muted: '#8b8598' },
light: { bg: '#faf9fc', cardBg: '#ffffff', text: '#1a1625', accent: '#6366f1', muted: '#6b7280' },
dark: { bg: '#18181b', cardBg: '#27272a', text: '#e4e4e7', accent: '#2dd4bf', muted: '#a1a1aa' }
};
const stage = document.getElementById('card-stage');
// Set initial stage height from Card 0
function initStageHeight() {
const card0 = document.getElementById('card-0');
// Temporarily make it position:relative to measure natural height
card0.style.position = 'relative';
stage.style.height = card0.offsetHeight + 'px';
card0.style.position = '';
}
initStageHeight();
window.addEventListener('resize', initStageHeight);
function measureCardHeight(card) {
// Temporarily show card to measure it
const prevOpacity = card.style.opacity;
const prevPosition = card.style.position;
const prevVisibility = card.style.visibility;
card.style.opacity = '0';
card.style.position = 'relative';
card.style.visibility = 'hidden';
const height = card.offsetHeight;
card.style.opacity = prevOpacity;
card.style.position = prevPosition;
card.style.visibility = prevVisibility;
return height;
}
let transitioning = false;
function goTo(index) {
if (index < 0 || index >= totalCards || index === current || transitioning) return;
transitioning = true;
const direction = index > current ? 'up' : 'down';
const oldCard = document.getElementById(`card-${current}`);
const newCard = document.getElementById(`card-${index}`);
// Animate stage height to new card's height
const newHeight = measureCardHeight(newCard);
stage.style.transition = 'height 400ms ease';
stage.style.height = newHeight + 'px';
// Step 1: Exit old card immediately
oldCard.classList.remove('active');
oldCard.classList.add(direction === 'up' ? 'exit-up' : 'exit-down');
// Step 2: After a short delay, bring in the new card
// This creates a staggered effect — old card is mostly gone before new arrives
setTimeout(() => {
// Prepare new card at starting position (no transition yet)
newCard.classList.remove('exit-up', 'exit-down');
newCard.style.transition = 'none';
newCard.style.transform = direction === 'up' ? 'translateY(30px)' : 'translateY(-30px)';
newCard.style.opacity = '0';
// Force reflow, then animate in
newCard.offsetHeight;
newCard.style.transition = '';
newCard.style.transform = '';
newCard.style.opacity = '';
newCard.classList.add('active');
}, 150);
current = index;
updateDots();
// Clean up after full transition
setTimeout(() => {
oldCard.classList.remove('exit-up', 'exit-down');
oldCard.style.transform = '';
oldCard.style.opacity = '';
transitioning = false;
}, 600);
}
function updateDots() {
const dots = document.querySelectorAll('.dot');
dots.forEach((dot, i) => {
dot.classList.toggle('active', i === current);
});
}
function onPathInput(value) {
const discovery = document.getElementById('repo-discovery');
if (value.trim().length > 0) {
discovery.classList.add('visible');
} else {
discovery.classList.remove('visible');
}
}
function simulateBrowse() {
const input = document.getElementById('path-input');
input.value = '~/Projects';
onPathInput(input.value);
input.focus();
}
function selectTheme(theme, el) {
selectedTheme = theme;
document.querySelectorAll('.theme-swatch').forEach(s => s.classList.remove('selected'));
el.classList.add('selected');
// Live preview the theme
const t = themes[theme];
document.body.style.background = t.bg;
document.documentElement.style.setProperty('--bg', t.bg);
document.documentElement.style.setProperty('--card-bg', t.cardBg);
document.documentElement.style.setProperty('--text', t.text);
document.documentElement.style.setProperty('--accent', t.accent);
document.documentElement.style.setProperty('--muted', t.muted);
}
function connectGitHub() {
const btn = document.querySelector('.btn-github');
btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><circle cx="9" cy="9" r="7" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="22" stroke-dashoffset="22"><animate attributeName="stroke-dashoffset" values="22;0" dur="0.6s" fill="freeze"/></circle><path d="M5.5 9L8 11.5L12.5 6.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.3s" begin="0.4s" fill="freeze"/></path></svg> Connected!`;
btn.style.background = '#4ade80';
btn.style.color = '#0c0a14';
setTimeout(() => goTo(4), 1000);
}
function selectRepo(el, name) {
selectedRepo = name;
document.querySelectorAll('.repo-item').forEach(r => r.classList.remove('selected'));
el.classList.add('selected');
const btn = document.getElementById('review-btn');
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
function launchReview() {
if (!selectedRepo) return;
skipToWorkspace();
}
function skipToWorkspace() {
const onboarding = document.getElementById('onboarding');
const workspace = document.getElementById('workspace-page');
const restart = document.getElementById('restart-btn');
const keyHint = document.querySelector('.key-hint');
onboarding.style.transition = 'opacity 0.4s, transform 0.4s';
onboarding.style.opacity = '0';
onboarding.style.transform = 'translateY(-30px)';
keyHint.style.opacity = '0';
setTimeout(() => {
onboarding.style.display = 'none';
workspace.classList.add('visible');
restart.classList.add('visible');
}, 400);
}
function restart() {
// Reset theme
selectTheme('void', document.querySelector('.swatch-void'));
// Reset all cards
document.querySelectorAll('.card').forEach(c => {
c.classList.remove('active', 'exit-up', 'exit-down');
c.style.transform = '';
c.style.opacity = '';
});
// Reset state
current = 0;
transitioning = false;
document.getElementById('card-0').classList.add('active');
initStageHeight();
updateDots();
// Reset path input
document.getElementById('path-input').value = '';
document.getElementById('repo-discovery').classList.remove('visible');
// Reset GitHub button
const btn = document.querySelector('.btn-github');
btn.innerHTML = `<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 1C5.03 1 1 5.03 1 10C1 13.97 3.67 17.35 7.35 18.54C7.73 18.61 7.87 18.38 7.87 18.18C7.87 18 7.86 17.42 7.86 16.81C5.49 17.31 5.03 15.56 5.03 15.56C4.69 14.57 4.19 14.34 4.19 14.34C3.49 13.87 4.24 13.88 4.24 13.88C5.01 13.93 5.42 14.67 5.42 14.67C6.1 15.85 7.21 15.47 7.59 15.28C7.66 14.78 7.86 14.44 8.08 14.25C6.18 14.06 4.17 13.3 4.17 9.87C4.17 8.94 4.5 8.17 5.04 7.57C4.96 7.37 4.7 6.5 5.12 5.33C5.12 5.33 5.78 5.12 7.85 6.2C8.5 6.03 9.2 5.94 9.9 5.94C10.6 5.94 11.3 6.03 11.95 6.2C14.02 5.12 14.68 5.33 14.68 5.33C15.1 6.5 14.84 7.37 14.76 7.57C15.31 8.17 15.63 8.94 15.63 9.87C15.63 13.31 13.61 14.06 11.7 14.24C11.98 14.49 12.23 14.97 12.23 15.72C12.23 16.82 12.22 17.71 12.22 18.18C12.22 18.39 12.36 18.62 12.75 18.54C16.33 17.34 19 13.97 19 10C19 5.03 14.97 1 10 1Z" fill="#0c0a14"/></svg> Connect with GitHub`;
btn.style.background = '';
btn.style.color = '';
// Reset repo selection
selectedRepo = null;
document.querySelectorAll('.repo-item').forEach(r => r.classList.remove('selected'));
const reviewBtn = document.getElementById('review-btn');
reviewBtn.disabled = true;
reviewBtn.style.opacity = '0.5';
reviewBtn.style.cursor = 'default';
// Show onboarding, hide workspace
const onboarding = document.getElementById('onboarding');
const workspace = document.getElementById('workspace-page');
const restartBtn = document.getElementById('restart-btn');
const keyHint = document.querySelector('.key-hint');
workspace.classList.remove('visible');
restartBtn.classList.remove('visible');
onboarding.style.display = '';
onboarding.style.transition = 'opacity 0.3s, transform 0.3s';
onboarding.style.opacity = '0';
onboarding.style.transform = 'translateY(20px)';
keyHint.style.opacity = '';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
onboarding.style.opacity = '1';
onboarding.style.transform = 'translateY(0)';
});
});
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
// Don't capture when typing in input
if (e.target.tagName === 'INPUT') {
if (e.key === 'Enter') {
e.preventDefault();
goTo(current + 1);
}
return;
}
if (e.key === 'Enter' || e.key === 'ArrowDown' || e.key === 'ArrowRight') {
e.preventDefault();
if (current < totalCards - 1) goTo(current + 1);
else skipToWorkspace();
} else if (e.key === 'Backspace' || e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
e.preventDefault();
goTo(current - 1);
} else if (e.key === 'Escape') {
skipToWorkspace();
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment