{"version":3,"file":"layout.mjs","names":[],"sources":["../../../../package/common/calendar/layout.ts"],"sourcesContent":["import type { CalendarEvent } from './calendar.types';\nimport { addDays, dayDiff, endAfter, MINUTES_PER_DAY, minutesOfDay, shiftDays, startOfDay } from './dates';\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Below this an event has no room for a line of text at the default hour height. It is drawn as a\n * bar of its real length with its label beneath, and packed with room for that label.\n */\nexport const BRIEF_EVENT_MINUTES = 15;\n/** The label's line (`--label` in timeGrid.module.pcss) at the default 48px hour: 15px. */\nconst BRIEF_LABEL_MINUTES = 20;\n\n/** `end`, clamped so an event that ends before it starts is treated as having no length. */\nexport function eventEnd(event: CalendarEvent): Date {\n  return event.end < event.start ? event.start : event.end;\n}\n\n/** `event` moved by `days` calendar days, keeping its clock times and its real length. */\nexport function moveByDays(event: CalendarEvent, days: number): { start: Date; end: Date } {\n  const start = shiftDays(event.start, days);\n  const length = (eventEnd(event).getTime() - event.start.getTime()) / 60_000;\n  return { start, end: endAfter(start, shiftDays(eventEnd(event), days), length) };\n}\n\n/** Drawn in the all-day row rather than the time grid. */\nexport function isAllDayLike(event: CalendarEvent): boolean {\n  return Boolean(event.allDay) || eventEnd(event).getTime() - event.start.getTime() >= DAY_MS;\n}\n\n/** First and last day (both midnight, both inclusive) the event appears on. */\nexport function eventDays(event: CalendarEvent): { first: Date; last: Date } {\n  const first = startOfDay(event.start);\n  const end = eventEnd(event);\n  if (event.allDay) {\n    // `end` is exclusive, but an all-day event ending on its own start day still takes that day.\n    const endDay = startOfDay(end);\n    return { first, last: dayDiff(first, endDay) >= 1 ? addDays(endDay, -1) : first };\n  }\n  // One millisecond back, so a timed event ending at midnight does not appear on the next day.\n  const last = end > event.start ? startOfDay(new Date(end.getTime() - 1)) : first;\n  return { first, last };\n}\n\n/** All-day events first, then by start, then the longer first, so a day reads the same everywhere. */\nexport function compareEvents(a: CalendarEvent, b: CalendarEvent): number {\n  const allDay = Number(isAllDayLike(b)) - Number(isAllDayLike(a));\n  if (allDay !== 0) return allDay;\n  const start = a.start.getTime() - b.start.getTime();\n  if (start !== 0) return start;\n  const length = eventEnd(b).getTime() - eventEnd(a).getTime();\n  if (length !== 0) return length;\n  return a.title.localeCompare(b.title);\n}\n\n/** The events on each of `dates`, sorted with `compareEvents`. */\nexport function eventsByDay<E extends CalendarEvent>(events: readonly E[], dates: readonly Date[]): E[][] {\n  const result: E[][] = dates.map(() => []);\n  for (const event of events) {\n    const { first, last } = eventDays(event);\n    dates.forEach((day, i) => {\n      if (dayDiff(first, day) >= 0 && dayDiff(day, last) >= 0) result[i].push(event);\n    });\n  }\n  for (const day of result) day.sort(compareEvents);\n  return result;\n}\n\nexport interface TimedBox<E extends CalendarEvent = CalendarEvent> {\n  event: E;\n  /** Wall-clock minutes from the column's midnight. */\n  top: number;\n  bottom: number;\n  /** Where the next event in its column may start: below the label of a brief event. */\n  reach: number;\n  /** Left edge, as a column of `columns`. */\n  column: number;\n  columns: number;\n  /** Columns it can widen into to its right without covering another event. */\n  span: number;\n  continuesBefore: boolean;\n  continuesAfter: boolean;\n}\n\n/**\n * Lays out one day column of the time grid: events that overlap share the width in columns, and\n * each one widens right into columns that are free for its whole height.\n *\n * Positions are wall-clock minutes, never milliseconds from midnight. Drawing and packing have\n * to agree on that: on a 25-hour day two events in the repeated hour occupy the same rows, and\n * packing them by real time would put them in one column, drawn over each other.\n */\nexport function packDay<E extends CalendarEvent>(events: readonly E[], day: Date): TimedBox<E>[] {\n  const dayStart = startOfDay(day);\n  const next = addDays(dayStart, 1);\n\n  const boxes: TimedBox<E>[] = [];\n  for (const event of events) {\n    if (isAllDayLike(event)) continue;\n    const { start } = event;\n    const end = eventEnd(event);\n    if (start >= next) continue;\n    // A zero-length event at midnight belongs to the day it starts on.\n    if (end <= dayStart && start < dayStart) continue;\n\n    const continuesBefore = start < dayStart;\n    const continuesAfter = end > next;\n    const top = continuesBefore ? 0 : minutesOfDay(start);\n    // Never upside down: on the day the clocks go back, an event can end on an earlier clock time.\n    const bottom = Math.max(end >= next ? MINUTES_PER_DAY : minutesOfDay(end), top);\n    const reach = bottom - top < BRIEF_EVENT_MINUTES ? Math.min(bottom + BRIEF_LABEL_MINUTES, MINUTES_PER_DAY) : bottom;\n    boxes.push({ event, top, bottom, reach, column: 0, columns: 1, span: 1, continuesBefore, continuesAfter });\n  }\n\n  boxes.sort((a, b) => a.top - b.top || b.reach - a.reach);\n\n  let cluster: TimedBox<E>[] = [];\n  let clusterEnd = -1;\n  for (const box of boxes) {\n    if (cluster.length > 0 && box.top >= clusterEnd) {\n      packCluster(cluster);\n      cluster = [];\n    }\n    cluster.push(box);\n    clusterEnd = Math.max(clusterEnd, box.reach);\n  }\n  if (cluster.length > 0) packCluster(cluster);\n\n  return boxes;\n}\n\nfunction packCluster(cluster: TimedBox[]): void {\n  const columnBottoms: number[] = [];\n  for (const box of cluster) {\n    let column = columnBottoms.findIndex((bottom) => bottom <= box.top);\n    if (column === -1) column = columnBottoms.push(0) - 1;\n    box.column = column;\n    columnBottoms[column] = box.reach;\n  }\n  const columns = columnBottoms.length;\n  for (const box of cluster) {\n    box.columns = columns;\n    let span = 1;\n    for (let column = box.column + 1; column < columns; column++) {\n      const blocked = cluster.some(\n        (other) => other.column === column && other.top < box.reach && other.reach > box.top,\n      );\n      if (blocked) break;\n      span++;\n    }\n    box.span = span;\n  }\n}\n\nexport interface LaneBar<E extends CalendarEvent = CalendarEvent> {\n  event: E;\n  /** First column of the row it covers, 0-based. */\n  start: number;\n  /** Columns it covers within the row. */\n  span: number;\n  lane: number;\n  /** It starts before its first column: in an earlier row, or on a hidden day. */\n  continuesBefore: boolean;\n  continuesAfter: boolean;\n}\n\n/**\n * Stacks the events of one row of days into lanes, as the month view and the all-day row draw\n * them: longer bars first, so a multi-day event keeps one lane across the week. `dates` are the\n * row's columns, so a hidden day between two of them is simply not drawn.\n */\nexport function packLanes<E extends CalendarEvent>(\n  events: readonly E[],\n  dates: readonly Date[],\n  include: (event: E) => boolean = () => true,\n): LaneBar<E>[] {\n  const bars: LaneBar<E>[] = [];\n  for (const event of events) {\n    if (!include(event)) continue;\n    const { first, last } = eventDays(event);\n    const start = dates.findIndex((day) => dayDiff(first, day) >= 0 && dayDiff(day, last) >= 0);\n    if (start === -1) continue;\n    let end = start;\n    while (end + 1 < dates.length && dayDiff(dates[end + 1], last) >= 0) end++;\n    bars.push({\n      event,\n      start,\n      span: end - start + 1,\n      lane: 0,\n      continuesBefore: dayDiff(first, dates[start]) > 0,\n      continuesAfter: dayDiff(dates[end], last) > 0,\n    });\n  }\n\n  bars.sort((a, b) => a.start - b.start || b.span - a.span || compareEvents(a.event, b.event));\n\n  const laneEnds: number[] = [];\n  for (const bar of bars) {\n    let lane = laneEnds.findIndex((end) => end <= bar.start);\n    if (lane === -1) lane = laneEnds.push(0) - 1;\n    bar.lane = lane;\n    laneEnds[lane] = bar.start + bar.span;\n  }\n  return bars;\n}\n\nexport interface LimitedLanes<E extends CalendarEvent = CalendarEvent> {\n  visible: LaneBar<E>[];\n  /** Lane the `+N` buttons go in, on the days that have one. */\n  moreLane: number;\n  /** Events hidden on each day of the row. */\n  hidden: number[];\n}\n\n/**\n * Fits the bars into `capacity` lanes. A day that needs more gives its last lane to a `+N`\n * button; a day that does not keeps every bar. Decided per day, so one crowded day does not\n * hide events on the quiet days next to it. A bar crossing into a crowded day's `+N` lane is\n * hidden along its whole length and counted on each day it covers.\n */\nexport function limitLanes<E extends CalendarEvent>(\n  bars: LaneBar<E>[],\n  days: number,\n  capacity: number,\n): LimitedLanes<E> {\n  const moreLane = Math.max(capacity - 1, 0);\n  const lanesPerDay = Array.from({ length: days }, () => 0);\n  for (const bar of bars) {\n    for (let day = bar.start; day < bar.start + bar.span; day++) {\n      lanesPerDay[day] = Math.max(lanesPerDay[day], bar.lane + 1);\n    }\n  }\n\n  const hidden = Array.from({ length: days }, () => 0);\n  const visible: LaneBar<E>[] = [];\n  for (const bar of bars) {\n    let fits = bar.lane < moreLane;\n    if (!fits && bar.lane === moreLane) {\n      fits = true;\n      for (let day = bar.start; day < bar.start + bar.span; day++) {\n        if (lanesPerDay[day] > capacity) fits = false;\n      }\n    }\n    if (fits) {\n      visible.push(bar);\n      continue;\n    }\n    for (let day = bar.start; day < bar.start + bar.span; day++) hidden[day]++;\n  }\n  return { visible, moreLane, hidden };\n}\n"],"mappings":"oJAGA,IAAM,EAAS,MAQT,EAAsB,GAG5B,SAAgB,EAAS,EAA4B,CACnD,OAAO,EAAM,IAAM,EAAM,MAAQ,EAAM,MAAQ,EAAM,GACvD,CAGA,SAAgB,EAAW,EAAsB,EAA0C,CACzF,IAAM,EAAQ,EAAU,EAAM,MAAO,CAAI,EACnC,GAAU,EAAS,CAAK,CAAC,CAAC,QAAQ,EAAI,EAAM,MAAM,QAAQ,GAAK,IACrE,MAAO,CAAE,QAAO,IAAK,EAAS,EAAO,EAAU,EAAS,CAAK,EAAG,CAAI,EAAG,CAAM,CAAE,CACjF,CAGA,SAAgB,EAAa,EAA+B,CAC1D,MAAO,EAAQ,EAAM,QAAW,EAAS,CAAK,CAAC,CAAC,QAAQ,EAAI,EAAM,MAAM,QAAQ,GAAK,CACvF,CAGA,SAAgB,EAAU,EAAmD,CAC3E,IAAM,EAAQ,EAAW,EAAM,KAAK,EAC9B,EAAM,EAAS,CAAK,EAC1B,GAAI,EAAM,OAAQ,CAEhB,IAAM,EAAS,EAAW,CAAG,EAC7B,MAAO,CAAE,QAAO,KAAM,EAAQ,EAAO,CAAM,GAAK,EAAI,EAAQ,EAAQ,EAAE,EAAI,CAAM,CAClF,CAGA,MAAO,CAAE,QAAO,KADH,EAAM,EAAM,MAAQ,EAAW,IAAI,KAAK,EAAI,QAAQ,EAAI,CAAC,CAAC,EAAI,CACtD,CACvB,CAGA,SAAgB,EAAc,EAAkB,EAA0B,CACxE,IAAM,EAAS,OAAO,EAAa,CAAC,CAAC,EAAI,OAAO,EAAa,CAAC,CAAC,EAC/D,GAAI,IAAW,EAAG,OAAO,EACzB,IAAM,EAAQ,EAAE,MAAM,QAAQ,EAAI,EAAE,MAAM,QAAQ,EAClD,GAAI,IAAU,EAAG,OAAO,EACxB,IAAM,EAAS,EAAS,CAAC,CAAC,CAAC,QAAQ,EAAI,EAAS,CAAC,CAAC,CAAC,QAAQ,EAE3D,OADI,IAAW,EACR,EAAE,MAAM,cAAc,EAAE,KAAK,EADX,CAE3B,CAGA,SAAgB,EAAqC,EAAsB,EAA+B,CACxG,IAAM,EAAgB,EAAM,QAAU,CAAC,CAAC,EACxC,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAM,CAAE,QAAO,QAAS,EAAU,CAAK,EACvC,EAAM,SAAS,EAAK,IAAM,CACpB,EAAQ,EAAO,CAAG,GAAK,GAAK,EAAQ,EAAK,CAAI,GAAK,GAAG,EAAO,EAAE,CAAC,KAAK,CAAK,CAC/E,CAAC,CACH,CACA,IAAK,IAAM,KAAO,EAAQ,EAAI,KAAK,CAAa,EAChD,OAAO,CACT,CA0BA,SAAgB,EAAiC,EAAsB,EAA0B,CAC/F,IAAM,EAAW,EAAW,CAAG,EACzB,EAAO,EAAQ,EAAU,CAAC,EAE1B,EAAuB,CAAC,EAC9B,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAI,EAAa,CAAK,EAAG,SACzB,GAAM,CAAE,SAAU,EACZ,EAAM,EAAS,CAAK,EAG1B,GAFI,GAAS,GAET,GAAO,GAAY,EAAQ,EAAU,SAEzC,IAAM,EAAkB,EAAQ,EAC1B,EAAiB,EAAM,EACvB,EAAM,EAAkB,EAAI,EAAa,CAAK,EAE9C,EAAS,KAAK,IAAI,GAAO,EAAO,EAAkB,EAAa,CAAG,EAAG,CAAG,EACxE,EAAQ,EAAS,EAAA,GAA4B,KAAK,IAAI,EAAS,EAAqB,CAAe,EAAI,EAC7G,EAAM,KAAK,CAAE,QAAO,MAAK,SAAQ,QAAO,OAAQ,EAAG,QAAS,EAAG,KAAM,EAAG,kBAAiB,gBAAe,CAAC,CAC3G,CAEA,EAAM,MAAM,EAAG,IAAM,EAAE,IAAM,EAAE,KAAO,EAAE,MAAQ,EAAE,KAAK,EAEvD,IAAI,EAAyB,CAAC,EAC1B,EAAa,GACjB,IAAK,IAAM,KAAO,EACZ,EAAQ,OAAS,GAAK,EAAI,KAAO,IACnC,EAAY,CAAO,EACnB,EAAU,CAAC,GAEb,EAAQ,KAAK,CAAG,EAChB,EAAa,KAAK,IAAI,EAAY,EAAI,KAAK,EAI7C,OAFI,EAAQ,OAAS,GAAG,EAAY,CAAO,EAEpC,CACT,CAEA,SAAS,EAAY,EAA2B,CAC9C,IAAM,EAA0B,CAAC,EACjC,IAAK,IAAM,KAAO,EAAS,CACzB,IAAI,EAAS,EAAc,UAAW,GAAW,GAAU,EAAI,GAAG,EAC9D,IAAW,KAAI,EAAS,EAAc,KAAK,CAAC,EAAI,GACpD,EAAI,OAAS,EACb,EAAc,GAAU,EAAI,KAC9B,CACA,IAAM,EAAU,EAAc,OAC9B,IAAK,IAAM,KAAO,EAAS,CACzB,EAAI,QAAU,EACd,IAAI,EAAO,EACX,IAAK,IAAI,EAAS,EAAI,OAAS,EAAG,EAAS,GACzB,GAAQ,KACrB,GAAU,EAAM,SAAW,GAAU,EAAM,IAAM,EAAI,OAAS,EAAM,MAAQ,EAAI,GAE/E,EAJ8C,IAKlD,IAEF,EAAI,KAAO,CACb,CACF,CAmBA,SAAgB,EACd,EACA,EACA,MAAuC,GACzB,CACd,IAAM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAI,CAAC,EAAQ,CAAK,EAAG,SACrB,GAAM,CAAE,QAAO,QAAS,EAAU,CAAK,EACjC,EAAQ,EAAM,UAAW,GAAQ,EAAQ,EAAO,CAAG,GAAK,GAAK,EAAQ,EAAK,CAAI,GAAK,CAAC,EAC1F,GAAI,IAAU,GAAI,SAClB,IAAI,EAAM,EACV,KAAO,EAAM,EAAI,EAAM,QAAU,EAAQ,EAAM,EAAM,GAAI,CAAI,GAAK,GAAG,IACrE,EAAK,KAAK,CACR,QACA,QACA,KAAM,EAAM,EAAQ,EACpB,KAAM,EACN,gBAAiB,EAAQ,EAAO,EAAM,EAAM,EAAI,EAChD,eAAgB,EAAQ,EAAM,GAAM,CAAI,EAAI,CAC9C,CAAC,CACH,CAEA,EAAK,MAAM,EAAG,IAAM,EAAE,MAAQ,EAAE,OAAS,EAAE,KAAO,EAAE,MAAQ,EAAc,EAAE,MAAO,EAAE,KAAK,CAAC,EAE3F,IAAM,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAO,EAAM,CACtB,IAAI,EAAO,EAAS,UAAW,GAAQ,GAAO,EAAI,KAAK,EACnD,IAAS,KAAI,EAAO,EAAS,KAAK,CAAC,EAAI,GAC3C,EAAI,KAAO,EACX,EAAS,GAAQ,EAAI,MAAQ,EAAI,IACnC,CACA,OAAO,CACT,CAgBA,SAAgB,EACd,EACA,EACA,EACiB,CACjB,IAAM,EAAW,KAAK,IAAI,EAAW,EAAG,CAAC,EACnC,EAAc,MAAM,KAAK,CAAE,OAAQ,CAAK,MAAS,CAAC,EACxD,IAAK,IAAM,KAAO,EAChB,IAAK,IAAI,EAAM,EAAI,MAAO,EAAM,EAAI,MAAQ,EAAI,KAAM,IACpD,EAAY,GAAO,KAAK,IAAI,EAAY,GAAM,EAAI,KAAO,CAAC,EAI9D,IAAM,EAAS,MAAM,KAAK,CAAE,OAAQ,CAAK,MAAS,CAAC,EAC7C,EAAwB,CAAC,EAC/B,IAAK,IAAM,KAAO,EAAM,CACtB,IAAI,EAAO,EAAI,KAAO,EACtB,GAAI,CAAC,GAAQ,EAAI,OAAS,EAAU,CAClC,EAAO,GACP,IAAK,IAAI,EAAM,EAAI,MAAO,EAAM,EAAI,MAAQ,EAAI,KAAM,IAChD,EAAY,GAAO,IAAU,EAAO,GAE5C,CACA,GAAI,EAAM,CACR,EAAQ,KAAK,CAAG,EAChB,QACF,CACA,IAAK,IAAI,EAAM,EAAI,MAAO,EAAM,EAAI,MAAQ,EAAI,KAAM,IAAO,EAAO,EAAI,EAC1E,CACA,MAAO,CAAE,UAAS,WAAU,QAAO,CACrC"}