From a16d562e06574f1ac60f6f192d2e1225dd65dd1c Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 15 Apr 2026 20:45:03 -0700 Subject: [PATCH 01/11] feat(demo): add WelcomeModal with localStorage gate --- web/src/components/WelcomeModal.vue | 160 ++++++++++++++++++ .../components/__tests__/WelcomeModal.test.ts | 35 ++++ 2 files changed, 195 insertions(+) create mode 100644 web/src/components/WelcomeModal.vue create mode 100644 web/src/components/__tests__/WelcomeModal.test.ts diff --git a/web/src/components/WelcomeModal.vue b/web/src/components/WelcomeModal.vue new file mode 100644 index 0000000..dd46f52 --- /dev/null +++ b/web/src/components/WelcomeModal.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/web/src/components/__tests__/WelcomeModal.test.ts b/web/src/components/__tests__/WelcomeModal.test.ts new file mode 100644 index 0000000..b64b7da --- /dev/null +++ b/web/src/components/__tests__/WelcomeModal.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import WelcomeModal from '../WelcomeModal.vue' + +const LS_KEY = 'peregrine_demo_visited' + +beforeEach(() => { + localStorage.clear() +}) + +describe('WelcomeModal', () => { + it('is visible when localStorage key is absent', () => { + const w = mount(WelcomeModal, { global: { stubs: { Teleport: true } } }) + expect(w.find('.welcome-modal').exists()).toBe(true) + }) + + it('is hidden when localStorage key is set', () => { + localStorage.setItem(LS_KEY, '1') + const w = mount(WelcomeModal, { global: { stubs: { Teleport: true } } }) + expect(w.find('.welcome-modal').exists()).toBe(false) + }) + + it('dismisses and sets localStorage on primary CTA click', async () => { + const w = mount(WelcomeModal, { global: { stubs: { Teleport: true } } }) + await w.find('.welcome-modal__explore').trigger('click') + expect(w.find('.welcome-modal').exists()).toBe(false) + expect(localStorage.getItem(LS_KEY)).toBe('1') + }) + + it('emits dismissed event on close', async () => { + const w = mount(WelcomeModal, { global: { stubs: { Teleport: true } } }) + await w.find('.welcome-modal__explore').trigger('click') + expect(w.emitted('dismissed')).toBeTruthy() + }) +}) From d96cdfa89b17085f6f897c7a9ec991d69021e8da Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 15 Apr 2026 20:46:11 -0700 Subject: [PATCH 02/11] feat(demo): add HintChip component with per-view localStorage dismiss --- web/src/components/HintChip.vue | 63 +++++++++++++++++++ web/src/components/__tests__/HintChip.test.ts | 28 +++++++++ 2 files changed, 91 insertions(+) create mode 100644 web/src/components/HintChip.vue create mode 100644 web/src/components/__tests__/HintChip.test.ts diff --git a/web/src/components/HintChip.vue b/web/src/components/HintChip.vue new file mode 100644 index 0000000..bc8960b --- /dev/null +++ b/web/src/components/HintChip.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/web/src/components/__tests__/HintChip.test.ts b/web/src/components/__tests__/HintChip.test.ts new file mode 100644 index 0000000..ccbbabf --- /dev/null +++ b/web/src/components/__tests__/HintChip.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import HintChip from '../HintChip.vue' + +beforeEach(() => { localStorage.clear() }) + +const factory = (viewKey = 'home', message = 'Test hint') => + mount(HintChip, { props: { viewKey, message } }) + +describe('HintChip', () => { + it('renders the message', () => { + const w = factory() + expect(w.text()).toContain('Test hint') + }) + + it('is hidden when localStorage key is already set', () => { + localStorage.setItem('peregrine_hint_home', '1') + const w = factory() + expect(w.find('.hint-chip').exists()).toBe(false) + }) + + it('hides and sets localStorage when dismiss button is clicked', async () => { + const w = factory() + await w.find('.hint-chip__dismiss').trigger('click') + expect(w.find('.hint-chip').exists()).toBe(false) + expect(localStorage.getItem('peregrine_hint_home')).toBe('1') + }) +}) From 55f464080fd742affb16876c695db1b37e128fc3 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 15 Apr 2026 20:57:29 -0700 Subject: [PATCH 03/11] feat(demo): wire DemoBanner, WelcomeModal, HintChip into app + views --- web/src/App.vue | 23 +++++------------------ web/src/views/ApplyView.vue | 9 +++++++++ web/src/views/ContactsView.vue | 9 +++++++++ web/src/views/HomeView.vue | 9 +++++++++ web/src/views/InterviewsView.vue | 9 +++++++++ web/src/views/JobReviewView.vue | 9 +++++++++ 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/web/src/App.vue b/web/src/App.vue index fb16f04..d3d7fc4 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -7,10 +7,9 @@ - -
- πŸ‘ Demo mode β€” changes are not saved and AI features are disabled. -
+ + + @@ -32,6 +31,8 @@ import { useHackerMode, useKonamiCode } from './composables/useEasterEgg' import { useTheme } from './composables/useTheme' import { useToast } from './composables/useToast' import AppNav from './components/AppNav.vue' +import DemoBanner from './components/DemoBanner.vue' +import WelcomeModal from './components/WelcomeModal.vue' import { useAppConfigStore } from './stores/appConfig' import { useDigestStore } from './stores/digest' @@ -128,20 +129,6 @@ body { padding-bottom: 0; } -/* Demo mode banner β€” sticky top bar */ -.demo-banner { - position: sticky; - top: 0; - z-index: 200; - background: var(--color-warning); - color: #1a1a1a; /* forced dark β€” warning bg is always light enough */ - text-align: center; - font-size: 0.85rem; - font-weight: 600; - padding: 6px var(--space-4, 16px); - letter-spacing: 0.01em; -} - /* Global toast β€” bottom-center, above tab bar */ .global-toast { position: fixed; diff --git a/web/src/views/ApplyView.vue b/web/src/views/ApplyView.vue index c1ef7b5..f8f5f5b 100644 --- a/web/src/views/ApplyView.vue +++ b/web/src/views/ApplyView.vue @@ -1,6 +1,11 @@