hazo_ui 6.4.0 → 6.5.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.
package/CHANGE_LOG.md CHANGED
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v6.5.0 (2026-08-02)
9
+
10
+ ### Added — swimlanes for `HazoUiKanban`
11
+
12
+ A board can now declare `lanes`, rendering one full-width row per lane —
13
+ a wide lane heading, then that lane's own copy of the whole column grid. Where
14
+ `sections` (v6.4.0) group *within* a column, lanes group *across* all of them,
15
+ so the second axis can be the row axis instead of the inner one.
16
+
17
+ - `HazoUiKanbanProps.lanes?: KanbanLane[]` — `{ key, title, accepts?,
18
+ collapseWhenEmpty? }`. Cards are placed by the new `KanbanItem.laneKey`.
19
+ Omit `lanes` and nothing changes: existing boards render exactly as before.
20
+ - `KanbanMoveEvent` gained `fromLane`/`toLane`; `KanbanReorderEvent` gained
21
+ `laneKey`. Move-vs-reorder is now decided on all three axes — same column,
22
+ section *and* lane is a reorder; changing any one is a move.
23
+ - **A lane heading is the lane-only drop target.** It carries a lane but no
24
+ column, read as "change the lane, keep the card's column" — the mirror of a
25
+ sectioned column's head ("change the column, keep the section").
26
+ - `collapseWhenEmpty` renders a lane with no cards as a thin full-width dashed
27
+ strip, which is what makes "declare every lane" affordable for a long
28
+ vocabulary. A card whose `laneKey` matches no declared lane renders in an
29
+ unlabelled trailing lane rather than disappearing.
30
+ - The optimistic overlay tracks the destination lane too, so a cross-lane drop
31
+ moves immediately instead of snapping back until the consumer's async commit
32
+ lands.
33
+
34
+ ### Fixed — drops landed in the wrong column on tall or scrolled boards
35
+
36
+ Two long-standing defects in drop resolution, both of which lanes made
37
+ impossible to miss:
38
+
39
+ - **Collision detection is now pointer-first** (`pointerWithin`, falling back
40
+ to `rectIntersection`, then `closestCorners`). `closestCorners` alone
41
+ averages the distance to all four corners, so a column stretched tall by a
42
+ neighbour's card stack scored *worse* than a short, far-away column — a drop
43
+ well inside the right column could resolve to a different one entirely.
44
+ Keyboard drags have no pointer and take the same fallbacks as before.
45
+ - **Droppable ids are scoped per layout tree.** The mobile tab strip and the
46
+ desktop grid both always render (CSS hides one), so every column previously
47
+ registered twice under one id. dnd-kit keys its registry by id, so one node
48
+ won and collisions were measured against the CSS-hidden copy's 0×0 rect.
49
+ Ids now carry the tree ("m"/"d") and, on a laned board, the lane.
50
+
8
51
  ## v6.4.0 (2026-08-01)
9
52
 
10
53
  ### Added — in-column sections for `HazoUiKanban`
package/README.md CHANGED
@@ -3677,6 +3677,48 @@ function Actions({ initial }) {
3677
3677
  event carries `revert()` — call it when your API request fails and the
3678
3678
  overlay snaps back to whatever `items` says.
3679
3679
 
3680
+ ### Grouping: sections (v6.4.0) and swimlanes (v6.5.0)
3681
+
3682
+ A board can group on a second axis, either *inside* each column or *across*
3683
+ all of them. Both are opt-in; omit them and the board is a plain column grid.
3684
+
3685
+ ```tsx
3686
+ // Sections: separator-headed bands inside each column, keyed by item.sectionKey
3687
+ <HazoUiKanban
3688
+ columns={COLUMNS.map(col => ({
3689
+ ...col,
3690
+ sections: TAGS.map(t => ({ key: t, title: t, collapseWhenEmpty: true })),
3691
+ }))}
3692
+ items={items.map(it => ({ ...it, sectionKey: it.tag }))}
3693
+ onMove={(e) => save(e.itemId, { column: e.toColumn, tag: e.toSection })}
3694
+ renderCard={renderCard}
3695
+ />
3696
+
3697
+ // Swimlanes: one full-width row per lane, each with its own column grid,
3698
+ // keyed by item.laneKey
3699
+ <HazoUiKanban
3700
+ columns={COLUMNS}
3701
+ lanes={TAGS.map(t => ({ key: t, title: t, collapseWhenEmpty: true }))}
3702
+ items={items.map(it => ({ ...it, laneKey: it.tag }))}
3703
+ onMove={(e) => save(e.itemId, { column: e.toColumn, tag: e.toLane })}
3704
+ renderCard={renderCard}
3705
+ />
3706
+ ```
3707
+
3708
+ | | Sections | Lanes |
3709
+ |---|---|---|
3710
+ | Declared on | `KanbanColumn.sections` | `HazoUiKanbanProps.lanes` |
3711
+ | Card placed by | `KanbanItem.sectionKey` | `KanbanItem.laneKey` |
3712
+ | Renders as | bands stacked inside one column | full-width rows, each with the whole column grid |
3713
+ | Move event carries | `fromSection`/`toSection` | `fromLane`/`toLane` |
3714
+ | "Keep the other axis" drop target | the column head | the lane heading |
3715
+ | `collapseWhenEmpty` | thin drop strip at the column's foot | full-width drop strip |
3716
+
3717
+ A drop expresses both axes at once, so one gesture can change status and tag
3718
+ together. `accepts(item)` on a column, section or lane rejects a drop into it.
3719
+ Cards whose key matches nothing declared render in an unlabelled trailing
3720
+ group (sections) or lane row (lanes) rather than disappearing.
3721
+
3680
3722
  ### Theming (CSS custom properties)
3681
3723
 
3682
3724
  | Variable | Default | Purpose |
package/dist/index.cjs CHANGED
@@ -9091,10 +9091,15 @@ function KanbanCard({
9091
9091
  isDragging
9092
9092
  } = sortable.useSortable({
9093
9093
  id: item.id,
9094
- // sectionKey rides along so a drop *onto a card* resolves to that card's
9095
- // section, not just its column (kanban_column.tsx's section bands cover
9096
- // the gaps between cards, but the cards themselves are the sortable).
9097
- data: { columnKey: item.columnKey, sectionKey: item.sectionKey }
9094
+ // sectionKey and laneKey ride along so a drop *onto a card* resolves to
9095
+ // that card's section and lane, not just its column (the section bands
9096
+ // and lane rows cover the gaps between cards, but the cards themselves
9097
+ // are the sortable).
9098
+ data: {
9099
+ columnKey: item.columnKey,
9100
+ sectionKey: item.sectionKey,
9101
+ laneKey: item.laneKey
9102
+ }
9098
9103
  });
9099
9104
  const priority_var = item.priority ? `--hazo-kanban-priority-${String(item.priority).toLowerCase()}` : null;
9100
9105
  const style = {
@@ -9155,8 +9160,16 @@ function KanbanCard({
9155
9160
  }
9156
9161
  );
9157
9162
  }
9158
- function section_droppable_id(column_key, section_key) {
9159
- return `section:${column_key}::${section_key}`;
9163
+ function tree_droppable_id(prefix, tree_key, lane_key, rest) {
9164
+ return lane_key == null ? `${prefix}:${tree_key}::${rest}` : `${prefix}:${tree_key}::${lane_key}::${rest}`;
9165
+ }
9166
+ function section_droppable_id(column_key, section_key, tree_key, lane_key) {
9167
+ return tree_droppable_id(
9168
+ "section",
9169
+ tree_key,
9170
+ lane_key,
9171
+ `${column_key}::${section_key}`
9172
+ );
9160
9173
  }
9161
9174
  function group_items_by_section(items, sections) {
9162
9175
  const bySection = /* @__PURE__ */ new Map();
@@ -9176,11 +9189,13 @@ function KanbanColumn({
9176
9189
  emptyColumn,
9177
9190
  cardClassName,
9178
9191
  showEdit,
9179
- onEditRequest
9192
+ onEditRequest,
9193
+ laneKey,
9194
+ treeKey
9180
9195
  }) {
9181
9196
  const { setNodeRef, isOver } = core$1.useDroppable({
9182
- id: `column:${column.key}`,
9183
- data: { columnKey: column.key, isColumn: true }
9197
+ id: tree_droppable_id("column", treeKey, laneKey, column.key),
9198
+ data: { columnKey: column.key, laneKey, isColumn: true }
9184
9199
  });
9185
9200
  const sections = column.sections;
9186
9201
  const { bySection, unmatched } = React27__namespace.useMemo(
@@ -9226,6 +9241,8 @@ function KanbanColumn({
9226
9241
  KanbanColumnHead,
9227
9242
  {
9228
9243
  columnKey: column.key,
9244
+ laneKey,
9245
+ treeKey,
9229
9246
  title: column.title,
9230
9247
  count: items.length,
9231
9248
  droppable: sections != null
@@ -9241,6 +9258,8 @@ function KanbanColumn({
9241
9258
  KanbanSectionBand,
9242
9259
  {
9243
9260
  columnKey: column.key,
9261
+ laneKey,
9262
+ treeKey,
9244
9263
  section,
9245
9264
  items: bySection.get(section.key) ?? [],
9246
9265
  emptyColumn,
@@ -9253,6 +9272,8 @@ function KanbanColumn({
9253
9272
  KanbanSectionStrip,
9254
9273
  {
9255
9274
  columnKey: column.key,
9275
+ laneKey,
9276
+ treeKey,
9256
9277
  section
9257
9278
  },
9258
9279
  section.key
@@ -9266,13 +9287,15 @@ function KanbanColumn({
9266
9287
  }
9267
9288
  function KanbanColumnHead({
9268
9289
  columnKey,
9290
+ laneKey,
9291
+ treeKey,
9269
9292
  title,
9270
9293
  count,
9271
9294
  droppable
9272
9295
  }) {
9273
9296
  const { setNodeRef, isOver } = core$1.useDroppable({
9274
- id: `columnhead:${columnKey}`,
9275
- data: { columnKey, isColumn: true },
9297
+ id: tree_droppable_id("columnhead", treeKey, laneKey, columnKey),
9298
+ data: { columnKey, laneKey, isColumn: true },
9276
9299
  disabled: !droppable
9277
9300
  });
9278
9301
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -9293,14 +9316,16 @@ function KanbanColumnHead({
9293
9316
  }
9294
9317
  function KanbanSectionBand({
9295
9318
  columnKey,
9319
+ laneKey,
9320
+ treeKey,
9296
9321
  section,
9297
9322
  items,
9298
9323
  emptyColumn,
9299
9324
  renderCards
9300
9325
  }) {
9301
9326
  const { setNodeRef, isOver } = core$1.useDroppable({
9302
- id: section_droppable_id(columnKey, section.key),
9303
- data: { columnKey, sectionKey: section.key, isSection: true }
9327
+ id: section_droppable_id(columnKey, section.key, treeKey, laneKey),
9328
+ data: { columnKey, laneKey, sectionKey: section.key, isSection: true }
9304
9329
  });
9305
9330
  return /* @__PURE__ */ jsxRuntime.jsxs(
9306
9331
  "div",
@@ -9323,11 +9348,13 @@ function KanbanSectionBand({
9323
9348
  }
9324
9349
  function KanbanSectionStrip({
9325
9350
  columnKey,
9351
+ laneKey,
9352
+ treeKey,
9326
9353
  section
9327
9354
  }) {
9328
9355
  const { setNodeRef, isOver } = core$1.useDroppable({
9329
- id: section_droppable_id(columnKey, section.key),
9330
- data: { columnKey, sectionKey: section.key, isSection: true }
9356
+ id: section_droppable_id(columnKey, section.key, treeKey, laneKey),
9357
+ data: { columnKey, laneKey, sectionKey: section.key, isSection: true }
9331
9358
  });
9332
9359
  return /* @__PURE__ */ jsxRuntime.jsx(
9333
9360
  "div",
@@ -9341,6 +9368,54 @@ function KanbanSectionStrip({
9341
9368
  }
9342
9369
  );
9343
9370
  }
9371
+ function lane_droppable_id(lane_key, tree_key) {
9372
+ return `lane:${tree_key}::${lane_key}`;
9373
+ }
9374
+ function KanbanLaneHead({
9375
+ lane,
9376
+ count,
9377
+ treeKey
9378
+ }) {
9379
+ const { setNodeRef, isOver } = core$1.useDroppable({
9380
+ id: lane_droppable_id(lane.key, treeKey),
9381
+ data: { laneKey: lane.key, isLane: true }
9382
+ });
9383
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9384
+ "div",
9385
+ {
9386
+ ref: setNodeRef,
9387
+ className: cn(
9388
+ "cls_hazo_kanban_lane_head mb-2 flex items-center gap-3 rounded-sm px-1 py-1",
9389
+ isOver && "cls_hazo_kanban_lane_head_over bg-accent/30 ring-1 ring-ring/40"
9390
+ ),
9391
+ children: [
9392
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_lane_title text-xs font-semibold uppercase tracking-wider text-foreground", children: lane.title }),
9393
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_lane_rule h-px flex-1 bg-border" }),
9394
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_lane_count rounded-full bg-muted px-2 py-0.5 text-[11px] tabular-nums text-foreground", children: count })
9395
+ ]
9396
+ }
9397
+ );
9398
+ }
9399
+ function KanbanLaneStrip({
9400
+ lane,
9401
+ treeKey
9402
+ }) {
9403
+ const { setNodeRef, isOver } = core$1.useDroppable({
9404
+ id: lane_droppable_id(lane.key, treeKey),
9405
+ data: { laneKey: lane.key, isLane: true }
9406
+ });
9407
+ return /* @__PURE__ */ jsxRuntime.jsx(
9408
+ "div",
9409
+ {
9410
+ ref: setNodeRef,
9411
+ className: cn(
9412
+ "cls_hazo_kanban_lane_strip mb-2 rounded-md border border-dashed border-border px-2 py-1 text-[11px] uppercase tracking-wider text-muted-foreground",
9413
+ isOver && "cls_hazo_kanban_lane_strip_over border-solid bg-accent/30 text-foreground ring-1 ring-ring/30"
9414
+ ),
9415
+ children: lane.title
9416
+ }
9417
+ );
9418
+ }
9344
9419
  var DEFAULT_PRIORITIES = ["P0", "P1", "P2", "P3"];
9345
9420
  function humanize(key) {
9346
9421
  return key.replace(/[_\-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\s+/g, " ").trim().split(" ").map((w) => w ? w[0].toUpperCase() + w.slice(1) : w).join(" ");
@@ -9747,6 +9822,10 @@ function applyKanbanFilter(items, filter) {
9747
9822
  return true;
9748
9823
  });
9749
9824
  }
9825
+ var UNMATCHED_LANE_KEY = "__hazo_lane_unmatched__";
9826
+ function bucket_key(lane_key, column_key) {
9827
+ return lane_key == null ? column_key : `${lane_key}\0${column_key}`;
9828
+ }
9750
9829
  function default_announcements(itemLabel) {
9751
9830
  return {
9752
9831
  onDragStart: ({ item, fromColumn }) => `Picked up ${itemLabel(item)}. Currently in ${fromColumn}. Use arrow keys to choose a different column. Press space to drop, escape to cancel.`,
@@ -9755,38 +9834,50 @@ function default_announcements(itemLabel) {
9755
9834
  onDragCancel: ({ item, fromColumn }) => `Movement cancelled. ${itemLabel(item)} returned to ${fromColumn}.`
9756
9835
  };
9757
9836
  }
9758
- function group_items_by_column(items, overlay, columns) {
9837
+ function group_items_by_column(items, overlay, columns, lanes) {
9759
9838
  const out = /* @__PURE__ */ new Map();
9760
- for (const col of columns) out.set(col.key, []);
9839
+ const lane_keys = lanes ? [...lanes.map((l) => l.key), UNMATCHED_LANE_KEY] : [void 0];
9840
+ for (const lane_key of lane_keys)
9841
+ for (const col of columns) out.set(bucket_key(lane_key, col.key), []);
9761
9842
  const overlay_inserts = /* @__PURE__ */ new Map();
9762
- for (const col of columns) overlay_inserts.set(col.key, []);
9843
+ for (const key of out.keys()) overlay_inserts.set(key, []);
9844
+ const declared = lanes ? new Set(lanes.map((l) => l.key)) : null;
9845
+ function resolve_lane(lane_key) {
9846
+ if (!declared) return void 0;
9847
+ return lane_key != null && declared.has(lane_key) ? lane_key : UNMATCHED_LANE_KEY;
9848
+ }
9763
9849
  for (const item of items) {
9764
9850
  const ov = overlay.get(item.id);
9765
9851
  if (ov) {
9766
- const bucket = overlay_inserts.get(ov.columnKey);
9852
+ const bucket = overlay_inserts.get(
9853
+ bucket_key(resolve_lane(ov.laneKey), ov.columnKey)
9854
+ );
9767
9855
  if (bucket) {
9768
- const placed = ov.sectionKey === item.sectionKey ? item : { ...item, sectionKey: ov.sectionKey };
9856
+ const placed = ov.sectionKey === item.sectionKey && ov.laneKey === item.laneKey ? item : { ...item, sectionKey: ov.sectionKey, laneKey: ov.laneKey };
9769
9857
  bucket.push({ item: placed, index: ov.index });
9770
9858
  }
9771
9859
  } else {
9772
- const bucket = out.get(item.columnKey);
9860
+ const bucket = out.get(
9861
+ bucket_key(resolve_lane(item.laneKey), item.columnKey)
9862
+ );
9773
9863
  if (bucket) bucket.push(item);
9774
9864
  }
9775
9865
  }
9776
- for (const col of columns) {
9777
- const inserts = overlay_inserts.get(col.key) ?? [];
9866
+ for (const [key, inserts] of overlay_inserts) {
9867
+ if (inserts.length === 0) continue;
9778
9868
  inserts.sort((a, b) => a.index - b.index);
9779
- const arr = out.get(col.key) ?? [];
9869
+ const arr = out.get(key) ?? [];
9780
9870
  for (const { item, index } of inserts) {
9781
9871
  const at = Math.max(0, Math.min(index, arr.length));
9782
9872
  arr.splice(at, 0, item);
9783
9873
  }
9784
- out.set(col.key, arr);
9874
+ out.set(key, arr);
9785
9875
  }
9786
9876
  return out;
9787
9877
  }
9788
9878
  function HazoUiKanban({
9789
9879
  columns,
9880
+ lanes,
9790
9881
  items,
9791
9882
  renderCard,
9792
9883
  onMove,
@@ -9834,7 +9925,7 @@ function HazoUiKanban({
9834
9925
  const next = new Map(prev);
9835
9926
  for (const [item_id, pos] of prev.entries()) {
9836
9927
  const item = items.find((i) => i.id === item_id);
9837
- if (item && item.columnKey === pos.columnKey && item.sectionKey === pos.sectionKey) {
9928
+ if (item && item.columnKey === pos.columnKey && item.sectionKey === pos.sectionKey && item.laneKey === pos.laneKey) {
9838
9929
  next.delete(item_id);
9839
9930
  changed = true;
9840
9931
  }
@@ -9843,8 +9934,8 @@ function HazoUiKanban({
9843
9934
  });
9844
9935
  }, [items, overlay]);
9845
9936
  const grouped = React27__namespace.useMemo(
9846
- () => group_items_by_column(items, overlay, columns),
9847
- [items, overlay, columns]
9937
+ () => group_items_by_column(items, overlay, columns, lanes),
9938
+ [items, overlay, columns, lanes]
9848
9939
  );
9849
9940
  const find_item = React27__namespace.useCallback(
9850
9941
  (id) => items.find((i) => i.id === id),
@@ -9854,6 +9945,13 @@ function HazoUiKanban({
9854
9945
  core$1.useSensor(core$1.PointerSensor, { activationConstraint: { distance: 5 } }),
9855
9946
  core$1.useSensor(core$1.KeyboardSensor, { coordinateGetter: sortable.sortableKeyboardCoordinates })
9856
9947
  );
9948
+ const collision_detection = React27__namespace.useCallback((args) => {
9949
+ const under_pointer = core$1.pointerWithin(args);
9950
+ if (under_pointer.length > 0) return under_pointer;
9951
+ const overlapping = core$1.rectIntersection(args);
9952
+ if (overlapping.length > 0) return overlapping;
9953
+ return core$1.closestCorners(args);
9954
+ }, []);
9857
9955
  const merged_announcements = React27__namespace.useMemo(() => {
9858
9956
  const defaults = default_announcements(label_fn);
9859
9957
  return {
@@ -9882,7 +9980,7 @@ function HazoUiKanban({
9882
9980
  const id = String(active.id);
9883
9981
  const item = find_item(id);
9884
9982
  if (!item) return "";
9885
- const over_col_key = over ? over.data?.current?.columnKey ?? String(over.id).replace(/^column:/, "") : null;
9983
+ const over_col_key = over ? over.data?.current?.columnKey ?? item.columnKey : null;
9886
9984
  const col = over_col_key ? columns.find((c) => c.key === over_col_key) : null;
9887
9985
  return merged_announcements.onDragOver({
9888
9986
  item,
@@ -9897,10 +9995,13 @@ function HazoUiKanban({
9897
9995
  const item = find_item(id);
9898
9996
  if (!item) return "";
9899
9997
  const from_col = columns.find((c) => c.key === item.columnKey);
9900
- const over_col_key = over ? over.data?.current?.columnKey ?? String(over.id).replace(/^column:/, "") : null;
9998
+ const over_col_key = over?.data?.current?.columnKey ?? item.columnKey;
9999
+ const over_lane_key = lanes ? over?.data?.current?.laneKey ?? item.laneKey : void 0;
9901
10000
  const to_col = over_col_key ? columns.find((c) => c.key === over_col_key) ?? from_col : from_col;
9902
- const target = over_col_key ? grouped.get(over_col_key) ?? [] : grouped.get(item.columnKey) ?? [];
9903
- const on_shell = !over || String(over.id).startsWith("column:") || String(over.id).startsWith("columnhead:") || String(over.id).startsWith("section:");
10001
+ const target = grouped.get(
10002
+ bucket_key(over_lane_key, over_col_key ?? item.columnKey)
10003
+ ) ?? [];
10004
+ const on_shell = !over || String(over.id).startsWith("column:") || String(over.id).startsWith("columnhead:") || String(over.id).startsWith("section:") || String(over.id).startsWith("lane:");
9904
10005
  const new_index = on_shell ? target.length : target.findIndex((it) => it.id === String(over.id));
9905
10006
  return merged_announcements.onDragEnd({
9906
10007
  item,
@@ -9920,7 +10021,7 @@ function HazoUiKanban({
9920
10021
  });
9921
10022
  }
9922
10023
  }),
9923
- [find_item, columns, grouped, merged_announcements]
10024
+ [find_item, columns, lanes, grouped, merged_announcements]
9924
10025
  );
9925
10026
  const handle_drag_start = React27__namespace.useCallback((event) => {
9926
10027
  set_active_id(String(event.active.id));
@@ -9947,31 +10048,35 @@ function HazoUiKanban({
9947
10048
  if (!event.over) return;
9948
10049
  const over_id = String(event.over.id);
9949
10050
  const over_data = event.over.data?.current;
9950
- const dest_column_key = over_data?.columnKey ?? over_id.replace(/^column:/, "");
10051
+ const dest_column_key = over_data?.columnKey ?? item.columnKey;
9951
10052
  const dest_column = columns.find((c) => c.key === dest_column_key);
9952
10053
  if (!dest_column) return;
10054
+ const dest_lane_key = lanes ? over_data?.laneKey ?? item.laneKey : void 0;
10055
+ const dest_lane = lanes?.find((l) => l.key === dest_lane_key);
10056
+ if (lanes && dest_lane?.accepts && !dest_lane.accepts(item)) return;
9953
10057
  const dest_section_key = dest_column.sections ? over_data?.sectionKey ?? item.sectionKey : void 0;
9954
10058
  const dest_section = dest_column.sections?.find(
9955
10059
  (s) => s.key === dest_section_key
9956
10060
  );
9957
10061
  if (dest_column.accepts && !dest_column.accepts(item)) return;
9958
10062
  if (dest_section?.accepts && !dest_section.accepts(item)) return;
9959
- const dest_items_pre_overlay = grouped.get(dest_column_key) ?? [];
10063
+ const dest_items_pre_overlay = grouped.get(bucket_key(dest_lane_key, dest_column_key)) ?? [];
9960
10064
  let new_index;
9961
- if (over_data?.isColumn || over_data?.isSection) {
10065
+ if (over_data?.isColumn || over_data?.isSection || over_data?.isLane) {
9962
10066
  new_index = dest_items_pre_overlay.length;
9963
10067
  } else {
9964
10068
  const idx = dest_items_pre_overlay.findIndex((i) => i.id === over_id);
9965
10069
  new_index = idx === -1 ? dest_items_pre_overlay.length : idx;
9966
10070
  }
9967
10071
  const handle = make_revert_handle(active_id_str);
9968
- if (item.columnKey === dest_column_key && item.sectionKey === dest_section_key) {
10072
+ if (item.columnKey === dest_column_key && item.sectionKey === dest_section_key && item.laneKey === dest_lane_key) {
9969
10073
  if (active_id_str === over_id) return;
9970
10074
  set_overlay((prev) => {
9971
10075
  const next = new Map(prev);
9972
10076
  next.set(active_id_str, {
9973
10077
  columnKey: dest_column_key,
9974
10078
  sectionKey: dest_section_key,
10079
+ laneKey: dest_lane_key,
9975
10080
  index: new_index
9976
10081
  });
9977
10082
  return next;
@@ -9981,6 +10086,7 @@ function HazoUiKanban({
9981
10086
  item,
9982
10087
  columnKey: dest_column_key,
9983
10088
  sectionKey: dest_section_key,
10089
+ laneKey: dest_lane_key,
9984
10090
  newIndex: new_index,
9985
10091
  revert: handle.revert
9986
10092
  };
@@ -9992,6 +10098,7 @@ function HazoUiKanban({
9992
10098
  next.set(active_id_str, {
9993
10099
  columnKey: dest_column_key,
9994
10100
  sectionKey: dest_section_key,
10101
+ laneKey: dest_lane_key,
9995
10102
  index: new_index
9996
10103
  });
9997
10104
  return next;
@@ -10003,12 +10110,14 @@ function HazoUiKanban({
10003
10110
  toColumn: dest_column_key,
10004
10111
  fromSection: item.sectionKey,
10005
10112
  toSection: dest_section_key,
10113
+ fromLane: item.laneKey,
10114
+ toLane: dest_lane_key,
10006
10115
  newIndex: new_index,
10007
10116
  revert: handle.revert
10008
10117
  };
10009
10118
  onMove?.(move_event);
10010
10119
  },
10011
- [find_item, columns, grouped, onMove, onReorder, make_revert_handle]
10120
+ [find_item, columns, lanes, grouped, onMove, onReorder, make_revert_handle]
10012
10121
  );
10013
10122
  const handle_drag_cancel = React27__namespace.useCallback(() => {
10014
10123
  set_active_id(null);
@@ -10019,13 +10128,56 @@ function HazoUiKanban({
10019
10128
  `(max-width: ${Math.max(0, mobileBreakpoint - 1)}px)`
10020
10129
  );
10021
10130
  const mobile_classes = is_custom_breakpoint ? is_mobile_via_query ? "block" : "hidden" : "sm:hidden";
10022
- const desktop_classes = is_custom_breakpoint ? is_mobile_via_query ? "hidden" : "grid" : "hidden sm:grid";
10131
+ const desktop_classes = is_custom_breakpoint ? is_mobile_via_query ? "hidden" : lanes ? "block" : "grid" : lanes ? "hidden sm:block" : "hidden sm:grid";
10132
+ const grid_style = {
10133
+ gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))`
10134
+ };
10135
+ function lane_total(lane_key) {
10136
+ let total = 0;
10137
+ for (const col of columns)
10138
+ total += (grouped.get(bucket_key(lane_key, col.key)) ?? []).length;
10139
+ return total;
10140
+ }
10141
+ function render_column(col, tree_key, lane_key) {
10142
+ return /* @__PURE__ */ jsxRuntime.jsx(
10143
+ KanbanColumn,
10144
+ {
10145
+ column: col,
10146
+ laneKey: lane_key,
10147
+ treeKey: tree_key,
10148
+ items: grouped.get(bucket_key(lane_key, col.key)) ?? [],
10149
+ renderCard,
10150
+ emptyColumn,
10151
+ cardClassName,
10152
+ showEdit: show_edit,
10153
+ onEditRequest: handle_edit_request
10154
+ },
10155
+ col.key
10156
+ );
10157
+ }
10158
+ function render_lane_row(lane, tree_key, only_column) {
10159
+ const count = only_column ? (grouped.get(bucket_key(lane.key, only_column.key)) ?? []).length : lane_total(lane.key);
10160
+ if (lane.collapseWhenEmpty && count === 0) {
10161
+ return /* @__PURE__ */ jsxRuntime.jsx(KanbanLaneStrip, { lane, treeKey: tree_key }, lane.key);
10162
+ }
10163
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cls_hazo_kanban_lane", children: [
10164
+ /* @__PURE__ */ jsxRuntime.jsx(KanbanLaneHead, { lane, count, treeKey: tree_key }),
10165
+ only_column ? render_column(only_column, tree_key, lane.key) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_lane_grid grid gap-3", style: grid_style, children: columns.map((col) => render_column(col, tree_key, lane.key)) })
10166
+ ] }, lane.key);
10167
+ }
10168
+ function render_unmatched_lane(tree_key, only_column) {
10169
+ const count = only_column ? (grouped.get(bucket_key(UNMATCHED_LANE_KEY, only_column.key)) ?? []).length : lane_total(UNMATCHED_LANE_KEY);
10170
+ if (count === 0) return null;
10171
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_lane cls_hazo_kanban_lane_unmatched", children: only_column ? render_column(only_column, tree_key, UNMATCHED_LANE_KEY) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_lane_grid grid gap-3", style: grid_style, children: columns.map(
10172
+ (col) => render_column(col, tree_key, UNMATCHED_LANE_KEY)
10173
+ ) }) });
10174
+ }
10023
10175
  return /* @__PURE__ */ jsxRuntime.jsxs(
10024
10176
  core$1.DndContext,
10025
10177
  {
10026
10178
  id: dnd_context_id,
10027
10179
  sensors,
10028
- collisionDetection: core$1.closestCorners,
10180
+ collisionDetection: collision_detection,
10029
10181
  onDragStart: handle_drag_start,
10030
10182
  onDragEnd: handle_drag_end,
10031
10183
  onDragCancel: handle_drag_cancel,
@@ -10034,43 +10186,33 @@ function HazoUiKanban({
10034
10186
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("cls_hazo_kanban w-full", className), children: [
10035
10187
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("cls_hazo_kanban_mobile", mobile_classes), children: /* @__PURE__ */ jsxRuntime.jsxs(Tabs, { value: active_tab, onValueChange: set_active_tab, children: [
10036
10188
  /* @__PURE__ */ jsxRuntime.jsx(TabsList, { className: "grid w-full", style: { gridTemplateColumns: `repeat(${columns.length}, 1fr)` }, children: columns.map((col) => {
10037
- const count = (grouped.get(col.key) ?? []).length;
10189
+ const count = lanes ? lanes.reduce(
10190
+ (n, lane) => n + (grouped.get(bucket_key(lane.key, col.key)) ?? []).length,
10191
+ (grouped.get(bucket_key(UNMATCHED_LANE_KEY, col.key)) ?? []).length
10192
+ ) : (grouped.get(col.key) ?? []).length;
10038
10193
  return /* @__PURE__ */ jsxRuntime.jsxs(TabsTrigger, { value: col.key, className: "flex flex-col gap-0.5", children: [
10039
10194
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs", children: col.title }),
10040
10195
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground", children: count })
10041
10196
  ] }, col.key);
10042
10197
  }) }),
10043
- columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx(TabsContent, { value: col.key, className: "mt-3", children: /* @__PURE__ */ jsxRuntime.jsx(
10044
- KanbanColumn,
10045
- {
10046
- column: col,
10047
- items: grouped.get(col.key) ?? [],
10048
- renderCard,
10049
- emptyColumn,
10050
- cardClassName,
10051
- showEdit: show_edit,
10052
- onEditRequest: handle_edit_request
10053
- }
10054
- ) }, col.key))
10198
+ columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx(TabsContent, { value: col.key, className: "mt-3", children: lanes ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cls_hazo_kanban_lanes flex flex-col gap-4", children: [
10199
+ lanes.map((lane) => render_lane_row(lane, "m", col)),
10200
+ render_unmatched_lane("m", col)
10201
+ ] }) : render_column(col, "m") }, col.key))
10055
10202
  ] }) }),
10056
10203
  /* @__PURE__ */ jsxRuntime.jsx(
10057
10204
  "div",
10058
10205
  {
10059
- className: cn("cls_hazo_kanban_desktop gap-3", desktop_classes),
10060
- style: { gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))` },
10061
- children: columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx(
10062
- KanbanColumn,
10063
- {
10064
- column: col,
10065
- items: grouped.get(col.key) ?? [],
10066
- renderCard,
10067
- emptyColumn,
10068
- cardClassName,
10069
- showEdit: show_edit,
10070
- onEditRequest: handle_edit_request
10071
- },
10072
- col.key
10073
- ))
10206
+ className: cn(
10207
+ "cls_hazo_kanban_desktop",
10208
+ lanes ? "space-y-4" : "gap-3",
10209
+ desktop_classes
10210
+ ),
10211
+ style: lanes ? void 0 : grid_style,
10212
+ children: lanes ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
10213
+ lanes.map((lane) => render_lane_row(lane, "d")),
10214
+ render_unmatched_lane("d")
10215
+ ] }) : columns.map((col) => render_column(col, "d"))
10074
10216
  }
10075
10217
  ),
10076
10218
  /* @__PURE__ */ jsxRuntime.jsx(core$1.DragOverlay, { children: active_item ? /* @__PURE__ */ jsxRuntime.jsx(