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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ npm run e2e # Playwright end-to-end tests
npm run ci # lint + test + build + e2e
```

Single-page React/Vite app — no router, anchor-scroll only. Entry is `src/main.jsx`, which mounts `src/App.jsx` into `#root` in the top-level `index.html` and imports the page styles (`App.css`, `lumen-overrides.css`, `mobile-polish.css`; `App.jsx` also imports `theme.css` and `simulator.css`). `src/styles.css` is unused if present. The hero embeds an interactive iOS simulator (`src/InteractivePhone.jsx`) that mirrors the native app's surfaces — all five tabs (Home rhythm/stats/rooms/"Lumen noticed", Rooms→room→device drill-down, Intel device list, Auto scenes, Settings incl. the Sensory Profile) and the consent flow (Awareness → Reasoning → Action → Execution). Its markup deliberately reuses the app's copy, seeded scenes, and dark palette; `simulator.css` holds the styles the earlier demo didn't cover. Keep it in step with the Swift UI when that changes. The same app UI (the shared `AppShell`) also powers a **full-screen mode** — `FullscreenApp`, launched from the hero's "Open the app full screen" button, fills the viewport (edge-to-edge on phones, a centered phone column on desktop) sharing one `PhoneProvider` state. Running a scene applies its light preset to every light and tints the dashboard (`activeSceneAmbient`), and the Rooms `+` opens an Add-Device sheet (mirrors `AddDeviceView`) that adds a planned device to the room. Unit tests run on Vitest (`src/main.test.jsx`, setup in `src/test/setup.js`); end-to-end coverage uses Playwright (`playwright.config.js`, `testDir: ./e2e`). `public/privacy/index.html` is fully self-contained (no React); the `vercel.json` rewrite maps `/privacy` → `/privacy/index.html`.
Single-page React/Vite app — no router, anchor-scroll only. Entry is `src/main.jsx`, which mounts `src/App.jsx` into `#root` in the top-level `index.html` and imports the page styles (`App.css`, `lumen-overrides.css`, `mobile-polish.css`; `App.jsx` also imports `theme.css` and `simulator.css`). `src/styles.css` is unused if present. The hero is a **demo-first in-page preview** (`src/InteractivePhone.jsx`): one `AppShell` mount at usable size (~390px on desktop, viewport-height with no nested bezel on mobile). It mirrors the native app's surfaces — all five tabs (Home rhythm/stats/rooms/"Lumen noticed", Rooms→room→device drill-down, Intel device list, Auto scenes, Settings incl. the Sensory Profile) and the consent flow (Awareness → Reasoning → Action → Execution). Optional **Expand preview** promotes that same `AppShell` to focus mode (page chrome hidden) instead of mounting a second tree. Sensory Profile settings in the preview are wired: Quiet hides the suggestion card, Reduced motion / Calm Mode mute springs and page ambient, Soft contrast actually changes the stage. Public copy stays calm and unnamed — do not add autism, ADHD, or neurodivergent claims on the site. Markup reuses the app's copy, seeded scenes, and dark palette; `simulator.css` holds the preview-stage scale. Keep it in step with the Swift UI when that changes. Running a scene applies its light preset to every light and tints the dashboard (`activeSceneAmbient`), and the Rooms `+` opens an Add-Device sheet (mirrors `AddDeviceView`) that adds a planned device to the room. Unit tests run on Vitest (`src/main.test.jsx`, setup in `src/test/setup.js`); end-to-end coverage uses Playwright (`playwright.config.js`, `testDir: ./e2e`). `public/privacy/index.html` is fully self-contained (no React); the `vercel.json` rewrite maps `/privacy` → `/privacy/index.html`.

Waitlist validation/delivery logic (`normalizeWaitlistPayload`, `isValidWaitlistEmail`, `postWebhook`/`deliverWaitlist`) is factored into `lib/waitlist.js` and shared by both sides of the submit path: `api/waitlist.js` (the Vercel serverless handler) imports it server-side, and `src/waitlistSubmit.js` (the browser-side submit + provider-fallback chain) imports `DEFAULT_TO_EMAIL` from it. `lib/waitlist.test.js` covers the shared module directly.

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ npm run e2e # Playwright end-to-end tests
npm run ci # lint + test + build + e2e
```

Single-page React/Vite app — no router, anchor-scroll only. Entry is `src/main.jsx`, which mounts `src/App.jsx` into `#root` in the top-level `index.html` and imports the page styles (`App.css`, `lumen-overrides.css`, `mobile-polish.css`; `App.jsx` also imports `theme.css` and `simulator.css`). `src/styles.css` is unused if present. The hero embeds an interactive iOS simulator (`src/InteractivePhone.jsx`) that mirrors the native app's surfaces — all five tabs (Home rhythm/stats/rooms/"Lumen noticed", Rooms→room→device drill-down, Intel device list, Auto scenes, Settings incl. the Sensory Profile) and the consent flow (Awareness → Reasoning → Action → Execution). Its markup deliberately reuses the app's copy, seeded scenes, and dark palette; `simulator.css` holds the styles the earlier demo didn't cover. Keep it in step with the Swift UI when that changes. The same app UI (the shared `AppShell`) also powers a **full-screen mode** — `FullscreenApp`, launched from the hero's "Open the app full screen" button, fills the viewport (edge-to-edge on phones, a centered phone column on desktop) sharing one `PhoneProvider` state. Running a scene applies its light preset to every light and tints the dashboard (`activeSceneAmbient`), and the Rooms `+` opens an Add-Device sheet (mirrors `AddDeviceView`) that adds a planned device to the room. Unit tests run on Vitest (`src/main.test.jsx`, setup in `src/test/setup.js`); end-to-end coverage uses Playwright (`playwright.config.js`, `testDir: ./e2e`). `public/privacy/index.html` is fully self-contained (no React); the `vercel.json` rewrite maps `/privacy` → `/privacy/index.html`.
Single-page React/Vite app — no router, anchor-scroll only. Entry is `src/main.jsx`, which mounts `src/App.jsx` into `#root` in the top-level `index.html` and imports the page styles (`App.css`, `lumen-overrides.css`, `mobile-polish.css`; `App.jsx` also imports `theme.css` and `simulator.css`). `src/styles.css` is unused if present. The hero is a **demo-first in-page preview** (`src/InteractivePhone.jsx`): one `AppShell` mount at usable size (~390px on desktop, viewport-height with no nested bezel on mobile). It mirrors the native app's surfaces — all five tabs (Home rhythm/stats/rooms/"Lumen noticed", Rooms→room→device drill-down, Intel device list, Auto scenes, Settings incl. the Sensory Profile) and the consent flow (Awareness → Reasoning → Action → Execution). Optional **Expand preview** promotes that same `AppShell` to focus mode (page chrome hidden) instead of mounting a second tree. Sensory Profile settings in the preview are wired: Quiet hides the suggestion card, Reduced motion / Calm Mode mute springs and page ambient, Soft contrast actually changes the stage. Public copy stays calm and unnamed — do not add autism, ADHD, or neurodivergent claims on the site. Markup reuses the app's copy, seeded scenes, and dark palette; `simulator.css` holds the preview-stage scale. Keep it in step with the Swift UI when that changes. Running a scene applies its light preset to every light and tints the dashboard (`activeSceneAmbient`), and the Rooms `+` opens an Add-Device sheet (mirrors `AddDeviceView`) that adds a planned device to the room. Unit tests run on Vitest (`src/main.test.jsx`, setup in `src/test/setup.js`); end-to-end coverage uses Playwright (`playwright.config.js`, `testDir: ./e2e`). `public/privacy/index.html` is fully self-contained (no React); the `vercel.json` rewrite maps `/privacy` → `/privacy/index.html`.

Waitlist validation/delivery logic (`normalizeWaitlistPayload`, `isValidWaitlistEmail`, `postWebhook`/`deliverWaitlist`) is factored into `lib/waitlist.js` and shared by both sides of the submit path: `api/waitlist.js` (the Vercel serverless handler) imports it server-side, and `src/waitlistSubmit.js` (the browser-side submit + provider-fallback chain) imports `DEFAULT_TO_EMAIL` from it. `lib/waitlist.test.js` covers the shared module directly.

Expand Down
50 changes: 40 additions & 10 deletions e2e/live-demo.e2e.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import { test, expect } from '@playwright/test';

test.describe('Lumen live demo', () => {
test('hero shows interactive phone and guided steps', async ({ page }) => {
test('hero shows interactive preview and guided steps', async ({ page }) => {
await page.goto('/');

await expect(page.getByRole('heading', { level: 1 })).toContainText(/when your home shifts/i);
await expect(page.getByText(/live interactive demo/i)).toBeVisible();
await expect(page.getByText(/^interactive preview$/i)).toBeVisible();
await expect(page.getByRole('button', { name: /home tab/i })).toBeVisible();
await expect(page.getByText(/try the flow/i)).toBeVisible();
});

test('consent loop runs on the in-page stage', async ({ page }) => {
await page.goto('/');
const demo = page.locator('#demo');

await demo.getByRole('button', { name: /review evening scene/i }).click();
await expect(demo.getByText(/why lumen noticed/i)).toBeVisible();
await demo.getByRole('button', { name: /apply evening/i }).click();
await expect(demo.getByText('Lumen will', { exact: true })).toBeVisible();
await demo.getByRole('button', { name: /^apply$/i }).click();
await expect(demo.getByText(/evening scene applied/i)).toBeVisible();
});

test('auto tab opens scene approval sheet', async ({ page }) => {
await page.goto('/');
const demo = page.locator('#demo');

await demo.getByRole('button', { name: /auto tab/i }).click();
await demo.getByRole('button', { name: /morning/i }).click();

// The scene approval sheet mirrors the app's SceneApprovalSheet: an "Apply
// scene" header for Morning and a plain "Apply" confirm button.
await expect(demo.getByText(/apply scene/i)).toBeVisible();
await expect(demo.getByRole('button', { name: /^apply$/i })).toBeVisible();
await expect(demo.getByText('Power', { exact: true })).toBeVisible();
Expand All @@ -28,13 +38,22 @@ test.describe('Lumen live demo', () => {
await page.goto('/');
const demo = page.locator('#demo');

await page.getByRole('button', { name: /lumen noticed/i }).click();
await page.getByRole('button', { name: /lumen noticed: tap the suggestion card/i }).click();

await expect(demo.getByText(/why lumen noticed/i)).toBeVisible();
await expect(demo.getByRole('button', { name: /apply evening/i })).toBeVisible();
});

test('flow card drives the live demo', async ({ page }) => {
test('guided execution step applies the scene', async ({ page }) => {
await page.goto('/');
const demo = page.locator('#demo');

await page.getByRole('button', { name: /runs: see it apply/i }).click();

await expect(demo.getByText(/evening scene applied/i)).toBeVisible();
});

test('flow card drives the preview', async ({ page }) => {
await page.goto('/');
const demo = page.locator('#demo');
const flow = page.locator('#flow');
Expand All @@ -45,14 +64,13 @@ test.describe('Lumen live demo', () => {
await expect(demo.getByText(/why lumen noticed/i)).toBeVisible();
});

test('opens full-screen app mode and plans a device', async ({ page }) => {
test('expands preview and plans a device', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: /open the app full screen/i }).click();
await page.getByRole('button', { name: /expand preview/i }).click();

const app = page.locator('.app-fullscreen');
await expect(app).toBeVisible();

// Rooms → Office (empty) → add a planned device.
await app.getByRole('button', { name: /rooms tab/i }).click();
await app.getByRole('button', { name: /office/i }).first().click();
await app.getByRole('button', { name: /add a device/i }).click();
Expand All @@ -64,8 +82,20 @@ test.describe('Lumen live demo', () => {
await expect(app.getByRole('button', { name: /reading lamp/i })).toBeVisible();
await expect(app.locator('.planned-dot')).toBeVisible();

// Close returns to the marketing page.
await app.getByRole('button', { name: /close app preview/i }).click();
await expect(page.locator('.app-fullscreen')).toHaveCount(0);
});

test('mobile stage is tappable without a nested bezel', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');

const stage = page.locator('.app-preview-stage');
await expect(stage).toBeVisible();
await expect(page.locator('#demo .phone.phone-featured')).toHaveCount(0);
await expect(page.getByRole('button', { name: /home tab/i })).toBeVisible();

await page.getByRole('button', { name: /auto tab/i }).click({ force: false });
await expect(page.locator('#demo').getByRole('button', { name: /morning/i })).toBeVisible();
});
});
43 changes: 32 additions & 11 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ h1, h2, h3, h4, p {
/* ===== AMBIENT BACKDROP =====
A slow-shifting color wash behind the whole page. Two full-viewport
layers crossfade opacity so the tint eases from one moment's color to
the next instead of snapping - driven by the live demo's current step,
the next instead of snapping - driven by the preview after you tap,
and by whichever section is centered in the viewport once you scroll
past the hero. */

Expand Down Expand Up @@ -333,13 +333,13 @@ h1, h2, h3, h4, p {
padding: 88px clamp(20px, 5%, 64px) 80px;
}

.hero-inner {
.hero-split .hero-inner {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: clamp(32px, 5vw, 72px);
align-items: center;
grid-template-columns: minmax(240px, 0.85fr) minmax(360px, 1.2fr);
gap: clamp(28px, 4vw, 64px);
align-items: start;
max-width: 1180px;
width: 100%;
margin: 0 auto;
Expand Down Expand Up @@ -370,11 +370,11 @@ h1, h2, h3, h4, p {
margin-top: 0;
flex-direction: column;
align-items: center;
width: 100%;
}

.hero-split .live-demo .phone-featured {
width: min(320px, 100%);
filter: drop-shadow(0 32px 80px rgba(0, 0, 0, 0.55));
.hero-split .live-demo {
width: 100%;
}

.demo-live-badge {
Expand Down Expand Up @@ -2310,6 +2310,23 @@ p {
text-align: center;
}

.hero-demo-first {
padding: 0 0 32px;
}

.hero-demo-first .hero-copy {
padding: 28px 20px 0;
}

.hero-demo-first .hero-copy h1 {
font-size: clamp(28px, 8vw, 42px);
}

.hero-demo-first .hero-lede {
font-size: 16px;
max-width: 36em;
}

.hero-split .hero-copy {
align-items: center;
text-align: center;
Expand Down Expand Up @@ -2829,7 +2846,7 @@ p {
}

.live-demo .phone-featured {
width: 260px;
width: auto;
}
}

Expand Down Expand Up @@ -2860,12 +2877,12 @@ p {
}

.hero-demo {
margin-top: 40px;
margin-top: 24px;
width: 100%;
}

.live-demo .phone-featured {
width: min(290px, 88vw);
width: auto;
}
}

Expand Down Expand Up @@ -3350,6 +3367,10 @@ p {
gap: 8px;
}

.guided-demo-strip [role="listitem"] {
width: 100%;
}

.guided-demo-step {
display: flex;
align-items: center;
Expand Down
Loading
Loading