{"version":3,"file":"scheduler-layout.cjs","names":[],"sources":["../../../src/components/Scheduler/scheduler-layout.ts"],"sourcesContent":["/** An event on the schedule. `start`/`end` are absolute instants in local time. */\nexport interface SchedulerEvent {\n    id: string;\n    title: string;\n    start: Date;\n    end: Date;\n    /** Render across the all-day lane instead of the time grid. */\n    allDay?: boolean;\n    /** Free-form payload the app passes through. */\n    data?: Record<string, unknown>;\n}\n\n/** An event placed in the grid: which day, where vertically, which column. */\nexport interface PlacedEvent {\n    event: SchedulerEvent;\n    /** Index into the rendered day list. */\n    dayIndex: number;\n    /** Distance from the top of the day column, as a fraction of its height. */\n    top: number;\n    /** Height as a fraction of the day column. */\n    height: number;\n    /** Column this event occupies among its overlapping cluster, zero-based. */\n    column: number;\n    /** How many columns the cluster was split into. */\n    columns: number;\n}\n\n/** The visible time window of a day, in minutes from midnight. */\nexport interface DayWindow {\n    startMinute: number;\n    endMinute: number;\n}\n\n/** Minutes from midnight, in local time. */\nexport function minutesOfDay(date: Date): number {\n    return date.getHours() * 60 + date.getMinutes();\n}\n\n/** Midnight local time on the same calendar day as `date`. */\nexport function startOfDay(date: Date): Date {\n    const copy = new Date(date);\n    copy.setHours(0, 0, 0, 0);\n    return copy;\n}\n\n/** `date` shifted by whole days, preserving local wall-clock time. */\nexport function addDays(date: Date, days: number): Date {\n    const copy = new Date(date);\n    copy.setDate(copy.getDate() + days);\n    return copy;\n}\n\n/** Whether two dates fall on the same local calendar day. */\nexport function isSameDay(a: Date, b: Date): boolean {\n    return (\n        a.getFullYear() === b.getFullYear() &&\n        a.getMonth() === b.getMonth() &&\n        a.getDate() === b.getDate()\n    );\n}\n\n/**\n * The consecutive local days a schedule should render.\n *\n * Built by incrementing the calendar day rather than adding 24 h of milliseconds:\n * across a DST boundary a day is 23 or 25 hours long, and millisecond arithmetic\n * would silently produce a duplicated or skipped date.\n *\n * @param anchor - Any instant within the first day.\n * @param count - How many days to produce.\n * @returns Midnight of each day, in order.\n */\nexport function dayRange(anchor: Date, count: number): Date[] {\n    const first = startOfDay(anchor);\n    return Array.from({ length: Math.max(0, count) }, (_, index) => addDays(first, index));\n}\n\n/**\n * Split an event into one segment per day it touches, clipped to the window.\n *\n * An event that crosses midnight has to appear in both columns, and each piece has\n * to be clipped to that day's visible window — otherwise a 23:00–01:00 booking\n * either vanishes or is drawn far outside its column.\n *\n * @param event - The event to place.\n * @param days - Midnight of each rendered day.\n * @param window - Visible minute range shared by every day.\n * @returns One entry per day the event is visible on.\n */\nfunction segmentsFor(\n    event: SchedulerEvent,\n    days: Date[],\n    window: DayWindow,\n): { dayIndex: number; from: number; to: number }[] {\n    const out: { dayIndex: number; from: number; to: number }[] = [];\n    days.forEach((day, dayIndex) => {\n        const dayStart = day.getTime();\n        const dayEnd = addDays(day, 1).getTime();\n        if (event.end.getTime() <= dayStart || event.start.getTime() >= dayEnd) return;\n\n        const from = event.start.getTime() <= dayStart ? 0 : minutesOfDay(event.start);\n        const to =\n            event.end.getTime() >= dayEnd\n                ? 24 * 60\n                : minutesOfDay(event.end) || (event.end.getTime() > dayStart ? 24 * 60 : 0);\n\n        const clippedFrom = Math.max(window.startMinute, from);\n        const clippedTo = Math.min(window.endMinute, to);\n        if (clippedTo <= clippedFrom) return;\n        out.push({ dayIndex, from: clippedFrom, to: clippedTo });\n    });\n    return out;\n}\n\n/**\n * Assign columns so overlapping events sit side by side instead of on top.\n *\n * Events are grouped into *clusters* of mutual overlap — a chain where each event\n * overlaps at least one other — and every event in a cluster is given the same\n * column count. That shared count is what makes the widths line up; assigning\n * columns per pair instead produces the ragged layout where two events claim half\n * the width and a third silently covers one of them.\n *\n * A column is reused as soon as it is free, so `9–10`, `9–10`, `10–11` needs two\n * columns rather than three.\n *\n * @param segments - Same-day segments, in any order.\n * @returns The same segments with `column` and `columns` filled in.\n */\nfunction assignColumns<T extends { from: number; to: number }>(\n    segments: T[],\n): (T & { column: number; columns: number })[] {\n    const sorted = [...segments].sort((a, b) => a.from - b.from || a.to - b.to);\n    const placed: (T & { column: number; columns: number })[] = [];\n\n    let cluster: (T & { column: number; columns: number })[] = [];\n    let clusterEnd = -Infinity;\n\n    /** Freeze the current cluster: everyone in it shares its column count. */\n    const closeCluster = (): void => {\n        const width = cluster.reduce((max, item) => Math.max(max, item.column + 1), 0);\n        for (const item of cluster) item.columns = width;\n        placed.push(...cluster);\n        cluster = [];\n        clusterEnd = -Infinity;\n    };\n\n    for (const segment of sorted) {\n        if (segment.from >= clusterEnd) closeCluster();\n\n        // Lowest column whose last event has already ended.\n        const taken = new Set(\n            cluster.filter((item) => item.to > segment.from).map((i) => i.column),\n        );\n        let column = 0;\n        while (taken.has(column)) column += 1;\n\n        cluster.push({ ...segment, column, columns: 1 });\n        clusterEnd = Math.max(clusterEnd, segment.to);\n    }\n    closeCluster();\n\n    return placed;\n}\n\n/**\n * Lay out timed events across the rendered days.\n *\n * All-day events are excluded — they belong in their own lane, where a vertical\n * position would be meaningless.\n *\n * @param params.events - Every event, all-day ones included.\n * @param params.days - Midnight of each rendered day.\n * @param params.window - Visible minute range.\n * @returns Placed events, positioned as fractions of the column height.\n */\nexport function layoutEvents({\n    events,\n    days,\n    window,\n}: {\n    events: readonly SchedulerEvent[];\n    days: Date[];\n    window: DayWindow;\n}): PlacedEvent[] {\n    const span = window.endMinute - window.startMinute;\n    if (span <= 0 || days.length === 0) return [];\n\n    const byDay = new Map<number, { event: SchedulerEvent; from: number; to: number }[]>();\n    for (const event of events) {\n        if (event.allDay) continue;\n        if (event.end.getTime() <= event.start.getTime()) continue;\n        for (const segment of segmentsFor(event, days, window)) {\n            const list = byDay.get(segment.dayIndex) ?? [];\n            list.push({ event, from: segment.from, to: segment.to });\n            byDay.set(segment.dayIndex, list);\n        }\n    }\n\n    const placed: PlacedEvent[] = [];\n    for (const [dayIndex, segments] of byDay) {\n        for (const item of assignColumns(segments)) {\n            placed.push({\n                event: item.event,\n                dayIndex,\n                top: (item.from - window.startMinute) / span,\n                height: (item.to - item.from) / span,\n                column: item.column,\n                columns: item.columns,\n            });\n        }\n    }\n    return placed;\n}\n\n/**\n * All-day events that intersect the rendered range, with their day span.\n *\n * @param params.events - Every event.\n * @param params.days - Midnight of each rendered day.\n * @returns One entry per all-day event, clipped to the visible days.\n */\nexport function layoutAllDay({\n    events,\n    days,\n}: {\n    events: readonly SchedulerEvent[];\n    days: Date[];\n}): { event: SchedulerEvent; dayIndex: number; span: number }[] {\n    if (days.length === 0) return [];\n    const rangeEnd = addDays(days[days.length - 1], 1).getTime();\n\n    return events\n        .filter((event) => event.allDay)\n        .filter(\n            (event) => event.end.getTime() > days[0].getTime() && event.start.getTime() < rangeEnd,\n        )\n        .map((event) => {\n            const firstIndex = days.findIndex(\n                (day) => addDays(day, 1).getTime() > event.start.getTime(),\n            );\n            const lastIndex = days.reduce(\n                (last, day, index) => (day.getTime() < event.end.getTime() ? index : last),\n                0,\n            );\n            const dayIndex = Math.max(0, firstIndex);\n            return { event, dayIndex, span: Math.max(1, lastIndex - dayIndex + 1) };\n        });\n}\n\n/**\n * Hour marks to label the time gutter with.\n *\n * @param window - Visible minute range.\n * @returns Each whole hour inside the window, in minutes from midnight.\n */\nexport function hourMarks(window: DayWindow): number[] {\n    const first = Math.ceil(window.startMinute / 60);\n    const last = Math.floor(window.endMinute / 60);\n    return Array.from({ length: Math.max(0, last - first + 1) }, (_, i) => (first + i) * 60);\n}\n\n/**\n * Where an instant falls within the window, as a fraction of the column height.\n *\n * @param date - The instant.\n * @param window - Visible minute range.\n * @returns A fraction in `0…1`, or `null` when outside the window.\n */\nexport function fractionOfWindow(date: Date, window: DayWindow): number | null {\n    const minute = minutesOfDay(date);\n    const span = window.endMinute - window.startMinute;\n    if (span <= 0 || minute < window.startMinute || minute > window.endMinute) return null;\n    return (minute - window.startMinute) / span;\n}\n"],"mappings":"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"}