TanStack
TanStack source
931 chart lines · 5 files · 26.4 kB
cases/92-editable-event-range/tanstack.ts543 lines · entry
cases/92-editable-event-range/tanstack.ts
import { defineChart, mountChart, rect, text } from '@tanstack/charts'
import { focusDisabled } from '@tanstack/charts/focus/disabled'
import { scaleBand, scaleUtc } from 'd3-scale'
import { editableEventColor } from './colors'
import {
editableDomain,
editableEvents,
editableEventStart,
editableLanes,
initialEditableEventEnd,
} from './scenario'
import {
clampEditableEventEnd,
editableDateFromAnchor,
editableDateKey,
editableDurationDays,
} from './model'
import { createEditableHandleOverlay } from './overlay'
import type { ChartHost, ChartScene, ChartHostOptions } from '@tanstack/charts'
import type { EditableEvent } from './scenario'
import type { EditableHandleLayout } from './overlay'
import type {
ConformanceGeometryQuery,
ConformanceGeometrySample,
ConformanceInput,
ConformanceMount,
ConformanceTarget,
ConformanceTestDriver,
} from '../../types'
interface EditableChartInput extends ConformanceInput {
end: Date
}
interface EditableState {
end: Date
editing: boolean
editCount: number
originEnd: Date | null
}
const margin = { top: 96, right: 26, bottom: 48, left: 82 }
const millisecondsPerDay = 86_400_000
const definition = (input: EditableChartInput) => {
const rows = editableEvents(input.revision, input.end)
const outsideLabels = rows
.filter((row) => row.id !== 'release')
.map((row) => ({
...row,
labelDate: row.end,
}))
return defineChart(({ width }) => {
const releaseLabels = rows
.filter(
(row) =>
row.id === 'release' && eventBarCanFitLabel(row, width, 'Release'),
)
.map((row) => ({
...row,
labelDate: row.start,
shortLabel: 'Release',
}))
return {
marks: [
rect(rows, {
x1: 'start',
x2: 'end',
y: 'lane',
color: 'id',
radius: 5,
stroke: '#ffffff',
strokeWidth: 1,
}),
text(outsideLabels, {
x: 'labelDate',
y: 'lane',
text: 'label',
anchor: 'start',
dx: 5,
fill: 'currentColor',
fontSize: 10,
fontWeight: 600,
}),
text(releaseLabels, {
x: 'labelDate',
y: 'lane',
text: 'shortLabel',
anchor: 'start',
dx: 5,
fill: '#431407',
fontSize: 10,
fontWeight: 700,
}),
],
x: {
scale: scaleUtc().domain(editableDomain),
grid: true,
axis: {
ticks: {
format: (value: Date) =>
value.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
timeZone: 'UTC',
}),
},
},
},
y: {
scale: scaleBand<string>()
.domain(editableLanes)
.paddingInner(0.38)
.paddingOuter(0.19),
grid: false,
},
color: {
domain: ['discovery', 'design', 'campaign', 'release'],
range: [
editableEventColor('discovery'),
editableEventColor('design'),
editableEventColor('campaign'),
editableEventColor('release'),
],
},
margin,
}
})
}
export const mount: ConformanceMount = (container, input) => {
let currentInput = input
const state: EditableState = {
end: initialEditableEventEnd,
editing: false,
editCount: 0,
originEnd: null,
}
const view = container.ownerDocument.createElement('div')
view.dataset.conformanceView = 'main'
view.style.position = 'relative'
view.style.touchAction = 'pan-y'
sizeView(view, input)
const chartSurface = container.ownerDocument.createElement('div')
view.append(chartSurface)
container.append(view)
const options = (): ChartHostOptions<EditableEvent> => ({
definition: defineChart(
definition({
...currentInput,
end: state.end,
}),
{ animate: false, keyboard: false, focus: focusDisabled },
),
width: currentInput.width,
height: currentInput.height,
ariaLabel: editableAriaLabel(currentInput.revision, state.end),
})
const host = mountChart(chartSurface, options())
const interactions = createEditableInteractions(
chartSurface,
view,
() => currentInput,
state,
host,
options,
)
interactions.paint()
return {
driver: interactions.driver,
update(nextInput) {
currentInput = nextInput
sizeView(view, nextInput)
host.update(options())
interactions.paint()
},
destroy() {
interactions.destroy()
host.destroy()
view.remove()
},
}
}
function createEditableInteractions(
chartSurface: HTMLDivElement,
view: HTMLDivElement,
getInput: () => ConformanceInput,
state: EditableState,
host: ChartHost<EditableEvent>,
options: () => ChartHostOptions<EditableEvent>,
) {
let paint = () => {}
const beginEdit = () => {
if (state.editing) return
state.originEnd = state.end
state.editing = true
}
const setEnd = (date: Date) => {
state.end = clampEditableEventEnd(date)
host.update(options())
paint()
}
const overlay = createEditableHandleOverlay(
view,
(value) => {
beginEdit()
setEnd(new Date(editableDomain[0].getTime() + value * millisecondsPerDay))
},
(value) => {
const date = editableDateFromAnchor(`date:${value}`)
if (!date) return false
beginEdit()
setEnd(date)
return true
},
)
const layout = () =>
editableLayout(chartSurface, view, host.getScene(), state.end)
paint = () => {
const nextLayout = layout()
if (!nextLayout) return
overlay.paint(nextLayout, {
value: editableDayIndex(state.end),
min: editableDayIndex(
new Date(editableEventStart.getTime() + millisecondsPerDay),
),
max: editableDayIndex(editableDomain[1]),
date: editableDateKey(state.end),
minDate: editableDateKey(
new Date(editableEventStart.getTime() + millisecondsPerDay),
),
maxDate: editableDateKey(editableDomain[1]),
valueText: `Release: ${editableDateKey(editableEventStart)} → ${editableDateKey(state.end)} · ${editableDurationDays(editableEventStart, state.end)} days`,
summaryText: `Release · ${compactDate(editableEventStart)} → ${compactDate(state.end)} · ${editableDurationDays(editableEventStart, state.end)} days`,
eventDescriptions: editableEvents(getInput().revision, state.end).map(
(row) =>
`${row.label}: ${editableDateKey(row.start)} to ${editableDateKey(row.end)}`,
),
})
}
const commitEdit = () => {
if (!state.editing) return
state.editing = false
state.originEnd = null
state.editCount += 1
paint()
}
const cancelEdit = () => {
if (!state.editing) return
const originEnd = state.originEnd
state.editing = false
state.originEnd = null
if (originEnd) setEnd(originEnd)
else paint()
}
overlay.range.addEventListener('pointerdown', beginEdit)
overlay.range.addEventListener('change', commitEdit)
overlay.range.addEventListener('pointercancel', cancelEdit)
overlay.dateInput.addEventListener('change', commitEdit)
overlay.dateInput.addEventListener('pointercancel', cancelEdit)
const driver: ConformanceTestDriver = {
resolveTarget(target) {
return resolveTarget(
chartSurface,
view,
host.getScene(),
state.end,
target,
)
},
readState() {
return interactionState(state, getInput())
},
geometry(query) {
return editableGeometry(
chartSurface,
view,
host.getScene(),
getInput(),
state.end,
layout(),
overlay.handleGeometry(),
overlay.trackGeometry(),
query,
)
},
viewBounds(viewName) {
return viewName === undefined || viewName === 'main'
? clientBounds(view)
: null
},
settle: paint,
}
return {
driver,
paint,
destroy() {
overlay.range.removeEventListener('pointerdown', beginEdit)
overlay.range.removeEventListener('change', commitEdit)
overlay.range.removeEventListener('pointercancel', cancelEdit)
overlay.dateInput.removeEventListener('change', commitEdit)
overlay.dateInput.removeEventListener('pointercancel', cancelEdit)
overlay.destroy()
},
}
}
function resolveTarget(
chartSurface: HTMLDivElement,
view: HTMLDivElement,
scene: ChartScene<EditableEvent>,
end: Date,
target: ConformanceTarget,
) {
if (target.view !== undefined && target.view !== 'main') return null
if (target.anchor === 'control:date') {
const dateInput = view.querySelector<HTMLInputElement>(
'.ts-conformance-event-date',
)
if (!dateInput) return null
const bounds = dateInput.getBoundingClientRect()
return {
x: bounds.left + bounds.width / 2,
y: bounds.top + bounds.height / 2,
focusElement: dateInput,
}
}
const date =
target.anchor === 'event:release:end'
? end
: editableDateFromAnchor(target.anchor)
if (!date) return null
const point = sceneLocalPoint(
chartSurface,
view,
scene,
scene.scales.x.map(date),
scene.scales.y.map('Engineering'),
)
if (!point) return null
const bounds = view.getBoundingClientRect()
return {
x: bounds.left + point[0],
y: bounds.top + point[1],
focusElement:
view.querySelector<HTMLInputElement>('.ts-conformance-event-range') ??
undefined,
}
}
function interactionState(state: EditableState, input: ConformanceInput) {
const rows = editableEvents(input.revision, state.end)
const design = rows.find((row) => row.id === 'design')
const campaign = rows.find((row) => row.id === 'campaign')
return {
editor: {
id: 'release',
start: editableDateKey(editableEventStart),
end: editableDateKey(state.end),
durationDays: editableDurationDays(editableEventStart, state.end),
editing: state.editing,
editCount: state.editCount,
},
events: {
count: rows.length,
ids: rows.map((row) => row.id),
designEnd: design ? editableDateKey(design.end) : null,
campaignStart: campaign ? editableDateKey(campaign.start) : null,
},
}
}
function editableGeometry(
chartSurface: HTMLDivElement,
view: HTMLDivElement,
scene: ChartScene<EditableEvent>,
input: ConformanceInput,
end: Date,
layout: EditableHandleLayout | null,
overlayHandle: ConformanceGeometrySample,
overlayTrack: ConformanceGeometrySample,
query: ConformanceGeometryQuery,
): readonly ConformanceGeometrySample[] {
if (!layout || (query.view !== undefined && query.view !== 'main')) {
return []
}
if (query.role === 'dot') return [overlayHandle]
if (query.role === 'rule') return [overlayTrack]
if (query.role !== 'rect') return []
const viewBounds = view.getBoundingClientRect()
const height = scene.scales.y.bandwidth
return editableEvents(input.revision, end).flatMap((row) => {
const start = sceneLocalPoint(
chartSurface,
view,
scene,
scene.scales.x.map(row.start),
scene.scales.y.map(row.lane),
)
const finish = sceneLocalPoint(
chartSurface,
view,
scene,
scene.scales.x.map(row.end),
scene.scales.y.map(row.lane),
)
const top = sceneLocalPoint(
chartSurface,
view,
scene,
scene.scales.x.map(row.start),
scene.scales.y.map(row.lane) - height / 2,
)
const bottom = sceneLocalPoint(
chartSurface,
view,
scene,
scene.scales.x.map(row.start),
scene.scales.y.map(row.lane) + height / 2,
)
if (!start || !finish || !top || !bottom) return []
return [
{
x: viewBounds.left + Math.min(start[0], finish[0]),
y: viewBounds.top + Math.min(top[1], bottom[1]),
width: Math.abs(finish[0] - start[0]),
height: Math.abs(bottom[1] - top[1]),
paint: editableEventColor(row.id),
},
]
})
}
function editableLayout(
chartSurface: HTMLDivElement,
view: HTMLDivElement,
scene: ChartScene<EditableEvent>,
end: Date,
): EditableHandleLayout | null {
const point = sceneLocalPoint(
chartSurface,
view,
scene,
scene.scales.x.map(end),
scene.scales.y.map('Engineering'),
)
const minimum = sceneLocalPoint(
chartSurface,
view,
scene,
scene.scales.x.map(
new Date(editableEventStart.getTime() + millisecondsPerDay),
),
scene.scales.y.map('Engineering'),
)
const maximum = sceneLocalPoint(
chartSurface,
view,
scene,
scene.scales.x.map(editableDomain[1]),
scene.scales.y.map('Engineering'),
)
return point && minimum && maximum
? { x: point[0], y: point[1], minX: minimum[0], maxX: maximum[0] }
: null
}
function sceneLocalPoint(
chartSurface: HTMLDivElement,
view: HTMLDivElement,
scene: ChartScene<EditableEvent>,
sceneX: number,
sceneY: number,
): readonly [number, number] | null {
const svg = chartSurface.querySelector<SVGSVGElement>('svg.ts-chart')
if (!svg) return null
const svgBounds = svg.getBoundingClientRect()
const viewBounds = view.getBoundingClientRect()
return [
svgBounds.left - viewBounds.left + (sceneX / scene.width) * svgBounds.width,
svgBounds.top - viewBounds.top + (sceneY / scene.height) * svgBounds.height,
]
}
function editableDayIndex(date: Date) {
return Math.round(
(date.getTime() - editableDomain[0].getTime()) / millisecondsPerDay,
)
}
function clientBounds(element: HTMLElement): ConformanceGeometrySample {
const bounds = element.getBoundingClientRect()
return {
x: bounds.left,
y: bounds.top,
width: bounds.width,
height: bounds.height,
}
}
function sizeView(view: HTMLDivElement, input: ConformanceInput) {
view.style.width = `${input.width}px`
view.style.height = `${input.height}px`
}
function compactDate(date: Date) {
return date.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
timeZone: 'UTC',
})
}
function editableAriaLabel(revision: number, end: Date) {
return `Editable schedule. ${editableEvents(revision, end)
.map(
(row) =>
`${row.label}, ${editableDateKey(row.start)} to ${editableDateKey(row.end)}`,
)
.join('. ')}.`
}
function eventBarCanFitLabel(
event: EditableEvent,
width: number,
label: string,
) {
const plotWidth = Math.max(0, width - margin.left - margin.right)
const domainWidth = editableDomain[1].getTime() - editableDomain[0].getTime()
const eventWidth = event.end.getTime() - event.start.getTime()
const barWidth = domainWidth > 0 ? (eventWidth / domainWidth) * plotWidth : 0
return barWidth >= label.length * 6 + 10
}cases/92-editable-event-range/colors.ts5 lines · support
cases/92-editable-event-range/colors.ts
import type { EditableEventId } from './scenario'
export function editableEventColor(id: EditableEventId) {
return id === 'release' ? '#f97316' : '#2563eb'
}cases/92-editable-event-range/model.ts36 lines · support
cases/92-editable-event-range/model.ts
import { utcDay } from 'd3-time'
import { editableDomain, editableEventStart } from './scenario'
const day = 86_400_000
export function editableDateKey(date: Date) {
return date.toISOString().slice(0, 10)
}
export function editableDateFromAnchor(anchor: string) {
const key = anchor.startsWith('date:') ? anchor.slice(5) : ''
if (!/^\d{4}-\d{2}-\d{2}$/.test(key)) return null
const date = new Date(`${key}T00:00:00.000Z`)
if (
!Number.isFinite(date.getTime()) ||
editableDateKey(date) !== key ||
date < editableDomain[0] ||
date > editableDomain[1]
) {
return null
}
return date
}
export function clampEditableEventEnd(date: Date) {
const minimum = editableEventStart.getTime() + day
const timestamp = Math.min(
editableDomain[1].getTime(),
Math.max(minimum, utcDay.round(date).getTime()),
)
return new Date(timestamp)
}
export function editableDurationDays(start: Date, end: Date) {
return (end.getTime() - start.getTime()) / day
}cases/92-editable-event-range/overlay.ts280 lines · support
cases/92-editable-event-range/overlay.ts
export interface EditableHandleLayout {
x: number
y: number
minX: number
maxX: number
}
export interface EditableHandleState {
value: number
min: number
max: number
date: string
minDate: string
maxDate: string
valueText: string
summaryText: string
eventDescriptions: readonly string[]
}
export function createEditableHandleOverlay(
view: HTMLDivElement,
onRangeInput: (value: number) => void,
onDateInput: (value: string) => boolean,
) {
const document = view.ownerDocument
const style = document.createElement('style')
style.textContent = `
.ts-conformance-event-range {
appearance: none;
-webkit-appearance: none;
background: transparent;
border: 0;
color-scheme: light dark;
cursor: ew-resize;
margin: 0;
padding: 0;
}
.ts-conformance-event-range::-webkit-slider-runnable-track {
height: 2px;
background: transparent;
}
.ts-conformance-event-range::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
width: 20px;
height: 20px;
margin-top: -9px;
border: 0;
background: transparent;
}
.ts-conformance-event-range::-moz-range-track {
height: 2px;
background: transparent;
}
.ts-conformance-event-range::-moz-range-thumb {
width: 20px;
height: 20px;
border: 0;
background: transparent;
}
.ts-conformance-event-range:focus-visible {
outline: none;
}
.ts-conformance-event-range:focus-visible
+ .ts-conformance-event-handle {
outline: 3px solid var(--ts-chart-1, #2563eb);
outline-offset: 3px;
}
.ts-conformance-event-date:focus-visible {
outline: 3px solid var(--ts-chart-1, #2563eb);
outline-offset: 2px;
}
.ts-conformance-event-summary,
.ts-conformance-event-date {
border: 1px solid color-mix(in srgb, currentColor 32%, transparent);
background: color-mix(in srgb, var(--ts-chart-2, #f97316) 12%, Canvas);
color: inherit;
}
.ts-conformance-event-date {
color-scheme: light dark;
}
.ts-conformance-event-date[aria-invalid="true"] {
border-color: #dc2626;
}
`
const layer = document.createElement('div')
layer.style.position = 'absolute'
layer.style.inset = '0'
layer.style.zIndex = '3'
layer.style.pointerEvents = 'none'
const track = document.createElement('div')
track.className = 'ts-conformance-event-track'
track.style.position = 'absolute'
track.style.height = '4px'
track.style.borderRadius = '999px'
track.style.background =
'color-mix(in srgb, var(--ts-chart-2, #f97316) 58%, transparent)'
const range = document.createElement('input')
range.className = 'ts-conformance-event-range'
range.type = 'range'
range.step = '1'
range.setAttribute('aria-label', 'Release end handle')
range.style.position = 'absolute'
range.style.height = '44px'
range.style.pointerEvents = 'auto'
const handle = document.createElement('div')
handle.className = 'ts-conformance-event-handle'
handle.style.position = 'absolute'
handle.style.boxSizing = 'border-box'
handle.style.width = '20px'
handle.style.height = '20px'
handle.style.border = '2px solid Canvas'
handle.style.borderRadius = '999px'
handle.style.background = 'var(--ts-chart-2, #f97316)'
handle.style.boxShadow = '0 1px 4px rgb(15 23 42 / 0.35)'
const toolbar = document.createElement('div')
toolbar.className = 'ts-conformance-event-toolbar'
toolbar.setAttribute('role', 'group')
toolbar.setAttribute('aria-label', 'Release event editor')
toolbar.style.position = 'absolute'
toolbar.style.top = '4px'
toolbar.style.left = '12px'
toolbar.style.right = '12px'
toolbar.style.display = 'flex'
toolbar.style.flexWrap = 'wrap'
toolbar.style.alignItems = 'flex-end'
toolbar.style.justifyContent = 'flex-end'
toolbar.style.gap = '8px'
toolbar.style.color = 'inherit'
toolbar.style.pointerEvents = 'none'
const status = document.createElement('output')
status.className = 'ts-conformance-event-summary'
status.setAttribute('role', 'status')
status.setAttribute('aria-live', 'polite')
status.setAttribute('aria-atomic', 'true')
status.style.boxSizing = 'border-box'
status.style.flex = '1 1 120px'
status.style.minWidth = '120px'
status.style.minHeight = '44px'
status.style.padding = '8px 10px'
status.style.borderRadius = '10px'
status.style.display = 'flex'
status.style.alignItems = 'center'
status.style.font = '600 12px/1.25 system-ui, sans-serif'
const dateLabel = document.createElement('label')
dateLabel.style.boxSizing = 'border-box'
dateLabel.style.flex = '0 1 140px'
dateLabel.style.minWidth = '128px'
dateLabel.style.display = 'grid'
dateLabel.style.gap = '2px'
dateLabel.style.color = 'inherit'
dateLabel.style.font = '600 11px/1.15 system-ui, sans-serif'
dateLabel.style.pointerEvents = 'auto'
dateLabel.append('Release end')
const dateInput = document.createElement('input')
dateInput.className = 'ts-conformance-event-date'
dateInput.type = 'date'
dateInput.required = true
dateInput.setAttribute('aria-label', 'Release end date input')
dateInput.setAttribute('aria-invalid', 'false')
dateInput.style.boxSizing = 'border-box'
dateInput.style.width = '100%'
dateInput.style.height = '44px'
dateInput.style.padding = '6px 8px'
dateInput.style.borderRadius = '8px'
dateInput.style.font = '600 12px/1 system-ui, sans-serif'
dateLabel.append(dateInput)
const validation = document.createElement('span')
validation.className = 'ts-conformance-event-validation'
validation.setAttribute('aria-live', 'polite')
validation.hidden = true
validation.style.flex = '1 0 100%'
validation.style.color = '#dc2626'
validation.style.font = '600 11px/1.2 system-ui, sans-serif'
const eventList = document.createElement('ul')
eventList.className = 'ts-conformance-event-identities'
eventList.style.position = 'absolute'
eventList.style.width = '1px'
eventList.style.height = '1px'
eventList.style.padding = '0'
eventList.style.margin = '-1px'
eventList.style.overflow = 'hidden'
eventList.style.clipPath = 'inset(50%)'
eventList.style.whiteSpace = 'nowrap'
const setDateValidity = (valid: boolean) => {
const message = valid ? '' : 'Choose a release end date within the range.'
dateInput.setAttribute('aria-invalid', String(!valid))
dateInput.setCustomValidity(message)
validation.hidden = valid
validation.textContent = message
}
const handleRangeInput = () => {
setDateValidity(true)
onRangeInput(Number(range.value))
}
const handleDateInput = () => {
setDateValidity(onDateInput(dateInput.value))
}
range.addEventListener('input', handleRangeInput)
dateInput.addEventListener('input', handleDateInput)
toolbar.append(status, dateLabel, validation)
layer.append(track, range, handle, toolbar, eventList)
view.append(style, layer)
return {
range,
dateInput,
handleGeometry() {
return measureElement(handle)
},
trackGeometry() {
return measureElement(track)
},
paint(layout: EditableHandleLayout, state: EditableHandleState) {
track.style.left = `${layout.minX}px`
track.style.top = `${layout.y - 2}px`
track.style.width = `${Math.max(0, layout.maxX - layout.minX)}px`
range.min = String(state.min)
range.max = String(state.max)
range.value = String(state.value)
range.setAttribute('aria-valuetext', state.valueText)
range.style.left = `${layout.minX - 10}px`
range.style.top = `${layout.y - 22}px`
range.style.width = `${Math.max(0, layout.maxX - layout.minX + 20)}px`
handle.style.left = `${layout.x - 10}px`
handle.style.top = `${layout.y - 10}px`
dateInput.min = state.minDate
dateInput.max = state.maxDate
if (
document.activeElement !== dateInput ||
dateInput.getAttribute('aria-invalid') !== 'true'
) {
dateInput.value = state.date
setDateValidity(true)
}
status.value = state.summaryText
status.textContent = state.summaryText
eventList.replaceChildren(
...state.eventDescriptions.map((description) => {
const item = document.createElement('li')
item.textContent = description
return item
}),
)
},
destroy() {
range.removeEventListener('input', handleRangeInput)
dateInput.removeEventListener('input', handleDateInput)
style.remove()
layer.remove()
},
}
}
function measureElement(element: HTMLElement) {
const bounds = element.getBoundingClientRect()
return {
x: bounds.left,
y: bounds.top,
width: bounds.width,
height: bounds.height,
paint: getComputedStyle(element).backgroundColor,
}
}cases/92-editable-event-range/scenario.ts67 lines · support
cases/92-editable-event-range/scenario.ts
export type EditableEventId = 'discovery' | 'design' | 'campaign' | 'release'
export type EditableLane = 'Product' | 'Design' | 'Marketing' | 'Engineering'
export interface EditableEvent {
id: EditableEventId
label: string
lane: EditableLane
start: Date
end: Date
}
export const editableLanes: readonly EditableLane[] = [
'Product',
'Design',
'Marketing',
'Engineering',
]
export const editableDomain: readonly [Date, Date] = [
utcDate(2025, 0, 1),
utcDate(2025, 2, 1),
]
export const editableEventStart = utcDate(2025, 1, 3)
export const initialEditableEventEnd = utcDate(2025, 1, 12)
export function editableEvents(
revision = 0,
releaseEnd = initialEditableEventEnd,
): readonly EditableEvent[] {
const updated = revision % 2 === 1
return [
{
id: 'discovery',
label: 'Discovery',
lane: 'Product',
start: utcDate(2025, 0, 4),
end: utcDate(2025, 0, 13),
},
{
id: 'design',
label: 'Design system',
lane: 'Design',
start: utcDate(2025, 0, 10),
end: utcDate(2025, 0, updated ? 26 : 24),
},
{
id: 'campaign',
label: 'Campaign',
lane: 'Marketing',
start: utcDate(2025, 0, updated ? 19 : 20),
end: utcDate(2025, 1, 7),
},
{
id: 'release',
label: 'Release window',
lane: 'Engineering',
start: editableEventStart,
end: releaseEnd,
},
]
}
function utcDate(year: number, month: number, date: number) {
return new Date(Date.UTC(year, month, date))
}