add calendar modal #123

Closed
opened 2026-05-11 21:01:47 +00:00 by maxtkc · 1 comment
Owner

Summary

Add a Calendar Modal that gives an at-a-glance overview of when service is scheduled across all services in the loaded feed. The modal has two tabs: a Month Grid (traditional calendar view with colored service chips per day) and a Timeline/Gantt (week-resolution horizontal bars per service with exception ticks). Clicking any service chip/row closes the modal and navigates to that service's edit view. This is read-only — no edits from within the modal. The modal is opened via a new navbar button (next to Fares).

The approach uses no external calendar libraries — all rendering is plain HTML strings via the existing showModal() infrastructure. Active-service computation follows standard GTFS logic (exceptions override weekly pattern, date range enforced) as a pure function over the already-loaded data.

Relevant Context

Files to create

  • src/modules/calendar-modal.ts — new module, all logic lives here

Files to modify

  • src/index.html — add calendar navbar button (between Shapes and Fares)
  • src/index.ts — wire up button click to showCalendarModal()

Types

  • Calendar, CalendarDates, GTFSTableMap from src/types/gtfs-entities.ts
  • queryRows interface from src/modules/gtfs-database.ts

Patterns to follow

  • showModal() in src/modules/modal-utils.ts — modal primitive; onMount callback to attach interactive DOM event listeners after insertion
  • showFaresModal / showAboutModal — examples of standalone modal functions wired via index.ts
  • Navbar button pattern: id="calendar-btn", class="btn btn-ghost btn-sm btn-square hidden md:flex", SVG icon
  • Service navigation: look at how onServiceClick (or equivalent) is wired in index.ts near service-view-controller usage

Active-service computation (GTFS spec)

For a given service_id and date (YYYYMMDD string):

  1. Check calendar_dates for exception_type=2 (removed) → inactive
  2. Check calendar_dates for exception_type=1 (added) → active
  3. If no exception, check calendar:
    • If date < start_date or date > end_date → inactive
    • Look up the weekday (0=Sun … 6=Sat) in calendar.{sunday,monday,…} → 1=active, 0=inactive
  4. If no calendar row and no exception → inactive

Color palette

Assign a stable color to each service by its index in the sorted list of service IDs. Use a fixed 10-color HSL palette cycling with enough contrast on both dark and light DaisyUI themes.

Navigation callback

showCalendarModal takes an onServiceClick: (service_id: string) => void callback. When a chip or timeline row is clicked the modal closes and the callback fires.


Phase 1 — Month Grid tab

Goal: Render a working month-grid view inside the modal. This is the primary view most users will interact with. Build the data loading, active-service computation, and month grid renderer. Add the navbar button and wire everything up.

  • Create src/modules/calendar-modal.ts with:

    • CalendarModalDeps interface: { gtfsDatabase: { queryRows }, onServiceClick: (service_id: string) => void }
    • PALETTE: string[] — 10 hex or hsl colors for service chips (readable on both dark/light backgrounds; use DaisyUI badge inline style approach)
    • getServiceColor(index: number): string — cycles through palette
    • parseGTFSDate(s: string): Date — parses YYYYMMDD to UTC midnight Date using Date.UTC(year, month-1, day)
    • formatGTFS(date: Date): string — Date → YYYYMMDD
    • getDayOfWeek(gtfsDate: string): number — 0=Sun…6=Sat, derived from YYYYMMDD without timezone issues (use UTC)
    • isServiceActive(calendar: Record<string,unknown> | null, exceptions: Record<string,unknown>[], gtfsDate: string): boolean — pure function implementing the 4-step logic above; use Number(calendar[dayKey]) === 1
    • loadCalendarData(db) — queries all calendar rows and all calendar_dates rows, returns Map<service_id, { calendar, exceptions, color, label }> where label is service_id truncated to 8 chars
    • renderMonthGrid(data: ServiceDataMap, year: number, month: number): string — returns full HTML for the calendar grid:
      • 7-column grid (Su Mo Tu We Th Fr Sa header row)
      • For each day in the month: day number + colored chips (badge badge-xs style, inline background-color) for each active service
      • Exception dates: if a calendar_dates entry exists for that day, add a small + (add, green tint on chip) or (remove, red tint) suffix inside the chip label
      • Days outside the current month are empty/dimmed placeholder cells (greyed background)
      • Each chip has data-service-id and class="cal-chip cursor-pointer" for click handling
      • If more than 4 services are active on a day, show 4 chips + a +N overflow badge
    • renderMonthNav(year: number, month: number): string< [Month Year] > navigation bar HTML with data-action="prev-month" / data-action="next-month" buttons
    • showCalendarModal(deps: CalendarModalDeps): Promise<void> — main entry point:
      • Calls loadCalendarData
      • Renders tab bar (Month Grid / Timeline) — Timeline tab is a placeholder ("Coming soon") for now
      • Renders initial body with month nav + grid for the current month
      • Calls showModal({ title: 'Service Calendar', body, actions: [{label:'Close', onClick:()=>{}}], escapeAction: 0, boxClassName: 'max-w-5xl w-full', onMount })
      • In onMount:
        • Attach click on [data-action="prev-month"] / [data-action="next-month"] to decrement/increment month state, re-render grid HTML in the grid container
        • Attach click on .cal-chip to read data-service-id, call close() then deps.onServiceClick(service_id)
        • Attach tab switching (Month Grid / Timeline tabs)
  • Add #calendar-btn button to src/index.html navbar, between #shapes-btn and #fares-btn:

    • Use Heroicons calendar-days outline SVG (h-5 w-5)
    • Classes: btn btn-ghost btn-sm btn-square hidden md:flex, title="Service Calendar"
  • Wire up in src/index.ts:

    • Import showCalendarModal from ./modules/calendar-modal
    • Add click listener on #calendar-btn that calls showCalendarModal({ gtfsDatabase: this.gtfsParser.gtfsDatabase, onServiceClick: (service_id) => { /* navigate to service */ } })
    • For the onServiceClick callback: navigate to the service using the same mechanism as the browse panel (inspect how service_id navigation works near pageStateManager or navigationActions in index.ts)

Gotchas / invariants:

  • YYYYMMDD arithmetic: never use new Date('YYYYMMDD') (browser-inconsistent timezone). Always use Date.UTC(year, month-1, day).
  • Month grid: JS months are 0-indexed in Date.UTC. Keep variables named clearly (month0 for 0-indexed, month1 for 1-indexed).
  • Chips: if many services are active, chips wrap. Cap at 4 + +N overflow indicator.
  • isServiceActive weekday keys: ['sunday','monday','tuesday','wednesday','thursday','friday','saturday'] — index by getDayOfWeek() result.

Phase 2 — Timeline / Gantt tab

Goal: Implement the Timeline tab with week-level X-axis resolution. One row per service, horizontal bar from start_date to end_date, small ticks for exceptions.

  • In calendar-modal.ts, add renderTimeline(data: ServiceDataMap): string:

    • Compute minDate / maxDate across all services (use both calendar date ranges and calendar_dates entries for services with no calendar row)
    • Snap minDate back to the nearest Sunday so columns are full weeks
    • Build a weeks: string[] array of YYYYMMDD week-start dates from snapped minDate to maxDate
    • Render as a scrollable grid:
      • Left column (fixed, ~120px): colored service chip + full service_id
      • Header row: abbreviated month label spanning the correct number of week columns (compute colspan per month)
      • For each service row: one cell per week
        • Cell background color: service color at ~20% opacity if this week overlaps the service's active range and the weekly pattern has at least one active day that week; else bg-base-100
        • Exception ticks: for each calendar_dates entry in this week, render a small (type=1, green) or (type=2, red) indicator
      • Each row has data-service-id and class="timeline-row cursor-pointer" for click handling
    • Wrap the grid in <div class="overflow-x-auto"> for horizontal scroll
    • Week column width: w-5 (20px). A 1-year feed = 52 × 20px = 1040px — fits in max-w-5xl with the service label column.
  • Replace the "Coming soon" placeholder in the Timeline tab with renderTimeline(data)

  • In onMount: attach click on .timeline-row elements (same onServiceClick close-and-navigate pattern as chips)

  • Handle services with only calendar_dates (no calendar row): derive min/max dates from exceptions; show no bar but show the exception ticks

Gotchas / invariants:

  • Cap range at 3 years max. If maxDate - minDate > 3 years, warn with a note and truncate to 3 years from minDate.
  • "Active week" heuristic: a week is colored active if calendar.start_date ≤ weekEnd AND calendar.end_date ≥ weekStart AND any weekday in calendar is 1. This avoids computing all 7 days per week per service.
  • Services with no data at all (no calendar, no exceptions): skip silently — they shouldn't appear in loadCalendarData's output anyway.

Phase 3 — Month Grid polish

Goal: Three targeted improvements to the Month Grid: scrollable overflow instead of the +N cap, full service IDs with clear visual truncation, and feed-level start/end date markers pulled from feed_info.txt.

  • Scrollable day cells: Remove the 4-chip cap and overflow badge logic entirely. Change the chips container inside each day cell to class="flex flex-col gap-0.5 max-h-24 overflow-y-auto". Keep min-h-16 on the outer cell. This lets every service show without an arbitrary cut-off, and the cell scrolls if tall.

  • Full service IDs with clear truncation: In loadCalendarData, change label: sid.length > 8 ? sid.slice(0, 8) : sid to label: sid. The chip already has truncate max-w-full CSS so it will show ellipsis when the text overflows, which signals truncation to the user. The existing title="${esc(sid)}" attribute shows the full ID on hover. No other changes needed.

  • Feed start/end markers:

    • In showCalendarModal, after loadCalendarData, also call deps.gtfsDatabase.getAllRows('feed_info') and extract feed_start_date / feed_end_date from the first row (if any). Store as feedStartDate: string | null and feedEndDate: string | null.
    • Update the CalendarModalDeps interface's gtfsDatabase type to include getAllRows (it currently only declares it; confirm or add if missing).
    • Pass { feedStartDate, feedEndDate } to renderMonthGrid and to rerenderGrid (update its signature).
    • Inside renderMonthGrid, for each day cell, after the day number <span>, conditionally append small inline badges:
      • If gtfsDate === feedStartDate: <span class="badge badge-xs badge-success ml-1" title="Feed start date">▶</span>
      • If gtfsDate === feedEndDate: <span class="badge badge-xs badge-error ml-1" title="Feed end date">◀</span>
    • The day number row becomes <div class="text-xs text-base-content/60 mb-0.5 flex items-center gap-0.5"> to hold the number and any badges inline.
    • If feed_info table is empty or missing feed_start_date/feed_end_date, skip silently.

Gotchas:

  • getAllRows is already on the gtfsDatabase interface (check the existing usage in loadCalendarData — it uses getAllRows, not queryRows).
  • The feed date badges must not break layout when both appear on the same day (unlikely but possible if feed is 1 day).
  • rerenderGrid in onMount captures year and month1 by closure; just pass the feed dates through the closure too.

Phase 4 — Timeline polish

Goal: Three improvements to the Timeline tab: auto-fit the service label column to the longest ID so nothing is needlessly truncated; add a weekday-pattern dot column so you can see at a glance which days each service runs; replace browser title tooltips on exception ticks with DaisyUI tooltips showing a human-friendly date.

  • Fit label column to longest ID:

    • Before building rowsHtml, compute const maxIdLen = Math.max(0, ...[...data.keys()].map((k) => k.length)).
    • Derive a pixel width: const labelColPx = Math.max(80, maxIdLen * 7 + 32) (7px per char at text-xs + 32px padding). Cap at a reasonable max like 300px.
    • Replace all occurrences of the fixed w-36 min-w-36 max-w-36 (and the w-36 min-w-36 in the <th>) with style="width:${labelColPx}px;min-width:${labelColPx}px;max-width:${labelColPx}px".
    • The truncate on the inner <span> still applies so very long IDs degrade gracefully.
  • Weekday dot column:

    • Define a helper renderWeekdayDots(calendar: Record<string, unknown> | null): string that returns 7 dot characters for Mon–Sun order (['monday','tuesday','wednesday','thursday','friday','saturday','sunday']). For each day: (U+25CF) if Number(calendar[key]) === 1, else (U+25CB). If calendar is null, return 7 dots. Wrap in <span class="font-mono tracking-tight text-base-content/70">.
    • Add a second sticky <th> in the header row (after the label <th>, before the month spans): width ~56px (w-14), label <span class="text-xs">M T W T F S S</span> with tracking-tight font-mono text-base-content/50.
    • In each <tr>, insert a <td> after the label <td> containing the dot string for that service's calendar.
    • This <td> is NOT sticky (doesn't need to scroll with the week columns). Give it class="w-14 min-w-14 px-1 py-1 border-b border-base-300/30 text-xs".
  • DaisyUI tooltips on exception ticks:

    • Add a helper formatHumanDate(gtfsDate: string): string that parses the YYYYMMDD string and returns a string like "Mon, Jan 15 2026" using Date.UTC + Date.prototype.toUTCString or manual formatting with MONTH_ABBR and a day-of-week array.
    • In the exception tick rendering loop, change:
      `<span style="color:#4ade80" title="${dateStr}">▲</span>`
      
      to:
      `<span class="tooltip tooltip-top" data-tip="${formatHumanDate(dateStr)}"><span style="color:#4ade80">▲</span></span>`
      
      Same for the (type 2) tick.
    • DaisyUI tooltip works purely via CSS ([data-tip] pseudo-element), so no JS needed.

Gotchas:

  • The weekday <th> header labels "M T W T F S S" are Mon–Sun, which matches the dot order. Include this as a comment in code if the order differs from WEEKDAY_KEYS (which is Sun-first).
  • tooltip-top ensures the tooltip opens upward; the timeline table scrolls horizontally so tooltip-right/tooltip-left could get clipped.
  • formatHumanDate should use UTC methods consistently (no local-time leakage). A clean approach: const d = parseGTFSDate(gtfsDate); const dayName = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getUTCDay()]; return \${dayName}, ${MONTH_ABBR[d.getUTCMonth()]} ${d.getUTCDate()} ${d.getUTCFullYear()}`;`

Original Issue

Make a calendar modal view to see how far ahead service has been scheduled and when there is holiday service and how they overlap.

## Summary Add a **Calendar Modal** that gives an at-a-glance overview of when service is scheduled across all services in the loaded feed. The modal has two tabs: a **Month Grid** (traditional calendar view with colored service chips per day) and a **Timeline/Gantt** (week-resolution horizontal bars per service with exception ticks). Clicking any service chip/row closes the modal and navigates to that service's edit view. This is read-only — no edits from within the modal. The modal is opened via a new navbar button (next to Fares). The approach uses no external calendar libraries — all rendering is plain HTML strings via the existing `showModal()` infrastructure. Active-service computation follows standard GTFS logic (exceptions override weekly pattern, date range enforced) as a pure function over the already-loaded data. ## Relevant Context ### Files to create - `src/modules/calendar-modal.ts` — new module, all logic lives here ### Files to modify - `src/index.html` — add calendar navbar button (between Shapes and Fares) - `src/index.ts` — wire up button click to `showCalendarModal()` ### Types - `Calendar`, `CalendarDates`, `GTFSTableMap` from `src/types/gtfs-entities.ts` - `queryRows` interface from `src/modules/gtfs-database.ts` ### Patterns to follow - `showModal()` in `src/modules/modal-utils.ts` — modal primitive; `onMount` callback to attach interactive DOM event listeners after insertion - `showFaresModal` / `showAboutModal` — examples of standalone modal functions wired via `index.ts` - Navbar button pattern: `id="calendar-btn"`, `class="btn btn-ghost btn-sm btn-square hidden md:flex"`, SVG icon - Service navigation: look at how `onServiceClick` (or equivalent) is wired in `index.ts` near `service-view-controller` usage ### Active-service computation (GTFS spec) For a given `service_id` and `date` (YYYYMMDD string): 1. Check `calendar_dates` for `exception_type=2` (removed) → inactive 2. Check `calendar_dates` for `exception_type=1` (added) → active 3. If no exception, check `calendar`: - If `date < start_date` or `date > end_date` → inactive - Look up the weekday (0=Sun … 6=Sat) in `calendar.{sunday,monday,…}` → 1=active, 0=inactive 4. If no `calendar` row and no exception → inactive ### Color palette Assign a stable color to each service by its index in the sorted list of service IDs. Use a fixed 10-color HSL palette cycling with enough contrast on both dark and light DaisyUI themes. ### Navigation callback `showCalendarModal` takes an `onServiceClick: (service_id: string) => void` callback. When a chip or timeline row is clicked the modal closes and the callback fires. --- ## Phase 1 — Month Grid tab **Goal:** Render a working month-grid view inside the modal. This is the primary view most users will interact with. Build the data loading, active-service computation, and month grid renderer. Add the navbar button and wire everything up. - [x] Create `src/modules/calendar-modal.ts` with: - `CalendarModalDeps` interface: `{ gtfsDatabase: { queryRows }, onServiceClick: (service_id: string) => void }` - `PALETTE: string[]` — 10 hex or hsl colors for service chips (readable on both dark/light backgrounds; use DaisyUI `badge` inline style approach) - `getServiceColor(index: number): string` — cycles through palette - `parseGTFSDate(s: string): Date` — parses YYYYMMDD to UTC midnight Date using `Date.UTC(year, month-1, day)` - `formatGTFS(date: Date): string` — Date → YYYYMMDD - `getDayOfWeek(gtfsDate: string): number` — 0=Sun…6=Sat, derived from YYYYMMDD without timezone issues (use UTC) - `isServiceActive(calendar: Record<string,unknown> | null, exceptions: Record<string,unknown>[], gtfsDate: string): boolean` — pure function implementing the 4-step logic above; use `Number(calendar[dayKey]) === 1` - `loadCalendarData(db)` — queries all `calendar` rows and all `calendar_dates` rows, returns `Map<service_id, { calendar, exceptions, color, label }>` where `label` is `service_id` truncated to 8 chars - `renderMonthGrid(data: ServiceDataMap, year: number, month: number): string` — returns full HTML for the calendar grid: - 7-column grid (Su Mo Tu We Th Fr Sa header row) - For each day in the month: day number + colored chips (`badge badge-xs` style, inline `background-color`) for each active service - Exception dates: if a `calendar_dates` entry exists for that day, add a small `+` (add, green tint on chip) or `−` (remove, red tint) suffix inside the chip label - Days outside the current month are empty/dimmed placeholder cells (greyed background) - Each chip has `data-service-id` and `class="cal-chip cursor-pointer"` for click handling - If more than 4 services are active on a day, show 4 chips + a `+N` overflow badge - `renderMonthNav(year: number, month: number): string` — `< [Month Year] >` navigation bar HTML with `data-action="prev-month"` / `data-action="next-month"` buttons - `showCalendarModal(deps: CalendarModalDeps): Promise<void>` — main entry point: - Calls `loadCalendarData` - Renders tab bar (Month Grid / Timeline) — Timeline tab is a placeholder ("Coming soon") for now - Renders initial body with month nav + grid for the current month - Calls `showModal({ title: 'Service Calendar', body, actions: [{label:'Close', onClick:()=>{}}], escapeAction: 0, boxClassName: 'max-w-5xl w-full', onMount })` - In `onMount`: - Attach click on `[data-action="prev-month"]` / `[data-action="next-month"]` to decrement/increment month state, re-render grid HTML in the grid container - Attach click on `.cal-chip` to read `data-service-id`, call `close()` then `deps.onServiceClick(service_id)` - Attach tab switching (Month Grid / Timeline tabs) - [x] Add `#calendar-btn` button to `src/index.html` navbar, between `#shapes-btn` and `#fares-btn`: - Use Heroicons `calendar-days` outline SVG (`h-5 w-5`) - Classes: `btn btn-ghost btn-sm btn-square hidden md:flex`, `title="Service Calendar"` - [x] Wire up in `src/index.ts`: - Import `showCalendarModal` from `./modules/calendar-modal` - Add click listener on `#calendar-btn` that calls `showCalendarModal({ gtfsDatabase: this.gtfsParser.gtfsDatabase, onServiceClick: (service_id) => { /* navigate to service */ } })` - For the `onServiceClick` callback: navigate to the service using the same mechanism as the browse panel (inspect how service_id navigation works near `pageStateManager` or `navigationActions` in `index.ts`) **Gotchas / invariants:** - YYYYMMDD arithmetic: **never** use `new Date('YYYYMMDD')` (browser-inconsistent timezone). Always use `Date.UTC(year, month-1, day)`. - Month grid: JS months are 0-indexed in `Date.UTC`. Keep variables named clearly (`month0` for 0-indexed, `month1` for 1-indexed). - Chips: if many services are active, chips wrap. Cap at 4 + `+N` overflow indicator. - `isServiceActive` weekday keys: `['sunday','monday','tuesday','wednesday','thursday','friday','saturday']` — index by `getDayOfWeek()` result. --- ## Phase 2 — Timeline / Gantt tab **Goal:** Implement the Timeline tab with week-level X-axis resolution. One row per service, horizontal bar from `start_date` to `end_date`, small ticks for exceptions. - [x] In `calendar-modal.ts`, add `renderTimeline(data: ServiceDataMap): string`: - Compute `minDate` / `maxDate` across all services (use both `calendar` date ranges and `calendar_dates` entries for services with no `calendar` row) - Snap `minDate` back to the nearest Sunday so columns are full weeks - Build a `weeks: string[]` array of YYYYMMDD week-start dates from snapped `minDate` to `maxDate` - Render as a scrollable grid: - Left column (fixed, ~120px): colored service chip + full `service_id` - Header row: abbreviated month label spanning the correct number of week columns (compute `colspan` per month) - For each service row: one cell per week - Cell background color: service color at ~20% opacity if this week overlaps the service's active range and the weekly pattern has at least one active day that week; else `bg-base-100` - Exception ticks: for each `calendar_dates` entry in this week, render a small `▲` (type=1, green) or `▼` (type=2, red) indicator - Each row has `data-service-id` and `class="timeline-row cursor-pointer"` for click handling - Wrap the grid in `<div class="overflow-x-auto">` for horizontal scroll - Week column width: `w-5` (20px). A 1-year feed = 52 × 20px = 1040px — fits in `max-w-5xl` with the service label column. - [x] Replace the "Coming soon" placeholder in the Timeline tab with `renderTimeline(data)` - [x] In `onMount`: attach click on `.timeline-row` elements (same `onServiceClick` close-and-navigate pattern as chips) - [x] Handle services with only `calendar_dates` (no `calendar` row): derive min/max dates from exceptions; show no bar but show the exception ticks **Gotchas / invariants:** - Cap range at 3 years max. If `maxDate - minDate > 3 years`, warn with a note and truncate to 3 years from `minDate`. - "Active week" heuristic: a week is colored active if `calendar.start_date ≤ weekEnd` AND `calendar.end_date ≥ weekStart` AND any weekday in `calendar` is 1. This avoids computing all 7 days per week per service. - Services with no data at all (no calendar, no exceptions): skip silently — they shouldn't appear in `loadCalendarData`'s output anyway. --- ## Phase 3 — Month Grid polish **Goal:** Three targeted improvements to the Month Grid: scrollable overflow instead of the +N cap, full service IDs with clear visual truncation, and feed-level start/end date markers pulled from `feed_info.txt`. - [x] **Scrollable day cells**: Remove the 4-chip cap and overflow badge logic entirely. Change the chips container inside each day cell to `class="flex flex-col gap-0.5 max-h-24 overflow-y-auto"`. Keep `min-h-16` on the outer cell. This lets every service show without an arbitrary cut-off, and the cell scrolls if tall. - [x] **Full service IDs with clear truncation**: In `loadCalendarData`, change `label: sid.length > 8 ? sid.slice(0, 8) : sid` to `label: sid`. The chip already has `truncate max-w-full` CSS so it will show ellipsis when the text overflows, which signals truncation to the user. The existing `title="${esc(sid)}"` attribute shows the full ID on hover. No other changes needed. - [x] **Feed start/end markers**: - In `showCalendarModal`, after `loadCalendarData`, also call `deps.gtfsDatabase.getAllRows('feed_info')` and extract `feed_start_date` / `feed_end_date` from the first row (if any). Store as `feedStartDate: string | null` and `feedEndDate: string | null`. - Update the `CalendarModalDeps` interface's `gtfsDatabase` type to include `getAllRows` (it currently only declares it; confirm or add if missing). - Pass `{ feedStartDate, feedEndDate }` to `renderMonthGrid` and to `rerenderGrid` (update its signature). - Inside `renderMonthGrid`, for each day cell, after the day number `<span>`, conditionally append small inline badges: - If `gtfsDate === feedStartDate`: `<span class="badge badge-xs badge-success ml-1" title="Feed start date">▶</span>` - If `gtfsDate === feedEndDate`: `<span class="badge badge-xs badge-error ml-1" title="Feed end date">◀</span>` - The day number row becomes `<div class="text-xs text-base-content/60 mb-0.5 flex items-center gap-0.5">` to hold the number and any badges inline. - If `feed_info` table is empty or missing `feed_start_date`/`feed_end_date`, skip silently. **Gotchas:** - `getAllRows` is already on the `gtfsDatabase` interface (check the existing usage in `loadCalendarData` — it uses `getAllRows`, not `queryRows`). - The feed date badges must not break layout when both appear on the same day (unlikely but possible if feed is 1 day). - `rerenderGrid` in `onMount` captures `year` and `month1` by closure; just pass the feed dates through the closure too. --- ## Phase 4 — Timeline polish **Goal:** Three improvements to the Timeline tab: auto-fit the service label column to the longest ID so nothing is needlessly truncated; add a weekday-pattern dot column so you can see at a glance which days each service runs; replace browser `title` tooltips on exception ticks with DaisyUI tooltips showing a human-friendly date. - [x] **Fit label column to longest ID**: - Before building `rowsHtml`, compute `const maxIdLen = Math.max(0, ...[...data.keys()].map((k) => k.length))`. - Derive a pixel width: `const labelColPx = Math.max(80, maxIdLen * 7 + 32)` (7px per char at `text-xs` + 32px padding). Cap at a reasonable max like 300px. - Replace all occurrences of the fixed `w-36 min-w-36 max-w-36` (and the `w-36 min-w-36` in the `<th>`) with `style="width:${labelColPx}px;min-width:${labelColPx}px;max-width:${labelColPx}px"`. - The `truncate` on the inner `<span>` still applies so very long IDs degrade gracefully. - [x] **Weekday dot column**: - Define a helper `renderWeekdayDots(calendar: Record<string, unknown> | null): string` that returns 7 dot characters for Mon–Sun order (`['monday','tuesday','wednesday','thursday','friday','saturday','sunday']`). For each day: `●` (U+25CF) if `Number(calendar[key]) === 1`, else `○` (U+25CB). If `calendar` is null, return 7 `○` dots. Wrap in `<span class="font-mono tracking-tight text-base-content/70">`. - Add a second sticky `<th>` in the header row (after the label `<th>`, before the month spans): width ~56px (`w-14`), label `<span class="text-xs">M T W T F S S</span>` with `tracking-tight font-mono text-base-content/50`. - In each `<tr>`, insert a `<td>` after the label `<td>` containing the dot string for that service's calendar. - This `<td>` is NOT sticky (doesn't need to scroll with the week columns). Give it `class="w-14 min-w-14 px-1 py-1 border-b border-base-300/30 text-xs"`. - [x] **DaisyUI tooltips on exception ticks**: - Add a helper `formatHumanDate(gtfsDate: string): string` that parses the YYYYMMDD string and returns a string like `"Mon, Jan 15 2026"` using `Date.UTC` + `Date.prototype.toUTCString` or manual formatting with `MONTH_ABBR` and a day-of-week array. - In the exception tick rendering loop, change: ```ts `<span style="color:#4ade80" title="${dateStr}">▲</span>` ``` to: ```ts `<span class="tooltip tooltip-top" data-tip="${formatHumanDate(dateStr)}"><span style="color:#4ade80">▲</span></span>` ``` Same for the `▼` (type 2) tick. - DaisyUI `tooltip` works purely via CSS (`[data-tip]` pseudo-element), so no JS needed. **Gotchas:** - The weekday `<th>` header labels "M T W T F S S" are Mon–Sun, which matches the dot order. Include this as a comment in code if the order differs from `WEEKDAY_KEYS` (which is Sun-first). - `tooltip-top` ensures the tooltip opens upward; the timeline table scrolls horizontally so `tooltip-right`/`tooltip-left` could get clipped. - `formatHumanDate` should use UTC methods consistently (no local-time leakage). A clean approach: `const d = parseGTFSDate(gtfsDate); const dayName = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getUTCDay()]; return \`${dayName}, ${MONTH_ABBR[d.getUTCMonth()]} ${d.getUTCDate()} ${d.getUTCFullYear()}\`;` --- ## Original Issue Make a calendar modal view to see how far ahead service has been scheduled and when there is holiday service and how they overlap.
maxtkc self-assigned this 2026-05-11 21:01:47 +00:00
Author
Owner

One last bug: overlapping cards on the Month Grid

One last bug: overlapping cards on the Month Grid
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gtfs.zone/coloring-book#123
No description provided.