@ahrowe/ui 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cjs-types/common/kanbanBoard/KanbanBoard.d.ts +1 -1
  3. package/dist/cjs-types/common/kanbanBoard/KanbanColumn.d.ts +21 -12
  4. package/dist/cjs-types/common/kanbanBoard/KanbanItem.d.ts +1 -1
  5. package/dist/cjs-types/common/kanbanBoard/kanbanBoard.types.d.ts +23 -17
  6. package/dist/cjs-types/common/kanbanBoard/kanbanBoard.utils.d.ts +47 -1
  7. package/dist/esm/common/kanbanBoard/KanbanBoard.mjs +3 -2
  8. package/dist/esm/common/kanbanBoard/KanbanBoard.mjs.map +1 -1
  9. package/dist/esm/common/kanbanBoard/KanbanColumn.mjs +1 -1
  10. package/dist/esm/common/kanbanBoard/KanbanColumn.mjs.map +1 -1
  11. package/dist/esm/common/kanbanBoard/KanbanItem.mjs +1 -1
  12. package/dist/esm/common/kanbanBoard/KanbanItem.mjs.map +1 -1
  13. package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs +1 -1
  14. package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs.map +1 -1
  15. package/dist/esm/common/kanbanBoard/kanbanBoard.utils.mjs +1 -1
  16. package/dist/esm/common/kanbanBoard/kanbanBoard.utils.mjs.map +1 -1
  17. package/dist/index.cjs +3 -2
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/style.css +1 -1
  20. package/dist/types/common/kanbanBoard/KanbanBoard.d.ts +1 -1
  21. package/dist/types/common/kanbanBoard/KanbanColumn.d.ts +21 -12
  22. package/dist/types/common/kanbanBoard/KanbanItem.d.ts +1 -1
  23. package/dist/types/common/kanbanBoard/kanbanBoard.types.d.ts +23 -17
  24. package/dist/types/common/kanbanBoard/kanbanBoard.utils.d.ts +47 -1
  25. package/docs/KanbanBoard.md +36 -18
  26. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"kanbanBoard.utils.mjs","names":[],"sources":["../../../../package/common/kanbanBoard/kanbanBoard.utils.ts"],"sourcesContent":["import { arrayMove, hasSortableData } from '@dnd-kit/sortable';\nimport {\n closestCorners,\n KeyboardCode,\n type Announcements,\n type ClientRect,\n type CollisionDetection,\n type DroppableContainer,\n type KeyboardCoordinateGetter,\n type UniqueIdentifier,\n} from '@dnd-kit/core';\n\nimport type { KanbanColumnDef } from './kanbanBoard.types';\n\nexport function columnsEqual(a: KanbanColumnDef[], b: KanbanColumnDef[]): boolean {\n if (a.length !== b.length) return false;\n return a.every((col, i) => {\n const other = b[i];\n return (\n col.id === other.id &&\n col.itemIds.length === other.itemIds.length &&\n col.itemIds.every((itemId, j) => itemId === other.itemIds[j])\n );\n });\n}\n\nexport function findColumnForItem(columns: KanbanColumnDef[], itemId: string): number {\n return columns.findIndex((col) => col.itemIds.includes(itemId));\n}\n\n/** Whether a drag's `active`/`over` participant is a whole column (vs. a card) — see `reorderableColumns`. */\nexport function isColumnDragData(data: unknown): boolean {\n return Boolean(data && typeof data === 'object' && (data as { type?: string }).type === 'column');\n}\n\n/**\n * Reorders whole columns: moves `activeColumnId` to `overColumnId`'s position. Returns the same\n * `columns` reference, unchanged, when either id doesn't resolve to a column or they're the same\n * — callers can use that to skip a state update.\n */\nexport function moveColumn(\n columns: KanbanColumnDef[],\n activeColumnId: string,\n overColumnId: string,\n): KanbanColumnDef[] {\n const oldIndex = columns.findIndex((col) => col.id === activeColumnId);\n const overIndex = columns.findIndex((col) => col.id === overColumnId);\n if (oldIndex === -1 || overIndex === -1 || oldIndex === overIndex) return columns;\n\n return arrayMove(columns, oldIndex, overIndex);\n}\n\n/**\n * Collision detection for the board's shared `DndContext`, which has to pick between two very\n * differently-scaled sets of droppables at once: individual cards / a column's own card list\n * (small, numerous) and whole columns (large, few). Plain `closestCorners` weighs every\n * registered droppable together regardless of what's actually being dragged, so while dragging a\n * column it competes against every card and card-list droppable it happens to fly over too — the\n * column-level target only wins when the pointer lands somewhere that geometrically favors it,\n * which reads as needing to \"wiggle it around\" before the right drop target lights up. This\n * restricts the candidates to only the same kind as whatever is being dragged: a column drag only\n * considers other columns, a card drag only considers cards and card lists.\n */\nexport const kanbanCollisionDetection: CollisionDetection = (args) => {\n const draggingColumn = isColumnDragData(args.active.data.current);\n const droppableContainers = args.droppableContainers.filter((container) =>\n draggingColumn ? isColumnDragData(container.data.current) : !isColumnDragData(container.data.current),\n );\n\n return closestCorners({ ...args, droppableContainers });\n};\n\n/** Extracts the column id from a column droppable's `column-<id>` identifier, or `null` for a card id. */\nexport function resolveColumnDropId(overId: string): string | null {\n return overId.startsWith('column-') ? overId.slice('column-'.length) : null;\n}\n\nexport interface DragGeometry {\n /** The dragged card's current rect (translated by the drag delta), if measured. */\n activeRect: ClientRect | null;\n /** The rect of whatever `overId` refers to — a card, or a column's own droppable. */\n overRect: ClientRect;\n}\n\n/**\n * Computes the columns that result from dragging `activeItemId` over `overId` (a card id, or a\n * `column-<id>` droppable id for empty column space). Returns the same `columns` reference,\n * unchanged, when the drag doesn't resolve to a valid source/destination — callers can use that\n * to skip a state update.\n *\n * `geometry` resolves cross-column drops that land on a card or gap with more than one possible\n * position (see the two branches below for what each one decides).\n */\nexport function moveItem(\n columns: KanbanColumnDef[],\n activeItemId: string,\n overId: string,\n geometry: DragGeometry,\n): KanbanColumnDef[] {\n // Only the source/destination column(s) actually change, so only those get new object/array\n // references — every other column keeps its exact `columns` reference. Cloning every column's\n // `itemIds` up front (as this used to) cost O(total cards across the whole board) on *every*\n // drag-over event, not just the couple of columns actually involved in the move.\n const sourceColIndex = findColumnForItem(columns, activeItemId);\n if (sourceColIndex === -1) return columns;\n\n const destColId = resolveColumnDropId(overId);\n const destColIndex =\n destColId !== null ? columns.findIndex((col) => col.id === destColId) : findColumnForItem(columns, overId);\n if (destColIndex === -1) return columns;\n\n if (sourceColIndex === destColIndex) {\n const col = columns[sourceColIndex];\n const oldIndex = col.itemIds.indexOf(activeItemId);\n if (oldIndex === -1) return columns;\n\n if (destColId !== null) {\n // over is the column's own droppable, not a card: nothing else claimed the collision,\n // which happens hovering the empty gap above/below the cards, or — critically — an edge\n // next to a disabled/pinned card with no other card nearby, since a disabled card's own\n // droppable is excluded from collision candidates entirely. Without this branch, that\n // case fell through to `col.itemIds.indexOf(overId)` below, which is always -1 for a\n // `column-<id>` id (it's not a card id), silently no-op-ing instead of reordering.\n const { activeRect, overRect } = geometry;\n const isBelowCenter = Boolean(\n activeRect && activeRect.top + activeRect.height / 2 > overRect.top + overRect.height / 2,\n );\n const targetIndex = isBelowCenter ? col.itemIds.length - 1 : 0;\n if (targetIndex === oldIndex) return columns;\n\n const next = [...columns];\n next[sourceColIndex] = { ...col, itemIds: arrayMove(col.itemIds, oldIndex, targetIndex) };\n return next;\n }\n\n const newIndex = col.itemIds.indexOf(overId);\n if (newIndex === -1 || oldIndex === newIndex) return columns;\n\n const next = [...columns];\n next[sourceColIndex] = { ...col, itemIds: arrayMove(col.itemIds, oldIndex, newIndex) };\n return next;\n }\n\n const sourceCol = columns[sourceColIndex];\n const destCol = columns[destColIndex];\n const destItemIds = [...destCol.itemIds];\n const { activeRect, overRect } = geometry;\n\n let insertAt: number;\n if (destColId !== null) {\n // Hovering the column's own droppable directly, rather than a specific card: the empty gap\n // above the first card, the empty gap below the last card, or the whole area when the column\n // has no cards at all. Comparing centers against the *container's* full-height rect is a\n // coarse but reliable way to tell the top gap (insert at the start) from the bottom gap\n // (append at the end) — treating every container hover as \"append\" put a card dropped just\n // above the first item at the end of the column instead.\n const isBelowCenter = Boolean(\n activeRect && activeRect.top + activeRect.height / 2 > overRect.top + overRect.height / 2,\n );\n insertAt = isBelowCenter ? destItemIds.length : 0;\n } else {\n const overIndex = destItemIds.indexOf(overId);\n\n // Only the last card's own drop zone extends past its bottom edge with nothing after it to\n // compete for the collision, so it's the one case where the dragged card can end up \"after\"\n // it. Requiring the dragged rect's *center* (not its top edge) to pass the hovered card's\n // bottom edge is deliberately more forgiving than requiring the whole dragged card to clear it\n // — that stricter version needs the user to drag a full card's height past the last item\n // before it registers, which in practice almost never happens before the drop, silently\n // landing the card second-to-last every time despite the live preview showing it last. A pure\n // center-to-center comparison is too lenient in the other direction: the dragged rect's center\n // still carries the grab offset from wherever in the source card it was originally picked up,\n // so a low enough grab point reads as \"past\" well before the drag is visually past the card at\n // all. Anchoring to the target's *bottom* edge (not its center) keeps that grab-offset problem\n // from creeping back in while still being achievable in a normal drag.\n const isLastItem = overIndex !== -1 && overIndex === destItemIds.length - 1;\n const hasClearedItem = Boolean(\n activeRect && activeRect.top + activeRect.height / 2 > overRect.top + overRect.height,\n );\n insertAt = overIndex === -1 ? destItemIds.length : isLastItem && hasClearedItem ? overIndex + 1 : overIndex;\n }\n\n destItemIds.splice(insertAt, 0, activeItemId);\n\n const next = [...columns];\n next[sourceColIndex] = { ...sourceCol, itemIds: sourceCol.itemIds.filter((id) => id !== activeItemId) };\n next[destColIndex] = { ...destCol, itemIds: destItemIds };\n return next;\n}\n\n/**\n * Builds screen-reader drag announcements against the columns layout as currently understood\n * (the live shadow layout while a drag is in progress, or the settled `columns` prop otherwise).\n */\nexport function buildAnnouncements<T>(\n columns: KanbanColumnDef[],\n items: Record<string, T>,\n getItemLabel: (item: T, id: string) => string,\n): Announcements {\n function labelFor(id: string): string {\n const item = items[id];\n return item !== undefined ? getItemLabel(item, id) : id;\n }\n\n function describeOver(overId: string): string {\n const destColId = resolveColumnDropId(overId);\n if (destColId !== null) {\n const col = columns.find((c) => c.id === destColId);\n return col ? `the ${col.title} column` : 'a column';\n }\n\n const col = columns.find((c) => c.itemIds.includes(overId));\n return col ? `${labelFor(overId)}, in the ${col.title} column` : labelFor(overId);\n }\n\n function columnTitle(id: string): string {\n return columns.find((c) => c.id === id)?.title ?? id;\n }\n\n return {\n onDragStart({ active }) {\n if (isColumnDragData(active.data.current)) return `Picked up column ${columnTitle(String(active.id))}.`;\n return `Picked up card ${labelFor(String(active.id))}.`;\n },\n onDragOver({ active, over }) {\n if (isColumnDragData(active.data.current)) {\n if (!over) return `Column ${columnTitle(String(active.id))} is no longer over a droppable area.`;\n return `Column ${columnTitle(String(active.id))} is over column ${columnTitle(String(over.id))}.`;\n }\n if (!over) return `Card ${labelFor(String(active.id))} is no longer over a droppable area.`;\n return `Card ${labelFor(String(active.id))} is over ${describeOver(String(over.id))}.`;\n },\n onDragEnd({ active, over }) {\n if (isColumnDragData(active.data.current)) {\n if (!over) return `Column ${columnTitle(String(active.id))} was dropped.`;\n return `Column ${columnTitle(String(active.id))} was dropped over column ${columnTitle(String(over.id))}.`;\n }\n if (!over) return `Card ${labelFor(String(active.id))} was dropped.`;\n return `Card ${labelFor(String(active.id))} was dropped over ${describeOver(String(over.id))}.`;\n },\n onDragCancel({ active }) {\n if (isColumnDragData(active.data.current)) {\n return `Dragging cancelled. Column ${columnTitle(String(active.id))} was returned to its original position.`;\n }\n return `Dragging cancelled. Card ${labelFor(String(active.id))} was returned to its original position.`;\n },\n };\n}\n\ninterface ContainerRect {\n container: DroppableContainer;\n rect: ClientRect;\n}\n\nfunction collectColumnItemRects(\n containers: DroppableContainer[],\n droppableRects: Map<UniqueIdentifier, ClientRect>,\n containerId: UniqueIdentifier,\n): ContainerRect[] {\n const entries: ContainerRect[] = [];\n for (const container of containers) {\n if (!hasSortableData(container)) continue;\n if (container.data.current.sortable.containerId !== containerId) continue;\n const rect = droppableRects.get(container.id);\n if (!rect) continue;\n entries.push({ container, rect });\n }\n return entries.sort((a, b) => a.rect.top - b.rect.top);\n}\n\n/**\n * Custom keyboard coordinate getter for the board's multi-column, multi-`SortableContext`\n * layout. dnd-kit's default `sortableKeyboardCoordinates` picks the geometrically closest\n * droppable across the *entire* board — cards and column containers alike — which is unreliable\n * once cards live inside independent per-column `SortableContext`s: up/down and left/right end up\n * interchangeable depending on incidental rect positions. This instead restricts up/down to\n * reordering within the active card's own column, and left/right to moving into the adjacent\n * column (landing on the closest card there by vertical position, or the column itself when\n * empty). Requires each `KanbanColumn`'s `SortableContext` to be given an explicit `id` matching\n * its column id, so a card's `sortable.containerId` can be correlated with its `column-<id>`\n * droppable.\n */\nexport const kanbanKeyboardCoordinateGetter: KeyboardCoordinateGetter = (event, { context }) => {\n const { active, collisionRect, droppableRects, droppableContainers } = context;\n\n if (!active || !collisionRect || !hasSortableData(active)) return undefined;\n\n const isVertical = event.code === KeyboardCode.Down || event.code === KeyboardCode.Up;\n const isHorizontal = event.code === KeyboardCode.Left || event.code === KeyboardCode.Right;\n if (!isVertical && !isHorizontal) return undefined;\n\n event.preventDefault();\n\n const activeContainerId = active.data.current.sortable.containerId;\n const allContainers = droppableContainers.getEnabled();\n\n if (isColumnDragData(active.data.current)) {\n // Reordering a whole column: only left/right matters, moving it within the top-level\n // column `SortableContext` (siblings share its containerId, same as cards share their\n // own column's). Up/down has nothing to do — columns don't have a \"column above/below\".\n if (!isHorizontal) return undefined;\n\n const direction = event.code === KeyboardCode.Right ? 1 : -1;\n const siblings = collectColumnItemRects(allContainers, droppableRects, activeContainerId).sort(\n (a, b) => a.rect.left - b.rect.left,\n );\n const currentIndex = siblings.findIndex((s) => s.container.id === active.id);\n if (currentIndex === -1) return undefined;\n\n const target = siblings[currentIndex + direction];\n if (!target) return undefined;\n\n return { x: target.rect.left, y: collisionRect.top };\n }\n\n if (isVertical) {\n const direction = event.code === KeyboardCode.Down ? 1 : -1;\n const siblings = collectColumnItemRects(allContainers, droppableRects, activeContainerId);\n const currentIndex = siblings.findIndex((s) => s.container.id === active.id);\n if (currentIndex === -1) return undefined;\n\n const target = siblings[currentIndex + direction];\n if (!target) return undefined;\n\n return { x: collisionRect.left, y: target.rect.top };\n }\n\n const direction = event.code === KeyboardCode.Right ? 1 : -1;\n\n const columns: ContainerRect[] = [];\n for (const container of allContainers) {\n const columnId = resolveColumnDropId(String(container.id));\n if (columnId === null) continue;\n const rect = droppableRects.get(container.id);\n if (!rect) continue;\n columns.push({ container, rect });\n }\n columns.sort((a, b) => a.rect.left - b.rect.left);\n\n const activeColumnIndex = columns.findIndex((c) => resolveColumnDropId(String(c.container.id)) === activeContainerId);\n if (activeColumnIndex === -1) return undefined;\n\n const targetColumn = columns[activeColumnIndex + direction];\n if (!targetColumn) return undefined;\n\n const targetContainerId = resolveColumnDropId(String(targetColumn.container.id)) as UniqueIdentifier;\n const targetItems = collectColumnItemRects(allContainers, droppableRects, targetContainerId);\n\n let targetRect = targetColumn.rect;\n if (targetItems.length > 0) {\n targetRect = targetItems.reduce((closest, item) =>\n Math.abs(item.rect.top - collisionRect.top) < Math.abs(closest.rect.top - collisionRect.top) ? item : closest,\n ).rect;\n }\n\n return { x: targetRect.left, y: targetRect.top };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"kanbanBoard.utils.mjs","names":[],"sources":["../../../../package/common/kanbanBoard/kanbanBoard.utils.ts"],"sourcesContent":["import { arrayMove, hasSortableData } from '@dnd-kit/sortable';\nimport {\n closestCorners,\n KeyboardCode,\n type Announcements,\n type ClientRect,\n type CollisionDetection,\n type DroppableContainer,\n type KeyboardCoordinateGetter,\n type UniqueIdentifier,\n} from '@dnd-kit/core';\n\nimport type { KanbanColumnDef } from './kanbanBoard.types';\n\nexport function columnsEqual(a: KanbanColumnDef[], b: KanbanColumnDef[]): boolean {\n if (a.length !== b.length) return false;\n return a.every((col, i) => {\n const other = b[i];\n return (\n col.id === other.id &&\n col.itemIds.length === other.itemIds.length &&\n col.itemIds.every((itemId, j) => itemId === other.itemIds[j])\n );\n });\n}\n\nexport function findColumnForItem(columns: KanbanColumnDef[], itemId: string): number {\n return columns.findIndex((col) => col.itemIds.includes(itemId));\n}\n\n/** Whether a drag's `active`/`over` participant is a whole column (vs. a card) — see `reorderableColumns`. */\nexport function isColumnDragData(data: unknown): boolean {\n return Boolean(data && typeof data === 'object' && (data as { type?: string }).type === 'column');\n}\n\n/**\n * Reorders whole columns: moves `activeColumnId` to `overColumnId`'s position. Returns the same\n * `columns` reference, unchanged, when either id doesn't resolve to a column or they're the same\n * — callers can use that to skip a state update.\n */\nexport function moveColumn(\n columns: KanbanColumnDef[],\n activeColumnId: string,\n overColumnId: string,\n): KanbanColumnDef[] {\n const oldIndex = columns.findIndex((col) => col.id === activeColumnId);\n const overIndex = columns.findIndex((col) => col.id === overColumnId);\n if (oldIndex === -1 || overIndex === -1 || oldIndex === overIndex) return columns;\n\n return arrayMove(columns, oldIndex, overIndex);\n}\n\n/**\n * Collision detection for the board's shared `DndContext`, which has to pick between two very\n * differently-scaled sets of droppables at once: individual cards / a column's own card list\n * (small, numerous) and whole columns (large, few). Plain `closestCorners` weighs every\n * registered droppable together regardless of what's actually being dragged, so while dragging a\n * column it competes against every card and card-list droppable it happens to fly over too — the\n * column-level target only wins when the pointer lands somewhere that geometrically favors it,\n * which reads as needing to \"wiggle it around\" before the right drop target lights up. This\n * restricts the candidates to only the same kind as whatever is being dragged: a column drag only\n * considers other columns, a card drag only considers cards and card lists.\n *\n * A card drag also lets the pointer pick the column, and `closestCorners` only the position within\n * it. Corners alone compare the dragged card's whole rect, so a wide card over a collapsed column\n * had its corners nearer the neighbours' cards, and landed beside the column under the pointer.\n * Without a pointer (the keyboard) or between columns, every column is a candidate as before.\n */\nexport const kanbanCollisionDetection: CollisionDetection = (args) => {\n const draggingColumn = isColumnDragData(args.active.data.current);\n const droppableContainers = args.droppableContainers.filter((container) =>\n draggingColumn ? isColumnDragData(container.data.current) : !isColumnDragData(container.data.current),\n );\n const pointerX = args.pointerCoordinates?.x;\n const lane =\n draggingColumn || pointerX === undefined\n ? undefined\n : droppableContainers.find((container) => {\n const rect = args.droppableRects.get(container.id);\n return (\n resolveColumnDropId(String(container.id)) !== null &&\n rect &&\n rect.left <= pointerX &&\n pointerX <= rect.right\n );\n });\n if (!lane) return closestCorners({ ...args, droppableContainers });\n\n const laneColumnId = resolveColumnDropId(String(lane.id));\n return closestCorners({\n ...args,\n droppableContainers: droppableContainers.filter(\n (container) =>\n container === lane ||\n (hasSortableData(container) && container.data.current.sortable.containerId === laneColumnId),\n ),\n });\n};\n\n/**\n * Whether the auto-scroll may move `scroller`, an ancestor of the board such as the page, while\n * something is dragged: only toward a part of the board that is out of view. Nothing beyond the\n * board takes a drop, and a page left free to scroll carried the whole board off screen.\n */\nexport function scrollsTowardBoard(\n scroller: { top: number; bottom: number },\n board: { top: number; bottom: number },\n dragged: { top: number; height: number },\n): boolean {\n const goingDown = dragged.top + dragged.height / 2 > (scroller.top + scroller.bottom) / 2;\n return goingDown ? board.bottom > scroller.bottom + 1 : board.top < scroller.top - 1;\n}\n\n/** Extracts the column id from a column droppable's `column-<id>` identifier, or `null` for a card id. */\nexport function resolveColumnDropId(overId: string): string | null {\n return overId.startsWith('column-') ? overId.slice('column-'.length) : null;\n}\n\nexport interface DragGeometry {\n /** The dragged card's current rect (translated by the drag delta), if measured. */\n activeRect: ClientRect | null;\n /** The rect of whatever `overId` refers to — a card, or a column's own droppable. */\n overRect: ClientRect;\n}\n\n/**\n * Computes the columns that result from dragging `activeItemId` over `overId` (a card id, or a\n * `column-<id>` droppable id for empty column space). Returns the same `columns` reference,\n * unchanged, when the drag doesn't resolve to a valid source/destination — callers can use that\n * to skip a state update.\n *\n * `geometry` resolves cross-column drops that land on a card or gap with more than one possible\n * position (see the two branches below for what each one decides).\n */\nexport function moveItem(\n columns: KanbanColumnDef[],\n activeItemId: string,\n overId: string,\n geometry: DragGeometry,\n): KanbanColumnDef[] {\n // Only the source/destination column(s) actually change, so only those get new object/array\n // references — every other column keeps its exact `columns` reference. Cloning every column's\n // `itemIds` up front (as this used to) cost O(total cards across the whole board) on *every*\n // drag-over event, not just the couple of columns actually involved in the move.\n const sourceColIndex = findColumnForItem(columns, activeItemId);\n if (sourceColIndex === -1) return columns;\n\n const destColId = resolveColumnDropId(overId);\n const destColIndex =\n destColId !== null ? columns.findIndex((col) => col.id === destColId) : findColumnForItem(columns, overId);\n if (destColIndex === -1) return columns;\n\n if (sourceColIndex === destColIndex) {\n const col = columns[sourceColIndex];\n const oldIndex = col.itemIds.indexOf(activeItemId);\n if (oldIndex === -1) return columns;\n\n if (destColId !== null) {\n // over is the column's own droppable, not a card: nothing else claimed the collision,\n // which happens hovering the empty gap above/below the cards, or — critically — an edge\n // next to a disabled/pinned card with no other card nearby, since a disabled card's own\n // droppable is excluded from collision candidates entirely. Without this branch, that\n // case fell through to `col.itemIds.indexOf(overId)` below, which is always -1 for a\n // `column-<id>` id (it's not a card id), silently no-op-ing instead of reordering.\n const { activeRect, overRect } = geometry;\n const isBelowCenter = Boolean(\n activeRect && activeRect.top + activeRect.height / 2 > overRect.top + overRect.height / 2,\n );\n const targetIndex = isBelowCenter ? col.itemIds.length - 1 : 0;\n if (targetIndex === oldIndex) return columns;\n\n const next = [...columns];\n next[sourceColIndex] = { ...col, itemIds: arrayMove(col.itemIds, oldIndex, targetIndex) };\n return next;\n }\n\n const newIndex = col.itemIds.indexOf(overId);\n if (newIndex === -1 || oldIndex === newIndex) return columns;\n\n const next = [...columns];\n next[sourceColIndex] = { ...col, itemIds: arrayMove(col.itemIds, oldIndex, newIndex) };\n return next;\n }\n\n const sourceCol = columns[sourceColIndex];\n const destCol = columns[destColIndex];\n const destItemIds = [...destCol.itemIds];\n const { activeRect, overRect } = geometry;\n\n let insertAt: number;\n if (destColId !== null) {\n // Hovering the column's own droppable directly, rather than a specific card: the empty gap\n // above the first card, the empty gap below the last card, or the whole area when the column\n // has no cards at all. Comparing centers against the *container's* full-height rect is a\n // coarse but reliable way to tell the top gap (insert at the start) from the bottom gap\n // (append at the end) — treating every container hover as \"append\" put a card dropped just\n // above the first item at the end of the column instead.\n const isBelowCenter = Boolean(\n activeRect && activeRect.top + activeRect.height / 2 > overRect.top + overRect.height / 2,\n );\n insertAt = isBelowCenter ? destItemIds.length : 0;\n } else {\n const overIndex = destItemIds.indexOf(overId);\n\n // Only the last card's own drop zone extends past its bottom edge with nothing after it to\n // compete for the collision, so it's the one case where the dragged card can end up \"after\"\n // it. Requiring the dragged rect's *center* (not its top edge) to pass the hovered card's\n // bottom edge is deliberately more forgiving than requiring the whole dragged card to clear it\n // — that stricter version needs the user to drag a full card's height past the last item\n // before it registers, which in practice almost never happens before the drop, silently\n // landing the card second-to-last every time despite the live preview showing it last. A pure\n // center-to-center comparison is too lenient in the other direction: the dragged rect's center\n // still carries the grab offset from wherever in the source card it was originally picked up,\n // so a low enough grab point reads as \"past\" well before the drag is visually past the card at\n // all. Anchoring to the target's *bottom* edge (not its center) keeps that grab-offset problem\n // from creeping back in while still being achievable in a normal drag.\n const isLastItem = overIndex !== -1 && overIndex === destItemIds.length - 1;\n const hasClearedItem = Boolean(\n activeRect && activeRect.top + activeRect.height / 2 > overRect.top + overRect.height,\n );\n insertAt = overIndex === -1 ? destItemIds.length : isLastItem && hasClearedItem ? overIndex + 1 : overIndex;\n }\n\n destItemIds.splice(insertAt, 0, activeItemId);\n\n const next = [...columns];\n next[sourceColIndex] = { ...sourceCol, itemIds: sourceCol.itemIds.filter((id) => id !== activeItemId) };\n next[destColIndex] = { ...destCol, itemIds: destItemIds };\n return next;\n}\n\n/**\n * Where a drag has put the dragged card or column so far. The board keeps this rather than a copy\n * of `columns`, so a change the consumer makes mid-drag (a page appended by `onLoadMore`) still\n * shows, and is still there in what `onChange` receives on drop.\n */\nexport interface DragPlacement {\n /** The dragged card's or column's id. */\n id: string;\n /** The column the card sits in, or `null` while a whole column is dragged. */\n columnId: string | null;\n index: number;\n}\n\n/** The position `id` holds in `columns`, as a placement to lay over a later `columns`. */\nexport function findPlacement(columns: KanbanColumnDef[], id: string, isColumn: boolean): DragPlacement | null {\n if (isColumn) {\n const index = columns.findIndex((col) => col.id === id);\n return index === -1 ? null : { id, columnId: null, index };\n }\n const columnIndex = findColumnForItem(columns, id);\n if (columnIndex === -1) return null;\n const col = columns[columnIndex];\n return { id, columnId: col.id, index: col.itemIds.indexOf(id) };\n}\n\n/**\n * Moves the dragged card or column to its placement in `columns`. Returns `columns` itself when\n * there is no placement, or when the card, the column, or its destination is no longer there.\n */\nexport function applyPlacement(columns: KanbanColumnDef[], placement: DragPlacement | null): KanbanColumnDef[] {\n if (!placement) return columns;\n const { id, columnId, index } = placement;\n\n if (columnId === null) {\n const oldIndex = columns.findIndex((col) => col.id === id);\n return oldIndex === -1 ? columns : arrayMove(columns, oldIndex, index);\n }\n\n const sourceColIndex = findColumnForItem(columns, id);\n const destColIndex = columns.findIndex((col) => col.id === columnId);\n if (sourceColIndex === -1 || destColIndex === -1) return columns;\n\n const next = [...columns];\n const sourceCol = columns[sourceColIndex];\n next[sourceColIndex] = { ...sourceCol, itemIds: sourceCol.itemIds.filter((itemId) => itemId !== id) };\n const destItemIds = [...next[destColIndex].itemIds];\n destItemIds.splice(index, 0, id);\n next[destColIndex] = { ...next[destColIndex], itemIds: destItemIds };\n return next;\n}\n\nconst FOCUS_STOP = '[data-kanban-handle], [data-kanban-card]';\n\n/**\n * Where an arrow key moves focus from a focused card or column handle while nothing is picked up.\n * It mirrors what the same keys do to a picked-up card: Up/Down within the column, Left/Right to\n * the nearest card in the next column with anything to focus, or from a handle to the next handle.\n * Returns `null` when `from` is not a card or handle, or there is nowhere to go.\n */\nexport function findArrowFocusTarget(board: HTMLElement, from: HTMLElement, key: string): HTMLElement | null {\n if (!from.matches(FOCUS_STOP)) return null;\n const columns = [...board.querySelectorAll<HTMLElement>(':scope > [data-kanban-column]')];\n const columnIndex = columns.findIndex((column) => column.contains(from));\n if (columnIndex === -1) return null;\n const stopsIn = (column: HTMLElement) => [...column.querySelectorAll<HTMLElement>(FOCUS_STOP)];\n\n if (key === 'ArrowUp' || key === 'ArrowDown') {\n const stops = stopsIn(columns[columnIndex]);\n return stops[stops.indexOf(from) + (key === 'ArrowDown' ? 1 : -1)] ?? null;\n }\n if (key !== 'ArrowLeft' && key !== 'ArrowRight') return null;\n\n const direction = key === 'ArrowRight' ? 1 : -1;\n const top = from.getBoundingClientRect().top;\n const distance = (el: HTMLElement) => Math.abs(el.getBoundingClientRect().top - top);\n for (let i = columnIndex + direction; i >= 0 && i < columns.length; i += direction) {\n const stops = stopsIn(columns[i]);\n const handle = stops.find((stop) => stop.hasAttribute('data-kanban-handle'));\n const cards = stops.filter((stop) => stop.hasAttribute('data-kanban-card'));\n if (handle && (from.hasAttribute('data-kanban-handle') || cards.length === 0)) return handle;\n if (cards.length > 0) return cards.reduce((closest, card) => (distance(card) < distance(closest) ? card : closest));\n }\n return null;\n}\n\n/**\n * Builds screen-reader drag announcements against the columns layout as currently understood\n * (the live layout while a drag is in progress, or the settled `columns` prop otherwise).\n */\nexport function buildAnnouncements<T>(\n columns: KanbanColumnDef[],\n items: Record<string, T>,\n getItemLabel: (item: T, id: string) => string,\n): Announcements {\n function labelFor(id: string): string {\n const item = items[id];\n return item !== undefined ? getItemLabel(item, id) : id;\n }\n\n function describeOver(overId: string): string {\n const destColId = resolveColumnDropId(overId);\n if (destColId !== null) {\n const col = columns.find((c) => c.id === destColId);\n return col ? `the ${col.title} column` : 'a column';\n }\n\n const col = columns.find((c) => c.itemIds.includes(overId));\n return col ? `${labelFor(overId)}, in the ${col.title} column` : labelFor(overId);\n }\n\n function columnTitle(id: string): string {\n return columns.find((c) => c.id === id)?.title ?? id;\n }\n\n return {\n onDragStart({ active }) {\n if (isColumnDragData(active.data.current)) return `Picked up column ${columnTitle(String(active.id))}.`;\n return `Picked up card ${labelFor(String(active.id))}.`;\n },\n onDragOver({ active, over }) {\n if (isColumnDragData(active.data.current)) {\n if (!over) return `Column ${columnTitle(String(active.id))} is no longer over a droppable area.`;\n return `Column ${columnTitle(String(active.id))} is over column ${columnTitle(String(over.id))}.`;\n }\n if (!over) return `Card ${labelFor(String(active.id))} is no longer over a droppable area.`;\n return `Card ${labelFor(String(active.id))} is over ${describeOver(String(over.id))}.`;\n },\n onDragEnd({ active, over }) {\n if (isColumnDragData(active.data.current)) {\n if (!over) return `Column ${columnTitle(String(active.id))} was dropped.`;\n return `Column ${columnTitle(String(active.id))} was dropped over column ${columnTitle(String(over.id))}.`;\n }\n if (!over) return `Card ${labelFor(String(active.id))} was dropped.`;\n return `Card ${labelFor(String(active.id))} was dropped over ${describeOver(String(over.id))}.`;\n },\n onDragCancel({ active }) {\n if (isColumnDragData(active.data.current)) {\n return `Dragging cancelled. Column ${columnTitle(String(active.id))} was returned to its original position.`;\n }\n return `Dragging cancelled. Card ${labelFor(String(active.id))} was returned to its original position.`;\n },\n };\n}\n\ninterface ContainerRect {\n container: DroppableContainer;\n rect: ClientRect;\n}\n\nfunction collectColumnItemRects(\n containers: DroppableContainer[],\n droppableRects: Map<UniqueIdentifier, ClientRect>,\n containerId: UniqueIdentifier,\n): ContainerRect[] {\n const entries: ContainerRect[] = [];\n for (const container of containers) {\n if (!hasSortableData(container)) continue;\n if (container.data.current.sortable.containerId !== containerId) continue;\n const rect = droppableRects.get(container.id);\n if (!rect) continue;\n entries.push({ container, rect });\n }\n return entries.sort((a, b) => a.rect.top - b.rect.top);\n}\n\n/**\n * Custom keyboard coordinate getter for the board's multi-column, multi-`SortableContext`\n * layout. dnd-kit's default `sortableKeyboardCoordinates` picks the geometrically closest\n * droppable across the *entire* board — cards and column containers alike — which is unreliable\n * once cards live inside independent per-column `SortableContext`s: up/down and left/right end up\n * interchangeable depending on incidental rect positions. This instead restricts up/down to\n * reordering within the active card's own column, and left/right to moving into the adjacent\n * column (landing on the closest card there by vertical position, or the column itself when\n * empty). Requires each `KanbanColumn`'s `SortableContext` to be given an explicit `id` matching\n * its column id, so a card's `sortable.containerId` can be correlated with its `column-<id>`\n * droppable.\n */\nexport const kanbanKeyboardCoordinateGetter: KeyboardCoordinateGetter = (event, { context }) => {\n const { active, collisionRect, droppableRects, droppableContainers } = context;\n\n if (!active || !collisionRect || !hasSortableData(active)) return undefined;\n\n const isVertical = event.code === KeyboardCode.Down || event.code === KeyboardCode.Up;\n const isHorizontal = event.code === KeyboardCode.Left || event.code === KeyboardCode.Right;\n if (!isVertical && !isHorizontal) return undefined;\n\n event.preventDefault();\n\n const activeContainerId = active.data.current.sortable.containerId;\n const allContainers = droppableContainers.getEnabled();\n\n if (isColumnDragData(active.data.current)) {\n // Reordering a whole column: only left/right matters, moving it within the top-level\n // column `SortableContext` (siblings share its containerId, same as cards share their\n // own column's). Up/down has nothing to do — columns don't have a \"column above/below\".\n if (!isHorizontal) return undefined;\n\n const direction = event.code === KeyboardCode.Right ? 1 : -1;\n const siblings = collectColumnItemRects(allContainers, droppableRects, activeContainerId).sort(\n (a, b) => a.rect.left - b.rect.left,\n );\n const currentIndex = siblings.findIndex((s) => s.container.id === active.id);\n if (currentIndex === -1) return undefined;\n\n const target = siblings[currentIndex + direction];\n if (!target) return undefined;\n\n return { x: target.rect.left, y: collisionRect.top };\n }\n\n if (isVertical) {\n const direction = event.code === KeyboardCode.Down ? 1 : -1;\n const siblings = collectColumnItemRects(allContainers, droppableRects, activeContainerId);\n const currentIndex = siblings.findIndex((s) => s.container.id === active.id);\n if (currentIndex === -1) return undefined;\n\n const target = siblings[currentIndex + direction];\n if (!target) return undefined;\n\n return { x: collisionRect.left, y: target.rect.top };\n }\n\n const direction = event.code === KeyboardCode.Right ? 1 : -1;\n\n const columns: ContainerRect[] = [];\n for (const container of allContainers) {\n const columnId = resolveColumnDropId(String(container.id));\n if (columnId === null) continue;\n const rect = droppableRects.get(container.id);\n if (!rect) continue;\n columns.push({ container, rect });\n }\n columns.sort((a, b) => a.rect.left - b.rect.left);\n\n const activeColumnIndex = columns.findIndex((c) => resolveColumnDropId(String(c.container.id)) === activeContainerId);\n if (activeColumnIndex === -1) return undefined;\n\n const targetColumn = columns[activeColumnIndex + direction];\n if (!targetColumn) return undefined;\n\n const targetContainerId = resolveColumnDropId(String(targetColumn.container.id)) as UniqueIdentifier;\n const targetItems = collectColumnItemRects(allContainers, droppableRects, targetContainerId);\n\n let targetRect = targetColumn.rect;\n if (targetItems.length > 0) {\n targetRect = targetItems.reduce((closest, item) =>\n Math.abs(item.rect.top - collisionRect.top) < Math.abs(closest.rect.top - collisionRect.top) ? item : closest,\n ).rect;\n }\n\n return { x: targetRect.left, y: targetRect.top };\n};\n"],"mappings":"iJAcA,SAAgB,EAAa,EAAsB,EAA+B,CAEhF,OADI,EAAE,SAAW,EAAE,QACZ,EAAE,OAAO,EAAK,IAAM,CACzB,IAAM,EAAQ,EAAE,GAChB,OACE,EAAI,KAAO,EAAM,IACjB,EAAI,QAAQ,SAAW,EAAM,QAAQ,QACrC,EAAI,QAAQ,OAAO,EAAQ,IAAM,IAAW,EAAM,QAAQ,EAAE,CAEhE,CAAC,CACH,CAEA,SAAgB,EAAkB,EAA4B,EAAwB,CACpF,OAAO,EAAQ,UAAW,GAAQ,EAAI,QAAQ,SAAS,CAAM,CAAC,CAChE,CAGA,SAAgB,EAAiB,EAAwB,CACvD,MAAO,GAAQ,GAAQ,OAAO,GAAS,UAAa,EAA2B,OAAS,SAC1F,CAOA,SAAgB,EACd,EACA,EACA,EACmB,CACnB,IAAM,EAAW,EAAQ,UAAW,GAAQ,EAAI,KAAO,CAAc,EAC/D,EAAY,EAAQ,UAAW,GAAQ,EAAI,KAAO,CAAY,EAGpE,OAFI,IAAa,IAAM,IAAc,IAAM,IAAa,EAAkB,EAEnE,EAAU,EAAS,EAAU,CAAS,CAC/C,CAkBA,IAAa,EAAgD,GAAS,CACpE,IAAM,EAAiB,EAAiB,EAAK,OAAO,KAAK,OAAO,EAC1D,EAAsB,EAAK,oBAAoB,OAAQ,GAC3D,EAAiB,EAAiB,EAAU,KAAK,OAAO,EAAI,CAAC,EAAiB,EAAU,KAAK,OAAO,CACtG,EACM,EAAW,EAAK,oBAAoB,EACpC,EACJ,GAAkB,IAAa,IAAA,GAC3B,IAAA,GACA,EAAoB,KAAM,GAAc,CACtC,IAAM,EAAO,EAAK,eAAe,IAAI,EAAU,EAAE,EACjD,OACE,EAAoB,OAAO,EAAU,EAAE,CAAC,IAAM,MAC9C,GACA,EAAK,MAAQ,GACb,GAAY,EAAK,KAErB,CAAC,EACP,GAAI,CAAC,EAAM,OAAO,EAAe,CAAE,GAAG,EAAM,qBAAoB,CAAC,EAEjE,IAAM,EAAe,EAAoB,OAAO,EAAK,EAAE,CAAC,EACxD,OAAO,EAAe,CACpB,GAAG,EACH,oBAAqB,EAAoB,OACtC,GACC,IAAc,GACb,EAAgB,CAAS,GAAK,EAAU,KAAK,QAAQ,SAAS,cAAgB,CACnF,CACF,CAAC,CACH,EAOA,SAAgB,EACd,EACA,EACA,EACS,CAET,OADkB,EAAQ,IAAM,EAAQ,OAAS,GAAK,EAAS,IAAM,EAAS,QAAU,EACrE,EAAM,OAAS,EAAS,OAAS,EAAI,EAAM,IAAM,EAAS,IAAM,CACrF,CAGA,SAAgB,EAAoB,EAA+B,CACjE,OAAO,EAAO,WAAW,SAAS,EAAI,EAAO,MAAM,CAAgB,EAAI,IACzE,CAkBA,SAAgB,EACd,EACA,EACA,EACA,EACmB,CAKnB,IAAM,EAAiB,EAAkB,EAAS,CAAY,EAC9D,GAAI,IAAmB,GAAI,OAAO,EAElC,IAAM,EAAY,EAAoB,CAAM,EACtC,EACJ,IAAc,KAA0D,EAAkB,EAAS,CAAM,EAApF,EAAQ,UAAW,GAAQ,EAAI,KAAO,CAAS,EACtE,GAAI,IAAiB,GAAI,OAAO,EAEhC,GAAI,IAAmB,EAAc,CACnC,IAAM,EAAM,EAAQ,GACd,EAAW,EAAI,QAAQ,QAAQ,CAAY,EACjD,GAAI,IAAa,GAAI,OAAO,EAE5B,GAAI,IAAc,KAAM,CAOtB,GAAM,CAAE,aAAY,YAAa,EAI3B,EAFJ,GAAc,EAAW,IAAM,EAAW,OAAS,EAAI,EAAS,IAAM,EAAS,OAAS,EAEtD,EAAI,QAAQ,OAAS,EAAI,EAC7D,GAAI,IAAgB,EAAU,OAAO,EAErC,IAAM,EAAO,CAAC,GAAG,CAAO,EAExB,MADA,GAAK,GAAkB,CAAE,GAAG,EAAK,QAAS,EAAU,EAAI,QAAS,EAAU,CAAW,CAAE,EACjF,CACT,CAEA,IAAM,EAAW,EAAI,QAAQ,QAAQ,CAAM,EAC3C,GAAI,IAAa,IAAM,IAAa,EAAU,OAAO,EAErD,IAAM,EAAO,CAAC,GAAG,CAAO,EAExB,MADA,GAAK,GAAkB,CAAE,GAAG,EAAK,QAAS,EAAU,EAAI,QAAS,EAAU,CAAQ,CAAE,EAC9E,CACT,CAEA,IAAM,EAAY,EAAQ,GACpB,EAAU,EAAQ,GAClB,EAAc,CAAC,GAAG,EAAQ,OAAO,EACjC,CAAE,aAAY,YAAa,EAE7B,EACJ,GAAI,IAAc,KAUhB,EAFE,GAAc,EAAW,IAAM,EAAW,OAAS,EAAI,EAAS,IAAM,EAAS,OAAS,EAE/D,EAAY,OAAS,MAC3C,CACL,IAAM,EAAY,EAAY,QAAQ,CAAM,EActC,EAAa,IAAc,IAAM,IAAc,EAAY,OAAS,EACpE,EAAiB,GACrB,GAAc,EAAW,IAAM,EAAW,OAAS,EAAI,EAAS,IAAM,EAAS,QAEjF,EAAW,IAAc,GAAK,EAAY,OAAS,GAAc,EAAiB,EAAY,EAAI,CACpG,CAEA,EAAY,OAAO,EAAU,EAAG,CAAY,EAE5C,IAAM,EAAO,CAAC,GAAG,CAAO,EAGxB,MAFA,GAAK,GAAkB,CAAE,GAAG,EAAW,QAAS,EAAU,QAAQ,OAAQ,GAAO,IAAO,CAAY,CAAE,EACtG,EAAK,GAAgB,CAAE,GAAG,EAAS,QAAS,CAAY,EACjD,CACT,CAgBA,SAAgB,EAAc,EAA4B,EAAY,EAAyC,CAC7G,GAAI,EAAU,CACZ,IAAM,EAAQ,EAAQ,UAAW,GAAQ,EAAI,KAAO,CAAE,EACtD,OAAO,IAAU,GAAK,KAAO,CAAE,KAAI,SAAU,KAAM,OAAM,CAC3D,CACA,IAAM,EAAc,EAAkB,EAAS,CAAE,EACjD,GAAI,IAAgB,GAAI,OAAO,KAC/B,IAAM,EAAM,EAAQ,GACpB,MAAO,CAAE,KAAI,SAAU,EAAI,GAAI,MAAO,EAAI,QAAQ,QAAQ,CAAE,CAAE,CAChE,CAMA,SAAgB,EAAe,EAA4B,EAAoD,CAC7G,GAAI,CAAC,EAAW,OAAO,EACvB,GAAM,CAAE,KAAI,WAAU,SAAU,EAEhC,GAAI,IAAa,KAAM,CACrB,IAAM,EAAW,EAAQ,UAAW,GAAQ,EAAI,KAAO,CAAE,EACzD,OAAO,IAAa,GAAK,EAAU,EAAU,EAAS,EAAU,CAAK,CACvE,CAEA,IAAM,EAAiB,EAAkB,EAAS,CAAE,EAC9C,EAAe,EAAQ,UAAW,GAAQ,EAAI,KAAO,CAAQ,EACnE,GAAI,IAAmB,IAAM,IAAiB,GAAI,OAAO,EAEzD,IAAM,EAAO,CAAC,GAAG,CAAO,EAClB,EAAY,EAAQ,GAC1B,EAAK,GAAkB,CAAE,GAAG,EAAW,QAAS,EAAU,QAAQ,OAAQ,GAAW,IAAW,CAAE,CAAE,EACpG,IAAM,EAAc,CAAC,GAAG,EAAK,EAAa,CAAC,OAAO,EAGlD,OAFA,EAAY,OAAO,EAAO,EAAG,CAAE,EAC/B,EAAK,GAAgB,CAAE,GAAG,EAAK,GAAe,QAAS,CAAY,EAC5D,CACT,CAEA,IAAM,EAAa,2CAQnB,SAAgB,EAAqB,EAAoB,EAAmB,EAAiC,CAC3G,GAAI,CAAC,EAAK,QAAQ,CAAU,EAAG,OAAO,KACtC,IAAM,EAAU,CAAC,GAAG,EAAM,iBAA8B,+BAA+B,CAAC,EAClF,EAAc,EAAQ,UAAW,GAAW,EAAO,SAAS,CAAI,CAAC,EACvE,GAAI,IAAgB,GAAI,OAAO,KAC/B,IAAM,EAAW,GAAwB,CAAC,GAAG,EAAO,iBAA8B,CAAU,CAAC,EAE7F,GAAI,IAAQ,WAAa,IAAQ,YAAa,CAC5C,IAAM,EAAQ,EAAQ,EAAQ,EAAY,EAC1C,OAAO,EAAM,EAAM,QAAQ,CAAI,GAAK,IAAQ,YAAc,EAAI,MAAQ,IACxE,CACA,GAAI,IAAQ,aAAe,IAAQ,aAAc,OAAO,KAExD,IAAM,EAAY,IAAQ,aAAe,EAAI,GACvC,EAAM,EAAK,sBAAsB,CAAC,CAAC,IACnC,EAAY,GAAoB,KAAK,IAAI,EAAG,sBAAsB,CAAC,CAAC,IAAM,CAAG,EACnF,IAAK,IAAI,EAAI,EAAc,EAAW,GAAK,GAAK,EAAI,EAAQ,OAAQ,GAAK,EAAW,CAClF,IAAM,EAAQ,EAAQ,EAAQ,EAAE,EAC1B,EAAS,EAAM,KAAM,GAAS,EAAK,aAAa,oBAAoB,CAAC,EACrE,EAAQ,EAAM,OAAQ,GAAS,EAAK,aAAa,kBAAkB,CAAC,EAC1E,GAAI,IAAW,EAAK,aAAa,oBAAoB,GAAK,EAAM,SAAW,GAAI,OAAO,EACtF,GAAI,EAAM,OAAS,EAAG,OAAO,EAAM,QAAQ,EAAS,IAAU,EAAS,CAAI,EAAI,EAAS,CAAO,EAAI,EAAO,CAAQ,CACpH,CACA,OAAO,IACT,CAMA,SAAgB,EACd,EACA,EACA,EACe,CACf,SAAS,EAAS,EAAoB,CACpC,IAAM,EAAO,EAAM,GACnB,OAAO,IAAS,IAAA,GAAqC,EAAzB,EAAa,EAAM,CAAE,CACnD,CAEA,SAAS,EAAa,EAAwB,CAC5C,IAAM,EAAY,EAAoB,CAAM,EAC5C,GAAI,IAAc,KAAM,CACtB,IAAM,EAAM,EAAQ,KAAM,GAAM,EAAE,KAAO,CAAS,EAClD,OAAO,EAAM,OAAO,EAAI,MAAM,SAAW,UAC3C,CAEA,IAAM,EAAM,EAAQ,KAAM,GAAM,EAAE,QAAQ,SAAS,CAAM,CAAC,EAC1D,OAAO,EAAM,GAAG,EAAS,CAAM,EAAE,WAAW,EAAI,MAAM,SAAW,EAAS,CAAM,CAClF,CAEA,SAAS,EAAY,EAAoB,CACvC,OAAO,EAAQ,KAAM,GAAM,EAAE,KAAO,CAAE,CAAC,EAAE,OAAS,CACpD,CAEA,MAAO,CACL,YAAY,CAAE,UAAU,CAEtB,OADI,EAAiB,EAAO,KAAK,OAAO,EAAU,oBAAoB,EAAY,OAAO,EAAO,EAAE,CAAC,EAAE,GAC9F,kBAAkB,EAAS,OAAO,EAAO,EAAE,CAAC,EAAE,EACvD,EACA,WAAW,CAAE,SAAQ,QAAQ,CAM3B,OALI,EAAiB,EAAO,KAAK,OAAO,EACjC,EACE,UAAU,EAAY,OAAO,EAAO,EAAE,CAAC,EAAE,kBAAkB,EAAY,OAAO,EAAK,EAAE,CAAC,EAAE,GAD7E,UAAU,EAAY,OAAO,EAAO,EAAE,CAAC,EAAE,sCAGxD,EACE,QAAQ,EAAS,OAAO,EAAO,EAAE,CAAC,EAAE,WAAW,EAAa,OAAO,EAAK,EAAE,CAAC,EAAE,GADlE,QAAQ,EAAS,OAAO,EAAO,EAAE,CAAC,EAAE,qCAExD,EACA,UAAU,CAAE,SAAQ,QAAQ,CAM1B,OALI,EAAiB,EAAO,KAAK,OAAO,EACjC,EACE,UAAU,EAAY,OAAO,EAAO,EAAE,CAAC,EAAE,2BAA2B,EAAY,OAAO,EAAK,EAAE,CAAC,EAAE,GADtF,UAAU,EAAY,OAAO,EAAO,EAAE,CAAC,EAAE,eAGxD,EACE,QAAQ,EAAS,OAAO,EAAO,EAAE,CAAC,EAAE,oBAAoB,EAAa,OAAO,EAAK,EAAE,CAAC,EAAE,GAD3E,QAAQ,EAAS,OAAO,EAAO,EAAE,CAAC,EAAE,cAExD,EACA,aAAa,CAAE,UAAU,CAIvB,OAHI,EAAiB,EAAO,KAAK,OAAO,EAC/B,8BAA8B,EAAY,OAAO,EAAO,EAAE,CAAC,EAAE,yCAE/D,4BAA4B,EAAS,OAAO,EAAO,EAAE,CAAC,EAAE,wCACjE,CACF,CACF,CAOA,SAAS,EACP,EACA,EACA,EACiB,CACjB,IAAM,EAA2B,CAAC,EAClC,IAAK,IAAM,KAAa,EAAY,CAElC,GADI,CAAC,EAAgB,CAAS,GAC1B,EAAU,KAAK,QAAQ,SAAS,cAAgB,EAAa,SACjE,IAAM,EAAO,EAAe,IAAI,EAAU,EAAE,EACvC,GACL,EAAQ,KAAK,CAAE,YAAW,MAAK,CAAC,CAClC,CACA,OAAO,EAAQ,MAAM,EAAG,IAAM,EAAE,KAAK,IAAM,EAAE,KAAK,GAAG,CACvD,CAcA,IAAa,GAA4D,EAAO,CAAE,aAAc,CAC9F,GAAM,CAAE,SAAQ,gBAAe,iBAAgB,uBAAwB,EAEvE,GAAI,CAAC,GAAU,CAAC,GAAiB,CAAC,EAAgB,CAAM,EAAG,OAE3D,IAAM,EAAa,EAAM,OAAS,EAAa,MAAQ,EAAM,OAAS,EAAa,GAC7E,EAAe,EAAM,OAAS,EAAa,MAAQ,EAAM,OAAS,EAAa,MACrF,GAAI,CAAC,GAAc,CAAC,EAAc,OAElC,EAAM,eAAe,EAErB,IAAM,EAAoB,EAAO,KAAK,QAAQ,SAAS,YACjD,EAAgB,EAAoB,WAAW,EAErD,GAAI,EAAiB,EAAO,KAAK,OAAO,EAAG,CAIzC,GAAI,CAAC,EAAc,OAEnB,IAAM,EAAY,EAAM,OAAS,EAAa,MAAQ,EAAI,GACpD,EAAW,EAAuB,EAAe,EAAgB,CAAiB,CAAC,CAAC,MACvF,EAAG,IAAM,EAAE,KAAK,KAAO,EAAE,KAAK,IACjC,EACM,EAAe,EAAS,UAAW,GAAM,EAAE,UAAU,KAAO,EAAO,EAAE,EAC3E,GAAI,IAAiB,GAAI,OAEzB,IAAM,EAAS,EAAS,EAAe,GAGvC,OAFK,EAEE,CAAE,EAAG,EAAO,KAAK,KAAM,EAAG,EAAc,GAAI,EAFtC,MAGf,CAEA,GAAI,EAAY,CACd,IAAM,EAAY,EAAM,OAAS,EAAa,KAAO,EAAI,GACnD,EAAW,EAAuB,EAAe,EAAgB,CAAiB,EAClF,EAAe,EAAS,UAAW,GAAM,EAAE,UAAU,KAAO,EAAO,EAAE,EAC3E,GAAI,IAAiB,GAAI,OAEzB,IAAM,EAAS,EAAS,EAAe,GAGvC,OAFK,EAEE,CAAE,EAAG,EAAc,KAAM,EAAG,EAAO,KAAK,GAAI,EAFtC,MAGf,CAEA,IAAM,EAAY,EAAM,OAAS,EAAa,MAAQ,EAAI,GAEpD,EAA2B,CAAC,EAClC,IAAK,IAAM,KAAa,EAAe,CAErC,GADiB,EAAoB,OAAO,EAAU,EAAE,CACpD,IAAa,KAAM,SACvB,IAAM,EAAO,EAAe,IAAI,EAAU,EAAE,EACvC,GACL,EAAQ,KAAK,CAAE,YAAW,MAAK,CAAC,CAClC,CACA,EAAQ,MAAM,EAAG,IAAM,EAAE,KAAK,KAAO,EAAE,KAAK,IAAI,EAEhD,IAAM,EAAoB,EAAQ,UAAW,GAAM,EAAoB,OAAO,EAAE,UAAU,EAAE,CAAC,IAAM,CAAiB,EACpH,GAAI,IAAsB,GAAI,OAE9B,IAAM,EAAe,EAAQ,EAAoB,GACjD,GAAI,CAAC,EAAc,OAGnB,IAAM,EAAc,EAAuB,EAAe,EADhC,EAAoB,OAAO,EAAa,UAAU,EAAE,CACJ,CAAiB,EAEvF,EAAa,EAAa,KAO9B,OANI,EAAY,OAAS,IACvB,EAAa,EAAY,QAAQ,EAAS,IACxC,KAAK,IAAI,EAAK,KAAK,IAAM,EAAc,GAAG,EAAI,KAAK,IAAI,EAAQ,KAAK,IAAM,EAAc,GAAG,EAAI,EAAO,CACxG,CAAC,CAAC,MAGG,CAAE,EAAG,EAAW,KAAM,EAAG,EAAW,GAAI,CACjD"}