feat(timetable): pinia store + CSS tokens (Session 4 steps 5+7)
useTimetableStore — pinia composition store carrying: - activeDayId synced to ?day query param at the page level - selectedPerformanceId for popover anchor + keyboard focus - drag state (dragPerformanceId / dragOriginSnapshot / dragGhost) for optimistic preview + 409 rollback - statusFilter (defaults: all on except cancelled, per prototype §4.7) - searchQuery for the wachtrij filter styles/tokens/_timetable.scss — RFC v0.2 D21: - 9 status palettes (bg / border / fg / dot custom properties) - cancelled-hatch repeating gradient - conflict / capacity-warn / capacity-critical / B2B / trashed colours - lane geometry (height, gap, padding, block radius) - canvas + axis backgrounds and tick lines - drag-ghost + focus-ring + day-tab chrome - tt-cascade-pulse keyframe animation for D18 cascaded[] visualisation Imported once via assets/styles/styles.scss so the variables are available everywhere via var(--tt-…). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1,4 @@
|
|||||||
// Write your overrides
|
// Write your overrides
|
||||||
|
|
||||||
|
// RFC-TIMETABLE v0.2 D21 — status palette + geometry custom properties.
|
||||||
|
@use "@/styles/tokens/timetable";
|
||||||
|
|||||||
118
apps/app/src/stores/useTimetableStore.ts
Normal file
118
apps/app/src/stores/useTimetableStore.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import {
|
||||||
|
ArtistEngagementStatus,
|
||||||
|
} from '@/types/timetable'
|
||||||
|
import type {
|
||||||
|
ArtistEngagementStatus as ArtistEngagementStatusType,
|
||||||
|
Performance,
|
||||||
|
} from '@/types/timetable'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI / cross-component state for the timetable canvas.
|
||||||
|
*
|
||||||
|
* Server state (stages / performances / wachtrij) lives in the TanStack
|
||||||
|
* cache via useTimetable.ts — this store carries only the bits that
|
||||||
|
* multiple components on the canvas need to share:
|
||||||
|
*
|
||||||
|
* - Active sub-event id (synced ↔ ?day query)
|
||||||
|
* - Selected performance id (drives popover anchor + keyboard focus)
|
||||||
|
* - In-flight drag state + origin snapshot for rollback (RFC D7)
|
||||||
|
* - Status filter (chips above wachtrij + canvas dimming)
|
||||||
|
* - Free-text search (wachtrij filter)
|
||||||
|
*/
|
||||||
|
export const useTimetableStore = defineStore('timetable', () => {
|
||||||
|
const activeDayId = ref<string | null>(null)
|
||||||
|
const selectedPerformanceId = ref<string | null>(null)
|
||||||
|
|
||||||
|
// Drag state — set by usePointerDrag handlers, consumed by mutation
|
||||||
|
// composables for optimistic preview + rollback.
|
||||||
|
const dragPerformanceId = ref<string | null>(null)
|
||||||
|
const dragOriginSnapshot = ref<Performance | null>(null)
|
||||||
|
|
||||||
|
const dragGhost = ref<{
|
||||||
|
stageId: string | null
|
||||||
|
startAt: string
|
||||||
|
endAt: string
|
||||||
|
lane: number
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
// Status filter — defaults to "all on except cancelled" (prototype audit §4.7).
|
||||||
|
const statusFilter = ref<Set<ArtistEngagementStatusType>>(new Set([
|
||||||
|
ArtistEngagementStatus.DRAFT,
|
||||||
|
ArtistEngagementStatus.REQUESTED,
|
||||||
|
ArtistEngagementStatus.OPTION,
|
||||||
|
ArtistEngagementStatus.OFFERED,
|
||||||
|
ArtistEngagementStatus.CONFIRMED,
|
||||||
|
ArtistEngagementStatus.CONTRACTED,
|
||||||
|
ArtistEngagementStatus.REJECTED,
|
||||||
|
ArtistEngagementStatus.DECLINED,
|
||||||
|
]))
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
function setActiveDay(id: string | null): void {
|
||||||
|
activeDayId.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPerformance(id: string | null): void {
|
||||||
|
selectedPerformanceId.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrag(perf: Performance): void {
|
||||||
|
dragPerformanceId.value = perf.id
|
||||||
|
dragOriginSnapshot.value = perf
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDragGhost(ghost: typeof dragGhost.value): void {
|
||||||
|
dragGhost.value = ghost
|
||||||
|
}
|
||||||
|
|
||||||
|
function endDrag(): void {
|
||||||
|
dragPerformanceId.value = null
|
||||||
|
dragOriginSnapshot.value = null
|
||||||
|
dragGhost.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStatus(status: ArtistEngagementStatusType): void {
|
||||||
|
const next = new Set(statusFilter.value)
|
||||||
|
if (next.has(status))
|
||||||
|
next.delete(status)
|
||||||
|
else next.add(status)
|
||||||
|
statusFilter.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatusFilter(statuses: ArtistEngagementStatusType[]): void {
|
||||||
|
statusFilter.value = new Set(statuses)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStatusVisible(status: ArtistEngagementStatusType | null | undefined): boolean {
|
||||||
|
return status !== null && status !== undefined && statusFilter.value.has(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSearchQuery(query: string): void {
|
||||||
|
searchQuery.value = query
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDragging = computed(() => dragPerformanceId.value !== null)
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeDayId,
|
||||||
|
selectedPerformanceId,
|
||||||
|
dragPerformanceId,
|
||||||
|
dragOriginSnapshot,
|
||||||
|
dragGhost,
|
||||||
|
statusFilter,
|
||||||
|
searchQuery,
|
||||||
|
isDragging,
|
||||||
|
setActiveDay,
|
||||||
|
selectPerformance,
|
||||||
|
startDrag,
|
||||||
|
updateDragGhost,
|
||||||
|
endDrag,
|
||||||
|
toggleStatus,
|
||||||
|
setStatusFilter,
|
||||||
|
isStatusVisible,
|
||||||
|
setSearchQuery,
|
||||||
|
}
|
||||||
|
})
|
||||||
140
apps/app/src/styles/tokens/_timetable.scss
Normal file
140
apps/app/src/styles/tokens/_timetable.scss
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
// RFC-TIMETABLE v0.2 D21 — status colour tokens for the timetable canvas.
|
||||||
|
//
|
||||||
|
// Per-status colour pairs (background + border + foreground + dot) live as
|
||||||
|
// CSS custom properties so PerformanceBlock + WachtrijCard + popovers all
|
||||||
|
// resolve through `var(--tt-status-{status}-*)`.
|
||||||
|
//
|
||||||
|
// ART-14 (deferred) will let an organisation override the palette by
|
||||||
|
// scoping these custom properties on a `[data-org-id="…"]` selector.
|
||||||
|
//
|
||||||
|
// Geometry tokens (lane height, time-axis spacing, block padding) live
|
||||||
|
// next to the colours so any rendering tweak is one stop.
|
||||||
|
|
||||||
|
:root {
|
||||||
|
// ─── Status palettes (8 visible + cancelled overlay) ─────────────
|
||||||
|
|
||||||
|
--tt-status-draft-bg: #f1efe9;
|
||||||
|
--tt-status-draft-border: #dcd9d1;
|
||||||
|
--tt-status-draft-fg: #3a3830;
|
||||||
|
--tt-status-draft-dot: #a09c92;
|
||||||
|
|
||||||
|
--tt-status-requested-bg: #fff6e0;
|
||||||
|
--tt-status-requested-border:#f0d99a;
|
||||||
|
--tt-status-requested-fg: #5d4612;
|
||||||
|
--tt-status-requested-dot: #d9a93c;
|
||||||
|
|
||||||
|
--tt-status-option-bg: #f3eefa;
|
||||||
|
--tt-status-option-border: #d8c8ee;
|
||||||
|
--tt-status-option-fg: #4b2d75;
|
||||||
|
--tt-status-option-dot: #8b5cd0;
|
||||||
|
|
||||||
|
--tt-status-offered-bg: #fef5e7;
|
||||||
|
--tt-status-offered-border: #f4d6a3;
|
||||||
|
--tt-status-offered-fg: #6d4406;
|
||||||
|
--tt-status-offered-dot: #e0992c;
|
||||||
|
|
||||||
|
--tt-status-confirmed-bg: #e8f8f0;
|
||||||
|
--tt-status-confirmed-border:#a8dec5;
|
||||||
|
--tt-status-confirmed-fg: #1a5b3b;
|
||||||
|
--tt-status-confirmed-dot: #2fa66a;
|
||||||
|
|
||||||
|
--tt-status-contracted-bg: #e6f1fb;
|
||||||
|
--tt-status-contracted-border:#a4c8eb;
|
||||||
|
--tt-status-contracted-fg: #134474;
|
||||||
|
--tt-status-contracted-dot: #2a78c8;
|
||||||
|
|
||||||
|
--tt-status-cancelled-bg: #f5f3ef;
|
||||||
|
--tt-status-cancelled-border:#cfcdc7;
|
||||||
|
--tt-status-cancelled-fg: #75706a;
|
||||||
|
--tt-status-cancelled-dot: #999591;
|
||||||
|
|
||||||
|
--tt-status-rejected-bg: #fbeaec;
|
||||||
|
--tt-status-rejected-border: #ecb6bd;
|
||||||
|
--tt-status-rejected-fg: #75162a;
|
||||||
|
--tt-status-rejected-dot: #c5354b;
|
||||||
|
|
||||||
|
--tt-status-declined-bg: #f7eee9;
|
||||||
|
--tt-status-declined-border: #ddc6b9;
|
||||||
|
--tt-status-declined-fg: #6b3915;
|
||||||
|
--tt-status-declined-dot: #b56331;
|
||||||
|
|
||||||
|
// ─── Cancelled hatch overlay ─────────────────────────────────────
|
||||||
|
|
||||||
|
--tt-cancelled-hatch: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
transparent 0,
|
||||||
|
transparent 6px,
|
||||||
|
rgba(0, 0, 0, 0.05) 6px,
|
||||||
|
rgba(0, 0, 0, 0.05) 8px
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Warnings + B2B ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
--tt-conflict-border: #d63d4b;
|
||||||
|
--tt-conflict-glow: rgba(214, 61, 75, 0.25);
|
||||||
|
|
||||||
|
--tt-capacity-warn: #e0992c;
|
||||||
|
--tt-capacity-critical: #c5354b;
|
||||||
|
|
||||||
|
--tt-b2b-dot: #2a78c8;
|
||||||
|
--tt-b2b-dot-size: 6px;
|
||||||
|
|
||||||
|
--tt-trashed-overlay: rgba(0, 0, 0, 0.35);
|
||||||
|
--tt-trashed-icon: #75706a;
|
||||||
|
|
||||||
|
// ─── Geometry ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
--tt-lane-height: 44px;
|
||||||
|
--tt-lane-gap: 4px;
|
||||||
|
--tt-lane-pad: 4px;
|
||||||
|
|
||||||
|
--tt-block-radius: 6px;
|
||||||
|
--tt-block-pad-x: 8px;
|
||||||
|
--tt-block-pad-y: 4px;
|
||||||
|
--tt-block-min-width: 24px;
|
||||||
|
|
||||||
|
--tt-row-divider: #e6e3dc;
|
||||||
|
--tt-axis-tick: #cfcdc7;
|
||||||
|
--tt-axis-tick-major: #a09c92;
|
||||||
|
--tt-axis-label-fg: #4b4b48;
|
||||||
|
|
||||||
|
--tt-canvas-bg: #fbfaf7;
|
||||||
|
--tt-canvas-grid-major: rgba(0, 0, 0, 0.06);
|
||||||
|
--tt-canvas-grid-minor: rgba(0, 0, 0, 0.025);
|
||||||
|
|
||||||
|
// ─── Drop / drag visuals ─────────────────────────────────────────
|
||||||
|
|
||||||
|
--tt-ghost-bg: rgba(255, 215, 90, 0.18);
|
||||||
|
--tt-ghost-border:#f0c45a;
|
||||||
|
|
||||||
|
--tt-focus-ring: #1f7ad1;
|
||||||
|
|
||||||
|
// ─── Day-tab chrome ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
--tt-tab-active-bg: #1f7ad1;
|
||||||
|
--tt-tab-active-fg: #ffffff;
|
||||||
|
--tt-tab-hover-bg: #eef2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Animations ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@keyframes tt-cascade-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(31, 122, 209, 0.55);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
box-shadow: 0 0 0 7px rgba(31, 122, 209, 0);
|
||||||
|
transform: scale(1.015);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(31, 122, 209, 0);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-cascade-pulse {
|
||||||
|
animation: tt-cascade-pulse 1.5s ease-out 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user