mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
feat(plugins): hero widget slot + Koffi reference plugin
Widget plugins can now declare capabilities.widget.slot 'hero' to render as a transparent, click-through overlay sitting on the boarding-pass bar's top edge (migration 157 persists capabilities; manifest validation server+SDK, feed exposes the slot, dashboard mounts hero frames above the pass). Sidebar stays the default slot. Replaces the trip-countdown example with Koffi, the TREK mascot: an animated suitcase with a 14-state behavior engine driven by real trip data — walking, waving, napping, trolley rolls, passport-stamp stickers, a split-flap luggage- tag countdown under 7 days, and sunglasses while the trip runs. Validated by the SDK suite like any author plugin; published as mauriceboe/trek-plugin-koffi in the registry. Migrations 156/157 follow the idempotent ALTER pattern (the reconciliation test re-runs everything from v135, so duplicate-column must stay non-fatal).
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
import { IcsSubscribeModal } from '../components/Planner/IcsSubscribeModal'
|
||||
import CollectionsWidget from '../components/Dashboard/CollectionsWidget'
|
||||
import PluginWidgets from '../components/Plugins/PluginWidgets'
|
||||
import PluginFrame from '../components/Plugins/PluginFrame'
|
||||
import { usePluginStore } from '../store/pluginStore'
|
||||
import { formatTime, splitReservationDateTime } from '../utils/formatters'
|
||||
import { convertDistance, getDistanceUnitLabel } from '../utils/units'
|
||||
@@ -127,7 +128,7 @@ export default function DashboardPage(): React.ReactElement {
|
||||
const isAddonEnabled = useAddonStore(s => s.isEnabled)
|
||||
const showCollections = isAddonEnabled('collections') && sideWidgets.collections
|
||||
// Desktop has a master toggle for the whole right column; off → centered layout.
|
||||
const widgetPlugins = usePluginStore(s => s.plugins).filter(p => p.type === 'widget')
|
||||
const widgetPlugins = usePluginStore(s => s.plugins).filter(p => p.type === 'widget' && p.slot !== 'hero')
|
||||
const sidebarVisible = (isMobile || dashCfg.desktop.sidebar) && (showCurrency || showCollections || showTimezones || showUpcoming || widgetPlugins.length > 0)
|
||||
|
||||
return (
|
||||
@@ -293,6 +294,7 @@ function BoardingPassHero({ trip, bundle, locale, onOpen, onEdit, onCopy, onArch
|
||||
}): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
const mobile = useIsMobile()
|
||||
const heroPlugins = usePluginStore(s => s.plugins).filter(p => p.type === 'widget' && p.slot === 'hero')
|
||||
const stop = (e: React.MouseEvent, fn: () => void) => { e.stopPropagation(); fn() }
|
||||
const status = getTripStatus(trip)
|
||||
const start = splitDate(trip.start_date, locale)
|
||||
@@ -408,7 +410,16 @@ function BoardingPassHero({ trip, bundle, locale, onOpen, onEdit, onCopy, onArch
|
||||
</div>
|
||||
|
||||
{!mobile && (
|
||||
<div className="hero-pass" onClick={(e) => { e.stopPropagation(); onOpen() }}>{passCells}</div>
|
||||
<div className="hero-pass-wrap">
|
||||
{heroPlugins.length > 0 && (
|
||||
<div className="hero-pass-overlay" aria-hidden="true">
|
||||
{heroPlugins.map(p => (
|
||||
<PluginFrame key={p.id} pluginId={p.id} tripId={String(trip.id)} title={p.name} className="hero-overlay-frame" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="hero-pass" onClick={(e) => { e.stopPropagation(); onOpen() }}>{passCells}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface ActivePlugin {
|
||||
name: string
|
||||
type: 'integration' | 'page' | 'widget'
|
||||
icon: string | null
|
||||
slot?: 'sidebar' | 'hero'
|
||||
}
|
||||
|
||||
interface PluginState {
|
||||
@@ -21,6 +22,7 @@ interface PluginState {
|
||||
getById: (id: string) => ActivePlugin | undefined
|
||||
pages: () => ActivePlugin[]
|
||||
widgets: () => ActivePlugin[]
|
||||
heroWidgets: () => ActivePlugin[]
|
||||
}
|
||||
|
||||
export const usePluginStore = create<PluginState>((set, get) => ({
|
||||
@@ -38,5 +40,6 @@ export const usePluginStore = create<PluginState>((set, get) => ({
|
||||
|
||||
getById: (id) => get().plugins.find((p) => p.id === id),
|
||||
pages: () => get().plugins.filter((p) => p.type === 'page'),
|
||||
widgets: () => get().plugins.filter((p) => p.type === 'widget'),
|
||||
widgets: () => get().plugins.filter((p) => p.type === 'widget' && p.slot !== 'hero'),
|
||||
heroWidgets: () => get().plugins.filter((p) => p.type === 'widget' && p.slot === 'hero'),
|
||||
}))
|
||||
|
||||
@@ -262,6 +262,19 @@
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none;
|
||||
}
|
||||
|
||||
/* Hero-slot plugin overlay: a transparent strip whose bottom edge sits on the
|
||||
pass's top edge, so a plugin (e.g. the mascot) appears to stand ON the bar.
|
||||
pointer-events none — it must never block the pass or hero interactions. */
|
||||
.trek-dash .hero-pass-wrap { position: relative; }
|
||||
.trek-dash .hero-pass-overlay {
|
||||
position: absolute; left: 16px; right: 16px; bottom: 100%; height: 110px;
|
||||
pointer-events: none; z-index: 3; overflow: hidden;
|
||||
}
|
||||
.trek-dash .hero-overlay-frame {
|
||||
position: absolute; inset: 0; width: 100%; height: 100% !important;
|
||||
border: 0; background: transparent; color-scheme: light;
|
||||
}
|
||||
.trek-dash .pass-cell {
|
||||
padding: 4px 18px; position: relative; display: flex; flex-direction: column;
|
||||
justify-content: center; align-items: center; text-align: center; gap: 6px; flex: 1; min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user