hazo_ui 6.3.1 → 6.4.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 +42 -0
- package/dist/index.cjs +178 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -3
- package/dist/index.d.ts +42 -3
- package/dist/index.js +178 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGE_LOG.md
CHANGED
|
@@ -5,6 +5,48 @@ 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.4.0 (2026-08-01)
|
|
9
|
+
|
|
10
|
+
### Added — in-column sections for `HazoUiKanban`
|
|
11
|
+
|
|
12
|
+
A kanban column can now declare `sections`, splitting its body into
|
|
13
|
+
separator-headed bands so a board can group on two axes at once (e.g. status
|
|
14
|
+
across, tag down) without a mode toggle between them.
|
|
15
|
+
|
|
16
|
+
- `KanbanColumn.sections?: KanbanSection[]` — `{ key, title, accepts?,
|
|
17
|
+
collapseWhenEmpty? }`. Cards are placed by the new
|
|
18
|
+
`KanbanItem.sectionKey`. Omit `sections` and nothing changes: existing
|
|
19
|
+
boards render exactly as before.
|
|
20
|
+
- Each band is its own droppable, so a single drop expresses both column and
|
|
21
|
+
section. `KanbanMoveEvent` gained `fromSection`/`toSection`, and
|
|
22
|
+
`KanbanReorderEvent` gained `sectionKey`.
|
|
23
|
+
- Move-vs-reorder is now decided on both axes — same column *and* same
|
|
24
|
+
section is a reorder; changing either is a move. Unsectioned boards have
|
|
25
|
+
`undefined` on both sides, so this collapses to the previous behaviour.
|
|
26
|
+
- On a sectioned column the **header becomes a droppable** carrying the
|
|
27
|
+
column but no section — the status-only drop target ("move column, keep
|
|
28
|
+
section"). Required, not cosmetic: section bands cover the column body and
|
|
29
|
+
win every `closestCorners` contest, so without it a sectioned column has no
|
|
30
|
+
reachable spot that isn't a band. Unsectioned columns keep an inert header.
|
|
31
|
+
- `collapseWhenEmpty` renders a section with no cards as a thin dashed drop
|
|
32
|
+
strip at the foot of the column. This is what makes "declare every section
|
|
33
|
+
in every column" practical for a long vocabulary: every section stays a
|
|
34
|
+
drop target without filling the column with empty headers.
|
|
35
|
+
- The optimistic overlay tracks the destination section too, so a
|
|
36
|
+
same-column cross-section drop moves immediately rather than snapping back
|
|
37
|
+
until the consumer's async commit lands.
|
|
38
|
+
- Dropping on a column's own padding leaves the card's section alone; a card
|
|
39
|
+
whose `sectionKey` matches no declared section renders in an unlabelled
|
|
40
|
+
trailing group rather than disappearing.
|
|
41
|
+
|
|
42
|
+
New styling hooks (no new CSS variables): `cls_hazo_kanban_section`,
|
|
43
|
+
`_section_head`, `_section_title`, `_section_rule`, `_section_count`,
|
|
44
|
+
`_section_body`, `_section_empty`, `_section_over`, `_section_strips`,
|
|
45
|
+
`cls_hazo_kanban_column_head_over`,
|
|
46
|
+
`_section_strip`, `_section_strip_over`, `_section_unmatched`.
|
|
47
|
+
|
|
48
|
+
Demo: test-app `/kanban` §10.
|
|
49
|
+
|
|
8
50
|
## v6.3.1 (2026-07-29)
|
|
9
51
|
|
|
10
52
|
### Fixed — test-app builds again
|
package/dist/index.cjs
CHANGED
|
@@ -9089,7 +9089,13 @@ function KanbanCard({
|
|
|
9089
9089
|
transform,
|
|
9090
9090
|
transition,
|
|
9091
9091
|
isDragging
|
|
9092
|
-
} = sortable.useSortable({
|
|
9092
|
+
} = sortable.useSortable({
|
|
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 }
|
|
9098
|
+
});
|
|
9093
9099
|
const priority_var = item.priority ? `--hazo-kanban-priority-${String(item.priority).toLowerCase()}` : null;
|
|
9094
9100
|
const style = {
|
|
9095
9101
|
transform: utilities.CSS.Transform.toString(transform),
|
|
@@ -9149,6 +9155,20 @@ function KanbanCard({
|
|
|
9149
9155
|
}
|
|
9150
9156
|
);
|
|
9151
9157
|
}
|
|
9158
|
+
function section_droppable_id(column_key, section_key) {
|
|
9159
|
+
return `section:${column_key}::${section_key}`;
|
|
9160
|
+
}
|
|
9161
|
+
function group_items_by_section(items, sections) {
|
|
9162
|
+
const bySection = /* @__PURE__ */ new Map();
|
|
9163
|
+
for (const section of sections) bySection.set(section.key, []);
|
|
9164
|
+
const unmatched = [];
|
|
9165
|
+
for (const item of items) {
|
|
9166
|
+
const bucket = item.sectionKey != null ? bySection.get(item.sectionKey) : void 0;
|
|
9167
|
+
if (bucket) bucket.push(item);
|
|
9168
|
+
else unmatched.push(item);
|
|
9169
|
+
}
|
|
9170
|
+
return { bySection, unmatched };
|
|
9171
|
+
}
|
|
9152
9172
|
function KanbanColumn({
|
|
9153
9173
|
column,
|
|
9154
9174
|
items,
|
|
@@ -9162,6 +9182,37 @@ function KanbanColumn({
|
|
|
9162
9182
|
id: `column:${column.key}`,
|
|
9163
9183
|
data: { columnKey: column.key, isColumn: true }
|
|
9164
9184
|
});
|
|
9185
|
+
const sections = column.sections;
|
|
9186
|
+
const { bySection, unmatched } = React27__namespace.useMemo(
|
|
9187
|
+
() => sections ? group_items_by_section(items, sections) : { bySection: /* @__PURE__ */ new Map(), unmatched: items },
|
|
9188
|
+
[items, sections]
|
|
9189
|
+
);
|
|
9190
|
+
const ordered = React27__namespace.useMemo(() => {
|
|
9191
|
+
if (!sections) return items;
|
|
9192
|
+
const out = [];
|
|
9193
|
+
for (const section of sections) out.push(...bySection.get(section.key) ?? []);
|
|
9194
|
+
out.push(...unmatched);
|
|
9195
|
+
return out;
|
|
9196
|
+
}, [items, sections, bySection, unmatched]);
|
|
9197
|
+
function render_cards(list) {
|
|
9198
|
+
return list.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
9199
|
+
KanbanCard,
|
|
9200
|
+
{
|
|
9201
|
+
item,
|
|
9202
|
+
renderCard,
|
|
9203
|
+
cardClassName,
|
|
9204
|
+
showEdit,
|
|
9205
|
+
onEditRequest
|
|
9206
|
+
},
|
|
9207
|
+
item.id
|
|
9208
|
+
));
|
|
9209
|
+
}
|
|
9210
|
+
const expanded_sections = (sections ?? []).filter(
|
|
9211
|
+
(s) => !(s.collapseWhenEmpty && (bySection.get(s.key) ?? []).length === 0)
|
|
9212
|
+
);
|
|
9213
|
+
const collapsed_sections = (sections ?? []).filter(
|
|
9214
|
+
(s) => s.collapseWhenEmpty && (bySection.get(s.key) ?? []).length === 0
|
|
9215
|
+
);
|
|
9165
9216
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9166
9217
|
"div",
|
|
9167
9218
|
{
|
|
@@ -9171,32 +9222,125 @@ function KanbanColumn({
|
|
|
9171
9222
|
isOver && "cls_hazo_kanban_column_over bg-accent/30 ring-2 ring-ring/40"
|
|
9172
9223
|
),
|
|
9173
9224
|
children: [
|
|
9174
|
-
/* @__PURE__ */ jsxRuntime.
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9225
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9226
|
+
KanbanColumnHead,
|
|
9227
|
+
{
|
|
9228
|
+
columnKey: column.key,
|
|
9229
|
+
title: column.title,
|
|
9230
|
+
count: items.length,
|
|
9231
|
+
droppable: sections != null
|
|
9232
|
+
}
|
|
9233
|
+
),
|
|
9178
9234
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9179
9235
|
sortable.SortableContext,
|
|
9180
9236
|
{
|
|
9181
|
-
items:
|
|
9237
|
+
items: ordered.map((i) => i.id),
|
|
9182
9238
|
strategy: sortable.verticalListSortingStrategy,
|
|
9183
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_column_body flex-1 min-h-[40px]", children: items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_column_empty text-xs text-muted-foreground py-2 px-1", children: emptyColumn }) : items
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9239
|
+
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: [
|
|
9240
|
+
expanded_sections.map((section) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
9241
|
+
KanbanSectionBand,
|
|
9242
|
+
{
|
|
9243
|
+
columnKey: column.key,
|
|
9244
|
+
section,
|
|
9245
|
+
items: bySection.get(section.key) ?? [],
|
|
9246
|
+
emptyColumn,
|
|
9247
|
+
renderCards: render_cards
|
|
9248
|
+
},
|
|
9249
|
+
section.key
|
|
9250
|
+
)),
|
|
9251
|
+
unmatched.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_section cls_hazo_kanban_section_unmatched", children: render_cards(unmatched) }),
|
|
9252
|
+
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(
|
|
9253
|
+
KanbanSectionStrip,
|
|
9254
|
+
{
|
|
9255
|
+
columnKey: column.key,
|
|
9256
|
+
section
|
|
9257
|
+
},
|
|
9258
|
+
section.key
|
|
9259
|
+
)) })
|
|
9260
|
+
] }) })
|
|
9194
9261
|
}
|
|
9195
9262
|
)
|
|
9196
9263
|
]
|
|
9197
9264
|
}
|
|
9198
9265
|
);
|
|
9199
9266
|
}
|
|
9267
|
+
function KanbanColumnHead({
|
|
9268
|
+
columnKey,
|
|
9269
|
+
title,
|
|
9270
|
+
count,
|
|
9271
|
+
droppable
|
|
9272
|
+
}) {
|
|
9273
|
+
const { setNodeRef, isOver } = core$1.useDroppable({
|
|
9274
|
+
id: `columnhead:${columnKey}`,
|
|
9275
|
+
data: { columnKey, isColumn: true },
|
|
9276
|
+
disabled: !droppable
|
|
9277
|
+
});
|
|
9278
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9279
|
+
"div",
|
|
9280
|
+
{
|
|
9281
|
+
ref: droppable ? setNodeRef : void 0,
|
|
9282
|
+
className: cn(
|
|
9283
|
+
"cls_hazo_kanban_column_head mb-2 flex items-center justify-between rounded-sm px-1 text-xs uppercase tracking-wider text-muted-foreground",
|
|
9284
|
+
droppable && "py-1",
|
|
9285
|
+
isOver && "cls_hazo_kanban_column_head_over bg-accent/30 text-foreground ring-1 ring-ring/40"
|
|
9286
|
+
),
|
|
9287
|
+
children: [
|
|
9288
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_column_title", children: title }),
|
|
9289
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_column_count rounded-full bg-muted px-2 py-0.5 text-foreground", children: count })
|
|
9290
|
+
]
|
|
9291
|
+
}
|
|
9292
|
+
);
|
|
9293
|
+
}
|
|
9294
|
+
function KanbanSectionBand({
|
|
9295
|
+
columnKey,
|
|
9296
|
+
section,
|
|
9297
|
+
items,
|
|
9298
|
+
emptyColumn,
|
|
9299
|
+
renderCards
|
|
9300
|
+
}) {
|
|
9301
|
+
const { setNodeRef, isOver } = core$1.useDroppable({
|
|
9302
|
+
id: section_droppable_id(columnKey, section.key),
|
|
9303
|
+
data: { columnKey, sectionKey: section.key, isSection: true }
|
|
9304
|
+
});
|
|
9305
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9306
|
+
"div",
|
|
9307
|
+
{
|
|
9308
|
+
ref: setNodeRef,
|
|
9309
|
+
className: cn(
|
|
9310
|
+
"cls_hazo_kanban_section rounded-sm",
|
|
9311
|
+
isOver && "cls_hazo_kanban_section_over bg-accent/20 ring-1 ring-ring/30"
|
|
9312
|
+
),
|
|
9313
|
+
children: [
|
|
9314
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cls_hazo_kanban_section_head mt-2 mb-1 flex items-center gap-2 px-1 first:mt-0", children: [
|
|
9315
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_section_title text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: section.title }),
|
|
9316
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_section_rule h-px flex-1 bg-border" }),
|
|
9317
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_hazo_kanban_section_count text-[11px] tabular-nums text-muted-foreground", children: items.length })
|
|
9318
|
+
] }),
|
|
9319
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_section_body min-h-[32px]", children: items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_hazo_kanban_section_empty px-1 py-1 text-xs text-muted-foreground", children: emptyColumn }) : renderCards(items) })
|
|
9320
|
+
]
|
|
9321
|
+
}
|
|
9322
|
+
);
|
|
9323
|
+
}
|
|
9324
|
+
function KanbanSectionStrip({
|
|
9325
|
+
columnKey,
|
|
9326
|
+
section
|
|
9327
|
+
}) {
|
|
9328
|
+
const { setNodeRef, isOver } = core$1.useDroppable({
|
|
9329
|
+
id: section_droppable_id(columnKey, section.key),
|
|
9330
|
+
data: { columnKey, sectionKey: section.key, isSection: true }
|
|
9331
|
+
});
|
|
9332
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
9333
|
+
"div",
|
|
9334
|
+
{
|
|
9335
|
+
ref: setNodeRef,
|
|
9336
|
+
className: cn(
|
|
9337
|
+
"cls_hazo_kanban_section_strip rounded-full border border-dashed border-border px-2 py-0.5 text-[11px] text-muted-foreground",
|
|
9338
|
+
isOver && "cls_hazo_kanban_section_strip_over border-solid bg-accent/30 text-foreground ring-1 ring-ring/30"
|
|
9339
|
+
),
|
|
9340
|
+
children: section.title
|
|
9341
|
+
}
|
|
9342
|
+
);
|
|
9343
|
+
}
|
|
9200
9344
|
var DEFAULT_PRIORITIES = ["P0", "P1", "P2", "P3"];
|
|
9201
9345
|
function humanize(key) {
|
|
9202
9346
|
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(" ");
|
|
@@ -9621,7 +9765,8 @@ function group_items_by_column(items, overlay, columns) {
|
|
|
9621
9765
|
if (ov) {
|
|
9622
9766
|
const bucket = overlay_inserts.get(ov.columnKey);
|
|
9623
9767
|
if (bucket) {
|
|
9624
|
-
|
|
9768
|
+
const placed = ov.sectionKey === item.sectionKey ? item : { ...item, sectionKey: ov.sectionKey };
|
|
9769
|
+
bucket.push({ item: placed, index: ov.index });
|
|
9625
9770
|
}
|
|
9626
9771
|
} else {
|
|
9627
9772
|
const bucket = out.get(item.columnKey);
|
|
@@ -9689,7 +9834,7 @@ function HazoUiKanban({
|
|
|
9689
9834
|
const next = new Map(prev);
|
|
9690
9835
|
for (const [item_id, pos] of prev.entries()) {
|
|
9691
9836
|
const item = items.find((i) => i.id === item_id);
|
|
9692
|
-
if (item && item.columnKey === pos.columnKey) {
|
|
9837
|
+
if (item && item.columnKey === pos.columnKey && item.sectionKey === pos.sectionKey) {
|
|
9693
9838
|
next.delete(item_id);
|
|
9694
9839
|
changed = true;
|
|
9695
9840
|
}
|
|
@@ -9755,7 +9900,8 @@ function HazoUiKanban({
|
|
|
9755
9900
|
const over_col_key = over ? over.data?.current?.columnKey ?? String(over.id).replace(/^column:/, "") : null;
|
|
9756
9901
|
const to_col = over_col_key ? columns.find((c) => c.key === over_col_key) ?? from_col : from_col;
|
|
9757
9902
|
const target = over_col_key ? grouped.get(over_col_key) ?? [] : grouped.get(item.columnKey) ?? [];
|
|
9758
|
-
const
|
|
9903
|
+
const on_shell = !over || String(over.id).startsWith("column:") || String(over.id).startsWith("columnhead:") || String(over.id).startsWith("section:");
|
|
9904
|
+
const new_index = on_shell ? target.length : target.findIndex((it) => it.id === String(over.id));
|
|
9759
9905
|
return merged_announcements.onDragEnd({
|
|
9760
9906
|
item,
|
|
9761
9907
|
fromColumn: from_col?.title ?? item.columnKey,
|
|
@@ -9804,22 +9950,28 @@ function HazoUiKanban({
|
|
|
9804
9950
|
const dest_column_key = over_data?.columnKey ?? over_id.replace(/^column:/, "");
|
|
9805
9951
|
const dest_column = columns.find((c) => c.key === dest_column_key);
|
|
9806
9952
|
if (!dest_column) return;
|
|
9953
|
+
const dest_section_key = dest_column.sections ? over_data?.sectionKey ?? item.sectionKey : void 0;
|
|
9954
|
+
const dest_section = dest_column.sections?.find(
|
|
9955
|
+
(s) => s.key === dest_section_key
|
|
9956
|
+
);
|
|
9807
9957
|
if (dest_column.accepts && !dest_column.accepts(item)) return;
|
|
9958
|
+
if (dest_section?.accepts && !dest_section.accepts(item)) return;
|
|
9808
9959
|
const dest_items_pre_overlay = grouped.get(dest_column_key) ?? [];
|
|
9809
9960
|
let new_index;
|
|
9810
|
-
if (over_data?.isColumn) {
|
|
9961
|
+
if (over_data?.isColumn || over_data?.isSection) {
|
|
9811
9962
|
new_index = dest_items_pre_overlay.length;
|
|
9812
9963
|
} else {
|
|
9813
9964
|
const idx = dest_items_pre_overlay.findIndex((i) => i.id === over_id);
|
|
9814
9965
|
new_index = idx === -1 ? dest_items_pre_overlay.length : idx;
|
|
9815
9966
|
}
|
|
9816
9967
|
const handle = make_revert_handle(active_id_str);
|
|
9817
|
-
if (item.columnKey === dest_column_key) {
|
|
9968
|
+
if (item.columnKey === dest_column_key && item.sectionKey === dest_section_key) {
|
|
9818
9969
|
if (active_id_str === over_id) return;
|
|
9819
9970
|
set_overlay((prev) => {
|
|
9820
9971
|
const next = new Map(prev);
|
|
9821
9972
|
next.set(active_id_str, {
|
|
9822
9973
|
columnKey: dest_column_key,
|
|
9974
|
+
sectionKey: dest_section_key,
|
|
9823
9975
|
index: new_index
|
|
9824
9976
|
});
|
|
9825
9977
|
return next;
|
|
@@ -9828,6 +9980,7 @@ function HazoUiKanban({
|
|
|
9828
9980
|
itemId: active_id_str,
|
|
9829
9981
|
item,
|
|
9830
9982
|
columnKey: dest_column_key,
|
|
9983
|
+
sectionKey: dest_section_key,
|
|
9831
9984
|
newIndex: new_index,
|
|
9832
9985
|
revert: handle.revert
|
|
9833
9986
|
};
|
|
@@ -9838,6 +9991,7 @@ function HazoUiKanban({
|
|
|
9838
9991
|
const next = new Map(prev);
|
|
9839
9992
|
next.set(active_id_str, {
|
|
9840
9993
|
columnKey: dest_column_key,
|
|
9994
|
+
sectionKey: dest_section_key,
|
|
9841
9995
|
index: new_index
|
|
9842
9996
|
});
|
|
9843
9997
|
return next;
|
|
@@ -9847,6 +10001,8 @@ function HazoUiKanban({
|
|
|
9847
10001
|
item,
|
|
9848
10002
|
fromColumn: item.columnKey,
|
|
9849
10003
|
toColumn: dest_column_key,
|
|
10004
|
+
fromSection: item.sectionKey,
|
|
10005
|
+
toSection: dest_section_key,
|
|
9850
10006
|
newIndex: new_index,
|
|
9851
10007
|
revert: handle.revert
|
|
9852
10008
|
};
|