Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
656 changes: 656 additions & 0 deletions docs/superpowers/plans/2026-06-14-quickstart-guide.md

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions docs/superpowers/specs/2026-06-14-quickstart-guide-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# First-login Quickstart Guide — Design

**Date:** 2026-06-14
**Status:** Approved

## Overview

A one-time welcome modal that appears the first time a user lands on the
Dashboard after signing up. It walks through a short sequence of slides covering
what OpenThorn is, where to connect a provider, where Templates live, how to open
the Restaurant Landing template, and the core build/deploy flow. Each feature
slide carries an **action button** that closes the modal and navigates the user
to the relevant place. The guide is shown once per account, tracked via a flag on
the `profiles` table.
Comment on lines +8 to +14

The existing inline "Launch checklist" card on the Dashboard
(`DashboardPage.tsx`) stays as the ongoing reference; the quickstart guide
complements it rather than replacing it.

## Show-once mechanism (database)

New migration `supabase/migrations/<timestamp>_add_quickstart_flag.sql`:

```sql
alter table public.profiles
add column if not exists has_seen_quickstart boolean not null default false;

-- Existing users have already used the app — don't re-onboard them.
-- Only accounts created after this migration get the default `false`.
update public.profiles set has_seen_quickstart = true;
```

- New signups receive `has_seen_quickstart = false` from the column default via
the existing `handle_new_user` trigger. Only these users see the guide.
- The existing `profiles_update_own` RLS policy already allows a user to update
their own row, so the client can flip the flag itself. No policy changes.

### Read / dismiss flow (DashboardPage)

- On mount, once `user` is available, query
`select has_seen_quickstart from profiles where id = user.id`.
- If the value is `false`, open the quickstart modal.
- On dismiss (finishing the last slide, closing, or pressing any action button),
optimistically hide the modal in local state and run
`update profiles set has_seen_quickstart = true where id = user.id`.
- Failures to persist are logged via the existing `logError` helper and do not
block the UI (worst case: the guide could reappear on next load).

## Component

New `src/components/QuickstartGuide/QuickstartGuide.tsx` with a co-located
`QuickstartGuide.module.css`, following the existing modal patterns in the
codebase (backdrop + centered card, like the Publish-to-Community modal in
`DashboardPage.tsx` and the Templates preview overlay): click-outside / Escape to
close, design-token colors from `src/index.css`.

**Props:**
- `firstName: string`
- `onClose: () => void` — called for both "finish" and "close"; the parent flips
the DB flag here.

The component owns its own slide state (`step`, prev/next handlers, progress
dots). It receives a `navigate`-style callback (or imports `useNavigate`) so
action buttons can route; each action button calls `onClose` (which persists the
flag) before navigating, so the guide never reappears.

**Slides** (each: a small inline SVG illustration, heading, one or two lines of
copy, and an action button):

1. **Welcome, {firstName}** — OpenThorn builds complete websites from a prompt
using your own AI provider key. → *Next*
2. **Connect a provider** — Your key stays yours (BYOK). Find it under
**Providers** in the sidebar. → *Go to Providers* — `navigate('/providers')`
3. **Browse Templates** — Production-ready starting points live under
**Templates**. → *Open Templates* — `navigate('/templates')`
4. **Try the Restaurant Landing template** — Open Templates, click a card to
preview it, then "Use this template." → *Open Restaurant template* —
`navigate('/templates', { state: { openTemplateId: 'restaurant-landing' } })`
5. **Build & deploy** — Describe your idea in the prompt box and deploy when
ready. → *Get started* (closes the guide)

The guide is rendered by `DashboardPage` as a sibling of the existing modals,
gated on the fetched `has_seen_quickstart` flag being `false`.

## Supporting change: TemplatesPage deep-link

`TemplatesPage` reads `location.state.openTemplateId` on mount and, if present and
matching a known template, auto-opens that template's preview overlay
(`setSelected(...)`). This lets slide 4's action button land the user directly on
the Restaurant Landing preview. The Restaurant template id is `restaurant-landing`
(verified in `src/lib/templates.ts`).

## Out of scope (YAGNI)

- No guided spotlight/coachmark tour over real UI elements.
- No re-show mechanism or "view guide again" entry point.
- No admin toggle or per-tenant configuration.
- No change to the existing inline Launch checklist.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@supabase/supabase-js": "^2.106.2",
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"driver.js": "^1.4.0",
"esbuild-wasm": "^0.28.0",
"framer-motion": "^12.12.1",
"hash-wasm": "^4.12.0",
Expand Down
4 changes: 4 additions & 0 deletions src/components/DashboardSidebar/DashboardSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface DashboardSidebarProps {
interface NavItem {
label: string
icon: ReactNode
tourId?: string
}

const iconSize = 20
Expand All @@ -48,6 +49,7 @@ const mainNavItems: NavItem[] = [
},
{
label: 'Templates',
tourId: 'templates',
icon: (
<svg width={iconSize} height={iconSize} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1"/>
Expand All @@ -70,6 +72,7 @@ const mainNavItems: NavItem[] = [
},
{
label: 'Providers',
tourId: 'providers',
icon: (
<svg width={iconSize} height={iconSize} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
Expand Down Expand Up @@ -193,6 +196,7 @@ export default function DashboardSidebar({ projects = [], activeFilter = 'all',
className={`${styles.navItem} ${isSub ? styles.navItemSub : ''} ${isActive ? styles.navItemActive : ''}`}
onClick={() => handleNavClick(item.label)}
type="button"
data-tour={item.tourId}
>
<span className={styles.navIcon}>{item.icon}</span>
<span className={styles.navLabel}>{item.label}</span>
Expand Down
50 changes: 50 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,56 @@ input, textarea { font-family: inherit; font-size: inherit; color: inherit; }
border-radius: 4px;
}

/* ===== driver.js spotlight tour — dark on-brand theme (popoverClass: openthorn-tour) ===== */
.driver-popover.openthorn-tour {
background: var(--color-surface-raised);
color: var(--color-text);
border: 1px solid var(--color-border-visible);
border-radius: 14px;
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.55);
max-width: 320px;
}
.driver-popover.openthorn-tour .driver-popover-title {
font-size: 1rem;
font-weight: 700;
color: var(--color-text);
}
.driver-popover.openthorn-tour .driver-popover-description {
font-size: 0.875rem;
line-height: 1.6;
color: var(--color-text-secondary);
}
.driver-popover.openthorn-tour .driver-popover-progress-text {
color: var(--color-text-muted);
font-size: 0.75rem;
}
.driver-popover.openthorn-tour .driver-popover-navigation-btns button {
background: var(--color-accent);
color: #fff;
text-shadow: none;
border: none;
border-radius: 8px;
padding: 0.45rem 0.9rem;
font-weight: 600;
}
.driver-popover.openthorn-tour .driver-popover-navigation-btns button:hover {
background: var(--color-accent-glow);
}
.driver-popover.openthorn-tour .driver-popover-prev-btn {
background: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border-visible);
}
.driver-popover.openthorn-tour .driver-popover-prev-btn:hover {
background: rgba(255, 255, 255, 0.06);
}
Comment on lines +149 to +168
.driver-popover.openthorn-tour .driver-popover-close-btn {
color: var(--color-text-muted);
}
.driver-popover.openthorn-tour .driver-popover-arrow {
border-color: var(--color-surface-raised);
}

/* ===== Shared entrance keyframes (global — referenced from page modules) ===== */
@keyframes pageRise {
from { opacity: 0; transform: translateY(18px); }
Expand Down
31 changes: 31 additions & 0 deletions src/lib/__tests__/quickstart.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest'
import { DASHBOARD_TOUR_STEPS, shouldShowQuickstart } from '../quickstart'

describe('shouldShowQuickstart', () => {
it('shows only when the flag is explicitly false', () => {
expect(shouldShowQuickstart(false)).toBe(true)
})
it('does not show when already seen', () => {
expect(shouldShowQuickstart(true)).toBe(false)
})
it('does not show while unknown/loading (null or undefined)', () => {
expect(shouldShowQuickstart(null)).toBe(false)
expect(shouldShowQuickstart(undefined)).toBe(false)
})
})

describe('DASHBOARD_TOUR_STEPS', () => {
it('targets the providers, templates, and prompt anchors', () => {
expect(DASHBOARD_TOUR_STEPS.map((s) => s.element)).toEqual([
'[data-tour="providers"]',
'[data-tour="templates"]',
'[data-tour="prompt"]',
])
})
it('gives every step a title and description', () => {
for (const step of DASHBOARD_TOUR_STEPS) {
expect(step.title.length).toBeGreaterThan(0)
expect(step.description.length).toBeGreaterThan(0)
}
})
})
44 changes: 44 additions & 0 deletions src/lib/dashboard-tour.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { driver } from 'driver.js'
import 'driver.js/dist/driver.css'
import { DASHBOARD_TOUR_STEPS } from './quickstart'

/**
* Runs the first-login dashboard spotlight tour. Steps whose target element is
* not currently in the DOM (e.g. sidebar hidden on small screens) are skipped.
* `onComplete` fires once when the tour finishes or is dismissed — the caller
* uses it to persist the "seen" flag.
*/
export function startDashboardTour(onComplete: () => void): void {
const steps = DASHBOARD_TOUR_STEPS
.filter((s) => document.querySelector(s.element))
.map((s) => ({
element: s.element,
popover: { title: s.title, description: s.description },
}))

if (steps.length === 0) {
onComplete()
return
}

let finished = false
const finishOnce = () => {
if (finished) return
finished = true
onComplete()
}

const tour = driver({
showProgress: true,
allowClose: true,
overlayColor: 'rgba(7, 7, 15, 0.7)',
popoverClass: 'openthorn-tour',
nextBtnText: 'Next',
prevBtnText: 'Back',
doneBtnText: 'Got it',
steps,
onDestroyed: finishOnce,
})

tour.drive()
}
36 changes: 36 additions & 0 deletions src/lib/quickstart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Steps for the first-login dashboard spotlight tour (driver.js). Each step
* points at a real element via a CSS selector and shows a small anchored popover.
*/
export interface DashboardTourStep {
/** CSS selector for the element to spotlight. */
element: string
title: string
description: string
}

export const DASHBOARD_TOUR_STEPS: DashboardTourStep[] = [
{
element: '[data-tour="providers"]',
title: 'Connect a provider',
description:
'Add your own AI provider key here — OpenAI, Anthropic, Gemini and more. Your key stays yours, and you only pay your provider’s raw rates.',
},
{
element: '[data-tour="templates"]',
title: 'Start from a template',
description:
'Browse ready-made templates. Open one to preview it, then “Use this template” to customize it with AI — try the Restaurant Landing template.',
},
{
element: '[data-tour="prompt"]',
title: 'Or describe your idea',
description:
'Tell OpenThorn what you want to build right here, and the agent generates your whole site live in the browser.',
},
]

/** Show the tour only when the persisted flag is explicitly false. */
export function shouldShowQuickstart(hasSeen: boolean | null | undefined): boolean {
return hasSeen === false
}
Loading