@stll/ui 0.59.1 → 0.61.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/README.md +120 -4
- package/dist/components/accordion.d.ts +17 -3
- package/dist/components/accordion.js +17 -3
- package/dist/components/button.d.ts +56 -5
- package/dist/components/button.js +97 -15
- package/dist/components/button.native-classes.d.ts +17 -0
- package/dist/components/button.native-classes.js +176 -0
- package/dist/components/button.native.d.ts +5 -2
- package/dist/components/button.native.js +17 -134
- package/dist/components/button.shared.d.ts +24 -2
- package/dist/components/button.shared.js +15 -1
- package/dist/components/collapsible.d.ts +26 -0
- package/dist/components/collapsible.js +40 -0
- package/dist/components/combobox.d.ts +3 -1
- package/dist/components/combobox.js +3 -2
- package/dist/components/date-picker-popover.d.ts +2 -0
- package/dist/components/date-picker-popover.js +5 -3
- package/dist/components/details-grid.d.ts +21 -2
- package/dist/components/details-grid.js +75 -20
- package/dist/components/field.d.ts +6 -0
- package/dist/components/field.js +6 -0
- package/dist/components/form.d.ts +43 -3
- package/dist/components/form.js +53 -3
- package/dist/components/input-control.d.ts +9 -0
- package/dist/components/input-control.js +10 -1
- package/dist/components/input.d.ts +7 -1
- package/dist/components/input.js +4 -4
- package/dist/components/label.d.ts +24 -2
- package/dist/components/label.js +21 -2
- package/dist/components/number-input.d.ts +9 -2
- package/dist/components/number-input.js +18 -6
- package/dist/components/search-field.js +1 -0
- package/dist/components/segmented-icon-toggle.d.ts +2 -1
- package/dist/components/segmented-icon-toggle.js +18 -19
- package/dist/components/switch.d.ts +21 -2
- package/dist/components/switch.js +44 -5
- package/dist/components/table.d.ts +16 -2
- package/dist/components/table.js +5 -4
- package/dist/components/textarea.d.ts +14 -1
- package/dist/components/textarea.js +11 -5
- package/dist/components/toggle-group.d.ts +32 -0
- package/dist/components/toggle-group.js +45 -0
- package/dist/components/toggle.d.ts +26 -0
- package/dist/components/toggle.js +26 -0
- package/dist/index.d.ts +14 -7
- package/dist/index.js +9 -3
- package/dist/kanban/column-bands.js +1 -1
- package/dist/kanban/index.d.ts +5 -2
- package/dist/kanban/index.js +4 -1
- package/dist/kanban/sortable-edge.d.ts +39 -0
- package/dist/kanban/sortable-edge.js +27 -0
- package/dist/kanban/sortable-interactions.d.ts +145 -0
- package/dist/kanban/sortable-interactions.js +503 -0
- package/dist/kanban/sortable-interactions.logic.d.ts +79 -0
- package/dist/kanban/sortable-interactions.logic.js +210 -0
- package/dist/kanban/subgroup-board.d.ts +27 -2
- package/dist/kanban/subgroup-board.js +26 -1
- package/dist/kanban/virtual-cell.d.ts +18 -1
- package/dist/kanban/virtual-cell.js +70 -17
- package/package.json +17 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { closestCorners, getFirstCollision, pointerWithin } from "@dnd-kit/core";
|
|
2
|
+
//#region src/kanban/sortable-interactions.logic.ts
|
|
3
|
+
const KANBAN_DROP_TARGET_TYPES = {
|
|
4
|
+
CELL: "kanban-cell",
|
|
5
|
+
ITEM: "kanban-item"
|
|
6
|
+
};
|
|
7
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
8
|
+
const isUniqueIdentifier = (value) => typeof value === "string" || typeof value === "number";
|
|
9
|
+
const isCellPosition = (value) => isRecord(value) && typeof value["column"] === "number" && typeof value["lane"] === "number" && Number.isInteger(value["column"]) && Number.isInteger(value["lane"]) && value["column"] >= 0 && value["lane"] >= 0;
|
|
10
|
+
const isKanbanCellVirtualNavigation = (value) => isRecord(value) && (value["type"] === "static" || value["type"] === "virtual" && typeof value["requestScroll"] === "function");
|
|
11
|
+
const isKanbanCellDropData = (value) => isRecord(value) && value["type"] === KANBAN_DROP_TARGET_TYPES.CELL && Array.isArray(value["itemIds"]) && value["itemIds"].every(isUniqueIdentifier) && isKanbanCellVirtualNavigation(value["navigation"]) && isCellPosition(value["position"]);
|
|
12
|
+
const isKanbanItemDropData = (value) => {
|
|
13
|
+
if (!isRecord(value) || value["type"] !== KANBAN_DROP_TARGET_TYPES.ITEM) return false;
|
|
14
|
+
const navigation = value["navigation"];
|
|
15
|
+
const current = isRecord(navigation) ? navigation["current"] : void 0;
|
|
16
|
+
return isRecord(navigation) && isRecord(current) && (current["type"] === "idle" || (current["type"] === "ready" || current["type"] === "pending") && isUniqueIdentifier(current["targetId"]));
|
|
17
|
+
};
|
|
18
|
+
const getKanbanKeyboardTargetState = (value) => isKanbanItemDropData(value) ? value.navigation.current : void 0;
|
|
19
|
+
/**
|
|
20
|
+
* dnd-kit computes collisions while rendering but publishes the resulting drop
|
|
21
|
+
* target to its sensor context a render later, and resolves a drop from that
|
|
22
|
+
* published value. A drag that ends before the two agree, which every input
|
|
23
|
+
* can do because a single move produces a single render, drops the item on the
|
|
24
|
+
* previously published target.
|
|
25
|
+
*
|
|
26
|
+
* Keyboard navigation names its target outright, so it is compared directly: a
|
|
27
|
+
* pending target is still waiting for a virtual cell to mount an offscreen row
|
|
28
|
+
* and has produced no collision at all, and a ready one is only settled once it
|
|
29
|
+
* is the published target. Until then both the collisions and the published
|
|
30
|
+
* target still describe where the board was before the user navigated away.
|
|
31
|
+
*/
|
|
32
|
+
const isKanbanDropSettled = ({ active, collisions, over }) => {
|
|
33
|
+
const target = getKanbanKeyboardTargetState(active?.data.current);
|
|
34
|
+
if (target?.type === "pending") return false;
|
|
35
|
+
if (target?.type === "ready") return over?.id === target.targetId;
|
|
36
|
+
return getFirstCollision(collisions, "id") === (over?.id ?? null);
|
|
37
|
+
};
|
|
38
|
+
const clearKanbanKeyboardTarget = (value) => {
|
|
39
|
+
if (isKanbanItemDropData(value) && value.navigation.current.type !== "idle") value.navigation.current = { type: "idle" };
|
|
40
|
+
};
|
|
41
|
+
const getSortableData = (value) => {
|
|
42
|
+
if (!isRecord(value)) return null;
|
|
43
|
+
const sortable = value["sortable"];
|
|
44
|
+
if (!isRecord(sortable) || !isUniqueIdentifier(sortable["containerId"]) || typeof sortable["index"] !== "number" || !Number.isInteger(sortable["index"]) || !Array.isArray(sortable["items"]) || !sortable["items"].every(isUniqueIdentifier)) return null;
|
|
45
|
+
return {
|
|
46
|
+
containerId: sortable["containerId"],
|
|
47
|
+
index: sortable["index"],
|
|
48
|
+
items: sortable["items"]
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const isKanbanDroppable = ({ data }) => {
|
|
52
|
+
const value = data.current;
|
|
53
|
+
return isKanbanCellDropData(value) || isKanbanItemDropData(value);
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Pointer and touch input must be inside a registered board target. Once that
|
|
57
|
+
* boundary is established, closest corners gives stable ranking across cells.
|
|
58
|
+
*/
|
|
59
|
+
const KANBAN_BOARD_COLLISION_DETECTION = (args) => {
|
|
60
|
+
const boardContainers = args.droppableContainers.filter(isKanbanDroppable);
|
|
61
|
+
if (args.pointerCoordinates === null) {
|
|
62
|
+
const activeData = args.active.data.current;
|
|
63
|
+
if (isKanbanItemDropData(activeData)) {
|
|
64
|
+
if (activeData.navigation.current.type === "pending") return [];
|
|
65
|
+
if (activeData.navigation.current.type !== "ready") return closestCorners({
|
|
66
|
+
...args,
|
|
67
|
+
droppableContainers: boardContainers
|
|
68
|
+
});
|
|
69
|
+
const { targetId } = activeData.navigation.current;
|
|
70
|
+
return boardContainers.some(({ id }) => id === targetId) ? [{ id: targetId }] : [];
|
|
71
|
+
}
|
|
72
|
+
return closestCorners({
|
|
73
|
+
...args,
|
|
74
|
+
droppableContainers: boardContainers
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const intersections = pointerWithin({
|
|
78
|
+
...args,
|
|
79
|
+
droppableContainers: boardContainers
|
|
80
|
+
});
|
|
81
|
+
if (intersections.length === 0) return [];
|
|
82
|
+
const intersectingIds = new Set(intersections.map((intersection) => intersection.id));
|
|
83
|
+
const cellIds = new Set(boardContainers.filter(({ data }) => isKanbanCellDropData(data.current)).map(({ id }) => id));
|
|
84
|
+
return closestCorners({
|
|
85
|
+
...args,
|
|
86
|
+
droppableContainers: boardContainers.filter(({ id, data }) => {
|
|
87
|
+
if (!intersectingIds.has(id)) return false;
|
|
88
|
+
const cellId = getSortableData(data.current)?.containerId;
|
|
89
|
+
return cellId === void 0 || !cellIds.has(cellId) || intersectingIds.has(cellId);
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const getAdjacentCell = (cells, current, direction) => {
|
|
94
|
+
let columnOffset = 0;
|
|
95
|
+
let laneOffset = 0;
|
|
96
|
+
switch (direction) {
|
|
97
|
+
case "down":
|
|
98
|
+
laneOffset = 1;
|
|
99
|
+
break;
|
|
100
|
+
case "left":
|
|
101
|
+
columnOffset = -1;
|
|
102
|
+
break;
|
|
103
|
+
case "right":
|
|
104
|
+
columnOffset = 1;
|
|
105
|
+
break;
|
|
106
|
+
case "up": laneOffset = -1;
|
|
107
|
+
}
|
|
108
|
+
return cells.find(({ position }) => position.column === current.position.column + columnOffset && position.lane === current.position.lane + laneOffset);
|
|
109
|
+
};
|
|
110
|
+
const getKanbanKeyboardTarget = ({ activeId, cells, currentCellId, currentOverId, direction }) => {
|
|
111
|
+
const currentCell = cells.find(({ id }) => id === currentCellId);
|
|
112
|
+
if (currentCell === void 0) return;
|
|
113
|
+
const overIndex = currentCell.itemIds.indexOf(currentOverId);
|
|
114
|
+
const activeIndex = currentCell.itemIds.indexOf(activeId);
|
|
115
|
+
const currentIndex = overIndex !== -1 ? overIndex : activeIndex;
|
|
116
|
+
if (direction === "down" && currentIndex >= 0) {
|
|
117
|
+
const nextItem = currentCell.itemIds.at(currentIndex + 1);
|
|
118
|
+
if (nextItem !== void 0) return nextItem;
|
|
119
|
+
}
|
|
120
|
+
if (direction === "up" && currentIndex > 0) return currentCell.itemIds.at(currentIndex - 1);
|
|
121
|
+
const adjacentCell = getAdjacentCell(cells, currentCell, direction);
|
|
122
|
+
if (adjacentCell === void 0) return;
|
|
123
|
+
if (adjacentCell.itemIds.length === 0) return adjacentCell.id;
|
|
124
|
+
if (direction === "down") return adjacentCell.itemIds.at(0);
|
|
125
|
+
if (direction === "up") return adjacentCell.itemIds.at(-1);
|
|
126
|
+
const targetIndex = Math.max(currentIndex, 0);
|
|
127
|
+
return adjacentCell.itemIds.at(Math.min(targetIndex, adjacentCell.itemIds.length - 1)) ?? adjacentCell.id;
|
|
128
|
+
};
|
|
129
|
+
const getDirection = (code) => {
|
|
130
|
+
switch (code) {
|
|
131
|
+
case "ArrowDown": return "down";
|
|
132
|
+
case "ArrowLeft": return "left";
|
|
133
|
+
case "ArrowRight": return "right";
|
|
134
|
+
case "ArrowUp": return "up";
|
|
135
|
+
default: return null;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const getCellId = (container) => {
|
|
139
|
+
const data = container.data.current;
|
|
140
|
+
if (isKanbanCellDropData(data)) return container.id;
|
|
141
|
+
if (!isKanbanItemDropData(data)) return null;
|
|
142
|
+
return getSortableData(data)?.containerId ?? null;
|
|
143
|
+
};
|
|
144
|
+
/** Ordered two-dimensional navigation over item and empty-cell targets. */
|
|
145
|
+
const kanbanKeyboardCoordinates = (event, { active, context }) => {
|
|
146
|
+
const direction = getDirection(event.code);
|
|
147
|
+
if (direction === null) return;
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
const cells = context.droppableContainers.getEnabled().filter(({ data }) => {
|
|
150
|
+
const value = data.current;
|
|
151
|
+
return isKanbanCellDropData(value);
|
|
152
|
+
}).flatMap((container) => {
|
|
153
|
+
const data = container.data.current;
|
|
154
|
+
return isKanbanCellDropData(data) ? [{
|
|
155
|
+
...data,
|
|
156
|
+
id: container.id
|
|
157
|
+
}] : [];
|
|
158
|
+
});
|
|
159
|
+
const activeData = context.active?.data.current;
|
|
160
|
+
const keyboardTarget = getKanbanKeyboardTargetState(activeData);
|
|
161
|
+
const overId = keyboardTarget?.type === "ready" ? keyboardTarget.targetId : context.over?.id ?? active;
|
|
162
|
+
const overContainer = context.droppableContainers.get(overId);
|
|
163
|
+
const activeContainer = context.droppableContainers.get(active);
|
|
164
|
+
const currentCellId = (overContainer && getCellId(overContainer)) ?? (activeContainer && getCellId(activeContainer));
|
|
165
|
+
if (currentCellId === null || currentCellId === void 0) return;
|
|
166
|
+
const targetId = keyboardTarget?.type === "pending" ? keyboardTarget.targetId : getKanbanKeyboardTarget({
|
|
167
|
+
activeId: active,
|
|
168
|
+
cells,
|
|
169
|
+
currentCellId,
|
|
170
|
+
currentOverId: overId,
|
|
171
|
+
direction
|
|
172
|
+
});
|
|
173
|
+
if (targetId === void 0) return;
|
|
174
|
+
const targetNode = context.droppableContainers.get(targetId)?.node.current;
|
|
175
|
+
if (targetNode === null || targetNode === void 0) {
|
|
176
|
+
const targetCell = cells.find(({ itemIds }) => itemIds.includes(targetId));
|
|
177
|
+
if (isKanbanItemDropData(activeData) && targetCell?.navigation.type === "virtual") {
|
|
178
|
+
activeData.navigation.current = {
|
|
179
|
+
targetId,
|
|
180
|
+
type: "pending"
|
|
181
|
+
};
|
|
182
|
+
targetCell.navigation.requestScroll({
|
|
183
|
+
itemId: targetId,
|
|
184
|
+
type: "item"
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const targetRect = context.droppableRects.get(targetId);
|
|
190
|
+
if (targetRect === void 0) return;
|
|
191
|
+
if (isKanbanItemDropData(activeData)) activeData.navigation.current = {
|
|
192
|
+
targetId,
|
|
193
|
+
type: "ready"
|
|
194
|
+
};
|
|
195
|
+
requestAnimationFrame(() => {
|
|
196
|
+
requestAnimationFrame(() => {
|
|
197
|
+
targetNode.scrollIntoView({
|
|
198
|
+
behavior: "instant",
|
|
199
|
+
block: "nearest",
|
|
200
|
+
inline: "nearest"
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
x: targetRect.left,
|
|
206
|
+
y: targetRect.top
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
//#endregion
|
|
210
|
+
export { KANBAN_BOARD_COLLISION_DETECTION, KANBAN_DROP_TARGET_TYPES, clearKanbanKeyboardTarget, getKanbanKeyboardTarget, getKanbanKeyboardTargetState, isKanbanCellDropData, isKanbanDropSettled, kanbanKeyboardCoordinates };
|
|
@@ -27,7 +27,11 @@ export type KanbanSubgroupCellContext<TRow> = {
|
|
|
27
27
|
/** Number of rows in this lane/column intersection, including zero. */
|
|
28
28
|
count: number;
|
|
29
29
|
laneValue: string | null;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* The band this cell's column belongs to, or `null` outside any band. Lets
|
|
32
|
+
* a host that drives its own drag-and-drop (see `dragOverBandId` on the
|
|
33
|
+
* board) map a cell's droppable id back to the band it should report.
|
|
34
|
+
*/
|
|
31
35
|
band: KanbanColumnBand | null;
|
|
32
36
|
};
|
|
33
37
|
export type KanbanSubgroupBandHeaderContext = {
|
|
@@ -110,6 +114,27 @@ export type KanbanSubgroupBoardProps<TRow> = {
|
|
|
110
114
|
formatCount?: ((count: number) => ReactNode) | undefined;
|
|
111
115
|
footer?: ReactNode;
|
|
112
116
|
className?: string | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* The band whose folded slot (or, for a peeked band, whose open part) the
|
|
119
|
+
* host's own drag is currently over, or `null` when it is over neither.
|
|
120
|
+
* Omit (`undefined`) when the host does not drive drags itself — the
|
|
121
|
+
* board's native `dragover`/`dragenter`/`dragleave` listeners already feed
|
|
122
|
+
* the peek controller in that case. A host drives its own drag when it
|
|
123
|
+
* builds the drop targets itself (for example a `KanbanSortableBoard`,
|
|
124
|
+
* whose `onDragOver` reports the active `over` droppable); it maps that
|
|
125
|
+
* droppable back to a band id — a folded cell's context and an open
|
|
126
|
+
* cell's context (see `KanbanSubgroupCellContext.band`) both carry the
|
|
127
|
+
* band — and passes it here. The controller treats "over the band" as
|
|
128
|
+
* entering the open band when it is already peeked, and as hovering its
|
|
129
|
+
* folded slot otherwise; leaving it is reported the same way.
|
|
130
|
+
*/
|
|
131
|
+
dragOverBandId?: string | null | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* Whether the host's own drag (see `dragOverBandId`) is in progress.
|
|
134
|
+
* Flipping to `false` ends any peek immediately, mirroring the native
|
|
135
|
+
* `dragend`/`drop` listeners the board installs for its own drags.
|
|
136
|
+
*/
|
|
137
|
+
isDragging?: boolean | undefined;
|
|
113
138
|
} & KanbanSubgroupCollapseControl;
|
|
114
139
|
/**
|
|
115
140
|
* Reusable swimlane layout over the canonical two-axis Kanban matrix.
|
|
@@ -128,6 +153,6 @@ export type KanbanSubgroupBoardProps<TRow> = {
|
|
|
128
153
|
* in every state; the caption line never grows past its own height, so a
|
|
129
154
|
* folded band costs no vertical space.
|
|
130
155
|
*/
|
|
131
|
-
export declare const KanbanSubgroupBoard: <TRow>({ scrollRef, matrix, renderColumnHeader, renderLaneIdentity, renderCell, renderLaneColumnSummary, renderLaneColumnAction, renderBandHeader, renderCollapsedBandCell, formatBandToggleLabel, formatCount, isLaneCollapsed, onLaneCollapsedChange, isBandCollapsed, onBandCollapsedChange, footer, className }: KanbanSubgroupBoardProps<TRow>) => import("react").JSX.Element;
|
|
156
|
+
export declare const KanbanSubgroupBoard: <TRow>({ scrollRef, matrix, renderColumnHeader, renderLaneIdentity, renderCell, renderLaneColumnSummary, renderLaneColumnAction, renderBandHeader, renderCollapsedBandCell, formatBandToggleLabel, formatCount, isLaneCollapsed, onLaneCollapsedChange, isBandCollapsed, onBandCollapsedChange, footer, className, dragOverBandId, isDragging }: KanbanSubgroupBoardProps<TRow>) => import("react").JSX.Element;
|
|
132
157
|
//#endregion
|
|
133
158
|
export {};
|
|
@@ -42,7 +42,7 @@ const rowsIn = (cells) => cells.reduce((sum, cell) => sum + cell.rows.length, 0)
|
|
|
42
42
|
* in every state; the caption line never grows past its own height, so a
|
|
43
43
|
* folded band costs no vertical space.
|
|
44
44
|
*/
|
|
45
|
-
const KanbanSubgroupBoard = ({ scrollRef, matrix, renderColumnHeader, renderLaneIdentity, renderCell, renderLaneColumnSummary, renderLaneColumnAction, renderBandHeader, renderCollapsedBandCell, formatBandToggleLabel, formatCount = String, isLaneCollapsed, onLaneCollapsedChange, isBandCollapsed, onBandCollapsedChange, footer, className }) => {
|
|
45
|
+
const KanbanSubgroupBoard = ({ scrollRef, matrix, renderColumnHeader, renderLaneIdentity, renderCell, renderLaneColumnSummary, renderLaneColumnAction, renderBandHeader, renderCollapsedBandCell, formatBandToggleLabel, formatCount = String, isLaneCollapsed, onLaneCollapsedChange, isBandCollapsed, onBandCollapsedChange, footer, className, dragOverBandId, isDragging }) => {
|
|
46
46
|
const [collapsedLaneValues, setCollapsedLaneValues] = useState(() => /* @__PURE__ */ new Set());
|
|
47
47
|
const [expandedEmptyLaneValues, setExpandedEmptyLaneValues] = useState(() => /* @__PURE__ */ new Set());
|
|
48
48
|
const [collapsedBandIds, setCollapsedBandIds] = useState(() => /* @__PURE__ */ new Set());
|
|
@@ -69,6 +69,31 @@ const KanbanSubgroupBoard = ({ scrollRef, matrix, renderColumnHeader, renderLane
|
|
|
69
69
|
window.removeEventListener("drop", end);
|
|
70
70
|
};
|
|
71
71
|
}, [peek]);
|
|
72
|
+
const previousDragOverBandId = useRef(null);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (dragOverBandId === void 0 || isDragging === false || dragOverBandId === previousDragOverBandId.current) return;
|
|
75
|
+
const previous = previousDragOverBandId.current;
|
|
76
|
+
previousDragOverBandId.current = dragOverBandId;
|
|
77
|
+
if (previous !== null) {
|
|
78
|
+
if (peekingBandId === previous) peek.openDragLeave(previous);
|
|
79
|
+
else peek.slotDragLeave(previous);
|
|
80
|
+
}
|
|
81
|
+
if (dragOverBandId !== null) {
|
|
82
|
+
if (peekingBandId === dragOverBandId) peek.openDragEnter(dragOverBandId);
|
|
83
|
+
else peek.slotDragOver(dragOverBandId);
|
|
84
|
+
}
|
|
85
|
+
}, [
|
|
86
|
+
dragOverBandId,
|
|
87
|
+
isDragging,
|
|
88
|
+
peek,
|
|
89
|
+
peekingBandId
|
|
90
|
+
]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (isDragging === false) {
|
|
93
|
+
previousDragOverBandId.current = null;
|
|
94
|
+
peek.dragEnded();
|
|
95
|
+
}
|
|
96
|
+
}, [isDragging, peek]);
|
|
72
97
|
const { cellsByLaneValue, countByColumnValue, ungroupedCells } = useMemo(() => {
|
|
73
98
|
const laneCells = /* @__PURE__ */ new Map();
|
|
74
99
|
const columnCounts = /* @__PURE__ */ new Map();
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Key, ReactNode, RefObject } from "react";
|
|
2
2
|
import { OptionColor } from "../lib/option-color.js";
|
|
3
|
+
import { SortableContextProps, SortingStrategy } from "@dnd-kit/sortable";
|
|
4
|
+
import { UniqueIdentifier } from "@dnd-kit/core";
|
|
5
|
+
import { UseKanbanDropTargetOptions } from "./sortable-interactions.js";
|
|
3
6
|
//#region src/kanban/virtual-cell.d.ts
|
|
4
7
|
export declare const KANBAN_VIRTUAL_CELL_PAGINATION: {
|
|
5
8
|
readonly NONE: "none";
|
|
@@ -14,11 +17,25 @@ export type KanbanVirtualCellPagination = {
|
|
|
14
17
|
pageKey: string | number;
|
|
15
18
|
onRequestMore: () => void;
|
|
16
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Makes a virtual cell the canonical sortable context for its rendered rows.
|
|
22
|
+
*
|
|
23
|
+
* `getRowKey` remains responsible for React and virtualizer identity. A
|
|
24
|
+
* separate sortable identifier avoids narrowing an existing React key contract
|
|
25
|
+
* merely to support dnd-kit consumers.
|
|
26
|
+
*/
|
|
27
|
+
export type KanbanVirtualCellSortableContext<TRow> = {
|
|
28
|
+
dropTarget: Omit<UseKanbanDropTargetOptions, "itemIds" | "navigation">;
|
|
29
|
+
getRowId: (row: TRow) => UniqueIdentifier;
|
|
30
|
+
disabled?: SortableContextProps["disabled"] | undefined;
|
|
31
|
+
strategy?: SortingStrategy | undefined;
|
|
32
|
+
};
|
|
17
33
|
export type KanbanVirtualCellProps<TRow> = {
|
|
18
34
|
rows: readonly TRow[];
|
|
19
35
|
getRowKey: (row: TRow) => Key;
|
|
20
36
|
renderRow: (row: TRow) => ReactNode;
|
|
21
37
|
pagination: KanbanVirtualCellPagination;
|
|
38
|
+
sortable?: KanbanVirtualCellSortableContext<TRow> | undefined;
|
|
22
39
|
containerRef?: RefObject<HTMLDivElement | null> | undefined;
|
|
23
40
|
active?: boolean | undefined;
|
|
24
41
|
backgroundColor?: string | undefined;
|
|
@@ -54,5 +71,5 @@ export type KanbanVirtualCellProps<TRow> = {
|
|
|
54
71
|
className?: string | undefined;
|
|
55
72
|
};
|
|
56
73
|
/** Bounded, virtualized Kanban cell with cursor-page request deduplication. */
|
|
57
|
-
export declare const KanbanVirtualCell: <TRow>({ rows, getRowKey, renderRow, pagination, containerRef, active, backgroundColor, accent, footer, footerPlacement, estimateSize, overscan, loadMoreThreshold, className }: KanbanVirtualCellProps<TRow>) => import("react").JSX.Element;
|
|
74
|
+
export declare const KanbanVirtualCell: <TRow>({ rows, getRowKey, renderRow, pagination, sortable, containerRef, active, backgroundColor, accent, footer, footerPlacement, estimateSize, overscan, loadMoreThreshold, className }: KanbanVirtualCellProps<TRow>) => import("react").JSX.Element;
|
|
58
75
|
//#endregion
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
2
|
import { KANBAN_CARD_STICKY_TOP_VAR, KANBAN_STICKY_TOP_CLASS, resolveKanbanCardStickyTop } from "./sticky-lane.js";
|
|
3
|
+
import { useKanbanDropTarget } from "./sortable-interactions.js";
|
|
3
4
|
import { resolveOptionColor } from "../lib/option-color.js";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import { useLayoutEffect, useRef, useState } from "react";
|
|
6
|
-
import {
|
|
6
|
+
import { useId, useLayoutEffect, useRef, useState } from "react";
|
|
7
|
+
import { useDndContext } from "@dnd-kit/core";
|
|
8
|
+
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
9
|
+
import { defaultRangeExtractor, useVirtualizer } from "@tanstack/react-virtual";
|
|
7
10
|
//#region src/kanban/virtual-cell.tsx
|
|
8
11
|
const DEFAULT_ESTIMATE_SIZE_PX = 128;
|
|
9
12
|
const DEFAULT_OVERSCAN = 8;
|
|
@@ -26,15 +29,26 @@ const KANBAN_CELL_SURFACE_CLASS = "bg-muted/20";
|
|
|
26
29
|
/** A caller's own surface (an explicit colour, or the accent wash), published
|
|
27
30
|
* so a pinned action can repaint that one instead. */
|
|
28
31
|
const KANBAN_CELL_SURFACE_VAR = "--kanban-cell-surface";
|
|
32
|
+
const retainActiveSortableIndex = (range, activeIndex) => {
|
|
33
|
+
const indexes = defaultRangeExtractor(range);
|
|
34
|
+
if (activeIndex < 0 || indexes.includes(activeIndex)) return indexes;
|
|
35
|
+
indexes.push(activeIndex);
|
|
36
|
+
indexes.sort((left, right) => left - right);
|
|
37
|
+
return indexes;
|
|
38
|
+
};
|
|
29
39
|
const KANBAN_VIRTUAL_CELL_PAGINATION = {
|
|
30
40
|
NONE: "none",
|
|
31
41
|
CURSOR: "cursor"
|
|
32
42
|
};
|
|
33
43
|
/** Bounded, virtualized Kanban cell with cursor-page request deduplication. */
|
|
34
|
-
const KanbanVirtualCell = ({ rows, getRowKey, renderRow, pagination, containerRef, active = false, backgroundColor, accent, footer, footerPlacement = "end", estimateSize = DEFAULT_ESTIMATE_SIZE_PX, overscan = DEFAULT_OVERSCAN, loadMoreThreshold = DEFAULT_LOAD_MORE_THRESHOLD_PX, className }) => {
|
|
44
|
+
const KanbanVirtualCell = ({ rows, getRowKey, renderRow, pagination, sortable, containerRef, active = false, backgroundColor, accent, footer, footerPlacement = "end", estimateSize = DEFAULT_ESTIMATE_SIZE_PX, overscan = DEFAULT_OVERSCAN, loadMoreThreshold = DEFAULT_LOAD_MORE_THRESHOLD_PX, className }) => {
|
|
35
45
|
const internalRef = useRef(null);
|
|
46
|
+
const fallbackDropTargetId = useId();
|
|
36
47
|
const scrollRef = internalRef;
|
|
37
48
|
const requestedPageKeyRef = useRef(null);
|
|
49
|
+
const itemIds = sortable ? rows.map(sortable.getRowId) : [];
|
|
50
|
+
const { active: activeDrag } = useDndContext();
|
|
51
|
+
const activeSortableIndex = activeDrag === null ? -1 : itemIds.indexOf(activeDrag.id);
|
|
38
52
|
const virtualizer = useVirtualizer({
|
|
39
53
|
count: rows.length,
|
|
40
54
|
estimateSize: () => estimateSize,
|
|
@@ -43,7 +57,25 @@ const KanbanVirtualCell = ({ rows, getRowKey, renderRow, pagination, containerRe
|
|
|
43
57
|
return row === void 0 ? index : getRowKey(row);
|
|
44
58
|
},
|
|
45
59
|
getScrollElement: () => scrollRef.current,
|
|
46
|
-
overscan
|
|
60
|
+
overscan,
|
|
61
|
+
rangeExtractor: (range) => retainActiveSortableIndex(range, activeSortableIndex)
|
|
62
|
+
});
|
|
63
|
+
const requestScroll = ({ itemId }) => {
|
|
64
|
+
const index = itemIds.indexOf(itemId);
|
|
65
|
+
if (index !== -1) virtualizer.scrollToIndex(index, { align: "auto" });
|
|
66
|
+
};
|
|
67
|
+
const dropTarget = useKanbanDropTarget({
|
|
68
|
+
disabled: sortable?.dropTarget.disabled ?? sortable === void 0,
|
|
69
|
+
id: sortable?.dropTarget.id ?? fallbackDropTargetId,
|
|
70
|
+
itemIds,
|
|
71
|
+
navigation: sortable === void 0 ? { type: "static" } : {
|
|
72
|
+
requestScroll,
|
|
73
|
+
type: "virtual"
|
|
74
|
+
},
|
|
75
|
+
position: sortable?.dropTarget.position ?? {
|
|
76
|
+
column: -1,
|
|
77
|
+
lane: -1
|
|
78
|
+
}
|
|
47
79
|
});
|
|
48
80
|
const handleScroll = ({ currentTarget }) => {
|
|
49
81
|
if (pagination.type !== "cursor" || !pagination.hasMore || pagination.loading) return;
|
|
@@ -55,17 +87,13 @@ const KanbanVirtualCell = ({ rows, getRowKey, renderRow, pagination, containerRe
|
|
|
55
87
|
const setScrollElement = (element) => {
|
|
56
88
|
internalRef.current = element;
|
|
57
89
|
if (containerRef) containerRef.current = element;
|
|
90
|
+
dropTarget.setNodeRef(element);
|
|
58
91
|
};
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
backgroundColor: surface,
|
|
65
|
-
[KANBAN_CELL_SURFACE_VAR]: surface,
|
|
66
|
-
...activeAccentRing === void 0 ? void 0 : { boxShadow: activeAccentRing },
|
|
67
|
-
...accentVariants === void 0 ? void 0 : { [KANBAN_CELL_ACCENT_VAR]: accentVariants.color }
|
|
68
|
-
};
|
|
92
|
+
const { surface, style, hasAccent } = resolveKanbanCellAppearance({
|
|
93
|
+
accent,
|
|
94
|
+
active,
|
|
95
|
+
backgroundColor
|
|
96
|
+
});
|
|
69
97
|
const hasStickyFooter = footerPlacement === "sticky-start" && footer !== null && footer !== void 0;
|
|
70
98
|
const stickyFooterRef = useRef(null);
|
|
71
99
|
const [stickyFooterHeight, setStickyFooterHeight] = useState(0);
|
|
@@ -94,9 +122,10 @@ const KanbanVirtualCell = ({ rows, getRowKey, renderRow, pagination, containerRe
|
|
|
94
122
|
children: footer
|
|
95
123
|
})
|
|
96
124
|
}) : null;
|
|
97
|
-
|
|
98
|
-
className: cn(KANBAN_CELL_SURFACE_CLASS, "min-h-20 rounded-xl p-2", rows.length === 0 ? "overflow-visible" : "max-h-[min(60dvh,40rem)] overflow-y-auto overscroll-y-contain", active &&
|
|
99
|
-
"data-kanban-cell
|
|
125
|
+
const content = /* @__PURE__ */ jsxs("div", {
|
|
126
|
+
className: cn(KANBAN_CELL_SURFACE_CLASS, "min-h-20 rounded-xl p-2", rows.length === 0 ? "overflow-visible" : "max-h-[min(60dvh,40rem)] overflow-y-auto overscroll-y-contain", active && !hasAccent && "bg-primary/5 ring-primary/50 ring-2", className),
|
|
127
|
+
"data-kanban-cell": sortable?.dropTarget.id,
|
|
128
|
+
"data-kanban-cell-accent": hasAccent ? "true" : void 0,
|
|
100
129
|
onScroll: handleScroll,
|
|
101
130
|
ref: setScrollElement,
|
|
102
131
|
style,
|
|
@@ -127,6 +156,30 @@ const KanbanVirtualCell = ({ rows, getRowKey, renderRow, pagination, containerRe
|
|
|
127
156
|
footerPlacement === "end" ? footer : null
|
|
128
157
|
]
|
|
129
158
|
});
|
|
159
|
+
if (sortable === void 0) return content;
|
|
160
|
+
return /* @__PURE__ */ jsx(SortableContext, {
|
|
161
|
+
...sortable.disabled === void 0 ? {} : { disabled: sortable.disabled },
|
|
162
|
+
id: sortable.dropTarget.id,
|
|
163
|
+
items: itemIds,
|
|
164
|
+
strategy: sortable.strategy ?? verticalListSortingStrategy,
|
|
165
|
+
children: content
|
|
166
|
+
});
|
|
130
167
|
};
|
|
168
|
+
function resolveKanbanCellAppearance({ accent, active, backgroundColor }) {
|
|
169
|
+
const accentVariants = accent === void 0 ? void 0 : resolveOptionColor(accent);
|
|
170
|
+
const accentBackground = accentVariants === void 0 ? void 0 : `color-mix(in srgb, var(${KANBAN_CELL_ACCENT_VAR}) ${active ? KANBAN_CELL_ACCENT_ACTIVE_ALPHA : KANBAN_CELL_ACCENT_RESTING_ALPHA}%, var(--background))`;
|
|
171
|
+
const activeAccentRing = active && accentVariants !== void 0 ? `0 0 0 2px color-mix(in srgb, var(${KANBAN_CELL_ACCENT_VAR}) ${KANBAN_CELL_ACCENT_ACTIVE_RING_ALPHA}%, transparent)` : void 0;
|
|
172
|
+
const surface = backgroundColor ?? accentBackground;
|
|
173
|
+
return {
|
|
174
|
+
surface,
|
|
175
|
+
style: surface === void 0 ? void 0 : {
|
|
176
|
+
backgroundColor: surface,
|
|
177
|
+
[KANBAN_CELL_SURFACE_VAR]: surface,
|
|
178
|
+
...activeAccentRing === void 0 ? void 0 : { boxShadow: activeAccentRing },
|
|
179
|
+
...accentVariants === void 0 ? void 0 : { [KANBAN_CELL_ACCENT_VAR]: accentVariants.color }
|
|
180
|
+
},
|
|
181
|
+
hasAccent: accentVariants !== void 0
|
|
182
|
+
};
|
|
183
|
+
}
|
|
131
184
|
//#endregion
|
|
132
185
|
export { KANBAN_VIRTUAL_CELL_PAGINATION, KanbanVirtualCell };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "Stella's design system: bidi-aware React primitives built on Base UI, the dockable inspector pane, and the Tailwind v4 theme they are styled with.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base-ui",
|
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
"types": "./dist/components/accordion.d.ts",
|
|
51
51
|
"import": "./dist/components/accordion.js"
|
|
52
52
|
},
|
|
53
|
+
"./collapsible": {
|
|
54
|
+
"types": "./dist/components/collapsible.d.ts",
|
|
55
|
+
"import": "./dist/components/collapsible.js"
|
|
56
|
+
},
|
|
53
57
|
"./application-rail": {
|
|
54
58
|
"types": "./dist/components/application-rail.d.ts",
|
|
55
59
|
"import": "./dist/components/application-rail.js"
|
|
@@ -502,6 +506,14 @@
|
|
|
502
506
|
"types": "./dist/components/toast.d.ts",
|
|
503
507
|
"import": "./dist/components/toast.js"
|
|
504
508
|
},
|
|
509
|
+
"./toggle": {
|
|
510
|
+
"types": "./dist/components/toggle.d.ts",
|
|
511
|
+
"import": "./dist/components/toggle.js"
|
|
512
|
+
},
|
|
513
|
+
"./toggle-group": {
|
|
514
|
+
"types": "./dist/components/toggle-group.d.ts",
|
|
515
|
+
"import": "./dist/components/toggle-group.js"
|
|
516
|
+
},
|
|
505
517
|
"./tooltip": {
|
|
506
518
|
"types": "./dist/components/tooltip.d.ts",
|
|
507
519
|
"import": "./dist/components/tooltip.js"
|
|
@@ -862,6 +874,8 @@
|
|
|
862
874
|
"@atlaskit/pragmatic-drag-and-drop": "^4.0.0",
|
|
863
875
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^3.2.1",
|
|
864
876
|
"@base-ui/react": "1.9.0",
|
|
877
|
+
"@dnd-kit/core": "^6.3.1",
|
|
878
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
865
879
|
"@happy-dom/global-registrator": "21.0.4",
|
|
866
880
|
"@playwright/test": "^1.63.0",
|
|
867
881
|
"@stll/playwright-tools": "0.0.0",
|
|
@@ -885,6 +899,8 @@
|
|
|
885
899
|
"@atlaskit/pragmatic-drag-and-drop": "^4.0.0",
|
|
886
900
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^3.2.1",
|
|
887
901
|
"@base-ui/react": "^1.7.0",
|
|
902
|
+
"@dnd-kit/core": "^6.3.1",
|
|
903
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
888
904
|
"@playwright/test": "^1.63.0",
|
|
889
905
|
"@tanstack/react-virtual": "^3.14.13",
|
|
890
906
|
"@types/react": ">=19",
|