use DepartureActivity, DepartureState, ServiceAlertActivity, ServiceAlertState from "./activities.contract" use NextTrains, NextTrainsProps from "./widget.contract" // Caltrain — the v1 app, in Contract, on the LLP 1004 boundary. // // No JavaScript anywhere: schedule data comes from the Rust data crate // (apps/caltrain/data) through `resource` declarations; the clock time comes // from the stdlib roster's `formatTime`; everything else is Contract. The // first frame is compiled data — the build bakes every resource's boot value // into the plan. // @ref LLP 1048.003 D5 — the one page renders at build into dist/ (and its // 404 with it); the view doesn't branch on the route, so nothing it shows moves. routes nav tab home "/" render=build notfound render=build // @ref LLP 1035.005.000 D8 — Caltrain's own wording, over the roster's floor, // max and toString: whole minutes to a departure (a ceiling is -floor(-x), // never below 0), miles to one decimal ("nearby" under 0.1; the tenths are // split so 2 prints "2.0"), and a walk at 80 metres a minute, at least 1. fn countdownText(at: number, now: number): string = max(0, -floor(-((at - now) / 60000))).toString() fn tenthsText(tenths: number): string = `${floor(tenths / 10)}.${tenths - floor(tenths / 10) * 10}` fn distanceText(metres: number): string = metres / 1609.344 < 0.1 ? "nearby" : `${tenthsText(floor(metres / 1609.344 * 10 + 0.5))} mi` fn walkText(metres: number): string = `${max(1, -floor(-(metres / 80)))} min walk` shape Location lat: number lon: number shape Station id: string name: string zone: number distance: number shape Departure id: string train: number service: string headsign: string at: number // What the binary knows about its own delivery (LLP 1030 D7): the runner // answers it before the data seam, from the update store — `staged` is what // the banner reads, and `deliveryActivate` applies the bundle with carry. shape DeliveryState stream: string seq: number staged: bool // CSS's `pointer` (Media Queries 4): a TV remote points at nothing. shape Viewport pointer: string // @ref LLP 1126.001 WP9 — tokens are asynchronous device facts, including after a relaunch. shape ActivityHandle id: string shape ActivityFact id: string kind: string state: string activityState: string token: option shape Activities linked: bool activities: ActivityFact[] frequentPushesEnabled: bool component Caltrain // The seeded schedule's day; `tick` reads the clock from it, so a tick a // suspension skipped leaves the time right state seededEpoch = 1787915400000 state nowMs = seededEpoch state screen = "home" state stationId = none state query = "" state material = "glass" state deck = false state focus = none // The sky: its colour behind the status bar and the home indicator, or // none for the plain page (the "Sky off" button). state sky = some("#05081a") // The pointer over a station row, the search field's focus, the last key it // saw: the events beyond press and change (LLP 1005 §3), each shown. state hoverId = "" state hoverOn = false state searchFocused = false state lastKey = "" state deckLoaded = false state deckLoads = 0 state deckMessage = "" state trainDelay = 0 // The start's reply names the activity until the source lists it; after an // end it names nothing, so a later tap sends no id it no longer owns. state following = false mutation activitySend as shape ActivityHandle mutation alertSend as shape ActivityHandle state alertStarted = false resource activities = exactActivities() as shape Activities derive tracked = first(activities.activities.filter(a => a.kind == "departure" and (a.activityState == "active" or a.activityState == "stale"))) derive trackedId = match tracked { case some(a) => a.id, case none => following ? match activitySend { case some(a) => a.id, case none => "" } : "" } derive departureState = DepartureState(station=station.name, headsign="San Francisco", train=101, at=seededEpoch + 600000 + trainDelay, status=(trainDelay > 0 ? "Delayed by 10 minutes" : "On time")) action startTracking send activitySend = startActivity("departure", departureState) following = true action delayTrain send activitySend = updateActivity(trackedId, DepartureState(departureState, at=departureState.at + 600000, status="Delayed by 10 minutes")) trainDelay = trainDelay + 600000 action endTracking send activitySend = endActivity(trackedId, DepartureState(departureState, status="Arrived"), "immediate") trainDelay = 0 following = false derive trackedAlert = first(activities.activities.filter(a => a.kind == "service-alert" and (a.activityState == "active" or a.activityState == "stale"))) derive alertId = match trackedAlert { case some(a) => a.id, case none => alertStarted ? match alertSend { case some(a) => a.id, case none => "" } : "" } derive alertFollowing = alertId != "" action startAlert send alertSend = startActivity("service-alert", ServiceAlertState(title="Delay", message="Northbound trains delayed by 10 minutes.", updatedAt=seededEpoch)) alertStarted = true action endAlert send alertSend = endActivity(alertId, ServiceAlertState(title="Service restored", message="Trains are running normally.", updatedAt=seededEpoch), "immediate") alertStarted = false resource location = defaultLocation() as shape Location derive selectedId = match stationId { case some(id) => id, case none => "mv" } resource station = station(selectedId, location) as shape Station resource northBoard = board(selectedId, "north", nowMs) as shape Departure[] resource southBoard = board(selectedId, "south", nowMs) as shape Departure[] resource nearby = nearest(location, 3) as shape Station[] resource allStations = stations(location) as shape Station[] resource matches = search(query, location) as shape Station[] resource delivery = exactDelivery() as shape DeliveryState resource viewport = exactViewport() as shape Viewport derive tv = viewport.pointer == "none" action tick nowMs = seededEpoch + performanceNow() action activateUpdate deliveryActivate() action reloadApp reload() action openStations screen = "stations" action openDeck screen = "web-deck" deckLoaded = false deckLoads = 0 deckMessage = "" action goHome screen = "home" query = "" action selectStation(id) stationId = some(id) screen = "home" query = "" searchFocused = false action setQuery(value) query = value action chooseScheme(scheme: string) setScheme(scheme) action setMaterial(m: string) material = m action toggleDeck deck = not deck action focusDeparture(id) focus = some(id) derive skyOn = match sky { case some(c) => true, case none => false } // Grouped ground keeps the cards distinct in either scheme. derive canvasColor = match sky { case some(c) => c, case none => "-exact-grouped-background" } action hoverStation(id, over) hoverId = id hoverOn = over action focusSearch searchFocused = true action blurSearch searchFocused = false action searchKey(k) lastKey = k action recordDeckLoad deckLoaded = true deckLoads = deckLoads + 1 action recordDeckMessage(payload) deckMessage = payload action toggleSky sky = match sky { case some(c) => none, case none => some("#05081a") } task ticker mount every(1000, tick) view // The sky is optional (the "Sky off" button): the same content inside // the aurora canvas — captured through its surface, the shader every // frame — or in a plain scroll, to see what each costs on a phone. The // root's background is what a phone paints behind the status bar and // the home indicator (the web's rule): the sky or the grouped ground. // Text is sized in em from this root: 16 on a phone or a desktop, 24 on // a TV (pointer none), read across a room. main testId="caltrain-main" viewport-fit="cover" width="100%" height="100%" font-size=(tv ? 24 : 16) background-color=canvasColor accent-color="light-dark(#a92d21, #ff9b90)" // @ref LLP 1020 D6 — the iframe screen is outside the live canvas. if screen == "web-deck" ArtworkScheme(on=skyOn, scheme="dark") section role="region" aria-label="Deck fixture" testId="deck-screen" color="CanvasText" padding-top="calc(env(safe-area-inset-top) + 20px)" padding-right="calc(env(safe-area-inset-right) + 20px)" padding-bottom="calc(env(safe-area-inset-bottom) + 20px)" padding-left="calc(env(safe-area-inset-left) + 20px)" row align-items="center" justify-content="space-between" text "Web deck" font-size="1.25em" font-weight=700 color="CanvasText" button press=goHome aria-label="Back" aria-keyshortcuts="Escape" testId="deck-back" padding=8 background-color="-exact-fill" text "Back" font-size="0.8125em" // The hosted-deck sandbox pair (LLP 1020 §8 Q1's survey): same-origin // scripts, so the agent's guest outline is live on the web arm too. iframe src="/deck/index.html" sandbox="allow-scripts allow-same-origin" testId="deck-frame" load=recordDeckLoad message=recordDeckMessage text `${deckLoaded}` testId="deck-loaded" color="CanvasText" text deckMessage testId="deck-message" color="CanvasText" else // The sky or the plain page around the same content, written once: // `Sky` declares a slot and the content is what is indented under it. Sky(on=skyOn, material=material, selectedId=selectedId, dim=tv) Content(activities=activities, startTracking=startTracking, delayTrain=delayTrain, endTracking=endTracking, startAlert=startAlert, endAlert=endAlert, alertFollowing=alertFollowing, tv=tv, station=station, selectedId=selectedId, screen=screen, material=material, deck=deck, focus=focus, nowMs=nowMs, query=query, sky=skyOn, hoverId=hoverId, hoverOn=hoverOn, searchFocused=searchFocused, lastKey=lastKey, northBoard=northBoard, southBoard=southBoard, nearby=nearby, allStations=allStations, matches=matches, delivery=delivery, openStations=openStations, openDeck=openDeck, setScheme=chooseScheme, setMaterial=setMaterial, toggleDeck=toggleDeck, toggleSky=toggleSky, focusDeparture=focusDeparture, selectStation=selectStation, setQuery=setQuery, goHome=goHome, hoverStation=hoverStation, focusSearch=focusSearch, blurSearch=blurSearch, searchKey=searchKey, activateUpdate=activateUpdate, reloadApp=reloadApp) // @ref LLP 1048.003 D1 — a link preview's summary; the title stays the app's. head description="The next Caltrain departures, north and south, from the station nearest you." component Sky props on: bool material: string selectedId: string dim: bool slot view // The sky is optional (the "Sky off" button): the same content inside // the aurora canvas — captured through its surface, the shader every // frame — or in a plain scroll, to see what each costs on a phone. // The fixed sky tint dims the artwork on TV, independently of the UI scheme. // It fills the screen (`viewport-fit="cover"`); the content keeps to the // safe area: a phone's status bar and home indicator, a TV's overscan. if on canvas surface=glass(material, selectedId) width="100%" height="100%" testId="sky" scroll width="100%" height="100%" background-color=(dim ? "#05081a99" : "#05081a00") padding-top="env(safe-area-inset-top)" padding-right="env(safe-area-inset-right)" padding-bottom="env(safe-area-inset-bottom)" padding-left="env(safe-area-inset-left)" box-sizing="border-box" children else scroll width="100%" height="100%" padding-top="env(safe-area-inset-top)" padding-right="env(safe-area-inset-right)" padding-bottom="env(safe-area-inset-bottom)" padding-left="env(safe-area-inset-left)" box-sizing="border-box" children component Content props activities: Activities startTracking: action delayTrain: action endTracking: action startAlert: action endAlert: action alertFollowing: bool tv: bool station: Station selectedId: string screen: string material: string deck: bool focus: option nowMs: number query: string sky: bool hoverId: string hoverOn: bool searchFocused: bool lastKey: string northBoard: Departure[] southBoard: Departure[] nearby: Station[] allStations: Station[] matches: Station[] delivery: DeliveryState openStations: action openDeck: action setScheme: action setMaterial: action toggleDeck: action toggleSky: action focusDeparture: action selectStation: action setQuery: action goHome: action hoverStation: action focusSearch: action blurSearch: action searchKey: action activateUpdate: action reloadApp: action view // One column on a phone; on a TV, two halves: the station and its map // on the left, the departures (or the station list) on the right. view display="flex" flex-direction=(tv ? "row" : "column") gap=(tv ? 0 : 16) padding=(tv ? 0 : 20) max-width=(tv ? 4000 : 640) align-self="center" width="100%" height=(tv ? "100%" : "auto") box-sizing="border-box" column gap=16 width=(tv ? "50%" : "100%") padding=(tv ? 20 : 0) box-sizing="border-box" focusGuide="auto" testId="column-station" // An update is staged (LLP 1030 D7): say so, and let the person take it // now — with carry, the selected station survives — or at the next launch. if delivery.staged row justify-content="space-between" align-items="center" background-color="light-dark(#fff4d6, #40351c)" border-radius=12 padding=12 testId="update-banner" text "An update is ready" font-size="0.8125em" font-weight=600 color="CanvasText" testId="update-ready" button press=activateUpdate aria-label="Apply the update now" testId="delivery-activate" padding=8 border-radius=8 background-color="-exact-fill" text "Update now" font-size="0.75em" font-weight=600 color="CanvasText" header width="100%" background-color="light-dark(#ffffffd9, #1c1c1ed9)" border-radius=16 padding=14 box-sizing="border-box" row justify-content="space-between" align-items="center" column gap=2 image "assets/caltrain.png" width=96 object-fit="contain" aria-label="A Caltrain train" testId="logo" text "Caltrain" font-size="0.8125em" font-weight=700 letter-spacing=1.2 color="AccentColor" testId="brand" text station.name font-size="1.5em" font-weight=700 testId="station-name" role="heading" aria-level=1 text `${distanceText(station.distance)} · ${walkText(station.distance)}` font-size="0.75em" color="-exact-secondary-label" column gap=6 align-items="flex-end" // The development host's Reload, as its dev menu has it. Only a // The large buttons, the full width of the column: a remote's Up // from a small button below lands here even when none is above it. column gap=6 align-items="flex-end" width="100%" focusGuide="auto" // TV shows it: a Siri Remote has no gesture for the dev menu. if tv button press=reloadApp aria-label="Reload" testId="reload" padding=8 border-radius=8 background-color="-exact-fill" text "Reload" font-size="0.75em" font-weight=600 button press=openStations aria-label="Change station" testId="change-station" padding=8 border-radius=8 background-color="-exact-fill" text "Change station" font-size="0.8125em" font-weight=600 button press=openDeck aria-label="Open web deck" testId="open-deck" padding=8 border-radius=8 background-color="-exact-fill" text "Web deck" font-size="0.75em" font-weight=600 if activities.linked column gap=6 align-items="flex-end" button press=startTracking testId="start-activity" text "Follow train" button press=delayTrain testId="delay-train" text "Delay train" button press=endTracking testId="end-activity" text "End activity" // The system-surface smoke starts both kinds through the app's controls. if alertFollowing button press=endAlert testId="end-alert" text "End alert" else button press=startAlert testId="start-alert" text "Follow service alert" // The small buttons get room for a focus ring (a TV remote, Tab, VoiceOver); // a TV draws its ring outside the button, so its rows are wider apart. row gap=(tv ? 12 : 6) // A TV has no light and dark scheme for the app to choose. if not tv button appearance="none" press=setScheme("light") aria-label="Light" testId="scheme-light" text "Light" font-size="0.75em" button appearance="none" press=setScheme("dark") aria-label="Dark" testId="scheme-dark" text "Dark" font-size="0.75em" button press=toggleSky aria-label="Toggle the sky" testId="sky-toggle" padding=8 border-radius=8 if sky text "Sky off" font-size="0.75em" else text "Sky on" font-size="0.75em" row gap=(tv ? 12 : 6) button press=setMaterial("glass") aria-label="Glass" testId="material-glass" padding=8 border-radius=8 text "Glass" font-size="0.75em" font-weight=600 button press=setMaterial("ink") aria-label="Ink" testId="material-ink" padding=8 border-radius=8 text "Ink" font-size="0.75em" font-weight=600 button press=setMaterial("crt") aria-label="CRT" testId="material-crt" padding=8 border-radius=8 text "CRT" font-size="0.75em" font-weight=600 button press=toggleDeck aria-label="Deck" testId="deck-toggle" padding=8 border-radius=8 text "Deck" font-size="0.75em" font-weight=600 color="AccentColor" if screen == "home" // A fixed band of sky on a TV, where the left column has room; on a // phone it fits its text, so the departures stay on screen. // Ink uses dark ink on paper; CRT turns dark ink into bright phosphor. // Only text drawn directly over the material takes its input scheme. ArtworkScheme(on=sky, scheme=(material == "glass" ? "dark" : "light")) column padding=20 gap=4 height=(tv ? 220 : "auto") box-sizing="border-box" justify-content="flex-end" testId="aurora" if material == "glass" text "Next stop" font-size="0.75em" font-weight=700 letter-spacing=1.2 color="-exact-secondary-label" text station.name font-size="2.125em" font-weight=800 color="CanvasText" testId="aurora-title" text `${distanceText(station.distance)} · ${walkText(station.distance)} · the whole app laid out by the kernel, painted by CoreText, refracted by the aurora` font-size="0.75em" color="-exact-secondary-label" else text "Next stop" font-size="0.75em" font-weight=700 letter-spacing=1.2 color="-exact-secondary-label" text station.name font-size="2.125em" font-weight=800 color="CanvasText" testId="aurora-title" text `${distanceText(station.distance)} · ${walkText(station.distance)} · the whole app laid out by the kernel, painted by CoreText, composed by the surface` font-size="0.75em" color="-exact-secondary-label" canvas surface=allStations.map(selectedId, northBoard, nowMs, tv ? 24 : 16) width="100%" height="15em" border-radius=16 background-color="-exact-secondary-grouped-background" testId="line-map" column height="100%" box-sizing="border-box" padding-top=16 padding-bottom=16 justify-content="space-between" for s in allStations key=s.id row align-items="center" height="1.25em" testId=`map-${s.id}` view flex=1 column width="0.875em" height="0.875em" align-items="center" justify-content="center" if s.id == selectedId view width="0.875em" height="0.875em" border-radius="0.4375em" background-color="AccentColor" else view width="0.4375em" height="0.4375em" border-radius="0.21875em" background-color="-exact-system-gray" view flex=1 padding-left=10 text s.name font-size="0.875em" color="-exact-secondary-label" testId=`map-name-${s.id}` Pane(tv=tv) if screen == "home" section role="region" aria-label="Departures" display="flex" flex-direction="column" gap=14 testId="home-screen" if deck canvas surface=stack(northBoard, focus, deck, nowMs) width="100%" height=460 testId="deck" for d in northBoard key=d.id button press=focusDeparture(d.id) aria-label=`Train ${d.train}` testId=`card-${d.id}` width="100%" text-align="start" column border-radius=16 padding=16 gap=6 background-color="-exact-secondary-grouped-background" width="100%" box-sizing="border-box" row gap=8 align-items="flex-end" text countdownText(d.at, nowMs) font-size="1.875em" font-weight=800 text "min" font-size="0.8125em" color="-exact-secondary-label" text `${formatTime(d.at, 0, "short")} · Train ${d.train} · ${d.service} to ${d.headsign}` font-size="0.8125em" color="-exact-secondary-label" else DirectionCard(title="Northbound", board=northBoard, nowMs=nowMs, focusDeparture=focusDeparture, testId="board-north") DirectionCard(title="Southbound", board=southBoard, nowMs=nowMs, focusDeparture=focusDeparture, testId="board-south") else section role="region" aria-label="Stations" display="flex" flex-direction="column" gap=12 testId="stations-screen" background-color="light-dark(#ffffffd9, #1c1c1ed9)" border-radius=16 padding=14 box-sizing="border-box" input appearance="none" value=query input=setQuery focus=focusSearch blur=blurSearch key=searchKey placeholder="Search stations" aria-label="Search stations" testId="station-search" padding=12 border-radius=10 border-style="solid" border-width=1 border-color="-exact-separator" if searchFocused text `searching · last key ${lastKey}` font-size="0.6875em" color="-exact-tertiary-label" testId="search-hint" if query == "" column gap=8 text "Nearest" font-size="0.75em" font-weight=700 letter-spacing=0.8 color="-exact-tertiary-label" for s in nearby key=s.id StationRow(station=s, press=selectStation, hover=hoverStation, hot=(hoverOn and s.id == hoverId)) text "All stations" font-size="0.75em" font-weight=700 letter-spacing=0.8 color="-exact-tertiary-label" for s in allStations key=s.id StationRow(station=s, press=selectStation, hover=hoverStation, hot=(hoverOn and s.id == hoverId)) else column gap=8 text "Matches" font-size="0.75em" font-weight=700 letter-spacing=0.8 color="-exact-tertiary-label" for s in matches key=s.id StationRow(station=s, press=selectStation, hover=hoverStation, hot=(hoverOn and s.id == hoverId)) button appearance="none" press=goHome aria-label="Back" aria-keyshortcuts="Escape" testId="stations-back" text "Back" font-size="0.8125em" // The departures' half: on a TV its own scroll, the full height, so the // station on the left stays put while the remote moves down the board. // Its focus guide brings a Right from the left column back to the row the // remote last left. component Pane props tv: bool slot view if tv scroll width="50%" height="100%" focusGuide="auto" testId="column-board" column gap=16 padding=20 box-sizing="border-box" children else column gap=16 width="100%" testId="column-board" children component DirectionCard props title: string board: Departure[] nowMs: number focusDeparture: action testId: string view column border-radius=16 padding=16 gap=10 background-color="light-dark(#ffffffd9, #1c1c1ed9)" testId=testId text title font-size="0.8125em" font-weight=700 color="AccentColor" if board.isEmpty() text "No more trains today" font-size="0.875em" color="-exact-secondary-label" else for d in board key=d.id // Focusable, so Tab and a TV remote can reach each departure. row gap=8 align-items="center" focus=focusDeparture(d.id) aria-label=`Train ${d.train}` testId=`dep-${d.id}` text countdownText(d.at, nowMs) font-size="1.75em" font-weight=700 testId=`countdown-${d.id}` text `min · ${formatTime(d.at, 0, "short")} · Train ${d.train} · ${d.service} to ${d.headsign}` font-size="0.8125em" color="-exact-secondary-label" component StationRow props station: Station press: action hover: action hot: bool view button press=press(station.id) hover=hover(station.id) aria-label=station.name testId=`station-${station.id}` width="100%" text-align="start" // One row whose highlight is bound, not two arms: a hover then sets // two rows on one view instead of retiring five views and building // five more (`perf stations-screen during "tap station-mv hover"`, // LLP 1079 §8). row justify-content="space-between" align-items="center" padding-top=10 padding-bottom=10 border-bottom-style="solid" border-bottom-width=1 border-color="-exact-separator" width="100%" background-color=(hot ? "-exact-secondary-fill" : "#00000000") testId=(hot ? `station-hot-${station.id}` : `row-${station.id}`) column gap=2 text station.name font-size="0.9375em" font-weight=600 text `Zone ${station.zone}` font-size="0.75em" color="-exact-tertiary-label" text distanceText(station.distance) font-size="0.75em" color="-exact-secondary-label" // Fix only the text over artwork; ordinary surfaces follow the app's scheme. component ArtworkScheme props on: bool scheme: string slot view if on view color-scheme=scheme children else children