From f3ce46e252e235dd93f1ef2da329295abfa5cc53 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 17 Mar 2026 22:00:42 -0700 Subject: [PATCH] =?UTF-8?q?feat(web):=20implement=20design=20spec=20?= =?UTF-8?q?=E2=80=94=20peregrine.css,=20sidebar=20nav,=20HomeView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies the full design spec from circuitforge-plans/peregrine/2026-03-03-nuxt-design-system.md: CSS tokens: - Falcon Blue (#2B6CB0 / #68A8D8 dark) — was incorrectly using forest green - Talon Orange (#E06820 / #F6872A dark) with --app-accent-text dark navy (never white) - Full pipeline status token set (--status-pending/approve/reject/applied/synced/...) - Match score tokens, motion tokens, type scale tokens - Dark mode + hacker mode overrides AppNav: sidebar layout (replaces top bar) - Desktop ≥1024px: persistent sidebar with brand, links, hacker-exit, settings footer - Mobile <1024px: bottom tab bar with 5 primary destinations - Click-the-bird easter egg (9.6): 5 rapid clicks → ruffle animation - Heroicons via @heroicons/vue/24/outline App.vue: - Skip-to-content link (a11y) - Sidebar margin-left layout (desktop) / tab bar clearance (mobile) HomeView: full dashboard implementation - Pipeline metric cards (Pending/Approved/Applied/Synced/Rejected) with status colors - Primary workflow buttons (Run Discovery, Sync Emails, Score Unscored) + sync banner - Auto-enrichment status row - Backlog management (conditionally visible) - Add Jobs by URL / CSV upload tabs - Advanced/danger zone in collapsible
- Stoop speed toast easter egg (9.2) - Midnight mode greeting easter egg (9.7) WorkflowButton component with loading spinner, proper touch targets (min-height 44px) Pinia jobs store (setup form) with counts + system status Build: clean 2.28s, 0 errors --- web/src/App.vue | 75 ++-- web/src/assets/peregrine.css | 101 ++++- web/src/components/AppNav.vue | 317 ++++++++++++-- web/src/components/WorkflowButton.vue | 96 +++++ web/src/stores/jobs.ts | 50 +++ web/src/views/HomeView.vue | 597 +++++++++++++++++++++++++- 6 files changed, 1129 insertions(+), 107 deletions(-) create mode 100644 web/src/components/WorkflowButton.vue create mode 100644 web/src/stores/jobs.ts diff --git a/web/src/App.vue b/web/src/App.vue index 8b1aa84..04a41e2 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,22 +1,18 @@ diff --git a/web/src/assets/peregrine.css b/web/src/assets/peregrine.css index 1669513..c580039 100644 --- a/web/src/assets/peregrine.css +++ b/web/src/assets/peregrine.css @@ -1,52 +1,113 @@ /* web/src/assets/peregrine.css - Peregrine token overrides — imported AFTER theme.css. - Only overrides what is genuinely different from the CircuitForge base theme. + Peregrine-specific token overrides — imported AFTER theme.css. + Source of truth: circuitforge-plans/peregrine/2026-03-03-nuxt-design-system.md - App colors: - Primary — Forest Green (#2d5a27) — inherited from theme.css --color-primary - Accent — Amber/Copper (#c4732a) — inherited from theme.css --color-accent + Brand: + Falcon Blue (#2B6CB0) — slate-blue back plumage of the peregrine falcon + Talon Orange (#E06820) — vivid orange-yellow talons and cere */ -/* ── Page-level overrides ───────────────────────────── */ +/* ── Page-level ─────────────────────────────────────── */ html { - /* Prevent Mac Chrome horizontal swipe-to-navigate on viewport edge */ overscroll-behavior-x: none; - /* clip (not hidden) — no BFC scroll-container side effect. Gotcha #3. */ - overflow-x: clip; + overflow-x: clip; /* clip (not hidden) — no BFC scroll-container side effect. Gotcha #3. */ } body { - /* Suppress horizontal scroll from animated transitions */ overflow-x: hidden; } /* ── Light mode (default) ──────────────────────────── */ :root { - /* Alias map — component-expected names → theme.css canonical names. Gotcha #5. - Components should prefer the theme.css names; add aliases only when needed. */ + /* ── Peregrine brand colors ── */ + --app-primary: #2B6CB0; /* Falcon Blue — 4.70:1 on #eaeff8 ✅ AA */ + --app-primary-hover: #245A9E; + --app-primary-light: #EBF4FF; /* subtle tint — background use only */ + + --app-accent: #E06820; /* Talon Orange — use dark text on top */ + --app-accent-hover: #C05415; + --app-accent-light: #FFF3EB; /* subtle tint — background use only */ + --app-accent-text: #1a2338; /* on-button text — dark navy, NEVER white (only 2.8:1) */ + + /* ── CSS variable aliases (component names → theme.css canonical names. Gotcha #5.) ── */ --color-bg: var(--color-surface); --color-text-secondary: var(--color-text-muted); - /* Nav height token — consumed by .app-main padding-top in App.vue */ - --nav-height: 4rem; + /* ── Layout ── */ + --nav-height: 4rem; /* top bar height (mobile) */ + --sidebar-width: 220px; /* persistent sidebar (≥1024px) */ - /* Motion tokens for future animated components (inspired by avocet bucket pattern) */ + /* ── Pipeline status colors ── */ + /* Always pair with text label or icon — never color alone (accessibility) */ + --status-pending: var(--color-warning); + --status-approve: var(--color-success); + --status-reject: var(--color-error); + --status-applied: var(--color-info); + --status-synced: #5b4fa8; + --status-archived: var(--color-text-muted); + --status-survey: #6d3fa8; + --status-phone: #1a7a6e; + --status-interview: var(--color-info); + --status-offer: #b8620a; + --status-hired: var(--color-success); + + /* ── Match score thresholds ── */ + --score-high: var(--color-success); /* ≥ 70% */ + --score-mid: var(--color-warning); /* 40–69% */ + --score-low: var(--color-error); /* < 40% */ + --score-none: var(--color-text-muted); + + /* ── Motion tokens ── */ + --swipe-exit: 300ms; + --swipe-spring: 400ms cubic-bezier(0.34, 1.56, 0.64, 1); + --confetti-dur: 3500ms; --transition-spring: 400ms cubic-bezier(0.34, 1.56, 0.64, 1); --transition-dismiss: 350ms ease-in; --transition-enter: 250ms ease-out; + + /* ── Type scale ── */ + --text-xs: 0.75rem; /* 12px — badge labels, timestamps, keyboard hints */ + --text-sm: 0.875rem; /* 14px — card secondary, captions */ + --text-base: 1rem; /* 16px — body, card descriptions */ + --text-lg: 1.125rem; /* 18px — card title, section headers */ + --text-xl: 1.25rem; /* 20px — page section headings */ + --text-2xl: 1.5rem; /* 24px — page titles */ + --text-3xl: 1.875rem; /* 30px — dashboard greeting */ } /* ── Dark mode ─────────────────────────────────────── */ @media (prefers-color-scheme: dark) { :root:not([data-theme="hacker"]) { - /* Aliases inherit dark values from theme.css automatically */ + --app-primary: #68A8D8; /* Falcon Blue (dark) — 6.54:1 on #16202e ✅ AA */ + --app-primary-hover: #7BBDE6; + --app-primary-light: #0D1F35; + + --app-accent: #F6872A; /* Talon Orange (dark) — 5.22:1 on #16202e ✅ AA */ + --app-accent-hover: #FF9840; + --app-accent-light: #2D1505; + --app-accent-text: #1a2338; + + --status-synced: #9b8fea; + --status-survey: #b08fea; + --status-phone: #4ec9be; + --status-offer: #f5a43a; } } /* ── Hacker mode (Konami easter egg) ──────────────── */ -/* Applied via document.documentElement.dataset.theme = 'hacker' */ -/* Full token overrides live in theme.css [data-theme="hacker"] block */ [data-theme="hacker"] { - /* Cursor trail uses this color — override for hacker palette */ - --color-accent: #00ff41; + --app-primary: #00ff41; + --app-primary-hover: #00cc33; + --app-primary-light: #001a00; + --app-accent: #00ff41; + --app-accent-hover: #00cc33; + --app-accent-light: #001a00; + --app-accent-text: #0a0c0a; +} + +/* ── Focus style — keyboard nav (accessibility requirement) ── */ +:focus-visible { + outline: 2px solid var(--app-primary); + outline-offset: 3px; + border-radius: var(--radius-sm); } diff --git a/web/src/components/AppNav.vue b/web/src/components/AppNav.vue index 0d56972..011afb4 100644 --- a/web/src/components/AppNav.vue +++ b/web/src/components/AppNav.vue @@ -1,13 +1,60 @@ diff --git a/web/src/components/WorkflowButton.vue b/web/src/components/WorkflowButton.vue new file mode 100644 index 0000000..52627d6 --- /dev/null +++ b/web/src/components/WorkflowButton.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/web/src/stores/jobs.ts b/web/src/stores/jobs.ts new file mode 100644 index 0000000..9c4f393 --- /dev/null +++ b/web/src/stores/jobs.ts @@ -0,0 +1,50 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useApiFetch } from '../composables/useApi' + +export interface JobCounts { + pending: number + approved: number + applied: number + synced: number + rejected: number + total: number +} + +export interface SystemStatus { + enrichment_enabled: boolean + enrichment_last_run: string | null + enrichment_next_run: string | null + tasks_running: number + integration_name: string | null // e.g. "Notion", "Airtable" + integration_unsynced: number +} + +// Pinia setup store — function form, not options form (gotcha #10) +export const useJobsStore = defineStore('jobs', () => { + const counts = ref(null) + const status = ref(null) + const loading = ref(false) + const error = ref(null) + + const hasPending = computed(() => (counts.value?.pending ?? 0) > 0) + + async function fetchCounts() { + loading.value = true + const { data, error: err } = await useApiFetch('/api/jobs/counts') + loading.value = false + if (err) { error.value = err.kind === 'network' ? 'Network error' : `Error ${err.status}`; return } + counts.value = data + } + + async function fetchStatus() { + const { data } = await useApiFetch('/api/system/status') + if (data) status.value = data + } + + async function refresh() { + await Promise.all([fetchCounts(), fetchStatus()]) + } + + return { counts, status, loading, error, hasPending, fetchCounts, fetchStatus, refresh } +}) diff --git a/web/src/views/HomeView.vue b/web/src/views/HomeView.vue index d0c00a5..821de1c 100644 --- a/web/src/views/HomeView.vue +++ b/web/src/views/HomeView.vue @@ -1,18 +1,595 @@