hazo_ui 6.5.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/CHANGE_LOG.md CHANGED
@@ -5,6 +5,81 @@ 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.7.0 (2026-08-02)
9
+
10
+ ### Added — `data-column-key` / `data-lane-key` on the kanban DOM
11
+
12
+ `cls_hazo_kanban_column` is one class shared by every column, so a consumer
13
+ whose columns carry meaning — an inbox is not a done pile — had no way to
14
+ style one differently. The only lever available was `:nth-child`, which
15
+ silently mis-paints the moment columns are reordered, filtered, or a lane
16
+ renders a subset. These attributes make the identity the component already
17
+ knows about addressable from CSS:
18
+
19
+ | Element | Attributes |
20
+ |---|---|
21
+ | `.cls_hazo_kanban_column` | `data-column-key`, `data-lane-key` |
22
+ | `.cls_hazo_kanban_column_head` | `data-column-key`, `data-lane-key` |
23
+ | `.cls_hazo_kanban_column_body` | `data-column-key`, `data-lane-key` |
24
+ | `.cls_hazo_kanban_card` | `data-column-key`, `data-lane-key`, `data-section-key` |
25
+
26
+ `data-lane-key` is absent (not empty) on an unlaned board, so
27
+ `[data-lane-key]` is a usable "is this board laned" test. Both layout trees
28
+ always render — pair with the ancestor `.cls_hazo_kanban_desktop` /
29
+ `.cls_hazo_kanban_mobile` when a rule must apply to only one.
30
+
31
+ Purely additive: no class, layout, or default paint changed.
32
+
33
+ ### Added — `--hazo-kanban-card-shadow`
34
+
35
+ Cards drew `background: hsl(var(--hazo-kanban-card-bg))` with a 1px border
36
+ and nothing else. Under any palette where `--card` and `--background` resolve
37
+ to the same value (the shadcn-derived default in `hazo_theme`, in both light
38
+ and dark), that border is the *only* thing separating a card from the column
39
+ behind it, and the board reads as flat.
40
+
41
+ The card's `box-shadow` now resolves `var(--hazo-kanban-card-shadow, none)`.
42
+ The fallback is `none`, so a consumer that never declares the token renders
43
+ byte-identically to v6.6.0. Set it (e.g. to `var(--shadow-sm)`) to give cards
44
+ a real lift.
45
+
46
+ ## v6.6.0 (2026-08-02)
47
+
48
+ ### Added — consumer slots on a laned `HazoUiKanban`
49
+
50
+ v6.5.0 gave the board a second, outer axis but no way to hang anything off
51
+ it. A lane could be dropped into and counted, and that was all — there was no
52
+ supported place to put "add an item to this lane", a per-lane overflow menu,
53
+ or an inline composer at the foot of a column. Both slots below are additive
54
+ and default to rendering nothing, so existing boards are byte-identical.
55
+
56
+ - **`KanbanLane.actions?: React.ReactNode`** — rendered in the lane heading
57
+ between the rule and the count, and right-aligned on a `collapseWhenEmpty`
58
+ lane's drop strip so a lane's controls do not vanish the moment it empties.
59
+ No `stopPropagation` is needed on controls placed here: a lane heading is a
60
+ `useDroppable` node, which registers a rect but attaches no pointer
61
+ handlers, unlike the `useSortable` card shell.
62
+ - **`HazoUiKanbanProps.renderColumnFooter?: (ctx) => React.ReactNode`** —
63
+ rendered under a column body, below the cards or the `emptyColumn`
64
+ placeholder. `ctx` is `{ columnKey, laneKey }` and the function is called
65
+ once per rendered copy of a column, so on a laned board it fires once per
66
+ (lane, column) pair — enough to build a composer scoped to one lane's
67
+ Inbox. Return `null` for the columns that should not have one.
68
+ - The footer sits inside the column's droppable but **outside** its
69
+ `SortableContext`, so it never joins the sort order or becomes a drop
70
+ target of its own.
71
+ - The unmatched-lane row is deliberately skipped: its lane key is the
72
+ private `__hazo_lane_unmatched__` sentinel, never a lane the consumer
73
+ declared, so a footer there would both leak an internal constant and ask
74
+ for controls scoped to a lane that does not exist.
75
+
76
+ New class hooks: `cls_hazo_kanban_lane_actions`,
77
+ `cls_hazo_kanban_column_footer`. The lane strip's title is now wrapped in its
78
+ own `cls_hazo_kanban_lane_title` span (it was a bare text node) so the
79
+ right-aligned actions have something to sit beside.
80
+
81
+ See `test-app/app/kanban/` §12.
82
+
8
83
  ## v6.5.0 (2026-08-02)
9
84
 
10
85
  ### Added — swimlanes for `HazoUiKanban`
package/dist/index.cjs CHANGED
@@ -9108,6 +9108,12 @@ function KanbanCard({
9108
9108
  backgroundColor: "hsl(var(--hazo-kanban-card-bg))",
9109
9109
  border: `1px solid hsl(var(--hazo-kanban-card-border))`,
9110
9110
  borderLeft: priority_var ? `4px solid hsl(var(${priority_var}))` : `1px solid hsl(var(--hazo-kanban-card-border))`,
9111
+ // Opt-in lift. A card whose background equals the column's (the case for
9112
+ // every palette where --card and --background resolve to the same value)
9113
+ // is separated by nothing but a 1px border, which reads as flat at a
9114
+ // glance. The fallback is `none`, so a consumer that never sets the token
9115
+ // renders exactly as before.
9116
+ boxShadow: "var(--hazo-kanban-card-shadow, none)",
9111
9117
  opacity: isDragging && !dragging ? 0.4 : 1,
9112
9118
  cursor: "grab"
9113
9119
  };
@@ -9116,6 +9122,9 @@ function KanbanCard({
9116
9122
  {
9117
9123
  ref: setNodeRef,
9118
9124
  style,
9125
+ "data-column-key": item.columnKey,
9126
+ "data-lane-key": item.laneKey,
9127
+ "data-section-key": item.sectionKey,
9119
9128
  className: cn(
9120
9129
  "cls_hazo_kanban_card group relative rounded-md p-3 mb-2 select-none",
9121
9130
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
@@ -9187,6 +9196,7 @@ function KanbanColumn({
9187
9196
  items,
9188
9197
  renderCard,
9189
9198
  emptyColumn,
9199
+ footer,
9190
9200
  cardClassName,
9191
9201
  showEdit,
9192
9202
  onEditRequest,
@@ -9232,6 +9242,8 @@ function KanbanColumn({
9232
9242
  "div",
9233
9243
  {
9234
9244
  ref: setNodeRef,
9245
+ "data-column-key": column.key,
9246
+ "data-lane-key": laneKey,
9235
9247
  className: cn(
9236
9248
  "cls_hazo_kanban_column flex flex-col rounded-md p-2",
9237
9249
  isOver && "cls_hazo_kanban_column_over bg-accent/30 ring-2 ring-ring/40"
@@ -9253,34 +9265,43 @@ function KanbanColumn({
9253
9265
  {
9254
9266
  items: ordered.map((i) => i.id),
9255
9267
  strategy: sortable.verticalListSortingStrategy,
9256
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_column_body flex-1 min-h-[40px]", children: !sections ? items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_column_empty text-xs text-muted-foreground py-2 px-1", children: emptyColumn }) : render_cards(items) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9257
- expanded_sections.map((section) => /* @__PURE__ */ jsxRuntime.jsx(
9258
- KanbanSectionBand,
9259
- {
9260
- columnKey: column.key,
9261
- laneKey,
9262
- treeKey,
9263
- section,
9264
- items: bySection.get(section.key) ?? [],
9265
- emptyColumn,
9266
- renderCards: render_cards
9267
- },
9268
- section.key
9269
- )),
9270
- unmatched.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_section cls_hazo_kanban_section_unmatched", children: render_cards(unmatched) }),
9271
- collapsed_sections.length > 0 && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
9272
- KanbanSectionStrip,
9273
- {
9274
- columnKey: column.key,
9275
- laneKey,
9276
- treeKey,
9277
- section
9278
- },
9279
- section.key
9280
- )) })
9281
- ] }) })
9268
+ children: /* @__PURE__ */ jsxRuntime.jsx(
9269
+ "div",
9270
+ {
9271
+ "data-column-key": column.key,
9272
+ "data-lane-key": laneKey,
9273
+ className: "cls_hazo_kanban_column_body flex-1 min-h-[40px]",
9274
+ children: !sections ? items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_column_empty text-xs text-muted-foreground py-2 px-1", children: emptyColumn }) : render_cards(items) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9275
+ expanded_sections.map((section) => /* @__PURE__ */ jsxRuntime.jsx(
9276
+ KanbanSectionBand,
9277
+ {
9278
+ columnKey: column.key,
9279
+ laneKey,
9280
+ treeKey,
9281
+ section,
9282
+ items: bySection.get(section.key) ?? [],
9283
+ emptyColumn,
9284
+ renderCards: render_cards
9285
+ },
9286
+ section.key
9287
+ )),
9288
+ unmatched.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_section cls_hazo_kanban_section_unmatched", children: render_cards(unmatched) }),
9289
+ collapsed_sections.length > 0 && /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
9290
+ KanbanSectionStrip,
9291
+ {
9292
+ columnKey: column.key,
9293
+ laneKey,
9294
+ treeKey,
9295
+ section
9296
+ },
9297
+ section.key
9298
+ )) })
9299
+ ] })
9300
+ }
9301
+ )
9282
9302
  }
9283
- )
9303
+ ),
9304
+ footer ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_column_footer mt-1", children: footer }) : null
9284
9305
  ]
9285
9306
  }
9286
9307
  );
@@ -9302,6 +9323,8 @@ function KanbanColumnHead({
9302
9323
  "div",
9303
9324
  {
9304
9325
  ref: droppable ? setNodeRef : void 0,
9326
+ "data-column-key": columnKey,
9327
+ "data-lane-key": laneKey,
9305
9328
  className: cn(
9306
9329
  "cls_hazo_kanban_column_head mb-2 flex items-center justify-between rounded-sm px-1 text-xs uppercase tracking-wider text-muted-foreground",
9307
9330
  droppable && "py-1",
@@ -9391,6 +9414,7 @@ function KanbanLaneHead({
9391
9414
  children: [
9392
9415
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_lane_title text-xs font-semibold uppercase tracking-wider text-foreground", children: lane.title }),
9393
9416
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_lane_rule h-px flex-1 bg-border" }),
9417
+ lane.actions ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_lane_actions flex items-center gap-1", children: lane.actions }) : null,
9394
9418
  /* @__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
9419
  ]
9396
9420
  }
@@ -9404,15 +9428,18 @@ function KanbanLaneStrip({
9404
9428
  id: lane_droppable_id(lane.key, treeKey),
9405
9429
  data: { laneKey: lane.key, isLane: true }
9406
9430
  });
9407
- return /* @__PURE__ */ jsxRuntime.jsx(
9431
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9408
9432
  "div",
9409
9433
  {
9410
9434
  ref: setNodeRef,
9411
9435
  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",
9436
+ "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",
9413
9437
  isOver && "cls_hazo_kanban_lane_strip_over border-solid bg-accent/30 text-foreground ring-1 ring-ring/30"
9414
9438
  ),
9415
- children: lane.title
9439
+ children: [
9440
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_lane_title", children: lane.title }),
9441
+ lane.actions ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_lane_actions ml-auto flex items-center gap-1", children: lane.actions }) : null
9442
+ ]
9416
9443
  }
9417
9444
  );
9418
9445
  }
@@ -9885,6 +9912,7 @@ function HazoUiKanban({
9885
9912
  mobileBreakpoint = 640,
9886
9913
  announcements,
9887
9914
  emptyColumn = "\u2014",
9915
+ renderColumnFooter,
9888
9916
  className,
9889
9917
  cardClassName,
9890
9918
  itemLabel,
@@ -10138,7 +10166,7 @@ function HazoUiKanban({
10138
10166
  total += (grouped.get(bucket_key(lane_key, col.key)) ?? []).length;
10139
10167
  return total;
10140
10168
  }
10141
- function render_column(col, tree_key, lane_key) {
10169
+ function render_column(col, tree_key, lane_key, with_footer = true) {
10142
10170
  return /* @__PURE__ */ jsxRuntime.jsx(
10143
10171
  KanbanColumn,
10144
10172
  {
@@ -10148,6 +10176,7 @@ function HazoUiKanban({
10148
10176
  items: grouped.get(bucket_key(lane_key, col.key)) ?? [],
10149
10177
  renderCard,
10150
10178
  emptyColumn,
10179
+ footer: with_footer ? renderColumnFooter?.({ columnKey: col.key, laneKey: lane_key }) ?? null : null,
10151
10180
  cardClassName,
10152
10181
  showEdit: show_edit,
10153
10182
  onEditRequest: handle_edit_request
@@ -10168,8 +10197,8 @@ function HazoUiKanban({
10168
10197
  function render_unmatched_lane(tree_key, only_column) {
10169
10198
  const count = only_column ? (grouped.get(bucket_key(UNMATCHED_LANE_KEY, only_column.key)) ?? []).length : lane_total(UNMATCHED_LANE_KEY);
10170
10199
  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)
10200
+ 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, false) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_lane_grid grid gap-3", style: grid_style, children: columns.map(
10201
+ (col) => render_column(col, tree_key, UNMATCHED_LANE_KEY, false)
10173
10202
  ) }) });
10174
10203
  }
10175
10204
  return /* @__PURE__ */ jsxRuntime.jsxs(