mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
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:
@@ -687,6 +687,21 @@ export interface PluginDayScheduleItem {
|
||||
tone: 'default' | 'success' | 'warn' | 'danger';
|
||||
}
|
||||
|
||||
/** The colours a dayTintProvider plugin puts into one day card, so leg membership is
|
||||
* visible while scrolling the itinerary. The card has three separately tintable
|
||||
* regions; an absent one is not tinted and renders exactly as it does with no plugin.
|
||||
* Server-normalized: dayIds checked against the trip, one contribution per day (first
|
||||
* granted provider wins), the `tone` shorthand already resolved into the regions,
|
||||
* labels sanitized + capped. Tone only — the client picks the alpha per theme. */
|
||||
export type PluginDayTintTone = 'default' | 'success' | 'warn' | 'danger'
|
||||
export interface PluginDayTint {
|
||||
pluginId: string; dayId: number;
|
||||
badgeTone?: PluginDayTintTone;
|
||||
headerTone?: PluginDayTintTone;
|
||||
activityTone?: PluginDayTintTone;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** A route computed by a routeProvider plugin (server-normalized: coordinates
|
||||
* range-checked, legs forced to waypoints-1, vias capped). null = provider failed
|
||||
* or refused — the caller falls back to straight lines like on an OSRM outage. */
|
||||
@@ -754,6 +769,10 @@ export const pluginsApi = {
|
||||
// hook (charging stops, security buffers). Host-normalized; fail-safe.
|
||||
daySchedule: (tripId: number | string) =>
|
||||
apiClient.get(`/day-schedule/${tripId}`).then(r => r.data as { items: PluginDayScheduleItem[] }),
|
||||
// Per-day colours plugins put behind the day cards via the dayTintProvider hook
|
||||
// (which leg of the trip a day belongs to). Host-normalized; fail-safe.
|
||||
dayTints: (tripId: number | string) =>
|
||||
apiClient.get(`/day-tints/${tripId}`).then(r => r.data as { tints: PluginDayTint[] }),
|
||||
// Text-only sections plugins append to the trip PDF export via the
|
||||
// pdfSectionProvider hook. Host-normalized (counts + lengths capped); fail-safe.
|
||||
pdfSections: (tripId: number | string) =>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ArrowUpCircle, Github, ExternalLink, ChevronDown, Check, Lock, Search, Link2, KeyRound, ShieldAlert,
|
||||
SlidersHorizontal, ArrowUpDown, CircleDot, MoreHorizontal, RotateCw, ArrowRight, Database, Users, LayoutDashboard,
|
||||
Radio, Luggage, Globe, Image, CalendarDays, Bell,
|
||||
Wallet, Puzzle, MapPin, ListChecks, Pencil, Tag, FileText, Route, Navigation, Clock, LocateFixed,
|
||||
Wallet, Puzzle, MapPin, ListChecks, Pencil, Tag, FileText, Route, Navigation, Clock, LocateFixed, Palette,
|
||||
} from 'lucide-react'
|
||||
import PluginIcon from '../shared/PluginIcon'
|
||||
import { adminApi } from '../../api/client'
|
||||
@@ -192,7 +192,7 @@ const PERM_KEYS = [
|
||||
'events:subscribe', 'jobs:run',
|
||||
'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', 'geolocation:read',
|
||||
'hook:map-layer-provider', 'hook:route-provider', 'hook:day-schedule-provider', 'hook:day-tint-provider', 'geolocation:read',
|
||||
'hook:pdf-section-provider', 'hook:atlas-layer-provider', 'hook:journal-entry-provider', 'hook:trip-card-provider', 'hook:notification-channel', 'hook:user-data', 'http:outbound',
|
||||
]
|
||||
|
||||
@@ -247,6 +247,7 @@ function deriveCaps(perms: string[], caps: { widget?: { slot?: string }; tripPag
|
||||
if (perms.includes('hook:map-layer-provider')) out.push({ icon: Route, label: t('admin.plugins.cap.mapLayers') })
|
||||
if (perms.includes('hook:route-provider')) out.push({ icon: Navigation, label: t('admin.plugins.cap.routing') })
|
||||
if (perms.includes('hook:day-schedule-provider')) out.push({ icon: Clock, label: t('admin.plugins.cap.daySchedule') })
|
||||
if (perms.includes('hook:day-tint-provider')) out.push({ icon: Palette, label: t('admin.plugins.cap.dayTint') })
|
||||
if (perms.includes('geolocation:read')) out.push({ icon: LocateFixed, label: t('admin.plugins.cap.geolocation') })
|
||||
if (perms.includes('hook:notification-channel')) out.push({ icon: Bell, label: t('admin.plugins.cap.notificationChannel') })
|
||||
if (perms.includes('events:subscribe')) out.push({ icon: Radio, label: t('admin.plugins.cap.events') })
|
||||
|
||||
@@ -34,7 +34,7 @@ import { useDayNotes } from '../../hooks/useDayNotes'
|
||||
import { useExchangeRates } from '../../hooks/useExchangeRates'
|
||||
import { RES_ICONS, getNoteIcon } from './DayPlanSidebar.constants'
|
||||
import { RouteConnector, HotelRouteConnector } from './DayPlanSidebarRouteConnector'
|
||||
import { usePluginDaySchedule, PluginDayScheduleRow, formatScheduleMinutes } from '../Plugins/PluginDaySchedule'
|
||||
import { usePluginDaySchedule, usePluginDayTints, dayTintBackground, PluginDayScheduleRow, formatScheduleMinutes } from '../Plugins/PluginDaySchedule'
|
||||
import { MobileAddPlaceButton } from './DayPlanSidebarMobileAddPlaceButton'
|
||||
import { DayPlanSidebarToolbar } from './DayPlanSidebarToolbar'
|
||||
import { DayPlanSidebarNoteModal } from './DayPlanSidebarNoteModal'
|
||||
@@ -1051,6 +1051,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
const activePlugins = usePluginStore(s => s.plugins)
|
||||
// Plugin time contributions in the day plan (dayScheduleProvider hook).
|
||||
const daySchedule = usePluginDaySchedule(S.tripId)
|
||||
// Per-day colours from the dayTintProvider hook — e.g. which leg of the trip a
|
||||
// day belongs to. Empty unless a granted plugin provides them.
|
||||
const dayTints = usePluginDayTints(S.tripId)
|
||||
const routeProfileOptions = useMemo(() => {
|
||||
const opts: Array<{ key: string; label: string }> = [
|
||||
{ key: 'driving', label: 'Driving' },
|
||||
@@ -1317,9 +1320,17 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
const merged = mergedItemsMap[day.id] || []
|
||||
const dayNoteUi = noteUi[day.id]
|
||||
const placeItems = merged.filter(i => i.type === 'place')
|
||||
const dayTint = dayTints[day.id]
|
||||
// Resolved once per day: the header owns a background that its hover
|
||||
// handlers reassign imperatively, so both the base and the hover value have
|
||||
// to be tint-aware or the first hover-out would wipe the colour.
|
||||
const headerTintBg = dayTintBackground(dayTint?.headerTone, '--day-tint-header') ?? 'transparent'
|
||||
const headerTintHoverBg = dayTintBackground(dayTint?.headerTone, '--day-tint-header-hover') ?? 'var(--bg-tertiary)'
|
||||
|
||||
return (
|
||||
<div key={day.id} style={{ borderBottom: '1px solid var(--border-faint)' }}>
|
||||
// The card wrapper stays untinted — its three regions (badge, header,
|
||||
// activity list) paint themselves, so a plugin controls them separately.
|
||||
<div key={day.id} title={dayTint?.label || undefined} style={{ borderBottom: '1px solid var(--border-faint)' }}>
|
||||
{/* Tages-Header — akzeptiert Drops aus der PlacesSidebar */}
|
||||
<div
|
||||
className="dp-day-header"
|
||||
@@ -1332,7 +1343,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '11px 14px 11px 16px',
|
||||
cursor: 'pointer',
|
||||
background: isDragTarget ? 'rgba(17,24,39,0.07)' : (isSelected ? 'var(--bg-selected)' : 'transparent'),
|
||||
background: isDragTarget ? 'rgba(17,24,39,0.07)' : (isSelected ? 'var(--bg-selected)' : headerTintBg),
|
||||
transition: 'background 0.12s',
|
||||
userSelect: 'none',
|
||||
outline: isDragTarget ? '2px dashed rgba(17,24,39,0.25)' : 'none',
|
||||
@@ -1340,8 +1351,12 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
borderRadius: isDragTarget ? 8 : 0,
|
||||
touchAction: 'manipulation',
|
||||
}}
|
||||
onMouseEnter={e => { if (!isSelected && !isDragTarget) e.currentTarget.style.background = 'var(--bg-tertiary)' }}
|
||||
onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = isDragTarget ? 'rgba(17,24,39,0.07)' : 'transparent' }}
|
||||
// A tinted header hovers to a deeper mix of its own tone rather than to
|
||||
// the neutral --bg-tertiary, so hovering reads as a state change on
|
||||
// this day instead of momentarily losing its colour. Selection keeps
|
||||
// winning outright, so a selected day is never ambiguous.
|
||||
onMouseEnter={e => { if (!isSelected && !isDragTarget) e.currentTarget.style.background = headerTintHoverBg }}
|
||||
onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = isDragTarget ? 'rgba(17,24,39,0.07)' : headerTintBg }}
|
||||
>
|
||||
{/* Tages-Badge: Nummer oben, darunter (falls vorhanden) das Wetter des Tages */}
|
||||
{(() => {
|
||||
@@ -1356,8 +1371,12 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
flexShrink: 0, alignSelf: 'flex-start',
|
||||
width: hasWeather ? 34 : 26,
|
||||
borderRadius: hasWeather ? 11 : '50%',
|
||||
background: isSelected ? 'var(--accent)' : 'var(--bg-hover)',
|
||||
color: isSelected ? 'var(--accent-text)' : 'var(--text-muted)',
|
||||
// Selection still wins. A tinted badge mixes the tone INTO
|
||||
// --bg-hover so it stays the same pill component, and takes
|
||||
// --text-secondary: the number is 11px bold, so 4.5:1 applies
|
||||
// and --text-muted is already borderline on the untinted pill.
|
||||
background: isSelected ? 'var(--accent)' : (dayTintBackground(dayTint?.badgeTone, '--day-tint-badge', 'var(--bg-hover)') ?? 'var(--bg-hover)'),
|
||||
color: isSelected ? 'var(--accent-text)' : (dayTint?.badgeTone ? 'var(--text-secondary)' : 'var(--text-muted)'),
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{ width: '100%', height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 700 }}>
|
||||
@@ -1479,7 +1498,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
{/* Aufgeklappte Orte + Notizen */}
|
||||
{isExpanded && (
|
||||
<div
|
||||
style={{ background: 'var(--bg-hover)', paddingTop: 6 }}
|
||||
// The activity list — the largest region and the one behind the
|
||||
// densest text, so its tint is the faintest of the three.
|
||||
style={{ background: dayTintBackground(dayTint?.activityTone, '--day-tint-activity', 'var(--bg-hover)') ?? 'var(--bg-hover)', paddingTop: 6 }}
|
||||
onDragOver={e => { e.preventDefault(); const cur = dropTargetRef.current; if (draggingId && (!cur || cur.startsWith('end-'))) setDropTargetKey(`end-${day.id}`) }}
|
||||
onDrop={e => {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Zap } from 'lucide-react'
|
||||
import { pluginsApi, type PluginDayScheduleItem } from '../../api/client'
|
||||
import { pluginsApi, type PluginDayScheduleItem, type PluginDayTint, type PluginDayTintTone } from '../../api/client'
|
||||
|
||||
/**
|
||||
* Host-rendered rows for the `dayScheduleProvider` plugin hook — time
|
||||
@@ -62,6 +62,60 @@ export function usePluginDaySchedule(tripId?: number | string | null): PluginDay
|
||||
}, [items])
|
||||
}
|
||||
|
||||
/** dayId → the per-region tones (and optional tooltip) for that day's card. */
|
||||
export type PluginDayTints = Record<number, Omit<PluginDayTint, 'pluginId' | 'dayId'>>
|
||||
|
||||
const EMPTY_TINTS: PluginDayTints = {}
|
||||
|
||||
/**
|
||||
* Host-rendered day colours for the `dayTintProvider` plugin hook — "day 12 belongs
|
||||
* to the Kanazawa leg". A sibling of usePluginDaySchedule in every respect: same
|
||||
* fail-safe fetch, same vetted-data-only contract. Separate from the schedule hook
|
||||
* because its output is bounded by the trip's day count rather than the schedule
|
||||
* hook's ≤60 items, which a long multi-destination trip would blow straight past.
|
||||
*
|
||||
* The server has already resolved precedence (one tint per day, first granted
|
||||
* provider wins), so this is a flat index — the callers just look a day up.
|
||||
*/
|
||||
export function usePluginDayTints(tripId?: number | string | null): PluginDayTints {
|
||||
const [tints, setTints] = useState<PluginDayTint[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (tripId == null) { setTints([]); return }
|
||||
let alive = true
|
||||
pluginsApi.dayTints(tripId)
|
||||
.then(r => { if (alive) setTints(r.tints || []) })
|
||||
.catch(() => { if (alive) setTints([]) }) // fail-safe: no tints
|
||||
return () => { alive = false }
|
||||
}, [tripId])
|
||||
|
||||
return useMemo(() => {
|
||||
if (tints.length === 0) return EMPTY_TINTS
|
||||
const out: PluginDayTints = {}
|
||||
for (const t of tints) {
|
||||
const { pluginId: _pluginId, dayId: _dayId, ...regions } = t
|
||||
out[t.dayId] = regions
|
||||
}
|
||||
return out
|
||||
}, [tints])
|
||||
}
|
||||
|
||||
/** The `color-mix` background for one tinted region, or undefined when that region has
|
||||
* no tone — in which case the caller keeps whatever it renders without plugins.
|
||||
*
|
||||
* `alphaVar` names a CSS custom property holding the strength, which varies per theme
|
||||
* AND per region: the tones are fixed hexes, so one alpha cannot serve both themes
|
||||
* (#4F46E5 is itself a dark colour and vanishes on a dark surface at a light theme's
|
||||
* alpha), and a large region behind dense text needs a fainter tint than a small badge. */
|
||||
export function dayTintBackground(
|
||||
tone: PluginDayTintTone | undefined,
|
||||
alphaVar: string,
|
||||
base = 'transparent',
|
||||
): string | undefined {
|
||||
if (!tone) return undefined
|
||||
return `color-mix(in srgb, ${TONE_COLORS[tone] ?? TONE_COLORS.default} var(${alphaVar}), ${base})`
|
||||
}
|
||||
|
||||
export function formatScheduleMinutes(minutes: number): string {
|
||||
const h = Math.floor(minutes / 60)
|
||||
const m = minutes % 60
|
||||
|
||||
@@ -529,6 +529,18 @@ input[type="number"], input[type="time"], input[type="date"], input[type="dateti
|
||||
--accent-hover: #1f2937; /* hover state of an accent fill */
|
||||
--accent-subtle: #f1f5f9; /* faint accent-tinted surface (chips, selected) */
|
||||
|
||||
/* Strength of a dayTintProvider plugin's colour in each region of a day card.
|
||||
Per-theme because the tones are fixed hexes: the default tone #4F46E5 is itself a
|
||||
dark colour, so a light theme's alpha makes it vanish against the dark sidebar,
|
||||
while warn #f59e0b at the dark alpha would outshine the selected day. Per-region
|
||||
because area and text density differ — the badge is a small bold pill, the
|
||||
activity list is a large surface behind dense rows. Tuned so every region reads
|
||||
above the hover step and below the selected one. */
|
||||
--day-tint-badge: 16%;
|
||||
--day-tint-header: 8%;
|
||||
--day-tint-header-hover: 14%;
|
||||
--day-tint-activity: 6%;
|
||||
|
||||
--fs-scale-title: 1;
|
||||
--fs-scale-subtitle: 1;
|
||||
--fs-scale-body: 1;
|
||||
@@ -620,6 +632,11 @@ input[type="number"], input[type="time"], input[type="date"], input[type="dateti
|
||||
--accent-hover: #d4d4d8;
|
||||
--accent-subtle: rgba(255, 255, 255, 0.08);
|
||||
|
||||
--day-tint-badge: 24%;
|
||||
--day-tint-header: 13%;
|
||||
--day-tint-header-hover: 20%;
|
||||
--day-tint-activity: 10%;
|
||||
|
||||
--success: #22c55e; --success-soft: rgba(34, 197, 94, 0.15);
|
||||
--danger: #ef4444; --danger-soft: rgba(239, 68, 68, 0.15);
|
||||
--warning: #f59e0b; --warning-soft: rgba(245, 158, 11, 0.15);
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
--m-mkfg: #fff;
|
||||
--m-trackoff: rgba(16,16,19,.14);
|
||||
|
||||
/* Strength of a dayTintProvider plugin's colour on an inactive day chip. A notch
|
||||
* above the desktop badge: the chip has no neutral base under it, so a softer tint
|
||||
* reads as a rendering artifact rather than a deliberate colour. */
|
||||
--day-tint-chip: 20%;
|
||||
|
||||
/* Toggle knob is white on both themes (off-track is translucent enough). */
|
||||
--m-knob: #fff;
|
||||
|
||||
@@ -78,6 +83,7 @@
|
||||
--m-mkr: #F5F5F7;
|
||||
--m-mkfg: #101013;
|
||||
--m-trackoff: rgba(255,255,255,.14);
|
||||
--day-tint-chip: 28%;
|
||||
}
|
||||
|
||||
/* Geist "subtext" tier weight — single knob. The design's Geist labels/numbers
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ArrowUpCircle, Github, ExternalLink, ChevronDown, Check, Lock, Search, Link2, KeyRound, ShieldAlert,
|
||||
SlidersHorizontal, ArrowUpDown, CircleDot, MoreHorizontal, RotateCw, ArrowRight, Database, Users, LayoutDashboard,
|
||||
Radio, Luggage, Globe, Image, CalendarDays, Bell,
|
||||
Wallet, Puzzle, MapPin, ListChecks, Pencil, Tag, FileText, Route, Navigation, Clock, LocateFixed,
|
||||
Wallet, Puzzle, MapPin, ListChecks, Pencil, Tag, FileText, Route, Navigation, Clock, LocateFixed, Palette,
|
||||
} from 'lucide-react'
|
||||
import PluginIcon from '../../../components/shared/PluginIcon'
|
||||
import { adminApi } from '../../../api/client'
|
||||
@@ -192,7 +192,7 @@ const PERM_KEYS = [
|
||||
'events:subscribe', 'jobs:run',
|
||||
'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', 'geolocation:read',
|
||||
'hook:map-layer-provider', 'hook:route-provider', 'hook:day-schedule-provider', 'hook:day-tint-provider', 'geolocation:read',
|
||||
'hook:pdf-section-provider', 'hook:atlas-layer-provider', 'hook:journal-entry-provider', 'hook:trip-card-provider', 'hook:notification-channel', 'hook:user-data', 'http:outbound',
|
||||
]
|
||||
|
||||
@@ -247,6 +247,7 @@ function deriveCaps(perms: string[], caps: { widget?: { slot?: string }; tripPag
|
||||
if (perms.includes('hook:map-layer-provider')) out.push({ icon: Route, label: t('admin.plugins.cap.mapLayers') })
|
||||
if (perms.includes('hook:route-provider')) out.push({ icon: Navigation, label: t('admin.plugins.cap.routing') })
|
||||
if (perms.includes('hook:day-schedule-provider')) out.push({ icon: Clock, label: t('admin.plugins.cap.daySchedule') })
|
||||
if (perms.includes('hook:day-tint-provider')) out.push({ icon: Palette, label: t('admin.plugins.cap.dayTint') })
|
||||
if (perms.includes('geolocation:read')) out.push({ icon: LocateFixed, label: t('admin.plugins.cap.geolocation') })
|
||||
if (perms.includes('hook:notification-channel')) out.push({ icon: Bell, label: t('admin.plugins.cap.notificationChannel') })
|
||||
if (perms.includes('events:subscribe')) out.push({ icon: Radio, label: t('admin.plugins.cap.events') })
|
||||
|
||||
@@ -12,6 +12,7 @@ import MPlacesBrowser from './places/MPlacesBrowser'
|
||||
import MTripTabPanel from './tabs/MTripTabPanel'
|
||||
import MTripSheets from './sheets/MTripSheets'
|
||||
import MTripLoadingSplash from './MTripLoadingSplash'
|
||||
import { usePluginDayTints, dayTintBackground } from '../../../components/Plugins/PluginDaySchedule'
|
||||
import type { Day } from '../../../types'
|
||||
|
||||
/**
|
||||
@@ -176,6 +177,10 @@ export default function MTripShell({
|
||||
const planner = useTripPlanner()
|
||||
const { t, language, tripId, days, trip, navigate, packingItems, todoItems } = planner
|
||||
|
||||
// Per-day colours from the dayTintProvider plugin hook — the mobile counterpart
|
||||
// of the desktop day-card wash, carried on the day chips. Empty without a plugin.
|
||||
const dayTints = usePluginDayTints(tripId)
|
||||
|
||||
const [view, setView] = useState<MTripView>('plan')
|
||||
const [mode, setMode] = useState<MTripMode>('go')
|
||||
const [browseFromEdit, setBrowseFromEdit] = useState(false)
|
||||
@@ -300,12 +305,18 @@ export default function MTripShell({
|
||||
<div className="flex flex-1 items-center gap-[2px] overflow-x-auto rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] p-[3px] backdrop-blur-[24px] backdrop-saturate-[1.7]">
|
||||
{days.map((day, idx) => {
|
||||
const active = day.id === planner.selectedDayId
|
||||
const tint = dayTints[day.id]
|
||||
return (
|
||||
<button
|
||||
key={day.id}
|
||||
type="button"
|
||||
onClick={() => onDayChipTap(day.id)}
|
||||
aria-current={active ? 'true' : undefined}
|
||||
title={tint?.label || undefined}
|
||||
// The chip is mobile's day-number badge, so it follows badgeTone.
|
||||
// Inactive chips only — an inline background would otherwise beat
|
||||
// the active chip's bg-m-act class.
|
||||
style={active ? undefined : { background: dayTintBackground(tint?.badgeTone, '--day-tint-chip') }}
|
||||
className={`flex-1 whitespace-nowrap rounded-full px-3 py-[5px] text-center text-[0.75rem] font-semibold ${
|
||||
active ? 'bg-m-act text-m-actfg shadow-[0_6px_16px_-6px_rgba(0,0,0,.4)]' : 'text-m-ink'
|
||||
}`}
|
||||
|
||||
Reference in New Issue
Block a user