@f-ewald/components 1.1.0 → 1.2.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.
Files changed (41) hide show
  1. package/README.md +4 -0
  2. package/custom-elements.json +2329 -301
  3. package/dist/dropdown-button.d.ts.map +1 -1
  4. package/dist/dropdown-button.js +1 -0
  5. package/dist/dropdown-button.js.map +1 -1
  6. package/dist/form-select.d.ts +5 -4
  7. package/dist/form-select.d.ts.map +1 -1
  8. package/dist/form-select.js +6 -5
  9. package/dist/form-select.js.map +1 -1
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/kanban-board.d.ts +110 -0
  15. package/dist/kanban-board.d.ts.map +1 -0
  16. package/dist/kanban-board.js +619 -0
  17. package/dist/kanban-board.js.map +1 -0
  18. package/dist/kanban-card.d.ts +35 -0
  19. package/dist/kanban-card.d.ts.map +1 -0
  20. package/dist/kanban-card.js +163 -0
  21. package/dist/kanban-card.js.map +1 -0
  22. package/dist/kanban-column.d.ts +30 -0
  23. package/dist/kanban-column.d.ts.map +1 -0
  24. package/dist/kanban-column.js +166 -0
  25. package/dist/kanban-column.js.map +1 -0
  26. package/dist/multi-select.d.ts +142 -0
  27. package/dist/multi-select.d.ts.map +1 -0
  28. package/dist/multi-select.js +1304 -0
  29. package/dist/multi-select.js.map +1 -0
  30. package/dist/tokens.css +3 -0
  31. package/dist/tokens.d.ts.map +1 -1
  32. package/dist/tokens.js +2 -0
  33. package/dist/tokens.js.map +1 -1
  34. package/docs/design-language.md +8 -0
  35. package/docs/form-select.md +5 -4
  36. package/docs/kanban-board.md +96 -0
  37. package/docs/kanban-card.md +56 -0
  38. package/docs/kanban-column.md +58 -0
  39. package/docs/multi-select.md +120 -0
  40. package/llms.txt +182 -4
  41. package/package.json +1 -1
@@ -0,0 +1,619 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, css, html, nothing } from "lit";
8
+ import { customElement, property, state } from "lit/decorators.js";
9
+ import { repeat } from "lit/directives/repeat.js";
10
+ import { iconCalendar, iconClock, iconTag } from "./icons.js";
11
+ import { tokens } from "./tokens.js";
12
+ import "./kanban-column.js";
13
+ import "./kanban-card.js";
14
+ import "./popover-panel.js";
15
+ import "./radio-pills.js";
16
+ import "./relative-time.js";
17
+ /** Duration (ms) of the warm "just moved" highlight flashed on a card after it lands. */
18
+ const HIGHLIGHT_MS = 1400;
19
+ /**
20
+ * A configurable kanban board: a horizontally scrolling row of columns, each
21
+ * holding cards. **A card's column is its state** — moving a card to another
22
+ * column (by drag-and-drop, keyboard, or the detail popover's state selector)
23
+ * changes its state, and the board emits a single `card-move` for all three.
24
+ *
25
+ * Data-driven: set the `columns` property to `KanbanColumnData[]`. The board
26
+ * keeps its own working copy and mutates it optimistically on every move, so it
27
+ * works standalone; re-assign `columns` at any time to stay controlled from a
28
+ * store. Cards show only their ticket number and title in the overview; the
29
+ * full detail (description, state, created/updated timestamps) opens in a
30
+ * screen-centered `popover-panel`.
31
+ *
32
+ * Pointer drag-and-drop supports both cross-column moves and within-column
33
+ * reordering with a live drop indicator (set `reorderable` false to keep only
34
+ * cross-column moves when intra-column order isn't persisted). Keyboard parity:
35
+ * focus a card and press Space to pick it up, arrow keys to move it (left/right
36
+ * across columns, up/down within a column), Space to drop, or Escape to cancel;
37
+ * Enter opens the detail. Moves are announced in a polite live region, and the
38
+ * moved card briefly flashes a warm highlight so you can see where it landed.
39
+ *
40
+ * Set `manual` for a server-authoritative board (API + WebSocket/SSE): every
41
+ * move still emits `card-move`, but the board does NOT apply it locally, so it
42
+ * reflects only what you assign to `columns` — e.g. the change echoed back over
43
+ * the socket. The default is optimistic local updates.
44
+ *
45
+ * @element kanban-board
46
+ * @fires card-open - A card's detail view was opened; detail: { cardId }.
47
+ * @fires card-move - A card changed column/position; detail: { cardId, fromColumnId, toColumnId, toIndex }.
48
+ */
49
+ let KanbanBoard = class KanbanBoard extends LitElement {
50
+ constructor() {
51
+ super(...arguments);
52
+ /** Columns (with their cards) to render, in display order. */
53
+ this.columns = [];
54
+ /** Accessible label for the board's group role. */
55
+ this.label = "Board";
56
+ /**
57
+ * Server-authoritative mode. When true, moves emit `card-move` but are not
58
+ * applied to the board locally; it reflects only what you assign to
59
+ * `columns` (e.g. echoed back over WebSocket/SSE), keeping the server as the
60
+ * single source of truth. Defaults to optimistic local updates.
61
+ */
62
+ this.manual = false;
63
+ /**
64
+ * Whether cards can be reordered within a column. Defaults to true. Set false
65
+ * when intra-column order isn't persisted (no server `rank`): drag and
66
+ * keyboard still move cards *between* columns (appended to the target), but
67
+ * reordering inside a column is disabled, so the UI only offers what sticks.
68
+ */
69
+ this.reorderable = true;
70
+ this._cols = [];
71
+ this._openCardId = null;
72
+ this._drag = null;
73
+ this._pointer = null;
74
+ this._grab = null;
75
+ this._liveMessage = "";
76
+ this._highlighted = new Set();
77
+ this.#refocus = null;
78
+ this.#pendingHighlight = null;
79
+ this.#highlightTimers = new Map();
80
+ }
81
+ #refocus;
82
+ #pendingHighlight;
83
+ #highlightTimers;
84
+ static { this.styles = [
85
+ tokens,
86
+ css `
87
+ :host {
88
+ display: block;
89
+ font-family: var(
90
+ --ui-font,
91
+ ui-sans-serif,
92
+ system-ui,
93
+ sans-serif,
94
+ "Apple Color Emoji",
95
+ "Segoe UI Emoji",
96
+ "Segoe UI Symbol",
97
+ "Noto Color Emoji"
98
+ );
99
+ }
100
+ .board {
101
+ display: flex;
102
+ align-items: flex-start;
103
+ gap: 1rem;
104
+ overflow-x: auto;
105
+ padding-bottom: 0.25rem;
106
+ }
107
+ .drop-indicator {
108
+ height: 0.125rem;
109
+ background: var(--ui-primary, #4f46e5);
110
+ border-radius: 999px;
111
+ }
112
+ .detail {
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 0.75rem;
116
+ padding: 0.75rem;
117
+ }
118
+ .detail-desc {
119
+ margin: 0;
120
+ font-size: var(--ui-font-size, 0.875rem);
121
+ line-height: var(--ui-line-height-normal, 1.5);
122
+ color: var(--ui-text, #0f172a);
123
+ }
124
+ .detail-muted {
125
+ color: var(--ui-text-muted, #64748b);
126
+ }
127
+ .detail-field {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 0.25rem;
131
+ }
132
+ .detail-label {
133
+ font-size: var(--ui-font-size-sm, 0.75rem);
134
+ font-weight: var(--ui-font-weight-medium, 500);
135
+ color: var(--ui-text-muted, #64748b);
136
+ }
137
+ .detail-meta {
138
+ margin: 0;
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 0.25rem;
142
+ }
143
+ .detail-meta-row {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: space-between;
147
+ gap: 0.5rem;
148
+ font-size: var(--ui-font-size-sm, 0.75rem);
149
+ }
150
+ .detail-meta-row dt {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ gap: 0.25rem;
154
+ color: var(--ui-text-muted, #64748b);
155
+ font-weight: var(--ui-font-weight-medium, 500);
156
+ }
157
+ .detail-meta-row dt svg {
158
+ display: inline-flex;
159
+ }
160
+ .detail-meta-row dd {
161
+ margin: 0;
162
+ color: var(--ui-text, #0f172a);
163
+ }
164
+ .sr-only {
165
+ position: absolute;
166
+ width: 1px;
167
+ height: 1px;
168
+ padding: 0;
169
+ margin: -1px;
170
+ overflow: hidden;
171
+ clip: rect(0 0 0 0);
172
+ clip-path: inset(50%);
173
+ white-space: nowrap;
174
+ border: 0;
175
+ }
176
+ `,
177
+ ]; }
178
+ willUpdate(changed) {
179
+ if (changed.has("columns")) {
180
+ this._cols = this.columns.map((column) => ({
181
+ id: column.id,
182
+ title: column.title,
183
+ cards: column.cards.map((card) => ({ ...card })),
184
+ }));
185
+ // In manual mode the highlight is deferred until the moved card actually
186
+ // arrives via a `columns` reassignment (e.g. the socket echo).
187
+ if (this.#pendingHighlight && this.#findCard(this.#pendingHighlight)) {
188
+ this.#highlight(this.#pendingHighlight);
189
+ }
190
+ this.#pendingHighlight = null;
191
+ }
192
+ }
193
+ updated() {
194
+ if (this.#refocus) {
195
+ const card = this.renderRoot.querySelector(`kanban-card[data-card-id="${this.#refocus}"]`);
196
+ card?.focus();
197
+ this.#refocus = null;
198
+ }
199
+ }
200
+ disconnectedCallback() {
201
+ super.disconnectedCallback();
202
+ this._grab = null;
203
+ this._drag = null;
204
+ this._pointer = null;
205
+ for (const timer of this.#highlightTimers.values())
206
+ clearTimeout(timer);
207
+ this.#highlightTimers.clear();
208
+ }
209
+ /** Returns the card, its column, and index for a card id, or null if unknown. */
210
+ #findCard(cardId) {
211
+ for (const column of this._cols) {
212
+ const index = column.cards.findIndex((card) => card.id === cardId);
213
+ if (index >= 0)
214
+ return { card: column.cards[index], column, index };
215
+ }
216
+ return null;
217
+ }
218
+ /** Number of cards currently in a column (0 if unknown). */
219
+ #columnCount(columnId) {
220
+ return this._cols.find((column) => column.id === columnId)?.cards.length ?? 0;
221
+ }
222
+ /** Flashes the warm "just moved" highlight on a card, clearing it after HIGHLIGHT_MS. */
223
+ #highlight(cardId) {
224
+ const existing = this.#highlightTimers.get(cardId);
225
+ if (existing)
226
+ clearTimeout(existing);
227
+ const next = new Set(this._highlighted);
228
+ next.add(cardId);
229
+ this._highlighted = next;
230
+ this.#highlightTimers.set(cardId, setTimeout(() => {
231
+ const remaining = new Set(this._highlighted);
232
+ remaining.delete(cardId);
233
+ this._highlighted = remaining;
234
+ this.#highlightTimers.delete(cardId);
235
+ }, HIGHLIGHT_MS));
236
+ }
237
+ /** Applies a move to the working copy and emits `card-move`. `toIndex` is the destination index after removal. */
238
+ #move(cardId, fromColumnId, toColumnId, toIndex) {
239
+ const cols = this._cols.map((column) => ({ ...column, cards: [...column.cards] }));
240
+ const from = cols.find((column) => column.id === fromColumnId);
241
+ const to = cols.find((column) => column.id === toColumnId);
242
+ if (!from || !to)
243
+ return;
244
+ const index = from.cards.findIndex((card) => card.id === cardId);
245
+ if (index < 0)
246
+ return;
247
+ const [card] = from.cards.splice(index, 1);
248
+ const target = Math.max(0, Math.min(toIndex, to.cards.length));
249
+ to.cards.splice(target, 0, card);
250
+ if (!this.manual)
251
+ this._cols = cols;
252
+ this.dispatchEvent(new CustomEvent("card-move", {
253
+ detail: { cardId, fromColumnId, toColumnId, toIndex: target },
254
+ bubbles: true,
255
+ composed: true,
256
+ }));
257
+ // Optimistic: highlight now (the card is already at its new position).
258
+ // Manual: defer until the move is echoed back via `columns`.
259
+ if (this.manual)
260
+ this.#pendingHighlight = cardId;
261
+ else
262
+ this.#highlight(cardId);
263
+ }
264
+ /** Converts a DOM insertion index (which counts the moving card) into a destination index after removal. */
265
+ #toDestIndex(fromColumnId, originIndex, toColumnId, domIndex) {
266
+ return fromColumnId === toColumnId && domIndex > originIndex ? domIndex - 1 : domIndex;
267
+ }
268
+ /** Computes the DOM insertion index within a column from the pointer's vertical position. */
269
+ #pointerIndex(columnId, clientY) {
270
+ const cards = [
271
+ ...this.renderRoot.querySelectorAll(`kanban-card[data-column-id="${columnId}"]`),
272
+ ];
273
+ for (let i = 0; i < cards.length; i++) {
274
+ const rect = cards[i].getBoundingClientRect();
275
+ if (clientY < rect.top + rect.height / 2)
276
+ return i;
277
+ }
278
+ return cards.length;
279
+ }
280
+ #onDragStart(event, columnId, cardId) {
281
+ const column = this._cols.find((entry) => entry.id === columnId);
282
+ const originIndex = column ? column.cards.findIndex((card) => card.id === cardId) : 0;
283
+ this._grab = null;
284
+ this._drag = { cardId, fromColumnId: columnId, originIndex };
285
+ this._pointer = { columnId, index: originIndex };
286
+ if (event.dataTransfer) {
287
+ event.dataTransfer.effectAllowed = "move";
288
+ try {
289
+ event.dataTransfer.setData("text/plain", cardId);
290
+ }
291
+ catch {
292
+ /* some browsers disallow setData outside trusted events */
293
+ }
294
+ }
295
+ }
296
+ #onDragOver(event, columnId) {
297
+ if (!this._drag)
298
+ return;
299
+ event.preventDefault();
300
+ if (event.dataTransfer)
301
+ event.dataTransfer.dropEffect = "move";
302
+ const index = this.#pointerIndex(columnId, event.clientY);
303
+ if (this._pointer?.columnId !== columnId || this._pointer.index !== index) {
304
+ this._pointer = { columnId, index };
305
+ }
306
+ }
307
+ #onDrop(event, columnId) {
308
+ if (!this._drag)
309
+ return;
310
+ event.preventDefault();
311
+ const { cardId, fromColumnId, originIndex } = this._drag;
312
+ this._drag = null;
313
+ this._pointer = null;
314
+ if (!this.reorderable) {
315
+ // Intra-column reordering is off: ignore same-column drops, and append
316
+ // cross-column moves to the end of the destination.
317
+ if (columnId === fromColumnId)
318
+ return;
319
+ this.#move(cardId, fromColumnId, columnId, this.#columnCount(columnId));
320
+ return;
321
+ }
322
+ const domIndex = this.#pointerIndex(columnId, event.clientY);
323
+ const dest = this.#toDestIndex(fromColumnId, originIndex, columnId, domIndex);
324
+ this.#move(cardId, fromColumnId, columnId, dest);
325
+ }
326
+ #onDragEnd() {
327
+ this._drag = null;
328
+ this._pointer = null;
329
+ }
330
+ #onCardKeydown(event, columnId, cardId) {
331
+ const grabbing = this._grab?.cardId === cardId;
332
+ if (event.key === "Enter") {
333
+ if (grabbing)
334
+ return;
335
+ event.preventDefault();
336
+ this.#openDetail(cardId);
337
+ return;
338
+ }
339
+ if (event.key === " " || event.key === "Spacebar") {
340
+ event.preventDefault();
341
+ if (grabbing)
342
+ this.#commitGrab();
343
+ else
344
+ this.#startGrab(columnId, cardId);
345
+ return;
346
+ }
347
+ if (!grabbing)
348
+ return;
349
+ switch (event.key) {
350
+ case "Escape":
351
+ event.preventDefault();
352
+ this.#cancelGrab();
353
+ break;
354
+ case "ArrowUp":
355
+ event.preventDefault();
356
+ this.#grabMove(0, -1);
357
+ break;
358
+ case "ArrowDown":
359
+ event.preventDefault();
360
+ this.#grabMove(0, 1);
361
+ break;
362
+ case "ArrowLeft":
363
+ event.preventDefault();
364
+ this.#grabMove(-1, 0);
365
+ break;
366
+ case "ArrowRight":
367
+ event.preventDefault();
368
+ this.#grabMove(1, 0);
369
+ break;
370
+ default:
371
+ break;
372
+ }
373
+ }
374
+ #startGrab(columnId, cardId) {
375
+ const column = this._cols.find((entry) => entry.id === columnId);
376
+ const index = column ? column.cards.findIndex((card) => card.id === cardId) : 0;
377
+ this._grab = { cardId, fromColumnId: columnId, originIndex: index, columnId, index };
378
+ this.#refocus = cardId;
379
+ const ticket = column?.cards[index]?.ticket ?? "card";
380
+ this.#announce(`Picked up ${ticket}. Use the arrow keys to move it, space to drop, or escape to cancel.`);
381
+ }
382
+ #grabMove(columnDelta, indexDelta) {
383
+ const grab = this._grab;
384
+ if (!grab)
385
+ return;
386
+ let columnIndex = this._cols.findIndex((column) => column.id === grab.columnId);
387
+ if (columnIndex < 0)
388
+ return;
389
+ let index = grab.index;
390
+ if (columnDelta !== 0) {
391
+ const next = columnIndex + columnDelta;
392
+ if (next < 0 || next >= this._cols.length)
393
+ return;
394
+ columnIndex = next;
395
+ // With reordering off, a cross-column move lands at the end.
396
+ index = this.reorderable
397
+ ? Math.min(index, this._cols[columnIndex].cards.length)
398
+ : this._cols[columnIndex].cards.length;
399
+ }
400
+ else {
401
+ if (!this.reorderable)
402
+ return; // no intra-column reordering
403
+ const max = this._cols[columnIndex].cards.length;
404
+ index = Math.max(0, Math.min(max, index + indexDelta));
405
+ }
406
+ const column = this._cols[columnIndex];
407
+ this._grab = { ...grab, columnId: column.id, index };
408
+ this.#refocus = grab.cardId;
409
+ const ticket = this.#findCard(grab.cardId)?.card.ticket ?? "Card";
410
+ this.#announce(`${ticket} moved to ${column.title}, position ${index + 1}.`);
411
+ }
412
+ #commitGrab() {
413
+ const grab = this._grab;
414
+ if (!grab)
415
+ return;
416
+ if (!this.reorderable && grab.columnId === grab.fromColumnId) {
417
+ // Nothing to persist for a same-column drop when reordering is off.
418
+ this._grab = null;
419
+ this.#refocus = grab.cardId;
420
+ this.#announce("Returned to its position.");
421
+ return;
422
+ }
423
+ const dest = this.#toDestIndex(grab.fromColumnId, grab.originIndex, grab.columnId, grab.index);
424
+ const column = this._cols.find((entry) => entry.id === grab.columnId);
425
+ const ticket = this.#findCard(grab.cardId)?.card.ticket ?? "Card";
426
+ this._grab = null;
427
+ this.#move(grab.cardId, grab.fromColumnId, grab.columnId, dest);
428
+ this.#refocus = grab.cardId;
429
+ this.#announce(`${ticket} dropped in ${column?.title ?? ""}.`);
430
+ }
431
+ #cancelGrab() {
432
+ const grab = this._grab;
433
+ this._grab = null;
434
+ this.#refocus = grab?.cardId ?? null;
435
+ this.#announce("Move cancelled.");
436
+ }
437
+ #announce(message) {
438
+ this._liveMessage = message;
439
+ }
440
+ #openDetail(cardId) {
441
+ if (this._grab)
442
+ return;
443
+ this._openCardId = cardId;
444
+ this.dispatchEvent(new CustomEvent("card-open", {
445
+ detail: { cardId },
446
+ bubbles: true,
447
+ composed: true,
448
+ }));
449
+ }
450
+ #closeDetail() {
451
+ this._openCardId = null;
452
+ }
453
+ #onStateChange(cardId, fromColumnId, toColumnId) {
454
+ if (!toColumnId || toColumnId === fromColumnId)
455
+ return;
456
+ const dest = this._cols.find((column) => column.id === toColumnId);
457
+ this.#move(cardId, fromColumnId, toColumnId, dest ? dest.cards.length : 0);
458
+ }
459
+ #renderColumn(column, indicator, dragCardId) {
460
+ let indicatorIndex = -1;
461
+ if (indicator?.columnId === column.id) {
462
+ if (this.reorderable) {
463
+ indicatorIndex = indicator.index;
464
+ }
465
+ else {
466
+ // Reordering off: only show an append indicator on a *different* column.
467
+ const sourceColumnId = this._grab?.fromColumnId ?? this._drag?.fromColumnId ?? null;
468
+ if (sourceColumnId !== null && sourceColumnId !== column.id) {
469
+ indicatorIndex = column.cards.length;
470
+ }
471
+ }
472
+ }
473
+ return html `
474
+ <kanban-column
475
+ .heading=${column.title}
476
+ .count=${column.cards.length}
477
+ .empty=${column.cards.length === 0}
478
+ .dragover=${this._pointer?.columnId === column.id}
479
+ data-column-id=${column.id}
480
+ @dragenter=${(event) => this.#onDragOver(event, column.id)}
481
+ @dragover=${(event) => this.#onDragOver(event, column.id)}
482
+ @drop=${(event) => this.#onDrop(event, column.id)}
483
+ >
484
+ ${repeat(column.cards, (card) => card.id, (card, i) => html `
485
+ ${indicatorIndex === i
486
+ ? html `<div class="drop-indicator" aria-hidden="true"></div>`
487
+ : nothing}
488
+ <kanban-card
489
+ .ticket=${card.ticket}
490
+ .heading=${card.title}
491
+ data-card-id=${card.id}
492
+ data-column-id=${column.id}
493
+ draggable="true"
494
+ ?dragging=${dragCardId === card.id && this._drag !== null}
495
+ ?grabbed=${this._grab?.cardId === card.id}
496
+ ?just-moved=${this._highlighted.has(card.id)}
497
+ @dragstart=${(event) => this.#onDragStart(event, column.id, card.id)}
498
+ @click=${() => this.#openDetail(card.id)}
499
+ @keydown=${(event) => this.#onCardKeydown(event, column.id, card.id)}
500
+ ></kanban-card>
501
+ `)}
502
+ ${indicatorIndex === column.cards.length
503
+ ? html `<div class="drop-indicator" aria-hidden="true"></div>`
504
+ : nothing}
505
+ </kanban-column>
506
+ `;
507
+ }
508
+ #renderDetail() {
509
+ const open = this._openCardId ? this.#findCard(this._openCardId) : null;
510
+ return html `
511
+ <popover-panel
512
+ centered
513
+ ?open=${open !== null}
514
+ heading=${open?.card.title ?? ""}
515
+ @panel-close=${() => this.#closeDetail()}
516
+ >
517
+ ${open
518
+ ? html `
519
+ <div class="detail">
520
+ ${open.card.description
521
+ ? html `<p class="detail-desc">${open.card.description}</p>`
522
+ : html `<p class="detail-desc detail-muted">No description.</p>`}
523
+ <div class="detail-field">
524
+ <span class="detail-label" id="kb-state-label">State</span>
525
+ <radio-pills
526
+ aria-labelledby="kb-state-label"
527
+ .options=${this._cols.map((column) => ({
528
+ value: column.id,
529
+ label: column.title,
530
+ }))}
531
+ .value=${open.column.id}
532
+ @change=${(event) => this.#onStateChange(open.card.id, open.column.id, event.detail.value)}
533
+ ></radio-pills>
534
+ </div>
535
+ <dl class="detail-meta">
536
+ <div class="detail-meta-row">
537
+ <dt><span aria-hidden="true">${iconTag(14)}</span> Ticket</dt>
538
+ <dd>${open.card.ticket}</dd>
539
+ </div>
540
+ ${open.card.createdAt
541
+ ? html `
542
+ <div class="detail-meta-row">
543
+ <dt><span aria-hidden="true">${iconCalendar(14)}</span> Created</dt>
544
+ <dd><relative-time datetime=${open.card.createdAt}></relative-time></dd>
545
+ </div>
546
+ `
547
+ : nothing}
548
+ ${open.card.updatedAt
549
+ ? html `
550
+ <div class="detail-meta-row">
551
+ <dt><span aria-hidden="true">${iconClock(14)}</span> Updated</dt>
552
+ <dd><relative-time datetime=${open.card.updatedAt}></relative-time></dd>
553
+ </div>
554
+ `
555
+ : nothing}
556
+ </dl>
557
+ </div>
558
+ `
559
+ : nothing}
560
+ </popover-panel>
561
+ `;
562
+ }
563
+ render() {
564
+ const indicator = this._grab
565
+ ? { columnId: this._grab.columnId, index: this._grab.index }
566
+ : this._pointer;
567
+ const dragCardId = this._grab?.cardId ?? this._drag?.cardId ?? null;
568
+ return html `
569
+ <div
570
+ class="board"
571
+ role="group"
572
+ aria-label=${this.label}
573
+ @dragend=${() => this.#onDragEnd()}
574
+ >
575
+ ${repeat(this._cols, (column) => column.id, (column) => this.#renderColumn(column, indicator, dragCardId))}
576
+ </div>
577
+ <div class="sr-only" role="status" aria-live="polite">${this._liveMessage}</div>
578
+ ${this.#renderDetail()}
579
+ `;
580
+ }
581
+ };
582
+ __decorate([
583
+ property({ attribute: false })
584
+ ], KanbanBoard.prototype, "columns", void 0);
585
+ __decorate([
586
+ property()
587
+ ], KanbanBoard.prototype, "label", void 0);
588
+ __decorate([
589
+ property({ type: Boolean, reflect: true })
590
+ ], KanbanBoard.prototype, "manual", void 0);
591
+ __decorate([
592
+ property({ type: Boolean, reflect: true })
593
+ ], KanbanBoard.prototype, "reorderable", void 0);
594
+ __decorate([
595
+ state()
596
+ ], KanbanBoard.prototype, "_cols", void 0);
597
+ __decorate([
598
+ state()
599
+ ], KanbanBoard.prototype, "_openCardId", void 0);
600
+ __decorate([
601
+ state()
602
+ ], KanbanBoard.prototype, "_drag", void 0);
603
+ __decorate([
604
+ state()
605
+ ], KanbanBoard.prototype, "_pointer", void 0);
606
+ __decorate([
607
+ state()
608
+ ], KanbanBoard.prototype, "_grab", void 0);
609
+ __decorate([
610
+ state()
611
+ ], KanbanBoard.prototype, "_liveMessage", void 0);
612
+ __decorate([
613
+ state()
614
+ ], KanbanBoard.prototype, "_highlighted", void 0);
615
+ KanbanBoard = __decorate([
616
+ customElement("kanban-board")
617
+ ], KanbanBoard);
618
+ export { KanbanBoard };
619
+ //# sourceMappingURL=kanban-board.js.map