@kernhq/module-tracker 0.10.1 → 0.11.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/package.json +19 -6
- package/src/client/api-instance.ts +34 -0
- package/src/client/components/BoardCard.svelte +182 -0
- package/src/client/components/BoardView.svelte +248 -0
- package/src/client/components/CommentComposer.svelte +219 -0
- package/src/client/components/CommentThread.svelte +389 -0
- package/src/client/components/CustomField.svelte +333 -0
- package/src/client/components/DueDate.svelte +44 -0
- package/src/client/components/FilterMenu.svelte +91 -0
- package/src/client/components/GroupHeader.svelte +137 -0
- package/src/client/components/HomeLinks.svelte +38 -0
- package/src/client/components/IssueApprovals.svelte +217 -0
- package/src/client/components/IssueConnections.svelte +424 -0
- package/src/client/components/IssueDetailPanel.svelte +1337 -0
- package/src/client/components/IssueInline.svelte +75 -0
- package/src/client/components/IssueListView.svelte +153 -0
- package/src/client/components/IssuePicker.svelte +180 -0
- package/src/client/components/IssueRow.svelte +171 -0
- package/src/client/components/IssueTime.svelte +306 -0
- package/src/client/components/KqlInput.svelte +295 -0
- package/src/client/components/NewIssueDialog.svelte +210 -0
- package/src/client/components/NewProjectDialog.svelte +235 -0
- package/src/client/components/PriorityGlyph.svelte +52 -0
- package/src/client/components/SaveViewDialog.svelte +153 -0
- package/src/client/components/SidebarProjects.svelte +263 -0
- package/src/client/components/SidebarViews.svelte +336 -0
- package/src/client/components/StatusIcon.svelte +50 -0
- package/src/client/components/TrackerControls.svelte +109 -0
- package/src/client/components/TrackerSidebar.svelte +96 -0
- package/src/client/context.svelte.ts +105 -0
- package/src/client/core-api.ts +35 -0
- package/src/client/csv.test.ts +50 -0
- package/src/client/csv.ts +60 -0
- package/src/client/filters.ts +94 -0
- package/src/client/i18n.ts +3260 -0
- package/src/client/index.ts +12 -0
- package/src/client/labels.ts +200 -0
- package/src/client/mock.ts +2729 -0
- package/src/client/module.ts +491 -0
- package/src/client/nav.test.ts +59 -0
- package/src/client/nav.ts +84 -0
- package/src/client/pages/IntakePage.svelte +287 -0
- package/src/client/pages/IssuesPage.svelte +701 -0
- package/src/client/pages/ProjectPage.svelte +29 -0
- package/src/client/pages/ReportsPage.svelte +365 -0
- package/src/client/permissions.ts +33 -0
- package/src/client/planning/PlanningSections.svelte +259 -0
- package/src/client/project/ComponentsPage.svelte +423 -0
- package/src/client/project/CyclesPage.svelte +488 -0
- package/src/client/project/MilestonesPage.svelte +342 -0
- package/src/client/project/PlanCard.svelte +191 -0
- package/src/client/project/ProjectShell.svelte +93 -0
- package/src/client/project/TemplatesPage.svelte +321 -0
- package/src/client/project/context.svelte.ts +58 -0
- package/src/client/query.ts +50 -0
- package/src/client/recurrence.test.ts +37 -0
- package/src/client/recurrence.ts +89 -0
- package/src/client/richtext.ts +155 -0
- package/src/client/settings/CycleList.svelte +422 -0
- package/src/client/settings/FieldEditor.svelte +324 -0
- package/src/client/settings/FieldsSettings.svelte +273 -0
- package/src/client/settings/ImportSettings.svelte +410 -0
- package/src/client/settings/LayoutEditor.svelte +263 -0
- package/src/client/settings/PlanningList.svelte +249 -0
- package/src/client/settings/PlanningSettings.svelte +179 -0
- package/src/client/settings/ProjectsSettings.svelte +462 -0
- package/src/client/settings/RepeatingSettings.svelte +293 -0
- package/src/client/settings/TypeEditor.svelte +214 -0
- package/src/client/settings/TypesSettings.svelte +384 -0
- package/src/client/settings/WorkflowsSettings.svelte +645 -0
- package/src/client/settings/mutations.ts +19 -0
- package/src/client/time.test.ts +39 -0
- package/src/client/time.ts +34 -0
- package/src/client/tracker.test.ts +399 -0
- package/src/client/views.ts +27 -0
- package/src/client/widgets/AssignedCountWidget.svelte +14 -0
- package/src/client/widgets/CountWidget.svelte +56 -0
- package/src/client/widgets/CycleWidget.svelte +85 -0
- package/src/client/widgets/DueSoonCountWidget.svelte +14 -0
- package/src/client/widgets/IssuesWidget.svelte +222 -0
- package/src/client/widgets/ThroughputWidget.svelte +68 -0
- package/src/client/widgets/TimerWidget.svelte +116 -0
- package/src/client/widgets/VelocityWidget.svelte +55 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
Button,
|
|
5
|
+
Checkbox,
|
|
6
|
+
coreApi,
|
|
7
|
+
Input,
|
|
8
|
+
keys,
|
|
9
|
+
navigation,
|
|
10
|
+
Select,
|
|
11
|
+
SettingsPage,
|
|
12
|
+
SettingsSection,
|
|
13
|
+
Spinner,
|
|
14
|
+
session,
|
|
15
|
+
Textarea,
|
|
16
|
+
toast,
|
|
17
|
+
} from '@kernhq/ui'
|
|
18
|
+
import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query'
|
|
19
|
+
import { getTrackerApi } from '../api-instance.js'
|
|
20
|
+
import type { CoreApi } from '../core-api.js'
|
|
21
|
+
import { t } from '../i18n.js'
|
|
22
|
+
import type { Project } from '../index.js'
|
|
23
|
+
import { canTracker } from '../permissions.js'
|
|
24
|
+
import { trackerKeys } from '../query.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Running a project after it has been created.
|
|
28
|
+
*
|
|
29
|
+
* A project could be created and never touched again: `projects.update`, `archive` and `delete` all
|
|
30
|
+
* had a server and nothing that called it, so a typo in a name outlived the project and a finished
|
|
31
|
+
* project stayed in every menu forever.
|
|
32
|
+
*
|
|
33
|
+
* The key is deliberately not editable. It is in every issue reference (`KRN-12`), every link
|
|
34
|
+
* anybody has pasted, and every saved query — renaming it would break all of them at once, and the
|
|
35
|
+
* server does not offer it either.
|
|
36
|
+
*/
|
|
37
|
+
const api = getTrackerApi()
|
|
38
|
+
const core = coreApi<CoreApi>()
|
|
39
|
+
const queryClient = useQueryClient()
|
|
40
|
+
|
|
41
|
+
const slug = $derived(navigation.workspaceSlug)
|
|
42
|
+
const workspaceId = $derived(session.workspaces.find((w) => w.slug === slug)?.id ?? '')
|
|
43
|
+
const canManage = $derived(canTracker('projectManage'))
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Which project the page is on, and how a link can say so.
|
|
47
|
+
*
|
|
48
|
+
* `?project=<id>` is what the sidebar's project menu links to — landing on the project you clicked
|
|
49
|
+
* rather than on whichever one happens to be first. An id for a project this workspace does not
|
|
50
|
+
* have is ignored rather than obeyed, so a stale link opens the page instead of an empty one.
|
|
51
|
+
*/
|
|
52
|
+
let selectedId = $state<string | null>(null)
|
|
53
|
+
const asked = $derived(navigation.search.project)
|
|
54
|
+
let showArchived = $state(false)
|
|
55
|
+
/** `null` means "no unsaved change", which is what disables Save. */
|
|
56
|
+
let draft = $state<Record<string, unknown> | null>(null)
|
|
57
|
+
let confirmDelete = $state(false)
|
|
58
|
+
let deleteKey = $state('')
|
|
59
|
+
|
|
60
|
+
const projectsQuery = createQuery(() => ({
|
|
61
|
+
queryKey: [...trackerKeys.projects(workspaceId), showArchived],
|
|
62
|
+
queryFn: () => api.projects.list({ workspaceId, includeArchived: showArchived }),
|
|
63
|
+
enabled: Boolean(workspaceId),
|
|
64
|
+
}))
|
|
65
|
+
const projects = $derived(projectsQuery.data ?? [])
|
|
66
|
+
const project = $derived(projects.find((p) => p.id === (selectedId ?? asked)) ?? projects[0] ?? null)
|
|
67
|
+
|
|
68
|
+
/** What the form shows: the unsaved value where there is one, the stored value otherwise. */
|
|
69
|
+
const field = <K extends keyof Project>(key: K): Project[K] =>
|
|
70
|
+
(draft && key in draft ? (draft[key] as Project[K]) : project?.[key]) as Project[K]
|
|
71
|
+
const edit = (patch: Record<string, unknown>) => {
|
|
72
|
+
draft = { ...(draft ?? {}), ...patch }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const settings = $derived(project?.settings)
|
|
76
|
+
const cycleSettings = $derived(
|
|
77
|
+
(draft?.settings as Project['settings'] | undefined)?.cycles ?? settings?.cycles,
|
|
78
|
+
)
|
|
79
|
+
const editSettings = (patch: Record<string, unknown>) => {
|
|
80
|
+
const base = (draft?.settings as Record<string, unknown> | undefined) ?? {}
|
|
81
|
+
edit({ settings: { ...base, ...patch } })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const refresh = () => queryClient.invalidateQueries({ queryKey: ['tracker', 'project'] })
|
|
85
|
+
|
|
86
|
+
const save = createMutation(() => ({
|
|
87
|
+
mutationFn: (patch: Record<string, unknown>) =>
|
|
88
|
+
api.projects.update({ workspaceId, projectId: project?.id as string, patch }),
|
|
89
|
+
onSuccess: () => {
|
|
90
|
+
draft = null
|
|
91
|
+
void refresh()
|
|
92
|
+
toast.success(t('project_saved'))
|
|
93
|
+
},
|
|
94
|
+
onError: (error: Error) => toast.error(error.message),
|
|
95
|
+
}))
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Archived, not deleted: the issues stay, the history stays, and the project comes off the menus.
|
|
99
|
+
* It is what a finished project wants, and it is reversible, which deletion is not.
|
|
100
|
+
*/
|
|
101
|
+
const archive = createMutation(() => ({
|
|
102
|
+
mutationFn: (input: { projectId: string; archived: boolean }) =>
|
|
103
|
+
api.projects.archive({ workspaceId, ...input }),
|
|
104
|
+
onSuccess: () => void refresh(),
|
|
105
|
+
onError: (error: Error) => toast.error(error.message),
|
|
106
|
+
}))
|
|
107
|
+
|
|
108
|
+
const remove = createMutation(() => ({
|
|
109
|
+
mutationFn: (projectId: string) => api.projects.delete({ workspaceId, projectId }),
|
|
110
|
+
onSuccess: () => {
|
|
111
|
+
confirmDelete = false
|
|
112
|
+
deleteKey = ''
|
|
113
|
+
selectedId = null
|
|
114
|
+
void refresh()
|
|
115
|
+
toast.success(t('project_deleted'))
|
|
116
|
+
},
|
|
117
|
+
onError: (error: Error) => toast.error(error.message),
|
|
118
|
+
}))
|
|
119
|
+
|
|
120
|
+
/** Switching project abandons an unsaved edit rather than carrying it to another project. */
|
|
121
|
+
const select = (id: string) => {
|
|
122
|
+
selectedId = id
|
|
123
|
+
draft = null
|
|
124
|
+
confirmDelete = false
|
|
125
|
+
deleteKey = ''
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The lead is a workspace member, which is core's list rather than the tracker's. */
|
|
129
|
+
const membersQuery = createQuery(() => ({
|
|
130
|
+
queryKey: keys.members(workspaceId),
|
|
131
|
+
queryFn: () => core.workspaces.members.list({ workspaceId, limit: 200 }),
|
|
132
|
+
enabled: Boolean(workspaceId),
|
|
133
|
+
}))
|
|
134
|
+
const leadOptions = $derived([
|
|
135
|
+
{ value: '', label: t('project_no_lead') },
|
|
136
|
+
...(membersQuery.data?.items ?? []).map((entry) => ({
|
|
137
|
+
value: entry.user.id,
|
|
138
|
+
label: entry.user.name ?? entry.user.email,
|
|
139
|
+
})),
|
|
140
|
+
])
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<SettingsPage title={t('settings_projects')} description={t('settings_projects_hint')}>
|
|
144
|
+
{#if projectsQuery.isPending}
|
|
145
|
+
<SettingsSection><div class="state"><Spinner /></div></SettingsSection>
|
|
146
|
+
{:else if !projects.length}
|
|
147
|
+
<SettingsSection>
|
|
148
|
+
<p class="state">{t('settings_planning_no_projects')}</p>
|
|
149
|
+
</SettingsSection>
|
|
150
|
+
{:else}
|
|
151
|
+
<SettingsSection flush>
|
|
152
|
+
<ul class="plist" data-testid="project-list">
|
|
153
|
+
{#each projects as p (p.id)}
|
|
154
|
+
<li class:on={project?.id === p.id}>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
class="prow"
|
|
158
|
+
onclick={() => select(p.id)}
|
|
159
|
+
data-testid="project-row"
|
|
160
|
+
data-project-key={p.key}
|
|
161
|
+
>
|
|
162
|
+
<span class="pkey">{p.key}</span>
|
|
163
|
+
<span class="pname">{p.name}</span>
|
|
164
|
+
{#if p.archivedAt}<Badge tone="grey">{t('project_archived')}</Badge>{/if}
|
|
165
|
+
<span class="pnote">{t('project_open_count', { count: p.openIssueCount })}</span>
|
|
166
|
+
</button>
|
|
167
|
+
</li>
|
|
168
|
+
{/each}
|
|
169
|
+
</ul>
|
|
170
|
+
|
|
171
|
+
{#snippet footer()}
|
|
172
|
+
<Checkbox
|
|
173
|
+
checked={showArchived}
|
|
174
|
+
label={t('project_show_archived')}
|
|
175
|
+
onCheckedChange={(on: boolean) => (showArchived = on)}
|
|
176
|
+
/>
|
|
177
|
+
{/snippet}
|
|
178
|
+
</SettingsSection>
|
|
179
|
+
|
|
180
|
+
{#if project}
|
|
181
|
+
<SettingsSection title={t('project_identity')} description={t('project_key_fixed')}>
|
|
182
|
+
<div class="grid">
|
|
183
|
+
<label class="row">
|
|
184
|
+
<span class="lbl">{t('project_name')}</span>
|
|
185
|
+
<Input
|
|
186
|
+
value={field('name') ?? ''}
|
|
187
|
+
disabled={!canManage}
|
|
188
|
+
oninput={(e: Event) => edit({ name: (e.currentTarget as HTMLInputElement).value })}
|
|
189
|
+
data-testid="project-name"
|
|
190
|
+
/>
|
|
191
|
+
</label>
|
|
192
|
+
<label class="row">
|
|
193
|
+
<span class="lbl">{t('project_description')}</span>
|
|
194
|
+
<Textarea
|
|
195
|
+
value={field('description') ?? ''}
|
|
196
|
+
rows={2}
|
|
197
|
+
disabled={!canManage}
|
|
198
|
+
oninput={(e: Event) =>
|
|
199
|
+
edit({ description: (e.currentTarget as HTMLTextAreaElement).value || null })}
|
|
200
|
+
/>
|
|
201
|
+
</label>
|
|
202
|
+
<label class="row">
|
|
203
|
+
<span class="lbl">{t('project_lead')}</span>
|
|
204
|
+
<Select
|
|
205
|
+
value={(field('leadId') as string) ?? ''}
|
|
206
|
+
options={leadOptions}
|
|
207
|
+
disabled={!canManage}
|
|
208
|
+
onValueChange={(v: string) => edit({ leadId: v || null })}
|
|
209
|
+
/>
|
|
210
|
+
</label>
|
|
211
|
+
<label class="row">
|
|
212
|
+
<span class="lbl">{t('project_visibility')}</span>
|
|
213
|
+
<Select
|
|
214
|
+
value={field('visibility') ?? 'workspace'}
|
|
215
|
+
options={[
|
|
216
|
+
{ value: 'workspace', label: t('project_visible_workspace') },
|
|
217
|
+
{ value: 'private', label: t('project_visible_private') },
|
|
218
|
+
]}
|
|
219
|
+
disabled={!canManage}
|
|
220
|
+
onValueChange={(v: string) => edit({ visibility: v })}
|
|
221
|
+
/>
|
|
222
|
+
</label>
|
|
223
|
+
<label class="row">
|
|
224
|
+
<span class="lbl">{t('project_default_assignee')}</span>
|
|
225
|
+
<Select
|
|
226
|
+
value={field('defaultAssignee') ?? 'unassigned'}
|
|
227
|
+
options={[
|
|
228
|
+
{ value: 'unassigned', label: t('project_assignee_nobody') },
|
|
229
|
+
{ value: 'lead', label: t('project_assignee_lead') },
|
|
230
|
+
]}
|
|
231
|
+
disabled={!canManage}
|
|
232
|
+
onValueChange={(v: string) => edit({ defaultAssignee: v })}
|
|
233
|
+
/>
|
|
234
|
+
</label>
|
|
235
|
+
</div>
|
|
236
|
+
</SettingsSection>
|
|
237
|
+
|
|
238
|
+
<SettingsSection title={t('project_behaviour')} description={t('project_behaviour_hint')}>
|
|
239
|
+
<div class="grid">
|
|
240
|
+
<label class="row">
|
|
241
|
+
<span class="lbl">{t('project_estimation')}</span>
|
|
242
|
+
<Select
|
|
243
|
+
value={(draft?.settings as Project['settings'] | undefined)?.estimation ??
|
|
244
|
+
settings?.estimation ??
|
|
245
|
+
'points'}
|
|
246
|
+
options={[
|
|
247
|
+
{ value: 'points', label: t('estimate_points') },
|
|
248
|
+
{ value: 'hours', label: t('estimate_hours') },
|
|
249
|
+
{ value: 'none', label: t('estimate_none') },
|
|
250
|
+
]}
|
|
251
|
+
disabled={!canManage}
|
|
252
|
+
onValueChange={(v: string) => editSettings({ estimation: v })}
|
|
253
|
+
/>
|
|
254
|
+
</label>
|
|
255
|
+
|
|
256
|
+
<!-- Triage is what the request link feeds: without it, anything a stranger sends lands
|
|
257
|
+
straight in the backlog with no one having looked at it. -->
|
|
258
|
+
<Checkbox
|
|
259
|
+
checked={(draft?.settings as Project['settings'] | undefined)?.triage?.enabled ??
|
|
260
|
+
settings?.triage.enabled ??
|
|
261
|
+
false}
|
|
262
|
+
label={t('project_triage')}
|
|
263
|
+
disabled={!canManage}
|
|
264
|
+
onCheckedChange={(on: boolean) => editSettings({ triage: { enabled: on } })}
|
|
265
|
+
/>
|
|
266
|
+
|
|
267
|
+
<span data-testid="project-cycles">
|
|
268
|
+
<Checkbox
|
|
269
|
+
checked={cycleSettings?.enabled ?? false}
|
|
270
|
+
label={t('project_cycles')}
|
|
271
|
+
disabled={!canManage}
|
|
272
|
+
onCheckedChange={(on: boolean) =>
|
|
273
|
+
editSettings({ cycles: { ...cycleSettings, enabled: on } })}
|
|
274
|
+
/>
|
|
275
|
+
</span>
|
|
276
|
+
{#if cycleSettings?.enabled}
|
|
277
|
+
<label class="row">
|
|
278
|
+
<span class="lbl">{t('project_cycle_length')}</span>
|
|
279
|
+
<Select
|
|
280
|
+
value={String(cycleSettings.lengthWeeks)}
|
|
281
|
+
options={[1, 2, 3, 4].map((n) => ({
|
|
282
|
+
value: String(n),
|
|
283
|
+
label: t('project_weeks', { count: n }),
|
|
284
|
+
}))}
|
|
285
|
+
disabled={!canManage}
|
|
286
|
+
onValueChange={(v: string) =>
|
|
287
|
+
editSettings({ cycles: { ...cycleSettings, lengthWeeks: Number(v) } })}
|
|
288
|
+
/>
|
|
289
|
+
</label>
|
|
290
|
+
{/if}
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
{#snippet footer()}
|
|
294
|
+
<Button variant="ghost" size="sm" onclick={() => (draft = null)} disabled={!draft}>
|
|
295
|
+
{t('common.discard')}
|
|
296
|
+
</Button>
|
|
297
|
+
<Button
|
|
298
|
+
size="sm"
|
|
299
|
+
disabled={!draft || !canManage}
|
|
300
|
+
loading={save.isPending}
|
|
301
|
+
onclick={() => draft && save.mutate(draft)}
|
|
302
|
+
data-testid="project-save"
|
|
303
|
+
>
|
|
304
|
+
{t('common.save')}
|
|
305
|
+
</Button>
|
|
306
|
+
{/snippet}
|
|
307
|
+
</SettingsSection>
|
|
308
|
+
|
|
309
|
+
<SettingsSection title={t('project_lifecycle')} description={t('project_lifecycle_hint')}>
|
|
310
|
+
<div class="lifecycle">
|
|
311
|
+
{#if project.archivedAt}
|
|
312
|
+
<Button
|
|
313
|
+
size="sm"
|
|
314
|
+
variant="ghost"
|
|
315
|
+
disabled={!canManage}
|
|
316
|
+
onclick={() => archive.mutate({ projectId: project.id, archived: false })}
|
|
317
|
+
data-testid="project-restore"
|
|
318
|
+
>
|
|
319
|
+
{t('project_restore')}
|
|
320
|
+
</Button>
|
|
321
|
+
{:else}
|
|
322
|
+
<Button
|
|
323
|
+
size="sm"
|
|
324
|
+
variant="ghost"
|
|
325
|
+
disabled={!canManage}
|
|
326
|
+
onclick={() => archive.mutate({ projectId: project.id, archived: true })}
|
|
327
|
+
data-testid="project-archive"
|
|
328
|
+
>
|
|
329
|
+
{t('common.archive')}
|
|
330
|
+
</Button>
|
|
331
|
+
{/if}
|
|
332
|
+
|
|
333
|
+
{#if confirmDelete}
|
|
334
|
+
<!-- Typing the key, not a yes/no: this takes every issue, comment and attachment with
|
|
335
|
+
it and there is no undo. The one control in the tracker that asks for proof. -->
|
|
336
|
+
<div class="danger" role="alertdialog" aria-label={t('project_delete_body')}>
|
|
337
|
+
<p>{t('project_delete_body')}</p>
|
|
338
|
+
<p class="ask">{t('project_delete_confirm', { key: project.key })}</p>
|
|
339
|
+
<div class="row">
|
|
340
|
+
<Input bind:value={deleteKey} placeholder={project.key} data-testid="project-delete-key" />
|
|
341
|
+
<Button
|
|
342
|
+
size="sm"
|
|
343
|
+
variant="danger"
|
|
344
|
+
disabled={deleteKey !== project.key}
|
|
345
|
+
loading={remove.isPending}
|
|
346
|
+
onclick={() => remove.mutate(project.id)}
|
|
347
|
+
data-testid="project-delete-confirm"
|
|
348
|
+
>
|
|
349
|
+
{t('common.delete')}
|
|
350
|
+
</Button>
|
|
351
|
+
<Button size="sm" variant="ghost" onclick={() => (confirmDelete = false)}>
|
|
352
|
+
{t('common.cancel')}
|
|
353
|
+
</Button>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
{:else}
|
|
357
|
+
<Button
|
|
358
|
+
size="sm"
|
|
359
|
+
variant="danger"
|
|
360
|
+
disabled={!canManage}
|
|
361
|
+
onclick={() => (confirmDelete = true)}
|
|
362
|
+
data-testid="project-delete"
|
|
363
|
+
>
|
|
364
|
+
{t('common.delete')}
|
|
365
|
+
</Button>
|
|
366
|
+
{/if}
|
|
367
|
+
</div>
|
|
368
|
+
</SettingsSection>
|
|
369
|
+
{/if}
|
|
370
|
+
{/if}
|
|
371
|
+
</SettingsPage>
|
|
372
|
+
|
|
373
|
+
<style>
|
|
374
|
+
.state {
|
|
375
|
+
display: grid;
|
|
376
|
+
place-items: center;
|
|
377
|
+
padding: 24px;
|
|
378
|
+
font-size: 13px;
|
|
379
|
+
}
|
|
380
|
+
.plist {
|
|
381
|
+
list-style: none;
|
|
382
|
+
margin: 0;
|
|
383
|
+
padding: 0;
|
|
384
|
+
}
|
|
385
|
+
.plist li {
|
|
386
|
+
border-bottom: 1px solid var(--kern-border-hairline);
|
|
387
|
+
}
|
|
388
|
+
.plist li:last-child {
|
|
389
|
+
border-bottom: 0;
|
|
390
|
+
}
|
|
391
|
+
.plist li.on {
|
|
392
|
+
background: var(--kern-surface-active);
|
|
393
|
+
}
|
|
394
|
+
.prow {
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
gap: 10px;
|
|
398
|
+
width: 100%;
|
|
399
|
+
padding: 10px 18px;
|
|
400
|
+
border: 0;
|
|
401
|
+
background: none;
|
|
402
|
+
color: inherit;
|
|
403
|
+
font: inherit;
|
|
404
|
+
text-align: start;
|
|
405
|
+
cursor: pointer;
|
|
406
|
+
}
|
|
407
|
+
.prow:hover {
|
|
408
|
+
background: var(--kern-surface-hover);
|
|
409
|
+
}
|
|
410
|
+
.pkey {
|
|
411
|
+
min-width: 44px;
|
|
412
|
+
font-family: var(--kern-font-mono);
|
|
413
|
+
font-size: 11.5px;
|
|
414
|
+
color: var(--kern-ink-400);
|
|
415
|
+
}
|
|
416
|
+
.pname {
|
|
417
|
+
flex: 1;
|
|
418
|
+
font-size: 13px;
|
|
419
|
+
color: var(--kern-ink-800);
|
|
420
|
+
}
|
|
421
|
+
.pnote {
|
|
422
|
+
font-size: 12px;
|
|
423
|
+
color: var(--kern-ink-400);
|
|
424
|
+
}
|
|
425
|
+
.grid {
|
|
426
|
+
display: grid;
|
|
427
|
+
gap: 12px;
|
|
428
|
+
}
|
|
429
|
+
.row {
|
|
430
|
+
display: grid;
|
|
431
|
+
gap: 4px;
|
|
432
|
+
}
|
|
433
|
+
.lbl {
|
|
434
|
+
font-size: 12px;
|
|
435
|
+
font-weight: 500;
|
|
436
|
+
color: var(--kern-ink-600);
|
|
437
|
+
}
|
|
438
|
+
.lifecycle {
|
|
439
|
+
display: flex;
|
|
440
|
+
flex-direction: column;
|
|
441
|
+
gap: 10px;
|
|
442
|
+
align-items: flex-start;
|
|
443
|
+
}
|
|
444
|
+
.danger {
|
|
445
|
+
width: 100%;
|
|
446
|
+
padding: 10px 12px;
|
|
447
|
+
border: 1px solid var(--kern-danger);
|
|
448
|
+
border-radius: var(--kern-r-sm);
|
|
449
|
+
font-size: 12.5px;
|
|
450
|
+
}
|
|
451
|
+
.danger p {
|
|
452
|
+
margin: 0 0 6px;
|
|
453
|
+
}
|
|
454
|
+
.danger .ask {
|
|
455
|
+
color: var(--kern-ink-600);
|
|
456
|
+
}
|
|
457
|
+
.danger .row {
|
|
458
|
+
display: flex;
|
|
459
|
+
align-items: center;
|
|
460
|
+
gap: 8px;
|
|
461
|
+
}
|
|
462
|
+
</style>
|