@kernhq/module-tracker 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -3
- 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/rank.test.ts +78 -0
- package/src/client/rank.ts +13 -4
- 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,423 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Avatar,
|
|
4
|
+
Button,
|
|
5
|
+
coreApi,
|
|
6
|
+
Dialog,
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
EmptyState,
|
|
9
|
+
IconButton,
|
|
10
|
+
Input,
|
|
11
|
+
keys,
|
|
12
|
+
type MenuItem,
|
|
13
|
+
Select,
|
|
14
|
+
Textarea,
|
|
15
|
+
toast,
|
|
16
|
+
} from '@kernhq/ui'
|
|
17
|
+
import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query'
|
|
18
|
+
import { getTrackerApi } from '../api-instance.js'
|
|
19
|
+
import type { CoreApi } from '../core-api.js'
|
|
20
|
+
import { t } from '../i18n.js'
|
|
21
|
+
import type { Component } from '../index.js'
|
|
22
|
+
import { projectKql, trackerHref } from '../nav.js'
|
|
23
|
+
import { canTracker } from '../permissions.js'
|
|
24
|
+
import { trackerKeys } from '../query.js'
|
|
25
|
+
import { useRouteProject } from './context.svelte.js'
|
|
26
|
+
import ProjectShell from './ProjectShell.svelte'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A project's components: the parts of the thing being built.
|
|
30
|
+
*
|
|
31
|
+
* A row per part, with who looks after it and how much work is filed against it — and the row is a
|
|
32
|
+
* link into that work, because a component is only ever interesting as the issues in it. New work
|
|
33
|
+
* given a component can be assigned to its lead by default, which is the whole reason a component
|
|
34
|
+
* has one; the settings screen could set neither.
|
|
35
|
+
*/
|
|
36
|
+
const api = getTrackerApi()
|
|
37
|
+
const core = coreApi<CoreApi>()
|
|
38
|
+
const queryClient = useQueryClient()
|
|
39
|
+
|
|
40
|
+
const at = useRouteProject()
|
|
41
|
+
const slug = $derived(at.slug)
|
|
42
|
+
const workspaceId = $derived(at.workspaceId)
|
|
43
|
+
const projectId = $derived(at.projectId)
|
|
44
|
+
const canManage = $derived(canTracker('projectManage'))
|
|
45
|
+
|
|
46
|
+
const componentsQuery = createQuery(() => ({
|
|
47
|
+
queryKey: trackerKeys.components(workspaceId, projectId),
|
|
48
|
+
queryFn: () => api.components.list({ workspaceId, projectId }),
|
|
49
|
+
enabled: Boolean(projectId),
|
|
50
|
+
}))
|
|
51
|
+
const components = $derived(componentsQuery.data ?? [])
|
|
52
|
+
|
|
53
|
+
const membersQuery = createQuery(() => ({
|
|
54
|
+
queryKey: keys.members(workspaceId),
|
|
55
|
+
queryFn: () => core.workspaces.members.list({ workspaceId }),
|
|
56
|
+
enabled: Boolean(workspaceId),
|
|
57
|
+
}))
|
|
58
|
+
/** `members.list` is paged; the picker wants people, not the envelope. */
|
|
59
|
+
const members = $derived(
|
|
60
|
+
(membersQuery.data?.items ?? []).map((member) => ({
|
|
61
|
+
id: member.user.id,
|
|
62
|
+
name: member.user.name,
|
|
63
|
+
avatarUrl: member.user.avatarUrl,
|
|
64
|
+
})),
|
|
65
|
+
)
|
|
66
|
+
const memberOf = (id: string | null) => members.find((member) => member.id === id) ?? null
|
|
67
|
+
|
|
68
|
+
const invalidate = () => {
|
|
69
|
+
void queryClient.invalidateQueries({ queryKey: ['tracker', 'component'] })
|
|
70
|
+
void queryClient.invalidateQueries({ queryKey: ['tracker', 'issue'] })
|
|
71
|
+
}
|
|
72
|
+
const fail = (error: Error) => toast.error(error.message)
|
|
73
|
+
|
|
74
|
+
let editing = $state<Component | null>(null)
|
|
75
|
+
let creating = $state(false)
|
|
76
|
+
let draftName = $state('')
|
|
77
|
+
let draftDescription = $state('')
|
|
78
|
+
let draftLead = $state('')
|
|
79
|
+
let draftAssignee = $state<'none' | 'lead' | 'project'>('none')
|
|
80
|
+
/** Named on screen before it happens, never straight off a menu. */
|
|
81
|
+
let confirming = $state<Component | null>(null)
|
|
82
|
+
|
|
83
|
+
const open = (component: Component | null) => {
|
|
84
|
+
editing = component
|
|
85
|
+
creating = component === null
|
|
86
|
+
draftName = component?.name ?? ''
|
|
87
|
+
draftDescription = component?.description ?? ''
|
|
88
|
+
draftLead = component?.leadId ?? ''
|
|
89
|
+
draftAssignee = component?.defaultAssignee ?? 'none'
|
|
90
|
+
}
|
|
91
|
+
const close = () => {
|
|
92
|
+
editing = null
|
|
93
|
+
creating = false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const save = createMutation(() => ({
|
|
97
|
+
mutationFn: () => {
|
|
98
|
+
const patch = {
|
|
99
|
+
name: draftName.trim(),
|
|
100
|
+
description: draftDescription.trim() || null,
|
|
101
|
+
leadId: draftLead || null,
|
|
102
|
+
defaultAssignee: draftAssignee,
|
|
103
|
+
}
|
|
104
|
+
return editing
|
|
105
|
+
? api.components.update({ workspaceId, id: editing.id, patch })
|
|
106
|
+
: api.components.create({ workspaceId, projectId, ...patch })
|
|
107
|
+
},
|
|
108
|
+
onSuccess: () => {
|
|
109
|
+
close()
|
|
110
|
+
invalidate()
|
|
111
|
+
},
|
|
112
|
+
onError: fail,
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
const remove = createMutation(() => ({
|
|
116
|
+
mutationFn: (id: string) => api.components.delete({ workspaceId, id }),
|
|
117
|
+
onSuccess: () => {
|
|
118
|
+
confirming = null
|
|
119
|
+
invalidate()
|
|
120
|
+
},
|
|
121
|
+
onError: fail,
|
|
122
|
+
}))
|
|
123
|
+
|
|
124
|
+
/** The work filed against one component, as the query the issue list already understands. */
|
|
125
|
+
const issuesHref = (component: Component) =>
|
|
126
|
+
trackerHref(slug, { q: `${projectKql(projectId)} and component = ${JSON.stringify(component.id)}` })
|
|
127
|
+
|
|
128
|
+
const assigneeOptions = $derived([
|
|
129
|
+
{ value: 'none', label: t('project_assignee_nobody') },
|
|
130
|
+
{ value: 'lead', label: t('component_assignee_lead') },
|
|
131
|
+
{ value: 'project', label: t('component_assignee_project') },
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
const menuFor = (component: Component): MenuItem[] => [
|
|
135
|
+
{
|
|
136
|
+
type: 'item',
|
|
137
|
+
id: 'issues',
|
|
138
|
+
label: t('component_issues'),
|
|
139
|
+
icon: 'list',
|
|
140
|
+
href: issuesHref(component),
|
|
141
|
+
},
|
|
142
|
+
...(canManage
|
|
143
|
+
? [
|
|
144
|
+
{ type: 'separator' as const },
|
|
145
|
+
{
|
|
146
|
+
type: 'item' as const,
|
|
147
|
+
id: 'edit',
|
|
148
|
+
label: t('common.edit'),
|
|
149
|
+
icon: 'pencil',
|
|
150
|
+
onSelect: () => open(component),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: 'item' as const,
|
|
154
|
+
id: 'delete',
|
|
155
|
+
label: t('common.delete'),
|
|
156
|
+
icon: 'trash-2',
|
|
157
|
+
danger: true,
|
|
158
|
+
onSelect: () => (confirming = component),
|
|
159
|
+
},
|
|
160
|
+
]
|
|
161
|
+
: []),
|
|
162
|
+
]
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<ProjectShell
|
|
166
|
+
project={at.project}
|
|
167
|
+
pending={at.pending}
|
|
168
|
+
{slug}
|
|
169
|
+
projectKey={at.projectKey}
|
|
170
|
+
title={t('planning_components')}
|
|
171
|
+
subtitle={t('components_count', { count: components.length })}
|
|
172
|
+
>
|
|
173
|
+
{#snippet headerActions()}
|
|
174
|
+
{#if canManage}
|
|
175
|
+
<Button size="sm" icon="plus" onclick={() => open(null)} data-testid="component-new">
|
|
176
|
+
{t('component_new')}
|
|
177
|
+
</Button>
|
|
178
|
+
{/if}
|
|
179
|
+
{/snippet}
|
|
180
|
+
|
|
181
|
+
{#snippet children()}
|
|
182
|
+
{#if componentsQuery.isPending}
|
|
183
|
+
<p class="quiet">{t('common.loading')}</p>
|
|
184
|
+
{:else if !components.length}
|
|
185
|
+
<EmptyState
|
|
186
|
+
icon="puzzle"
|
|
187
|
+
title={t('planning_components_empty')}
|
|
188
|
+
description={t('planning_components_hint')}
|
|
189
|
+
>
|
|
190
|
+
{#snippet actions()}
|
|
191
|
+
{#if canManage}
|
|
192
|
+
<Button size="sm" icon="plus" onclick={() => open(null)}>{t('component_new')}</Button>
|
|
193
|
+
{/if}
|
|
194
|
+
{/snippet}
|
|
195
|
+
</EmptyState>
|
|
196
|
+
{:else}
|
|
197
|
+
<div class="table" data-testid="component-table">
|
|
198
|
+
<div class="head">
|
|
199
|
+
<span>{t('planning_components')}</span>
|
|
200
|
+
<span>{t('component_lead')}</span>
|
|
201
|
+
<span class="num">{t('component_issues')}</span>
|
|
202
|
+
<span></span>
|
|
203
|
+
</div>
|
|
204
|
+
{#each components as component (component.id)}
|
|
205
|
+
{@const lead = memberOf(component.leadId)}
|
|
206
|
+
<div class="row" data-item={component.name}>
|
|
207
|
+
<a class="what" href={issuesHref(component)}>
|
|
208
|
+
<span class="name">{component.name}</span>
|
|
209
|
+
{#if component.description}<span class="rule">{component.description}</span>{/if}
|
|
210
|
+
</a>
|
|
211
|
+
<span class="lead">
|
|
212
|
+
{#if lead}
|
|
213
|
+
<Avatar id={lead.id} name={lead.name} src={lead.avatarUrl} size={22} />
|
|
214
|
+
<span class="who">{lead.name}</span>
|
|
215
|
+
{:else}
|
|
216
|
+
<span class="none">{t('project_no_lead')}</span>
|
|
217
|
+
{/if}
|
|
218
|
+
</span>
|
|
219
|
+
<span class="num count">{t('planning_issue_count', { count: component.issueCount })}</span>
|
|
220
|
+
<DropdownMenu items={menuFor(component)} align="end">
|
|
221
|
+
{#snippet trigger(props)}
|
|
222
|
+
<IconButton
|
|
223
|
+
{...props}
|
|
224
|
+
icon="ellipsis"
|
|
225
|
+
size={26}
|
|
226
|
+
label={t('component_actions', { name: component.name })}
|
|
227
|
+
/>
|
|
228
|
+
{/snippet}
|
|
229
|
+
</DropdownMenu>
|
|
230
|
+
</div>
|
|
231
|
+
{/each}
|
|
232
|
+
</div>
|
|
233
|
+
{/if}
|
|
234
|
+
{/snippet}
|
|
235
|
+
</ProjectShell>
|
|
236
|
+
|
|
237
|
+
<Dialog
|
|
238
|
+
open={creating || editing !== null}
|
|
239
|
+
title={editing ? t('component_edit') : t('component_new')}
|
|
240
|
+
size="sm"
|
|
241
|
+
onOpenChange={(next: boolean) => {
|
|
242
|
+
if (!next) close()
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
<div class="form">
|
|
246
|
+
<label class="frow">
|
|
247
|
+
<span class="lbl">{t('project_name')}</span>
|
|
248
|
+
<Input bind:value={draftName} data-testid="component-name" />
|
|
249
|
+
</label>
|
|
250
|
+
<label class="frow">
|
|
251
|
+
<span class="lbl">{t('project_description')}</span>
|
|
252
|
+
<Textarea bind:value={draftDescription} rows={2} data-testid="component-description" />
|
|
253
|
+
</label>
|
|
254
|
+
<div class="frow">
|
|
255
|
+
<span class="lbl">{t('component_lead')}</span>
|
|
256
|
+
<Select
|
|
257
|
+
value={draftLead}
|
|
258
|
+
options={[
|
|
259
|
+
{ value: '', label: t('project_no_lead') },
|
|
260
|
+
...members.map((member) => ({ value: member.id, label: member.name ?? member.id })),
|
|
261
|
+
]}
|
|
262
|
+
onValueChange={(value: string) => (draftLead = value)}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="frow">
|
|
266
|
+
<span class="lbl">{t('component_assignee')}</span>
|
|
267
|
+
<Select
|
|
268
|
+
value={draftAssignee}
|
|
269
|
+
options={assigneeOptions}
|
|
270
|
+
onValueChange={(value: string) => (draftAssignee = value as typeof draftAssignee)}
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{#snippet footer()}
|
|
276
|
+
<Button variant="ghost" size="sm" onclick={close}>{t('common.cancel')}</Button>
|
|
277
|
+
<Button
|
|
278
|
+
size="sm"
|
|
279
|
+
disabled={!draftName.trim()}
|
|
280
|
+
loading={save.isPending}
|
|
281
|
+
onclick={() => save.mutate()}
|
|
282
|
+
data-testid="component-save"
|
|
283
|
+
>
|
|
284
|
+
{t('common.save')}
|
|
285
|
+
</Button>
|
|
286
|
+
{/snippet}
|
|
287
|
+
</Dialog>
|
|
288
|
+
|
|
289
|
+
<Dialog
|
|
290
|
+
open={confirming !== null}
|
|
291
|
+
title={t('planning_remove', { name: confirming?.name ?? '' })}
|
|
292
|
+
size="sm"
|
|
293
|
+
onOpenChange={(next: boolean) => {
|
|
294
|
+
if (!next) confirming = null
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
<p class="body">{t('planning_remove_body', { name: confirming?.name ?? '' })}</p>
|
|
298
|
+
|
|
299
|
+
{#snippet footer()}
|
|
300
|
+
<Button variant="ghost" size="sm" onclick={() => (confirming = null)}>{t('common.cancel')}</Button>
|
|
301
|
+
<Button
|
|
302
|
+
size="sm"
|
|
303
|
+
variant="danger"
|
|
304
|
+
loading={remove.isPending}
|
|
305
|
+
onclick={() => confirming && remove.mutate(confirming.id)}
|
|
306
|
+
data-testid="component-delete-confirm"
|
|
307
|
+
>
|
|
308
|
+
{t('common.delete')}
|
|
309
|
+
</Button>
|
|
310
|
+
{/snippet}
|
|
311
|
+
</Dialog>
|
|
312
|
+
|
|
313
|
+
<style>
|
|
314
|
+
/* A table in the shape of the issue list's rows (DESIGN.md 3.1/3.2): a header of sub-labels, then
|
|
315
|
+
44px rows that light up under the pointer. */
|
|
316
|
+
.table {
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
border: 1px solid var(--kern-border);
|
|
320
|
+
border-radius: var(--kern-r-2xl);
|
|
321
|
+
background: var(--kern-surface-raised);
|
|
322
|
+
overflow: hidden;
|
|
323
|
+
}
|
|
324
|
+
.head,
|
|
325
|
+
.row {
|
|
326
|
+
display: grid;
|
|
327
|
+
grid-template-columns: minmax(0, 1fr) 180px 96px 34px;
|
|
328
|
+
align-items: center;
|
|
329
|
+
gap: 12px;
|
|
330
|
+
padding: 0 14px;
|
|
331
|
+
}
|
|
332
|
+
.head {
|
|
333
|
+
height: 34px;
|
|
334
|
+
background: var(--kern-surface-header);
|
|
335
|
+
border-bottom: 1px solid var(--kern-border);
|
|
336
|
+
font-size: 11px;
|
|
337
|
+
font-weight: 600;
|
|
338
|
+
letter-spacing: 0.06em;
|
|
339
|
+
text-transform: uppercase;
|
|
340
|
+
color: var(--kern-ink-450);
|
|
341
|
+
}
|
|
342
|
+
.row {
|
|
343
|
+
min-height: 46px;
|
|
344
|
+
border-bottom: 1px solid var(--kern-border-hairline);
|
|
345
|
+
}
|
|
346
|
+
.row:last-child {
|
|
347
|
+
border-bottom: none;
|
|
348
|
+
}
|
|
349
|
+
.row:hover {
|
|
350
|
+
background: var(--kern-surface-hover);
|
|
351
|
+
}
|
|
352
|
+
.what {
|
|
353
|
+
display: flex;
|
|
354
|
+
flex-direction: column;
|
|
355
|
+
gap: 2px;
|
|
356
|
+
min-width: 0;
|
|
357
|
+
text-decoration: none;
|
|
358
|
+
padding: 8px 0;
|
|
359
|
+
}
|
|
360
|
+
.name {
|
|
361
|
+
font-size: 14px;
|
|
362
|
+
color: var(--kern-ink-800);
|
|
363
|
+
overflow: hidden;
|
|
364
|
+
text-overflow: ellipsis;
|
|
365
|
+
white-space: nowrap;
|
|
366
|
+
}
|
|
367
|
+
.what:hover .name {
|
|
368
|
+
text-decoration: underline;
|
|
369
|
+
}
|
|
370
|
+
.rule {
|
|
371
|
+
font-size: 12.5px;
|
|
372
|
+
color: var(--kern-ink-400);
|
|
373
|
+
overflow: hidden;
|
|
374
|
+
text-overflow: ellipsis;
|
|
375
|
+
white-space: nowrap;
|
|
376
|
+
}
|
|
377
|
+
.lead {
|
|
378
|
+
display: flex;
|
|
379
|
+
align-items: center;
|
|
380
|
+
gap: 8px;
|
|
381
|
+
min-width: 0;
|
|
382
|
+
}
|
|
383
|
+
.who {
|
|
384
|
+
font-size: 13px;
|
|
385
|
+
color: var(--kern-ink-650);
|
|
386
|
+
overflow: hidden;
|
|
387
|
+
text-overflow: ellipsis;
|
|
388
|
+
white-space: nowrap;
|
|
389
|
+
}
|
|
390
|
+
.none {
|
|
391
|
+
font-size: 12.5px;
|
|
392
|
+
color: var(--kern-ink-350);
|
|
393
|
+
}
|
|
394
|
+
.num {
|
|
395
|
+
text-align: end;
|
|
396
|
+
}
|
|
397
|
+
.count {
|
|
398
|
+
font-family: var(--kern-font-mono);
|
|
399
|
+
font-size: 11.5px;
|
|
400
|
+
color: var(--kern-ink-350);
|
|
401
|
+
}
|
|
402
|
+
.quiet {
|
|
403
|
+
margin: 0;
|
|
404
|
+
font-size: 13px;
|
|
405
|
+
color: var(--kern-ink-400);
|
|
406
|
+
}
|
|
407
|
+
.form {
|
|
408
|
+
display: grid;
|
|
409
|
+
gap: 12px;
|
|
410
|
+
}
|
|
411
|
+
.frow {
|
|
412
|
+
display: grid;
|
|
413
|
+
gap: 4px;
|
|
414
|
+
}
|
|
415
|
+
.lbl {
|
|
416
|
+
font-size: 12px;
|
|
417
|
+
color: var(--kern-ink-550);
|
|
418
|
+
}
|
|
419
|
+
.body {
|
|
420
|
+
margin: 0;
|
|
421
|
+
font-size: 13px;
|
|
422
|
+
}
|
|
423
|
+
</style>
|