gtfs-garage-web 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +25 -2
  2. package/dist/dataset.js +1 -1
  3. package/dist/derive-BnZIwdMc.js +122 -0
  4. package/dist/gtfs-garage-web.css +1 -1
  5. package/dist/layers-BfGuBObN.js +167 -0
  6. package/dist/map/layers.d.ts +45 -0
  7. package/dist/map/map.d.ts +85 -1
  8. package/dist/map.js +224 -198
  9. package/dist/mount-_v5kWtDw.js +3157 -0
  10. package/dist/mount.d.ts +58 -1
  11. package/dist/mount.js +21 -5
  12. package/dist/parquet.js +1024 -157
  13. package/dist/react.js +1 -1
  14. package/dist/sources/api.gen.d.ts +597 -0
  15. package/dist/sources/explore/derive.d.ts +81 -0
  16. package/dist/sources/explore/geojson.d.ts +65 -0
  17. package/dist/sources/explore/index.d.ts +33 -0
  18. package/dist/sources/explore/sql.d.ts +88 -0
  19. package/dist/sources/parquet.d.ts +47 -3
  20. package/dist/sources/rest.d.ts +6 -1
  21. package/dist/sources/types.d.ts +48 -0
  22. package/dist/state.d.ts +60 -1
  23. package/dist/ui/calendar-grid.d.ts +66 -0
  24. package/dist/ui/date-bar.d.ts +40 -0
  25. package/dist/ui/explore-format.d.ts +94 -0
  26. package/dist/ui/files-map.d.ts +83 -0
  27. package/dist/ui/history.d.ts +9 -1
  28. package/dist/ui/html.d.ts +32 -0
  29. package/dist/ui/navigation.d.ts +30 -2
  30. package/dist/ui/route-filters.d.ts +73 -0
  31. package/dist/ui/screen-pane.d.ts +52 -0
  32. package/dist/ui/screens/calendar.d.ts +22 -0
  33. package/dist/ui/screens/index.d.ts +26 -0
  34. package/dist/ui/screens/overview.d.ts +17 -0
  35. package/dist/ui/screens/routes.d.ts +48 -0
  36. package/dist/ui/screens/timetable.d.ts +86 -0
  37. package/dist/ui/screens/types.d.ts +152 -0
  38. package/dist/ui/service-periods.d.ts +56 -0
  39. package/dist/ui/sidebar.d.ts +9 -2
  40. package/dist/ui/table.d.ts +11 -1
  41. package/package.json +1 -1
  42. package/dist/mount-HIxPWN03.js +0 -1337
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # gtfs-garage-web
2
2
 
3
- Browse a GTFS feed: a table with filters and foreign-key navigation, and a map
4
- of its stops, shapes and zones. The viewer of
3
+ Browse a GTFS feed: an overview of what is in it, a calendar of the days it
4
+ runs, a page per route - and under all of that, a table with filters and
5
+ foreign-key navigation, and a map of its stops, shapes and zones. The viewer of
5
6
  [GTFS Garage](https://github.com/MobilityData/gtfs-garage), packaged so it can
6
7
  be mounted in another application.
7
8
 
@@ -71,6 +72,28 @@ from your own backend, either serve the same API — specified in
71
72
  a backend of any other shape, for authentication, or for reading Parquet in the
72
73
  browser with no server at all.
73
74
 
75
+ The derived views - the overview, the calendar and the route pages - are four
76
+ optional methods on that interface. `ParquetSource` implements all four against
77
+ Parquet in the browser, so a dataset on a static host gets them with no server
78
+ anywhere; a source that omits them shows the file browser alone, with no
79
+ Explore entries in the sidebar and nothing else changed.
80
+
81
+ ## Changing what it shows
82
+
83
+ The derived views are **screens**, and the screens are a list:
84
+
85
+ ```ts
86
+ import { mount, defaultScreens } from "gtfs-garage-web";
87
+
88
+ mount(root, { screens: [...defaultScreens, myScreen] }); // add one
89
+ mount(root, { screens: [] }); // the file browser alone
90
+ ```
91
+
92
+ A screen is an id, an optional sidebar label, an `available` predicate, a
93
+ `fetch` and a `render`. See
94
+ [`docs/INTEGRATION.md`](https://github.com/MobilityData/gtfs-garage/blob/main/docs/INTEGRATION.md)
95
+ for the whole interface and the helpers a screen can build on.
96
+
74
97
  ## Styling
75
98
 
76
99
  Every rule is scoped under `.gtfs-viewer`, so the stylesheet cannot reach your
package/dist/dataset.js CHANGED
@@ -1,4 +1,4 @@
1
- import { m as h, p as L } from "./mount-HIxPWN03.js";
1
+ import { m as h, p as L } from "./mount-_v5kWtDw.js";
2
2
  function P(e, s) {
3
3
  const { description: a, canPrepare: n, asked: c, phrase: f } = s;
4
4
  switch (e.state) {
@@ -0,0 +1,122 @@
1
+ const l = [1, 2, 3, 4, 5, 6, 0], f = {
2
+ 0: "Sunday",
3
+ 1: "Monday",
4
+ 2: "Tuesday",
5
+ 3: "Wednesday",
6
+ 4: "Thursday",
7
+ 5: "Friday",
8
+ 6: "Saturday"
9
+ }, d = [
10
+ "Jan",
11
+ "Feb",
12
+ "Mar",
13
+ "Apr",
14
+ "May",
15
+ "Jun",
16
+ "Jul",
17
+ "Aug",
18
+ "Sep",
19
+ "Oct",
20
+ "Nov",
21
+ "Dec"
22
+ ], h = 1100, m = 1200;
23
+ function y(r, n, o) {
24
+ if (!o.length) return "No service";
25
+ if (r.length === 1) {
26
+ const [t, a, c] = r[0].split("-").map(Number);
27
+ return `${c} ${d[a - 1]} ${t}`;
28
+ }
29
+ const e = new Set([...n.entries()].filter(([, t]) => t).map(([t]) => t)), s = (...t) => t.length === e.size && t.every((a) => e.has(a));
30
+ return s(1, 2, 3, 4, 5) ? "Weekdays" : s(0, 6) ? "Weekends" : e.size === 1 ? `${f[[...e][0]]}s` : s(1, 2, 3, 4, 5, 6) ? "Monday to Saturday" : e.size === 7 ? "Every day" : l.filter((t) => e.has(t)).map((t) => f[t].slice(0, 3)).join(", ");
31
+ }
32
+ function M(r) {
33
+ const n = /* @__PURE__ */ new Map();
34
+ for (const e of r) {
35
+ const s = JSON.stringify(e.services);
36
+ let t = n.get(s);
37
+ t || (t = { services: [...e.services], dates: [], weekdays: /* @__PURE__ */ new Map(), trips: 0, routes: 0 }, n.set(s, t)), t.dates.push(e.service_date), t.weekdays.set(e.weekday, (t.weekdays.get(e.weekday) ?? 0) + 1), t.trips = e.trips, t.routes = e.routes;
38
+ }
39
+ const o = [];
40
+ for (const e of n.values()) {
41
+ const s = [...e.dates].sort();
42
+ o.push({
43
+ name: y(s, e.weekdays, e.services),
44
+ services: e.services,
45
+ trips: e.trips,
46
+ routes: e.routes,
47
+ days: s.length,
48
+ weekdays: [...e.weekdays.entries()].filter(([, t]) => t).map(([t]) => t).sort((t, a) => t - a),
49
+ first_date: s[0],
50
+ last_date: s[s.length - 1],
51
+ sample_dates: s.slice(0, 5)
52
+ });
53
+ }
54
+ return o.sort((e, s) => s.days - e.days || s.trips - e.trips || g(e.name, s.name));
55
+ }
56
+ function g(r, n) {
57
+ return r < n ? -1 : r > n ? 1 : 0;
58
+ }
59
+ function v(r) {
60
+ var o;
61
+ const n = new Map(l.map((e) => [e, []]));
62
+ for (const e of r) (o = n.get(e.weekday)) == null || o.push(e.trips);
63
+ return l.map((e) => {
64
+ const s = n.get(e) ?? [], t = s.filter((i) => i), a = /* @__PURE__ */ new Map();
65
+ for (const i of t) a.set(i, (a.get(i) ?? 0) + 1);
66
+ let c = 0, u = 0;
67
+ for (const [i, p] of a)
68
+ p > u && (u = p, c = i);
69
+ return {
70
+ weekday: e,
71
+ name: f[e],
72
+ dates: s.length,
73
+ dates_with_service: t.length,
74
+ typical_trips: c,
75
+ max_trips: s.length ? Math.max(...s) : 0
76
+ };
77
+ });
78
+ }
79
+ function w(r) {
80
+ if (r == null) return null;
81
+ const n = (o) => String(o).padStart(2, "0");
82
+ return `${n(Math.floor(r / 3600))}:${n(Math.floor(r % 3600 / 60))}:${n(r % 60)}`;
83
+ }
84
+ const _ = 400;
85
+ function k(r) {
86
+ if (!r.length) return [];
87
+ let n = r[0];
88
+ for (const s of r) s.length > n.length && (n = s);
89
+ const o = [];
90
+ for (const s of n) o.includes(s) || o.push(s);
91
+ let e = new Map(o.map((s, t) => [s, t]));
92
+ for (const s of r) {
93
+ let t = -1;
94
+ for (const a of s) {
95
+ const c = e.get(a);
96
+ if (c !== void 0) {
97
+ t = c;
98
+ continue;
99
+ }
100
+ const u = t + 1;
101
+ o.splice(u, 0, a), e = new Map(o.map((i, p) => [i, p])), t = u;
102
+ }
103
+ }
104
+ return o;
105
+ }
106
+ function S(r) {
107
+ if (!r) return null;
108
+ const n = r.split(":").slice(0, 3).map((o) => Number(o));
109
+ if (n.some((o) => !Number.isInteger(o))) return null;
110
+ for (; n.length < 3; ) n.push(0);
111
+ return n[0] * 3600 + n[1] * 60 + n[2];
112
+ }
113
+ export {
114
+ _ as M,
115
+ M as a,
116
+ S as b,
117
+ w as c,
118
+ h as d,
119
+ m as e,
120
+ k as s,
121
+ v as w
122
+ };
@@ -1 +1 @@
1
- .gtfs-viewer{--bg: #f7f7f8;--panel: #ffffff;--border: #dcdde0;--text: #1c1e21;--muted: #6b7280;--accent: #2563eb;--accent-bg: #eef2ff;--chip-bg: #eef1f5;--font: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-family:var(--font);color:var(--text);background:var(--bg);display:flex;flex-direction:column;height:100%;min-height:0;position:relative}.gtfs-viewer,.gtfs-viewer *{box-sizing:border-box}.gtfs-viewer header{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--panel);border-bottom:1px solid var(--border);flex-shrink:0}.gtfs-viewer header h1{font-size:16px;margin:0}.gtfs-viewer [data-el=source-label]{color:var(--muted);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gtfs-viewer button{font:inherit;cursor:pointer;border:1px solid var(--border);background:var(--panel);border-radius:6px;padding:6px 10px}.gtfs-viewer button.primary{background:var(--accent);color:#fff;border-color:var(--accent)}.gtfs-viewer button:disabled{opacity:.45;cursor:default}.gtfs-viewer button:hover{filter:brightness(.97)}.gtfs-viewer [data-el=layout]{flex:1;display:flex;min-height:0}.gtfs-viewer [data-el=sidebar]{width:210px;flex-shrink:0;background:var(--panel);border-right:1px solid var(--border);overflow-y:auto;padding:8px}.gtfs-viewer .table-tab{display:flex;justify-content:space-between;padding:7px 10px;border-radius:6px;cursor:pointer;font-size:13px}.gtfs-viewer .table-tab:hover{background:var(--bg)}.gtfs-viewer .table-tab.active{background:var(--accent-bg);color:var(--accent);font-weight:600}.gtfs-viewer .table-tab .count{color:var(--muted);font-weight:400}.gtfs-viewer .table-tab.active .count{color:var(--accent)}.gtfs-viewer .table-tab.missing{color:var(--muted);cursor:default;font-style:italic}.gtfs-viewer .table-tab.missing:hover{background:none}.gtfs-viewer .table-tab.forbidden .count{font-style:italic}.gtfs-viewer [data-el=content]{flex:1;display:flex;min-width:0;min-height:0}.gtfs-viewer [data-el=table-pane]{flex:1.3;display:flex;flex-direction:column;min-width:0;border-right:1px solid var(--border)}.gtfs-viewer [data-el=map-pane]{flex:1;min-width:0;position:relative}.gtfs-viewer [data-el=map]{position:absolute;top:0;right:0;bottom:0;left:0}.gtfs-viewer [data-el=filter-bar]{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--panel)}.gtfs-viewer .raw-toggle{display:flex;align-items:center;gap:5px;margin-left:auto;padding:0 4px;font-size:12px;color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none}.gtfs-viewer .raw-toggle input{cursor:pointer}.gtfs-viewer .chip{display:flex;align-items:center;gap:6px;background:var(--chip-bg);border-radius:999px;padding:4px 10px;font-size:12px}.gtfs-viewer .chip button{border:none;background:none;padding:0;color:var(--muted);font-size:13px;line-height:1}.gtfs-viewer [data-el=add-filter-form]{display:none;gap:6px;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--bg);font-size:13px}.gtfs-viewer [data-el=add-filter-form].open{display:flex}.gtfs-viewer [data-el=add-filter-form] select,.gtfs-viewer [data-el=add-filter-form] input{font:inherit;padding:4px 6px;border:1px solid var(--border);border-radius:4px}.gtfs-viewer [data-el=dataset-status]{flex:1;display:flex;align-items:center;justify-content:center;padding:24px}.gtfs-viewer [data-el=dataset-status] .is-error{color:#b42318}.gtfs-viewer [data-el=table-scroll]{flex:1;overflow:auto}.gtfs-viewer table{border-collapse:collapse;width:100%;font-size:12.5px}.gtfs-viewer th,.gtfs-viewer td{border-bottom:1px solid var(--border);padding:5px 10px;text-align:left;white-space:nowrap}.gtfs-viewer th{position:sticky;top:0;background:var(--panel);z-index:1}.gtfs-viewer tr:hover td{background:#fafafa}.gtfs-viewer a.fk-link{color:var(--accent);text-decoration:none}.gtfs-viewer a.fk-link:hover{text-decoration:underline}.gtfs-viewer .related-row{display:flex;gap:4px;flex-wrap:wrap;margin-top:2px}.gtfs-viewer .related-btn{font-size:10.5px;padding:1px 6px;border-radius:999px;background:var(--chip-bg);border:none;color:var(--muted)}.gtfs-viewer .related-btn:hover{background:var(--accent-bg);color:var(--accent)}.gtfs-viewer [data-el=pager]{display:flex;align-items:center;gap:8px;padding:6px 10px;border-top:1px solid var(--border);background:var(--panel);font-size:12.5px}.gtfs-viewer [data-el=pager] .spacer{flex:1}.gtfs-viewer [data-el=load-overlay]{position:fixed;top:0;right:0;bottom:0;left:0;background:#14141973;display:flex;align-items:center;justify-content:center;z-index:50}.gtfs-viewer [data-el=load-overlay].hidden{display:none}.gtfs-viewer [data-el=load-panel]{background:var(--panel);border-radius:10px;padding:24px;width:420px;max-width:90vw}.gtfs-viewer [data-el=load-panel] h2{margin-top:0;font-size:16px}.gtfs-viewer [data-el=dropzone]{border:2px dashed var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--muted);font-size:13px;margin-bottom:12px}.gtfs-viewer [data-el=dropzone].drag{border-color:var(--accent);color:var(--accent)}.gtfs-viewer [data-el=load-panel] .or{text-align:center;color:var(--muted);font-size:12px;margin:8px 0}.gtfs-viewer [data-el=load-panel] input[type=text]{width:100%;padding:6px 8px;border:1px solid var(--border);border-radius:6px;font:inherit;margin-bottom:10px}.gtfs-viewer [data-el=load-panel] input[type=text]:focus{outline:2px solid var(--accent);outline-offset:-1px;border-color:var(--accent)}.gtfs-viewer .field-label{display:block;font-size:12px;color:var(--muted);margin-bottom:4px}.gtfs-viewer [data-el=pick-row]{display:flex;gap:8px;margin-bottom:4px}.gtfs-viewer [data-el=pick-row] button{flex:1}.gtfs-viewer [data-el=load-actions]{display:flex;gap:8px;align-items:center}.gtfs-viewer [data-el=load-status]{color:var(--muted);font-size:12.5px;margin-top:8px}.gtfs-viewer [data-el=load-error]{color:[data-el="b91c1c"];font-size:12.5px;margin-top:8px}.gtfs-viewer [data-el=empty-state]{padding:40px;color:var(--muted);text-align:center}.gtfs-viewer [data-el=options-btn]{flex-shrink:0;white-space:nowrap}.gtfs-viewer [data-el=options-panel]{position:absolute;top:46px;right:16px;z-index:5;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:12px 14px;font-size:12.5px;width:460px;max-width:90vw;max-height:70vh;overflow:auto;box-shadow:0 6px 20px #0000001f}.gtfs-viewer [data-el=options-panel].hidden{display:none}.gtfs-viewer [data-el=options-header]{display:flex;align-items:center;gap:12px;margin-bottom:8px}.gtfs-viewer [data-el=options-header] strong{flex:1}.gtfs-viewer [data-el=options-header] button{padding:0 6px;line-height:1.4}.gtfs-viewer [data-el=options-panel] section+section{margin-top:14px;padding-top:12px;border-top:1px solid var(--border)}.gtfs-viewer [data-el=options-section-title]{font-weight:600;margin-bottom:6px}.gtfs-viewer [data-el=workdir-path]{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;color:var(--muted);cursor:pointer;overflow-wrap:anywhere}.gtfs-viewer [data-el=workdir-summary],.gtfs-viewer [data-el=metrics-summary]{color:var(--muted);margin:2px 0 8px}.gtfs-viewer [data-el=workdir-feeds]{display:flex;flex-direction:column;gap:4px}.gtfs-viewer .workdir-feed{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:var(--bg)}.gtfs-viewer .workdir-feed.current{background:var(--accent-bg);color:var(--accent)}.gtfs-viewer .workdir-feed-text{flex:1;min-width:0}.gtfs-viewer .workdir-feed-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gtfs-viewer .workdir-feed-detail{color:var(--muted);font-size:11.5px}.gtfs-viewer .workdir-feed.current .workdir-feed-detail{color:var(--accent)}.gtfs-viewer .workdir-feed-actions{display:flex;gap:4px;flex-shrink:0}.gtfs-viewer .workdir-feed-actions button{font-size:11.5px;padding:3px 7px}.gtfs-viewer .workdir-remove{line-height:1.2}.gtfs-viewer .workdir-startup.on{background:var(--accent);border-color:var(--accent);color:var(--bg)}.gtfs-viewer .workdir-empty{color:var(--muted);padding:6px 8px}.gtfs-viewer [data-el=workdir-actions]{margin-top:8px}.gtfs-viewer [data-el=recent-feeds]{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.gtfs-viewer .recent-feed{display:flex;align-items:baseline;gap:8px;width:100%;text-align:left;padding:7px 10px}.gtfs-viewer .recent-feed-detail{color:var(--muted);font-size:11.5px;margin-left:auto;flex-shrink:0}.gtfs-viewer [data-el=workdir-status]{color:var(--muted);min-height:1.2em;margin-top:4px}.gtfs-viewer [data-el=workdir-section].busy{opacity:.6}.gtfs-viewer [data-el=metrics-body]{overflow-x:auto}.gtfs-viewer [data-el=metrics-phases]{display:grid;grid-template-columns:auto auto;gap:2px 14px;margin:0 0 12px}.gtfs-viewer [data-el=metrics-phases] dt{color:var(--muted)}.gtfs-viewer [data-el=metrics-phases] dd{margin:0;text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer [data-el=metrics-body] table{border-collapse:collapse;white-space:nowrap}.gtfs-viewer [data-el=metrics-body] th{text-align:left;color:var(--muted);font-weight:500;border-bottom:1px solid var(--border)}.gtfs-viewer [data-el=metrics-body] th,.gtfs-viewer [data-el=metrics-body] td{padding:3px 10px 3px 0}.gtfs-viewer [data-el=metrics-body] td:not(:first-child){text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer [data-el=map-progress]{position:absolute;left:12px;bottom:12px;z-index:2;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:5px 10px;color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums;box-shadow:0 2px 8px #0000001a;pointer-events:none}.gtfs-viewer [data-el=table-scroll] td.value-numeric{text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer [data-el=table-scroll] td.value-implied{color:var(--muted);font-style:italic;cursor:help}.gtfs-viewer [data-el=table-scroll] td.value-malformed{cursor:help}.gtfs-viewer .value-swatch{display:inline-block;width:.75em;height:.75em;margin-right:6px;border:1px solid var(--border);border-radius:2px;vertical-align:baseline}.gtfs-viewer .value-link{color:var(--accent)}
1
+ .gtfs-viewer{--bg: #f7f7f8;--panel: #ffffff;--border: #dcdde0;--text: #1c1e21;--muted: #6b7280;--accent: #2563eb;--accent-bg: #eef2ff;--chip-bg: #eef1f5;--font: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-family:var(--font);color:var(--text);background:var(--bg);display:flex;flex-direction:column;height:100%;min-height:0;position:relative}.gtfs-viewer,.gtfs-viewer *{box-sizing:border-box}.gtfs-viewer header{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--panel);border-bottom:1px solid var(--border);flex-shrink:0}.gtfs-viewer header h1{font-size:16px;margin:0}.gtfs-viewer [data-el=source-label]{color:var(--muted);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gtfs-viewer button{font:inherit;cursor:pointer;border:1px solid var(--border);background:var(--panel);border-radius:6px;padding:6px 10px}.gtfs-viewer button.primary{background:var(--accent);color:#fff;border-color:var(--accent)}.gtfs-viewer button:disabled{opacity:.45;cursor:default}.gtfs-viewer button:hover{filter:brightness(.97)}.gtfs-viewer [data-el=layout]{flex:1;display:flex;min-height:0}.gtfs-viewer [data-el=sidebar]{width:210px;flex-shrink:0;background:var(--panel);border-right:1px solid var(--border);overflow-y:auto;padding:8px}.gtfs-viewer .table-tab{display:flex;justify-content:space-between;padding:7px 10px;border-radius:6px;cursor:pointer;font-size:13px}.gtfs-viewer .table-tab:hover{background:var(--bg)}.gtfs-viewer .table-tab.active{background:var(--accent-bg);color:var(--accent);font-weight:600}.gtfs-viewer .table-tab .count{color:var(--muted);font-weight:400}.gtfs-viewer .table-tab.active .count{color:var(--accent)}.gtfs-viewer .table-tab.missing{color:var(--muted);cursor:default;font-style:italic}.gtfs-viewer .table-tab.missing:hover{background:none}.gtfs-viewer .table-tab.forbidden .count{font-style:italic}.gtfs-viewer [data-el=content]{flex:1;display:flex;flex-direction:column;min-width:0;min-height:0}.gtfs-viewer [data-el=panes]{flex:1;display:flex;min-width:0;min-height:0}.gtfs-viewer [data-el=browse-pane]{flex:1.3;display:flex;flex-direction:column;min-width:0;min-height:0;border-right:1px solid var(--border)}.gtfs-viewer [data-el=toolbar]{display:flex;align-items:center;gap:6px;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--panel);flex-shrink:0}.gtfs-viewer [data-el=table-pane]{flex:1;display:flex;flex-direction:column;min-width:0;min-height:0}.gtfs-viewer [data-el=map-pane]{flex:1;min-width:0;min-height:0;position:relative}.gtfs-viewer [data-el=map]{position:absolute;top:0;right:0;bottom:0;left:0}.gtfs-viewer [data-el=filter-bar]{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--panel)}.gtfs-viewer .raw-toggle{display:flex;align-items:center;gap:5px;margin-left:auto;padding:0 4px;font-size:12px;color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none}.gtfs-viewer .raw-toggle input{cursor:pointer}.gtfs-viewer .chip{display:flex;align-items:center;gap:6px;background:var(--chip-bg);border-radius:999px;padding:4px 10px;font-size:12px}.gtfs-viewer .chip button{border:none;background:none;padding:0;color:var(--muted);font-size:13px;line-height:1}.gtfs-viewer [data-el=add-filter-form]{display:none;gap:6px;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);background:var(--bg);font-size:13px}.gtfs-viewer [data-el=add-filter-form].open{display:flex}.gtfs-viewer [data-el=add-filter-form] select,.gtfs-viewer [data-el=add-filter-form] input{font:inherit;padding:4px 6px;border:1px solid var(--border);border-radius:4px}.gtfs-viewer [data-el=dataset-status]{flex:1;display:flex;align-items:center;justify-content:center;padding:24px}.gtfs-viewer [data-el=dataset-status] .is-error{color:#b42318}.gtfs-viewer [data-el=table-scroll]{flex:1;overflow:auto}.gtfs-viewer table{border-collapse:collapse;width:100%;font-size:12.5px}.gtfs-viewer th,.gtfs-viewer td{border-bottom:1px solid var(--border);padding:5px 10px;text-align:left;white-space:nowrap}.gtfs-viewer th{position:sticky;top:0;background:var(--panel);z-index:1}.gtfs-viewer tr:hover td{background:#fafafa}.gtfs-viewer a.fk-link{color:var(--accent);text-decoration:none}.gtfs-viewer a.fk-link:hover{text-decoration:underline}.gtfs-viewer .related-row{display:flex;gap:4px;flex-wrap:wrap;margin-top:2px}.gtfs-viewer .related-btn{font-size:10.5px;padding:1px 6px;border-radius:999px;background:var(--chip-bg);border:none;color:var(--muted)}.gtfs-viewer .related-btn:hover{background:var(--accent-bg);color:var(--accent)}.gtfs-viewer [data-el=pager]{display:flex;align-items:center;gap:8px;padding:6px 10px;border-top:1px solid var(--border);background:var(--panel);font-size:12.5px}.gtfs-viewer [data-el=pager] .spacer{flex:1}.gtfs-viewer [data-el=load-overlay]{position:fixed;top:0;right:0;bottom:0;left:0;background:#14141973;display:flex;align-items:center;justify-content:center;z-index:50}.gtfs-viewer [data-el=load-overlay].hidden{display:none}.gtfs-viewer [data-el=load-panel]{background:var(--panel);border-radius:10px;padding:24px;width:420px;max-width:90vw}.gtfs-viewer [data-el=load-panel] h2{margin-top:0;font-size:16px}.gtfs-viewer [data-el=dropzone]{border:2px dashed var(--border);border-radius:8px;padding:24px;text-align:center;color:var(--muted);font-size:13px;margin-bottom:12px}.gtfs-viewer [data-el=dropzone].drag{border-color:var(--accent);color:var(--accent)}.gtfs-viewer [data-el=load-panel] .or{text-align:center;color:var(--muted);font-size:12px;margin:8px 0}.gtfs-viewer [data-el=load-panel] input[type=text]{width:100%;padding:6px 8px;border:1px solid var(--border);border-radius:6px;font:inherit;margin-bottom:10px}.gtfs-viewer [data-el=load-panel] input[type=text]:focus{outline:2px solid var(--accent);outline-offset:-1px;border-color:var(--accent)}.gtfs-viewer .field-label{display:block;font-size:12px;color:var(--muted);margin-bottom:4px}.gtfs-viewer [data-el=pick-row]{display:flex;gap:8px;margin-bottom:4px}.gtfs-viewer [data-el=pick-row] button{flex:1}.gtfs-viewer [data-el=load-actions]{display:flex;gap:8px;align-items:center}.gtfs-viewer [data-el=load-status]{color:var(--muted);font-size:12.5px;margin-top:8px}.gtfs-viewer [data-el=load-error]{color:[data-el="b91c1c"];font-size:12.5px;margin-top:8px}.gtfs-viewer [data-el=empty-state]{padding:40px;color:var(--muted);text-align:center}.gtfs-viewer [data-el=options-btn]{flex-shrink:0;white-space:nowrap}.gtfs-viewer [data-el=options-panel]{position:absolute;top:46px;right:16px;z-index:5;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:12px 14px;font-size:12.5px;width:460px;max-width:90vw;max-height:70vh;overflow:auto;box-shadow:0 6px 20px #0000001f}.gtfs-viewer [data-el=options-panel].hidden{display:none}.gtfs-viewer [data-el=options-header]{display:flex;align-items:center;gap:12px;margin-bottom:8px}.gtfs-viewer [data-el=options-header] strong{flex:1}.gtfs-viewer [data-el=options-header] button{padding:0 6px;line-height:1.4}.gtfs-viewer [data-el=options-panel] section+section{margin-top:14px;padding-top:12px;border-top:1px solid var(--border)}.gtfs-viewer [data-el=options-section-title]{font-weight:600;margin-bottom:6px}.gtfs-viewer [data-el=workdir-path]{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;color:var(--muted);cursor:pointer;overflow-wrap:anywhere}.gtfs-viewer [data-el=workdir-summary],.gtfs-viewer [data-el=metrics-summary]{color:var(--muted);margin:2px 0 8px}.gtfs-viewer [data-el=workdir-feeds]{display:flex;flex-direction:column;gap:4px}.gtfs-viewer .workdir-feed{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:var(--bg)}.gtfs-viewer .workdir-feed.current{background:var(--accent-bg);color:var(--accent)}.gtfs-viewer .workdir-feed-text{flex:1;min-width:0}.gtfs-viewer .workdir-feed-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gtfs-viewer .workdir-feed-detail{color:var(--muted);font-size:11.5px}.gtfs-viewer .workdir-feed.current .workdir-feed-detail{color:var(--accent)}.gtfs-viewer .workdir-feed-actions{display:flex;gap:4px;flex-shrink:0}.gtfs-viewer .workdir-feed-actions button{font-size:11.5px;padding:3px 7px}.gtfs-viewer .workdir-remove{line-height:1.2}.gtfs-viewer .workdir-startup.on{background:var(--accent);border-color:var(--accent);color:var(--bg)}.gtfs-viewer .workdir-empty{color:var(--muted);padding:6px 8px}.gtfs-viewer [data-el=workdir-actions]{margin-top:8px}.gtfs-viewer [data-el=recent-feeds]{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.gtfs-viewer .recent-feed{display:flex;align-items:baseline;gap:8px;width:100%;text-align:left;padding:7px 10px}.gtfs-viewer .recent-feed-detail{color:var(--muted);font-size:11.5px;margin-left:auto;flex-shrink:0}.gtfs-viewer [data-el=workdir-status]{color:var(--muted);min-height:1.2em;margin-top:4px}.gtfs-viewer [data-el=workdir-section].busy{opacity:.6}.gtfs-viewer [data-el=metrics-body]{overflow-x:auto}.gtfs-viewer [data-el=metrics-phases]{display:grid;grid-template-columns:auto auto;gap:2px 14px;margin:0 0 12px}.gtfs-viewer [data-el=metrics-phases] dt{color:var(--muted)}.gtfs-viewer [data-el=metrics-phases] dd{margin:0;text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer [data-el=metrics-body] table{border-collapse:collapse;white-space:nowrap}.gtfs-viewer [data-el=metrics-body] th{text-align:left;color:var(--muted);font-weight:500;border-bottom:1px solid var(--border)}.gtfs-viewer [data-el=metrics-body] th,.gtfs-viewer [data-el=metrics-body] td{padding:3px 10px 3px 0}.gtfs-viewer [data-el=metrics-body] td:not(:first-child){text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer [data-el=map-progress]{position:absolute;left:12px;bottom:12px;z-index:2;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:5px 10px;color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums;box-shadow:0 2px 8px #0000001a;pointer-events:none}.gtfs-viewer [data-el=table-scroll] td.value-numeric,.gtfs-viewer [data-el=table-scroll] th.value-numeric{text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer [data-el=table-scroll] td.value-implied{color:var(--muted);font-style:italic;cursor:help}.gtfs-viewer [data-el=table-scroll] td.value-malformed{cursor:help}.gtfs-viewer .value-swatch{display:inline-block;width:.75em;height:.75em;margin-right:6px;border:1px solid var(--border);border-radius:2px;vertical-align:baseline}.gtfs-viewer .value-link{color:var(--accent)}.gtfs-viewer [hidden]{display:none!important}.gtfs-viewer [data-el=screen-pane]{flex:1;min-width:0;min-height:0;overflow-y:auto;background:var(--bg)}.gtfs-viewer .sidebar-group{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);padding:10px 10px 4px}.gtfs-viewer .explore-tab{justify-content:flex-start}.gtfs-viewer .explore{padding:16px 20px 32px}.gtfs-viewer .explore h2{margin:0 0 2px;font-size:18px}.gtfs-viewer .explore h3{margin:0 0 2px;font-size:13px;letter-spacing:.02em;text-transform:uppercase;color:var(--muted)}.gtfs-viewer .explore-hint{color:var(--muted);font-size:12px}.gtfs-viewer .explore-section{margin-top:22px}.gtfs-viewer .explore-section>p{margin:0 0 10px}.gtfs-viewer .explore-loading,.gtfs-viewer .explore-error{padding:24px;color:var(--muted);font-size:13px}.gtfs-viewer .explore-error strong{color:var(--text)}.gtfs-viewer .explore-link{color:var(--accent);text-decoration:none;font-size:12px;margin-right:8px;cursor:pointer}.gtfs-viewer .explore-link:hover{text-decoration:underline}.gtfs-viewer .overview-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap;min-width:0}.gtfs-viewer .overview-header>div{min-width:0}.gtfs-viewer .overview-header h2{overflow-wrap:anywhere}.gtfs-viewer .window-badge{text-align:right;border:1px solid var(--border);border-radius:8px;padding:8px 12px;background:var(--panel);font:inherit;cursor:pointer}.gtfs-viewer .window-dates{font-size:12px;color:var(--muted)}.gtfs-viewer .window-remaining{font-weight:600;font-size:14px}.gtfs-viewer .window-ok .window-remaining{color:#15803d}.gtfs-viewer .window-warn .window-remaining{color:#b45309}.gtfs-viewer .window-gone .window-remaining{color:#b91c1c}.gtfs-viewer .stat-row{display:flex;flex-wrap:wrap;gap:10px}.gtfs-viewer .stat{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:10px 14px;min-width:104px}.gtfs-viewer .stat-clickable{cursor:pointer}.gtfs-viewer .stat-clickable:hover{border-color:var(--accent)}.gtfs-viewer .stat-value{font-size:20px;font-weight:600;font-variant-numeric:tabular-nums}.gtfs-viewer .stat-label{font-size:12px;color:var(--muted)}.gtfs-viewer .agency-list{list-style:none;margin:0;padding:0}.gtfs-viewer .agency-list li{display:flex;align-items:baseline;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)}.gtfs-viewer .agency-name{font-weight:600;font-size:13px}.gtfs-viewer .chip-row{display:flex;flex-wrap:wrap;gap:8px}.gtfs-viewer .chip{display:inline-flex;align-items:center;gap:6px;background:var(--chip-bg);border:1px solid var(--border);border-radius:999px;padding:4px 12px;font-size:12px;cursor:pointer}.gtfs-viewer .chip:hover{border-color:var(--accent)}.gtfs-viewer .chip-count{color:var(--muted);font-variant-numeric:tabular-nums}.gtfs-viewer .feature-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.gtfs-viewer .feature{border:1px solid var(--border);border-radius:8px;padding:8px 10px;background:var(--panel)}.gtfs-viewer .feature-off{opacity:.55}.gtfs-viewer .feature-name{font-size:13px;font-weight:600}.gtfs-viewer .feature-links{margin-top:4px;display:flex;flex-wrap:wrap;gap:2px 8px}.gtfs-viewer .feature-links .explore-link{margin-right:0}.gtfs-viewer .explore-table{width:100%;border-collapse:collapse;background:var(--panel);font-size:13px}.gtfs-viewer .explore-table th{text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);border-bottom:1px solid var(--border);padding:6px 8px}.gtfs-viewer .explore-table td{padding:7px 8px;border-bottom:1px solid var(--border);vertical-align:top}.gtfs-viewer .explore-table .numeric{text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer .pattern-name{font-weight:600}.gtfs-viewer .pattern-empty{opacity:.6}.gtfs-viewer .service-ids{max-width:260px;display:flex;flex-wrap:wrap;gap:2px 8px}.gtfs-viewer .service-ids .explore-link{margin-right:0;overflow-wrap:anywhere}.gtfs-viewer .explore-scroll{overflow-x:auto;max-width:100%}.gtfs-viewer .weekday-chart{display:flex;flex-direction:column;gap:3px}.gtfs-viewer .weekday-row{display:flex;align-items:center;gap:8px;font-size:12px}.gtfs-viewer .weekday-name{width:74px;color:var(--muted)}.gtfs-viewer .weekday-track{flex:1;background:var(--chip-bg);border-radius:4px;height:14px}.gtfs-viewer .weekday-bar{display:block;height:100%;border-radius:4px;background:var(--accent);min-width:2px}.gtfs-viewer .weekday-value{width:56px;text-align:right;font-variant-numeric:tabular-nums}.gtfs-viewer .gap-list{list-style:none;margin:0;padding:0;font-size:13px}.gtfs-viewer .gap-list li{padding:3px 0}.gtfs-viewer .calendar-months{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:14px}.gtfs-viewer .month-label{font-size:12px;color:var(--muted);margin-bottom:4px}.gtfs-viewer .month-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}.gtfs-viewer .month-weekday{font-size:9px;color:var(--muted);text-align:center}.gtfs-viewer .day{aspect-ratio:1;display:flex;align-items:center;justify-content:center;border-radius:3px;font-size:9px;color:var(--muted);background:var(--chip-bg)}.gtfs-viewer .day-0{background:var(--chip-bg)}.gtfs-viewer .day-1{background:#cfe0fb;color:#1e3a8a}.gtfs-viewer .day-2{background:#9dc1f7;color:#1e3a8a}.gtfs-viewer .day-3{background:#5d93ee;color:#fff}.gtfs-viewer .day-4{background:#2563eb;color:#fff}.gtfs-viewer .day-blank{background:none}.gtfs-viewer .route-search{font:inherit;font-size:13px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;flex:1 1 160px;min-width:0;max-width:320px}.gtfs-viewer .route-group{margin-top:18px}.gtfs-viewer .route-group-label{font-size:12px;color:var(--muted);margin-bottom:4px}.gtfs-viewer .route-row{cursor:pointer}.gtfs-viewer .route-row:hover{background:var(--accent-bg)}.gtfs-viewer .route-name{font-weight:600}.gtfs-viewer .route-badge{display:inline-block;min-width:34px;text-align:center;padding:2px 8px;border-radius:5px;font-weight:700;font-size:13px;background:var(--chip-bg)}.gtfs-viewer .route-title{display:flex;align-items:center;gap:10px}.gtfs-viewer .route-actions{display:flex;gap:4px;align-items:center}.gtfs-viewer .direction-list{display:flex;flex-wrap:wrap;gap:10px}.gtfs-viewer .direction{border:1px solid var(--border);border-radius:8px;padding:10px 12px;background:var(--panel);min-width:180px}.gtfs-viewer .direction-label{font-weight:600;font-size:13px}.gtfs-viewer .direction .numeric{font-variant-numeric:tabular-nums;margin:4px 0 6px;font-size:12px}.gtfs-viewer .pattern-list{display:flex;flex-direction:column;gap:6px}.gtfs-viewer details.pattern{border:1px solid var(--border);border-radius:8px;background:var(--panel);padding:8px 12px}.gtfs-viewer details.pattern summary{cursor:pointer}.gtfs-viewer .pattern-head{display:inline-flex;gap:10px;align-items:baseline}.gtfs-viewer .stop-sequence{margin:8px 0 0;padding-left:22px;font-size:12px}.gtfs-viewer .stop-sequence li{padding:1px 0}.gtfs-viewer .hour-chart{display:flex;align-items:flex-end;gap:2px;height:110px;padding-top:8px}.gtfs-viewer .hour{flex:1;display:flex;flex-direction:column;justify-content:flex-end;height:100%}.gtfs-viewer .hour-bar{display:block;background:var(--accent);border-radius:2px 2px 0 0;min-height:1px}.gtfs-viewer .hour-label{font-size:9px;color:var(--muted);text-align:center;height:12px}.gtfs-viewer .calendar-legend{display:flex;align-items:center;gap:4px;margin-bottom:10px;font-variant-numeric:tabular-nums}.gtfs-viewer .calendar-legend .day{width:14px;height:14px;aspect-ratio:auto}.gtfs-viewer .calendar-legend .explore-hint{margin-right:6px}.gtfs-viewer .month-grid .day{border:1px solid transparent;font:inherit;font-size:9px;padding:0;cursor:pointer}.gtfs-viewer .month-grid .day-blank{cursor:default}.gtfs-viewer .month-grid .day:hover{border-color:var(--accent)}.gtfs-viewer .day-selected{outline:2px solid var(--accent);outline-offset:1px}.gtfs-viewer .day-panel{border:1px solid var(--accent);border-radius:8px;background:var(--panel);padding:10px 12px;margin-top:14px}.gtfs-viewer .day-panel-head{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;margin-bottom:8px}.gtfs-viewer .day-routes{display:flex;flex-wrap:wrap;gap:4px;align-items:center}.gtfs-viewer .day-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:10px}.gtfs-viewer .route-chip{font:inherit;font-size:12px;font-weight:600;border:1px solid var(--border);background:var(--chip-bg);border-radius:5px;padding:2px 8px;cursor:pointer}.gtfs-viewer .route-chip:hover{border-color:var(--accent)}.gtfs-viewer .agency-more{padding-top:8px;border-bottom:none}.gtfs-viewer .map-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:12px;padding:8px 10px;border:1px solid var(--border);border-radius:8px;background:var(--panel);font-size:12px}.gtfs-viewer .pattern-headsign{font-size:12px;color:var(--text)}.gtfs-viewer .pattern-body{margin-top:8px}.gtfs-viewer .pattern-links{margin-top:6px}.gtfs-viewer [data-el=content][data-layout=above] [data-el=panes]{flex-direction:column}.gtfs-viewer [data-el=content][data-layout=above] [data-el=map-pane]{flex:0 0 32%;border-right:none;border-bottom:1px solid var(--border);order:-1}.gtfs-viewer [data-el=content][data-layout=above] [data-el=browse-pane]{border-right:none}.gtfs-viewer [data-el=content][data-layout=none] [data-el=map-pane]{display:none}.gtfs-viewer [data-el=mode-switch]{display:inline-flex;border:1px solid var(--border);border-radius:6px;overflow:hidden}.gtfs-viewer button.mode{border:none;border-radius:0;padding:5px 14px;font-size:13px;background:var(--panel);color:var(--muted)}.gtfs-viewer button.mode.active{background:var(--accent-bg);color:var(--accent);font-weight:600}.gtfs-viewer .date-chip{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:4px 6px 4px 12px;border:1px solid var(--accent);background:var(--accent-bg);border-radius:999px;font-size:13px}.gtfs-viewer [data-el=date-chip-day]{font-weight:600;color:var(--accent)}.gtfs-viewer .date-chip button{border:none;background:none;padding:2px 7px;line-height:1;color:var(--accent);border-radius:999px}.gtfs-viewer .date-chip button:hover{background:var(--panel)}.gtfs-viewer [data-el=date-clear]{font-size:16px}.gtfs-viewer [data-el=toolbar]{position:relative}.gtfs-viewer .date-menu{position:absolute;top:100%;left:12px;z-index:20;min-width:280px;background:var(--panel);border:1px solid var(--border);border-radius:8px;box-shadow:0 6px 24px #0000001f;padding:4px}.gtfs-viewer .date-menu-item{display:flex;justify-content:space-between;gap:16px;width:100%;border:none;background:none;text-align:left;padding:7px 10px;border-radius:6px;font-size:13px}.gtfs-viewer .date-menu-item:hover{background:var(--accent-bg);color:var(--accent)}.gtfs-viewer .date-menu-date{color:var(--muted);font-variant-numeric:tabular-nums}.gtfs-viewer .periods{display:flex;flex-direction:column;gap:4px}.gtfs-viewer .period-row{display:flex;align-items:center;gap:10px;font-size:12px}.gtfs-viewer .period-name{width:150px;flex-shrink:0;text-align:right;color:var(--muted);overflow-wrap:anywhere}.gtfs-viewer .period-track{position:relative;flex:1;min-width:0;height:18px;background:var(--chip-bg);border-radius:4px}.gtfs-viewer .period-bar{position:absolute;top:0;height:100%;border:none;border-radius:4px;background:var(--accent);cursor:pointer;padding:0;min-width:3px}.gtfs-viewer .period-bar:hover{background:#1d4ed8}.gtfs-viewer .period-days{width:62px;text-align:right;color:var(--muted);font-variant-numeric:tabular-nums}.gtfs-viewer .period-axis .period-track{background:none;height:auto}.gtfs-viewer .axis-start,.gtfs-viewer .axis-end{color:var(--muted);font-size:11px}.gtfs-viewer .axis-end{float:right}.gtfs-viewer .change-list{list-style:none;margin:0;padding:0;font-size:13px}.gtfs-viewer .change-list li{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;padding:5px 0;border-bottom:1px solid var(--border)}.gtfs-viewer .change-date{font-weight:600;font-variant-numeric:tabular-nums}.gtfs-viewer .day-choices{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}.gtfs-viewer .day-choice{display:flex;flex-direction:column;gap:2px;align-items:flex-start;padding:10px 14px;border:1px solid var(--border);border-radius:8px;background:var(--panel);text-align:left}.gtfs-viewer .day-choice:hover{border-color:var(--accent)}.gtfs-viewer .day-choice-label{font-weight:600;font-size:13px}.gtfs-viewer .day-choice-date{color:var(--muted);font-size:12px}.gtfs-viewer .timetable-scroll{overflow:auto;max-height:100%;border:1px solid var(--border);border-radius:8px;background:var(--panel)}.gtfs-viewer table.timetable{width:auto;border-collapse:separate;border-spacing:0;font-size:12px;font-variant-numeric:tabular-nums}.gtfs-viewer table.timetable th,.gtfs-viewer table.timetable td{padding:4px 8px;border-bottom:1px solid var(--border);white-space:nowrap}.gtfs-viewer table.timetable thead th{position:sticky;top:0;z-index:2;background:var(--panel);border-bottom:1px solid var(--border)}.gtfs-viewer table.timetable .stop-head,.gtfs-viewer table.timetable .stop-cell{position:sticky;left:0;z-index:1;background:var(--panel);border-right:1px solid var(--border);text-align:left;min-width:170px}.gtfs-viewer table.timetable thead .stop-head{z-index:3}.gtfs-viewer .trip-head{cursor:pointer;font-weight:600;text-align:right}.gtfs-viewer .trip-head:hover{background:var(--accent-bg)}.gtfs-viewer .trip-head.selected{background:var(--accent-bg);color:var(--accent)}.gtfs-viewer .trip-block{font-weight:400;font-size:10px;color:var(--muted)}.gtfs-viewer .time-cell{text-align:right}.gtfs-viewer .destination-chips{gap:16px}.gtfs-viewer .destination-chips .chip-group{display:flex;flex-wrap:wrap;gap:6px}.gtfs-viewer .time-cell.col-selected{background:var(--accent-bg)}.gtfs-viewer .time-linked{cursor:pointer}.gtfs-viewer .time-linked:hover{background:var(--accent-bg);color:var(--accent)}.gtfs-viewer .stop-name{border:none;background:none;padding:0;font:inherit;color:var(--text);text-align:left;cursor:pointer}.gtfs-viewer .stop-name:hover{color:var(--accent);text-decoration:underline}.gtfs-viewer .stop-cell a.explore-link{margin-left:8px;font-size:10.5px;color:var(--muted)}.gtfs-viewer .stop-cell a.explore-link:hover{color:var(--accent)}.gtfs-viewer .stop-cell.selected{background:var(--accent-bg)}.gtfs-viewer .stop-cell.selected .stop-name{color:var(--accent);font-weight:600}.gtfs-viewer .timetable-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.gtfs-viewer .timetable-controls .raw-toggle{margin-left:auto}.gtfs-viewer .map-note{margin-top:8px}.gtfs-viewer .stop-mark{width:18px;height:18px;border-radius:50%;border:2px solid var(--accent);background:#2563eb2e;box-shadow:0 0 #2563eb80;animation:gtfs-stop-mark 1.4s ease-out 3}@keyframes gtfs-stop-mark{0%{box-shadow:0 0 #2563eb80}to{box-shadow:0 0 0 16px #2563eb00}}@media(prefers-reduced-motion:reduce){.gtfs-viewer .stop-mark{animation:none}}.gtfs-viewer .time-blank{color:var(--border)}.gtfs-viewer .timetable-grid-wrap{margin-top:14px;min-height:0}.gtfs-viewer .chip.active{background:var(--accent-bg);color:var(--accent);border-color:var(--accent)}.gtfs-viewer .facets{display:flex;flex-direction:column;gap:6px;margin-top:14px}.gtfs-viewer .facet-row{display:flex;align-items:baseline;flex-wrap:wrap;gap:6px}.gtfs-viewer .facet-label{width:110px;flex-shrink:0;font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:var(--muted)}.gtfs-viewer .facet-chip{padding:3px 10px;font-size:12px}.gtfs-viewer .facet-chip.active{background:var(--accent);color:#fff;border-color:var(--accent)}.gtfs-viewer .facet-chip.active .chip-count{color:#fffc}.gtfs-viewer .facet-chip.facet-empty{opacity:.45}.gtfs-viewer .facet-summary{display:flex;align-items:baseline;flex-wrap:wrap;gap:10px;margin-top:12px;font-size:13px;font-weight:600}.gtfs-viewer .facet-summary .explore-hint{font-weight:400}
@@ -0,0 +1,167 @@
1
+ const s = {
2
+ type: "FeatureCollection",
3
+ features: []
4
+ }, a = () => ({ type: "geojson", data: s }), u = ["interpolate", ["linear"], ["zoom"], 6, 1.5, 12, 3, 16, 5], p = 0.45, S = ["interpolate", ["linear"], ["zoom"], 6, 3, 12, 5, 16, 7], C = 0.95, h = ["match", ["get", "route_type"], "3", 1, "1", 4, 3], m = ["match", ["get", "route_type"], "3", 3.5, "1", 5, 4], y = ["match", ["get", "route_type"], "3", 5, "1", 10, 7], I = ["match", ["get", "route_type"], "3", 6, "1", 9, 7], f = ["match", ["get", "route_type"], "1", 0.8, "3", 0.6, 0.7], U = 0.95, g = "#6b7280", d = [
5
+ "let",
6
+ "rgba",
7
+ ["to-rgba", ["to-color", ["concat", "#", ["get", "route_color"]], "#3388ff"]],
8
+ [
9
+ "case",
10
+ [
11
+ ">",
12
+ [
13
+ "+",
14
+ ["*", 0.2126, ["at", 0, ["var", "rgba"]]],
15
+ ["*", 0.7152, ["at", 1, ["var", "rgba"]]],
16
+ ["*", 0.0722, ["at", 2, ["var", "rgba"]]]
17
+ ],
18
+ 0.85 * 255
19
+ ],
20
+ g,
21
+ ["to-color", ["concat", "#", ["get", "route_color"]], "#3388ff"]
22
+ ]
23
+ ], r = "#ff0066";
24
+ function R(t) {
25
+ const e = typeof t == "string" ? t.trim().replace(/^#/, "") : "";
26
+ if (!/^[0-9a-fA-F]{6}$/.test(e)) return null;
27
+ const [o, i, l] = [0, 2, 4].map((c) => parseInt(e.slice(c, c + 2), 16) / 255);
28
+ return 0.2126 * o + 0.7152 * i + 0.0722 * l > 0.85 ? null : `#${e}`;
29
+ }
30
+ const O = ["locations", "routes", "shapes", "stops", "highlight"];
31
+ function _() {
32
+ return Object.fromEntries(O.map((t) => [t, a()]));
33
+ }
34
+ function T() {
35
+ return [
36
+ // Zones first, so they sit under the lines. A zone is an area a rider can
37
+ // be picked up in, not a thing drawn on top of the network.
38
+ {
39
+ id: "locations-fill",
40
+ type: "fill",
41
+ source: "locations",
42
+ paint: { "fill-color": "#7c3aed", "fill-opacity": 0.12 }
43
+ },
44
+ {
45
+ id: "locations-outline",
46
+ type: "line",
47
+ source: "locations",
48
+ paint: { "line-color": "#7c3aed", "line-width": 1, "line-opacity": 0.5 }
49
+ },
50
+ // Shapes no route claims (feeds whose trips.txt has no shape_id) still need
51
+ // drawing, otherwise those feeds render an empty map.
52
+ {
53
+ id: "shapes",
54
+ type: "line",
55
+ source: "shapes",
56
+ paint: { "line-color": "#9ca3af", "line-width": 1.5, "line-opacity": 0.6 }
57
+ },
58
+ {
59
+ id: "routes-white",
60
+ type: "line",
61
+ source: "routes",
62
+ layout: { "line-cap": "round", "line-join": "round" },
63
+ paint: {
64
+ "line-color": "#ffffff",
65
+ "line-width": y,
66
+ "line-opacity": 0.8
67
+ }
68
+ },
69
+ {
70
+ id: "routes",
71
+ type: "line",
72
+ source: "routes",
73
+ layout: { "line-cap": "round", "line-join": "round" },
74
+ paint: {
75
+ "line-color": d,
76
+ "line-width": h,
77
+ "line-opacity": f
78
+ }
79
+ },
80
+ {
81
+ id: "stops",
82
+ type: "circle",
83
+ source: "stops",
84
+ paint: {
85
+ "circle-radius": u,
86
+ "circle-color": "#1c1e21",
87
+ "circle-opacity": p
88
+ }
89
+ },
90
+ {
91
+ id: "highlight-fill",
92
+ type: "fill",
93
+ source: "highlight",
94
+ filter: ["==", ["geometry-type"], "Polygon"],
95
+ paint: { "fill-color": r, "fill-opacity": 0.25 }
96
+ },
97
+ {
98
+ id: "highlight-line",
99
+ type: "line",
100
+ source: "highlight",
101
+ filter: ["!=", ["geometry-type"], "Point"],
102
+ layout: { "line-cap": "round", "line-join": "round" },
103
+ // A focused route's shapes carry its own colour, so the line reads as
104
+ // the same route it is everywhere else; anything else - a row picked in
105
+ // a table - has none and keeps the highlight's own.
106
+ paint: {
107
+ "line-color": ["coalesce", ["get", "highlight_color"], r],
108
+ "line-width": 5,
109
+ "line-opacity": 0.95
110
+ }
111
+ },
112
+ {
113
+ id: "highlight-point-outer",
114
+ type: "circle",
115
+ source: "highlight",
116
+ filter: ["==", ["geometry-type"], "Point"],
117
+ paint: { "circle-radius": 9, "circle-color": "#ffffff", "circle-opacity": 1 }
118
+ },
119
+ {
120
+ id: "highlight-point",
121
+ type: "circle",
122
+ source: "highlight",
123
+ filter: ["==", ["geometry-type"], "Point"],
124
+ paint: { "circle-radius": 6, "circle-color": r, "circle-opacity": 1 }
125
+ }
126
+ ];
127
+ }
128
+ function n() {
129
+ return { version: 8, sources: {}, layers: [] };
130
+ }
131
+ async function P(t) {
132
+ let e = n();
133
+ if (t.kind === "vector" && t.url)
134
+ try {
135
+ const o = await fetch(t.url);
136
+ if (!o.ok) throw new Error(`${o.status} ${o.statusText}`);
137
+ if (e = await o.json(), t.attribution)
138
+ for (const i of Object.values(e.sources))
139
+ i.attribution = t.attribution;
140
+ } catch (o) {
141
+ console.error(`Basemap ${t.url} could not be loaded; continuing without one.`, o), e = n();
142
+ }
143
+ else t.kind === "raster" && t.url && (e.sources.basemap = {
144
+ type: "raster",
145
+ tiles: [t.url],
146
+ tileSize: 256,
147
+ attribution: t.attribution
148
+ }, e.layers.push({ id: "basemap", type: "raster", source: "basemap", minzoom: 0, maxzoom: 22 }));
149
+ return Object.assign(e.sources, _()), e.layers.push(...T()), e;
150
+ }
151
+ export {
152
+ s as E,
153
+ g as P,
154
+ m as R,
155
+ S,
156
+ h as a,
157
+ P as b,
158
+ U as c,
159
+ f as d,
160
+ I as e,
161
+ y as f,
162
+ u as g,
163
+ C as h,
164
+ p as i,
165
+ O as j,
166
+ R as r
167
+ };
@@ -16,6 +16,51 @@ export declare const EMPTY_FEATURE_COLLECTION: {
16
16
  type: "FeatureCollection";
17
17
  features: never[];
18
18
  };
19
+ /**
20
+ * How stops are drawn, and how they are drawn when they are the subject.
21
+ *
22
+ * Scaled by zoom rather than hidden at low zoom: an inspection tool has to
23
+ * show where the stops are before you know where to zoom. The focused pair is
24
+ * exported beside the ordinary one so the map can swap between them without a
25
+ * second copy of either drifting out of step.
26
+ */
27
+ export declare const STOP_RADIUS: (string | number | string[])[];
28
+ export declare const STOP_OPACITY = 0.45;
29
+ export declare const STOP_RADIUS_FOCUSED: (string | number | string[])[];
30
+ export declare const STOP_OPACITY_FOCUSED = 0.95;
31
+ /**
32
+ * How routes are drawn: across the network, and when they are the subject.
33
+ *
34
+ * Thin across the network, where two hundred routes have to share the map.
35
+ * Picked out on their own - a filtered list, a route's card - the same widths
36
+ * left a bus route a 1px line inside its white casing, which read as a white
37
+ * road rather than a route. Exported in pairs so the map swaps between them
38
+ * the way it swaps the stops'.
39
+ */
40
+ export declare const ROUTE_WIDTH: (string | number | string[])[];
41
+ export declare const ROUTE_WIDTH_FOCUSED: (string | number | string[])[];
42
+ export declare const ROUTE_CASING_WIDTH: (string | number | string[])[];
43
+ export declare const ROUTE_CASING_WIDTH_FOCUSED: (string | number | string[])[];
44
+ export declare const ROUTE_OPACITY: (string | number | string[])[];
45
+ export declare const ROUTE_OPACITY_FOCUSED = 0.95;
46
+ /**
47
+ * What a route whose own colour is too pale to see is drawn in.
48
+ *
49
+ * GTFS defaults `route_color` to white, and a white line inside a white casing
50
+ * is no line at all - which is how route 84 vanished from a filtered map.
51
+ */
52
+ export declare const PALE_ROUTE_COLOR = "#6b7280";
53
+ /** What the highlight is drawn in when the thing highlighted has no colour of its own. */
54
+ export declare const HIGHLIGHT_COLOR = "#ff0066";
55
+ /**
56
+ * A route's `route_color` as a line colour for its highlight, or null to keep
57
+ * the highlight's own.
58
+ *
59
+ * Null for a colour the feed leaves empty or states unreadably, and for one too
60
+ * pale to see against a light basemap: GTFS defaults `route_color` to white,
61
+ * and a focused route drawn in white is a route that has vanished.
62
+ */
63
+ export declare function routeLineColor(raw: unknown): string | null;
19
64
  export declare const SOURCE_IDS: readonly ["locations", "routes", "shapes", "stops", "highlight"];
20
65
  export type SourceId = (typeof SOURCE_IDS)[number];
21
66
  /**
package/dist/map/map.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Dom } from "../dom";
2
+ import type { FocusResult } from "../mount";
2
3
  import type { FeatureCollection, ViewerSource } from "../sources/types";
3
4
  import type { AppState } from "../state";
4
5
  export declare class MapController {
@@ -25,13 +26,87 @@ export declare class MapController {
25
26
  private pendingRefresh;
26
27
  /** Layers the server thinned to fit, and by how much. */
27
28
  private readonly simplifiedLayers;
29
+ /**
30
+ * The route features as drawn, kept for framing a focus.
31
+ *
32
+ * `querySourceFeatures` only returns what is currently in view, so framing
33
+ * on it cannot move the map to a route that is off screen - which is every
34
+ * route, the moment a feed covers more than one city. This is the same
35
+ * object MapLibre already holds, so keeping it costs nothing.
36
+ */
37
+ private routeFeatures;
38
+ /** The stops as drawn, for framing a focus that has no lines to frame on. */
39
+ private stopFeatures;
40
+ /** The mark left by `focusStop`, so the next call can move it. */
41
+ private stopMark;
42
+ /**
43
+ * What is currently in focus, so a redraw can put it back.
44
+ *
45
+ * `isolate` is the difference between "this route and nothing else" and
46
+ * "this route, picked out against the rest of the network". Both keep the
47
+ * route emphasised; only the first hides everything around it.
48
+ */
49
+ private focus;
28
50
  private constructor();
51
+ /** See `MapHandle.resize`. */
52
+ resize(): void;
29
53
  isVisible(): boolean;
30
54
  /** Restore the saved preference; defaults to shown. */
31
55
  restoreVisibility(): void;
32
56
  setVisible(visible: boolean): void;
33
57
  private setSourceData;
34
58
  private fitTo;
59
+ /**
60
+ * Show these routes and nothing else, or go back to the whole network.
61
+ *
62
+ * The cheap half is a filter on the layer the feed is already drawn into:
63
+ * the `routes` source carries `route_id` on every feature, so narrowing to
64
+ * one costs a style update and no request, which is what makes it usable on
65
+ * a feed whose shapes took half a minute to arrive.
66
+ *
67
+ * That layer holds **one representative shape per route**, though, so for a
68
+ * single route the caller can pass the route's own shapes in `extra` and get
69
+ * all of its variants rather than one of them. A route whose trips name no
70
+ * shape at all has neither, which is why this reports how much it drew: an
71
+ * empty map with a panel claiming to show a route is worse than saying there
72
+ * is nothing to show.
73
+ *
74
+ * `shapes` holds the lines no route claims, and `stops` every stop in the
75
+ * feed. Neither can be filtered to a route - their features carry no
76
+ * route_id - so both are hidden while a focus is on, but only when the focus
77
+ * has something of its own to put there.
78
+ */
79
+ focusRoutes(routeIds: string[] | null, extra?: FeatureCollection | null, isolate?: boolean, stopIds?: string[]): Promise<FocusResult>;
80
+ /**
81
+ * Put the current focus on the map, and say how many features it drew.
82
+ *
83
+ * Separate from `focusRoutes` because a redraw has to reapply it: `refresh`
84
+ * clears every source, and a focus set while the feed was still streaming
85
+ * would otherwise be wiped by the draw it was waiting for.
86
+ */
87
+ private applyFocus;
88
+ /**
89
+ * Frame the map on what the focus is showing.
90
+ *
91
+ * `stopsOnly` frames on the focused stops instead of the route's line, for a
92
+ * caller that has narrowed to part of a route - one trip of it, say. The
93
+ * line is still drawn, but framing on it would hold the viewport over the
94
+ * whole route and a short working would move the map not at all, which reads
95
+ * as a click that did nothing.
96
+ */
97
+ frameFocus(routeIds: string[], options?: {
98
+ stopsOnly?: boolean;
99
+ }): Promise<void>;
100
+ /**
101
+ * Centre on one stop and mark it, so a row in a table has a place on the map.
102
+ *
103
+ * The mark is a DOM marker rather than a layer: the `highlight` source is
104
+ * already carrying the focused route's own geometry, and borrowing it here
105
+ * would erase the route to show one of its stops. Returns whether the stop
106
+ * has a position at all - plenty of feeds have stops with no coordinates,
107
+ * and a reader deserves to be told rather than watch nothing happen.
108
+ */
109
+ focusStop(stopId: string): Promise<boolean>;
35
110
  highlight(collection: FeatureCollection, options?: {
36
111
  point?: boolean;
37
112
  }): Promise<void>;
@@ -46,8 +121,17 @@ export declare class MapController {
46
121
  private drawLayer;
47
122
  /** Names the layers the server had to reduce, once everything is drawn. */
48
123
  private simplificationNote;
49
- /** Fetch and draw the whole feed. Skipped entirely while the map is hidden. */
124
+ /**
125
+ * Fetch and draw the whole feed. Skipped entirely while the map is hidden.
126
+ *
127
+ * Publishes itself as `pendingRefresh` for the whole of its run, so anything
128
+ * that draws - a highlight, a screen's focus - waits for it instead of being
129
+ * cleared by the pass it was racing. That the *application's* first draw was
130
+ * not published this way is what made a deep-linked route report "nothing to
131
+ * draw": it asked an empty layer and believed the answer.
132
+ */
50
133
  refresh(): Promise<void>;
134
+ private draw;
51
135
  clear(): Promise<void>;
52
136
  /**
53
137
  * Give up the map and everything holding it.