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/CHANGE_LOG.md CHANGED
@@ -5,6 +5,124 @@ 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
+
83
+ ## v6.5.0 (2026-08-02)
84
+
85
+ ### Added — swimlanes for `HazoUiKanban`
86
+
87
+ A board can now declare `lanes`, rendering one full-width row per lane —
88
+ a wide lane heading, then that lane's own copy of the whole column grid. Where
89
+ `sections` (v6.4.0) group *within* a column, lanes group *across* all of them,
90
+ so the second axis can be the row axis instead of the inner one.
91
+
92
+ - `HazoUiKanbanProps.lanes?: KanbanLane[]` — `{ key, title, accepts?,
93
+ collapseWhenEmpty? }`. Cards are placed by the new `KanbanItem.laneKey`.
94
+ Omit `lanes` and nothing changes: existing boards render exactly as before.
95
+ - `KanbanMoveEvent` gained `fromLane`/`toLane`; `KanbanReorderEvent` gained
96
+ `laneKey`. Move-vs-reorder is now decided on all three axes — same column,
97
+ section *and* lane is a reorder; changing any one is a move.
98
+ - **A lane heading is the lane-only drop target.** It carries a lane but no
99
+ column, read as "change the lane, keep the card's column" — the mirror of a
100
+ sectioned column's head ("change the column, keep the section").
101
+ - `collapseWhenEmpty` renders a lane with no cards as a thin full-width dashed
102
+ strip, which is what makes "declare every lane" affordable for a long
103
+ vocabulary. A card whose `laneKey` matches no declared lane renders in an
104
+ unlabelled trailing lane rather than disappearing.
105
+ - The optimistic overlay tracks the destination lane too, so a cross-lane drop
106
+ moves immediately instead of snapping back until the consumer's async commit
107
+ lands.
108
+
109
+ ### Fixed — drops landed in the wrong column on tall or scrolled boards
110
+
111
+ Two long-standing defects in drop resolution, both of which lanes made
112
+ impossible to miss:
113
+
114
+ - **Collision detection is now pointer-first** (`pointerWithin`, falling back
115
+ to `rectIntersection`, then `closestCorners`). `closestCorners` alone
116
+ averages the distance to all four corners, so a column stretched tall by a
117
+ neighbour's card stack scored *worse* than a short, far-away column — a drop
118
+ well inside the right column could resolve to a different one entirely.
119
+ Keyboard drags have no pointer and take the same fallbacks as before.
120
+ - **Droppable ids are scoped per layout tree.** The mobile tab strip and the
121
+ desktop grid both always render (CSS hides one), so every column previously
122
+ registered twice under one id. dnd-kit keys its registry by id, so one node
123
+ won and collisions were measured against the CSS-hidden copy's 0×0 rect.
124
+ Ids now carry the tree ("m"/"d") and, on a laned board, the lane.
125
+
8
126
  ## v6.4.0 (2026-08-01)
9
127
 
10
128
  ### 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 |