diff --git a/.gitignore b/.gitignore index c2c69fe..39863d1 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ pnpm-debug.log* docs/designs/_ref .qoder/ +docs/superpowers/ + diff --git a/docs/superpowers/plans/2026-06-22-deploy-anywhere.md b/docs/superpowers/plans/2026-06-22-deploy-anywhere.md deleted file mode 100644 index abadb00..0000000 --- a/docs/superpowers/plans/2026-06-22-deploy-anywhere.md +++ /dev/null @@ -1,1026 +0,0 @@ -# Deploy Anywhere Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -> **Amendment (2026-06-22, post-implementation).** This plan describes the -> original 4-category build. The shipped feature added a 5th category afterward; -> where this plan and the code differ, the code is authoritative: -> - `CategoryId` is `'registry' | 'oss-paas' | 'kubernetes' | 'managed-paas' | 'cloud'` -> (a `kubernetes` "Kubernetes & orchestration" category was added). -> - GitHub star counts show on open-source platforms in **both** `oss-paas` and -> `kubernetes` (the global constraint "ONLY on `oss-paas`" is superseded); -> `starRepos` derives from both, and `FALLBACK_STARS` covers both. -> - Koyeb/Render gained one-click `deployUrl`s (Render `planned → available`). - -**Goal:** Build a `/deploy` page plus a condensed homepage section that present LibreDB Studio's "one open-source image → every layer of the deploy stack" distribution story, with live GitHub star counts on open-source platform cards. - -**Architecture:** A typed data layer (`src/data/`) is the single source of truth for every platform/registry. A build-time helper (`src/lib/github-stars.ts`) fetches star counts (with a baked fallback) and a client script refreshes them live. Astro components (`PlatformCard`, `StatusBadge`) render the data on a full `/deploy` page and a condensed homepage `DeployAnywhere` section. - -**Tech Stack:** Astro 6 (static output), Tailwind CSS v4 (theme tokens in `src/styles/global.css`), TypeScript (strict), `bun` for scripts/tests. - -## Global Constraints - -- Static site — no SSR adapter. Build-time `fetch` runs in Node during `astro build`; the page must render even when offline (fallback stars). -- Match existing visual conventions: dark slate theme, `gradient-text`/`glow`/`glow-sm` utilities, `primary-*` / `accent-*` Tailwind tokens, `font-sans` body. -- No external image hosts — all logos vendored under `public/logos/deploy/`. Missing logo → lettermark fallback, never a broken ``. -- Status taxonomy is exactly: `official` | `available` | `planned`. -- GitHub star counts shown ONLY on category `oss-paas` cards. -- All registries (GHCR image, Docker Hub, Helm/OCI, ArtifactHub, npm) are `available`. -- Official integrations: Railway (`managed-paas`), CapRover (`oss-paas`). Koyeb is `available`. Everything else `planned`. -- Reuse the existing copy-button ` -``` - -Structure check: the file ends with exactly one `` (right after the inline JSON-LD `` block sits OUTSIDE the Layout at the very end of the file. - -- [ ] **Step 2: Build and verify the route renders** - -Run: `bunx astro build` -Expected: build completes; `dist/deploy/index.html` exists. - -- [ ] **Step 3: Verify key content rendered into the HTML** - -Run: -```bash -grep -c "Deploy LibreDB Studio" dist/deploy/index.html && \ -grep -c "data-stars-repo" dist/deploy/index.html && \ -grep -c "data-stars-total" dist/deploy/index.html && \ -grep -c "Railway" dist/deploy/index.html && \ -grep -c "CapRover" dist/deploy/index.html -``` -Expected: each count ≥ 1 (the `data-stars-repo` count should equal the number of oss-paas repos, ≥ 9). - -- [ ] **Step 4: Commit** - -```bash -git add src/pages/deploy.astro -git commit -m "feat: add /deploy page with classification grids and live stars" -``` - ---- - -### Task 5: Homepage DeployAnywhere section - -**Files:** -- Create: `src/components/DeployAnywhere.astro` -- Modify: `src/pages/index.astro` (add import + place `` directly before ``) - -**Interfaces:** -- Consumes: `deployTargets`, `deployCategories` from data; `Layout` not needed (it's a section). -- Produces: `
` on the homepage. No stars here (kept light); links to `/deploy`. - -- [ ] **Step 1: Create the homepage section** - -Create `src/components/DeployAnywhere.astro`: - -```astro ---- -import { deployTargets } from '../data/deploy-targets'; -import { deployCategories } from '../data/deploy-categories'; - -const platformCount = deployTargets.length; -const installMethodCount = deployTargets.filter((t) => t.category === 'registry').length; -const official = deployTargets.filter((t) => t.status === 'official'); - -const countFor = (id: string) => deployTargets.filter((t) => t.category === id).length; -const categories = [...deployCategories].sort((a, b) => a.order - b.order); ---- - -
-
-
-

- Deploy anywhere -

-

- One open-source image, every layer of the deploy stack — {platformCount}+ platforms - and {installMethodCount} install methods. -

-
- - -
- {categories.map((cat) => ( - -
{countFor(cat.id)}
-
{cat.title}
-
- ))} -
- - -
-
- Official one-click apps: - {official.map((t) => ( - - {t.logo && {`${t.name}} - {t.name} - - ))} -
- - See all {platformCount}+ platforms - - -
-
-
-``` - -- [ ] **Step 2: Wire it into the homepage** - -Modify `src/pages/index.astro`. Add the import after the `GetStarted` import (line 11): - -```astro -import GetStarted from '../components/GetStarted.astro'; -import DeployAnywhere from '../components/DeployAnywhere.astro'; -``` - -And place the component directly before `` in the `
`: - -```astro - - - -``` - -- [ ] **Step 3: Build and verify** - -Run: `bunx astro build` -Expected: build completes. - -Run: `grep -c 'id="deploy"' dist/index.html && grep -c "See all" dist/index.html` -Expected: each ≥ 1. - -- [ ] **Step 4: Commit** - -```bash -git add src/components/DeployAnywhere.astro src/pages/index.astro -git commit -m "feat: add Deploy Anywhere homepage section" -``` - ---- - -### Task 6: Wire navigation (Header, Footer) and refresh GetStarted - -**Files:** -- Modify: `src/components/Header.astro` (add Deploy nav link) -- Modify: `src/components/Footer.astro` (add Deploy link to Product section) -- Modify: `src/components/GetStarted.astro:122-149` (replace stale Deploy Buttons block) - -**Interfaces:** -- Consumes: the `/deploy` route from Task 4. No new exports. - -- [ ] **Step 1: Add the Header nav link** - -In `src/components/Header.astro`, in the desktop nav block, add a Deploy link after the Get Started link: - -```astro - Get Started - Deploy -``` - -- [ ] **Step 2: Add the Footer link** - -In `src/components/Footer.astro`, add an entry to the `product` section's `links` array (after "Tech Stack"): - -```astro - { label: "Tech Stack", href: "#tech-stack" }, - { label: "Deploy", href: "/deploy" }, - { label: "Live Demo", href: "https://app.libredb.org", external: true }, -``` - -- [ ] **Step 3: Replace the stale "Deploy Buttons" block in GetStarted** - -In `src/components/GetStarted.astro`, replace the entire `` block (the `div` spanning lines 122-149, containing the "Deploy to Render" and "View on GitHub" buttons) with: - -```astro - -
-

Prefer a one-click deploy?

-

- LibreDB Studio is an official Railway template and CapRover one-click app — and runs - on Docker, Helm, npm, and every major PaaS and cloud. -

- - Explore all deploy options - - -
-``` - -- [ ] **Step 4: Build and verify all links resolve** - -Run: `bunx astro build` -Expected: build completes. - -Run: -```bash -grep -c 'href="/deploy"' dist/index.html -``` -Expected: ≥ 3 (Header link + Footer link + GetStarted pointer + DeployAnywhere CTAs). - -- [ ] **Step 5: Commit** - -```bash -git add src/components/Header.astro src/components/Footer.astro src/components/GetStarted.astro -git commit -m "feat: link /deploy from header, footer, and Get Started" -``` - ---- - -### Task 7: Vendor platform logos - -**Files:** -- Create: `public/logos/deploy/*.svg` (one per `logo:` path declared in `deploy-targets.ts`) - -**Interfaces:** -- Consumes: the `logo` paths referenced in Task 1's data. Targets without a vendored SVG (Easypanel, Cloudron, Northflank, Qovery, Platform.sh, Alibaba) intentionally have NO `logo` field and render the lettermark fallback — do not add files for them. - -The full list of required SVG files (filename = `.svg`): -`ghcr, docker, helm, artifacthub, npm, caprover, coolify, dokploy, portainer, dokku, kubero, kamal, rancher, openshift, appwrite, nhost, cosmos, railway, koyeb, render, fly, netlify, heroku, aws, gcp, azure, digitalocean`. - -- [ ] **Step 1: Create the logos directory** - -Run: `mkdir -p public/logos/deploy` - -- [ ] **Step 2: Add brand SVGs** - -For each slug above, place a brand SVG at `public/logos/deploy/.svg`. Source from the project's brand kits or Simple Icons (https://simpleicons.org, CC0) where a brand mark exists. Each file is a standalone `` (no external refs). Optimize/minify is optional. If a brand SVG genuinely cannot be sourced for one slug, remove that target's `logo:` field in `src/data/deploy-targets.ts` so it falls back to the lettermark (do NOT leave a path pointing at a missing file). - -Example minimal monochrome SVG shape (Docker), to confirm the rendering path works — replace with the real brand mark: - -```svg - - - -``` - -- [ ] **Step 3: Build and verify no broken logo paths** - -Run: `bunx astro build` - -Then verify every referenced logo file exists: -```bash -node -e "import('./src/data/deploy-targets.ts').then(m=>{const fs=require('fs');const missing=m.deployTargets.filter(t=>t.logo&&!fs.existsSync('public'+t.logo)).map(t=>t.logo);console.log(missing.length?('MISSING: '+missing.join(', ')):'all logos present');})" -``` -Expected: `all logos present`. (If `node` cannot import the `.ts` directly, run the same with `bun -e` instead.) - -- [ ] **Step 4: Commit** - -```bash -git add public/logos/deploy -git commit -m "feat: vendor platform logos for /deploy" -``` - ---- - -### Task 8: Full build + final verification - -**Files:** none (verification only). - -- [ ] **Step 1: Run the complete production build (includes the docker-compose sync step)** - -Run: `bun run build` -Expected: build completes with no errors; `dist/deploy/index.html` and updated `dist/index.html` present. - -- [ ] **Step 2: Run all unit tests** - -Run: `bun test` -Expected: all tests from Tasks 1–2 pass. - -- [ ] **Step 3: Manual smoke test (optional but recommended)** - -Run: `bunx astro dev` and open `http://localhost:4321/deploy`. Confirm: hero, stat band, official spotlight (Railway + CapRover), four category grids, star counts visible on oss-paas cards, investor band shows an aggregate, no broken images, no horizontal scroll on mobile widths. - -- [ ] **Step 4: Final commit if anything was adjusted** - -```bash -git add -A -git commit -m "chore: final verification for deploy-anywhere" || echo "nothing to commit" -``` - ---- - -## Self-Review - -**Spec coverage:** -- Purpose / 3 audiences → Task 4 hero + investor band, Task 5 homepage. ✓ -- Files (data, lib, components, page, edits, logos) → Tasks 1–7 map 1:1 to spec §3. ✓ -- Data model → Task 1 (added `slug` for logo filenames; `logo` made optional for lettermark — noted). ✓ -- Platform inventory incl. statuses, official Railway/CapRover, Koyeb available, all registries available → Task 1 data + test assertions. ✓ -- Page sections (hero, primitives/stat band, official spotlight, classification grids, investor band, CTA) → Task 4. (Note: the "primitives strip" from spec §6.2 is represented by the `registry` category grid + stat band rather than a separate strip — a deliberate consolidation to avoid duplicating the same 5 registries twice on one page.) ✓ -- Stars build-time + client refresh + fallback, oss-paas only, aggregate → Task 2 + Task 4 script. ✓ -- Logos vendored + lettermark fallback → Task 3 (fallback) + Task 7 (assets). ✓ -- SEO/JSON-LD ItemList + sitemap → Task 4 (ItemList; sitemap is automatic via `@astrojs/sitemap`). ✓ -- Nav/footer/GetStarted wiring → Task 6. ✓ - -**Placeholder scan:** No "TBD/TODO/handle appropriately". The one example SVG in Task 7 is explicitly labeled a placeholder to replace with the real brand mark, with a defined fallback path — acceptable as it's an asset-sourcing chore, not code logic. ✓ - -**Type consistency:** `DeployTarget`/`DeployStatus`/`CategoryId` defined in Task 1 are imported with identical names in Tasks 3, 4, 5. `formatStars`/`getStars`/`FALLBACK_STARS`/`starRepos` names consistent across Tasks 2, 4. `data-stars-repo` / `data-stars-count` / `data-stars-total` attribute names consistent between PlatformCard (Task 3), deploy.astro markup and its client script (Task 4). ✓ diff --git a/docs/superpowers/plans/2026-06-23-interactive-controls.md b/docs/superpowers/plans/2026-06-23-interactive-controls.md deleted file mode 100644 index d99bf67..0000000 --- a/docs/superpowers/plans/2026-06-23-interactive-controls.md +++ /dev/null @@ -1,1222 +0,0 @@ -# Interactive Controls ("Zero Dead Clicks") Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make every clickable-looking control in the IDE chrome either perform a real action or respond in-character (playful console message) — never a silent dead click. - -**Architecture:** Pure logic lives in small unit-tested modules under `src/scripts/lib/` (console copy map, export serializers, fuzzy filter). DOM wiring lives in `src/scripts/studio.ts` via a single delegated `[data-action]` click handler. Two new Astro components (`Console`, `CommandPalette`) provide markup the script drives. The Explain panel is per-section inside `SectionShell`. Controls declare intent declaratively with `data-action` / `data-notice` attributes. - -**Tech Stack:** Astro 6, Tailwind v4, TypeScript, `bun:test` (run with `bun test`). Build check: `bunx astro build` (NOT `bun run build` — that mutates tracked compose files). - -## Global Constraints -- Design tokens only (no raw hex): surfaces `bg-canvas/panel/raised`, borders `edge/edge-strong/line`, text `fg/bright/muted/dim/faint`, accents `primary/ok/bad/warn/ai/keyword`. Mono everywhere. Sharp corners (6px max on chips/buttons). -- Tone of all console copy: **bold & playful** (verbatim strings in `console-copy.ts` Task 1). -- Demo URL: `https://app.libredb.org`. GitHub: `https://github.com/libredb/libredb-studio`. Docs: `https://github.com/libredb/libredb-studio#readme`. -- Progressive enhancement: with JS off, all section content stays present/navigable; new behaviors simply don't activate; nothing breaks. -- Respect `prefers-reduced-motion` for RUN shimmer and palette transitions. -- Verify with `bunx astro build` (must pass) after each DOM task; never run `bun run build`. -- Section ids/manifest: `src/data/sections.ts` (`sections`, `sectionById`, `SectionMeta`). - -## File Structure -``` -src/scripts/lib/console-copy.ts NEW redirect copy map + types (tested) -src/scripts/lib/console-copy.test.ts NEW -src/scripts/lib/export.ts NEW toJSON/toCSV/serialize (tested) -src/scripts/lib/export.test.ts NEW -src/scripts/lib/filter.ts NEW fuzzyMatch/filterItems (tested) -src/scripts/lib/filter.test.ts NEW -src/components/studio/Console.astro NEW toast container (aria-live) -src/components/studio/CommandPalette.astro NEW ⌘K modal shell -src/scripts/studio.ts MOD console API, palette, run/copy/export/explain, redirect dispatch, search filter -src/data/sections.ts MOD + explain: string per section -src/components/studio/SectionShell.astro MOD + per-section Explain panel -src/components/studio/QueryChrome.astro MOD data-action attrs + inert markup -src/components/studio/MobileQueryCard.astro MOD Explain action -src/components/studio/TopBar.astro MOD ⌘K chip; Monitoring redirect; inert status -src/components/studio/StatusBar.astro MOD inert (cursor/aria) -src/components/studio/Explorer.astro MOD data-explorer-root on root -src/components/sections/*Section.astro MOD emit embedded export JSON payload -src/styles/global.css MOD .is-rerunning shimmer; toast/palette helpers -src/pages/index.astro MOD mount + -``` - ---- - -### Task 1: Console copy map module - -**Files:** -- Create: `src/scripts/lib/console-copy.ts` -- Test: `src/scripts/lib/console-copy.test.ts` - -**Interfaces:** -- Produces: `type ConsoleKind = 'notice'|'error'|'ok'|'comment'`; `interface ConsoleCta { label: string; href: string }`; `interface ConsoleMessage { kind: ConsoleKind; text: string; hint?: string; cta?: ConsoleCta }`; `const NOTICES: Record`. - -- [ ] **Step 1: Write the failing test** - -```ts -// src/scripts/lib/console-copy.test.ts -import { test, expect } from 'bun:test'; -import { NOTICES } from './console-copy'; - -const REQUIRED_KEYS = [ - 'monitoring', 'query', 'save', 'begin', 'sandbox', 'edit', 'import', - 'format', 'clear', 'lines', 'history', 'saved', 'charts', 'autopilot', - 'pivot', 'diff', 'dashboard', 'newtab', 'closetab', -]; -const VALID_KINDS = ['notice', 'error', 'ok', 'comment']; - -test('every required control has a console message', () => { - for (const k of REQUIRED_KEYS) expect(NOTICES[k]).toBeDefined(); -}); - -test('messages are well-formed', () => { - for (const [key, m] of Object.entries(NOTICES)) { - expect(VALID_KINDS).toContain(m.kind); - expect(m.text.length).toBeGreaterThan(0); - if (m.cta) expect(m.cta.href.startsWith('http')).toBe(true); - if (m.cta) expect(m.cta.label.length).toBeGreaterThan(0); - } -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `bun test src/scripts/lib/console-copy.test.ts` -Expected: FAIL — `Cannot find module './console-copy'`. - -- [ ] **Step 3: Write the module** - -```ts -// src/scripts/lib/console-copy.ts -export type ConsoleKind = 'notice' | 'error' | 'ok' | 'comment'; -export interface ConsoleCta { label: string; href: string; } -export interface ConsoleMessage { - kind: ConsoleKind; - text: string; - hint?: string; - cta?: ConsoleCta; -} - -const DEMO = 'https://app.libredb.org'; -const demo = (label = 'Open demo'): ConsoleCta => ({ label, href: DEMO }); - -/** Playful, bold in-character responses for non-functional chrome controls. */ -export const NOTICES: Record = { - monitoring: { kind: 'comment', text: "nothing's on fire here. live monitoring runs in the app", cta: demo() }, - query: { kind: 'notice', text: 'the visual query builder is a live-app superpower', cta: demo() }, - save: { kind: 'error', text: 'ERROR 42501: permission denied — must be superuser', hint: 'superusers save queries in the live demo', cta: demo('Become one') }, - begin: { kind: 'notice', text: 'BEGIN…COMMIT — real transactions, real database. In the app', cta: demo() }, - sandbox: { kind: 'notice', text: 'SANDBOX runs scary queries safely. Try it in the app', cta: demo() }, - edit: { kind: 'comment', text: 'read-only out here; full edit mode lives in the app', cta: demo() }, - import: { kind: 'notice', text: 'drop a .sql / .csv and IMPORT it — in the live app', cta: demo() }, - format: { kind: 'notice', text: 'one-keystroke SQL formatting ships in the app', cta: demo() }, - clear: { kind: 'comment', text: 'nothing to clear on a landing page ;)' }, - lines: { kind: 'comment', text: 'line numbers are bolted on around here' }, - history: { kind: 'notice', text: '↺ every query you run is logged per-workspace — in the app', cta: demo() }, - saved: { kind: 'notice', text: '⭐ star a query to save it — saved queries live in the app', cta: demo() }, - charts: { kind: 'notice', text: '◔ turn any result set into a chart, one click — in the app', cta: demo() }, - autopilot: { kind: 'notice', text: 'Autopilot hunts slow queries and writes the fix. In the app', cta: demo() }, - pivot: { kind: 'notice', text: '▥ pivot any result like a spreadsheet — only works in production', cta: demo() }, - diff: { kind: 'notice', text: '⇄ diff two schemas or result sets side-by-side — in the app', cta: demo() }, - dashboard: { kind: 'notice', text: '▦ pin queries into a live dashboard — build yours in the app', cta: demo() }, - newtab: { kind: 'notice', text: 'more tabs unlock in the app', cta: demo() }, - closetab: { kind: 'comment', text: "you can't close the one thing selling you on us ;)" }, -}; -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `bun test src/scripts/lib/console-copy.test.ts` -Expected: PASS (2 tests). - -- [ ] **Step 5: Commit** - -```bash -git add src/scripts/lib/console-copy.ts src/scripts/lib/console-copy.test.ts -git commit -m "feat: console copy map for in-character control redirects" -``` - ---- - -### Task 2: Export serializers - -**Files:** -- Create: `src/scripts/lib/export.ts` -- Test: `src/scripts/lib/export.test.ts` - -**Interfaces:** -- Produces: `type Row = Record`; `toJSON(rows: Row[]): string`; `toCSV(rows: Row[]): string`; `serialize(rows: Row[], format: 'json'|'csv'): string`. - -- [ ] **Step 1: Write the failing test** - -```ts -// src/scripts/lib/export.test.ts -import { test, expect } from 'bun:test'; -import { toJSON, toCSV, serialize } from './export'; - -test('toJSON pretty-prints rows', () => { - expect(toJSON([{ a: 1 }])).toBe('[\n {\n "a": 1\n }\n]'); -}); - -test('toCSV writes header then rows', () => { - const csv = toCSV([{ name: 'pg', type: 'relational' }, { name: 'redis', type: 'kv' }]); - expect(csv).toBe('name,type\npg,relational\nredis,kv'); -}); - -test('toCSV quotes cells containing comma, quote, or newline', () => { - const csv = toCSV([{ a: 'x,y', b: 'he said "hi"' }]); - expect(csv).toBe('a,b\n"x,y","he said ""hi"""'); -}); - -test('toCSV on empty array is empty string', () => { - expect(toCSV([])).toBe(''); -}); - -test('serialize dispatches by format', () => { - expect(serialize([{ a: 1 }], 'csv')).toBe('a\n1'); - expect(serialize([{ a: 1 }], 'json')).toBe(toJSON([{ a: 1 }])); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `bun test src/scripts/lib/export.test.ts` -Expected: FAIL — module not found. - -- [ ] **Step 3: Write the module** - -```ts -// src/scripts/lib/export.ts -export type Row = Record; - -export function toJSON(rows: Row[]): string { - return JSON.stringify(rows, null, 2); -} - -function csvCell(v: unknown): string { - const s = v == null ? '' : String(v); - return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; -} - -export function toCSV(rows: Row[]): string { - if (rows.length === 0) return ''; - const headers = Object.keys(rows[0]); - const lines = [headers.join(',')]; - for (const r of rows) lines.push(headers.map((h) => csvCell(r[h])).join(',')); - return lines.join('\n'); -} - -export function serialize(rows: Row[], format: 'json' | 'csv'): string { - return format === 'csv' ? toCSV(rows) : toJSON(rows); -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `bun test src/scripts/lib/export.test.ts` -Expected: PASS (5 tests). - -- [ ] **Step 5: Commit** - -```bash -git add src/scripts/lib/export.ts src/scripts/lib/export.test.ts -git commit -m "feat: JSON/CSV export serializers" -``` - ---- - -### Task 3: Fuzzy filter - -**Files:** -- Create: `src/scripts/lib/filter.ts` -- Test: `src/scripts/lib/filter.test.ts` - -**Interfaces:** -- Produces: `fuzzyMatch(query: string, text: string): number | null` (null = no match; lower number = better); `filterItems(query: string, items: T[], keyFn: (it: T) => string): T[]`. - -- [ ] **Step 1: Write the failing test** - -```ts -// src/scripts/lib/filter.test.ts -import { test, expect } from 'bun:test'; -import { fuzzyMatch, filterItems } from './filter'; - -test('empty query matches everything with score 0', () => { - expect(fuzzyMatch('', 'features')).toBe(0); -}); - -test('subsequence matches, non-subsequence does not', () => { - expect(fuzzyMatch('ftr', 'features')).not.toBeNull(); - expect(fuzzyMatch('xyz', 'features')).toBeNull(); -}); - -test('contiguous match scores better (lower) than gapped', () => { - const contiguous = fuzzyMatch('feat', 'features')!; - const gapped = fuzzyMatch('fts', 'features')!; - expect(contiguous).toBeLessThan(gapped); -}); - -test('filterItems returns matches ordered by score, empty query returns all', () => { - const items = ['home', 'features', 'deploy', 'faq']; - expect(filterItems('', items, (s) => s)).toEqual(items); - expect(filterItems('fa', items, (s) => s)).toEqual(['faq', 'features']); - expect(filterItems('zzz', items, (s) => s)).toEqual([]); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `bun test src/scripts/lib/filter.test.ts` -Expected: FAIL — module not found. - -- [ ] **Step 3: Write the module** - -```ts -// src/scripts/lib/filter.ts -/** Case-insensitive subsequence match. Returns null if not a subsequence, - * else a score (sum of gap sizes; lower = tighter = better). */ -export function fuzzyMatch(query: string, text: string): number | null { - const q = query.toLowerCase().trim(); - const t = text.toLowerCase(); - if (q === '') return 0; - let qi = 0; - let score = 0; - let last = -1; - for (let ti = 0; ti < t.length && qi < q.length; ti++) { - if (t[ti] === q[qi]) { - if (last >= 0) score += ti - last - 1; - last = ti; - qi++; - } - } - return qi === q.length ? score : null; -} - -export function filterItems(query: string, items: T[], keyFn: (it: T) => string): T[] { - if (query.trim() === '') return items; - return items - .map((item) => ({ item, score: fuzzyMatch(query, keyFn(item)) })) - .filter((r): r is { item: T; score: number } => r.score !== null) - .sort((a, b) => a.score - b.score) - .map((r) => r.item); -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `bun test src/scripts/lib/filter.test.ts` -Expected: PASS (4 tests). (`filterItems('fa', …)` → `faq` score 0 beats `features` which has a gap.) - -- [ ] **Step 5: Commit** - -```bash -git add src/scripts/lib/filter.ts src/scripts/lib/filter.test.ts -git commit -m "feat: fuzzy subsequence filter for palette + explorer search" -``` - ---- - -### Task 4: Console toast component + JS API - -**Files:** -- Create: `src/components/studio/Console.astro` -- Modify: `src/pages/index.astro` (mount the component) -- Modify: `src/scripts/studio.ts` (add `studioConsole` API) -- Modify: `src/styles/global.css` (toast prefix colors via tokens — only if not expressible with utilities; otherwise none) - -**Interfaces:** -- Consumes: `ConsoleMessage`, `NOTICES` from Task 1. -- Produces: `studioConsole.push(msg: ConsoleMessage)`, `.notice(text, cta?)`, `.error(text, hint?, cta?)`, `.ok(text)`, `.comment(text)` — used by Tasks 5,7,8,9. - -- [ ] **Step 1: Create the container component** - -```astro ---- -// src/components/studio/Console.astro -// Toast/console output. Populated by studioConsole in studio.ts. ---- -
-``` - -- [ ] **Step 2: Mount it in the page** - -In `src/pages/index.astro`, add the import with the other studio imports: -```astro -import Console from '../components/studio/Console.astro'; -``` -And add it just before the closing `` of `.studio` (after ``): -```astro - - -``` - -- [ ] **Step 3: Add the `studioConsole` API to `studio.ts`** - -At the top of `src/scripts/studio.ts`, add the import: -```ts -import type { ConsoleMessage, ConsoleKind } from './lib/console-copy'; -``` -Add this block above `function init()`: -```ts -/* ---- Console toast output ---- */ -const prefixClass: Record = { - notice: 'text-primary', - error: 'text-bad', - ok: 'text-ok', - comment: 'text-faint', -}; -const prefixLabel: Record = { - notice: 'NOTICE', - error: 'ERROR', - ok: '✓', - comment: '--', -}; - -function renderToast(msg: ConsoleMessage): HTMLElement { - const el = document.createElement('div'); - el.className = - 'pointer-events-auto flex w-[min(92vw,440px)] items-start gap-2 border border-edge bg-panel px-3 py-2 text-[12.5px] leading-relaxed shadow-lg'; - el.setAttribute('role', 'status'); - - const body = document.createElement('div'); - body.className = 'flex-1'; - const line = document.createElement('div'); - line.innerHTML = - `${prefixLabel[msg.kind]} ` + - `${escapeHtml(msg.text)}`; - body.appendChild(line); - if (msg.hint) { - const hint = document.createElement('div'); - hint.className = 'text-faint'; - hint.textContent = `HINT: ${msg.hint}`; - body.appendChild(hint); - } - if (msg.cta) { - const cta = document.createElement('a'); - cta.href = msg.cta.href; - cta.target = '_blank'; - cta.rel = 'noopener noreferrer'; - cta.className = 'mt-1 inline-block border border-edge-strong px-2 py-0.5 text-primary hover:bg-raised'; - cta.textContent = `${msg.cta.label} →`; - body.appendChild(cta); - } - el.appendChild(body); - - const close = document.createElement('button'); - close.type = 'button'; - close.setAttribute('aria-label', 'Dismiss'); - close.className = 'shrink-0 text-faint hover:text-fg'; - close.textContent = '✕'; - close.addEventListener('click', () => el.remove()); - el.appendChild(close); - return el; -} - -function escapeHtml(s: string): string { - return s.replace(/&/g, '&').replace(//g, '>'); -} - -const studioConsole = { - push(msg: ConsoleMessage) { - const root = document.querySelector('[data-console]'); - if (!root) return; - const el = renderToast(msg); - root.appendChild(el); - while (root.children.length > 3) root.firstElementChild?.remove(); - const t = window.setTimeout(() => el.remove(), 6000); - el.addEventListener('mouseenter', () => clearTimeout(t)); - }, - notice(text: string, cta?: ConsoleMessage['cta']) { this.push({ kind: 'notice', text, cta }); }, - error(text: string, hint?: string, cta?: ConsoleMessage['cta']) { this.push({ kind: 'error', text, hint, cta }); }, - ok(text: string) { this.push({ kind: 'ok', text }); }, - comment(text: string) { this.push({ kind: 'comment', text }); }, -}; -``` - -- [ ] **Step 4: Build + browser verify** - -Run: `bunx astro build` -Expected: PASS (5 pages). -Then in dev (`bun run dev`, port 4321) open the browser console and run: -```js -// temporary smoke check -document.querySelector('[data-console]') !== null -``` -Expected: `true` (container present). The API is exercised by later tasks. - -- [ ] **Step 5: Commit** - -```bash -git add src/components/studio/Console.astro src/pages/index.astro src/scripts/studio.ts -git commit -m "feat: console toast container + studioConsole output API" -``` - ---- - -### Task 5: Playful redirect dispatch (the biggest coverage win) - -**Files:** -- Modify: `src/components/studio/QueryChrome.astro` (add `data-action="notice"` + `data-notice` to redirect controls; make them ` - - -``` -Change the right-side `BEGIN/SANDBOX/EDIT/IMPORT` block (remove `aria-hidden`, make buttons): -```astro -
- - - - -
-``` -Change the **sub-toolbar** left block (`Format/Copy/Clear/Lines/AI`) — note Copy/AI/Explain become real in later tasks; for now wire the redirects and leave Copy/AI/Explain as placeholders to be wired in Tasks 8/11/10 (give them `data-action` now so markup is final): -```astro -
- - - - - -
-``` -Change the sub-toolbar right block (`✦ Explain` + `⌘+Enter`): -```astro -
- - -
-``` -Change the **result tabs** row: `Results` stays the selected tab (give it `data-action="run-scroll"` is unnecessary — leave as a plain active span); wire the rest. Replace the `resultTabs.map(...)` span loop with explicit buttons: -```astro -
- ▦ Results - - - - - - - - ▤ Docs - - -
-``` -And the result-tabs right side `↑ Export`: -```astro -
- {section.rows} rows • {section.execMs}ms - -
-``` -Change the **tab bar** `README.md`, `✕` (close), `+` (new): -```astro - ▤ README.md -``` -For the active `{section.table}.sql` tab keep as-is, but change its `✕` to a button: -```astro - -``` -And the `+`: -```astro - -``` - -- [ ] **Step 3: Monitoring → notice in TopBar** - -In `src/components/studio/TopBar.astro`, replace the Monitoring span: -```astro - -``` - -- [ ] **Step 4: Build + browser verify** - -Run: `bunx astro build` → PASS. -In dev, click `Save` → a toast appears: `ERROR ERROR 42501… / HINT: superusers… / Become one →`. Click `Autopilot` → NOTICE toast with `Open demo →`. Click `Docs` / `README.md` → opens GitHub in a new tab. Verify in the browser via Playwright: -```js -document.querySelector('[data-notice="save"]').click(); -document.querySelectorAll('[data-console] [role="status"]').length // 1 -``` -Expected: `1` (toast rendered). - -- [ ] **Step 5: Commit** - -```bash -git add src/components/studio/QueryChrome.astro src/components/studio/TopBar.astro src/scripts/studio.ts -git commit -m "feat: in-character console redirects for non-functional chrome controls" -``` - ---- - -### Task 6: Honestly-inert ambient status - -**Files:** -- Modify: `src/components/studio/TopBar.astro`, `StatusBar.astro`, `QueryChrome.astro`, `MobileQueryCard.astro` (mark decorative status non-interactive) - -**Interfaces:** none (visual/markup only). - -- [ ] **Step 1: Mark inert elements** - -Add `cursor-default select-none` and (where purely decorative) `aria-hidden="true"` to: TopBar `PRODUCTION • ONLINE` span, `● Online` span, version span, the `▤ +` brand-adjacent glyph group; the result-meta strip in `QueryChrome.astro` (`● N rows | C columns | EXEC TIME` row — add `cursor-default select-none`); the traffic-light dot groups in `QueryChrome.astro` and `MobileQueryCard.astro` (already `aria-hidden`; add `select-none`); the whole `StatusBar.astro` footer (add `cursor-default select-none` to the root `