@design.estate/dees-catalog 12.4.0 → 12.5.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 (67) hide show
  1. package/dist_bundle/bundle.js +2455 -1133
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_ts_web/00_commitinfo_data.js +3 -3
  4. package/dist_ts_web/elements/00group-input/dees-input-dropdown/dees-input-dropdown-popup.d.ts +13 -0
  5. package/dist_ts_web/elements/00group-input/dees-input-dropdown/dees-input-dropdown-popup.js +76 -7
  6. package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.d.ts +42 -0
  7. package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.demo.d.ts +1 -0
  8. package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.demo.js +8 -0
  9. package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.js +347 -0
  10. package/dist_ts_web/elements/00group-input/dees-input-reference/document.d.ts +5 -0
  11. package/dist_ts_web/elements/00group-input/dees-input-reference/document.js +45 -0
  12. package/dist_ts_web/elements/00group-input/dees-input-reference/index.d.ts +2 -0
  13. package/dist_ts_web/elements/00group-input/dees-input-reference/index.js +3 -0
  14. package/dist_ts_web/elements/00group-input/dees-input-reference/interfaces.d.ts +16 -0
  15. package/dist_ts_web/elements/00group-input/dees-input-reference/interfaces.js +2 -0
  16. package/dist_ts_web/elements/00group-input/index.d.ts +1 -0
  17. package/dist_ts_web/elements/00group-input/index.js +2 -1
  18. package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.d.ts +58 -0
  19. package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.demo.d.ts +1 -0
  20. package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.demo.js +9 -0
  21. package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.js +536 -0
  22. package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.d.ts +20 -0
  23. package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.js +157 -0
  24. package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.d.ts +23 -0
  25. package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.js +192 -0
  26. package/dist_ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.d.ts +24 -0
  27. package/dist_ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.js +211 -0
  28. package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.d.ts +35 -0
  29. package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.js +269 -0
  30. package/dist_ts_web/elements/00group-kanban/index.d.ts +6 -0
  31. package/dist_ts_web/elements/00group-kanban/index.js +7 -0
  32. package/dist_ts_web/elements/00group-kanban/interfaces.d.ts +84 -0
  33. package/dist_ts_web/elements/00group-kanban/interfaces.js +2 -0
  34. package/dist_ts_web/elements/00group-kanban/kanban.demodata.d.ts +7 -0
  35. package/dist_ts_web/elements/00group-kanban/kanban.demodata.js +60 -0
  36. package/dist_ts_web/elements/00group-kanban/kanban.helpers.d.ts +6 -0
  37. package/dist_ts_web/elements/00group-kanban/kanban.helpers.js +11 -0
  38. package/dist_ts_web/elements/00group-kanban/kanban.styles.d.ts +1 -0
  39. package/dist_ts_web/elements/00group-kanban/kanban.styles.js +18 -0
  40. package/dist_ts_web/elements/index.d.ts +1 -0
  41. package/dist_ts_web/elements/index.js +2 -1
  42. package/dist_ts_web/services/DeesServiceLibLoader.d.ts +1 -0
  43. package/dist_ts_web/services/DeesServiceLibLoader.js +2 -1
  44. package/package.json +1 -1
  45. package/readme.md +154 -0
  46. package/scripts/check-packed-consumer.mjs +1 -1
  47. package/ts_web/00_commitinfo_data.ts +3 -3
  48. package/ts_web/elements/00group-input/dees-input-dropdown/dees-input-dropdown-popup.ts +40 -5
  49. package/ts_web/elements/00group-input/dees-input-reference/dees-input-reference.demo.ts +9 -0
  50. package/ts_web/elements/00group-input/dees-input-reference/dees-input-reference.ts +221 -0
  51. package/ts_web/elements/00group-input/dees-input-reference/document.ts +38 -0
  52. package/ts_web/elements/00group-input/dees-input-reference/index.ts +2 -0
  53. package/ts_web/elements/00group-input/dees-input-reference/interfaces.ts +8 -0
  54. package/ts_web/elements/00group-input/index.ts +1 -0
  55. package/ts_web/elements/00group-kanban/dees-kanban/dees-kanban.demo.ts +9 -0
  56. package/ts_web/elements/00group-kanban/dees-kanban/dees-kanban.ts +315 -0
  57. package/ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.ts +58 -0
  58. package/ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.ts +60 -0
  59. package/ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.ts +81 -0
  60. package/ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.ts +114 -0
  61. package/ts_web/elements/00group-kanban/index.ts +6 -0
  62. package/ts_web/elements/00group-kanban/interfaces.ts +67 -0
  63. package/ts_web/elements/00group-kanban/kanban.demodata.ts +56 -0
  64. package/ts_web/elements/00group-kanban/kanban.helpers.ts +13 -0
  65. package/ts_web/elements/00group-kanban/kanban.styles.ts +18 -0
  66. package/ts_web/elements/index.ts +1 -0
  67. package/ts_web/services/DeesServiceLibLoader.ts +2 -0
@@ -0,0 +1,114 @@
1
+ import { DeesElement, customElement, property, state, html, css } from '@design.estate/dees-element';
2
+ import type { IKanbanColumn, IKanbanComment, IKanbanPerson, IKanbanProvider, IKanbanTicket, IKanbanTicketDraft, TKanbanPriority } from '../interfaces.js';
3
+ import type { TReferenceContent, TReferenceSearch } from '../../00group-input/dees-input-reference/interfaces.js';
4
+ import { kanbanStyles } from '../kanban.styles.js';
5
+ import { kanbanEvent, sortKanbanTickets, ticketDraft, ticketLabel } from '../kanban.helpers.js';
6
+ import type { DeesInputText } from '../../00group-input/dees-input-text/dees-input-text.js';
7
+ import type { DeesInputTextarea } from '../../00group-input/dees-input-textarea/dees-input-textarea.js';
8
+ import '../../00group-input/dees-input-text/dees-input-text.js';
9
+ import '../../00group-input/dees-input-textarea/dees-input-textarea.js';
10
+ import '../../00group-input/dees-input-dropdown/dees-input-dropdown.js';
11
+ import '../dees-kanban-discussion/dees-kanban-discussion.js';
12
+
13
+ declare global { interface HTMLElementTagNameMap { 'dees-kanban-ticket-detail': DeesKanbanTicketDetail; } }
14
+
15
+ @customElement('dees-kanban-ticket-detail')
16
+ export class DeesKanbanTicketDetail extends DeesElement {
17
+ @property({ attribute: false }) accessor ticket: IKanbanTicket | undefined;
18
+ @property({ attribute: false }) accessor tickets: IKanbanTicket[] = [];
19
+ @property({ attribute: false }) accessor columns: IKanbanColumn[] = [];
20
+ @property({ attribute: false }) accessor people: IKanbanPerson[] = [];
21
+ @property({ attribute: false }) accessor provider: IKanbanProvider = {};
22
+ @property({ attribute: false }) accessor draft: IKanbanTicketDraft | undefined;
23
+ @property({ attribute: false }) accessor commentDraft: TReferenceContent = [];
24
+ @property({ attribute: false }) accessor comments: IKanbanComment[] = [];
25
+ @property({ attribute: false }) accessor searchTickets: TReferenceSearch | undefined;
26
+ @property({ type: Boolean }) accessor loadingComments = false;
27
+ @property({ type: String }) accessor commentsError = '';
28
+ @property({ type: String }) accessor error = '';
29
+ @property({ type: Boolean }) accessor saving = false;
30
+ @property({ type: Boolean }) accessor canGoBack = false;
31
+ @state() private accessor editing = false;
32
+ public static styles = [...kanbanStyles, css`
33
+ .layout { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: 28px; }
34
+ .main { min-width: 0; }
35
+ .toolbar { margin-bottom: 16px; }
36
+ h2 { font-size: var(--dees-font-title2-size); line-height: 1.3; font-weight: 600; margin-bottom: 18px; overflow-wrap: anywhere; }
37
+ .description { white-space: pre-wrap; line-height: 1.65; overflow-wrap: anywhere; margin-bottom: 24px; }
38
+ aside { border-left: 1px solid var(--dees-color-border-subtle); padding-left: 22px; }
39
+ .fields { display: grid; gap: 16px; }
40
+ .fields label { display: block; margin-bottom: 6px; }
41
+ h3 { font-size: var(--dees-font-body-size); margin-bottom: 12px; }
42
+ .children { margin-bottom: 24px; }
43
+ .child { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--dees-color-border-subtle); }
44
+ .discussion { border-top: 1px solid var(--dees-color-border-subtle); padding-top: 22px; }
45
+ .edit { display: grid; gap: 14px; margin-bottom: 22px; }
46
+ @media (max-width: 700px) { .layout { grid-template-columns: minmax(0, 1fr); gap: 24px; } aside { grid-row: 1; border-left: 0; padding-left: 0; } .fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
47
+ `];
48
+ public updated(changed: Map<string, unknown>): void {
49
+ const previous = changed.get('ticket') as IKanbanTicket | undefined;
50
+ if (changed.has('ticket') && previous?.id !== this.ticket?.id) this.editing = Boolean(this.draft);
51
+ }
52
+ private changeDraft(change: Partial<IKanbanTicketDraft>): void {
53
+ this.draft = { ...(this.draft ?? ticketDraft(this.ticket!)), ...change };
54
+ kanbanEvent(this, 'kanban-ticket-draft', { ticketId: this.ticket!.id, draft: this.draft });
55
+ }
56
+ private addComment = (input: Parameters<NonNullable<IKanbanProvider['addComment']>>[0]) => this.provider.addComment!(input);
57
+ public render() {
58
+ const ticket = this.ticket;
59
+ if (!ticket) return html``;
60
+ const draft = this.draft ?? ticketDraft(ticket);
61
+ const parent = this.tickets.find(item => item.id === ticket.parentId);
62
+ const children = sortKanbanTickets(this.tickets.filter(item => item.parentId === ticket.id));
63
+ const editable = Boolean(this.provider.updateTicket) && ticket.permissions?.edit !== false;
64
+ const movable = Boolean(this.provider.moveTicket) && ticket.permissions?.move !== false;
65
+ const parents = this.tickets.filter(item => item.kind === (ticket.kind === 'subtask' ? 'task' : 'epic'));
66
+ return html`<div class="toolbar row">
67
+ ${this.canGoBack ? html`<dees-button size="sm" type="ghost" icon="lucide:ArrowLeft" text="Back" @clicked=${() => kanbanEvent(this, 'kanban-detail-back', {})}></dees-button>` : ''}
68
+ <span class="badge">${ticket.kind}</span><span class="caption">${ticket.key}</span>
69
+ ${parent ? html`<span class="caption">/</span><button class="link caption" @click=${() => kanbanEvent(this, 'kanban-ticket-open', { ticketId: parent.id })}>${parent.title}</button>` : ''}
70
+ <span class="spacer"></span>${editable && !this.editing ? html`<dees-button size="sm" type="ghost" text="Edit ticket" icon="lucide:Pencil" @clicked=${() => { this.editing = true; this.changeDraft({}); }}></dees-button>` : ''}
71
+ </div>
72
+ ${this.error ? html`<p class="error" role="alert">${this.error}</p>` : ''}
73
+ <div class="layout"><div class="main">
74
+ ${this.editing && editable ? html`<div class="edit">
75
+ <dees-input-text label="Title" .value=${draft.title} .disabled=${this.saving}
76
+ @input=${(event: Event) => this.changeDraft({ title: String((event.currentTarget as DeesInputText).value ?? '') })}></dees-input-text>
77
+ <dees-input-textarea label="Description" .value=${draft.description} .rows=${5} .disabled=${this.saving}
78
+ @input=${(event: Event) => this.changeDraft({ description: (event.currentTarget as DeesInputTextarea).value })}></dees-input-textarea>
79
+ <div class="row"><dees-button size="sm" type="accent" text="Save changes" .disabled=${this.saving || !draft.title.trim()} .status=${this.saving ? 'pending' : 'normal'}
80
+ @clicked=${() => kanbanEvent(this, 'kanban-ticket-save', { ticketId: ticket.id, draft })}></dees-button>
81
+ <dees-button size="sm" type="ghost" text="Cancel" .disabled=${this.saving} @clicked=${() => {
82
+ this.editing = false; this.draft = undefined; kanbanEvent(this, 'kanban-ticket-draft', { ticketId: ticket.id, draft: undefined });
83
+ }}></dees-button></div>
84
+ </div>` : html`<h2>${ticket.title}</h2><div class="description ${ticket.description ? '' : 'muted'}">${ticket.description || 'No description yet.'}</div>`}
85
+ ${ticket.kind !== 'subtask' ? html`<section class="children"><div class="row"><h3>${ticket.kind === 'epic' ? 'Tasks' : 'Subtasks'}</h3><span class="spacer"></span>
86
+ ${this.provider.createTicket && ticket.permissions?.createChild !== false ? html`<dees-button size="sm" type="ghost" icon="lucide:Plus" text=${ticket.kind === 'epic' ? 'Add task' : 'Add subtask'}
87
+ @clicked=${() => kanbanEvent(this, 'kanban-ticket-create', { kind: ticket.kind === 'epic' ? 'task' : 'subtask', parentId: ticket.id, columnId: this.columns[0]?.id ?? ticket.columnId })}></dees-button>` : ''}</div>
88
+ ${children.map(child => html`<div class="child"><button class="link" @click=${() => kanbanEvent(this, 'kanban-ticket-open', { ticketId: child.id })}>${ticketLabel(child)}</button>
89
+ <span class="badge">${this.columns.find(column => column.id === child.columnId)?.title ?? child.columnId}</span></div>`)}
90
+ ${!children.length ? html`<p class="caption">No ${ticket.kind === 'epic' ? 'tasks' : 'subtasks'} yet.</p>` : ''}</section>` : ''}
91
+ <dees-kanban-discussion class="discussion" .ticketId=${ticket.id} .comments=${this.comments} .draft=${this.commentDraft}
92
+ .loading=${this.loadingComments} .error=${this.commentsError} .readOnly=${ticket.permissions?.comment === false}
93
+ .searchTickets=${this.searchTickets} .addComment=${this.provider.addComment ? this.addComment : undefined}></dees-kanban-discussion>
94
+ </div><aside><div class="fields">
95
+ <dees-input-dropdown label="Status" .options=${this.columns.map(column => ({ key: column.id, option: column.title }))}
96
+ .value=${{ key: ticket.columnId, option: this.columns.find(column => column.id === ticket.columnId)?.title ?? ticket.columnId }}
97
+ .disabled=${!movable || this.saving} @selectedOption=${(event: CustomEvent<{ key: string }>) => kanbanEvent(this, 'kanban-ticket-move', { ticketId: ticket.id, columnId: event.detail.key })}></dees-input-dropdown>
98
+ ${this.editing && editable ? html`
99
+ <dees-input-dropdown label="Priority" .disabled=${this.saving} .value=${{ key: draft.priority, option: draft.priority }}
100
+ .options=${['none', 'low', 'normal', 'high', 'urgent'].map(key => ({ key, option: key }))}
101
+ @selectedOption=${(event: CustomEvent<{ key: TKanbanPriority }>) => this.changeDraft({ priority: event.detail.key })}></dees-input-dropdown>
102
+ <dees-input-dropdown label="Assignee" .disabled=${this.saving} .value=${{ key: draft.assigneeId, option: this.people.find(person => person.id === draft.assigneeId)?.name ?? ticket.assignee?.name ?? 'Unassigned' }}
103
+ .options=${[{ key: '', option: 'Unassigned' }, ...this.people.map(person => ({ key: person.id, option: person.name }))]}
104
+ @selectedOption=${(event: CustomEvent<{ key: string }>) => this.changeDraft({ assigneeId: event.detail.key })}></dees-input-dropdown>
105
+ ${ticket.kind !== 'epic' ? html`<dees-input-dropdown label=${ticket.kind === 'task' ? 'Epic' : 'Parent task'} .disabled=${this.saving}
106
+ .value=${{ key: draft.parentId, option: parents.find(item => item.id === draft.parentId)?.title ?? 'None' }}
107
+ .options=${[...(ticket.kind === 'task' ? [{ key: '', option: 'None' }] : []), ...parents.map(item => ({ key: item.id, option: ticketLabel(item) }))]}
108
+ @selectedOption=${(event: CustomEvent<{ key: string }>) => this.changeDraft({ parentId: event.detail.key })}></dees-input-dropdown>` : ''}
109
+ ` : html`<div><label class="caption">Priority</label>${ticket.priority ?? 'none'}</div><div><label class="caption">Assignee</label>${ticket.assignee?.name ?? 'Unassigned'}</div>`}
110
+ ${ticket.labels?.length ? html`<div><label class="caption">Labels</label><div class="row">${ticket.labels.map(label => html`<span class="badge">${label}</span>`)}</div></div>` : ''}
111
+ </div></aside></div>`;
112
+ }
113
+ public finishEditing(): void { this.editing = false; }
114
+ }
@@ -0,0 +1,6 @@
1
+ export * from './interfaces.js';
2
+ export * from './dees-kanban/dees-kanban.js';
3
+ export * from './dees-kanban-card/dees-kanban-card.js';
4
+ export * from './dees-kanban-column/dees-kanban-column.js';
5
+ export * from './dees-kanban-ticket-detail/dees-kanban-ticket-detail.js';
6
+ export * from './dees-kanban-discussion/dees-kanban-discussion.js';
@@ -0,0 +1,67 @@
1
+ import type { TReferenceContent } from '../00group-input/dees-input-reference/interfaces.js';
2
+
3
+ export type TKanbanTicketKind = 'epic' | 'task' | 'subtask';
4
+ export type TKanbanPriority = 'none' | 'low' | 'normal' | 'high' | 'urgent';
5
+ export interface IKanbanPerson { id: string; name: string; }
6
+ export interface IKanbanColumn { id: string; title: string; /** Marks completed subtasks. */ done?: boolean; }
7
+ export interface IKanbanTicket {
8
+ id: string;
9
+ key: string;
10
+ kind: TKanbanTicketKind;
11
+ title: string;
12
+ description?: string;
13
+ /** Task -> epic, subtask -> task. Epics have no parent. */
14
+ parentId?: string;
15
+ columnId: string;
16
+ order: number;
17
+ priority?: TKanbanPriority;
18
+ assignee?: IKanbanPerson;
19
+ labels?: string[];
20
+ /** UI capabilities; the provider must enforce authorization independently. */
21
+ permissions?: { edit?: boolean; move?: boolean; comment?: boolean; createChild?: boolean };
22
+ }
23
+ export interface IKanbanComment {
24
+ id: string;
25
+ ticketId: string;
26
+ author: IKanbanPerson;
27
+ /** ISO 8601 timestamp. */
28
+ createdAt: string;
29
+ body: TReferenceContent;
30
+ }
31
+ export interface IKanbanTicketChanges {
32
+ title?: string;
33
+ description?: string;
34
+ priority?: TKanbanPriority;
35
+ assignee?: IKanbanPerson | null;
36
+ parentId?: string | null;
37
+ }
38
+ export interface IKanbanTicketDraft {
39
+ title: string;
40
+ description: string;
41
+ priority: TKanbanPriority;
42
+ assigneeId: string;
43
+ parentId: string;
44
+ }
45
+ export interface IKanbanMove {
46
+ ticketId: string;
47
+ columnId: string;
48
+ /** Omitted means append. Provider assigns ordering atomically with the move. */
49
+ beforeTicketId?: string;
50
+ }
51
+ export interface IKanbanCreate {
52
+ kind: TKanbanTicketKind;
53
+ title: string;
54
+ columnId: string;
55
+ parentId?: string;
56
+ }
57
+ /** Missing mutation callbacks make the corresponding controls read-only. AbortSignals cancel obsolete reads. */
58
+ export interface IKanbanProvider {
59
+ getTicket?: (id: string, signal: AbortSignal) => Promise<IKanbanTicket>;
60
+ searchTickets?: (query: string, signal: AbortSignal) => Promise<IKanbanTicket[]>;
61
+ loadComments?: (ticketId: string, signal: AbortSignal) => Promise<IKanbanComment[]>;
62
+ addComment?: (input: { ticketId: string; body: TReferenceContent }) => Promise<IKanbanComment>;
63
+ updateTicket?: (input: { ticketId: string; changes: IKanbanTicketChanges }) => Promise<IKanbanTicket>;
64
+ /** Return all records whose order or column changed. */
65
+ moveTicket?: (input: IKanbanMove) => Promise<IKanbanTicket[]>;
66
+ createTicket?: (input: IKanbanCreate) => Promise<IKanbanTicket>;
67
+ }
@@ -0,0 +1,56 @@
1
+ import type { IKanbanColumn, IKanbanComment, IKanbanPerson, IKanbanProvider, IKanbanTicket } from './interfaces.js';
2
+
3
+ export const kanbanDemoPeople: IKanbanPerson[] = [{ id: 'mara', name: 'Mara Chen' }, { id: 'jonas', name: 'Jonas Berg' }, { id: 'alex', name: 'Alex Rivera' }];
4
+ export const kanbanDemoColumns: IKanbanColumn[] = [{ id: 'backlog', title: 'Backlog' }, { id: 'progress', title: 'In progress' }, { id: 'review', title: 'In review' }, { id: 'done', title: 'Done', done: true }];
5
+ export const createKanbanDemo = (): { tickets: IKanbanTicket[]; provider: IKanbanProvider } => {
6
+ let tickets: IKanbanTicket[] = [
7
+ { id: 'epic-1', key: 'CAT-100', kind: 'epic', title: 'A calmer workspace', description: 'Make daily work feel focused, connected and easy to navigate.', columnId: 'progress', order: 0 },
8
+ { id: 'epic-2', key: 'CAT-200', kind: 'epic', title: 'Built for teams', description: 'Keep everyone close to the work and the decisions behind it.', columnId: 'progress', order: 1 },
9
+ { id: 'task-1', key: 'CAT-101', kind: 'task', title: 'Design the project overview', description: 'Give each project a clear home. Surface the next action, recent activity and open questions.', parentId: 'epic-1', columnId: 'backlog', order: 0, priority: 'normal', assignee: kanbanDemoPeople[0], labels: ['Design'] },
10
+ { id: 'task-2', key: 'CAT-102', kind: 'task', title: 'Connect related tickets', description: 'Type #_ in a comment to search for another ticket. Keep the reference connected when its title changes.', parentId: 'epic-2', columnId: 'progress', order: 0, priority: 'high', assignee: kanbanDemoPeople[1], labels: ['Collaboration'] },
11
+ { id: 'task-3', key: 'CAT-103', kind: 'task', title: 'Make every action keyboard accessible', description: 'Support focus, selection, navigation and moving tickets without a mouse.', parentId: 'epic-1', columnId: 'progress', order: 1, priority: 'normal', assignee: kanbanDemoPeople[2], labels: ['Accessibility'] },
12
+ { id: 'task-4', key: 'CAT-104', kind: 'task', title: 'Review the ticket detail experience', description: 'Check hierarchy navigation, discussion drafts and small-screen layouts.', parentId: 'epic-2', columnId: 'review', order: 0, priority: 'low', assignee: kanbanDemoPeople[0] },
13
+ { id: 'task-5', key: 'CAT-105', kind: 'task', title: 'Unify status and priority styles', description: 'Use shared catalog tokens in light and dark themes.', parentId: 'epic-1', columnId: 'done', order: 0, assignee: kanbanDemoPeople[2], labels: ['Foundations'] },
14
+ { id: 'sub-1', key: 'CAT-106', kind: 'subtask', title: 'Anchor search to the cursor', parentId: 'task-2', columnId: 'done', order: 0, assignee: kanbanDemoPeople[1] },
15
+ { id: 'sub-2', key: 'CAT-107', kind: 'subtask', title: 'Restore focus after selection', parentId: 'task-2', columnId: 'progress', order: 1, assignee: kanbanDemoPeople[1] },
16
+ { id: 'sub-3', key: 'CAT-108', kind: 'subtask', title: 'Check arrow-key navigation', parentId: 'task-3', columnId: 'done', order: 0, assignee: kanbanDemoPeople[2] },
17
+ ];
18
+ let comments: IKanbanComment[] = [
19
+ { id: 'comment-1', ticketId: 'task-2', author: kanbanDemoPeople[0], createdAt: '2026-09-18T09:30:00Z', body: [{ type: 'text', text: 'The picker should feel like part of the conversation. Can we check this together with ' }, { type: 'reference', id: 'task-3', label: '#CAT-103 Keyboard accessibility' }, { type: 'text', text: '?' }] },
20
+ { id: 'comment-2', ticketId: 'task-2', author: kanbanDemoPeople[1], createdAt: '2026-09-18T09:42:00Z', body: [{ type: 'text', text: 'Yes. Search receives focus immediately, and choosing a ticket returns you to exactly where you were writing.' }] },
21
+ ];
22
+ let nextKey = 300;
23
+ // Demo-only in-memory provider. Applications persist through their own authorized APIs.
24
+ const provider: IKanbanProvider = {
25
+ getTicket: async id => {
26
+ const ticket = tickets.find(item => item.id === id);
27
+ if (!ticket) throw new Error('Ticket not found.');
28
+ return ticket;
29
+ },
30
+ searchTickets: async query => tickets.filter(ticket => `${ticket.key} ${ticket.title}`.toLowerCase().includes(query.toLowerCase())),
31
+ loadComments: async id => comments.filter(comment => comment.ticketId === id),
32
+ addComment: async input => {
33
+ const comment: IKanbanComment = { ...input, id: crypto.randomUUID(), author: kanbanDemoPeople[0], createdAt: new Date().toISOString() };
34
+ comments = [...comments, comment]; return comment;
35
+ },
36
+ updateTicket: async ({ ticketId, changes }) => {
37
+ const previous = tickets.find(ticket => ticket.id === ticketId)!;
38
+ const ticket: IKanbanTicket = { ...previous, ...changes, assignee: changes.assignee === null ? undefined : changes.assignee ?? previous.assignee,
39
+ parentId: changes.parentId === null ? undefined : changes.parentId ?? previous.parentId };
40
+ tickets = tickets.map(item => item.id === ticketId ? ticket : item); return ticket;
41
+ },
42
+ moveTicket: async ({ ticketId, columnId, beforeTicketId }) => {
43
+ const ticket = tickets.find(item => item.id === ticketId)!;
44
+ const destination = tickets.filter(item => item.id !== ticketId && item.columnId === columnId && item.kind === ticket.kind).sort((a, b) => a.order - b.order);
45
+ const index = beforeTicketId ? destination.findIndex(item => item.id === beforeTicketId) : destination.length;
46
+ destination.splice(index < 0 ? destination.length : index, 0, { ...ticket, columnId });
47
+ const changed = destination.map((item, order) => ({ ...item, order }));
48
+ tickets = tickets.map(item => changed.find(record => record.id === item.id) ?? item); return changed;
49
+ },
50
+ createTicket: async input => {
51
+ const ticket: IKanbanTicket = { ...input, id: crypto.randomUUID(), key: `CAT-${nextKey++}`, order: tickets.length };
52
+ tickets = [...tickets, ticket]; return ticket;
53
+ },
54
+ };
55
+ return { tickets, provider };
56
+ };
@@ -0,0 +1,13 @@
1
+ import type { IKanbanTicket, IKanbanTicketDraft } from './interfaces.js';
2
+
3
+ export const sortKanbanTickets = (tickets: readonly IKanbanTicket[]): IKanbanTicket[] =>
4
+ [...tickets].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
5
+ export const ticketLabel = (ticket: IKanbanTicket): string => `#${ticket.key} ${ticket.title}`;
6
+ export const ticketDraft = (ticket: IKanbanTicket): IKanbanTicketDraft => ({
7
+ title: ticket.title, description: ticket.description ?? '', priority: ticket.priority ?? 'none',
8
+ assigneeId: ticket.assignee?.id ?? '', parentId: ticket.parentId ?? '',
9
+ });
10
+ export const kanbanEvent = <T>(owner: HTMLElement, name: string, detail: T): void => {
11
+ owner.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
12
+ };
13
+ export const kanbanError = (error: unknown): string => error instanceof Error ? error.message : String(error);
@@ -0,0 +1,18 @@
1
+ import { css, cssManager } from '@design.estate/dees-element';
2
+ import { themeDefaultStyles } from '../00theme.js';
3
+
4
+ export const kanbanStyles = [themeDefaultStyles, cssManager.defaultStyles, css`
5
+ :host { display: block; min-width: 0; font-family: var(--dees-font-family); color: var(--dees-color-text-primary); font-size: var(--dees-font-body-size); }
6
+ * { box-sizing: border-box; }
7
+ h2, h3, p { margin: 0; }
8
+ .muted { color: var(--dees-color-text-secondary); }
9
+ .caption { font-size: var(--dees-font-footnote-size); color: var(--dees-color-text-secondary); }
10
+ .error { color: var(--dees-color-text-error); padding: 10px 0; font-size: var(--dees-font-footnote-size); }
11
+ .empty { padding: 24px 12px; text-align: center; color: var(--dees-color-text-secondary); font-size: var(--dees-font-footnote-size); }
12
+ .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
13
+ .spacer { flex: 1; }
14
+ button.link { border: 0; background: transparent; font: inherit; color: var(--dees-color-link); padding: 2px 0; text-align: left; cursor: pointer; overflow-wrap: anywhere; }
15
+ button:focus-visible, summary:focus-visible { outline: 2px solid var(--dees-color-focus-ring); outline-offset: 3px; border-radius: var(--dees-radius-sm); }
16
+ dees-icon { font-size: 16px; }
17
+ .badge { display: inline-flex; align-items: center; gap: 4px; background: var(--dees-color-badge-default-bg); padding: 3px 7px; border-radius: var(--dees-radius-sm); font-size: var(--dees-font-caption1-size); color: var(--dees-color-text-secondary); }
18
+ `];
@@ -10,6 +10,7 @@ export * from './00group-dataview/index.js';
10
10
  export * from './00group-feedback/index.js';
11
11
  export * from './00group-form/index.js';
12
12
  export * from './00group-harness/index.js';
13
+ export * from './00group-kanban/index.js';
13
14
  export * from './00group-input/index.js';
14
15
  export * from './00group-layout/index.js';
15
16
  export * from './00group-media/index.js';
@@ -101,6 +101,7 @@ export interface IEchartsBundle {
101
101
  */
102
102
  export interface ITiptapBundle {
103
103
  Editor: typeof Editor;
104
+ Node: typeof import('@tiptap/core').Node;
104
105
  StarterKit: { configure: (options?: Partial<StarterKitOptions>) => any };
105
106
  Underline: { configure: (options?: Partial<UnderlineOptions>) => any };
106
107
  TextAlign: { configure: (options?: Partial<TextAlignOptions>) => any };
@@ -464,6 +465,7 @@ export class DeesServiceLibLoader {
464
465
 
465
466
  this.tiptapLib = {
466
467
  Editor: coreModule.Editor,
468
+ Node: coreModule.Node,
467
469
  StarterKit: starterKitModule.default || starterKitModule.StarterKit,
468
470
  Underline: underlineModule.default || underlineModule.Underline,
469
471
  TextAlign: textAlignModule.default || textAlignModule.TextAlign,