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
6 changes: 3 additions & 3 deletions yuhm/HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 |
Expand Down
7 changes: 4 additions & 3 deletions yuhm/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
10 changes: 9 additions & 1 deletion yuhm/src/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,19 @@ export function LandingPage() {
transition={{ ...springy, delay: 0.55 }}
>{t('landing.intro')}</motion.p>
<FlowStrip animate={animate} />
<motion.div
className="hero-cta-row"
initial={animate ? { opacity: 0, y: 18 } : false}
animate={{ opacity: 1, y: 0 }}
transition={{ ...springy, delay: 0.92 }}
>
<AppLink className="hero-world-cta" href="/app/">{t('landing.world.action')} <ArrowUpRight size={19} /></AppLink>
</motion.div>
<motion.div
className="food-entry-proof"
initial={animate ? { opacity: 0, y: 16 } : false}
animate={{ opacity: 1, y: 0 }}
transition={{ ...springy, delay: 1.05 }}
transition={{ ...springy, delay: 1.1 }}
><ShieldCheck size={16} /><span>{t('landing.proof')}</span></motion.div>
</div>
<div className="hero-illustration">
Expand Down
7 changes: 7 additions & 0 deletions yuhm/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; } }
6 changes: 5 additions & 1 deletion yuhm/src/world/World.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App />)

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()
})

Expand Down
1 change: 1 addition & 0 deletions yuhm/src/world/panels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export function DiscoverPanel({ progress, onOpen, poolShareTaken, layer = 'all'
<AppLink href="/app/?mode=anonymous&intent=gather">{w('world.more.gather')}</AppLink>
<AppLink href="/app/?mode=anonymous&intent=request">{w('world.more.requests')}</AppLink>
<AppLink href="/app/?mode=login">{w('world.more.signIn')}</AppLink>
<AppLink href="/app/?mode=advanced">{w('world.more.advanced')}</AppLink>
</div>
</section>
</div>
Expand Down
2 changes: 2 additions & 0 deletions yuhm/src/world/worldStrings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -233,6 +234,7 @@ const es: Record<WorldKey, string> = {
'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铆',
Expand Down
Loading