@karibukit/planner-ui
v0.3.0
Published
The RafikiGo Planner UI (trip-concierge plan Phase 3): `PlannerApp` — chat + animated route map + itinerary day cards, mobile-first, themable per tenant. Consumed by rafikigo-web (Astro island, `/planner`) and marahill-fe (Next route, `/plan`).
Downloads
771
Readme
@karibukit/planner-ui
The RafikiGo Planner UI (trip-concierge plan Phase 3): PlannerApp — chat +
animated route map + itinerary day cards, mobile-first, themable per tenant.
Consumed by rafikigo-web (Astro island, /planner) and marahill-fe (Next
route, /plan).
Usage
import { PlannerApp } from '@karibukit/planner-ui'
import '@karibukit/planner-ui/styles.css'
import 'maplibre-gl/dist/maplibre-gl.css'
<PlannerApp
apiBaseUrl="https://api.karibukit.com"
tenantSlug="rafikigo"
publicToken={PLANNER_PUBLIC_TOKEN} // the tenant's public token
whatsappNumber="+254 114 505977"
mapStyle={mapStyleUrlOrSpec} // host picks the tile source
theme={{ headingFont: "'Fraunces', serif" }} // tenant themeTokens
utm={utmParamsFromUrl} // lands in session.utm
prefillMessage={prefillFromQueryParams} // ?d=&m=&p=&i= → seeds the composer
autoSendPrefill={goFlagFromUrl} // &go=1 (host forms only) → sends it instead
/>- Layout: the component fills its container (
height: 100%) — give it a full-height parent. Desktop ≥1024px: chat left, map + day cards right. Mobile: full-screen chat, sticky trip bar, slide-over sheet. - Sessions are created lazily on the first send. Retries reuse the
clientMsgId, so the server's idempotent replay applies. - Standalone map:
import { RouteMap } from '@karibukit/planner-ui/map'— deliberately a separate entry so maplibre-gl stays out of the first-paint chunk (PlannerApplazy-loads it with the first route). - Theming: every color/typeface routes through
--kkp-*CSS variables; passtheme(seePlannerTheme) built from the tenant'sthemeTokens.
Demo
pnpm --filter @karibukit/planner-ui dev # http://localhost:5199Runs against a scripted mock client by default (full conversation offline: try a starter, then "is it available?", then "book it").
The mock is a rendering fixture, not the agent — say so before demoing it.
It knows exactly two itineraries (6-day and 8-day, picked by regex on
8 days|two weeks|2 weeks|beach) and its price band is a hard-coded literal
that the 8-day branch never touches. So "make it 3 days" silently returns the
6-day trip, and the price never moves however the trip changes. Both read as
product bugs to anyone walking through it. Set the two env vars below to test
real planning.
Demo URLs:
| | |
|---|---|
| / | the planner |
| /?frame=phone | the planner inside a real 375×812 viewport |
| /?view=share | the public trip page, demo fixture |
| /?view=share&trip=<shareId> | the public trip page for a REAL trip, fetched from the configured API |
That last one exists because a trip's share URL is
${tenant.publicBaseUrl}/trip/<shareId> — production, by construction, since
it's what you hand another human. Nothing in this monorepo serves that path:
SharePage is a component and the host site mounts it. So the itinerary's
"View" button on a locally-planned trip opens the live domain. As of
2026-08-25 rafikigo.com/planner is 200 but /planner/trip/* is 404 — the
host route was never built, so every share link the planner has produced (the
WhatsApp button and get_share_link included) is dead. demo/SharePreview.tsx
is the reference implementation for that missing route.
Env in packages/planner-ui/.env:
VITE_MAPTILER_KEY— vector tiles (falls back to OSM raster, dev only)VITE_PLANNER_API_BASE+VITE_PLANNER_PUBLIC_TOKEN— hit a real API instead of the mock
Release contract (how this package ships)
The ranger-widget npm channel exists but is manual and version-drifted — this package fixes the process instead of inheriting it:
- Build produces compiled ESM + bundled
.d.ts(chat-protocol's types are inlined — it is private TS source no consumer can install) +dist/styles.css. - CI runs
pack:smokeon every push: entry files present in the tarball, noworkspace:ranges in installable fields, block types bundled, maplibre-gl absent from the main chunk. - Publish is explicit and versioned:
Public, matchingcd packages/planner-ui pnpm typecheck && pnpm test && pnpm build && pnpm pack:smoke npm version <patch|minor> # bump + git tag planner-ui-vX.Y.Z npm publish --access public@karibukit/ranger-widget(already public on npm under the same scope). Nothing in the tarball is secret —filesshipsdistonly, and the tenant token is a host-supplied prop. Access control lives in the API: a stranger's install failsverifyToken(401), and a valid token from an unlisted domain failsverifyOrigin(403). Restricted would need a paid npm org plus an auth token in every host's CI, to protect UI code that isn't sensitive. - Hosts pin exact versions (
"@karibukit/planner-ui": "0.1.0", no^) and bump deliberately. A host upgrade PR states the version delta and what changed. Never point a host at a git branch or a local tarball in prod.
Map data
src/map/legs/*.geojson — committed OSRM road polylines, one per pair in the
operator road network (trip/destinations.ts DRIVE_HOURS), keyed by the API's
real destination slugs. 27 legs, ~23KB gz (budget 150KB). One geometry serves
both directions; unknown pairs fall back to a straight line (logged) so the
map never breaks the page.
| | |
|---|---|
| Source | OSRM demo server, road data © OpenStreetMap contributors (ODbL) |
| Generated | 2026-08-24 via pnpm generate:legs |
| Simplification | turf tolerance 0.0005 (~55m), 5-decimal coords |
src/map/legs/anchors.json — generated destination anchors (display name,
prose name, coord per slug); the same script writes an identical copy to
apps/api/src/ai/planner/anchors.json for the OG card (an og-image unit test
keeps them matching). legIndex, prefill and og-image import it — never
hand-edit the JSON.
Regeneration is rare and manual: add the destination to PLACES and the pair
to LEGS in scripts/generate-leg-polylines.ts, run pnpm generate:legs
(polite to the shared OSRM server: sequential, 1s apart; enforces the size
budget). Name/coord edits alone: pnpm generate:legs --anchors-only — no
OSRM traffic.
Tile hosting
The host passes mapStyle; nothing in this package assumes a tile vendor.
Current lean (spike decision, unchanged): MapTiler free tier with an
origin-restricted key; revisit self-hosted Protomaps only if tile spend
becomes real money. Deploy-time call with NJ.
Deferred (with triggers)
- Curated-trips starter row — needs an engine endpoint that lists curated trips; add when the engine exposes one.
- ~~Chat transcript restore on reload~~ — shipped.
session-store.tskeeps the session JWT in sessionStorage andgetHistoryreplays the turns through the same reducer as live ones, so the card, map and cards all come back (QA 2026-08-24). The waiting state for it landed after QA 2026-08-25 — a reload used to paint the cold-start screen until the fetch returned. - Per-day drive-hours chip — day cards carry hours in prose; a structured
chip needs
driveHourson the itinerary block's day shape (API change). The LONG_DRIVE warning renders amber in "Worth knowing" instead.
