@design.estate/dees-catalog 12.5.0 → 13.0.1

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 (32) hide show
  1. package/dist_bundle/bundle.js +340 -177
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_ts_web/00_commitinfo_data.js +1 -1
  4. package/dist_ts_web/elements/00group-chart/dees-chart-area/component.d.ts +26 -16
  5. package/dist_ts_web/elements/00group-chart/dees-chart-area/component.js +161 -72
  6. package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.d.ts +2 -1
  7. package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.js +3 -2
  8. package/dist_ts_web/elements/00group-chart/dees-chart-area/template.js +4 -3
  9. package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.js +4 -4
  10. package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.js +32 -18
  11. package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.js +37 -21
  12. package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.js +16 -12
  13. package/dist_ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.js +26 -15
  14. package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.d.ts +2 -0
  15. package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.js +73 -40
  16. package/dist_ts_web/elements/00group-kanban/kanban.helpers.d.ts +3 -1
  17. package/dist_ts_web/elements/00group-kanban/kanban.helpers.js +3 -1
  18. package/dist_ts_web/elements/00group-kanban/kanban.styles.js +11 -3
  19. package/package.json +1 -1
  20. package/readme.md +19 -1
  21. package/ts_web/00_commitinfo_data.ts +1 -1
  22. package/ts_web/elements/00group-chart/dees-chart-area/component.ts +195 -79
  23. package/ts_web/elements/00group-chart/dees-chart-area/realtime-updates.ts +4 -2
  24. package/ts_web/elements/00group-chart/dees-chart-area/template.ts +3 -2
  25. package/ts_web/elements/00group-input/dees-input-reference/dees-input-reference.ts +3 -3
  26. package/ts_web/elements/00group-kanban/dees-kanban/dees-kanban.ts +31 -17
  27. package/ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.ts +36 -20
  28. package/ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.ts +15 -11
  29. package/ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.ts +25 -14
  30. package/ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.ts +64 -38
  31. package/ts_web/elements/00group-kanban/kanban.helpers.ts +5 -1
  32. package/ts_web/elements/00group-kanban/kanban.styles.ts +10 -2
@@ -1,7 +1,7 @@
1
1
  import { DeesElement, customElement, property, html, css } from '@design.estate/dees-element';
2
2
  import type { IKanbanTicket, IKanbanColumn } from '../interfaces.js';
3
3
  import { kanbanStyles } from '../kanban.styles.js';
4
- import { kanbanEvent } from '../kanban.helpers.js';
4
+ import { kanbanEvent, kanbanInitials, kanbanPriorityIcon } from '../kanban.helpers.js';
5
5
  import '../../00group-button/dees-button/dees-button.js';
6
6
  import '../../00group-utility/dees-icon/dees-icon.js';
7
7
 
@@ -16,17 +16,32 @@ export class DeesKanbanCard extends DeesElement {
16
16
  @property({ type: Boolean }) accessor movable = false;
17
17
  @property({ type: Boolean }) accessor pending = false;
18
18
  public static styles = [...kanbanStyles, css`
19
- article { padding: 14px; border: 1px solid var(--dees-color-border-default); border-radius: var(--dees-radius-lg); background: var(--dees-color-bg-primary); box-shadow: var(--dees-shadow-xs); }
19
+ 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
20
  article:hover { border-color: var(--dees-color-border-strong); }
21
21
  article[aria-busy=true] { opacity: .65; }
22
- .title { display: block; width: 100%; border: 0; background: transparent; padding: 8px 0 12px; color: inherit; text-align: left; cursor: pointer; font: inherit; font-weight: 550; line-height: 1.45; overflow-wrap: anywhere; }
23
- .key { font-variant-numeric: tabular-nums; }
24
- .link.epic { margin-bottom: 5px; max-width: 100%; font-size: var(--dees-font-caption1-size); }
25
- .labels { margin-bottom: 10px; }
26
- .priority[data-priority=urgent], .priority[data-priority=high] { color: var(--dees-color-text-warning); }
27
- .avatar { width: 24px; height: 24px; border-radius: 50%; background: var(--dees-color-badge-default-bg); display: inline-grid; place-items: center; font-size: 10px; font-weight: 600; }
28
- summary { cursor: pointer; padding: 10px 0 2px; font-size: var(--dees-font-footnote-size); color: var(--dees-color-text-secondary); }
29
- .subtask { display: flex; gap: 6px; align-items: baseline; padding-top: 8px; font-size: var(--dees-font-footnote-size); }
22
+ .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
+ .key { flex-shrink: 0; font-weight: 500; font-variant-numeric: tabular-nums; }
24
+ .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
+ .link.epic:hover { color: var(--dees-color-link); text-decoration: underline; }
26
+ .move { flex-shrink: 0; margin-left: auto; }
27
+ .move::part(button) { width: 24px; height: 24px; min-height: 24px; }
28
+ .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
+ .title:hover { color: var(--dees-color-link); }
30
+ .footer { margin-top: 10px; gap: 6px; }
31
+ .badge { padding: 1px 6px; }
32
+ .assignee { display: flex; align-items: center; gap: 5px; margin-left: auto; min-width: 0; color: var(--dees-color-text-secondary); font-size: var(--dees-font-caption1-size); }
33
+ .assignee-name { max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
34
+ .avatar { width: 20px; height: 20px; font-size: 9px; }
35
+ details { border-top: 1px solid var(--dees-color-border-subtle); margin-top: 10px; }
36
+ summary { cursor: pointer; display: flex; align-items: center; gap: 6px; padding: 9px 0 0; font-size: var(--dees-font-caption1-size); color: var(--dees-color-text-secondary); list-style: none; }
37
+ summary::-webkit-details-marker { display: none; }
38
+ summary .disclosure { margin-left: auto; }
39
+ details[open] summary .disclosure { transform: rotate(90deg); }
40
+ .subtask { display: flex; gap: 7px; align-items: center; padding-top: 8px; font-size: var(--dees-font-footnote-size); }
41
+ .subtask dees-icon { flex-shrink: 0; color: var(--dees-color-text-secondary); font-size: 14px; }
42
+ .subtask .link { color: var(--dees-color-text-primary); }
43
+ .subtask .link:hover { color: var(--dees-color-link); }
44
+ @media (pointer: coarse) { .move::part(button) { width: 32px; height: 32px; min-height: 32px; } summary { min-height: 36px; } }
30
45
  `];
31
46
  private open(ticketId: string): void { kanbanEvent(this, 'kanban-ticket-open', { ticketId }); }
32
47
  public render() {
@@ -40,18 +55,19 @@ export class DeesKanbanCard extends DeesElement {
40
55
  event.dataTransfer.effectAllowed = 'move';
41
56
  kanbanEvent(this, 'kanban-drag-start', { ticketId: ticket.id });
42
57
  }} @dragend=${() => kanbanEvent(this, 'kanban-drag-end', {})}>
43
- ${this.epic ? html`<button class="link epic" @click=${() => this.open(this.epic!.id)}>${this.epic.title}</button>` : ''}
44
- <div class="row caption"><span class="key">${ticket.key}</span><span class="spacer"></span>
45
- ${ticket.priority && ticket.priority !== 'none' ? html`<span class="priority" data-priority=${ticket.priority}>${ticket.priority}</span>` : ''}</div>
46
- <button class="title" @click=${() => this.open(ticket.id)}>${ticket.title}</button>
47
- ${ticket.labels?.length ? html`<div class="row labels">${ticket.labels.map(label => html`<span class="badge">${label}</span>`)}</div>` : ''}
48
- <div class="row caption"><span class="avatar" title=${ticket.assignee?.name ?? 'Unassigned'}>${ticket.assignee?.name.split(/\s+/).map(part => part[0]).join('').slice(0, 2) ?? '–'}</span>
49
- <span>${ticket.assignee?.name ?? 'Unassigned'}</span><span class="spacer"></span>
50
- ${this.movable ? html`<dees-button size="icon" type="ghost" icon="lucide:Ellipsis" text="Move ticket" .disabled=${this.pending}
58
+ <div class="meta"><span class="key">${ticket.key}</span>
59
+ ${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}
51
61
  @clicked=${(event: Event) => kanbanEvent(this, 'kanban-move-menu', { ticketId: ticket.id, anchor: event.currentTarget as HTMLElement })}></dees-button>` : ''}
52
62
  </div>
53
- ${this.subtasks.length ? html`<details><summary>${completed}/${this.subtasks.length} subtasks complete</summary>
54
- ${this.subtasks.map(task => html`<div class="subtask"><span>${this.columns.some(column => column.id === task.columnId && column.done) ? '✓' : '○'}</span>
63
+ <button class="title" @click=${() => this.open(ticket.id)}>${ticket.title}</button>
64
+ <div class="row footer">
65
+ ${ticket.priority && ticket.priority !== 'none' ? html`<span class="priority" data-priority=${ticket.priority}><dees-icon .icon=${kanbanPriorityIcon(ticket.priority)} iconSize="13" aria-hidden="true"></dees-icon>${ticket.priority}</span>` : ''}
66
+ ${ticket.labels?.map(label => html`<span class="badge">${label}</span>`)}
67
+ <span class="assignee" title=${ticket.assignee?.name ?? 'Unassigned'}><span class="avatar" aria-hidden="true">${ticket.assignee ? kanbanInitials(ticket.assignee.name) : '–'}</span><span class="assignee-name">${ticket.assignee?.name ?? 'Unassigned'}</span></span>
68
+ </div>
69
+ ${this.subtasks.length ? html`<details><summary><dees-icon icon="lucide:ListChecks" iconSize="14" aria-hidden="true"></dees-icon>${completed}/${this.subtasks.length} subtasks complete<dees-icon class="disclosure" icon="lucide:ChevronRight" iconSize="14" aria-hidden="true"></dees-icon></summary>
70
+ ${this.subtasks.map(task => html`<div class="subtask"><dees-icon .icon=${this.columns.some(column => column.id === task.columnId && column.done) ? 'lucide:CircleCheck' : 'lucide:Circle'} iconSize="14" aria-hidden="true"></dees-icon>
55
71
  <button class="link" @click=${() => this.open(task.id)}>${task.title}</button></div>`)}</details>` : ''}
56
72
  </article>`;
57
73
  }
@@ -19,16 +19,20 @@ export class DeesKanbanColumn extends DeesElement {
19
19
  @state() private accessor dropBefore: string | undefined;
20
20
  @state() private accessor draggingOver = false;
21
21
  public static styles = [...kanbanStyles, css`
22
- :host { display: flex; flex-direction: column; width: 300px; min-width: 270px; flex: 1 0 270px; }
23
- section { min-height: 220px; height: 100%; border-radius: var(--dees-radius-lg); background: var(--dees-color-bg-secondary); padding: 10px; }
24
- header { display: flex; align-items: center; gap: 8px; padding: 3px 3px 12px; }
25
- h3 { font-size: var(--dees-font-body-size); font-weight: 600; }
26
- .count { font-size: var(--dees-font-caption1-size); color: var(--dees-color-text-secondary); }
27
- .stack { display: grid; gap: 10px; min-height: 80px; align-content: start; }
22
+ :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); }
24
+ 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
+ h3 { font-size: var(--dees-font-footnote-size); font-weight: 600; }
26
+ header dees-button::part(button) { width: 24px; height: 24px; min-height: 24px; }
27
+ .status { width: 8px; height: 8px; border: 1.5px solid var(--dees-color-text-secondary); border-radius: 50%; flex-shrink: 0; }
28
+ .status.done { background: var(--dees-color-accent-success); border-color: var(--dees-color-accent-success); }
29
+ .stack { display: grid; gap: 8px; min-height: 120px; align-content: start; }
28
30
  .target { position: relative; }
29
- .target.before::before, .end-target.before::before { content: ''; display: block; height: 3px; background: var(--dees-color-accent-primary); border-radius: 2px; margin-bottom: 7px; }
30
- .end-target { min-height: 20px; padding-top: 10px; }
31
- section.over { outline: 2px solid var(--dees-color-focus-ring); outline-offset: 1px; }
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; }
33
+ .end-target { position: relative; min-height: 32px; margin-top: 8px; }
34
+ section.over { background: var(--dees-color-row-hover); }
35
+ @media (pointer: coarse) { header dees-button::part(button) { width: 32px; height: 32px; min-height: 32px; } }
32
36
  `];
33
37
  public updated(): void { if (!this.draggingTicketId) this.draggingOver = false; }
34
38
  public render() {
@@ -41,11 +45,11 @@ export class DeesKanbanColumn extends DeesElement {
41
45
  if (this.movable && this.draggingTicketId && event.dataTransfer?.getData('application/x-dees-kanban-ticket') === this.draggingTicketId)
42
46
  kanbanEvent(this, 'kanban-ticket-move', { ticketId: this.draggingTicketId, columnId: this.column!.id, beforeTicketId: this.dropBefore });
43
47
  }}>
44
- <header><h3>${this.column.title}</h3><span class="count">${this.tickets.length}</span><span class="spacer"></span>
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>
45
49
  ${this.canCreate ? html`<dees-button type="ghost" size="icon" icon="lucide:Plus" text=${`Add task to ${this.column.title}`}
46
50
  @clicked=${() => kanbanEvent(this, 'kanban-ticket-create', { kind: 'task', columnId: this.column!.id })}></dees-button>` : ''}</header>
47
51
  <div class="stack">${sortKanbanTickets(this.tickets).map(ticket => html`
48
- <div class="target ${this.draggingOver && this.dropBefore === ticket.id ? 'before' : ''}"
52
+ <div class="target ${this.draggingOver && this.dropBefore === ticket.id ? 'before' : ''} ${this.draggingTicketId === ticket.id ? 'dragging' : ''}"
49
53
  @dragover=${() => { this.dropBefore = ticket.id; }}>
50
54
  <dees-kanban-card .ticket=${ticket} .columns=${this.columns}
51
55
  .epic=${this.allTickets.find(item => item.id === ticket.parentId && item.kind === 'epic')}
@@ -1,7 +1,7 @@
1
1
  import { DeesElement, customElement, property, state, html, css } from '@design.estate/dees-element';
2
2
  import type { IKanbanComment, IKanbanProvider } from '../interfaces.js';
3
3
  import { kanbanStyles } from '../kanban.styles.js';
4
- import { kanbanError, kanbanEvent } from '../kanban.helpers.js';
4
+ import { kanbanError, kanbanEvent, kanbanInitials } from '../kanban.helpers.js';
5
5
  import { referenceText, type TReferenceContent, type TReferenceSearch } from '../../00group-input/dees-input-reference/index.js';
6
6
  import '../../00group-button/dees-button/dees-button.js';
7
7
 
@@ -20,31 +20,42 @@ export class DeesKanbanDiscussion extends DeesElement {
20
20
  @state() private accessor pending = new Set<string>();
21
21
  @state() private accessor failures = new Map<string, string>();
22
22
  public static styles = [...kanbanStyles, css`
23
- header { display: flex; align-items: center; margin-bottom: 16px; }
24
- h3 { font-size: var(--dees-font-body-size); font-weight: 600; }
23
+ header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
24
+ h3 { font-size: var(--dees-font-footnote-size); font-weight: 600; }
25
+ .refresh::part(button) { width: 28px; height: 28px; min-height: 28px; }
25
26
  ol { list-style: none; padding: 0; margin: 0; }
26
- li { padding: 0 0 20px; }
27
- .author { font-weight: 550; font-size: var(--dees-font-footnote-size); }
27
+ li { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 10px; padding: 10px 0; }
28
+ li > .avatar { width: 28px; height: 28px; margin-top: 1px; }
29
+ .author { font-weight: 600; font-size: var(--dees-font-footnote-size); }
30
+ .meta { gap: 4px 10px; }
28
31
  time { font-size: var(--dees-font-caption1-size); color: var(--dees-color-text-secondary); }
29
- .message { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; margin-top: 6px; }
30
- .reference { display: inline; border: 0; border-radius: var(--dees-radius-sm); padding: 1px 4px; color: var(--dees-color-link); background: var(--dees-color-badge-default-bg); font: inherit; cursor: pointer; }
31
- .compose { padding-top: 4px; }
32
- .compose .row { padding-top: 8px; }
32
+ .message { white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--dees-font-subhead-size); line-height: 22px; margin-top: 2px; }
33
+ .reference { display: inline; border: 0; border-radius: var(--dees-radius-sm); padding: 1px 5px; color: var(--dees-color-link); background: var(--dees-color-fill-tertiary); font: inherit; font-weight: 500; cursor: pointer; text-align: left; overflow-wrap: anywhere; }
34
+ .compose { margin: 14px 0 0 38px; padding: 0; border: 1px solid var(--dees-color-control-border); border-radius: var(--dees-radius-xl); corner-shape: var(--dees-corner-shape); background: var(--dees-color-control-bg); }
35
+ .compose:has(dees-input-reference:focus-within) { outline: 2px solid var(--dees-color-focus-ring); outline-offset: 1px; }
36
+ dees-input-reference { --dees-reference-min-height: 64px; --dees-reference-font-size: var(--dees-font-subhead-size); }
37
+ dees-input-reference::part(editor) { border: 0; background: transparent; outline: none; }
38
+ .compose .row { padding: 4px 10px 10px; gap: 8px; }
39
+ .compose .caption { font-size: var(--dees-font-caption1-size); }
40
+ .shortcut { white-space: nowrap; }
41
+ .compose .error { padding: 0 12px 10px; }
42
+ @media (max-width: 600px) { .compose { margin-left: 0; } .compose .caption { flex-basis: 100%; } }
33
43
  `];
34
44
  public render() {
35
45
  const pending = this.pending.has(this.ticketId);
36
46
  const comments = this.comments.filter(comment => comment.ticketId === this.ticketId)
37
47
  .slice().sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
38
- return html`<header><h3>Discussion <span class="caption">${comments.length}</span></h3><span class="spacer"></span>
39
- <dees-button size="sm" type="ghost" icon="lucide:RefreshCw" text="Refresh" .disabled=${this.loading}
48
+ return html`<header><h3>Discussion</h3><span class="count">${comments.length}</span><span class="spacer"></span>
49
+ <dees-button class="refresh" size="icon" type="ghost" icon="lucide:RefreshCw" text="Refresh" .disabled=${this.loading}
40
50
  @clicked=${() => kanbanEvent(this, 'kanban-comments-refresh', { ticketId: this.ticketId })}></dees-button></header>
41
51
  ${this.loading ? html`<p class="caption" role="status">Loading comments…</p>` : ''}
42
52
  ${this.error ? html`<p class="error" role="alert">${this.error}</p>` : ''}
43
53
  <ol aria-label="Comments">${comments.map(comment => html`<li>
44
- <div class="row"><span class="author">${comment.author.name}</span><time datetime=${comment.createdAt}>${new Date(comment.createdAt).toLocaleString()}</time></div>
54
+ <span class="avatar" aria-hidden="true">${kanbanInitials(comment.author.name)}</span><div>
55
+ <div class="row meta"><span class="author">${comment.author.name}</span><time datetime=${comment.createdAt} title=${comment.createdAt}>${new Date(comment.createdAt).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}</time></div>
45
56
  <div class="message">${comment.body.map(part => part.type === 'text' ? part.text : html`<button class="reference"
46
57
  @click=${() => kanbanEvent(this, 'kanban-ticket-open', { ticketId: part.id })}>${part.label}</button>`)}</div>
47
- </li>`)}</ol>
58
+ </div></li>`)}</ol>
48
59
  ${!comments.length && !this.loading && !this.error ? html`<p class="empty">Start the conversation.</p>` : ''}
49
60
  ${this.addComment && !this.readOnly ? html`<div class="compose">
50
61
  <dees-input-reference label="Comment" placeholder="Write a comment…" .documentKey=${this.ticketId} .value=${this.draft} .disabled=${pending}
@@ -55,7 +66,7 @@ export class DeesKanbanDiscussion extends DeesElement {
55
66
  }}
56
67
  @reference-open=${(event: CustomEvent<{ id: string }>) => kanbanEvent(this, 'kanban-ticket-open', { ticketId: event.detail.id })}
57
68
  @reference-submit=${() => { void this.submit(); }}></dees-input-reference>
58
- <div class="row"><span class="caption">Type #_ to link a ticket · Ctrl/⌘ Enter to send</span><span class="spacer"></span>
69
+ <div class="row"><span class="caption">Type <strong>#_</strong> to link a ticket · <span class="shortcut">Ctrl/⌘ Enter to send</span></span><span class="spacer"></span>
59
70
  <dees-button type="accent" size="sm" text="Comment" .disabled=${pending || !referenceText(this.draft).trim()}
60
71
  .status=${pending ? 'pending' : 'normal'} @clicked=${() => { void this.submit(); }}></dees-button></div>
61
72
  ${this.failures.get(this.ticketId) ? html`<p class="error" role="alert">${this.failures.get(this.ticketId)}</p>` : ''}
@@ -2,7 +2,7 @@ import { DeesElement, customElement, property, state, html, css } from '@design.
2
2
  import type { IKanbanColumn, IKanbanComment, IKanbanPerson, IKanbanProvider, IKanbanTicket, IKanbanTicketDraft, TKanbanPriority } from '../interfaces.js';
3
3
  import type { TReferenceContent, TReferenceSearch } from '../../00group-input/dees-input-reference/interfaces.js';
4
4
  import { kanbanStyles } from '../kanban.styles.js';
5
- import { kanbanEvent, sortKanbanTickets, ticketDraft, ticketLabel } from '../kanban.helpers.js';
5
+ import { kanbanEvent, kanbanInitials, kanbanPriorityIcon, sortKanbanTickets, ticketDraft, ticketLabel } from '../kanban.helpers.js';
6
6
  import type { DeesInputText } from '../../00group-input/dees-input-text/dees-input-text.js';
7
7
  import type { DeesInputTextarea } from '../../00group-input/dees-input-textarea/dees-input-textarea.js';
8
8
  import '../../00group-input/dees-input-text/dees-input-text.js';
@@ -28,22 +28,44 @@ export class DeesKanbanTicketDetail extends DeesElement {
28
28
  @property({ type: String }) accessor error = '';
29
29
  @property({ type: Boolean }) accessor saving = false;
30
30
  @property({ type: Boolean }) accessor canGoBack = false;
31
+ /** Keep the identifier visible unless the containing surface already labels the ticket. */
32
+ @property({ type: Boolean }) accessor showTicketKey = true;
31
33
  @state() private accessor editing = false;
32
34
  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; }
35
+ :host { padding: 24px; }
36
+ .layout { display: grid; grid-template-columns: minmax(0, 1fr) 248px; grid-template-areas: 'lead props' 'body props'; gap: 24px; align-items: start; }
37
+ .lead { grid-area: lead; min-width: 0; }
38
+ .body { grid-area: body; min-width: 0; }
39
+ .toolbar { margin-bottom: 20px; flex-wrap: nowrap; min-width: 0; }
40
+ .toolbar .badge { text-transform: capitalize; flex-shrink: 0; }
41
+ .ticket-key { flex-shrink: 0; font-variant-numeric: tabular-nums; }
42
+ .link.parent { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--dees-font-footnote-size); }
43
+ .toolbar dees-button { flex-shrink: 0; }
44
+ h2 { font-size: var(--dees-font-title2-size); line-height: 1.3; font-weight: 600; margin-bottom: 12px; letter-spacing: -.015em; overflow-wrap: anywhere; }
45
+ .description { white-space: pre-wrap; font-size: var(--dees-font-subhead-size); line-height: 22px; overflow-wrap: anywhere; max-width: 64ch; }
46
+ aside { grid-area: props; min-width: 0; padding: 14px 16px; border-radius: var(--dees-radius-xl); corner-shape: var(--dees-corner-shape); background: var(--dees-color-settings-group); }
47
+ .fields { display: grid; gap: 14px; font-size: var(--dees-font-footnote-size); font-weight: 500; }
48
+ .fields label { display: block; margin-bottom: 5px; font-size: var(--dees-font-caption1-size); font-weight: 400; }
49
+ .value { min-height: var(--dees-control-height-lg); display: flex; align-items: center; }
50
+ .person { display: flex; align-items: center; gap: 7px; }
42
51
  .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)); } }
52
+ .child-list { border-radius: var(--dees-radius-xl); corner-shape: var(--dees-corner-shape); background: var(--dees-color-settings-group); overflow: hidden; }
53
+ .child { display: flex; width: 100%; align-items: center; gap: 10px; min-height: 44px; padding: 10px 12px; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
54
+ .child + .child { border-top: 1px solid var(--dees-color-border-subtle); }
55
+ .child:hover { background: var(--dees-color-row-hover); }
56
+ .child:focus-visible { outline-offset: -3px; }
57
+ .child-key { font-size: var(--dees-font-caption1-size); color: var(--dees-color-text-secondary); font-variant-numeric: tabular-nums; flex-shrink: 0; }
58
+ .child-title { flex: 1; min-width: 0; font-size: var(--dees-font-footnote-size); font-weight: 500; overflow-wrap: anywhere; }
59
+ .child .badge { flex-shrink: 0; }
60
+ .discussion { border-top: 1px solid var(--dees-color-border-subtle); padding-top: 20px; }
61
+ .edit { display: grid; gap: 14px; }
62
+ @media (max-width: 700px) {
63
+ :host { padding: 16px; }
64
+ .layout { grid-template-columns: minmax(0, 1fr); grid-template-areas: 'lead' 'props' 'body'; gap: 20px; }
65
+ .fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
66
+ .toolbar { gap: 6px; margin-bottom: 16px; }
67
+ .child { gap: 7px; }
68
+ }
47
69
  `];
48
70
  public updated(changed: Map<string, unknown>): void {
49
71
  const previous = changed.get('ticket') as IKanbanTicket | undefined;
@@ -65,12 +87,13 @@ export class DeesKanbanTicketDetail extends DeesElement {
65
87
  const parents = this.tickets.filter(item => item.kind === (ticket.kind === 'subtask' ? 'task' : 'epic'));
66
88
  return html`<div class="toolbar row">
67
89
  ${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>` : ''}
90
+ <span class="badge">${ticket.kind}</span>
91
+ ${this.showTicketKey ? html`<span class="caption ticket-key">${ticket.key}</span>` : ''}
92
+ ${parent ? html`<span class="caption" aria-hidden="true">/</span><button class="link parent" title=${parent.title} @click=${() => kanbanEvent(this, 'kanban-ticket-open', { ticketId: parent.id })}>${parent.title}</button>` : ''}
70
93
  <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
94
  </div>
72
95
  ${this.error ? html`<p class="error" role="alert">${this.error}</p>` : ''}
73
- <div class="layout"><div class="main">
96
+ <div class="layout"><section class="lead">
74
97
  ${this.editing && editable ? html`<div class="edit">
75
98
  <dees-input-text label="Title" .value=${draft.title} .disabled=${this.saving}
76
99
  @input=${(event: Event) => this.changeDraft({ title: String((event.currentTarget as DeesInputText).value ?? '') })}></dees-input-text>
@@ -82,33 +105,36 @@ export class DeesKanbanTicketDetail extends DeesElement {
82
105
  this.editing = false; this.draft = undefined; kanbanEvent(this, 'kanban-ticket-draft', { ticketId: ticket.id, draft: undefined });
83
106
  }}></dees-button></div>
84
107
  </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 }))}
108
+ </section><aside><div class="fields">
109
+ <div><label class="caption">Status</label><dees-input-dropdown label="Status" .labelPosition=${'none'} .options=${this.columns.map(column => ({ key: column.id, option: column.title }))}
96
110
  .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>
111
+ .disabled=${!movable || this.saving} @selectedOption=${(event: CustomEvent<{ key: string }>) => kanbanEvent(this, 'kanban-ticket-move', { ticketId: ticket.id, columnId: event.detail.key })}></dees-input-dropdown></div>
98
112
  ${this.editing && editable ? html`
99
- <dees-input-dropdown label="Priority" .disabled=${this.saving} .value=${{ key: draft.priority, option: draft.priority }}
113
+ <div><label class="caption">Priority</label><dees-input-dropdown label="Priority" .labelPosition=${'none'} .disabled=${this.saving} .value=${{ key: draft.priority, option: draft.priority }}
100
114
  .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' }}
115
+ @selectedOption=${(event: CustomEvent<{ key: TKanbanPriority }>) => this.changeDraft({ priority: event.detail.key })}></dees-input-dropdown></div>
116
+ <div><label class="caption">Assignee</label><dees-input-dropdown label="Assignee" .labelPosition=${'none'} .disabled=${this.saving} .value=${{ key: draft.assigneeId, option: this.people.find(person => person.id === draft.assigneeId)?.name ?? ticket.assignee?.name ?? 'Unassigned' }}
103
117
  .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}
118
+ @selectedOption=${(event: CustomEvent<{ key: string }>) => this.changeDraft({ assigneeId: event.detail.key })}></dees-input-dropdown></div>
119
+ ${ticket.kind !== 'epic' ? html`<div><label class="caption">${ticket.kind === 'task' ? 'Epic' : 'Parent task'}</label><dees-input-dropdown label=${ticket.kind === 'task' ? 'Epic' : 'Parent task'} .labelPosition=${'none'} .disabled=${this.saving}
106
120
  .value=${{ key: draft.parentId, option: parents.find(item => item.id === draft.parentId)?.title ?? 'None' }}
107
121
  .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>`;
122
+ @selectedOption=${(event: CustomEvent<{ key: string }>) => this.changeDraft({ parentId: event.detail.key })}></dees-input-dropdown></div>` : ''}
123
+ ` : html`<div><label class="caption">Priority</label><span class="priority value" data-priority=${ticket.priority ?? 'none'}><dees-icon .icon=${kanbanPriorityIcon(ticket.priority)} iconSize="13" aria-hidden="true"></dees-icon>${ticket.priority ?? 'none'}</span></div>
124
+ <div><label class="caption">Assignee</label><span class="person value"><span class="avatar" aria-hidden="true">${ticket.assignee ? kanbanInitials(ticket.assignee.name) : '–'}</span>${ticket.assignee?.name ?? 'Unassigned'}</span></div>`}
125
+ ${ticket.labels?.length ? html`<div><label class="caption">Labels</label><div class="row value">${ticket.labels.map(label => html`<span class="badge">${label}</span>`)}</div></div>` : ''}
126
+ </div></aside><div class="body">
127
+ ${ticket.kind !== 'subtask' ? html`<section class="children"><div class="section-title"><h3>${ticket.kind === 'epic' ? 'Tasks' : 'Subtasks'}</h3><span class="count">${children.length}</span><span class="spacer"></span>
128
+ ${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'}
129
+ @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>
130
+ <div class="child-list">${children.map(child => html`<button class="child" @click=${() => kanbanEvent(this, 'kanban-ticket-open', { ticketId: child.id })}>
131
+ <span class="child-key">${child.key}</span><span class="child-title">${child.title}</span>
132
+ <span class="badge">${this.columns.find(column => column.id === child.columnId)?.title ?? child.columnId}</span></button>`)}</div>
133
+ ${!children.length ? html`<p class="caption">No ${ticket.kind === 'epic' ? 'tasks' : 'subtasks'} yet.</p>` : ''}</section>` : ''}
134
+ <dees-kanban-discussion class="discussion" .ticketId=${ticket.id} .comments=${this.comments} .draft=${this.commentDraft}
135
+ .loading=${this.loadingComments} .error=${this.commentsError} .readOnly=${ticket.permissions?.comment === false}
136
+ .searchTickets=${this.searchTickets} .addComment=${this.provider.addComment ? this.addComment : undefined}></dees-kanban-discussion>
137
+ </div></div>`;
112
138
  }
113
139
  public finishEditing(): void { this.editing = false; }
114
140
  }
@@ -1,4 +1,8 @@
1
- import type { IKanbanTicket, IKanbanTicketDraft } from './interfaces.js';
1
+ import type { IKanbanTicket, IKanbanTicketDraft, TKanbanPriority } from './interfaces.js';
2
+
3
+ export const kanbanInitials = (name: string): string => name.trim().split(/\s+/).map(part => part[0]).join('').slice(0, 2);
4
+ export const kanbanPriorityIcon = (priority?: TKanbanPriority): string =>
5
+ priority === 'urgent' ? 'lucide:ChevronsUp' : priority === 'high' ? 'lucide:ChevronUp' : priority === 'low' ? 'lucide:ChevronDown' : 'lucide:Minus';
2
6
 
3
7
  export const sortKanbanTickets = (tickets: readonly IKanbanTicket[]): IKanbanTicket[] =>
4
8
  [...tickets].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
@@ -2,7 +2,7 @@ import { css, cssManager } from '@design.estate/dees-element';
2
2
  import { themeDefaultStyles } from '../00theme.js';
3
3
 
4
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); }
5
+ :host { display: block; min-width: 0; font-family: var(--dees-font-family); color: var(--dees-color-text-primary); font-size: var(--dees-font-subhead-size); line-height: 1.5; }
6
6
  * { box-sizing: border-box; }
7
7
  h2, h3, p { margin: 0; }
8
8
  .muted { color: var(--dees-color-text-secondary); }
@@ -14,5 +14,13 @@ export const kanbanStyles = [themeDefaultStyles, cssManager.defaultStyles, css`
14
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
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
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); }
17
+ .badge { display: inline-flex; align-items: center; gap: 4px; background: var(--dees-color-fill-tertiary); padding: 3px 7px; border-radius: var(--dees-radius-sm); font-size: var(--dees-font-caption1-size); color: var(--dees-color-text-secondary); }
18
+ .avatar { flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%; background: var(--dees-color-fill-secondary); color: var(--dees-color-text-secondary); display: inline-grid; place-items: center; font-size: 10px; font-weight: 600; line-height: 1; }
19
+ .priority { display: inline-flex; align-items: center; gap: 3px; font-size: var(--dees-font-caption1-size); color: var(--dees-color-text-secondary); text-transform: capitalize; }
20
+ .priority[data-priority=urgent] { color: var(--dees-color-text-error); }
21
+ .priority[data-priority=high] { color: var(--dees-color-text-warning); }
22
+ .priority dees-icon { font-size: 13px; }
23
+ .section-title { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
24
+ .section-title h3 { font-size: var(--dees-font-footnote-size); font-weight: 600; }
25
+ .count { display: inline-grid; place-items: center; min-width: 20px; height: 20px; padding: 0 5px; border-radius: var(--dees-radius-sm); background: var(--dees-color-fill-tertiary); color: var(--dees-color-text-secondary); font-size: var(--dees-font-caption1-size); font-weight: 500; font-variant-numeric: tabular-nums; }
18
26
  `];