@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/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 the board; the provider must assign ordering
2646
- atomically. Omitting `beforeTicketId` appends. Cards support drag-and-drop and a
2647
- keyboard-accessible Move menu with within-column ordering and column selection.
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 emits `kanban-ticket-move` with the destination and insertion target.
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.1',
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', 'kanban-drag-start', 'kanban-drag-end'] as const;
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.map(column => html`<dees-kanban-column .column=${column} .columns=${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) && !this.pendingTicketId} .canCreate=${Boolean(this.provider.createTicket)}
120
- .pendingTicketId=${this.pendingTicketId} .draggingTicketId=${this.draggingTicketId}></dees-kanban-column>`)}</div>`}`;
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
- this.pendingTicketId = ticket.id; this.operationError = ''; this.draggingTicketId = '';
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
- } catch (error) { this.reportError(error); }
241
- finally { this.pendingTicketId = ''; this.syncDetail(); }
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)} draggable=${this.movable && !this.pending ? 'true' : 'false'}
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
- @state() private accessor dropBefore: string | undefined;
20
- @state() private accessor draggingOver = false;
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
- .target.dragging { opacity: .4; }
32
- .target.before::before, .end-target.before::before { content: ''; position: absolute; top: -5px; left: 0; right: 0; height: 2px; background: var(--dees-color-accent-primary); border-radius: 2px; }
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.draggingOver = false; }
43
+ public updated(): void { if (!this.draggingTicketId) this.nativeOver = false; }
38
44
  public render() {
39
45
  if (!this.column) return html``;
40
- return html`<section class=${this.draggingOver ? 'over' : ''} aria-label=${this.column.title}
41
- @dragover=${(event: DragEvent) => { if (this.draggingTicketId && this.movable) { event.preventDefault(); this.draggingOver = true; if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; } }}
42
- @dragleave=${(event: DragEvent) => { if (!(event.relatedTarget instanceof Node) || !(event.currentTarget as Node).contains(event.relatedTarget)) this.draggingOver = false; }}
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
- event.preventDefault(); this.draggingOver = false;
45
- if (this.movable && this.draggingTicketId && event.dataTransfer?.getData('application/x-dees-kanban-ticket') === this.draggingTicketId)
46
- kanbanEvent(this, 'kanban-ticket-move', { ticketId: this.draggingTicketId, columnId: this.column!.id, beforeTicketId: this.dropBefore });
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">${this.tickets.length}</span><span class="spacer"></span>
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">${sortKanbanTickets(this.tickets).map(ticket => html`
52
- <div class="target ${this.draggingOver && this.dropBefore === ticket.id ? 'before' : ''} ${this.draggingTicketId === ticket.id ? 'dragging' : ''}"
53
- @dragover=${() => { this.dropBefore = ticket.id; }}>
54
- <dees-kanban-card .ticket=${ticket} .columns=${this.columns}
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}></dees-kanban-card>
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
- ${!this.tickets.length ? html`<div class="empty" @dragover=${() => { this.dropBefore = undefined; }}>No tasks</div>` : ''}
75
+ ${!tickets.length ? html`<div class="empty" @dragover=${() => { this.nativeDropBefore = undefined; }}>No tasks</div>` : ''}
60
76
  </div>
61
- <div class="end-target ${this.draggingOver && !this.dropBefore ? 'before' : ''}" @dragover=${() => { this.dropBefore = undefined; }}></div>
77
+ <div class="end-target ${this.nativeOver && !this.nativeDropBefore ? 'native-before' : ''}" @dragover=${() => { this.nativeDropBefore = undefined; }}></div>
62
78
  </section>`;
63
79
  }
64
80
  }