hazo_ui 6.4.0 → 6.7.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/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import * as SelectPrimitive from '@radix-ui/react-select';
15
15
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
16
16
  import { DayPicker } from 'react-day-picker';
17
17
  import { format } from 'date-fns';
18
- import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter, closestCorners, DragOverlay, useDroppable } from '@dnd-kit/core';
18
+ import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter, pointerWithin, rectIntersection, closestCorners, DragOverlay, useDroppable } from '@dnd-kit/core';
19
19
  import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from '@dnd-kit/sortable';
20
20
  import { CSS } from '@dnd-kit/utilities';
21
21
  import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
@@ -9021,10 +9021,15 @@ function KanbanCard({
9021
9021
  isDragging
9022
9022
  } = useSortable({
9023
9023
  id: item.id,
9024
- // sectionKey rides along so a drop *onto a card* resolves to that card's
9025
- // section, not just its column (kanban_column.tsx's section bands cover
9026
- // the gaps between cards, but the cards themselves are the sortable).
9027
- data: { columnKey: item.columnKey, sectionKey: item.sectionKey }
9024
+ // sectionKey and laneKey ride along so a drop *onto a card* resolves to
9025
+ // that card's section and lane, not just its column (the section bands
9026
+ // and lane rows cover the gaps between cards, but the cards themselves
9027
+ // are the sortable).
9028
+ data: {
9029
+ columnKey: item.columnKey,
9030
+ sectionKey: item.sectionKey,
9031
+ laneKey: item.laneKey
9032
+ }
9028
9033
  });
9029
9034
  const priority_var = item.priority ? `--hazo-kanban-priority-${String(item.priority).toLowerCase()}` : null;
9030
9035
  const style = {
@@ -9033,6 +9038,12 @@ function KanbanCard({
9033
9038
  backgroundColor: "hsl(var(--hazo-kanban-card-bg))",
9034
9039
  border: `1px solid hsl(var(--hazo-kanban-card-border))`,
9035
9040
  borderLeft: priority_var ? `4px solid hsl(var(${priority_var}))` : `1px solid hsl(var(--hazo-kanban-card-border))`,
9041
+ // Opt-in lift. A card whose background equals the column's (the case for
9042
+ // every palette where --card and --background resolve to the same value)
9043
+ // is separated by nothing but a 1px border, which reads as flat at a
9044
+ // glance. The fallback is `none`, so a consumer that never sets the token
9045
+ // renders exactly as before.
9046
+ boxShadow: "var(--hazo-kanban-card-shadow, none)",
9036
9047
  opacity: isDragging && !dragging ? 0.4 : 1,
9037
9048
  cursor: "grab"
9038
9049
  };
@@ -9041,6 +9052,9 @@ function KanbanCard({
9041
9052
  {
9042
9053
  ref: setNodeRef,
9043
9054
  style,
9055
+ "data-column-key": item.columnKey,
9056
+ "data-lane-key": item.laneKey,
9057
+ "data-section-key": item.sectionKey,
9044
9058
  className: cn(
9045
9059
  "cls_hazo_kanban_card group relative rounded-md p-3 mb-2 select-none",
9046
9060
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
@@ -9085,8 +9099,16 @@ function KanbanCard({
9085
9099
  }
9086
9100
  );
9087
9101
  }
9088
- function section_droppable_id(column_key, section_key) {
9089
- return `section:${column_key}::${section_key}`;
9102
+ function tree_droppable_id(prefix, tree_key, lane_key, rest) {
9103
+ return lane_key == null ? `${prefix}:${tree_key}::${rest}` : `${prefix}:${tree_key}::${lane_key}::${rest}`;
9104
+ }
9105
+ function section_droppable_id(column_key, section_key, tree_key, lane_key) {
9106
+ return tree_droppable_id(
9107
+ "section",
9108
+ tree_key,
9109
+ lane_key,
9110
+ `${column_key}::${section_key}`
9111
+ );
9090
9112
  }
9091
9113
  function group_items_by_section(items, sections) {
9092
9114
  const bySection = /* @__PURE__ */ new Map();
@@ -9104,13 +9126,16 @@ function KanbanColumn({
9104
9126
  items,
9105
9127
  renderCard,
9106
9128
  emptyColumn,
9129
+ footer,
9107
9130
  cardClassName,
9108
9131
  showEdit,
9109
- onEditRequest
9132
+ onEditRequest,
9133
+ laneKey,
9134
+ treeKey
9110
9135
  }) {
9111
9136
  const { setNodeRef, isOver } = useDroppable({
9112
- id: `column:${column.key}`,
9113
- data: { columnKey: column.key, isColumn: true }
9137
+ id: tree_droppable_id("column", treeKey, laneKey, column.key),
9138
+ data: { columnKey: column.key, laneKey, isColumn: true }
9114
9139
  });
9115
9140
  const sections = column.sections;
9116
9141
  const { bySection, unmatched } = React27.useMemo(
@@ -9147,6 +9172,8 @@ function KanbanColumn({
9147
9172
  "div",
9148
9173
  {
9149
9174
  ref: setNodeRef,
9175
+ "data-column-key": column.key,
9176
+ "data-lane-key": laneKey,
9150
9177
  className: cn(
9151
9178
  "cls_hazo_kanban_column flex flex-col rounded-md p-2",
9152
9179
  isOver && "cls_hazo_kanban_column_over bg-accent/30 ring-2 ring-ring/40"
@@ -9156,6 +9183,8 @@ function KanbanColumn({
9156
9183
  KanbanColumnHead,
9157
9184
  {
9158
9185
  columnKey: column.key,
9186
+ laneKey,
9187
+ treeKey,
9159
9188
  title: column.title,
9160
9189
  count: items.length,
9161
9190
  droppable: sections != null
@@ -9166,49 +9195,66 @@ function KanbanColumn({
9166
9195
  {
9167
9196
  items: ordered.map((i) => i.id),
9168
9197
  strategy: verticalListSortingStrategy,
9169
- children: /* @__PURE__ */ jsx("div", { className: "cls_hazo_kanban_column_body flex-1 min-h-[40px]", children: !sections ? items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "cls_hazo_kanban_column_empty text-xs text-muted-foreground py-2 px-1", children: emptyColumn }) : render_cards(items) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
9170
- expanded_sections.map((section) => /* @__PURE__ */ jsx(
9171
- KanbanSectionBand,
9172
- {
9173
- columnKey: column.key,
9174
- section,
9175
- items: bySection.get(section.key) ?? [],
9176
- emptyColumn,
9177
- renderCards: render_cards
9178
- },
9179
- section.key
9180
- )),
9181
- unmatched.length > 0 && /* @__PURE__ */ jsx("div", { className: "cls_hazo_kanban_section cls_hazo_kanban_section_unmatched", children: render_cards(unmatched) }),
9182
- collapsed_sections.length > 0 && /* @__PURE__ */ jsx("div", { className: "cls_hazo_kanban_section_strips mt-2 flex flex-wrap gap-1 border-t border-dashed border-border pt-2", children: collapsed_sections.map((section) => /* @__PURE__ */ jsx(
9183
- KanbanSectionStrip,
9184
- {
9185
- columnKey: column.key,
9186
- section
9187
- },
9188
- section.key
9189
- )) })
9190
- ] }) })
9198
+ children: /* @__PURE__ */ jsx(
9199
+ "div",
9200
+ {
9201
+ "data-column-key": column.key,
9202
+ "data-lane-key": laneKey,
9203
+ className: "cls_hazo_kanban_column_body flex-1 min-h-[40px]",
9204
+ children: !sections ? items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "cls_hazo_kanban_column_empty text-xs text-muted-foreground py-2 px-1", children: emptyColumn }) : render_cards(items) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
9205
+ expanded_sections.map((section) => /* @__PURE__ */ jsx(
9206
+ KanbanSectionBand,
9207
+ {
9208
+ columnKey: column.key,
9209
+ laneKey,
9210
+ treeKey,
9211
+ section,
9212
+ items: bySection.get(section.key) ?? [],
9213
+ emptyColumn,
9214
+ renderCards: render_cards
9215
+ },
9216
+ section.key
9217
+ )),
9218
+ unmatched.length > 0 && /* @__PURE__ */ jsx("div", { className: "cls_hazo_kanban_section cls_hazo_kanban_section_unmatched", children: render_cards(unmatched) }),
9219
+ collapsed_sections.length > 0 && /* @__PURE__ */ jsx("div", { className: "cls_hazo_kanban_section_strips mt-2 flex flex-wrap gap-1 border-t border-dashed border-border pt-2", children: collapsed_sections.map((section) => /* @__PURE__ */ jsx(
9220
+ KanbanSectionStrip,
9221
+ {
9222
+ columnKey: column.key,
9223
+ laneKey,
9224
+ treeKey,
9225
+ section
9226
+ },
9227
+ section.key
9228
+ )) })
9229
+ ] })
9230
+ }
9231
+ )
9191
9232
  }
9192
- )
9233
+ ),
9234
+ footer ? /* @__PURE__ */ jsx("div", { className: "cls_hazo_kanban_column_footer mt-1", children: footer }) : null
9193
9235
  ]
9194
9236
  }
9195
9237
  );
9196
9238
  }
9197
9239
  function KanbanColumnHead({
9198
9240
  columnKey,
9241
+ laneKey,
9242
+ treeKey,
9199
9243
  title,
9200
9244
  count,
9201
9245
  droppable
9202
9246
  }) {
9203
9247
  const { setNodeRef, isOver } = useDroppable({
9204
- id: `columnhead:${columnKey}`,
9205
- data: { columnKey, isColumn: true },
9248
+ id: tree_droppable_id("columnhead", treeKey, laneKey, columnKey),
9249
+ data: { columnKey, laneKey, isColumn: true },
9206
9250
  disabled: !droppable
9207
9251
  });
9208
9252
  return /* @__PURE__ */ jsxs(
9209
9253
  "div",
9210
9254
  {
9211
9255
  ref: droppable ? setNodeRef : void 0,
9256
+ "data-column-key": columnKey,
9257
+ "data-lane-key": laneKey,
9212
9258
  className: cn(
9213
9259
  "cls_hazo_kanban_column_head mb-2 flex items-center justify-between rounded-sm px-1 text-xs uppercase tracking-wider text-muted-foreground",
9214
9260
  droppable && "py-1",
@@ -9223,14 +9269,16 @@ function KanbanColumnHead({
9223
9269
  }
9224
9270
  function KanbanSectionBand({
9225
9271
  columnKey,
9272
+ laneKey,
9273
+ treeKey,
9226
9274
  section,
9227
9275
  items,
9228
9276
  emptyColumn,
9229
9277
  renderCards
9230
9278
  }) {
9231
9279
  const { setNodeRef, isOver } = useDroppable({
9232
- id: section_droppable_id(columnKey, section.key),
9233
- data: { columnKey, sectionKey: section.key, isSection: true }
9280
+ id: section_droppable_id(columnKey, section.key, treeKey, laneKey),
9281
+ data: { columnKey, laneKey, sectionKey: section.key, isSection: true }
9234
9282
  });
9235
9283
  return /* @__PURE__ */ jsxs(
9236
9284
  "div",
@@ -9253,11 +9301,13 @@ function KanbanSectionBand({
9253
9301
  }
9254
9302
  function KanbanSectionStrip({
9255
9303
  columnKey,
9304
+ laneKey,
9305
+ treeKey,
9256
9306
  section
9257
9307
  }) {
9258
9308
  const { setNodeRef, isOver } = useDroppable({
9259
- id: section_droppable_id(columnKey, section.key),
9260
- data: { columnKey, sectionKey: section.key, isSection: true }
9309
+ id: section_droppable_id(columnKey, section.key, treeKey, laneKey),
9310
+ data: { columnKey, laneKey, sectionKey: section.key, isSection: true }
9261
9311
  });
9262
9312
  return /* @__PURE__ */ jsx(
9263
9313
  "div",
@@ -9271,6 +9321,58 @@ function KanbanSectionStrip({
9271
9321
  }
9272
9322
  );
9273
9323
  }
9324
+ function lane_droppable_id(lane_key, tree_key) {
9325
+ return `lane:${tree_key}::${lane_key}`;
9326
+ }
9327
+ function KanbanLaneHead({
9328
+ lane,
9329
+ count,
9330
+ treeKey
9331
+ }) {
9332
+ const { setNodeRef, isOver } = useDroppable({
9333
+ id: lane_droppable_id(lane.key, treeKey),
9334
+ data: { laneKey: lane.key, isLane: true }
9335
+ });
9336
+ return /* @__PURE__ */ jsxs(
9337
+ "div",
9338
+ {
9339
+ ref: setNodeRef,
9340
+ className: cn(
9341
+ "cls_hazo_kanban_lane_head mb-2 flex items-center gap-3 rounded-sm px-1 py-1",
9342
+ isOver && "cls_hazo_kanban_lane_head_over bg-accent/30 ring-1 ring-ring/40"
9343
+ ),
9344
+ children: [
9345
+ /* @__PURE__ */ jsx("span", { className: "cls_hazo_kanban_lane_title text-xs font-semibold uppercase tracking-wider text-foreground", children: lane.title }),
9346
+ /* @__PURE__ */ jsx("span", { className: "cls_hazo_kanban_lane_rule h-px flex-1 bg-border" }),
9347
+ lane.actions ? /* @__PURE__ */ jsx("span", { className: "cls_hazo_kanban_lane_actions flex items-center gap-1", children: lane.actions }) : null,
9348
+ /* @__PURE__ */ 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 })
9349
+ ]
9350
+ }
9351
+ );
9352
+ }
9353
+ function KanbanLaneStrip({
9354
+ lane,
9355
+ treeKey
9356
+ }) {
9357
+ const { setNodeRef, isOver } = useDroppable({
9358
+ id: lane_droppable_id(lane.key, treeKey),
9359
+ data: { laneKey: lane.key, isLane: true }
9360
+ });
9361
+ return /* @__PURE__ */ jsxs(
9362
+ "div",
9363
+ {
9364
+ ref: setNodeRef,
9365
+ className: cn(
9366
+ "cls_hazo_kanban_lane_strip mb-2 flex items-center gap-3 rounded-md border border-dashed border-border px-2 py-1 text-[11px] uppercase tracking-wider text-muted-foreground",
9367
+ isOver && "cls_hazo_kanban_lane_strip_over border-solid bg-accent/30 text-foreground ring-1 ring-ring/30"
9368
+ ),
9369
+ children: [
9370
+ /* @__PURE__ */ jsx("span", { className: "cls_hazo_kanban_lane_title", children: lane.title }),
9371
+ lane.actions ? /* @__PURE__ */ jsx("span", { className: "cls_hazo_kanban_lane_actions ml-auto flex items-center gap-1", children: lane.actions }) : null
9372
+ ]
9373
+ }
9374
+ );
9375
+ }
9274
9376
  var DEFAULT_PRIORITIES = ["P0", "P1", "P2", "P3"];
9275
9377
  function humanize(key) {
9276
9378
  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(" ");
@@ -9677,6 +9779,10 @@ function applyKanbanFilter(items, filter) {
9677
9779
  return true;
9678
9780
  });
9679
9781
  }
9782
+ var UNMATCHED_LANE_KEY = "__hazo_lane_unmatched__";
9783
+ function bucket_key(lane_key, column_key) {
9784
+ return lane_key == null ? column_key : `${lane_key}\0${column_key}`;
9785
+ }
9680
9786
  function default_announcements(itemLabel) {
9681
9787
  return {
9682
9788
  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.`,
@@ -9685,38 +9791,50 @@ function default_announcements(itemLabel) {
9685
9791
  onDragCancel: ({ item, fromColumn }) => `Movement cancelled. ${itemLabel(item)} returned to ${fromColumn}.`
9686
9792
  };
9687
9793
  }
9688
- function group_items_by_column(items, overlay, columns) {
9794
+ function group_items_by_column(items, overlay, columns, lanes) {
9689
9795
  const out = /* @__PURE__ */ new Map();
9690
- for (const col of columns) out.set(col.key, []);
9796
+ const lane_keys = lanes ? [...lanes.map((l) => l.key), UNMATCHED_LANE_KEY] : [void 0];
9797
+ for (const lane_key of lane_keys)
9798
+ for (const col of columns) out.set(bucket_key(lane_key, col.key), []);
9691
9799
  const overlay_inserts = /* @__PURE__ */ new Map();
9692
- for (const col of columns) overlay_inserts.set(col.key, []);
9800
+ for (const key of out.keys()) overlay_inserts.set(key, []);
9801
+ const declared = lanes ? new Set(lanes.map((l) => l.key)) : null;
9802
+ function resolve_lane(lane_key) {
9803
+ if (!declared) return void 0;
9804
+ return lane_key != null && declared.has(lane_key) ? lane_key : UNMATCHED_LANE_KEY;
9805
+ }
9693
9806
  for (const item of items) {
9694
9807
  const ov = overlay.get(item.id);
9695
9808
  if (ov) {
9696
- const bucket = overlay_inserts.get(ov.columnKey);
9809
+ const bucket = overlay_inserts.get(
9810
+ bucket_key(resolve_lane(ov.laneKey), ov.columnKey)
9811
+ );
9697
9812
  if (bucket) {
9698
- const placed = ov.sectionKey === item.sectionKey ? item : { ...item, sectionKey: ov.sectionKey };
9813
+ const placed = ov.sectionKey === item.sectionKey && ov.laneKey === item.laneKey ? item : { ...item, sectionKey: ov.sectionKey, laneKey: ov.laneKey };
9699
9814
  bucket.push({ item: placed, index: ov.index });
9700
9815
  }
9701
9816
  } else {
9702
- const bucket = out.get(item.columnKey);
9817
+ const bucket = out.get(
9818
+ bucket_key(resolve_lane(item.laneKey), item.columnKey)
9819
+ );
9703
9820
  if (bucket) bucket.push(item);
9704
9821
  }
9705
9822
  }
9706
- for (const col of columns) {
9707
- const inserts = overlay_inserts.get(col.key) ?? [];
9823
+ for (const [key, inserts] of overlay_inserts) {
9824
+ if (inserts.length === 0) continue;
9708
9825
  inserts.sort((a, b) => a.index - b.index);
9709
- const arr = out.get(col.key) ?? [];
9826
+ const arr = out.get(key) ?? [];
9710
9827
  for (const { item, index } of inserts) {
9711
9828
  const at = Math.max(0, Math.min(index, arr.length));
9712
9829
  arr.splice(at, 0, item);
9713
9830
  }
9714
- out.set(col.key, arr);
9831
+ out.set(key, arr);
9715
9832
  }
9716
9833
  return out;
9717
9834
  }
9718
9835
  function HazoUiKanban({
9719
9836
  columns,
9837
+ lanes,
9720
9838
  items,
9721
9839
  renderCard,
9722
9840
  onMove,
@@ -9724,6 +9842,7 @@ function HazoUiKanban({
9724
9842
  mobileBreakpoint = 640,
9725
9843
  announcements,
9726
9844
  emptyColumn = "\u2014",
9845
+ renderColumnFooter,
9727
9846
  className,
9728
9847
  cardClassName,
9729
9848
  itemLabel,
@@ -9764,7 +9883,7 @@ function HazoUiKanban({
9764
9883
  const next = new Map(prev);
9765
9884
  for (const [item_id, pos] of prev.entries()) {
9766
9885
  const item = items.find((i) => i.id === item_id);
9767
- if (item && item.columnKey === pos.columnKey && item.sectionKey === pos.sectionKey) {
9886
+ if (item && item.columnKey === pos.columnKey && item.sectionKey === pos.sectionKey && item.laneKey === pos.laneKey) {
9768
9887
  next.delete(item_id);
9769
9888
  changed = true;
9770
9889
  }
@@ -9773,8 +9892,8 @@ function HazoUiKanban({
9773
9892
  });
9774
9893
  }, [items, overlay]);
9775
9894
  const grouped = React27.useMemo(
9776
- () => group_items_by_column(items, overlay, columns),
9777
- [items, overlay, columns]
9895
+ () => group_items_by_column(items, overlay, columns, lanes),
9896
+ [items, overlay, columns, lanes]
9778
9897
  );
9779
9898
  const find_item = React27.useCallback(
9780
9899
  (id) => items.find((i) => i.id === id),
@@ -9784,6 +9903,13 @@ function HazoUiKanban({
9784
9903
  useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
9785
9904
  useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
9786
9905
  );
9906
+ const collision_detection = React27.useCallback((args) => {
9907
+ const under_pointer = pointerWithin(args);
9908
+ if (under_pointer.length > 0) return under_pointer;
9909
+ const overlapping = rectIntersection(args);
9910
+ if (overlapping.length > 0) return overlapping;
9911
+ return closestCorners(args);
9912
+ }, []);
9787
9913
  const merged_announcements = React27.useMemo(() => {
9788
9914
  const defaults = default_announcements(label_fn);
9789
9915
  return {
@@ -9812,7 +9938,7 @@ function HazoUiKanban({
9812
9938
  const id = String(active.id);
9813
9939
  const item = find_item(id);
9814
9940
  if (!item) return "";
9815
- const over_col_key = over ? over.data?.current?.columnKey ?? String(over.id).replace(/^column:/, "") : null;
9941
+ const over_col_key = over ? over.data?.current?.columnKey ?? item.columnKey : null;
9816
9942
  const col = over_col_key ? columns.find((c) => c.key === over_col_key) : null;
9817
9943
  return merged_announcements.onDragOver({
9818
9944
  item,
@@ -9827,10 +9953,13 @@ function HazoUiKanban({
9827
9953
  const item = find_item(id);
9828
9954
  if (!item) return "";
9829
9955
  const from_col = columns.find((c) => c.key === item.columnKey);
9830
- const over_col_key = over ? over.data?.current?.columnKey ?? String(over.id).replace(/^column:/, "") : null;
9956
+ const over_col_key = over?.data?.current?.columnKey ?? item.columnKey;
9957
+ const over_lane_key = lanes ? over?.data?.current?.laneKey ?? item.laneKey : void 0;
9831
9958
  const to_col = over_col_key ? columns.find((c) => c.key === over_col_key) ?? from_col : from_col;
9832
- const target = over_col_key ? grouped.get(over_col_key) ?? [] : grouped.get(item.columnKey) ?? [];
9833
- const on_shell = !over || String(over.id).startsWith("column:") || String(over.id).startsWith("columnhead:") || String(over.id).startsWith("section:");
9959
+ const target = grouped.get(
9960
+ bucket_key(over_lane_key, over_col_key ?? item.columnKey)
9961
+ ) ?? [];
9962
+ const on_shell = !over || String(over.id).startsWith("column:") || String(over.id).startsWith("columnhead:") || String(over.id).startsWith("section:") || String(over.id).startsWith("lane:");
9834
9963
  const new_index = on_shell ? target.length : target.findIndex((it) => it.id === String(over.id));
9835
9964
  return merged_announcements.onDragEnd({
9836
9965
  item,
@@ -9850,7 +9979,7 @@ function HazoUiKanban({
9850
9979
  });
9851
9980
  }
9852
9981
  }),
9853
- [find_item, columns, grouped, merged_announcements]
9982
+ [find_item, columns, lanes, grouped, merged_announcements]
9854
9983
  );
9855
9984
  const handle_drag_start = React27.useCallback((event) => {
9856
9985
  set_active_id(String(event.active.id));
@@ -9877,31 +10006,35 @@ function HazoUiKanban({
9877
10006
  if (!event.over) return;
9878
10007
  const over_id = String(event.over.id);
9879
10008
  const over_data = event.over.data?.current;
9880
- const dest_column_key = over_data?.columnKey ?? over_id.replace(/^column:/, "");
10009
+ const dest_column_key = over_data?.columnKey ?? item.columnKey;
9881
10010
  const dest_column = columns.find((c) => c.key === dest_column_key);
9882
10011
  if (!dest_column) return;
10012
+ const dest_lane_key = lanes ? over_data?.laneKey ?? item.laneKey : void 0;
10013
+ const dest_lane = lanes?.find((l) => l.key === dest_lane_key);
10014
+ if (lanes && dest_lane?.accepts && !dest_lane.accepts(item)) return;
9883
10015
  const dest_section_key = dest_column.sections ? over_data?.sectionKey ?? item.sectionKey : void 0;
9884
10016
  const dest_section = dest_column.sections?.find(
9885
10017
  (s) => s.key === dest_section_key
9886
10018
  );
9887
10019
  if (dest_column.accepts && !dest_column.accepts(item)) return;
9888
10020
  if (dest_section?.accepts && !dest_section.accepts(item)) return;
9889
- const dest_items_pre_overlay = grouped.get(dest_column_key) ?? [];
10021
+ const dest_items_pre_overlay = grouped.get(bucket_key(dest_lane_key, dest_column_key)) ?? [];
9890
10022
  let new_index;
9891
- if (over_data?.isColumn || over_data?.isSection) {
10023
+ if (over_data?.isColumn || over_data?.isSection || over_data?.isLane) {
9892
10024
  new_index = dest_items_pre_overlay.length;
9893
10025
  } else {
9894
10026
  const idx = dest_items_pre_overlay.findIndex((i) => i.id === over_id);
9895
10027
  new_index = idx === -1 ? dest_items_pre_overlay.length : idx;
9896
10028
  }
9897
10029
  const handle = make_revert_handle(active_id_str);
9898
- if (item.columnKey === dest_column_key && item.sectionKey === dest_section_key) {
10030
+ if (item.columnKey === dest_column_key && item.sectionKey === dest_section_key && item.laneKey === dest_lane_key) {
9899
10031
  if (active_id_str === over_id) return;
9900
10032
  set_overlay((prev) => {
9901
10033
  const next = new Map(prev);
9902
10034
  next.set(active_id_str, {
9903
10035
  columnKey: dest_column_key,
9904
10036
  sectionKey: dest_section_key,
10037
+ laneKey: dest_lane_key,
9905
10038
  index: new_index
9906
10039
  });
9907
10040
  return next;
@@ -9911,6 +10044,7 @@ function HazoUiKanban({
9911
10044
  item,
9912
10045
  columnKey: dest_column_key,
9913
10046
  sectionKey: dest_section_key,
10047
+ laneKey: dest_lane_key,
9914
10048
  newIndex: new_index,
9915
10049
  revert: handle.revert
9916
10050
  };
@@ -9922,6 +10056,7 @@ function HazoUiKanban({
9922
10056
  next.set(active_id_str, {
9923
10057
  columnKey: dest_column_key,
9924
10058
  sectionKey: dest_section_key,
10059
+ laneKey: dest_lane_key,
9925
10060
  index: new_index
9926
10061
  });
9927
10062
  return next;
@@ -9933,12 +10068,14 @@ function HazoUiKanban({
9933
10068
  toColumn: dest_column_key,
9934
10069
  fromSection: item.sectionKey,
9935
10070
  toSection: dest_section_key,
10071
+ fromLane: item.laneKey,
10072
+ toLane: dest_lane_key,
9936
10073
  newIndex: new_index,
9937
10074
  revert: handle.revert
9938
10075
  };
9939
10076
  onMove?.(move_event);
9940
10077
  },
9941
- [find_item, columns, grouped, onMove, onReorder, make_revert_handle]
10078
+ [find_item, columns, lanes, grouped, onMove, onReorder, make_revert_handle]
9942
10079
  );
9943
10080
  const handle_drag_cancel = React27.useCallback(() => {
9944
10081
  set_active_id(null);
@@ -9949,13 +10086,57 @@ function HazoUiKanban({
9949
10086
  `(max-width: ${Math.max(0, mobileBreakpoint - 1)}px)`
9950
10087
  );
9951
10088
  const mobile_classes = is_custom_breakpoint ? is_mobile_via_query ? "block" : "hidden" : "sm:hidden";
9952
- const desktop_classes = is_custom_breakpoint ? is_mobile_via_query ? "hidden" : "grid" : "hidden sm:grid";
10089
+ const desktop_classes = is_custom_breakpoint ? is_mobile_via_query ? "hidden" : lanes ? "block" : "grid" : lanes ? "hidden sm:block" : "hidden sm:grid";
10090
+ const grid_style = {
10091
+ gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))`
10092
+ };
10093
+ function lane_total(lane_key) {
10094
+ let total = 0;
10095
+ for (const col of columns)
10096
+ total += (grouped.get(bucket_key(lane_key, col.key)) ?? []).length;
10097
+ return total;
10098
+ }
10099
+ function render_column(col, tree_key, lane_key, with_footer = true) {
10100
+ return /* @__PURE__ */ jsx(
10101
+ KanbanColumn,
10102
+ {
10103
+ column: col,
10104
+ laneKey: lane_key,
10105
+ treeKey: tree_key,
10106
+ items: grouped.get(bucket_key(lane_key, col.key)) ?? [],
10107
+ renderCard,
10108
+ emptyColumn,
10109
+ footer: with_footer ? renderColumnFooter?.({ columnKey: col.key, laneKey: lane_key }) ?? null : null,
10110
+ cardClassName,
10111
+ showEdit: show_edit,
10112
+ onEditRequest: handle_edit_request
10113
+ },
10114
+ col.key
10115
+ );
10116
+ }
10117
+ function render_lane_row(lane, tree_key, only_column) {
10118
+ const count = only_column ? (grouped.get(bucket_key(lane.key, only_column.key)) ?? []).length : lane_total(lane.key);
10119
+ if (lane.collapseWhenEmpty && count === 0) {
10120
+ return /* @__PURE__ */ jsx(KanbanLaneStrip, { lane, treeKey: tree_key }, lane.key);
10121
+ }
10122
+ return /* @__PURE__ */ jsxs("div", { className: "cls_hazo_kanban_lane", children: [
10123
+ /* @__PURE__ */ jsx(KanbanLaneHead, { lane, count, treeKey: tree_key }),
10124
+ only_column ? render_column(only_column, tree_key, lane.key) : /* @__PURE__ */ 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)) })
10125
+ ] }, lane.key);
10126
+ }
10127
+ function render_unmatched_lane(tree_key, only_column) {
10128
+ const count = only_column ? (grouped.get(bucket_key(UNMATCHED_LANE_KEY, only_column.key)) ?? []).length : lane_total(UNMATCHED_LANE_KEY);
10129
+ if (count === 0) return null;
10130
+ return /* @__PURE__ */ jsx("div", { className: "cls_hazo_kanban_lane cls_hazo_kanban_lane_unmatched", children: only_column ? render_column(only_column, tree_key, UNMATCHED_LANE_KEY, false) : /* @__PURE__ */ jsx("div", { className: "cls_hazo_kanban_lane_grid grid gap-3", style: grid_style, children: columns.map(
10131
+ (col) => render_column(col, tree_key, UNMATCHED_LANE_KEY, false)
10132
+ ) }) });
10133
+ }
9953
10134
  return /* @__PURE__ */ jsxs(
9954
10135
  DndContext,
9955
10136
  {
9956
10137
  id: dnd_context_id,
9957
10138
  sensors,
9958
- collisionDetection: closestCorners,
10139
+ collisionDetection: collision_detection,
9959
10140
  onDragStart: handle_drag_start,
9960
10141
  onDragEnd: handle_drag_end,
9961
10142
  onDragCancel: handle_drag_cancel,
@@ -9964,43 +10145,33 @@ function HazoUiKanban({
9964
10145
  /* @__PURE__ */ jsxs("div", { className: cn("cls_hazo_kanban w-full", className), children: [
9965
10146
  /* @__PURE__ */ jsx("div", { className: cn("cls_hazo_kanban_mobile", mobile_classes), children: /* @__PURE__ */ jsxs(Tabs, { value: active_tab, onValueChange: set_active_tab, children: [
9966
10147
  /* @__PURE__ */ jsx(TabsList, { className: "grid w-full", style: { gridTemplateColumns: `repeat(${columns.length}, 1fr)` }, children: columns.map((col) => {
9967
- const count = (grouped.get(col.key) ?? []).length;
10148
+ const count = lanes ? lanes.reduce(
10149
+ (n, lane) => n + (grouped.get(bucket_key(lane.key, col.key)) ?? []).length,
10150
+ (grouped.get(bucket_key(UNMATCHED_LANE_KEY, col.key)) ?? []).length
10151
+ ) : (grouped.get(col.key) ?? []).length;
9968
10152
  return /* @__PURE__ */ jsxs(TabsTrigger, { value: col.key, className: "flex flex-col gap-0.5", children: [
9969
10153
  /* @__PURE__ */ jsx("span", { className: "text-xs", children: col.title }),
9970
10154
  /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: count })
9971
10155
  ] }, col.key);
9972
10156
  }) }),
9973
- columns.map((col) => /* @__PURE__ */ jsx(TabsContent, { value: col.key, className: "mt-3", children: /* @__PURE__ */ jsx(
9974
- KanbanColumn,
9975
- {
9976
- column: col,
9977
- items: grouped.get(col.key) ?? [],
9978
- renderCard,
9979
- emptyColumn,
9980
- cardClassName,
9981
- showEdit: show_edit,
9982
- onEditRequest: handle_edit_request
9983
- }
9984
- ) }, col.key))
10157
+ columns.map((col) => /* @__PURE__ */ jsx(TabsContent, { value: col.key, className: "mt-3", children: lanes ? /* @__PURE__ */ jsxs("div", { className: "cls_hazo_kanban_lanes flex flex-col gap-4", children: [
10158
+ lanes.map((lane) => render_lane_row(lane, "m", col)),
10159
+ render_unmatched_lane("m", col)
10160
+ ] }) : render_column(col, "m") }, col.key))
9985
10161
  ] }) }),
9986
10162
  /* @__PURE__ */ jsx(
9987
10163
  "div",
9988
10164
  {
9989
- className: cn("cls_hazo_kanban_desktop gap-3", desktop_classes),
9990
- style: { gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))` },
9991
- children: columns.map((col) => /* @__PURE__ */ jsx(
9992
- KanbanColumn,
9993
- {
9994
- column: col,
9995
- items: grouped.get(col.key) ?? [],
9996
- renderCard,
9997
- emptyColumn,
9998
- cardClassName,
9999
- showEdit: show_edit,
10000
- onEditRequest: handle_edit_request
10001
- },
10002
- col.key
10003
- ))
10165
+ className: cn(
10166
+ "cls_hazo_kanban_desktop",
10167
+ lanes ? "space-y-4" : "gap-3",
10168
+ desktop_classes
10169
+ ),
10170
+ style: lanes ? void 0 : grid_style,
10171
+ children: lanes ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
10172
+ lanes.map((lane) => render_lane_row(lane, "d")),
10173
+ render_unmatched_lane("d")
10174
+ ] }) : columns.map((col) => render_column(col, "d"))
10004
10175
  }
10005
10176
  ),
10006
10177
  /* @__PURE__ */ jsx(DragOverlay, { children: active_item ? /* @__PURE__ */ jsx(