Skip to content

Commit 344f4d3

Browse files
Merge pull request #42 from OneBusAway/ui-redesign
Redesign chrome and Active Blocks page around the OneBusAway look
2 parents 5a3a65e + ebb6a84 commit 344f4d3

7 files changed

Lines changed: 379 additions & 210 deletions

File tree

transitclockWebapp/frontend/tailwind.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,20 @@
55
@source "../src/main/webapp/**/*.html";
66
@source "../src/main/webapp/**/*.js";
77
@source "../frontend/**/*.js";
8+
9+
/* OneBusAway brand tokens. Source of truth: the OneBusAway design
10+
package (separate repo) at project/assets/colors_and_type.css
11+
(#78aa36 brand, #486621 accent). The status palette mirrors what
12+
Wayfinder's ArrivalDeparture component uses (red = early, green =
13+
on time, violet = late, blue = scheduled-only). */
14+
@theme {
15+
--color-brand: #78aa36;
16+
--color-brand-accent: #486621;
17+
--color-brand-tint: #f0f5e6;
18+
--color-canvas: #f5f6f3;
19+
--color-status-on-time: #22c55e;
20+
--color-status-on-time-ink: #15803d;
21+
--color-status-early: #ef4444;
22+
--color-status-late: #7c3aed;
23+
--color-status-scheduled: #3b82f6;
24+
}

transitclockWebapp/src/main/resources/org/transitclock/i18n/text.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,6 @@ div.Headsign = Headsign
201201
div.Start = Start:
202202
div.End = End:
203203
div.Service = Service:
204+
div.Schedule = Schedule
205+
div.operations = Operations
204206
div.loading = Loading...

transitclockWebapp/src/main/webapp/WEB-INF/tags/layout.tag

Lines changed: 150 additions & 40 deletions
Large diffs are not rendered by default.

transitclockWebapp/src/main/webapp/css/general.css

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,6 @@ body {
88
margin: 0px 0px 10px 0px;
99
}
1010

11-
/* For the header on each page */
12-
#header {
13-
background: linear-gradient(to right, #009933 , #4DDB4D);
14-
margin-left: 0px;
15-
margin-right: 0px;
16-
padding: 10px;
17-
font-weight: 100;
18-
font-size: xx-large;
19-
color: white;
20-
text-shadow: 2px 2px 4px #0D6E0E;
21-
box-shadow: 0px 0px 10px black;
22-
}
23-
/* For header need to override link styles */
24-
#header a:link { color: white;}
25-
#header a:visited { color: white;}
26-
#header a:hover { font-weight: normal;}
27-
2811
#mainDiv {
2912
margin-left: auto;
3013
margin-right: auto;
@@ -54,20 +37,6 @@ body {
5437
margin-top: 5px;
5538
}
5639

57-
/* Make links look better. No underlining, keep colors consistent. */
58-
a:link {
59-
text-decoration: none;
60-
color: #0000a6;
61-
}
62-
63-
a:visited {
64-
color: #0000a6;
65-
}
66-
67-
a:hover {
68-
color: #0000FF;
69-
}
70-
7140
.ui-tooltip {
7241
/* Change background color of tooltips a bit and use a reasonable font size */
7342
background: #faf7f1;

transitclockWebapp/src/main/webapp/javascript/controllers/active_blocks_controller.js

Lines changed: 79 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@ import { Controller } from "https://unpkg.com/@hotwired/stimulus@3.2.2/dist/stim
22

33
const SUMMARY_REFRESH_MS = 60_000;
44

5-
const ADH_CLASSES = Object.freeze({
6-
early: ["text-amber-700", "bg-amber-50"],
7-
onTime: ["text-emerald-700", "bg-emerald-50"],
8-
late: ["text-red-700", "bg-red-50"],
5+
// Source of truth: OneBusAway design package (separate repo) at
6+
// project/assets/colors_and_type.css — red = early, green = on time,
7+
// violet = late.
8+
const STATUS_STYLES = Object.freeze({
9+
early: { text: "text-status-early", bg: "bg-status-early/10", solid: "bg-status-early" },
10+
onTime: { text: "text-status-on-time-ink", bg: "bg-status-on-time/10", solid: "bg-status-on-time" },
11+
late: { text: "text-status-late", bg: "bg-status-late/10", solid: "bg-status-late" },
912
});
10-
const ALL_ADH_CLASSES = [...new Set(Object.values(ADH_CLASSES).flat())];
13+
const ALL_ADH_CLASSES = Object.values(STATUS_STYLES).flatMap((s) => [s.text, s.bg]);
14+
const ALL_STRIPE_CLASSES = [...Object.values(STATUS_STYLES).map((s) => s.solid), "bg-gray-200"];
15+
16+
const blocksLabel = (n) => `${n} ${n === 1 ? "block" : "blocks"}`;
1117

1218
export default class extends Controller {
13-
static targets = ["summary", "accordion", "routeTemplate", "blockTemplate", "loadAll"];
19+
static targets = ["summary", "asOf", "accordion", "routeTemplate", "blockTemplate", "loadAll"];
1420
static values = {
1521
earlyMsec: Number,
1622
lateMsec: Number,
@@ -81,20 +87,31 @@ export default class extends Controller {
8187

8288
#renderSummary(total) {
8389
const blocks = total.blocks ?? 0;
84-
const vehicleCount = (total.late ?? 0) + (total.ontime ?? 0) + (total.early ?? 0);
90+
const late = total.late ?? 0;
91+
const onTime = total.ontime ?? 0;
92+
const early = total.early ?? 0;
93+
const assigned = late + onTime + early;
8594
const pct = (n) => (blocks ? `${((100 * n) / blocks).toFixed(0)}%` : "—");
8695

8796
this.#fill("total-blocks", blocks);
88-
this.#fill("percent-late", pct(total.late ?? 0));
89-
this.#fill("percent-on-time", pct(total.ontime ?? 0));
90-
this.#fill("percent-early", pct(total.early ?? 0));
91-
this.#fill("as-of", new Date().toLocaleTimeString());
97+
this.#fill("percent-late", pct(late));
98+
this.#fill("percent-on-time", pct(onTime));
99+
this.#fill("percent-early", pct(early));
100+
this.#fill("late-count", late ? blocksLabel(late) : "");
101+
this.#fill("on-time-count", onTime ? blocksLabel(onTime) : "");
102+
this.#fill("early-count", early ? blocksLabel(early) : "");
103+
this.#fill("assigned-detail", blocks ? `${assigned}/${blocks}` : "");
104+
105+
if (this.hasAsOfTarget) this.asOfTarget.textContent = new Date().toLocaleTimeString();
92106

93107
const assignedEl = this.summaryTarget.querySelector("[data-field='percent-assigned']");
94108
if (assignedEl) {
95-
assignedEl.textContent = pct(vehicleCount);
96-
const assignedFrac = blocks ? vehicleCount / blocks : 1;
97-
assignedEl.classList.toggle("text-red-600", assignedFrac < 0.9);
109+
assignedEl.textContent = pct(assigned);
110+
const assignedFrac = blocks ? assigned / blocks : 1;
111+
// Drop the brand-accent color and switch to red when assignment is
112+
// unhealthy, so the at-a-glance signal still reads correctly.
113+
assignedEl.classList.toggle("text-brand-accent", assignedFrac >= 0.9);
114+
assignedEl.classList.toggle("text-status-early", assignedFrac < 0.9);
98115
}
99116
}
100117

@@ -121,8 +138,12 @@ export default class extends Controller {
121138
item.dataset.routeName = route.name;
122139
accordion.appendChild(item);
123140
}
141+
const blockCount = route.block?.length ?? 0;
142+
item.querySelector("[data-field='route-tag']").textContent = route.id;
124143
item.querySelector("[data-field='route-name']").textContent = route.name;
125-
item.querySelector("[data-field='route-blocks']").textContent = route.block?.length ?? 0;
144+
item.querySelector("[data-field='route-blocks']").textContent = blockCount;
145+
item.querySelector("[data-field='route-blocks-noun']").textContent = blockCount === 1 ? "block" : "blocks";
146+
item.querySelector("[data-field='route-blocks-denom']").textContent = blockCount;
126147
}
127148

128149
for (const [id, el] of existing) {
@@ -146,46 +167,61 @@ export default class extends Controller {
146167
const vehicleTotal = early + onTime + late;
147168
const blockCount = (route.block ?? []).length;
148169

149-
const earlyEl = item.querySelector("[data-field='route-early']");
150-
const onTimeEl = item.querySelector("[data-field='route-on-time']");
151-
const lateEl = item.querySelector("[data-field='route-late']");
152-
const vehEl = item.querySelector("[data-field='route-vehicles']");
153-
const blocksEl = item.querySelector("[data-field='route-blocks']");
154-
155-
earlyEl.textContent = early;
156-
onTimeEl.textContent = onTime;
157-
lateEl.textContent = late;
158-
vehEl.textContent = vehicleTotal;
159-
blocksEl.textContent = blockCount;
160-
161-
earlyEl.classList.toggle("text-amber-700", early > 0);
162-
earlyEl.classList.toggle("bg-amber-50", early > 0);
163-
lateEl.classList.toggle("text-red-700", late > 0);
164-
lateEl.classList.toggle("bg-red-50", late > 0);
165-
const understaffed = vehicleTotal < blockCount;
166-
vehEl.classList.toggle("text-red-700", understaffed);
167-
vehEl.classList.toggle("bg-red-50", understaffed);
170+
item.querySelector("[data-field='route-early']").textContent = early;
171+
item.querySelector("[data-field='route-on-time']").textContent = onTime;
172+
item.querySelector("[data-field='route-late']").textContent = late;
173+
item.querySelector("[data-field='route-blocks']").textContent = blockCount;
174+
item.querySelector("[data-field='route-blocks-denom']").textContent = blockCount;
175+
item.querySelector("[data-field='route-blocks-noun']").textContent = blockCount === 1 ? "block" : "blocks";
176+
177+
// Vehicle count below block count means the route is short-staffed;
178+
// surface that as red regardless of the per-vehicle adherence colors.
179+
const vehEl = item.querySelector("[data-field='route-vehicles']");
180+
vehEl.textContent = vehicleTotal;
181+
vehEl.classList.toggle("text-status-early", vehicleTotal < blockCount);
182+
183+
this.#toggleVisible(item, "route-early-pair", early > 0);
184+
this.#toggleVisible(item, "route-on-time-pair", onTime > 0);
185+
this.#toggleVisible(item, "route-late-pair", late > 0);
168186

169187
const summary = item.querySelector("[data-vehicle-summary]");
170188
summary?.classList.remove("hidden");
171189
summary?.classList.add("flex");
172190

191+
// Stripe priority is late > early > on-time, falling back to neutral
192+
// gray when no vehicles report yet — otherwise every unloaded card
193+
// would mis-signal "all on time".
194+
const stripe = item.querySelector("[data-field='route-stripe']");
195+
if (stripe) {
196+
stripe.classList.remove(...ALL_STRIPE_CLASSES);
197+
const kind = late > 0 ? "late" : early > 0 ? "early" : onTime > 0 ? "onTime" : null;
198+
stripe.classList.add(kind ? STATUS_STYLES[kind].solid : "bg-gray-200");
199+
}
200+
173201
const blockList = item.querySelector("[data-block-list]");
174202
blockList.replaceChildren();
175203
for (const block of route.block ?? []) {
176204
blockList.appendChild(this.#renderBlock(block));
177205
}
178206
}
179207

208+
#toggleVisible(item, field, visible) {
209+
const el = item.querySelector(`[data-field='${field}']`);
210+
if (!el) return;
211+
el.classList.toggle("hidden", !visible);
212+
el.classList.toggle("inline-flex", visible);
213+
}
214+
180215
#classifyAdh(schAdh) {
181216
if (schAdh < -this.lateMsecValue) return "late";
182217
if (schAdh > this.earlyMsecValue) return "early";
183218
return "onTime";
184219
}
185220

186-
#applyAdhClasses(el, schAdh) {
221+
#applyAdhClasses(el, kind) {
187222
el.classList.remove(...ALL_ADH_CLASSES);
188-
el.classList.add(...ADH_CLASSES[this.#classifyAdh(schAdh)]);
223+
const style = STATUS_STYLES[kind];
224+
el.classList.add(style.text, style.bg);
189225
}
190226

191227
#renderBlock(block) {
@@ -213,8 +249,13 @@ export default class extends Controller {
213249

214250
vehiclesEl.textContent = vehicles.map((v) => v.id).join(", ");
215251
const v0 = vehicles[0];
216-
adhEl.textContent = v0.schAdhStr ?? "—";
217-
this.#applyAdhClasses(adhEl, v0.schAdh);
252+
const kind = this.#classifyAdh(parseInt(v0.schAdh, 10));
253+
adhEl.replaceChildren();
254+
const dot = document.createElement("span");
255+
dot.className = `inline-block size-2 rounded-full ${STATUS_STYLES[kind].solid}`;
256+
adhEl.appendChild(dot);
257+
adhEl.appendChild(document.createTextNode(v0.schAdhStr ?? "—"));
258+
this.#applyAdhClasses(adhEl, kind);
218259
return row;
219260
}
220261
}

0 commit comments

Comments
 (0)