diff --git a/yuhm/HANDOFF.md b/yuhm/HANDOFF.md index f4ba836..250b97e 100644 --- a/yuhm/HANDOFF.md +++ b/yuhm/HANDOFF.md @@ -10,7 +10,7 @@ Public-facing product behavior and safety boundaries are maintained in `docs/LIV ## Living-map world experience, 2026-08-24 (now the default app) -An immersive "cooperative living world" surface that is now the **default app experience**: bare `/app/` and `/app/?mode=anonymous` open it on every viewport (`mode=world` is an explicit alias that also overrides a saved advanced preference). It presents yuhm as a living neighborhood food ecosystem rather than pins on a map. The focused flows keep their URLs: `intent=food` still opens the zero-friction classic finder (command-bar map on phones), `intent=contribute/gather/request` are unchanged, auth modes are unchanged, and `mode=advanced`/`workspace=` (or the persisted `yuhm:experience-mode`) still open the dashboard. The world links back out through a "More ways to take part" block (Find food, Contribute, Gather, Requests, Sign in) and a "Classic food finder" note on the map. Note: successful login redirects to `/app/`, so signed-in members without the advanced preference now land in the world. +An immersive "cooperative living world" surface that is now the **default app experience**: bare `/app/` and `/app/?mode=anonymous` always open it, on every viewport (`mode=world` is an equivalent alias). The saved `yuhm:experience-mode` preference no longer influences routing; the dashboard is reached only through explicit `mode=advanced` or `workspace=` URLs (one tap away via the world's Command center link), so the flag writes in SimpleExperience, MapLab, and DashboardApp are now vestigial and can be retired in a cleanup pass. The landing hero carries a primary "Enter the living map" CTA into `/app/`, and first entry runs the find-your-yuhm onboarding (stored once in `yuhm:world-intro`; replayable from the profile panel). It presents yuhm as a living neighborhood food ecosystem rather than pins on a map. The focused flows keep their URLs: `intent=food` still opens the zero-friction classic finder (command-bar map on phones), `intent=contribute/gather/request` are unchanged, auth modes are unchanged, and `mode=advanced`/`workspace=` (or the persisted `yuhm:experience-mode`) still open the dashboard. The world links back out through a "More ways to take part" block (Find food, Contribute, Gather, Requests, Sign in) and a "Classic food finder" note on the map. Note: successful login redirects to `/app/`, so signed-in members without the advanced preference now land in the world. - **Where it lives.** Everything is in `src/world/`: `WorldExperience.tsx` (shell, routing between panels), `WorldMap.tsx` (Leaflet living map), `WorldSheet.tsx` (draggable mobile sheet, docked desktop panel), `panels.tsx` (discover, spot, mission, pool, run), `views.tsx` (onboarding, pulse, profile, gratitude), `worldData.ts` (sample world), `worldStrings.ts` (bilingual EN/ES chrome copy), `world.css` (scoped tokens from the yuhm palette: cacao `#493128`, garden green `#52734D`, tomato coral `#E96545`, sweet corn `#F2C14E`, herb mint `#A9C98B`, warm oat `#FFF3DC`). The chunk lazy-loads; `App.tsx` routes `mode=world` ahead of the mobile map. - **Honesty boundary (load-bearing).** The world merges two data kinds: the real public directory listings from `foodLocations.ts` (rendered activity-free, with verified/community labels, confirm-hours note, and a real directions link) and an invented sample circle ("Eastside Circle": six invented spots, six invented neighbors, missions, one pool, one run). Every sample record carries `sample: true` and the interface labels it Sample; the map carries a persistent "Demo world" note. Do not attach sample activity to real listings and do not present the demo loop as live network movement. @@ -204,9 +204,9 @@ After reviewing those recommendations, choose one bounded target and use `$impro | Route | Current behavior | |---|---| | `/` | yuhm landing page | -| `/app/` | The living-map world experience on every viewport (a saved advanced preference opens the dashboard instead) | +| `/app/` | Always the living-map world experience, on every viewport; the dashboard is explicit only (`mode=advanced` or `workspace=`) | | `/app/?mode=anonymous` | Same living-map world; a valid Supabase session still determines write access on other surfaces | -| `/app/?mode=world` | Explicit world entry; also overrides a saved advanced preference | +| `/app/?mode=world` | Explicit world alias (same as `/app/`) | | `/app/?mode=login` | Email and password login | | `/app/?mode=login&signup=1` | Account creation | | `/app/?mode=reset` | Request a password-reset email | diff --git a/yuhm/src/App.tsx b/yuhm/src/App.tsx index 8a84b65..8c31729 100644 --- a/yuhm/src/App.tsx +++ b/yuhm/src/App.tsx @@ -44,11 +44,12 @@ function AppRoutes() { const workspace = params.get('workspace') const authMode = mode === 'login' || mode === 'reset' || mode === 'recovery' const consumerIntent = intent === 'food' || intent === 'contribute' || intent === 'gather' || intent === 'request' ? intent : null - const advancedMode = mode === 'advanced' || Boolean(workspace) || (!consumerIntent && mode !== 'world' && localStorage.getItem('yuhm:experience-mode') === 'advanced') + const advancedMode = mode === 'advanced' || Boolean(workspace) const isMapLab = path.startsWith('/app') && mode === 'map-lab' // The living world is the default app experience: bare /app/ and the anonymous - // entry open it on every viewport. Focused intents, auth, map-lab, and the - // advanced dashboard keep their existing routes. + // entry always open it, on every viewport. The dashboard is explicit only + // (mode=advanced or workspace=); focused intents, auth, and map-lab keep + // their existing routes. const isWorld = path.startsWith('/app') && !isMapLab && (mode === 'world' || (!authMode && !advancedMode && !consumerIntent)) diff --git a/yuhm/src/LandingPage.tsx b/yuhm/src/LandingPage.tsx index a011d1b..3e61357 100644 --- a/yuhm/src/LandingPage.tsx +++ b/yuhm/src/LandingPage.tsx @@ -196,11 +196,19 @@ export function LandingPage() { transition={{ ...springy, delay: 0.55 }} >{t('landing.intro')} + + {t('landing.world.action')} + {t('landing.proof')}
diff --git a/yuhm/src/styles.css b/yuhm/src/styles.css index 642a928..42e8c42 100644 --- a/yuhm/src/styles.css +++ b/yuhm/src/styles.css @@ -1543,3 +1543,10 @@ button.food-alert-banner { cursor: pointer; } .entry-world-banner { flex-wrap: wrap; } .entry-world-action { margin-left: 84px; } } + +/* Landing hero CTA into the living world (the default app at /app/). */ +.hero-cta-row { margin-top: 24px; } +.hero-world-cta { display: inline-flex; align-items: center; gap: 9px; min-height: 54px; padding: 14px 27px; background: #2d6b50; color: #fff8e8; border-radius: 17px; font: 600 17px/1 'Baloo 2', 'DM Sans', sans-serif; letter-spacing: .01em; text-decoration: none; box-shadow: 0 4px 0 #1f4a37, 0 10px 22px #2d6b5040; transition: transform .2s var(--ease-out), box-shadow .2s var(--ease-out); } +.hero-world-cta:hover { transform: translateY(-2px); box-shadow: 0 6px 0 #1f4a37, 0 14px 26px #2d6b5045; } +.hero-world-cta:active { transform: translateY(1px); box-shadow: 0 2px 0 #1f4a37; } +@media (prefers-reduced-motion: reduce) { .hero-world-cta { transition: none; } } diff --git a/yuhm/src/world/World.test.tsx b/yuhm/src/world/World.test.tsx index 158e5cc..c5fb4e9 100644 --- a/yuhm/src/world/World.test.tsx +++ b/yuhm/src/world/World.test.tsx @@ -99,11 +99,15 @@ describe('living-map world experience', () => { expect(within(panel).getByRole('link', { name: 'Find food' })).toHaveAttribute('href', '/app/?mode=anonymous&intent=food') }) - it('keeps the advanced dashboard preference ahead of the world default', async () => { + it('opens the world at /app/ even with a saved advanced preference, dashboard stays explicit', async () => { window.history.replaceState({}, '', '/app/') localStorage.setItem('yuhm:experience-mode', 'advanced') render() + expect(await screen.findByRole('heading', { name: 'Find your local yuhm.' })).toBeInTheDocument() + + window.history.replaceState({}, '', '/app/?mode=advanced') + window.dispatchEvent(new PopStateEvent('popstate')) expect(await screen.findByRole('button', { name: /^Rescue operations$/i })).toBeInTheDocument() }) diff --git a/yuhm/src/world/panels.tsx b/yuhm/src/world/panels.tsx index 75ac9c9..26447bf 100644 --- a/yuhm/src/world/panels.tsx +++ b/yuhm/src/world/panels.tsx @@ -178,6 +178,7 @@ export function DiscoverPanel({ progress, onOpen, poolShareTaken, layer = 'all' {w('world.more.gather')} {w('world.more.requests')} {w('world.more.signIn')} + {w('world.more.advanced')}
diff --git a/yuhm/src/world/worldStrings.ts b/yuhm/src/world/worldStrings.ts index 47da7e1..14783c4 100644 --- a/yuhm/src/world/worldStrings.ts +++ b/yuhm/src/world/worldStrings.ts @@ -85,6 +85,7 @@ const en = { 'world.more.gather': 'Gather', 'world.more.requests': 'Requests', 'world.more.signIn': 'Sign in', + 'world.more.advanced': 'Command center', // Spot card 'world.spot.offers': 'Available or needed', @@ -233,6 +234,7 @@ const es: Record = { 'world.more.gather': 'Reunirse', 'world.more.requests': 'Solicitudes', 'world.more.signIn': 'Iniciar sesión', + 'world.more.advanced': 'Centro de mando', 'world.spot.offers': 'Disponible o necesario', 'world.spot.people': 'Quién está aquí',