@stacksjs/defaults 0.70.365 → 0.70.366
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/ai/skills/stacks-dashboard/SKILL.md +23 -14
- package/app/Actions/Auth/LoginAction.ts +11 -2
- package/app/Actions/Auth/RefreshTokenAction.ts +6 -2
- package/app/Actions/Auth/RegisterAction.ts +6 -2
- package/app/Actions/Auth/VerifyTwoFactorLoginAction.ts +6 -2
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/Auth/ForgotPasswordDashboard.stx +0 -2
- package/resources/components/Dashboard/Auth/LoginDashboard.stx +1 -1
- package/resources/components/Dashboard/Auth/RegisterDashboard.stx +1 -3
- package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +9 -435
- package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +416 -0
- package/resources/components/Dashboard/Modals/BaseModal.stx +13 -36
- package/resources/components/Dashboard/Modals/Popups/Alert.stx +18 -44
- package/resources/components/Dashboard/UI/Modal.stx +11 -7
- package/resources/components/Dashboard/UI/Table.stx +30 -25
- package/views/dashboard/AUDIT.md +21 -16
|
@@ -27,14 +27,6 @@
|
|
|
27
27
|
const error = derived(() => kanban.errorBoard())
|
|
28
28
|
const board = derived(() => kanban.currentBoard())
|
|
29
29
|
const columns = derived(() => kanban.currentColumns())
|
|
30
|
-
const allLabels = derived(() => kanban.currentLabels())
|
|
31
|
-
|
|
32
|
-
// Card detail modal state - read straight from the store. The
|
|
33
|
-
// modal itself lives at the bottom of this file.
|
|
34
|
-
const openCard = derived(() => kanban.openCard())
|
|
35
|
-
const loadingCard = derived(() => kanban.loadingCard())
|
|
36
|
-
const errorCard = derived(() => kanban.errorCard())
|
|
37
|
-
|
|
38
30
|
// Inline composer state. Each column has its own (independent) "add
|
|
39
31
|
// card" composer; the boolean tracks whether the input is open.
|
|
40
32
|
const composingCardForColumnId = state(null)
|
|
@@ -42,23 +34,7 @@
|
|
|
42
34
|
const composingNewColumn = state(false)
|
|
43
35
|
const draftColumnName = state('')
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
// due-date commits on change. Labels + assignees commit instantly
|
|
47
|
-
// via the sync endpoints (one round-trip per toggle).
|
|
48
|
-
const titleDraft = state('')
|
|
49
|
-
const descDraft = state('')
|
|
50
|
-
const commentDraft = state('')
|
|
51
|
-
const editingCommentId = state(null)
|
|
52
|
-
const commentEditDraft = state('')
|
|
53
|
-
const savingCommentId = state(null)
|
|
54
|
-
const showLabelPicker = state(false)
|
|
55
|
-
const showAssigneePicker = state(false)
|
|
56
|
-
const showNewLabelComposer = state(false)
|
|
57
|
-
const newLabelName = state('')
|
|
58
|
-
const newLabelColor = state('slate')
|
|
59
|
-
const dueDateDraft = state('')
|
|
60
|
-
|
|
61
|
-
type DeleteKind = 'card' | 'column' | 'comment' | 'label'
|
|
37
|
+
type DeleteKind = 'card' | 'column'
|
|
62
38
|
interface PendingDelete {
|
|
63
39
|
kind: DeleteKind
|
|
64
40
|
id: number
|
|
@@ -80,119 +56,15 @@
|
|
|
80
56
|
const cardDropTarget = state<CardDropTarget | null>(null)
|
|
81
57
|
const columnDropBeforeId = state<number | null>(null)
|
|
82
58
|
|
|
83
|
-
function syncDraftsFromCard() {
|
|
84
|
-
const c = openCard()
|
|
85
|
-
titleDraft.set(c ? c.title : '')
|
|
86
|
-
descDraft.set(c ? (c.description || '') : '')
|
|
87
|
-
dueDateDraft.set(c?.dueDate ? c.dueDate.slice(0, 10) : '')
|
|
88
|
-
commentDraft.set('')
|
|
89
|
-
editingCommentId.set(null)
|
|
90
|
-
commentEditDraft.set('')
|
|
91
|
-
savingCommentId.set(null)
|
|
92
|
-
}
|
|
93
|
-
// Sync drafts when the open card changes (open / close / refetch).
|
|
94
|
-
effect(() => { openCard(); Promise.resolve().then(syncDraftsFromCard) })
|
|
95
|
-
|
|
96
|
-
async function commitTitle() {
|
|
97
|
-
const c = openCard()
|
|
98
|
-
if (!c) return
|
|
99
|
-
const next = titleDraft().trim()
|
|
100
|
-
if (!next || next === c.title) return
|
|
101
|
-
await kanban.updateCard({ title: next })
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function commitDescription() {
|
|
105
|
-
const c = openCard()
|
|
106
|
-
if (!c) return
|
|
107
|
-
const next = descDraft()
|
|
108
|
-
if (next === (c.description || '')) return
|
|
109
|
-
await kanban.updateCard({ description: next || null })
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function commitDueDate(value) {
|
|
113
|
-
await kanban.updateCard({ dueDate: value || null })
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function toggleLabel(labelId) {
|
|
117
|
-
const c = openCard()
|
|
118
|
-
if (!c) return
|
|
119
|
-
const has = c.labels.some(l => l.id === labelId)
|
|
120
|
-
const next = has
|
|
121
|
-
? c.labels.filter(l => l.id !== labelId).map(l => l.id)
|
|
122
|
-
: [...c.labels.map(l => l.id), labelId]
|
|
123
|
-
await kanban.syncCardLabels(c.id, next)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async function toggleAssignee(userId) {
|
|
127
|
-
const c = openCard()
|
|
128
|
-
if (!c) return
|
|
129
|
-
const has = c.assignees.some(a => a.userId === userId)
|
|
130
|
-
const next = has
|
|
131
|
-
? c.assignees.filter(a => a.userId !== userId).map(a => a.userId)
|
|
132
|
-
: [...c.assignees.map(a => a.userId), userId]
|
|
133
|
-
await kanban.syncCardAssignees(c.id, next)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function submitComment() {
|
|
137
|
-
const text = commentDraft().trim()
|
|
138
|
-
if (!text) return
|
|
139
|
-
const ok = await kanban.addComment(text)
|
|
140
|
-
if (ok) commentDraft.set('')
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function startCommentEdit(id, body) {
|
|
144
|
-
editingCommentId.set(id)
|
|
145
|
-
commentEditDraft.set(body)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function cancelCommentEdit() {
|
|
149
|
-
editingCommentId.set(null)
|
|
150
|
-
commentEditDraft.set('')
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function saveCommentEdit(id) {
|
|
154
|
-
const body = commentEditDraft().trim()
|
|
155
|
-
if (!body || savingCommentId()) return
|
|
156
|
-
savingCommentId.set(id)
|
|
157
|
-
try {
|
|
158
|
-
if (await kanban.updateComment(id, body))
|
|
159
|
-
cancelCommentEdit()
|
|
160
|
-
}
|
|
161
|
-
finally {
|
|
162
|
-
savingCommentId.set(null)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function handleCommentEditKeydown(event, id) {
|
|
167
|
-
if (event.key === 'Escape') {
|
|
168
|
-
event.preventDefault()
|
|
169
|
-
cancelCommentEdit()
|
|
170
|
-
}
|
|
171
|
-
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
|
172
|
-
event.preventDefault()
|
|
173
|
-
void saveCommentEdit(id)
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
59
|
function requestDelete(kind: DeleteKind, id: number, hasCards = false): void {
|
|
178
60
|
pendingDelete.set({ kind, id, hasCards })
|
|
179
61
|
}
|
|
180
62
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const c = openCard()
|
|
185
|
-
const boardId = c ? c.boardId : (board() ? board().id : null)
|
|
186
|
-
if (!boardId) return
|
|
187
|
-
const created = await kanban.createLabel({ boardId, name, color: newLabelColor() })
|
|
188
|
-
if (created && c)
|
|
189
|
-
await kanban.syncCardLabels(c.id, [...c.labels.map(l => l.id), created.id])
|
|
190
|
-
newLabelName.set('')
|
|
191
|
-
showNewLabelComposer.set(false)
|
|
63
|
+
function closeDelete(): void {
|
|
64
|
+
if (!deleting())
|
|
65
|
+
pendingDelete.set(null)
|
|
192
66
|
}
|
|
193
67
|
|
|
194
|
-
const labelColors = ['slate', 'red', 'amber', 'emerald', 'blue', 'violet', 'rose']
|
|
195
|
-
|
|
196
68
|
const deleteTitle = derived(() => {
|
|
197
69
|
const pending = pendingDelete()
|
|
198
70
|
if (!pending)
|
|
@@ -200,8 +72,6 @@
|
|
|
200
72
|
return {
|
|
201
73
|
card: 'Delete this card?',
|
|
202
74
|
column: 'Delete this column?',
|
|
203
|
-
comment: 'Delete this comment?',
|
|
204
|
-
label: 'Delete this label?',
|
|
205
75
|
}[pending.kind]
|
|
206
76
|
})
|
|
207
77
|
|
|
@@ -211,8 +81,6 @@
|
|
|
211
81
|
return ''
|
|
212
82
|
if (pending.kind === 'column' && pending.hasCards)
|
|
213
83
|
return 'Every card in this column will also be permanently deleted.'
|
|
214
|
-
if (pending.kind === 'label')
|
|
215
|
-
return 'The label will be removed from every card that currently uses it.'
|
|
216
84
|
return 'This action cannot be undone.'
|
|
217
85
|
})
|
|
218
86
|
|
|
@@ -224,11 +92,7 @@
|
|
|
224
92
|
try {
|
|
225
93
|
const ok = pending.kind === 'card'
|
|
226
94
|
? await kanban.deleteCard(pending.id)
|
|
227
|
-
: pending.
|
|
228
|
-
? await kanban.deleteColumn(pending.id)
|
|
229
|
-
: pending.kind === 'comment'
|
|
230
|
-
? await kanban.deleteComment(pending.id)
|
|
231
|
-
: await kanban.deleteLabel(pending.id)
|
|
95
|
+
: await kanban.deleteColumn(pending.id)
|
|
232
96
|
if (ok)
|
|
233
97
|
pendingDelete.set(null)
|
|
234
98
|
}
|
|
@@ -306,29 +170,12 @@
|
|
|
306
170
|
closeColumnComposer()
|
|
307
171
|
}
|
|
308
172
|
|
|
309
|
-
// ─── Card detail
|
|
173
|
+
// ─── Card detail launcher ─────────────────────────────────────────
|
|
310
174
|
|
|
311
175
|
function openDetail(cardId) {
|
|
312
176
|
kanban.loadUsers() // background-fetch for assignee picker
|
|
313
177
|
kanban.openCardDetail(cardId)
|
|
314
178
|
}
|
|
315
|
-
function closeDetail() { kanban.closeCardDetail() }
|
|
316
|
-
|
|
317
|
-
function handleBoardKeydown(event: KeyboardEvent): void {
|
|
318
|
-
if (event.key !== 'Escape' || pendingDelete())
|
|
319
|
-
return
|
|
320
|
-
if (openCard())
|
|
321
|
-
closeDetail()
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function handleCommentKeydown(event: KeyboardEvent): void {
|
|
325
|
-
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
|
326
|
-
event.preventDefault()
|
|
327
|
-
void submitComment()
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
useEventListener('keydown', handleBoardKeydown)
|
|
332
179
|
|
|
333
180
|
// Label color → tailwind hue. Mirrors the column-accent map but
|
|
334
181
|
// returns a more saturated chip class.
|
|
@@ -359,13 +206,6 @@
|
|
|
359
206
|
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
360
207
|
}
|
|
361
208
|
|
|
362
|
-
function formatTimestamp(iso) {
|
|
363
|
-
if (!iso) return ''
|
|
364
|
-
const d = new Date(iso)
|
|
365
|
-
if (Number.isNaN(d.getTime())) return ''
|
|
366
|
-
return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
|
|
367
|
-
}
|
|
368
|
-
|
|
369
209
|
// ─── Native drag and drop ─────────────────────────────────────────
|
|
370
210
|
|
|
371
211
|
function setDragTransfer(event: DragEvent, kind: 'card' | 'column', id: number): void {
|
|
@@ -687,282 +527,16 @@
|
|
|
687
527
|
</div>
|
|
688
528
|
</div>
|
|
689
529
|
|
|
690
|
-
|
|
691
|
-
<!-- Opens on card click. Fixed positioned with a backdrop; closes
|
|
692
|
-
on backdrop click + Escape. Sections (top → bottom):
|
|
693
|
-
· Title (inline-editable, blurs to commit)
|
|
694
|
-
· Description (textarea, blurs to commit)
|
|
695
|
-
· Sidebar: labels picker, assignees picker, due date, archive
|
|
696
|
-
· Comments thread + composer
|
|
697
|
-
All mutations route through the store, which fires optimistic
|
|
698
|
-
updates with rollback on failure. -->
|
|
699
|
-
<div
|
|
700
|
-
:if="openCard()"
|
|
701
|
-
class="flex fixed inset-0 overflow-y-auto z-50 items-start justify-center px-4 py-8 bg-black/50 dashboard-modal-layer"
|
|
702
|
-
@click="closeDetail()"
|
|
703
|
-
>
|
|
704
|
-
<div
|
|
705
|
-
@click.stop
|
|
706
|
-
role="dialog"
|
|
707
|
-
aria-modal="true"
|
|
708
|
-
aria-label="Card details"
|
|
709
|
-
tabindex="-1"
|
|
710
|
-
class="my-8 max-w-3xl w-full bg-white dark:bg-zinc-900 border border-zinc-200 rounded-lg dark:border-zinc-800 shadow-xl"
|
|
711
|
-
>
|
|
712
|
-
<!-- Header: title + close -->
|
|
713
|
-
<div class="flex gap-3 items-start px-6 py-4 border-b border-zinc-200 dark:border-zinc-800">
|
|
714
|
-
<input
|
|
715
|
-
x-model="titleDraft"
|
|
716
|
-
@blur="commitTitle()"
|
|
717
|
-
@keydown.enter.prevent="(e) => e.target.blur()"
|
|
718
|
-
class="flex-1 -mx-2 -my-1 px-2 py-1 font-semibold text-lg text-zinc-900 dark:text-zinc-100 bg-transparent focus:bg-zinc-50 dark:focus:bg-zinc-800 rounded focus:outline-none"
|
|
719
|
-
/>
|
|
720
|
-
<Button variant="ghost" size="sm" iconOnly ariaLabel="Close card details" @click="closeDetail()">
|
|
721
|
-
<span class="block h-5 w-5 i-hugeicons-cancel-01"></span>
|
|
722
|
-
</Button>
|
|
723
|
-
</div>
|
|
724
|
-
|
|
725
|
-
<div :if="loadingCard() && (!openCard() || openCard().comments.length === 0)" class="px-6 py-8 text-center text-sm text-zinc-500">
|
|
726
|
-
Loading…
|
|
727
|
-
</div>
|
|
728
|
-
|
|
729
|
-
<div :if="errorCard()" class="px-6 py-3 bg-red-50 dark:bg-red-950/30 border-b border-red-200 dark:border-red-900/40">
|
|
730
|
-
<p class="text-red-700 text-xs dark:text-red-300" :text="errorCard()"></p>
|
|
731
|
-
</div>
|
|
732
|
-
|
|
733
|
-
<div class="grid gap-6 grid-cols-1 md:grid-cols-3 px-6 py-5">
|
|
734
|
-
<!-- Main column: description + comments -->
|
|
735
|
-
<div class="md:col-span-2 space-y-6">
|
|
736
|
-
<!-- Description -->
|
|
737
|
-
<section>
|
|
738
|
-
<h4 class="mb-2 font-medium text-xs text-zinc-500 tracking-wide uppercase">Description</h4>
|
|
739
|
-
<textarea
|
|
740
|
-
x-model="descDraft"
|
|
741
|
-
@blur="commitDescription()"
|
|
742
|
-
rows="4"
|
|
743
|
-
placeholder="Add a more detailed description…"
|
|
744
|
-
class="px-3 py-2 w-full text-sm text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-y"
|
|
745
|
-
></textarea>
|
|
746
|
-
</section>
|
|
747
|
-
|
|
748
|
-
<!-- Comments -->
|
|
749
|
-
<section>
|
|
750
|
-
<h4 class="mb-2 font-medium text-xs text-zinc-500 tracking-wide uppercase">Activity</h4>
|
|
751
|
-
|
|
752
|
-
<!-- Composer -->
|
|
753
|
-
<div class="mb-4">
|
|
754
|
-
<textarea
|
|
755
|
-
x-model="commentDraft"
|
|
756
|
-
@keydown="handleCommentKeydown"
|
|
757
|
-
rows="2"
|
|
758
|
-
placeholder="Add a comment… (⌘+Enter to send)"
|
|
759
|
-
class="px-3 py-2 w-full text-sm text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-none"
|
|
760
|
-
></textarea>
|
|
761
|
-
<div class="flex justify-end mt-2">
|
|
762
|
-
<Button size="sm" :disabled="!commentDraft().trim()" @click="submitComment()">
|
|
763
|
-
Comment
|
|
764
|
-
</Button>
|
|
765
|
-
</div>
|
|
766
|
-
</div>
|
|
767
|
-
|
|
768
|
-
<!-- Thread -->
|
|
769
|
-
<div :if="openCard().comments.length === 0 && !loadingCard()" class="italic text-xs text-zinc-400">
|
|
770
|
-
No comments yet.
|
|
771
|
-
</div>
|
|
772
|
-
<ul class="space-y-3">
|
|
773
|
-
<li
|
|
774
|
-
:for="cm in openCard.comments"
|
|
775
|
-
class="flex gap-3"
|
|
776
|
-
>
|
|
777
|
-
<span class="inline-flex flex-shrink-0 items-center justify-center h-7 w-7 font-medium text-[10px] text-white bg-violet-500 rounded-full" :text="initialsOf(cm.authorName, cm.authorEmail)"></span>
|
|
778
|
-
<div class="flex-1 min-w-0">
|
|
779
|
-
<div class="flex gap-2 items-baseline">
|
|
780
|
-
<span class="font-medium text-xs text-zinc-700 dark:text-zinc-200" :text="cm.authorName || cm.authorEmail || 'Unknown'"></span>
|
|
781
|
-
<span class="text-[10px] text-zinc-400" :text="formatTimestamp(cm.createdAt)"></span>
|
|
782
|
-
<template :if="editingCommentId() !== cm.id">
|
|
783
|
-
<Button variant="ghost" size="xs" @click="startCommentEdit(cm.id, cm.body)">Edit</Button>
|
|
784
|
-
<Button variant="ghost" size="xs" @click="requestDelete('comment', cm.id)"><span class="text-red-500">Delete</span></Button>
|
|
785
|
-
</template>
|
|
786
|
-
</div>
|
|
787
|
-
<p :if="editingCommentId() !== cm.id" class="mt-0.5 text-xs text-zinc-700 whitespace-pre-wrap dark:text-zinc-300" :text="cm.body"></p>
|
|
788
|
-
<div :if="editingCommentId() === cm.id" class="mt-2">
|
|
789
|
-
<textarea
|
|
790
|
-
x-model="commentEditDraft"
|
|
791
|
-
@keydown="handleCommentEditKeydown($event, cm.id)"
|
|
792
|
-
rows="3"
|
|
793
|
-
maxlength="10000"
|
|
794
|
-
:aria-label="'Edit comment by ' + (cm.authorName || cm.authorEmail || 'Unknown')"
|
|
795
|
-
class="px-3 py-2 w-full text-sm text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none dark:focus:border-violet-600 resize-y"
|
|
796
|
-
></textarea>
|
|
797
|
-
<div class="flex gap-2 justify-end mt-2">
|
|
798
|
-
<Button variant="secondary" size="xs" :disabled="savingCommentId() === cm.id" @click="cancelCommentEdit()">Cancel</Button>
|
|
799
|
-
<Button size="xs" :loading="savingCommentId() === cm.id" :disabled="!commentEditDraft().trim()" @click="saveCommentEdit(cm.id)">Save comment</Button>
|
|
800
|
-
</div>
|
|
801
|
-
</div>
|
|
802
|
-
</div>
|
|
803
|
-
</li>
|
|
804
|
-
</ul>
|
|
805
|
-
</section>
|
|
806
|
-
</div>
|
|
807
|
-
|
|
808
|
-
<!-- Sidebar: labels, assignees, due date, archive -->
|
|
809
|
-
<aside class="space-y-5">
|
|
810
|
-
<!-- Labels -->
|
|
811
|
-
<section>
|
|
812
|
-
<div class="flex items-center justify-between mb-2">
|
|
813
|
-
<h4 class="font-medium text-xs text-zinc-500 tracking-wide uppercase">Labels</h4>
|
|
814
|
-
<Button
|
|
815
|
-
variant="ghost"
|
|
816
|
-
size="xs"
|
|
817
|
-
interaction="toggle"
|
|
818
|
-
:pressed="showLabelPicker()"
|
|
819
|
-
@click="showLabelPicker.set(!showLabelPicker())"
|
|
820
|
-
>
|
|
821
|
-
<span :if="!showLabelPicker()">Edit</span>
|
|
822
|
-
<span :if="showLabelPicker()">Done</span>
|
|
823
|
-
</Button>
|
|
824
|
-
</div>
|
|
825
|
-
|
|
826
|
-
<!-- Selected labels -->
|
|
827
|
-
<div :if="!showLabelPicker()" class="flex flex-wrap gap-1">
|
|
828
|
-
<span
|
|
829
|
-
:for="lbl in openCard.labels"
|
|
830
|
-
:class="`px-2 py-0.5 text-xs font-medium rounded ${labelChipClass(lbl.color)}`"
|
|
831
|
-
:text="lbl.name"
|
|
832
|
-
></span>
|
|
833
|
-
<span :if="openCard().labels.length === 0" class="italic text-xs text-zinc-400">None</span>
|
|
834
|
-
</div>
|
|
835
|
-
|
|
836
|
-
<!-- Picker -->
|
|
837
|
-
<div :if="showLabelPicker()" class="space-y-2">
|
|
838
|
-
<div
|
|
839
|
-
:for="lbl in allLabels"
|
|
840
|
-
:class="`flex items-center gap-1 px-1 py-0.5 w-full rounded ${labelChipClass(lbl.color)}`"
|
|
841
|
-
>
|
|
842
|
-
<button
|
|
843
|
-
type="button"
|
|
844
|
-
class="flex flex-1 items-center justify-between px-1 py-0.5 min-w-0 text-left text-xs rounded hover:opacity-90"
|
|
845
|
-
:aria-pressed="String(openCard().labels.some(l => l.id === lbl.id))"
|
|
846
|
-
@click="toggleLabel(lbl.id)"
|
|
847
|
-
>
|
|
848
|
-
<span class="truncate" :text="lbl.name"></span>
|
|
849
|
-
<span :if="openCard().labels.some(l => l.id === lbl.id)" class="h-3 w-3 i-hugeicons-tick-02"></span>
|
|
850
|
-
</button>
|
|
851
|
-
<Button variant="ghost" size="xs" iconOnly :ariaLabel="'Delete label ' + lbl.name" @click="requestDelete('label', lbl.id)">
|
|
852
|
-
<span class="block h-3 w-3 text-red-500 i-hugeicons-cancel-01"></span>
|
|
853
|
-
</Button>
|
|
854
|
-
</div>
|
|
855
|
-
|
|
856
|
-
<!-- New label composer -->
|
|
857
|
-
<div :if="showNewLabelComposer()" class="pt-2 space-y-2 border-t border-zinc-200 dark:border-zinc-800">
|
|
858
|
-
<input
|
|
859
|
-
x-model="newLabelName"
|
|
860
|
-
@keydown.enter.prevent="submitNewLabel()"
|
|
861
|
-
placeholder="Label name"
|
|
862
|
-
class="px-2 py-1 w-full text-xs bg-white dark:bg-zinc-950 border border-zinc-300 rounded dark:border-zinc-700 focus:border-violet-500 focus:outline-none"
|
|
863
|
-
/>
|
|
864
|
-
<div class="flex gap-1 items-center" role="group" aria-label="Label color">
|
|
865
|
-
<button
|
|
866
|
-
:for="c in labelColors"
|
|
867
|
-
type="button"
|
|
868
|
-
@click="newLabelColor.set(c)"
|
|
869
|
-
:class="`w-5 h-5 rounded ${labelChipClass(c)} ${newLabelColor() === c ? 'ring-2 ring-offset-1 ring-violet-500 dark:ring-offset-zinc-900' : ''}`"
|
|
870
|
-
:aria-label="'Use ' + c + ' label color'"
|
|
871
|
-
:aria-pressed="String(newLabelColor() === c)"
|
|
872
|
-
></button>
|
|
873
|
-
</div>
|
|
874
|
-
<div class="flex gap-1">
|
|
875
|
-
<Button size="xs" @click="submitNewLabel()">Create</Button>
|
|
876
|
-
<Button variant="ghost" size="xs" @click="showNewLabelComposer.set(false)">Cancel</Button>
|
|
877
|
-
</div>
|
|
878
|
-
</div>
|
|
879
|
-
<Button
|
|
880
|
-
:if="!showNewLabelComposer()"
|
|
881
|
-
variant="outline"
|
|
882
|
-
size="sm"
|
|
883
|
-
fullWidth
|
|
884
|
-
@click="showNewLabelComposer.set(true)"
|
|
885
|
-
>
|
|
886
|
-
+ New label
|
|
887
|
-
</Button>
|
|
888
|
-
</div>
|
|
889
|
-
</section>
|
|
890
|
-
|
|
891
|
-
<!-- Assignees -->
|
|
892
|
-
<section>
|
|
893
|
-
<div class="flex items-center justify-between mb-2">
|
|
894
|
-
<h4 class="font-medium text-xs text-zinc-500 tracking-wide uppercase">Assignees</h4>
|
|
895
|
-
<Button
|
|
896
|
-
variant="ghost"
|
|
897
|
-
size="xs"
|
|
898
|
-
interaction="toggle"
|
|
899
|
-
:pressed="showAssigneePicker()"
|
|
900
|
-
@click="showAssigneePicker.set(!showAssigneePicker())"
|
|
901
|
-
>
|
|
902
|
-
<span :if="!showAssigneePicker()">Edit</span>
|
|
903
|
-
<span :if="showAssigneePicker()">Done</span>
|
|
904
|
-
</Button>
|
|
905
|
-
</div>
|
|
906
|
-
|
|
907
|
-
<div :if="!showAssigneePicker()" class="flex flex-wrap gap-2">
|
|
908
|
-
<div
|
|
909
|
-
:for="asg in openCard.assignees"
|
|
910
|
-
class="flex gap-1.5 items-center text-xs text-zinc-700 dark:text-zinc-200"
|
|
911
|
-
>
|
|
912
|
-
<span class="inline-flex items-center justify-center h-6 w-6 font-medium text-[10px] text-white bg-violet-500 rounded-full" :text="initialsOf(asg.name, asg.email)"></span>
|
|
913
|
-
<span :text="asg.name || asg.email || 'Unknown'"></span>
|
|
914
|
-
</div>
|
|
915
|
-
<span :if="openCard().assignees.length === 0" class="italic text-xs text-zinc-400">Unassigned</span>
|
|
916
|
-
</div>
|
|
917
|
-
|
|
918
|
-
<div :if="showAssigneePicker()" class="overflow-y-auto space-y-1 max-h-48">
|
|
919
|
-
<p :if="kanban.users().length === 0" class="italic text-xs text-zinc-400">No users to assign.</p>
|
|
920
|
-
<button
|
|
921
|
-
:for="u in kanban.users"
|
|
922
|
-
type="button"
|
|
923
|
-
:aria-pressed="String(openCard().assignees.some(a => a.userId === u.id))"
|
|
924
|
-
@click="toggleAssignee(u.id)"
|
|
925
|
-
class="flex gap-2 items-center px-2 py-1.5 w-full text-left text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded"
|
|
926
|
-
>
|
|
927
|
-
<span class="inline-flex flex-shrink-0 items-center justify-center h-6 w-6 font-medium text-[10px] text-white bg-violet-500 rounded-full" :text="initialsOf(u.name, u.email)"></span>
|
|
928
|
-
<span class="flex-1 text-zinc-700 truncate dark:text-zinc-200" :text="u.name || u.email"></span>
|
|
929
|
-
<span :if="openCard().assignees.some(a => a.userId === u.id)" class="h-3.5 w-3.5 text-violet-500 i-hugeicons-tick-02"></span>
|
|
930
|
-
</button>
|
|
931
|
-
</div>
|
|
932
|
-
</section>
|
|
933
|
-
|
|
934
|
-
<!-- Due date -->
|
|
935
|
-
<section>
|
|
936
|
-
<h4 class="mb-2 font-medium text-xs text-zinc-500 tracking-wide uppercase">Due date</h4>
|
|
937
|
-
<input
|
|
938
|
-
type="date"
|
|
939
|
-
x-model="dueDateDraft"
|
|
940
|
-
@change="commitDueDate(dueDateDraft())"
|
|
941
|
-
class="px-2 py-1 w-full text-xs text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-950 border border-zinc-200 rounded dark:border-zinc-800 focus:border-violet-400 focus:outline-none"
|
|
942
|
-
/>
|
|
943
|
-
</section>
|
|
944
|
-
|
|
945
|
-
<!-- Meta -->
|
|
946
|
-
<section class="pt-3 border-t border-zinc-200 dark:border-zinc-800">
|
|
947
|
-
<p class="text-[10px] text-zinc-400">
|
|
948
|
-
Created <span :text="formatTimestamp(openCard().createdAt)"></span>
|
|
949
|
-
</p>
|
|
950
|
-
<p :if="openCard().updatedAt && openCard().updatedAt !== openCard().createdAt" class="text-[10px] text-zinc-400">
|
|
951
|
-
Updated <span :text="formatTimestamp(openCard().updatedAt)"></span>
|
|
952
|
-
</p>
|
|
953
|
-
</section>
|
|
954
|
-
</aside>
|
|
955
|
-
</div>
|
|
956
|
-
</div>
|
|
957
|
-
</div>
|
|
530
|
+
<KanbanCardDialog />
|
|
958
531
|
|
|
959
532
|
<ConfirmDialog
|
|
960
533
|
:isOpen="Boolean(pendingDelete())"
|
|
961
534
|
:title="deleteTitle()"
|
|
962
535
|
:description="deleteDescription()"
|
|
963
536
|
:busy="deleting()"
|
|
537
|
+
:error="error() || ''"
|
|
964
538
|
:confirmLabel="deleting() ? 'Deleting' : 'Delete'"
|
|
965
|
-
@close="
|
|
539
|
+
@close="closeDelete"
|
|
966
540
|
@confirm="confirmDelete"
|
|
967
541
|
/>
|
|
968
542
|
</div>
|