@svar-ui/svelte-kanban 2.6.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/components/Avatar.svelte +87 -0
- package/dist/components/Avatar.svelte.d.ts +17 -0
- package/dist/components/Card.svelte +346 -0
- package/dist/components/Card.svelte.d.ts +9 -0
- package/dist/components/CardList.svelte +343 -0
- package/dist/components/CardList.svelte.d.ts +21 -0
- package/dist/components/CardWrapper.svelte +49 -0
- package/dist/components/CardWrapper.svelte.d.ts +15 -0
- package/dist/components/Column.svelte +230 -0
- package/dist/components/Column.svelte.d.ts +24 -0
- package/dist/components/ContextMenu.svelte +111 -0
- package/dist/components/ContextMenu.svelte.d.ts +18 -0
- package/dist/components/DragGhost.svelte +57 -0
- package/dist/components/DragGhost.svelte.d.ts +14 -0
- package/dist/components/Editor.svelte +108 -0
- package/dist/components/Editor.svelte.d.ts +10 -0
- package/dist/components/ExportLayout.svelte +52 -0
- package/dist/components/ExportLayout.svelte.d.ts +15 -0
- package/dist/components/Kanban.svelte +100 -0
- package/dist/components/Kanban.svelte.d.ts +186 -0
- package/dist/components/Layout.svelte +362 -0
- package/dist/components/Layout.svelte.d.ts +19 -0
- package/dist/components/Toolbar.svelte +97 -0
- package/dist/components/Toolbar.svelte.d.ts +12 -0
- package/dist/components/useCardOverlay.svelte.d.ts +24 -0
- package/dist/components/useCardOverlay.svelte.js +60 -0
- package/dist/components/useDrag.svelte.d.ts +22 -0
- package/dist/components/useDrag.svelte.js +16 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.js +3 -0
- package/dist/defaults.d.ts +8 -0
- package/dist/defaults.js +85 -0
- package/dist/directives/dblclick.d.ts +13 -0
- package/dist/directives/dblclick.js +26 -0
- package/dist/directives/drag.d.ts +11 -0
- package/dist/directives/drag.js +183 -0
- package/dist/env.d.ts +9 -0
- package/dist/export/Card.svelte +5 -0
- package/dist/export/Card.svelte.d.ts +11 -0
- package/dist/export/Kanban.svelte +30 -0
- package/dist/export/Kanban.svelte.d.ts +16 -0
- package/dist/export.d.ts +3 -0
- package/dist/export.js +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/themes/Print.svelte +126 -0
- package/dist/themes/Print.svelte.d.ts +13 -0
- package/dist/themes/PrintBW.svelte +153 -0
- package/dist/themes/PrintBW.svelte.d.ts +13 -0
- package/dist/themes/Willow.svelte +45 -0
- package/dist/themes/Willow.svelte.d.ts +7 -0
- package/dist/themes/WillowDark.svelte +49 -0
- package/dist/themes/WillowDark.svelte.d.ts +7 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +1 -0
- package/license.txt +21 -0
- package/package.json +59 -0
- package/readme.md +100 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
<script lang="ts">import { getContext, onDestroy } from "svelte";
|
|
2
|
+
import { setID } from "@svar-ui/lib-dom";
|
|
3
|
+
import CardWrapper from "./CardWrapper.svelte";
|
|
4
|
+
import { KANBAN_API_CONTEXT, DND_CONTEXT, SCROLL_CONTAINER_CONTEXT } from "../context.js";
|
|
5
|
+
import { DndState } from "./useDrag.svelte.js";
|
|
6
|
+
import { dblclick } from "../directives/dblclick.js";
|
|
7
|
+
let { column, readonly = false, cardContent, cardShape, contentVisible, virtualizeCards, estimatedCardHeight, cardOverscan, fixedColumnWidth, cardCss } = $props();
|
|
8
|
+
function getCardExtraCss(card) {
|
|
9
|
+
return cardCss ? cardCss(card, column) ?? "" : "";
|
|
10
|
+
}
|
|
11
|
+
const store = getContext(KANBAN_API_CONTEXT);
|
|
12
|
+
const dnd = getContext(DND_CONTEXT);
|
|
13
|
+
const getScrollContainer = getContext(SCROLL_CONTAINER_CONTEXT);
|
|
14
|
+
const columnAccessor = $derived(store.getState().columnAccessor);
|
|
15
|
+
let container = $state();
|
|
16
|
+
let range = $state({
|
|
17
|
+
start: 0,
|
|
18
|
+
end: -1,
|
|
19
|
+
top: 0,
|
|
20
|
+
bottom: 0,
|
|
21
|
+
total: 0
|
|
22
|
+
});
|
|
23
|
+
let cardGap = $state(8);
|
|
24
|
+
let frame = 0;
|
|
25
|
+
let previousVirtualizeCards = false;
|
|
26
|
+
const heightCache = new Map();
|
|
27
|
+
const measuredNodes = new Map();
|
|
28
|
+
let cardObserver;
|
|
29
|
+
const isDropColumn = $derived(dnd?.active && dnd.target?.column === column.id);
|
|
30
|
+
const renderStart = $derived(virtualizeCards ? range.start : 0);
|
|
31
|
+
const renderEnd = $derived(virtualizeCards ? range.end : column.cards.length - 1);
|
|
32
|
+
const renderedCards = $derived(contentVisible ? column.cards.slice(renderStart, Math.max(renderStart, renderEnd + 1)) : []);
|
|
33
|
+
const hiddenHeight = $derived(range.total || estimateTotalHeight(column.cards.length));
|
|
34
|
+
const topSpacerHeight = $derived(range.start > 0 ? Math.max(0, range.top - cardGap) : 0);
|
|
35
|
+
const bottomSpacerHeight = $derived(range.bottom > 0 ? Math.max(0, range.bottom - cardGap) : 0);
|
|
36
|
+
const afterRenderedBeforeId = $derived(contentVisible && virtualizeCards && renderEnd >= 0 && renderEnd < column.cards.length - 1 ? column.cards[renderEnd + 1]?.id : undefined);
|
|
37
|
+
const trailingPlaceholder = $derived(isDropColumn && (dnd.target.beforeId == null || dnd.target.beforeId === afterRenderedBeforeId));
|
|
38
|
+
function estimateTotalHeight(count) {
|
|
39
|
+
if (!count) return 0;
|
|
40
|
+
const height = Math.max(1, estimatedCardHeight || 1);
|
|
41
|
+
return count * height + Math.max(0, count - 1) * cardGap;
|
|
42
|
+
}
|
|
43
|
+
function readGap() {
|
|
44
|
+
if (!container) return;
|
|
45
|
+
const styles = getComputedStyle(container);
|
|
46
|
+
const configuredGap = styles.getPropertyValue("--wx-card-gap").trim();
|
|
47
|
+
const raw = configuredGap || styles.rowGap || styles.gap;
|
|
48
|
+
const next = Number.parseFloat(raw);
|
|
49
|
+
cardGap = Number.isFinite(next) ? next : 8;
|
|
50
|
+
}
|
|
51
|
+
function getCardHeight(card) {
|
|
52
|
+
return card.id != null ? heightCache.get(card.id) ?? Math.max(1, estimatedCardHeight || 1) : Math.max(1, estimatedCardHeight || 1);
|
|
53
|
+
}
|
|
54
|
+
function buildOffsets() {
|
|
55
|
+
const offsets = [0];
|
|
56
|
+
let total = 0;
|
|
57
|
+
for (let i = 0; i < column.cards.length; i++) {
|
|
58
|
+
total += getCardHeight(column.cards[i]);
|
|
59
|
+
if (i < column.cards.length - 1) total += cardGap;
|
|
60
|
+
offsets.push(total);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
offsets,
|
|
64
|
+
total
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function upperBound(values, value) {
|
|
68
|
+
let low = 0;
|
|
69
|
+
let high = values.length;
|
|
70
|
+
while (low < high) {
|
|
71
|
+
const mid = Math.floor((low + high) / 2);
|
|
72
|
+
if (values[mid] <= value) low = mid + 1;
|
|
73
|
+
else high = mid;
|
|
74
|
+
}
|
|
75
|
+
return low;
|
|
76
|
+
}
|
|
77
|
+
function recalculate() {
|
|
78
|
+
if (!container) return;
|
|
79
|
+
readGap();
|
|
80
|
+
if (!contentVisible || !virtualizeCards) {
|
|
81
|
+
range = {
|
|
82
|
+
start: 0,
|
|
83
|
+
end: contentVisible ? column.cards.length - 1 : -1,
|
|
84
|
+
top: 0,
|
|
85
|
+
bottom: 0,
|
|
86
|
+
total: estimateTotalHeight(column.cards.length)
|
|
87
|
+
};
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const count = column.cards.length;
|
|
91
|
+
if (!count) {
|
|
92
|
+
range = {
|
|
93
|
+
start: 0,
|
|
94
|
+
end: -1,
|
|
95
|
+
top: 0,
|
|
96
|
+
bottom: 0,
|
|
97
|
+
total: 0
|
|
98
|
+
};
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const { offsets, total } = buildOffsets();
|
|
102
|
+
const boardScroll = getScrollContainer?.() ?? null;
|
|
103
|
+
let viewportTop = 0;
|
|
104
|
+
let viewportBottom = 0;
|
|
105
|
+
if (boardScroll) {
|
|
106
|
+
const boardRect = boardScroll.getBoundingClientRect();
|
|
107
|
+
const containerRect = container.getBoundingClientRect();
|
|
108
|
+
viewportTop = Math.max(0, boardRect.top - containerRect.top);
|
|
109
|
+
viewportBottom = Math.min(total, boardRect.bottom - containerRect.top);
|
|
110
|
+
} else {
|
|
111
|
+
viewportTop = container.scrollTop;
|
|
112
|
+
viewportBottom = viewportTop + container.clientHeight;
|
|
113
|
+
}
|
|
114
|
+
const safeOverscan = Math.max(0, Math.floor(cardOverscan || 0));
|
|
115
|
+
let start = Math.max(0, upperBound(offsets, viewportTop) - 1);
|
|
116
|
+
let end = Math.max(start, upperBound(offsets, viewportBottom) - 1);
|
|
117
|
+
start = Math.max(0, start - safeOverscan);
|
|
118
|
+
end = Math.min(count - 1, end + safeOverscan);
|
|
119
|
+
range = {
|
|
120
|
+
start,
|
|
121
|
+
end,
|
|
122
|
+
top: offsets[start],
|
|
123
|
+
bottom: Math.max(0, total - offsets[end + 1]),
|
|
124
|
+
total
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function scheduleRecalculate(..._tracked) {
|
|
128
|
+
if (frame) return;
|
|
129
|
+
frame = requestAnimationFrame(() => {
|
|
130
|
+
frame = 0;
|
|
131
|
+
recalculate();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function pruneHeightCache() {
|
|
135
|
+
const ids = new Set(column.cards.map((card) => card.id));
|
|
136
|
+
for (const id of heightCache.keys()) {
|
|
137
|
+
if (!ids.has(id)) heightCache.delete(id);
|
|
138
|
+
}
|
|
139
|
+
while (heightCache.size > 1e4) {
|
|
140
|
+
const first = heightCache.keys().next().value;
|
|
141
|
+
if (first === undefined) break;
|
|
142
|
+
heightCache.delete(first);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function ensureCardObserver() {
|
|
146
|
+
if (cardObserver || typeof ResizeObserver === "undefined") return;
|
|
147
|
+
cardObserver = new ResizeObserver((entries) => {
|
|
148
|
+
let changed = false;
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const id = measuredNodes.get(entry.target);
|
|
151
|
+
if (id == null) continue;
|
|
152
|
+
const height = Math.ceil(entry.target.offsetHeight);
|
|
153
|
+
if (height > 0 && heightCache.get(id) !== height) {
|
|
154
|
+
heightCache.delete(id);
|
|
155
|
+
heightCache.set(id, height);
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (changed) scheduleRecalculate();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function measureCard(node, card) {
|
|
163
|
+
if (card.id != null) {
|
|
164
|
+
ensureCardObserver();
|
|
165
|
+
measuredNodes.set(node, card.id);
|
|
166
|
+
cardObserver?.observe(node);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
update(next) {
|
|
170
|
+
cardObserver?.unobserve(node);
|
|
171
|
+
if (next.id != null) {
|
|
172
|
+
measuredNodes.set(node, next.id);
|
|
173
|
+
cardObserver?.observe(node);
|
|
174
|
+
} else {
|
|
175
|
+
measuredNodes.delete(node);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
destroy() {
|
|
179
|
+
cardObserver?.unobserve(node);
|
|
180
|
+
measuredNodes.delete(node);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
$effect(() => {
|
|
185
|
+
if (virtualizeCards && !previousVirtualizeCards) {
|
|
186
|
+
heightCache.clear();
|
|
187
|
+
}
|
|
188
|
+
previousVirtualizeCards = virtualizeCards;
|
|
189
|
+
pruneHeightCache();
|
|
190
|
+
scheduleRecalculate(column.cards, contentVisible, estimatedCardHeight, cardOverscan);
|
|
191
|
+
});
|
|
192
|
+
$effect(() => {
|
|
193
|
+
if (!container || !contentVisible || !virtualizeCards) return;
|
|
194
|
+
const scrollElement = getScrollContainer?.() ?? container;
|
|
195
|
+
const onScroll = () => scheduleRecalculate();
|
|
196
|
+
scrollElement.addEventListener("scroll", onScroll, { passive: true });
|
|
197
|
+
window.addEventListener("resize", onScroll);
|
|
198
|
+
scheduleRecalculate();
|
|
199
|
+
return () => {
|
|
200
|
+
scrollElement.removeEventListener("scroll", onScroll);
|
|
201
|
+
window.removeEventListener("resize", onScroll);
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
$effect(() => {
|
|
205
|
+
if (!container || !contentVisible || !virtualizeCards || fixedColumnWidth || typeof ResizeObserver === "undefined") {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
let width = container.clientWidth;
|
|
209
|
+
const observer = new ResizeObserver(() => {
|
|
210
|
+
const next = container?.clientWidth ?? 0;
|
|
211
|
+
if (next && next !== width) {
|
|
212
|
+
width = next;
|
|
213
|
+
heightCache.clear();
|
|
214
|
+
scheduleRecalculate();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
observer.observe(container);
|
|
218
|
+
return () => observer.disconnect();
|
|
219
|
+
});
|
|
220
|
+
onDestroy(() => {
|
|
221
|
+
if (frame) cancelAnimationFrame(frame);
|
|
222
|
+
cardObserver?.disconnect();
|
|
223
|
+
});
|
|
224
|
+
</script>
|
|
225
|
+
|
|
226
|
+
<div
|
|
227
|
+
class="wx-column-cards"
|
|
228
|
+
data-kanban-column-cards={setID(column.id)}
|
|
229
|
+
data-kanban-render-start={renderStart}
|
|
230
|
+
data-kanban-render-end={renderEnd}
|
|
231
|
+
data-kanban-card-count={column.cards.length}
|
|
232
|
+
data-kanban-after-rendered-before-id={afterRenderedBeforeId == null
|
|
233
|
+
? undefined
|
|
234
|
+
: setID(afterRenderedBeforeId)}
|
|
235
|
+
bind:this={container}
|
|
236
|
+
use:dblclick={{
|
|
237
|
+
store,
|
|
238
|
+
column: column.id,
|
|
239
|
+
columnAccessor,
|
|
240
|
+
readonly,
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{#if contentVisible}
|
|
244
|
+
{#if virtualizeCards}
|
|
245
|
+
{#if topSpacerHeight > 0}
|
|
246
|
+
<div
|
|
247
|
+
class="wx-virtual-spacer"
|
|
248
|
+
style:height="{topSpacerHeight}px"
|
|
249
|
+
></div>
|
|
250
|
+
{/if}
|
|
251
|
+
{#each renderedCards as cardItem (cardItem.id)}
|
|
252
|
+
{#if isDropColumn && dnd.target!.beforeId === cardItem.id}
|
|
253
|
+
<div
|
|
254
|
+
class="wx-drop-placeholder"
|
|
255
|
+
style:height="{dnd.height}px"
|
|
256
|
+
></div>
|
|
257
|
+
{/if}
|
|
258
|
+
{@const extraCss = getCardExtraCss(cardItem)}
|
|
259
|
+
<div
|
|
260
|
+
class="wx-card-row {cardItem.css ?? ''} {extraCss}"
|
|
261
|
+
data-kanban-card-id={cardItem.id == null
|
|
262
|
+
? undefined
|
|
263
|
+
: setID(cardItem.id)}
|
|
264
|
+
use:measureCard={cardItem}
|
|
265
|
+
class:wx-dragging={dnd?.active &&
|
|
266
|
+
dnd.cardId === cardItem.id}
|
|
267
|
+
>
|
|
268
|
+
<CardWrapper card={cardItem} {cardContent} {cardShape} {extraCss} />
|
|
269
|
+
</div>
|
|
270
|
+
{/each}
|
|
271
|
+
{#if trailingPlaceholder}
|
|
272
|
+
<div
|
|
273
|
+
class="wx-drop-placeholder"
|
|
274
|
+
style:height="{dnd.height}px"
|
|
275
|
+
></div>
|
|
276
|
+
{/if}
|
|
277
|
+
{#if bottomSpacerHeight > 0}
|
|
278
|
+
<div
|
|
279
|
+
class="wx-virtual-spacer"
|
|
280
|
+
style:height="{bottomSpacerHeight}px"
|
|
281
|
+
></div>
|
|
282
|
+
{/if}
|
|
283
|
+
{:else}
|
|
284
|
+
{#each column.cards as cardItem (cardItem.id)}
|
|
285
|
+
{#if isDropColumn && dnd.target!.beforeId === cardItem.id}
|
|
286
|
+
<div
|
|
287
|
+
class="wx-drop-placeholder"
|
|
288
|
+
style:height="{dnd.height}px"
|
|
289
|
+
></div>
|
|
290
|
+
{/if}
|
|
291
|
+
{@const extraCss = getCardExtraCss(cardItem)}
|
|
292
|
+
<div
|
|
293
|
+
class="wx-card-row {cardItem.css ?? ''} {extraCss}"
|
|
294
|
+
data-kanban-card-id={cardItem.id == null
|
|
295
|
+
? undefined
|
|
296
|
+
: setID(cardItem.id)}
|
|
297
|
+
class:wx-dragging={dnd?.active &&
|
|
298
|
+
dnd.cardId === cardItem.id}
|
|
299
|
+
>
|
|
300
|
+
<CardWrapper card={cardItem} {cardContent} {cardShape} {extraCss} />
|
|
301
|
+
</div>
|
|
302
|
+
{/each}
|
|
303
|
+
{#if trailingPlaceholder}
|
|
304
|
+
<div
|
|
305
|
+
class="wx-drop-placeholder"
|
|
306
|
+
style:height="{dnd.height}px"
|
|
307
|
+
></div>
|
|
308
|
+
{/if}
|
|
309
|
+
{/if}
|
|
310
|
+
{:else if hiddenHeight > 0}
|
|
311
|
+
<div
|
|
312
|
+
class="wx-virtual-spacer"
|
|
313
|
+
style:height="{hiddenHeight}px"
|
|
314
|
+
></div>
|
|
315
|
+
{/if}
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<style>
|
|
319
|
+
.wx-column-cards {
|
|
320
|
+
display: flex;
|
|
321
|
+
flex-direction: column;
|
|
322
|
+
gap: 8px;
|
|
323
|
+
padding: 8px;
|
|
324
|
+
overflow-y: auto;
|
|
325
|
+
flex: 1;
|
|
326
|
+
min-height: 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.wx-column-cards > :global(div) {
|
|
330
|
+
flex-shrink: 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.wx-drop-placeholder {
|
|
334
|
+
border: 1px dashed var(--wx-kanban-border-color);
|
|
335
|
+
border-radius: var(--wx-border-radius);
|
|
336
|
+
background: var(--wx-kanban-drop-placeholder-bg);
|
|
337
|
+
box-sizing: border-box;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.wx-dragging {
|
|
341
|
+
display: none;
|
|
342
|
+
}
|
|
343
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
import type { ColumnView, KanbanCard } from "@svar-ui/kanban-store";
|
|
3
|
+
import type { CardShape, CardCssFn } from "../types.js";
|
|
4
|
+
type Props = {
|
|
5
|
+
column: ColumnView;
|
|
6
|
+
readonly?: boolean;
|
|
7
|
+
cardContent?: Component<{
|
|
8
|
+
card: KanbanCard;
|
|
9
|
+
cardShape: CardShape;
|
|
10
|
+
}>;
|
|
11
|
+
cardShape: CardShape;
|
|
12
|
+
contentVisible: boolean;
|
|
13
|
+
virtualizeCards: boolean;
|
|
14
|
+
estimatedCardHeight: number;
|
|
15
|
+
cardOverscan: number;
|
|
16
|
+
fixedColumnWidth: boolean;
|
|
17
|
+
cardCss?: CardCssFn;
|
|
18
|
+
};
|
|
19
|
+
declare const CardList: Component<Props, {}, "">;
|
|
20
|
+
type CardList = ReturnType<typeof CardList>;
|
|
21
|
+
export default CardList;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">import { getContext } from "svelte";
|
|
2
|
+
import { setID } from "@svar-ui/lib-dom";
|
|
3
|
+
import Card from "./Card.svelte";
|
|
4
|
+
const { cardContent: CardContent, card, cardShape, extraCss = "" } = $props();
|
|
5
|
+
const _ = getContext("wx-i18n").getGroup("kanban");
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -->
|
|
9
|
+
<article
|
|
10
|
+
class="wx-card {card.css ?? ''} {extraCss}"
|
|
11
|
+
data-id={card.id == null ? undefined : setID(card.id)}
|
|
12
|
+
role="button"
|
|
13
|
+
tabindex="0"
|
|
14
|
+
aria-label={card.label ?? `${_("Card")} ${card.id}`}
|
|
15
|
+
>
|
|
16
|
+
{#if CardContent}
|
|
17
|
+
<CardContent
|
|
18
|
+
{card}
|
|
19
|
+
{cardShape}
|
|
20
|
+
></CardContent>
|
|
21
|
+
{:else}
|
|
22
|
+
<Card card={card} {cardShape} />
|
|
23
|
+
{/if}
|
|
24
|
+
</article>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.wx-card {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: 6px;
|
|
31
|
+
padding: 10px;
|
|
32
|
+
background: var(--wx-kanban-card-bg);
|
|
33
|
+
border-radius: var(--wx-border-radius);
|
|
34
|
+
box-shadow: var(--wx-kanban-card-shadow);
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
border-top: 3px solid transparent;
|
|
37
|
+
touch-action: none;
|
|
38
|
+
user-select: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.wx-card:hover {
|
|
42
|
+
box-shadow: var(--wx-kanban-card-shadow-hover);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.wx-card:focus-visible {
|
|
46
|
+
outline: 2px solid var(--wx-color-primary);
|
|
47
|
+
outline-offset: 1px;
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
import type { KanbanCard } from "@svar-ui/kanban-store";
|
|
3
|
+
import type { CardShape } from "../types.js";
|
|
4
|
+
type Props = {
|
|
5
|
+
cardContent?: Component<{
|
|
6
|
+
card: KanbanCard;
|
|
7
|
+
cardShape: CardShape;
|
|
8
|
+
}>;
|
|
9
|
+
card: KanbanCard;
|
|
10
|
+
cardShape: CardShape;
|
|
11
|
+
extraCss?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const CardWrapper: Component<Props, {}, "">;
|
|
14
|
+
type CardWrapper = ReturnType<typeof CardWrapper>;
|
|
15
|
+
export default CardWrapper;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<script lang="ts">import { getContext } from "svelte";
|
|
2
|
+
import CardList from "./CardList.svelte";
|
|
3
|
+
import { createColumnCard } from "../directives/dblclick.js";
|
|
4
|
+
import { KANBAN_API_CONTEXT } from "../context.js";
|
|
5
|
+
let { column, readonly = false, cardContent, cardShape, contentVisible, requestVisible, virtualizeCards, estimatedCardHeight, cardOverscan, fixedColumnWidth, registerColumn, cardCss, columnCss } = $props();
|
|
6
|
+
const dynamicColumnCss = $derived(columnCss ? columnCss(column.cards, column) ?? "" : "");
|
|
7
|
+
const store = getContext(KANBAN_API_CONTEXT);
|
|
8
|
+
const columnAccessor = $derived(store.getState().columnAccessor);
|
|
9
|
+
const _ = getContext("wx-i18n").getGroup("kanban");
|
|
10
|
+
let root = $state();
|
|
11
|
+
const cardLimitVisible = $derived(typeof column.cardLimit === "number" || column.cardLimit === true);
|
|
12
|
+
const cardLimitNumber = $derived(typeof column.cardLimit === "number" ? column.cardLimit : null);
|
|
13
|
+
const addCardVisible = $derived(column.addCard !== false && !readonly);
|
|
14
|
+
function toggleCollapsed() {
|
|
15
|
+
store.exec("update-column", {
|
|
16
|
+
id: column.id,
|
|
17
|
+
column: { collapsed: !column.collapsed }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function addCard() {
|
|
21
|
+
if (!addCardVisible) return;
|
|
22
|
+
const card = createColumnCard({}, columnAccessor, column.id);
|
|
23
|
+
store.exec("add-card", {
|
|
24
|
+
card,
|
|
25
|
+
edit: true
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
$effect(() => {
|
|
29
|
+
if (!registerColumn || !root) return;
|
|
30
|
+
const id = column.id;
|
|
31
|
+
registerColumn(id, root);
|
|
32
|
+
return () => registerColumn(id, null);
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<section
|
|
37
|
+
class="wx-column {column.css ?? ''} {dynamicColumnCss}"
|
|
38
|
+
class:wx-collapsed={column.collapsed}
|
|
39
|
+
class:wx-over-limit={column.overLimit}
|
|
40
|
+
bind:this={root}
|
|
41
|
+
>
|
|
42
|
+
{#if column.collapsed}
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
class="wx-expand"
|
|
46
|
+
onclick={toggleCollapsed}
|
|
47
|
+
aria-label={_("Expand column")}
|
|
48
|
+
>
|
|
49
|
+
<i class="wx-icon wxi-angle-right"></i>
|
|
50
|
+
</button>
|
|
51
|
+
<div class="wx-body">
|
|
52
|
+
<h3 class="wx-title">
|
|
53
|
+
<span>{column.label}</span>
|
|
54
|
+
{#if cardLimitVisible}
|
|
55
|
+
<span class="wx-count" class:wx-over={column.overLimit}>
|
|
56
|
+
{column.cards.length}{#if cardLimitNumber != null}/{cardLimitNumber}{/if}
|
|
57
|
+
</span>
|
|
58
|
+
{/if}
|
|
59
|
+
</h3>
|
|
60
|
+
</div>
|
|
61
|
+
{:else}
|
|
62
|
+
<header class="wx-column-header">
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
class="wx-toggle"
|
|
66
|
+
onclick={toggleCollapsed}
|
|
67
|
+
aria-label={_("Collapse column")}
|
|
68
|
+
>
|
|
69
|
+
<i class="wx-icon wxi-angle-left"></i>
|
|
70
|
+
</button>
|
|
71
|
+
<h3 class="wx-title">{column.label}</h3>
|
|
72
|
+
{#if cardLimitVisible}
|
|
73
|
+
<span class="wx-count" class:wx-over={column.overLimit}>
|
|
74
|
+
{column.cards.length}{#if cardLimitNumber != null}/{cardLimitNumber}{/if}
|
|
75
|
+
</span>
|
|
76
|
+
{/if}
|
|
77
|
+
{#if addCardVisible}
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class="wx-add"
|
|
81
|
+
onclick={addCard}
|
|
82
|
+
aria-label="{_('Add card to')} {column.label}"
|
|
83
|
+
>
|
|
84
|
+
<i class="wx-icon wxi-plus"></i>
|
|
85
|
+
</button>
|
|
86
|
+
{/if}
|
|
87
|
+
</header>
|
|
88
|
+
<CardList
|
|
89
|
+
{column}
|
|
90
|
+
{readonly}
|
|
91
|
+
{cardContent}
|
|
92
|
+
{cardShape}
|
|
93
|
+
{contentVisible}
|
|
94
|
+
{virtualizeCards}
|
|
95
|
+
{estimatedCardHeight}
|
|
96
|
+
{cardOverscan}
|
|
97
|
+
{fixedColumnWidth}
|
|
98
|
+
{cardCss}
|
|
99
|
+
/>
|
|
100
|
+
{/if}
|
|
101
|
+
</section>
|
|
102
|
+
|
|
103
|
+
<style>
|
|
104
|
+
.wx-icon {
|
|
105
|
+
font-size: 24px;
|
|
106
|
+
margin-top: 5px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.wx-column {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
flex: 0 0 280px;
|
|
113
|
+
min-width: 280px;
|
|
114
|
+
max-height: 100%;
|
|
115
|
+
background: var(--wx-kanban-column-bg);
|
|
116
|
+
border-radius: var(--wx-radius-major);
|
|
117
|
+
overflow: hidden;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.wx-collapsed {
|
|
121
|
+
flex: 0 0 40px;
|
|
122
|
+
min-width: 40px;
|
|
123
|
+
max-width: 40px;
|
|
124
|
+
align-items: center;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.wx-over-limit .wx-column-header,
|
|
128
|
+
.wx-over-limit .wx-body {
|
|
129
|
+
background: var(--wx-kanban-column-over-limit-bg);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.wx-column-header {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 8px;
|
|
136
|
+
padding: 10px 12px;
|
|
137
|
+
border-bottom: 1px solid var(--wx-kanban-border-color);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.wx-title {
|
|
141
|
+
flex: 1;
|
|
142
|
+
margin: 0;
|
|
143
|
+
font-weight: var(--wx-font-weight-md);
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
text-overflow: ellipsis;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.wx-count {
|
|
150
|
+
font-size: var(--wx-font-size-sm);
|
|
151
|
+
color: var(--wx-color-font-alt);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.wx-over {
|
|
155
|
+
color: var(--wx-color-danger);
|
|
156
|
+
font-weight: var(--wx-font-weight-md);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.wx-add,
|
|
160
|
+
.wx-toggle {
|
|
161
|
+
display: inline-flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: center;
|
|
164
|
+
background: none;
|
|
165
|
+
border: none;
|
|
166
|
+
border-radius: var(--wx-icon-border-radius);
|
|
167
|
+
padding: 0;
|
|
168
|
+
width: 18px;
|
|
169
|
+
height: 18px;
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
font-size: var(--wx-font-size-sm);
|
|
172
|
+
color: var(--wx-color-font-alt);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.wx-add:hover,
|
|
176
|
+
.wx-toggle:hover {
|
|
177
|
+
background: var(--wx-background-hover);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.wx-add:focus,
|
|
181
|
+
.wx-toggle:focus {
|
|
182
|
+
outline: none;
|
|
183
|
+
border: 1px solid var(--wx-color-primary);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.wx-expand {
|
|
187
|
+
display: inline-flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
background: none;
|
|
191
|
+
border: none;
|
|
192
|
+
padding: 0;
|
|
193
|
+
width: 100%;
|
|
194
|
+
height: 36px;
|
|
195
|
+
flex: 0 0 36px;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
font-size: var(--wx-font-size-sm);
|
|
198
|
+
color: var(--wx-color-font-alt);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.wx-body {
|
|
202
|
+
position: relative;
|
|
203
|
+
flex: 1;
|
|
204
|
+
width: 100%;
|
|
205
|
+
min-height: 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.wx-collapsed .wx-title {
|
|
209
|
+
position: absolute;
|
|
210
|
+
left: 50%;
|
|
211
|
+
bottom: 0px;
|
|
212
|
+
display: flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
gap: 8px;
|
|
215
|
+
max-width: calc(var(--wx-kanban-scroll-height, 100vh) - 96px);
|
|
216
|
+
transform: rotate(-90deg);
|
|
217
|
+
transform-origin: left center;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.wx-collapsed .wx-title > span:first-child {
|
|
221
|
+
min-width: 0;
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
text-overflow: ellipsis;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.wx-collapsed .wx-count {
|
|
227
|
+
flex: 0 0 auto;
|
|
228
|
+
font-weight: var(--wx-font-weight);
|
|
229
|
+
}
|
|
230
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
import type { ColumnID, ColumnView, KanbanCard } from "@svar-ui/kanban-store";
|
|
3
|
+
import type { CardShape, CardCssFn, ColumnCssFn } from "../types.js";
|
|
4
|
+
type Props = {
|
|
5
|
+
column: ColumnView;
|
|
6
|
+
readonly?: boolean;
|
|
7
|
+
cardContent?: Component<{
|
|
8
|
+
card: KanbanCard;
|
|
9
|
+
cardShape: CardShape;
|
|
10
|
+
}>;
|
|
11
|
+
cardShape: CardShape;
|
|
12
|
+
contentVisible: boolean;
|
|
13
|
+
requestVisible: boolean;
|
|
14
|
+
virtualizeCards: boolean;
|
|
15
|
+
estimatedCardHeight: number;
|
|
16
|
+
cardOverscan: number;
|
|
17
|
+
fixedColumnWidth: boolean;
|
|
18
|
+
registerColumn?: (id: ColumnID, element: HTMLElement | null) => void;
|
|
19
|
+
cardCss?: CardCssFn;
|
|
20
|
+
columnCss?: ColumnCssFn;
|
|
21
|
+
};
|
|
22
|
+
declare const Column: Component<Props, {}, "">;
|
|
23
|
+
type Column = ReturnType<typeof Column>;
|
|
24
|
+
export default Column;
|