From 03966746807fe5a4fb82840dd37772b3184cb58f Mon Sep 17 00:00:00 2001 From: CodeDotJS Date: Sun, 4 Oct 2026 19:15:25 +0530 Subject: [PATCH 1/2] fix(ui): add a text size slider and tighten the phone ride --- .cursor/rules/00-product.mdc | 2 +- .cursor/rules/60-deployment.mdc | 6 +++- .cursor/rules/70-phone.mdc | 1 + AGENTS.md | 2 +- src/App.tsx | 20 ++++--------- src/index.css | 52 +++++++++++++++++++++++++++++---- src/lib/plan/plan.test.tsx | 2 +- src/lib/plan/textSize.ts | 17 +++++++++++ src/ui/HelpView.tsx | 30 +++++++++++-------- 9 files changed, 96 insertions(+), 36 deletions(-) create mode 100644 src/lib/plan/textSize.ts diff --git a/.cursor/rules/00-product.mdc b/.cursor/rules/00-product.mdc index 2a7c644..3f75e41 100644 --- a/.cursor/rules/00-product.mdc +++ b/.cursor/rules/00-product.mdc @@ -19,6 +19,6 @@ Follow `docs/DMRC_API.md` for upstream calls. Follow `docs/DECISIONS.md` for def Files that stay on this machine and out of git are listed in the local-files rule. Do not delete them, and do not add them to a commit. -Ship through a branch and a pull request. Follow the deployment rule. Do not push straight to `main`. +Do not push or open a pull request until the user has verified the change locally and said to deploy. Follow the deployment rule. Commits use `type(scope): description`. `type` is `feat`, `fix`, `docs`, `test`, `refactor`, or `chore`. `scope` is the area that changed, such as `plan`, `map`, `city`, `offline`, `deploy`, `data`, or `docs`. The description is the change itself, in lowercase, with no period. One commit is one change. Do not use a label that does not name the work. diff --git a/.cursor/rules/60-deployment.mdc b/.cursor/rules/60-deployment.mdc index 49e0362..097f254 100644 --- a/.cursor/rules/60-deployment.mdc +++ b/.cursor/rules/60-deployment.mdc @@ -5,7 +5,11 @@ alwaysApply: true # Deployment -Every change goes through a branch and a pull request. Do not commit or push directly to `main`. +Do not deploy until the user has used the change on this machine and said to deploy. A push that opens a pull request uploads a preview. A merge to `main` uploads production. Both are a deploy. Do not push, do not open a pull request, and do not merge before that. + +Check the change with `npm run dev` at `http://127.0.0.1:5173`, including a phone-width window. + +When the user says to deploy: 1. Branch from `main`. 2. Commit on that branch. `type(scope): description`. One commit is one change. diff --git a/.cursor/rules/70-phone.mdc b/.cursor/rules/70-phone.mdc index f2d3fca..3c0881c 100644 --- a/.cursor/rules/70-phone.mdc +++ b/.cursor/rules/70-phone.mdc @@ -16,3 +16,4 @@ Check 320, 390, and 430 CSS pixels. The page does not scroll sideways. - The header is the mark plus four tabs. It stays on screen, clears the safe area, and does not cover the first line of the page. - On City, the map is visible without scrolling. The trip sheet is the part that scrolls. - Do not add a banner, a tutorial, or extra chrome to fix spacing. +- Default type on a phone is compact. Help has a text-size slider. Do not leave a layout that only becomes readable after the user drags that slider. diff --git a/AGENTS.md b/AGENTS.md index 5279f2a..f122337 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ Routes are calculated from station order. A packed journey supplies the fare, pl - Do not add a Hindi language control. Hindi strings in `src/i18n/copy.ts` exist so the types compile. - Do not load fonts from a CDN. Vendored font files are allowed. - Do not claim live arrivals, live lifts, or official affiliation. -- Ship through a branch and a pull request. Follow `docs/DEPLOYMENT.md`. Do not push straight to `main`. Do not add analytics, ads, or a paid service. +- Do not push or open a pull request until the user has verified the change locally and said to deploy. Follow `docs/DEPLOYMENT.md`. Do not add analytics, ads, or a paid service. ## Local files diff --git a/src/App.tsx b/src/App.tsx index 7cee1a4..1c96985 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import { savedJourneys } from "./lib/offline/saved" import { alternateRide, planRoute, sameRide } from "./lib/transit/route" import { startLineCode } from "./lib/transit/routeMap" import { readSelection, writeSelection } from "./lib/plan/selection" +import { readTextSize, textDefault } from "./lib/plan/textSize" import { createPlanState, planReducer, routeBlockReason } from "./lib/plan/state" import { clearTrips, readTripQuery, readTrips, removeTrip, saveTrip, tripQuery, type SavedTrip } from "./lib/plan/trips" import type { Journey, Lang, Suggestion } from "./lib/transit/types" @@ -120,7 +121,7 @@ export function App() { const [cityMessage, setCityMessage] = useState(null) const [routeOpen, setRouteOpen] = useState(false) const cityRequest = useRef(0) - const [textSize, setTextSize] = useState<"small" | "normal" | "large">(readTextSize) + const [textSize, setTextSize] = useState(readTextSize) const [helpNotice, setHelpNotice] = useState(null) const [trips, setTrips] = useState(readStoredTrips) const [online, setOnline] = useState(() => navigator.onLine) @@ -129,11 +130,10 @@ export function App() { useEffect(() => { document.documentElement.lang = lang - document.documentElement.style.fontSize = - textSize === "large" ? "22px" : textSize === "normal" ? "18px" : "14px" + document.documentElement.style.fontSize = `${textSize}px` try { localStorage.removeItem("dms-lang") - localStorage.setItem("dms-text", textSize) + localStorage.setItem("dms-text", String(textSize)) } catch { // The planner still works when storage is blocked. } @@ -390,7 +390,7 @@ export function App() { } catch { // The controls still reset on screen. } - setTextSize("small") + setTextSize(textDefault) setTrips([]) dispatch({ type: "apply-language", names: namesFor(snapshot.stations, "en") }) clearResult() @@ -580,16 +580,6 @@ export function App() { ) } -function readTextSize(): "small" | "normal" | "large" { - try { - const saved = localStorage.getItem("dms-text") - if (saved === "large" || saved === "normal" || saved === "small") return saved - return "small" - } catch { - return "small" - } -} - function readStoredTrips(): SavedTrip[] { try { return readTrips(localStorage) diff --git a/src/index.css b/src/index.css index e0d4940..6b69d0b 100644 --- a/src/index.css +++ b/src/index.css @@ -48,6 +48,30 @@ summary:active { transform: scale(0.98); } +.text-size { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 10px; +} + +.text-size input[type="range"] { + width: 100%; + min-height: 44px; + margin: 0; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; + accent-color: var(--ink); +} + +.text-size-end { + font-size: 0.85rem; + font-weight: 600; + color: var(--muted); +} + .primary:hover:not(:disabled) { background: #2c3038; } .primary:active:not(:disabled) { background: #000; } @@ -1982,7 +2006,7 @@ summary { } } -@media (max-width: 719px) { +@media (max-width: 959px) { .sheet { gap: 12px; padding: 12px; @@ -2015,6 +2039,7 @@ summary { .board { padding: 0.7rem 0.75rem; + overflow: visible; } .board-line { @@ -2039,9 +2064,9 @@ summary { display: flex; flex-wrap: wrap; gap: 0.15rem 0.35rem; - font-size: 0.78rem; + font-size: 0.9rem; font-weight: 700; - line-height: 1.3; + line-height: 1.35; } .board-names li:not(:last-child)::after { @@ -2057,16 +2082,33 @@ summary { } .stats { + grid-template-columns: 1.35fr 0.82fr 0.82fr; gap: 8px; } + .fare-stat { + grid-column: auto; + } + .stats div { - padding: 10px; + padding: 8px; } .stats dd, .depart-clock { - font-size: 1.35rem; + font-size: 1.05rem; + } + + .stat-note { + font-size: 0.68rem; + } + + .timeline { + gap: 12px; + } + + .fact { + padding: 4px 8px; } .city-route-bar { diff --git a/src/lib/plan/plan.test.tsx b/src/lib/plan/plan.test.tsx index 6f5b4b2..d255cd7 100644 --- a/src/lib/plan/plan.test.tsx +++ b/src/lib/plan/plan.test.tsx @@ -195,7 +195,7 @@ describe("plan screen", () => { const html = renderToStaticMarkup( void + onTextSize: (size: number) => void onClear: () => void onDismissNotice: () => void }) { @@ -33,16 +34,21 @@ export function HelpView({

{copy.textSize}

-
- - - +
+ {copy.textSmall} + onTextSize(Number(event.target.value))} + /> + {copy.textLarger}
From b27793ff23c1c25a812b045835a7944067507b13 Mon Sep 17 00:00:00 2001 From: CodeDotJS Date: Sun, 4 Oct 2026 23:17:08 +0530 Subject: [PATCH 2/2] fix(ui): split weekday and weekend fare into chips --- src/index.css | 303 ++++++++++++++++++++++++++++++++------------ src/ui/CityMap.tsx | 51 +------- src/ui/PlanView.tsx | 110 +++++++++------- 3 files changed, 290 insertions(+), 174 deletions(-) diff --git a/src/index.css b/src/index.css index 6b69d0b..10d6577 100644 --- a/src/index.css +++ b/src/index.css @@ -1287,39 +1287,6 @@ ul[role="listbox"] li { margin: 0; } gap: calc(var(--space) * 2); } -.city-facts { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 0; - padding: 0; - list-style: none; -} - -.city-facts li { - flex: 1 1 9rem; - margin: 0; - padding: 0.75rem 0.9rem; - border-radius: 12px; -} - -.city-facts .stat-fare { background: #ffe08a; } -.city-facts .stat-stops { background: #d4e6ff; } -.city-facts .stat-changes { background: #d4f2de; } - -.city-facts strong { - display: block; - font-size: 1.15rem; - line-height: 1.3; -} - -.city-facts li > span { - display: block; - margin-top: 0.15rem; - color: var(--ink); - font-size: 0.85rem; -} - .city-legs { list-style: none; display: flex; @@ -1579,45 +1546,51 @@ ul[role="listbox"] li { margin: 0; } .stats { display: grid; grid-template-columns: 1fr 1fr; - gap: calc(var(--space) * 1.5); + gap: 8px; margin: 0; } -.fare-stat { grid-column: 1 / -1; } - -.fare-figures { - display: flex; - flex-wrap: wrap; - gap: 0.35rem 1rem; +.stat-row { + display: grid; + gap: 8px; } -.fare-figures > span { display: block; } +.fare-single { grid-column: 1 / -1; } -.stats div { +.stats > div { margin: 0; - padding: calc(var(--space) * 1.5); - border-radius: var(--radius); + min-width: 0; + padding: 8px 10px; + border-radius: 10px; } -.stats .stat-fare { background: #ffe08a; } +.stats .stat-weekday, +.stats .fare-single { background: #ffe08a; } +.stats .stat-weekend { background: #fff3c9; } .stats .stat-stops { background: #d4e6ff; } .stats .stat-changes { background: #d4f2de; } .stats dt { display: flex; align-items: center; - gap: 0.45rem; + gap: 0.35rem; + font-size: 0.75rem; font-weight: 600; + line-height: 1.2; color: var(--ink); } +.stats dt > span:last-child { + min-width: 0; +} + .stat-icon { display: inline-flex; align-items: center; justify-content: center; - width: 1.85rem; - height: 1.85rem; - border-radius: 0.55rem; + width: 1.35rem; + height: 1.35rem; + border-radius: 0.4rem; background: #fff; flex: none; } @@ -1633,10 +1606,10 @@ ul[role="listbox"] li { margin: 0; } .stats dd { margin: 0.15rem 0 0; font-family: var(--display); - font-size: 1.35rem; + font-size: 1.2rem; font-weight: 600; font-variant-numeric: tabular-nums; - line-height: 1.2; + line-height: 1.15; } section.departures { @@ -1731,9 +1704,23 @@ section.departures { font-size: 1.05rem; } -@media (min-width: 720px) { - .stats { grid-template-columns: 1.6fr 1fr 1fr; } - .fare-stat { grid-column: auto; } +.itinerary, +.city-ride-sheet { + container-type: inline-size; +} + +@container (min-width: 36rem) { + .stat-row { + grid-template-columns: 1fr 1fr; + } + + .stats > div { + padding: 10px 12px; + } + + .stats dd { + font-size: 1.35rem; + } } .stat-note { @@ -2008,17 +1995,36 @@ summary { @media (max-width: 959px) { .sheet { - gap: 12px; - padding: 12px; + gap: 8px; + padding: 8px 12px 12px; + } + + .pane { + padding: 0; + gap: 8px; } - .pane, .planner, .map-intro, .map-list, + .map-stage, .itinerary, - .ticket { - padding: 12px; + .ticket, + section:not(.ticket):not(.departures) { + padding: 10px 12px; + gap: 8px; + } + + .result { + gap: 8px; + } + + section.departures { + padding: 10px 12px; + } + + .city-page { + margin: 8px; } .choices { @@ -2038,7 +2044,7 @@ summary { } .board { - padding: 0.7rem 0.75rem; + padding: 8px 10px; overflow: visible; } @@ -2060,12 +2066,21 @@ summary { right: -9px; } + h1 { + font-size: 1.15rem; + } + + .lede, + .criterion { + font-size: 0.88rem; + } + .board-names { display: flex; flex-wrap: wrap; gap: 0.15rem 0.35rem; - font-size: 0.9rem; - font-weight: 700; + font-size: 0.82rem; + font-weight: 600; line-height: 1.35; } @@ -2077,47 +2092,175 @@ summary { .itinerary h2, .city-route-bar h2, - .map-route-title { - font-size: 1.15rem; + .map-route-title, + .city-leg-stop, + .stop-name { + font-size: 1.05rem; } .stats { - grid-template-columns: 1.35fr 0.82fr 0.82fr; gap: 8px; } - .fare-stat { - grid-column: auto; + .stats > div { + padding: 8px 10px; } - .stats div { - padding: 8px; + .stats dd { + margin-top: 0.1rem; + font-size: 1.15rem; } - .stats dd, .depart-clock { - font-size: 1.05rem; + font-size: 1.45rem; + } + + .timeline, + .city-legs, + .ride, + .city-leg { + gap: 8px; + } + + .ride { + margin-left: 0.45rem; + padding-left: 1.35rem; + border-left-width: 4px; + } + + .stop-name::before { + width: 0.78rem; + height: 0.78rem; + border-width: 3px; + left: -1.9rem; + top: 0.16rem; + } + + .stat-row { + display: contents; } - .stat-note { - font-size: 0.68rem; + .fare-stats { + order: 1; } - .timeline { - gap: 12px; + section.departures, + .departures-strip { + order: 2; } - .fact { - padding: 4px 8px; + .line-pill, + .ride-facts .fact, + .change-note, + .stops-toggle summary { + align-items: center; + min-height: 36px; + padding: 0 10px; + border-radius: 8px; + font-size: 0.92rem; + font-weight: 600; + line-height: 1.2; + } + + .line-pill, + .ride-facts .fact { + max-width: 100%; + min-width: 0; + } + + .ride-facts { + gap: 6px; + } + + .ride-facts svg, + .change-note svg, + .stops-label svg, + .stops-chevron { + width: 15px; + height: 15px; + } + + .stops-toggle { + width: 100%; + } + + .stops-toggle summary { + position: relative; + width: fit-content; + max-width: 100%; + min-height: 36px; + background: var(--shade); + border: 0; + } + + .stops-toggle summary::after { + content: ""; + position: absolute; + inset: -4px 0; + } + + .stops-toggle[open] summary { + border-radius: 8px; + } + + .stops-count { + min-width: 1.25rem; + height: 1.25rem; + font-size: 0.78rem; + } + + .city-leg { + padding: 10px 10px 10px 12px; + border-left-width: 4px; + border-radius: 10px; + } + + .route-map ol { + max-height: 38vh; + overflow: auto; + } + + .route-stop { + padding-bottom: 0.35rem; + font-size: 0.92rem; + } + + .route-stem { + min-height: 0.35rem; + } + + .route-dot { + width: 0.7rem; + height: 0.7rem; + border-width: 2px; + } + + .route-change { + font-size: 0.78rem; + } + + .map-stage .zoom-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .map-stage .zoom-row button { + min-height: 44px; + padding: 0 0.35rem; + font-size: 0.88rem; + } + + .schematic text { + font-size: 16px; } .city-route-bar { - padding: 0.75rem 0.75rem 0.15rem; + padding: 10px 12px 2px; } .city-route-body { - gap: 10px; - padding: 0.55rem 0.75rem 1rem; + gap: 8px; + padding: 8px 12px 12px; } .city-split { diff --git a/src/ui/CityMap.tsx b/src/ui/CityMap.tsx index 0b66553..9d2e9fa 100644 --- a/src/ui/CityMap.tsx +++ b/src/ui/CityMap.tsx @@ -11,7 +11,7 @@ import { routeStops } from "../lib/transit/routeMap" import type { Journey, Lang } from "../lib/transit/types" import { measureSwap, playSwap, type SwapDelta } from "./flipSwap" import { Notice } from "./Notice" -import { ServiceTimes, TripMeta } from "./PlanView" +import { JourneyStats, ServiceTimes, TripMeta } from "./PlanView" const coordinates = coordinatesFile.stations as Record const CLEAR_TILE = @@ -503,25 +503,10 @@ function CityRide({ lines: LineSequence[] lang: Lang }) { - const stops = journey.legs.some((leg) => leg.rideStops === null) - ? null - : journey.legs.reduce((sum, leg) => sum + (leg.rideStops ?? 0), 0) return (
-
    -
  • - -
  • -
  • - {stops === null ? copy.timingUnavailable : stops} - {copy.stopsLabel} -
  • -
  • - {journey.changes} - {copy.changesLabel} -
  • -
+
    {journey.legs.map((leg, index) => { const color = legColor(leg, lines, lang) @@ -580,38 +565,6 @@ function CityRide({ ) } -function FareFact({ copy, journey }: { copy: Copy; journey: Journey }) { - if (journey.fare.kind === "weekday-weekend") { - return ( - <> - - ₹{journey.fare.weekday} - ₹{journey.fare.weekend} - - - {copy.weekdayFare} - - {copy.weekendFare} - - - ) - } - if (journey.fare.kind === "untyped") { - return ( - <> - ₹{journey.fare.amount} - {copy.fareLabel} - - ) - } - return ( - <> - {copy.fareUnavailable} - {copy.fareLabel} - - ) -} - function choiceDetail(journey: Journey, copy: Copy): string { const changes = journey.changes === 0 ? copy.noChanges : copy.changeCount(journey.changes) if (journey.durationMinutes === null) return changes diff --git a/src/ui/PlanView.tsx b/src/ui/PlanView.tsx index 637747f..533046a 100644 --- a/src/ui/PlanView.tsx +++ b/src/ui/PlanView.tsx @@ -471,7 +471,6 @@ export function JourneyCard({ lines: LineSequence[] lang: Lang }) { - const stops = totalStops(journey) return (
    @@ -483,37 +482,7 @@ export function JourneyCard({ {journey.destinationName} -
    -
    -
    - - - {copy.fareLabel} -
    -
    - -
    -
    -
    -
    - - - {copy.stopsLabel} -
    -
    {stops === null ? copy.timingUnavailable : copy.stopCount(stops)}
    -
    -
    -
    - - - {copy.changesLabel} -
    -
    {journey.changes === 0 ? copy.noChanges : copy.changeCount(journey.changes)}
    -
    -
    +
      {journey.legs.map((leg, index) => { const color = legColor(leg, lines, lang) @@ -660,21 +629,72 @@ function totalStops(journey: Journey): number | null { return journey.legs.reduce((sum, leg) => sum + (leg.rideStops ?? 0), 0) } -function Fare({ copy, journey }: { copy: Copy; journey: Journey }) { +export function JourneyStats({ copy, journey }: { copy: Copy; journey: Journey }) { + const stops = totalStops(journey) + return ( +
      +
      + +
      +
      +
      +
      + + + {copy.stopsLabel} +
      +
      {stops === null ? copy.timingUnavailable : copy.stopCount(stops)}
      +
      +
      +
      + + + {copy.changesLabel} +
      +
      {journey.changes === 0 ? copy.noChanges : copy.changeCount(journey.changes)}
      +
      +
      +
      + ) +} + +function FareTiles({ copy, journey }: { copy: Copy; journey: Journey }) { if (journey.fare.kind === "weekday-weekend") { return ( - - - ₹{journey.fare.weekday} - {copy.weekdayFare} - - - ₹{journey.fare.weekend} - {copy.weekendFare} - - + <> +
      +
      + + + {copy.weekdayFare} +
      +
      ₹{journey.fare.weekday}
      +
      +
      +
      + + + {copy.weekendFare} +
      +
      ₹{journey.fare.weekend}
      +
      + ) } - if (journey.fare.kind === "untyped") return <>₹{journey.fare.amount} - return <>{copy.fareUnavailable} + const value = journey.fare.kind === "untyped" ? `₹${journey.fare.amount}` : copy.fareUnavailable + return ( +
      +
      + + + {copy.fareLabel} +
      +
      {value}
      +
      + ) }