diff --git a/.gitignore b/.gitignore index c608123..664e68c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ dist/ .vscode/ *.local .superpowers/ + +# Agent/session planning artifacts (not product docs) +docs/superpowers/ diff --git a/docs/superpowers/plans/2026-04-25-devkey-logo.md b/docs/superpowers/plans/2026-04-25-devkey-logo.md deleted file mode 100644 index 490afc3..0000000 --- a/docs/superpowers/plans/2026-04-25-devkey-logo.md +++ /dev/null @@ -1,688 +0,0 @@ -# DevKey Logo 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:** Ship the DevKey logo system — inline Vue component, static SVG mark + favicon, generated raster assets — and wire it into the app shell, README, and meta tags. Replace the scaffolded placeholder branding. - -**Architecture:** A single `DkLogo.vue` component is the canonical inline mark (uses `currentColor`, sized via prop). Static brand SVGs (`devkey-mark.svg`, `favicon.svg`) live in `public/brand/`. Two PNGs (`apple-touch-icon.png`, `og-image.png`) are produced by a one-shot Playwright-driven Node script (`scripts/generate-brand-assets.mjs`) that renders an HTML template with the real Geist webfont and screenshots at exact dimensions. Wordmark uses Geist 800 via the existing `unplugin-fonts` + `@fontsource/*` pipeline. - -**Tech Stack:** Vue 3 + Vite + TypeScript, Vuetify0, `unplugin-fonts` (fontsource provider), Playwright (one-shot raster generation), pnpm. - -**Commit conventions:** This repo follows the user's global rule that `feat`/`fix` are reserved for `packages/*` source. DevKey is a demo app, so all commits in this plan use `chore` (build/asset) or `docs` (README) types. - ---- - -## File Structure - -**Create:** -- `src/components/DkLogo.vue` — inline SVG mark component -- `public/brand/devkey-mark.svg` — standalone mark, `currentColor` -- `public/brand/favicon.svg` — favicon with `prefers-color-scheme` switching -- `public/brand/apple-touch-icon.png` — generated, 180×180 -- `public/brand/og-image.png` — generated, 1200×630 -- `scripts/generate-brand-assets.mjs` — Playwright-driven raster generator -- `scripts/brand-template.html` — static HTML template the generator screenshots - -**Modify:** -- `package.json` — add `@fontsource/geist`, add Playwright dev dep, add `gen:brand` script -- `vite.config.mts` — register Geist 800 with `unplugin-fonts` -- `index.html` — replace favicon link, add apple-touch-icon, update og:image path -- `src/pages/LandingPage.vue` — add hero brand lockup -- `src/pages/LoginPage.vue` — add mark to login card header -- `src/pages/DashboardPage.vue` — add mark + wordmark to dashboard header -- `README.md` — add brand header - -**Delete:** -- `src/assets/logo.png` -- `src/assets/logo.svg` -- `public/0.png` -- `public/og-image.png` (replaced by `public/brand/og-image.png`) - ---- - -## Task 1: Add Geist font and create the inline mark component - -**Files:** -- Modify: `package.json` -- Modify: `vite.config.mts` -- Create: `src/components/DkLogo.vue` - -- [ ] **Step 1: Install Geist font** - -```bash -cd ~/sites/devkey && pnpm add @fontsource/geist -``` - -Expected: `@fontsource/geist` added to `dependencies` in `package.json`. Lockfile updated. - -- [ ] **Step 2: Register Geist 800 in vite.config.mts** - -Open `vite.config.mts` and update the `Fonts` plugin call. Add a Geist family entry beside the existing Inter entry. - -```ts -Fonts({ - fontsource: { - families: [ - { - name: 'Inter', - weights: [100, 300, 400, 500, 700, 900], - styles: ['normal', 'italic'], - }, - { - name: 'Geist', - weights: [800], - styles: ['normal'], - }, - ], - }, -}), -``` - -- [ ] **Step 3: Create the DkLogo component** - -Write `src/components/DkLogo.vue`. The mark uses `currentColor` for both stroke and fill so the parent context controls color. Size defaults to 32. - -```vue - - - -``` - -- [ ] **Step 4: Verify the build still passes** - -```bash -cd ~/sites/devkey && pnpm build -``` - -Expected: PASS — `vue-tsc` succeeds, vite emits `dist/` with no errors. The new component compiles even though it's not referenced anywhere yet. - -- [ ] **Step 5: Commit** - -```bash -cd ~/sites/devkey && git add package.json pnpm-lock.yaml vite.config.mts src/components/DkLogo.vue -git commit -m "chore(brand): add Geist font and DkLogo component" -``` - ---- - -## Task 2: Create static SVG brand assets - -**Files:** -- Create: `public/brand/devkey-mark.svg` -- Create: `public/brand/favicon.svg` - -- [ ] **Step 1: Create the public/brand directory** - -```bash -mkdir -p ~/sites/devkey/public/brand -``` - -- [ ] **Step 2: Create the mark SVG** - -Write `public/brand/devkey-mark.svg`: - -```xml - - - - - - - -``` - -- [ ] **Step 3: Create the favicon SVG with auto dark/light** - -The favicon needs an actual color since browsers don't reliably propagate `currentColor` to favicon contexts. Use a ` - - - - - - - - -``` - -- [ ] **Step 4: Visually verify in a browser** - -```bash -cd ~/sites/devkey && pnpm dev -``` - -Open `http://localhost:3000/brand/devkey-mark.svg` and `http://localhost:3000/brand/favicon.svg`. The mark.svg will appear black against white (no parent `color`) — that's correct, it's intentional for inline embedding. favicon.svg should render brass. - -Stop the dev server with Ctrl+C. - -- [ ] **Step 5: Commit** - -```bash -cd ~/sites/devkey && git add public/brand/devkey-mark.svg public/brand/favicon.svg -git commit -m "chore(brand): add mark and favicon SVGs" -``` - ---- - -## Task 3: Build the brand asset generation script - -**Files:** -- Create: `scripts/brand-template.html` -- Create: `scripts/generate-brand-assets.mjs` -- Modify: `package.json` - -The script renders an HTML template in headless Chromium (via Playwright) at the exact target dimensions, then captures a screenshot of a specific element. The wordmark uses the real Geist webfont so the raster is pixel-perfect. - -The template renders both variants (apple-touch-icon and og-image) into the DOM at page load — visibility is controlled by a `data-variant` attribute on ``, which the script sets via Playwright before each screenshot. This keeps everything as static HTML (no runtime DOM construction). - -- [ ] **Step 1: Create the HTML render template** - -Write `scripts/brand-template.html`: - -```html - - - - - - - - - -
- - - - - - - -
- -
- - - - - - - - DevKey -
- - - -``` - -- [ ] **Step 2: Create the generation script** - -Write `scripts/generate-brand-assets.mjs`: - -```js -import { chromium } from 'playwright' -import { fileURLToPath } from 'node:url' -import { dirname, resolve } from 'node:path' - -const here = dirname(fileURLToPath(import.meta.url)) -const templatePath = resolve(here, 'brand-template.html') -const outDir = resolve(here, '..', 'public', 'brand') - -const targets = [ - { variant: 'apple', selector: '.stage.apple', file: 'apple-touch-icon.png', width: 180, height: 180 }, - { variant: 'og', selector: '.stage.og', file: 'og-image.png', width: 1200, height: 630 }, -] - -async function main () { - const browser = await chromium.launch() - const context = await browser.newContext({ deviceScaleFactor: 2 }) - const page = await context.newPage() - - for (const target of targets) { - await page.setViewportSize({ width: target.width, height: target.height }) - await page.goto(`file://${templatePath}`) - await page.evaluate(variant => { - document.body.setAttribute('data-variant', variant) - }, target.variant) - await page.evaluate(() => document.fonts.ready) - const element = await page.$(target.selector) - const out = resolve(outDir, target.file) - await element.screenshot({ path: out, omitBackground: false }) - console.log(`wrote ${out}`) - } - - await browser.close() -} - -main().catch(err => { - console.error(err) - process.exit(1) -}) -``` - -- [ ] **Step 3: Add Playwright as a devDependency** - -```bash -cd ~/sites/devkey && pnpm add -D playwright -cd ~/sites/devkey && pnpm exec playwright install chromium -``` - -Expected: Playwright installed; Chromium browser binaries downloaded. - -- [ ] **Step 4: Add the gen:brand script to package.json** - -Open `package.json`. In the `"scripts"` block, add `"gen:brand"` after `"preview"`: - -```json -"scripts": { - "dev": "vite", - "build": "run-p type-check \"build-only {@}\" --", - "preview": "vite preview", - "gen:brand": "node scripts/generate-brand-assets.mjs", - "build-only": "vite build", - "type-check": "vue-tsc --build --force" -}, -``` - -- [ ] **Step 5: Run the generator and verify outputs** - -```bash -cd ~/sites/devkey && pnpm gen:brand -``` - -Expected console output: - -``` -wrote /home/john/sites/devkey/public/brand/apple-touch-icon.png -wrote /home/john/sites/devkey/public/brand/og-image.png -``` - -Then check dimensions: - -```bash -file ~/sites/devkey/public/brand/apple-touch-icon.png ~/sites/devkey/public/brand/og-image.png -``` - -Expected: both files report PNG format. apple-touch-icon at 360×360 (2× DPR of 180×180), og-image at 2400×1260 (2× DPR of 1200×630). - -- [ ] **Step 6: Visual sanity check** - -Open both PNGs in an image viewer (or have the user open them): - -```bash -xdg-open ~/sites/devkey/public/brand/apple-touch-icon.png & -xdg-open ~/sites/devkey/public/brand/og-image.png & -``` - -Confirm: -- `apple-touch-icon.png`: dark ink tile, brass mark centered, rounded corners -- `og-image.png`: dark background, brass mark on the left, "DevKey" wordmark in Geist 800 (compressed, distinctive geometry — not Inter) - -If the OG image text appears in a generic system font instead of Geist, the Google Fonts request didn't load before the screenshot. Re-run; if it persists, increase wait by adding `await page.waitForTimeout(500)` after the `document.fonts.ready` promise. - -- [ ] **Step 7: Commit** - -```bash -cd ~/sites/devkey && git add scripts/ package.json pnpm-lock.yaml public/brand/apple-touch-icon.png public/brand/og-image.png -git commit -m "chore(brand): add asset generation script and rasters" -``` - ---- - -## Task 4: Wire favicon, apple-touch-icon, and OG image in index.html - -**Files:** -- Modify: `index.html` - -- [ ] **Step 1: Update favicon and add apple-touch-icon** - -In `index.html`, find this line: - -```html - -``` - -Replace with: - -```html - - -``` - -- [ ] **Step 2: Update OG and Twitter image paths** - -In `index.html`, find: - -```html - -``` - -Replace with: - -```html - -``` - -Then find: - -```html - -``` - -Replace with: - -```html - -``` - -- [ ] **Step 3: Verify in browser** - -```bash -cd ~/sites/devkey && pnpm dev -``` - -Open `http://localhost:3000`. Browser tab favicon should be the brass key. In DevTools → Network, reload and confirm `/brand/favicon.svg` returns 200. (Apple-touch-icon only loads under iOS UA, so not testable here directly — just verify the link tag is present in the rendered HTML.) - -Stop dev server. - -- [ ] **Step 4: Commit** - -```bash -cd ~/sites/devkey && git add index.html -git commit -m "chore(brand): wire favicon and og image to brand assets" -``` - ---- - -## Task 5: Place DkLogo across the app pages - -**Files:** -- Modify: `src/pages/LandingPage.vue` -- Modify: `src/pages/LoginPage.vue` -- Modify: `src/pages/DashboardPage.vue` - -The component goes in different visual contexts on each page. Read each page's full template before editing to find the right insertion point — class names below assume the existing BEM-style scoping (`.dk-landing__*`, `.dk-login__*`, `.dk-dashboard__*`); confirm against actual file content. - -- [ ] **Step 1: Add DkLogo + wordmark to LandingPage hero** - -Read `src/pages/LandingPage.vue` first to identify the hero section. - -In the ` + + + + diff --git a/src/components/DkCheckbox.vue b/src/components/DkCheckbox.vue new file mode 100644 index 0000000..818cb49 --- /dev/null +++ b/src/components/DkCheckbox.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/components/DkCreateKeyDialog.vue b/src/components/DkCreateKeyDialog.vue new file mode 100644 index 0000000..fbe90be --- /dev/null +++ b/src/components/DkCreateKeyDialog.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/components/DkDialog.vue b/src/components/DkDialog.vue new file mode 100644 index 0000000..cbcbca5 --- /dev/null +++ b/src/components/DkDialog.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/src/components/DkForm.vue b/src/components/DkForm.vue new file mode 100644 index 0000000..f912b3f --- /dev/null +++ b/src/components/DkForm.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/src/components/DkLayout.vue b/src/components/DkLayout.vue index 8462dca..5803694 100644 --- a/src/components/DkLayout.vue +++ b/src/components/DkLayout.vue @@ -1,6 +1,7 @@ + + + + diff --git a/src/components/DkPagination.vue b/src/components/DkPagination.vue new file mode 100644 index 0000000..1324add --- /dev/null +++ b/src/components/DkPagination.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/src/components/DkSelect.vue b/src/components/DkSelect.vue new file mode 100644 index 0000000..1dcc6e8 --- /dev/null +++ b/src/components/DkSelect.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/components/DkSnackbar.vue b/src/components/DkSnackbar.vue new file mode 100644 index 0000000..a5dedec --- /dev/null +++ b/src/components/DkSnackbar.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/src/components/DkSwitch.vue b/src/components/DkSwitch.vue new file mode 100644 index 0000000..eeae1ab --- /dev/null +++ b/src/components/DkSwitch.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/src/components/DkTable.vue b/src/components/DkTable.vue index 6ce339e..c1d78b3 100644 --- a/src/components/DkTable.vue +++ b/src/components/DkTable.vue @@ -1,30 +1,38 @@ @@ -84,10 +155,25 @@ } .dk-table__toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; padding: 1rem; border-bottom: 1px solid var(--v0-theme-border); } + .dk-table__bulk { + padding: 0.375rem 0.75rem; + border: 1px solid var(--v0-theme-error); + border-radius: 0.375rem; + background: transparent; + color: var(--v0-theme-error); + font-size: 0.8125rem; + cursor: pointer; + white-space: nowrap; + } + .dk-table__grid { width: 100%; border-collapse: collapse; @@ -108,6 +194,11 @@ color: var(--v0-theme-text); } + .dk-table__check { + width: 1px; + white-space: nowrap; + } + .dk-table__grid code { font-size: 0.8125rem; background: var(--v0-theme-background); @@ -115,30 +206,31 @@ border-radius: 0.25rem; } - .dk-table__footer { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 1rem; - font-size: 0.875rem; - color: var(--v0-theme-muted); - } - - .dk-table__nav { + .dk-table__actions { display: flex; gap: 0.5rem; } - .dk-table__nav button { - padding: 0.375rem 0.75rem; + .dk-table__actions button { + padding: 0.25rem 0.5rem; border: 1px solid var(--v0-theme-border); border-radius: 0.375rem; background: var(--v0-theme-surface); + color: var(--v0-theme-text); + font-size: 0.75rem; cursor: pointer; } - .dk-table__nav button:disabled { - opacity: 0.5; - cursor: not-allowed; + .dk-table__danger { + color: var(--v0-theme-error); + } + + .dk-table__footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + font-size: 0.875rem; + color: var(--v0-theme-muted); } diff --git a/src/components/DkTooltip.vue b/src/components/DkTooltip.vue new file mode 100644 index 0000000..4b33022 --- /dev/null +++ b/src/components/DkTooltip.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/composables/useKeys.ts b/src/composables/useKeys.ts new file mode 100644 index 0000000..0a2560b --- /dev/null +++ b/src/composables/useKeys.ts @@ -0,0 +1,81 @@ +import { useStorage } from '@vuetify/v0' +import type { Ref } from 'vue' + +export interface ApiKey { + id: string + name: string + key: string + env: 'live' | 'test' + autoRotate: boolean + expiresInDays: number + created: string + lastUsed: string +} + +export interface CreateKeyInput { + name: string + env: 'live' | 'test' + autoRotate: boolean + expiresInDays: number +} + +// NOTE: persisting "secret" keys to localStorage is unrealistic for a real +// product — this is a demonstration of useStorage driving CRUD, not a security +// pattern. +const SEED: ApiKey[] = [ + { id: 'seed-1', name: 'Production', key: 'dk_live_abc123', env: 'live', autoRotate: true, expiresInDays: 90, created: '2026-01-15', lastUsed: '2026-04-05' }, + { id: 'seed-2', name: 'Staging', key: 'dk_test_def456', env: 'test', autoRotate: false, expiresInDays: 30, created: '2026-02-20', lastUsed: '2026-04-04' }, + { id: 'seed-3', name: 'CI/CD', key: 'dk_live_ghi789', env: 'live', autoRotate: true, expiresInDays: 90, created: '2026-03-01', lastUsed: '2026-04-05' }, + { id: 'seed-4', name: 'Development', key: 'dk_test_jkl012', env: 'test', autoRotate: false, expiresInDays: 30, created: '2026-03-10', lastUsed: '2026-04-03' }, + { id: 'seed-5', name: 'Mobile App', key: 'dk_live_mno345', env: 'live', autoRotate: false, expiresInDays: 365, created: '2026-03-15', lastUsed: '2026-04-05' }, +] + +function today (): string { + return new Date().toISOString().slice(0, 10) +} + +function uid (): string { + return `key_${Math.random().toString(36).slice(2, 10)}` +} + +function generate (env: 'live' | 'test'): string { + return `dk_${env}_${Math.random().toString(36).slice(2, 10)}` +} + +export function useKeys () { + const storage = useStorage() + const all = storage.get('devkey:keys', SEED) as Ref + + function add (input: CreateKeyInput) { + all.value = [ + { + id: uid(), + name: input.name, + key: generate(input.env), + env: input.env, + autoRotate: input.autoRotate, + expiresInDays: input.expiresInDays, + created: today(), + lastUsed: today(), + }, + ...all.value, + ] + } + + function remove (id: string) { + all.value = all.value.filter(k => k.id !== id) + } + + function removeMany (ids: string[]) { + const set = new Set(ids) + all.value = all.value.filter(k => !set.has(k.id)) + } + + function rotate (id: string) { + all.value = all.value.map(k => + k.id === id ? { ...k, key: generate(k.env), lastUsed: today() } : k, + ) + } + + return { all, add, remove, removeMany, rotate } +} diff --git a/src/pages/DashboardPage.vue b/src/pages/DashboardPage.vue index e564326..88bd8f1 100644 --- a/src/pages/DashboardPage.vue +++ b/src/pages/DashboardPage.vue @@ -1,5 +1,6 @@