@design.estate/dees-catalog 13.0.1 → 13.0.2
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_bundle/bundle.js +638 -68
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.d.ts +3 -1
- package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.js +44 -29
- package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.d.ts +4 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.js +37 -8
- package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.d.ts +7 -2
- package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.js +77 -39
- package/dist_ts_web/elements/00group-kanban/kanban.dragcontroller.d.ts +67 -0
- package/dist_ts_web/elements/00group-kanban/kanban.dragcontroller.js +548 -0
- package/package.json +1 -1
- package/readme.md +11 -4
- package/scripts/check-packed-consumer.mjs +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-kanban/dees-kanban/dees-kanban.ts +30 -17
- package/ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.ts +12 -5
- package/ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.ts +37 -21
- package/ts_web/elements/00group-kanban/kanban.dragcontroller.ts +481 -0
package/readme.md
CHANGED
|
@@ -2642,9 +2642,15 @@ must enforce the same permissions.
|
|
|
2642
2642
|
Provider reads receive an `AbortSignal`; obsolete reads cannot overwrite a newer
|
|
2643
2643
|
selection. Reject mutations with an `Error` to show the failure and preserve the
|
|
2644
2644
|
draft. Successful callbacks supply authoritative records to the component.
|
|
2645
|
-
Moves are awaited before changing
|
|
2646
|
-
atomically. Omitting `beforeTicketId` appends.
|
|
2647
|
-
|
|
2645
|
+
Moves are awaited before changing `.tickets` or emitting `kanban-change`; the
|
|
2646
|
+
provider must assign ordering atomically. Omitting `beforeTicketId` appends.
|
|
2647
|
+
Mouse and pen dragging shows a card-sized drop slot, animates neighboring cards,
|
|
2648
|
+
and lands the preview into position. A render-only placement stays visible while
|
|
2649
|
+
saving; slow saves show a status, and failures animate back to the saved position.
|
|
2650
|
+
Escape or dropping outside the board cancels. Edge scrolling works inside scroll
|
|
2651
|
+
containers, and reduced motion removes lift, rearrangement and landing animation.
|
|
2652
|
+
Touch retains normal scrolling. The keyboard-accessible Move menu supports both
|
|
2653
|
+
within-column ordering and column selection.
|
|
2648
2654
|
|
|
2649
2655
|
Hierarchy is independent of workflow: tasks optionally belong to epics, and
|
|
2650
2656
|
subtasks belong to tasks. Each ticket has its own `columnId`; moving a parent
|
|
@@ -2671,7 +2677,8 @@ Public methods and events:
|
|
|
2671
2677
|
|
|
2672
2678
|
`dees-kanban-column` accepts `.column`, `.tickets`, `.allTickets`, `.columns`,
|
|
2673
2679
|
`.movable`, `.canCreate`, `.pendingTicketId` and `.draggingTicketId`. It composes
|
|
2674
|
-
cards and
|
|
2680
|
+
cards and forwards their open, create and Move menu requests. Standalone columns
|
|
2681
|
+
retain native dragging and emit `kanban-ticket-move` with the insertion target.
|
|
2675
2682
|
Use it through `dees-kanban` for provider handling and board-local drag ownership.
|
|
2676
2683
|
|
|
2677
2684
|
#### DeesKanbanCard
|
|
@@ -30,7 +30,7 @@ writeFileSync(join(consumer, 'package.json'), JSON.stringify({
|
|
|
30
30
|
}, null, 2));
|
|
31
31
|
writeFileSync(join(consumer, 'pnpm-workspace.yaml'), 'packages:\n - "."\n');
|
|
32
32
|
// Run the same regressions against installed JavaScript, not sources.
|
|
33
|
-
const browserTests = ['table-construction', 'datepicker-validation', 'textarea', 'composer-attachments', 'kanban'];
|
|
33
|
+
const browserTests = ['table-construction', 'datepicker-validation', 'textarea', 'composer-attachments', 'kanban', 'kanban-motion'];
|
|
34
34
|
for (const name of browserTests) {
|
|
35
35
|
const browserTest = readFileSync(join(root, `test/test.${name}.browser.ts`), 'utf8')
|
|
36
36
|
.replaceAll('../ts_web/', '@design.estate/dees-catalog/dist_ts_web/');
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@design.estate/dees-catalog',
|
|
6
|
-
version: '13.0.
|
|
6
|
+
version: '13.0.2',
|
|
7
7
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
|
8
8
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DeesElement, customElement, property, state, html, css } from '@design.estate/dees-element';
|
|
1
|
+
import { DeesElement, customElement, property, state, html, css, directives } from '@design.estate/dees-element';
|
|
2
2
|
import type { IKanbanColumn, IKanbanComment, IKanbanCreate, IKanbanMove, IKanbanPerson, IKanbanProvider, IKanbanTicket, IKanbanTicketDraft } from '../interfaces.js';
|
|
3
3
|
import type { TReferenceContent, TReferenceSearch } from '../../00group-input/dees-input-reference/interfaces.js';
|
|
4
4
|
import type { DeesInputText } from '../../00group-input/dees-input-text/dees-input-text.js';
|
|
@@ -8,13 +8,14 @@ import { DeesKanbanTicketDetail } from '../dees-kanban-ticket-detail/dees-kanban
|
|
|
8
8
|
import { kanbanStyles } from '../kanban.styles.js';
|
|
9
9
|
import { kanbanError, kanbanEvent, sortKanbanTickets, ticketLabel } from '../kanban.helpers.js';
|
|
10
10
|
import { demoFunc } from './dees-kanban.demo.js';
|
|
11
|
+
import { KanbanDragController } from '../kanban.dragcontroller.js';
|
|
11
12
|
import '../dees-kanban-column/dees-kanban-column.js';
|
|
12
13
|
|
|
13
14
|
declare global { interface HTMLElementTagNameMap { 'dees-kanban': DeesKanban; } }
|
|
14
15
|
|
|
15
16
|
const interactionEvents = ['kanban-ticket-open', 'kanban-ticket-move', 'kanban-ticket-create', 'kanban-move-menu',
|
|
16
17
|
'kanban-ticket-save', 'kanban-ticket-draft', 'kanban-comment-draft', 'kanban-comment-added', 'kanban-comments-refresh',
|
|
17
|
-
'kanban-detail-back'
|
|
18
|
+
'kanban-detail-back'] as const;
|
|
18
19
|
|
|
19
20
|
interface IKanbanInteractionMap {
|
|
20
21
|
'kanban-ticket-open': { ticketId: string };
|
|
@@ -27,8 +28,6 @@ interface IKanbanInteractionMap {
|
|
|
27
28
|
'kanban-comment-added': { comment: IKanbanComment; submittedBody: TReferenceContent };
|
|
28
29
|
'kanban-comments-refresh': { ticketId: string };
|
|
29
30
|
'kanban-detail-back': Record<string, never>;
|
|
30
|
-
'kanban-drag-start': { ticketId: string };
|
|
31
|
-
'kanban-drag-end': Record<string, never>;
|
|
32
31
|
}
|
|
33
32
|
type TKanbanInteraction = { [K in keyof IKanbanInteractionMap]: { type: K; detail: IKanbanInteractionMap[K] } }[keyof IKanbanInteractionMap];
|
|
34
33
|
|
|
@@ -47,7 +46,6 @@ export class DeesKanban extends DeesElement {
|
|
|
47
46
|
@state() private accessor epicId = '';
|
|
48
47
|
@state() private accessor operationError = '';
|
|
49
48
|
@state() private accessor pendingTicketId = '';
|
|
50
|
-
@state() private accessor draggingTicketId = '';
|
|
51
49
|
@state() private accessor openingTicket = false;
|
|
52
50
|
private modal?: DeesModal;
|
|
53
51
|
private modalOpening?: Promise<DeesModal>;
|
|
@@ -61,6 +59,8 @@ export class DeesKanban extends DeesElement {
|
|
|
61
59
|
private commentDrafts = new Map<string, TReferenceContent>();
|
|
62
60
|
private ticketDrafts = new Map<string, IKanbanTicketDraft>();
|
|
63
61
|
private resolvedTickets = new Map<string, IKanbanTicket>();
|
|
62
|
+
private activeMove?: symbol;
|
|
63
|
+
private readonly dragController = new KanbanDragController(this, input => this.persistMove(input), () => !this.pendingTicketId);
|
|
64
64
|
|
|
65
65
|
public static styles = [...kanbanStyles, css`
|
|
66
66
|
:host { padding: 20px 24px; background: var(--dees-color-bg-canvas); min-height: 100%; }
|
|
@@ -89,9 +89,10 @@ export class DeesKanban extends DeesElement {
|
|
|
89
89
|
public async connectedCallback(): Promise<void> {
|
|
90
90
|
const connection = super.connectedCallback();
|
|
91
91
|
for (const name of interactionEvents) this.addEventListener(name, this.handleInteraction);
|
|
92
|
+
this.dragController.connect();
|
|
92
93
|
await connection;
|
|
93
94
|
}
|
|
94
|
-
public updated(): void { this.syncDetail(); }
|
|
95
|
+
public updated(): void { this.syncDetail(); this.dragController.sync(); }
|
|
95
96
|
public render() {
|
|
96
97
|
const epics = sortKanbanTickets(this.tickets.filter(ticket => ticket.kind === 'epic'));
|
|
97
98
|
const query = this.query.trim().toLocaleLowerCase();
|
|
@@ -114,10 +115,12 @@ export class DeesKanban extends DeesElement {
|
|
|
114
115
|
${this.loading || this.openingTicket ? html`<p class="caption" role="status">${this.loading ? 'Loading board…' : 'Opening ticket…'}</p>` : ''}
|
|
115
116
|
${this.error || this.operationError ? html`<p class="error" role="alert">${this.error || this.operationError}</p>` : ''}
|
|
116
117
|
${!this.columns.length ? html`<p class="empty">No columns configured.</p>` : html`<div class="columns" aria-label=${this.heading}>
|
|
117
|
-
${this.columns.
|
|
118
|
+
${directives.repeat(this.columns, column => column.id, column => html`<dees-kanban-column .column=${column} .columns=${this.columns}
|
|
119
|
+
.pointerManaged=${true}
|
|
118
120
|
.tickets=${tasks.filter(ticket => ticket.columnId === column.id)} .allTickets=${this.tickets}
|
|
119
|
-
.movable=${Boolean(this.provider.moveTicket)
|
|
120
|
-
.
|
|
121
|
+
.movable=${Boolean(this.provider.moveTicket)} .canCreate=${Boolean(this.provider.createTicket)}
|
|
122
|
+
.movesDisabled=${Boolean(this.pendingTicketId)}
|
|
123
|
+
.pendingTicketId=${this.pendingTicketId}></dees-kanban-column>`)}</div>`}`;
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
private findTicket(id: string): IKanbanTicket | undefined { return this.tickets.find(ticket => ticket.id === id) ?? this.resolvedTickets.get(id); }
|
|
@@ -227,18 +230,28 @@ export class DeesKanban extends DeesElement {
|
|
|
227
230
|
|
|
228
231
|
/** Provider commits the move and supplies the new ordering. Hierarchy is never part of a move request. */
|
|
229
232
|
public async moveTicket(input: IKanbanMove): Promise<void> {
|
|
233
|
+
if (this.dragController.busy) return;
|
|
234
|
+
await this.persistMove(input, true);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async persistMove(input: IKanbanMove, animate = false): Promise<boolean> {
|
|
230
238
|
const ticket = this.findTicket(input.ticketId);
|
|
231
|
-
if (!ticket || !this.provider.moveTicket || ticket.permissions?.move === false || this.pendingTicketId || input.beforeTicketId === ticket.id) return;
|
|
232
|
-
if (!this.columns.some(column => column.id === input.columnId)) { this.reportError(new Error('Unknown destination column.')); return; }
|
|
239
|
+
if (!ticket || !this.provider.moveTicket || ticket.permissions?.move === false || this.pendingTicketId || input.beforeTicketId === ticket.id) return false;
|
|
240
|
+
if (!this.columns.some(column => column.id === input.columnId)) { this.reportError(new Error('Unknown destination column.')); return false; }
|
|
233
241
|
const before = input.beforeTicketId ? this.findTicket(input.beforeTicketId) : undefined;
|
|
234
|
-
if (input.beforeTicketId && (!before || before.columnId !== input.columnId || before.kind !== ticket.kind)) { this.reportError(new Error('Invalid insertion target.')); return; }
|
|
235
|
-
|
|
242
|
+
if (input.beforeTicketId && (!before || before.columnId !== input.columnId || before.kind !== ticket.kind)) { this.reportError(new Error('Invalid insertion target.')); return false; }
|
|
243
|
+
const operation = this.activeMove = Symbol();
|
|
244
|
+
this.pendingTicketId = ticket.id; this.operationError = '';
|
|
236
245
|
try {
|
|
237
246
|
const changed = await this.provider.moveTicket(input);
|
|
247
|
+
if (this.activeMove !== operation) return false;
|
|
238
248
|
if (!changed.some(item => item.id === ticket.id)) throw new Error('The move provider did not return the moved ticket.');
|
|
249
|
+
const positions = animate ? this.dragController.capture() : undefined;
|
|
239
250
|
this.mergeTickets(changed);
|
|
240
|
-
|
|
241
|
-
|
|
251
|
+
if (positions) { await this.updateComplete; await this.dragController.animateFrom(positions); }
|
|
252
|
+
return true;
|
|
253
|
+
} catch (error) { if (this.activeMove === operation) this.reportError(error); return false; }
|
|
254
|
+
finally { if (this.activeMove === operation) { this.activeMove = undefined; this.pendingTicketId = ''; this.syncDetail(); } }
|
|
242
255
|
}
|
|
243
256
|
|
|
244
257
|
private async saveTicket(ticketId: string, draft: IKanbanTicketDraft): Promise<void> {
|
|
@@ -315,13 +328,13 @@ export class DeesKanban extends DeesElement {
|
|
|
315
328
|
}
|
|
316
329
|
case 'kanban-comments-refresh': void this.refreshComments(detail.ticketId); break;
|
|
317
330
|
case 'kanban-detail-back': { const previous = this.history.pop(); if (previous) void this.openTicket(previous, false); break; }
|
|
318
|
-
case 'kanban-drag-start': this.draggingTicketId = detail.ticketId; break;
|
|
319
|
-
case 'kanban-drag-end': this.draggingTicketId = ''; break;
|
|
320
331
|
}
|
|
321
332
|
};
|
|
322
333
|
|
|
323
334
|
public async disconnectedCallback(): Promise<void> {
|
|
324
335
|
for (const name of interactionEvents) this.removeEventListener(name, this.handleInteraction);
|
|
336
|
+
this.dragController.disconnect();
|
|
337
|
+
if (this.activeMove) { this.activeMove = undefined; this.pendingTicketId = ''; }
|
|
325
338
|
this.openAbort?.abort(); this.commentsAbort?.abort();
|
|
326
339
|
void this.modal?.destroy();
|
|
327
340
|
await super.disconnectedCallback();
|
|
@@ -14,16 +14,22 @@ export class DeesKanbanCard extends DeesElement {
|
|
|
14
14
|
@property({ attribute: false }) accessor subtasks: IKanbanTicket[] = [];
|
|
15
15
|
@property({ attribute: false }) accessor columns: IKanbanColumn[] = [];
|
|
16
16
|
@property({ type: Boolean }) accessor movable = false;
|
|
17
|
+
@property({ type: Boolean }) accessor moveDisabled = false;
|
|
17
18
|
@property({ type: Boolean }) accessor pending = false;
|
|
19
|
+
@property({ type: Boolean }) accessor saving = false;
|
|
20
|
+
/** The assembled board owns pointer previews; standalone cards retain native drag events. */
|
|
21
|
+
@property({ attribute: false }) accessor pointerManaged = false;
|
|
18
22
|
public static styles = [...kanbanStyles, css`
|
|
19
23
|
article { padding: 12px; border: 1px solid var(--dees-color-border-subtle); border-radius: var(--dees-radius-xl); corner-shape: var(--dees-corner-shape); background: var(--dees-color-bg-primary); box-shadow: var(--dees-shadow-xs); }
|
|
20
24
|
article:hover { border-color: var(--dees-color-border-strong); }
|
|
21
|
-
article[aria-busy=true] { opacity: .65; }
|
|
25
|
+
article[aria-busy=true] > :not(.meta), article[aria-busy=true] .meta > :not(.saving) { opacity: .65; }
|
|
26
|
+
article[data-movable=true] { cursor: grab; }
|
|
22
27
|
.meta { display: flex; align-items: center; gap: 8px; min-height: 24px; font-size: var(--dees-font-caption1-size); color: var(--dees-color-text-secondary); }
|
|
23
28
|
.key { flex-shrink: 0; font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
24
29
|
.link.epic { min-width: 0; color: var(--dees-color-text-secondary); font-size: var(--dees-font-caption1-size); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
25
30
|
.link.epic:hover { color: var(--dees-color-link); text-decoration: underline; }
|
|
26
31
|
.move { flex-shrink: 0; margin-left: auto; }
|
|
32
|
+
.saving { margin-left: auto; font-size: var(--dees-font-caption1-size); white-space: nowrap; }
|
|
27
33
|
.move::part(button) { width: 24px; height: 24px; min-height: 24px; }
|
|
28
34
|
.title { display: block; width: 100%; border: 0; background: transparent; padding: 4px 0 0; color: inherit; text-align: left; cursor: pointer; font: inherit; font-weight: 600; line-height: 22px; overflow-wrap: anywhere; }
|
|
29
35
|
.title:hover { color: var(--dees-color-link); }
|
|
@@ -48,16 +54,17 @@ export class DeesKanbanCard extends DeesElement {
|
|
|
48
54
|
const ticket = this.ticket;
|
|
49
55
|
if (!ticket) return html``;
|
|
50
56
|
const completed = this.subtasks.filter(task => this.columns.some(column => column.id === task.columnId && column.done)).length;
|
|
51
|
-
return html`<article aria-busy=${String(this.pending)}
|
|
57
|
+
return html`<article aria-busy=${String(this.pending)} data-movable=${String(this.movable && !this.moveDisabled && !this.pending)}
|
|
58
|
+
draggable=${String(this.movable && !this.moveDisabled && !this.pending && !this.pointerManaged)}
|
|
52
59
|
@dragstart=${(event: DragEvent) => {
|
|
53
|
-
if (!this.movable || this.pending || !event.dataTransfer) { event.preventDefault(); return; }
|
|
60
|
+
if (this.pointerManaged || !this.movable || this.moveDisabled || this.pending || !event.dataTransfer) { event.preventDefault(); return; }
|
|
54
61
|
event.dataTransfer.setData('application/x-dees-kanban-ticket', ticket.id);
|
|
55
62
|
event.dataTransfer.effectAllowed = 'move';
|
|
56
63
|
kanbanEvent(this, 'kanban-drag-start', { ticketId: ticket.id });
|
|
57
|
-
}} @dragend=${() => kanbanEvent(this, 'kanban-drag-end', {})}>
|
|
64
|
+
}} @dragend=${() => { if (!this.pointerManaged) kanbanEvent(this, 'kanban-drag-end', {}); }}>
|
|
58
65
|
<div class="meta"><span class="key">${ticket.key}</span>
|
|
59
66
|
${this.epic ? html`<button class="link epic" title=${this.epic.title} @click=${() => this.open(this.epic!.id)}>${this.epic.title}</button>` : ''}
|
|
60
|
-
${this.movable ? html`<dees-button class="move" size="icon" type="ghost" icon="lucide:Ellipsis" text="Move ticket" .disabled=${this.pending}
|
|
67
|
+
${this.saving ? html`<span class="saving" role="status">Saving…</span>` : this.movable ? html`<dees-button class="move" size="icon" type="ghost" icon="lucide:Ellipsis" text="Move ticket" .disabled=${this.pending || this.moveDisabled}
|
|
61
68
|
@clicked=${(event: Event) => kanbanEvent(this, 'kanban-move-menu', { ticketId: ticket.id, anchor: event.currentTarget as HTMLElement })}></dees-button>` : ''}
|
|
62
69
|
</div>
|
|
63
70
|
<button class="title" @click=${() => this.open(ticket.id)}>${ticket.title}</button>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { DeesElement, customElement, property, state, html, css } from '@design.estate/dees-element';
|
|
1
|
+
import { DeesElement, customElement, property, state, html, css, directives } from '@design.estate/dees-element';
|
|
2
2
|
import type { IKanbanColumn, IKanbanTicket } from '../interfaces.js';
|
|
3
|
+
import type { IKanbanMotion } from '../kanban.dragcontroller.js';
|
|
3
4
|
import { kanbanStyles } from '../kanban.styles.js';
|
|
4
5
|
import { kanbanEvent, sortKanbanTickets } from '../kanban.helpers.js';
|
|
5
6
|
import '../dees-kanban-card/dees-kanban-card.js';
|
|
@@ -13,14 +14,18 @@ export class DeesKanbanColumn extends DeesElement {
|
|
|
13
14
|
@property({ attribute: false }) accessor allTickets: IKanbanTicket[] = [];
|
|
14
15
|
@property({ attribute: false }) accessor columns: IKanbanColumn[] = [];
|
|
15
16
|
@property({ type: Boolean }) accessor movable = false;
|
|
17
|
+
@property({ type: Boolean }) accessor movesDisabled = false;
|
|
16
18
|
@property({ type: Boolean }) accessor canCreate = false;
|
|
17
19
|
@property({ type: String }) accessor pendingTicketId = '';
|
|
18
20
|
@property({ type: String }) accessor draggingTicketId = '';
|
|
19
|
-
@
|
|
20
|
-
@state() private accessor
|
|
21
|
+
@property({ attribute: false }) accessor pointerManaged = false;
|
|
22
|
+
@state() private accessor nativeDropBefore: string | undefined;
|
|
23
|
+
@state() private accessor nativeOver = false;
|
|
24
|
+
/** Internal render projection; provider-owned ticket data remains unchanged until saved. */
|
|
25
|
+
@property({ attribute: false }) accessor motion: IKanbanMotion | undefined;
|
|
21
26
|
public static styles = [...kanbanStyles, css`
|
|
22
27
|
:host { display: flex; flex-direction: column; min-width: 272px; max-width: 360px; flex: 1 0 272px; }
|
|
23
|
-
section { min-height: 220px; height: 100%; border-radius: var(--dees-radius-xl); }
|
|
28
|
+
section { min-height: 220px; height: 100%; border-radius: var(--dees-radius-xl); transition: background var(--dees-transition-default) var(--dees-ease-out); }
|
|
24
29
|
header { display: flex; align-items: center; gap: 8px; min-height: 40px; margin-bottom: 10px; padding: 0 2px 8px; border-bottom: 1px solid var(--dees-color-border-subtle); }
|
|
25
30
|
h3 { font-size: var(--dees-font-footnote-size); font-weight: 600; }
|
|
26
31
|
header dees-button::part(button) { width: 24px; height: 24px; min-height: 24px; }
|
|
@@ -28,37 +33,48 @@ export class DeesKanbanColumn extends DeesElement {
|
|
|
28
33
|
.status.done { background: var(--dees-color-accent-success); border-color: var(--dees-color-accent-success); }
|
|
29
34
|
.stack { display: grid; gap: 8px; min-height: 120px; align-content: start; }
|
|
30
35
|
.target { position: relative; }
|
|
31
|
-
.
|
|
32
|
-
.
|
|
36
|
+
.native-before::before { content: ''; position: absolute; top: -5px; left: 0; right: 0; height: 2px; background: var(--dees-color-accent-primary); border-radius: 2px; }
|
|
37
|
+
.placeholder { border-radius: var(--dees-radius-xl); background: var(--dees-color-fill-tertiary); box-shadow: inset 0 0 0 1px var(--dees-color-border-default); }
|
|
33
38
|
.end-target { position: relative; min-height: 32px; margin-top: 8px; }
|
|
34
39
|
section.over { background: var(--dees-color-row-hover); }
|
|
40
|
+
@media (prefers-reduced-motion: reduce) { section { transition: none; } }
|
|
35
41
|
@media (pointer: coarse) { header dees-button::part(button) { width: 32px; height: 32px; min-height: 32px; } }
|
|
36
42
|
`];
|
|
37
|
-
public updated(): void { if (!this.draggingTicketId) this.
|
|
43
|
+
public updated(): void { if (!this.draggingTicketId) this.nativeOver = false; }
|
|
38
44
|
public render() {
|
|
39
45
|
if (!this.column) return html``;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
const motion = this.motion;
|
|
47
|
+
const tickets = sortKanbanTickets(this.tickets).filter(ticket => ticket.id !== motion?.move.ticketId);
|
|
48
|
+
const active = motion?.move.columnId === this.column.id;
|
|
49
|
+
if (motion && active) {
|
|
50
|
+
const index = tickets.findIndex(ticket => ticket.id === motion.move.beforeTicketId);
|
|
51
|
+
tickets.splice(index < 0 ? tickets.length : index, 0, motion.ticket);
|
|
52
|
+
}
|
|
53
|
+
return html`<section class=${active && motion?.placeholder || this.nativeOver ? 'over' : ''} aria-label=${this.column.title}
|
|
54
|
+
@dragover=${(event: DragEvent) => { if (!this.pointerManaged && this.draggingTicketId && this.movable && !this.movesDisabled) { event.preventDefault(); this.nativeOver = true; if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; } }}
|
|
55
|
+
@dragleave=${(event: DragEvent) => { if (!(event.relatedTarget instanceof Node) || !(event.currentTarget as Node).contains(event.relatedTarget)) this.nativeOver = false; }}
|
|
43
56
|
@drop=${(event: DragEvent) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
if (this.pointerManaged) return;
|
|
58
|
+
event.preventDefault(); this.nativeOver = false;
|
|
59
|
+
if (this.movable && !this.movesDisabled && this.draggingTicketId && event.dataTransfer?.getData('application/x-dees-kanban-ticket') === this.draggingTicketId)
|
|
60
|
+
kanbanEvent(this, 'kanban-ticket-move', { ticketId: this.draggingTicketId, columnId: this.column!.id, beforeTicketId: this.nativeDropBefore });
|
|
47
61
|
}}>
|
|
48
|
-
<header><span class="status ${this.column.done ? 'done' : ''}" aria-hidden="true"></span><h3>${this.column.title}</h3><span class="count">${
|
|
62
|
+
<header><span class="status ${this.column.done ? 'done' : ''}" aria-hidden="true"></span><h3>${this.column.title}</h3><span class="count">${tickets.length}</span><span class="spacer"></span>
|
|
49
63
|
${this.canCreate ? html`<dees-button type="ghost" size="icon" icon="lucide:Plus" text=${`Add task to ${this.column.title}`}
|
|
50
64
|
@clicked=${() => kanbanEvent(this, 'kanban-ticket-create', { kind: 'task', columnId: this.column!.id })}></dees-button>` : ''}</header>
|
|
51
|
-
<div class="stack">${
|
|
52
|
-
<div class="target ${this.
|
|
53
|
-
@dragover=${() => { this.
|
|
54
|
-
|
|
65
|
+
<div class="stack">${directives.repeat(tickets, ticket => ticket.id, ticket => html`
|
|
66
|
+
<div class="target ${this.nativeOver && this.nativeDropBefore === ticket.id ? 'native-before' : ''}" data-ticket-id=${ticket.id}
|
|
67
|
+
@dragover=${() => { if (!this.pointerManaged) this.nativeDropBefore = ticket.id; }}>
|
|
68
|
+
${motion?.move.ticketId === ticket.id && motion.placeholder ? html`<div class="placeholder" style=${`height: ${motion.height}px`} aria-label="Drop ticket here"></div>` : html`<dees-kanban-card .ticket=${ticket} .columns=${this.columns}
|
|
55
69
|
.epic=${this.allTickets.find(item => item.id === ticket.parentId && item.kind === 'epic')}
|
|
56
70
|
.subtasks=${sortKanbanTickets(this.allTickets.filter(item => item.parentId === ticket.id && item.kind === 'subtask'))}
|
|
57
|
-
.movable=${this.movable && ticket.permissions?.move !== false} .pending=${this.pendingTicketId === ticket.id}
|
|
71
|
+
.movable=${this.movable && ticket.permissions?.move !== false} .pending=${motion?.move.ticketId === ticket.id ? motion.dimmed : this.pendingTicketId === ticket.id}
|
|
72
|
+
.moveDisabled=${this.movesDisabled}
|
|
73
|
+
.saving=${motion?.move.ticketId === ticket.id && motion.saving} .pointerManaged=${this.pointerManaged}></dees-kanban-card>`}
|
|
58
74
|
</div>`)}
|
|
59
|
-
${!
|
|
75
|
+
${!tickets.length ? html`<div class="empty" @dragover=${() => { this.nativeDropBefore = undefined; }}>No tasks</div>` : ''}
|
|
60
76
|
</div>
|
|
61
|
-
<div class="end-target ${this.
|
|
77
|
+
<div class="end-target ${this.nativeOver && !this.nativeDropBefore ? 'native-before' : ''}" @dragover=${() => { this.nativeDropBefore = undefined; }}></div>
|
|
62
78
|
</section>`;
|
|
63
79
|
}
|
|
64
80
|
}
|