@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.
- package/dist_bundle/bundle.js +340 -177
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-chart/dees-chart-area/component.d.ts +26 -16
- package/dist_ts_web/elements/00group-chart/dees-chart-area/component.js +161 -72
- package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.d.ts +2 -1
- package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.js +3 -2
- package/dist_ts_web/elements/00group-chart/dees-chart-area/template.js +4 -3
- package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.js +4 -4
- package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.js +32 -18
- package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.js +37 -21
- package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.js +16 -12
- package/dist_ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.js +26 -15
- package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.d.ts +2 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.js +73 -40
- package/dist_ts_web/elements/00group-kanban/kanban.helpers.d.ts +3 -1
- package/dist_ts_web/elements/00group-kanban/kanban.helpers.js +3 -1
- package/dist_ts_web/elements/00group-kanban/kanban.styles.js +11 -3
- package/package.json +1 -1
- package/readme.md +19 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-chart/dees-chart-area/component.ts +195 -79
- package/ts_web/elements/00group-chart/dees-chart-area/realtime-updates.ts +4 -2
- package/ts_web/elements/00group-chart/dees-chart-area/template.ts +3 -2
- package/ts_web/elements/00group-input/dees-input-reference/dees-input-reference.ts +3 -3
- package/ts_web/elements/00group-kanban/dees-kanban/dees-kanban.ts +31 -17
- package/ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.ts +36 -20
- package/ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.ts +15 -11
- package/ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.ts +25 -14
- package/ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.ts +64 -38
- package/ts_web/elements/00group-kanban/kanban.helpers.ts +5 -1
- 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:
|
|
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
|
-
.
|
|
23
|
-
.key { font-variant-numeric: tabular-nums; }
|
|
24
|
-
.link.epic {
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
.
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
${
|
|
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
|
-
|
|
54
|
-
|
|
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:
|
|
23
|
-
section { min-height: 220px; height: 100%; border-radius: var(--dees-radius-
|
|
24
|
-
header { display: flex; align-items: center; gap: 8px; padding:
|
|
25
|
-
h3 { font-size: var(--dees-font-
|
|
26
|
-
|
|
27
|
-
.
|
|
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.
|
|
30
|
-
.end-target {
|
|
31
|
-
|
|
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:
|
|
24
|
-
h3 { font-size: var(--dees-font-
|
|
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 {
|
|
27
|
-
.
|
|
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:
|
|
30
|
-
.reference { display: inline; border: 0; border-radius: var(--dees-radius-sm); padding: 1px
|
|
31
|
-
.compose { padding-
|
|
32
|
-
.compose
|
|
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
|
|
39
|
-
<dees-button size="
|
|
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
|
-
<
|
|
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
|
|
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>` : ''}
|
package/ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.ts
CHANGED
|
@@ -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
|
-
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
|
|
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 {
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
|
|
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
|
|
69
|
-
${
|
|
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"><
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
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-
|
|
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-
|
|
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
|
`];
|