Improve table view #11
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
gtfs.zone/coloring-book#11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The timetable/schedule view has several UX rough edges: direction tabs show cryptic
Direction 0 (3)labels instead of meaningful destination names; tabs use the outdatedtabs-borderedDaisyUI v4 class; the pinned "Stop" header cell is visually occluded by sticky body cells when scrolling; the trip property rows use a bespoke(i)tooltip icon instead of the shared field-component presence-mark system; and the schedule header duplicates context already shown in breadcrumbs. All fixes are contained in two files —timetable-renderer.tsandtimetable-data-processor.ts.Relevant Context
src/modules/timetable-renderer.tsrenderTimetableHTML()(line 48) — top-level entry point; callsrenderScheduleHeader,renderDirectionTabs,renderTimetableContent. RemoverenderScheduleHeadercall to eliminate the duplicate title.renderDirectionTabs()(line 105) — builds tab markup. Changetabs-bordered→tabs-border(DaisyUI v5).getDirectionDisplayName()(line 574) — returns"${direction.name} (${direction.tripCount})". Change to"Direction X: To [lastStopName]"or"Direction X: No trips".renderTimetableHeader()(line 391) — the<th>for "Stop" needsz-[2] bg-base-100so it renders above sticky body<th>cells when bothtable-pin-rowsandtable-pin-colsare active.renderTripPropertyRows()(line 214) — replace ad-hocrenderTooltip()+requiredMarkwith: tooltip on the whole<th>viadata-tip, colored presence mark (*) fromconfig.presence, no(i)icon.src/modules/timetable-data-processor.tsDirectionInfointerface (line 57) — addlastStopName?: string.getAvailableDirectionsAsync()(line 574) — after grouping trips by direction, for each direction with trips > 0: pick first trip, query itsstop_timessorted bystop_sequence, look up the last stop viathis.relationships.getStopByIdAsync(), attachstop_nameaslastStopName.src/modules/schedule-controller.tsrenderSchedule()(line 826) — the default-direction padding block (lines 850-857) creates bareDirectionInfoobjects withoutlastStopName; these will correctly render as "No trips" so no change needed here.DaisyUI v5 note:
table-pin-rowssetsz-index: 1onthead th;table-pin-colssetsz-index: 1ontbody th:first-child. At the intersection the header cell needsz-index: 2(Tailwind classz-[2]) plus an opaque background to avoid bleed-through.Phase 1 — All fixes (single phase, low risk)
All changes are rendering/display only; data writes and patch system are untouched.
timetable-data-processor.ts: AddlastStopName?: stringtoDirectionInfo.timetable-data-processor.ts: IngetAvailableDirectionsAsync, for each direction with trips, fetch stop_times for one representative trip, sort bystop_sequence, look up last stop name, attach toDirectionInfo.timetable-renderer.ts: UpdategetDirectionDisplayName— returnDirection X: To [lastStopName]when name is known,Direction X: No tripswhentripCount === 0,Direction Xas fallback.timetable-renderer.ts: Changetabs-bordered→tabs-borderinrenderDirectionTabs.timetable-renderer.ts: Addz-[2] bg-base-100to the "Stop"<th>inrenderTimetableHeader; addbg-base-100to body<th>cells inrenderTimetableBodyandrenderTripPropertyRowsfor consistent opaque backgrounds.timetable-renderer.ts: InrenderTripPropertyRows, replacerenderTooltip(description)+ barerequiredMarkwith:data-tiptooltip on the<th>itself (whole cell hoverable), colored*fromconfig.presenceusing the same color map asfield-component.ts(text-error/text-warning/text-success/text-base-content opacity-40), no(i)SVG icon.timetable-renderer.ts: RemoverenderScheduleHeadercall fromrenderTimetableHTML(method body can stay, call is removed).Gotchas
getStopByIdAsyncreturnsRecord<string, unknown> | null— cast to accessstop_name.<th>cells haveclass="stop-name ..."— they already have border and padding; only addbg-base-100.renderTooltipwas used in other paths — it was not; the method was removed entirely sincenoUnusedLocalsis enforced.Phase 2 — Bug fixes from phase 1 review
Six bugs were found during testing. They fall into three groups: code-sharing problems (the timetable duplicated logic that should live in
field-component.ts), tooltip content problems, and layout/sticky-column problems.Group A — Code sharing:
(i)icon and GTFS ref link (bugs 1 & 2)Bug 1: General editor field labels (all of
field-component.ts) still render a(i)SVG viarenderTooltip. The timetable trip property rows do NOT share this code — they roll their owndata-tipinline. Fix: updaterenderTooltipinfield-component.tsto drop the SVG icon and instead put thedata-tipon a slim<a>that wraps a?glyph (or the spec link text) next to the label. This unifies the pattern across the whole app.Bug 2: Trip property
<th>cells have no clickable GTFS spec link.renderTooltipinfield-component.tsalready wraps in<a href={specUrl}>when a URL is available. After fixing Bug 1, exportgetSpecUrlfromfield-component.tsand import it intimetable-renderer.ts; wrap the label text in<a href={specUrl} target="_blank" rel="noopener noreferrer">so the whole label opens the spec.field-component.ts: ChangerenderTooltip— remove the inline SVG, instead render<a class="tooltip tooltip-right" data-tip='...' href="${specUrl}" ...>ⓘ</a>(or no link wrapper when specUrl is empty). The(i)SVG element is eliminated entirely.field-component.ts: ExportgetSpecUrl(changefunctiontoexport function).timetable-renderer.ts: ImportgetSpecUrlfromfield-component.ts. InrenderTripPropertyRows, wrap the human-readable label text in<a href="${getSpecUrl('trips.txt')}" target="_blank" rel="noopener noreferrer">…</a>so clicking the label opens the GTFS spec.Group B — Tooltip content & compactness (bug 3)
The tooltip on trip property
<th>cells shows only the field description. The presence level is not mentioned, and thesnake_casefield name is displayed as a separate visible sub-line rather than inside the tooltip.timetable-renderer.ts: InrenderTripPropertyRows, build thedata-tipvalue as a multi-part string:${description}+ (if presence and presence !== 'Optional')\n\n${config.presence}+ (if presenceCondition)\n${config.presenceCondition}+\n\n${config.field}. Escape the whole thing as a single attribute value.timetable-renderer.ts: Remove the<div class="stop-id text-xs opacity-70">${config.field}</div>sub-line from the<th>— the snake_case name is now in the tooltip.Group C — Layout / sticky-column bugs (bugs 4, 5, 6)
Bug 4: Trip property
<th>cells appear above the "Stop"<thead th>when scrolling. The<thead th>hasz-[2]but is still being obscured. Root cause: thetooltip tooltip-rightclass on the trip property<th>creates a stacking context that competes with the sticky header corner cell.Bug 5: Trip property label column is narrower than the stop column — each
<th>shrinks to fit its text. Fix: addmin-w-[200px]to the trip property<th>cells (matching the Stop header).Bug 6: When scrolling right, trip property label cells get pushed aside by the data cells next to them. Cause:
tooltip tooltip-righton the<th>itself interferes withposition: sticky+z-indexestablished bytable-pin-cols. Fix: move the tooltip off the<th>and onto an inner<div>wrapper; the<th>becomes a clean sticky container.timetable-renderer.ts: InrenderTripPropertyRows, movetooltip tooltip-rightanddata-tipoff the<th>and onto the inner<div class="stop-name-text">wrapper. The<th>itself retains only:stop-name min-w-[200px] p-2 font-medium border-r border-base-300 bg-base-100.timetable-renderer.ts: Verified the Stop header<th>inrenderTimetableHeaderhasz-[2]. Trip property<th>cells now have no tooltip stacking context — the tooltip is on the inner<div>, so DaisyUI'sz-index: 1fromtable-pin-colsis not competing with the header corner.Gotchas
getSpecUrlcurrently buildstrips.txt→#tripstxt(strips the.). Verify the generated anchor matches the live GTFS spec page before shipping.tooltip tooltip-rightfrom the<th>means the tooltip trigger element is now the inner<div>. DaisyUI requires the.tooltipcontainer to be a block or inline-block element — addblockorw-fullif the tooltip arrow misaligns.<th>). Fixing Bug 6 (moving the tooltip off<th>) should also resolve Bug 4 — validate both after the single change.Phase 3 — Shared tooltip abstraction, apostrophe fix, floating header fix
Three issues remain from Phase 2 review: the tooltip content has no labels so it's hard to read at a glance; the Trip Headsign tooltip is silently broken (apostrophe in the description crashes the
data-tip='...'attribute); and the trip property<th>cells still float over the "Stop"<thead>row when scrolling vertically. The fix also extends the new shared pattern to the route and stop property pages, which currently look nothing like the timetable headers.Root cause — apostrophe bug:
escapeHtml(DOM-baseddiv.textContent → div.innerHTML) escapes&,<,>but NOT single quotes.trip_headsign's description contains"trip's", which breaksdata-tip='...'by closing the attribute early. Fix: addescapeAttrthat escapes'→'and"→", switch alldata-tipto double-quoted.Root cause — floating header: DaisyUI v5's
table-pin-rowsCSS appliesz-index: 1to<thead tr>(the row, not individual<th>cells).table-pin-colssets no z-index on sticky<tbody tr th>cells (confirmed from source). The currentz-[2]is on the corner<th>— this only affects stacking within the<thead tr>stacking context, not vs. other rows. Fix: movez-[2]to the<thead tr>element itself.Scope note:
renderFormField/renderLabelinfield-component.tsare used by the route browse page, stop view, agency view, feed info, and all other property panels. UpdatingrenderLabelto use the new shared pattern automatically brings all those pages in line with the timetable.Group A — Shared tooltip abstraction
field-component.ts: AddescapeAttr(text: unknown): string— wrapsescapeHtmlthen replaces'→'and"→". Replace alldata-tip='${escapeHtml(...)}'occurrences in the file withdata-tip="${escapeAttr(...)}".field-component.ts: Add exportedbuildFieldTooltipContent(config: FieldConfig): string— assembles labeled parts joined by\n\n:Description: [text](if any),ID: field_name,Presence: [value](if non-Optional),Condition: [presenceCondition](if present).field-component.ts: SimplifyrenderPresenceMark(config: FieldConfig)— drop thespecUrlparam and the per-*condition tooltip (condition is now in the main tooltip). Return just a plain colored<span class="${colorClass}">*</span>.field-component.ts: Add exportedrenderFieldLabelContent(config: FieldConfig): string— the one shared "display name *(colored)" pattern: label text wrapped in<a href="specUrl">when a spec URL exists, whole label wrapped in<span class="tooltip tooltip-right cursor-help" data-tip="${escapeAttr(buildFieldTooltipContent(config))}">, followed byrenderPresenceMark(config). No ⓘ icon anywhere.field-component.ts: UpdaterenderLabel(config: FieldConfig, inputId: string)— drop thetooltip: stringparameter, callrenderFieldLabelContent(config)for the label text + presence mark, keep thereadonlyIconappended after. Update the one call site inrenderFormFieldaccordingly.field-component.ts: DeleterenderTooltipentirely (no remaining callers).timetable-renderer.ts: AddrenderFieldLabelContentto the import fromfield-component.ts. InrenderTripPropertyRows, replace the entire inlinetipParts/escapedTip/tooltipAttrs/presenceMark/labelContentblock with a singlerenderFieldLabelContent(config)call placed inside<div class="stop-name-text">. RemovegetSpecUrlfrom the import if it's no longer used elsewhere in the file.Group B — Floating header fix
timetable-renderer.ts: InrenderTimetableHeader, addclass="z-[2]"to the<thead tr>element. Removez-[2]from the corner<th class="stop-header ...">(it was on the wrong element — z-index on a child of a sticky<tr>only affects intra-row stacking, not row-vs-row stacking).Gotchas
renderPresenceMarkis currently called withspecUrlin two places withinfield-component.ts(renderLabeland inline). After removingspecUrl, verify neither call site passes it.escapeAttrmust handlenull/undefinedgracefully (same asescapeHtml).page-content-renderer.ts) and stop view (stop-view-controller.ts) both callrenderFormFieldsfromfield-component.ts— they automatically pick up the new pattern without any changes to those files.z-[2]from the corner<th>, verify the Stop text isn't occluded by the trip property row th cells at the intersection of horizontal + vertical scroll.Original Issue
It looks like shit rn