Add a dayTintProvider plugin hook

Lets a plugin colour-code days in the Plan sidebar. trip-segments can
already announce "Kanazawa begins" in the day plan, but it can't make
day 12 look like it belongs to Kanazawa.

dayScheduleProvider can't cover this: it renders a fixed row, and it's
capped at 60 items per provider, so one item per day would tint the
first 60 days of a six-month trip and silently stop. This hook is
bounded by day count instead.

GET /api/day-tints/:tripId returns per-day tones for three regions of a
day card - the number badge (also the mobile day chip), the header row,
and the expanded activity list. `tone` is a shorthand filling every
region a contribution doesn't name; unnamed regions render as they do
today. Tones only, never hex, so the host can pick the alpha per theme
and per region.

One contribution per day, resolved whole: first entry wins within a
provider, first granted provider wins across plugins.
This commit is contained in:
Renzo Beux
2026-07-29 12:05:31 -03:00
committed by Maurice
parent 28ffc814c0
commit c89d2bf3a1
46 changed files with 664 additions and 17 deletions
+1
View File
@@ -218,6 +218,7 @@ export const PERMISSION_FAMILIES: PermissionFamily[] = [
{ value: 'hook:map-layer-provider', hint: 'Draw routes, corridors and zones on the trip map' },
{ value: 'hook:route-provider', hint: 'Offer routing profiles the planner can route days with (e.g. EV with charging stops)' },
{ value: 'hook:day-schedule-provider', hint: 'Attach time contributions to the day plan (charging, security, buffers)' },
{ value: 'hook:day-tint-provider', hint: 'Colour-code days in the day plan (e.g. which leg of the trip each day belongs to)' },
{ value: 'hook:pdf-section-provider', hint: 'Add a section to the exported trip PDF' },
{ value: 'hook:atlas-layer-provider', hint: 'Add a layer to the Atlas map' },
{ value: 'hook:journal-entry-provider', hint: 'Contribute entries to a journey' },
+37
View File
@@ -585,6 +585,42 @@ export interface DayScheduleProvider {
getSchedule(tripId: number, ctx: PluginContext): Promise<DayScheduleContribution[]>;
}
/** A colour the host paints into one day card in the Plan sidebar (and into that day's
* mobile chip) — so a trip split into legs shows its leg membership while you scroll
* the itinerary. Tone only, never a hex: the host picks the alpha per theme AND per
* region, so the tint stays subtle and readable in both light and dark.
*
* The card has three separately tintable regions. Set `tone` to colour all of them,
* or name regions individually for finer control — e.g. a bold badge marking the leg
* with the activity list left plain, so a dense day stays easy to read. */
export interface DayTintContribution {
dayId: number; // must be a day of the requested trip
/** Shorthand: tints every region you do NOT name below. Omit it and only the
* regions you name are tinted. */
tone?: ContributionTone;
/** The day-number badge — the smallest, boldest mark. Also tints the mobile day chip. */
badgeTone?: ContributionTone;
/** The day header row (number, title, date, cost). Its hover state deepens the tint. */
headerTone?: ContributionTone;
/** The expanded activity list — places, bookings and transport for the day. The
* largest surface and the one behind the densest text, so it is tinted faintest. */
activityTone?: ContributionTone;
label?: string; // optional tooltip on the day (≤60 chars)
}
export interface DayTintProvider {
/** Return one tint per day you want coloured — the natural cap is the trip's day
* count, so this scales to a six-month itinerary where a per-day dayScheduleProvider
* item would hit the ≤60 limit. Runs with the current user bound, on a short
* timeout; a failing call is skipped.
*
* A day takes at most one contribution — it is resolved whole, per day, never
* per region. Within your own list the first entry for a day wins; across plugins
* the first granted provider wins. Both are deterministic, so a day never flickers
* between two colours, and two plugins can never each own part of one card.
* Needs `hook:day-tint-provider`. */
getDayTints(tripId: number, ctx: PluginContext): Promise<DayTintContribution[]>;
}
/** A text-only section the host appends to a trip's PDF export. Declarative only —
* plain strings the host lays out and escapes; no markup ever reaches the document. */
export interface PdfSection {
@@ -703,6 +739,7 @@ export interface PluginDefinition {
mapLayerProvider?: MapLayerProvider;
routeProvider?: RouteProvider;
dayScheduleProvider?: DayScheduleProvider;
dayTintProvider?: DayTintProvider;
pdfSectionProvider?: PdfSectionProvider;
atlasLayerProvider?: AtlasLayerProvider;
journalEntryProvider?: JournalEntryProvider;
+1 -1
View File
@@ -89,7 +89,7 @@ export const KNOWN_PERMISSIONS = [
'db:meta',
'ws:broadcast:trip', 'ws:broadcast:user',
'hook:photo-provider', 'hook:calendar-source', 'hook:place-detail-provider', 'hook:trip-warning-provider',
'hook:table-contributor', 'hook:map-marker-provider', 'hook:map-layer-provider', 'hook:route-provider', 'hook:day-schedule-provider', 'hook:pdf-section-provider', 'hook:atlas-layer-provider',
'hook:table-contributor', 'hook:map-marker-provider', 'hook:map-layer-provider', 'hook:route-provider', 'hook:day-schedule-provider', 'hook:day-tint-provider', 'hook:pdf-section-provider', 'hook:atlas-layer-provider',
'hook:journal-entry-provider', 'hook:trip-card-provider', 'hook:notification-channel', 'hook:user-data',
'events:subscribe', 'jobs:run', 'http:outbound',
'weather:read', 'rates:read', 'notify:send', 'ai:invoke', 'oauth:client',
+1
View File
@@ -28,6 +28,7 @@ export const HOOK_PERMISSION: Readonly<Record<string, string>> = {
mapLayerProvider: 'hook:map-layer-provider',
routeProvider: 'hook:route-provider',
dayScheduleProvider: 'hook:day-schedule-provider',
dayTintProvider: 'hook:day-tint-provider',
pdfSectionProvider: 'hook:pdf-section-provider',
atlasLayerProvider: 'hook:atlas-layer-provider',
journalEntryProvider: 'hook:journal-entry-provider',