Frontend layers
Frontend layers
Responsibilities and boundaries per FSD layer in apps/web/src.
app
Application setup: providers (i18n, React Query, router), global style imports,
and the root render. No business logic. Files: app/providers, app/styles,
app/router.tsx, app/App.tsx.
pages
Route-level compositions. A page reads routing/session data and composes widgets and features. Two pages:
-
pages/trips— the authenticated home content hub. It owns the three collection surfaces (/today,/, and/journal),/journal/:entryIdtravelogue reading routes, the phase-aware trip grid, guided create-trip wizard, and the travelogue frontend preview. Travelogues support local create, read, update, and delete flows and are stored in a user-partitioned, versioned localStorage document until the backend travelogue aggregate and sync adapter ship. Trips, Today, and Travelogues share the same responsive card-grid language in the main content panel. Trip cards render a non-interactive MapLibre place thumbnail when coordinates are available, then fall back to a cover or route illustration. The travelogue reader uses a cover, article typography, active section rail, and a clearly labeled local AI-answer preview. A stop detail can launch the composer through/journal?compose=stop: the stop name is prefilled as the title and the current trip is selected, while the body remains empty so the product never invents the member's memory. The Today surface can remember a user-selected city or region locally and show its live weather through the existing weather proxy. It selects in-trip, pre-trip, post-trip, or everyday guidance from trip dates and links the existing trip agent rather than claiming backend-generated travelogues. -
pages/invite— the/invite/:tokenaccept surface. Previews the invite (public), renders the auth form inline when signed out so the token survives sign-in, then adds the user and routes into the planner on accept. -
pages/travel-planner— the single-trip workspace. Because this is one large prototype surface, its map/schedule/budget/sidebar blocks live as page-private widgets underpages/travel-planner/ui. The page composes theAppSidebarand main content with theshared/ui/splitterprimitive and persists the resulting width/collapse state through the backend preferences API.The trip agent (see ../backend/agent.md) is also page-private:
ui/agent/holds the top-right sparkle toggle (mirroring the left sidebar's expand control), the right agent panel with the shared chat — abg-sidebarbase layer mirroring the leftAppSidebarthat reveals via a width transition, so the main panel rounds both edges when it is open — and the bottom-right intervention cards (Approve/Deny, AI SDK approval DTO). Write-tool approval cards render the tool arguments throughui/agent/AgentToolPreview.tsx, which reuses the planner's ownStopCardfor stop mutations (and a labeled expense row / one-line summaries for the other tools) so the trip receives a rendered DTO preview, never raw JSON; assistant bubbles render Markdown via Streamdown (AI SDK UI's streaming Markdown path) with the shared.wf-markdowntypography; reasoning parts render throughui/agent/AgentReasoning.tsx, a Collapsible modeled on AI SDK UI'sReasoningelement (auto-opens while the model thinks, auto-collapses ~1 s after it finishes, and caps its content in a scrollable max-height container); the composer (ui/agent/AgentComposer.tsx) and stop-detail comment input (ui/StopDetail.tsx) shareui/mention/for an inline@-mention list of trip members and the agent that opens only when@is typed (never on paste/drop), filters as you type, and is navigated with Up/Down + Tab/Enter to insert (Escape dismisses); member mentions persist amentionspart on the message and polled clients show a toast to each @mentioned user (never the author) — stop comments that@mentionmembers are mirrored into the agent session so the same toast path fires; stop comments and agent chat bubbles expose a hover reply control (ui/quote/) that quotes the snippet into the composer (markdown>block + optional@mention) and renders received quotes as an inline chip;@agentin chat routes through the streaminguseChatpath so reasoning/tool approval stay live;model/useAgentChat.tswraps AI SDK UI'suseChatwithaddToolApprovalResponseandsendAutomaticallyWhenfor write-tool approvals (streaming buffer only — the shared history lives in React Query; chat turns reuse the client UIMessage id on persist so live vs history dedupe by id), andmodel/useAgentEvents.tspolls the session every 12 s for all members. The panel's collapsed state persists via the preferences API. All of it renders only whenGET /api/agent/statusreports the agent enabled.
Pages may hold page-specific state and data fetching (Pages First).
Trip mutation lifecycle and QueryClient calls remain page-owned. The reusable
entities/trip public API exposes only pure immutable mappers such as
toTripSummary and upsertTripSummary; it does not import TanStack Query,
routing, or transport code.
widgets
Reusable composite blocks used by more than one page:
widgets/app-sidebar— the persistent left sidebar shared by both pages. Pins a brand (or a page-providedtop, e.g. the planner's back + trip title) above a scrollable content slot, with the account menu docked at the bottom. The home page fills that slot with page-owned Trips / Today / Travelogue navigation, a capture action, country-flag recent-trip shortcuts, and recent travelogue drafts; the reusable widget remains unaware of those routes. The trips collection opens a guided create-trip wizard from the main header (and empty-state CTA). On success the wizard echoes thePOST /api/tripsbody into the trips list + trip detail caches (see data-caching.md; no immediate list refetch — Hyperdrive read-after-write) and navigates to the planner, opens the agent panel, and pre-fills a one-shot@agentsuggestion. The member can edit it and must explicitly send it. The planner injects its itinerary into the middle slot. It is the base layer: pages set the shell background to the sidebar color and float the main panel above it with rounded left corners + a left shadow. A collapse control sits at the sidebar's top-right; collapsing hides it and shows a floating expand control over the panel. The state persists in localStorage.widgets/user-menu— avatar trigger docked at the sidebar bottom. Opens an upward popover holding account info, settings entry points, and sign out.widgets/settings-dialog— responsive account settings composition for profile, language, appearance, and application information.
There is no desktop top bar; global chrome lives in the sidebar. Most planner
blocks stay page-private under pages/travel-planner/ui until reuse emerges —
the narrow-screen planner shell (bottom navigation, itinerary/detail/agent
sheets) is page-private under pages/travel-planner/ui/mobile. See
mobile-pwa.md.
features
Reusable, self-contained user scenarios (e.g. features/auth sign-in form).
UI + model + api for that scenario.
entities
Reusable domain data and mapping with no transport logic. entities/trip,
entities/stop, entities/expense, entities/member hold TypeScript types and
pure helpers (formatting, grouping, balance math mirrored for display).
shared
Framework-agnostic reused code:
shared/ui— cossUI primitives + the map wrapper.shared/api— typed API client.shared/auth— Better Auth React client.shared/i18n— i18next setup and locale resources.shared/lib— utilities (class names, formatters,useMediaQuery/useIsMobileviewport detection).shared/config— env access, route/query keys.
Import rule
Downward only. entities must not import features; slices on the same layer
must not import each other. Enforced by review and the alias structure.