@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,384 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
Button,
|
|
5
|
+
Checkbox,
|
|
6
|
+
Icon,
|
|
7
|
+
navigation,
|
|
8
|
+
Select,
|
|
9
|
+
SettingsPage,
|
|
10
|
+
SettingsSection,
|
|
11
|
+
Spinner,
|
|
12
|
+
session,
|
|
13
|
+
toast,
|
|
14
|
+
} from '@kernhq/ui'
|
|
15
|
+
import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query'
|
|
16
|
+
import { getTrackerApi } from '../api-instance.js'
|
|
17
|
+
import { t } from '../i18n.js'
|
|
18
|
+
import type { FieldLayoutItem, WorkItemType } from '../index.js'
|
|
19
|
+
import { canTracker } from '../permissions.js'
|
|
20
|
+
import { trackerKeys } from '../query.js'
|
|
21
|
+
import LayoutEditor from './LayoutEditor.svelte'
|
|
22
|
+
import TypeEditor from './TypeEditor.svelte'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Work item types, and what each one shows.
|
|
26
|
+
*
|
|
27
|
+
* This is where the layout that `types.layout` resolves is actually written. Everything else in the
|
|
28
|
+
* tracker reads it: the issue panel, the create dialog, and the cards.
|
|
29
|
+
*
|
|
30
|
+
* A workspace can also add its own types here. Until it could, the per-type customisation this page
|
|
31
|
+
* exists for only reached the four types a template happened to seed — a support desk could arrange
|
|
32
|
+
* "Bug" but never have an "Incident".
|
|
33
|
+
*/
|
|
34
|
+
const api = getTrackerApi()
|
|
35
|
+
const queryClient = useQueryClient()
|
|
36
|
+
|
|
37
|
+
const slug = $derived(navigation.workspaceSlug)
|
|
38
|
+
const workspaceId = $derived(session.workspaces.find((w) => w.slug === slug)?.id ?? '')
|
|
39
|
+
const canManage = $derived(canTracker('typeManage'))
|
|
40
|
+
|
|
41
|
+
let selectedId = $state<string | null>(null)
|
|
42
|
+
/** The arrangement in progress. `null` means "unchanged", which is what disables Save. */
|
|
43
|
+
let draft = $state<FieldLayoutItem[] | null>(null)
|
|
44
|
+
/** The type being created or edited: `undefined` is closed, `null` is a new one. */
|
|
45
|
+
let editorFor = $state<WorkItemType | null | undefined>(undefined)
|
|
46
|
+
let archivingId = $state<string | null>(null)
|
|
47
|
+
let showArchived = $state(false)
|
|
48
|
+
|
|
49
|
+
const typesQuery = createQuery(() => ({
|
|
50
|
+
queryKey: [...trackerKeys.types(workspaceId), showArchived],
|
|
51
|
+
queryFn: () => api.types.list({ workspaceId, includeArchived: showArchived }),
|
|
52
|
+
enabled: Boolean(workspaceId),
|
|
53
|
+
}))
|
|
54
|
+
const types = $derived(typesQuery.data ?? [])
|
|
55
|
+
const selected = $derived(types.find((type) => type.id === selectedId) ?? types[0] ?? null)
|
|
56
|
+
|
|
57
|
+
const layoutQuery = createQuery(() => ({
|
|
58
|
+
queryKey: trackerKeys.layout(workspaceId, selected?.id ?? '', null),
|
|
59
|
+
queryFn: () => api.types.layout({ workspaceId, id: selected?.id as string, projectId: null }),
|
|
60
|
+
enabled: Boolean(selected?.id),
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
const save = createMutation(() => ({
|
|
64
|
+
mutationFn: (items: FieldLayoutItem[]) =>
|
|
65
|
+
api.types.update({
|
|
66
|
+
workspaceId,
|
|
67
|
+
id: selected?.id as string,
|
|
68
|
+
// A `$state` array is a deep proxy, and a proxy cannot be structured-cloned or posted as-is.
|
|
69
|
+
patch: { fieldLayout: $state.snapshot(items) as FieldLayoutItem[] },
|
|
70
|
+
}),
|
|
71
|
+
onSuccess: () => {
|
|
72
|
+
draft = null
|
|
73
|
+
void queryClient.invalidateQueries({ queryKey: ['tracker', 'type', workspaceId] })
|
|
74
|
+
toast.success(t('layout_saved'))
|
|
75
|
+
},
|
|
76
|
+
onError: (error: Error) => toast.error(error.message),
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
const refreshTypes = () => queryClient.invalidateQueries({ queryKey: ['tracker', 'type', workspaceId] })
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The rules that decide what may sit under what.
|
|
83
|
+
*
|
|
84
|
+
* These were enforced and unreachable, which is the worst combination: somebody adding a sub-item
|
|
85
|
+
* got "Sub-items may only nest 1 level(s) deep" from the server with nowhere to go and change it.
|
|
86
|
+
* They live here because they are about types and their levels, which is what this page is.
|
|
87
|
+
*/
|
|
88
|
+
const rulesQuery = createQuery(() => ({
|
|
89
|
+
queryKey: [...trackerKeys.types(workspaceId), 'hierarchy'],
|
|
90
|
+
queryFn: () => api.types.hierarchyRules({ workspaceId }),
|
|
91
|
+
enabled: Boolean(workspaceId),
|
|
92
|
+
}))
|
|
93
|
+
const rules = $derived(rulesQuery.data ?? null)
|
|
94
|
+
|
|
95
|
+
const saveRules = createMutation(() => ({
|
|
96
|
+
mutationFn: (next: NonNullable<typeof rules>) => api.types.setHierarchyRules({ workspaceId, rules: next }),
|
|
97
|
+
onSuccess: () => {
|
|
98
|
+
void refreshTypes()
|
|
99
|
+
toast.success(t('hierarchy_saved'))
|
|
100
|
+
},
|
|
101
|
+
onError: (error: Error) => toast.error(error.message),
|
|
102
|
+
}))
|
|
103
|
+
const setRule = (patch: Record<string, unknown>) => {
|
|
104
|
+
if (rules) saveRules.mutate({ ...rules, ...patch } as NonNullable<typeof rules>)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const saveType = createMutation(() => ({
|
|
108
|
+
mutationFn: (input: Record<string, unknown>) =>
|
|
109
|
+
editorFor
|
|
110
|
+
? api.types.update({ workspaceId, id: editorFor.id, patch: input })
|
|
111
|
+
: api.types.create({ workspaceId, ...input } as never),
|
|
112
|
+
onSuccess: (saved: WorkItemType) => {
|
|
113
|
+
editorFor = undefined
|
|
114
|
+
selectedId = saved.id
|
|
115
|
+
draft = null
|
|
116
|
+
void refreshTypes()
|
|
117
|
+
toast.success(t('type_saved'))
|
|
118
|
+
},
|
|
119
|
+
onError: (error: Error) => toast.error(error.message),
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Archived, never deleted. Issues keep pointing at their type, so removing one would leave work
|
|
124
|
+
* with no answer to "what is this" — archiving takes it off every menu and leaves the past intact.
|
|
125
|
+
*/
|
|
126
|
+
const archiveType = createMutation(() => ({
|
|
127
|
+
mutationFn: (input: { id: string; archived: boolean }) => api.types.archive({ workspaceId, ...input }),
|
|
128
|
+
onSuccess: () => {
|
|
129
|
+
archivingId = null
|
|
130
|
+
void refreshTypes()
|
|
131
|
+
},
|
|
132
|
+
onError: (error: Error) => toast.error(error.message),
|
|
133
|
+
}))
|
|
134
|
+
|
|
135
|
+
/** Switching type abandons an unsaved arrangement rather than carrying it onto another type. */
|
|
136
|
+
const select = (type: WorkItemType) => {
|
|
137
|
+
selectedId = type.id
|
|
138
|
+
draft = null
|
|
139
|
+
}
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<SettingsPage title={t('settings_types')} description={t('settings_types_hint')}>
|
|
143
|
+
{#if typesQuery.isPending}
|
|
144
|
+
<SettingsSection><div class="state"><Spinner /></div></SettingsSection>
|
|
145
|
+
{:else if typesQuery.isError}
|
|
146
|
+
<SettingsSection>
|
|
147
|
+
<div class="state">
|
|
148
|
+
<p>{t('settings_types_failed')}</p>
|
|
149
|
+
<Button size="sm" variant="ghost" onclick={() => typesQuery.refetch()}>{t('common.retry')}</Button>
|
|
150
|
+
</div>
|
|
151
|
+
</SettingsSection>
|
|
152
|
+
{:else}
|
|
153
|
+
<SettingsSection flush>
|
|
154
|
+
<ul class="types" data-testid="type-list">
|
|
155
|
+
{#each types as type (type.id)}
|
|
156
|
+
<li class:archived={type.archivedAt} class:on={selected?.id === type.id}>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
class="trow"
|
|
160
|
+
onclick={() => select(type)}
|
|
161
|
+
data-testid="type-row"
|
|
162
|
+
data-type-key={type.key}
|
|
163
|
+
>
|
|
164
|
+
<Icon name={type.icon ?? 'square-check-big'} size={14} strokeWidth={1.8} />
|
|
165
|
+
<span class="tname">{type.name}</span>
|
|
166
|
+
<span class="tkey">{type.key}</span>
|
|
167
|
+
{#if type.isDefault}<Badge>{t('type_default')}</Badge>{/if}
|
|
168
|
+
{#if type.archivedAt}<Badge tone="grey">{t('type_archived')}</Badge>{/if}
|
|
169
|
+
{#if type.fieldLayout.length}<Badge tone="info">{t('type_customised')}</Badge>{/if}
|
|
170
|
+
</button>
|
|
171
|
+
{#if canManage}
|
|
172
|
+
<div class="tactions">
|
|
173
|
+
<Button size="sm" variant="ghost" onclick={() => (editorFor = type)} data-testid="type-edit">
|
|
174
|
+
{t('common.edit')}
|
|
175
|
+
</Button>
|
|
176
|
+
{#if type.archivedAt}
|
|
177
|
+
<Button
|
|
178
|
+
size="sm"
|
|
179
|
+
variant="ghost"
|
|
180
|
+
onclick={() => archiveType.mutate({ id: type.id, archived: false })}
|
|
181
|
+
>
|
|
182
|
+
{t('type_restore')}
|
|
183
|
+
</Button>
|
|
184
|
+
{:else}
|
|
185
|
+
<Button
|
|
186
|
+
size="sm"
|
|
187
|
+
variant="ghost"
|
|
188
|
+
disabled={type.isDefault}
|
|
189
|
+
onclick={() => (archivingId = type.id)}
|
|
190
|
+
data-testid="type-archive"
|
|
191
|
+
>
|
|
192
|
+
{t('common.archive')}
|
|
193
|
+
</Button>
|
|
194
|
+
{/if}
|
|
195
|
+
</div>
|
|
196
|
+
{/if}
|
|
197
|
+
</li>
|
|
198
|
+
{#if archivingId === type.id}
|
|
199
|
+
<li class="confirm">
|
|
200
|
+
<div role="alertdialog" aria-label={t('type_archive_body', { name: type.name })}>
|
|
201
|
+
<span>{t('type_archive_body', { name: type.name })}</span>
|
|
202
|
+
<Button
|
|
203
|
+
size="sm"
|
|
204
|
+
onclick={() => archiveType.mutate({ id: type.id, archived: true })}
|
|
205
|
+
data-testid="type-archive-confirm"
|
|
206
|
+
>
|
|
207
|
+
{t('common.archive')}
|
|
208
|
+
</Button>
|
|
209
|
+
<Button size="sm" variant="ghost" onclick={() => (archivingId = null)}>
|
|
210
|
+
{t('common.cancel')}
|
|
211
|
+
</Button>
|
|
212
|
+
</div>
|
|
213
|
+
</li>
|
|
214
|
+
{/if}
|
|
215
|
+
{/each}
|
|
216
|
+
</ul>
|
|
217
|
+
|
|
218
|
+
{#snippet footer()}
|
|
219
|
+
<Checkbox
|
|
220
|
+
checked={showArchived}
|
|
221
|
+
label={t('type_show_archived')}
|
|
222
|
+
onCheckedChange={(on: boolean) => (showArchived = on)}
|
|
223
|
+
/>
|
|
224
|
+
<Button size="sm" disabled={!canManage} onclick={() => (editorFor = null)} data-testid="type-new">
|
|
225
|
+
{t('type_new')}
|
|
226
|
+
</Button>
|
|
227
|
+
{/snippet}
|
|
228
|
+
</SettingsSection>
|
|
229
|
+
|
|
230
|
+
{#if rules}
|
|
231
|
+
<SettingsSection
|
|
232
|
+
title={t('hierarchy_title')}
|
|
233
|
+
description={t('hierarchy_hint')}
|
|
234
|
+
>
|
|
235
|
+
<div class="rules">
|
|
236
|
+
<Checkbox
|
|
237
|
+
checked={rules.allowSameLevel}
|
|
238
|
+
label={t('hierarchy_same_level')}
|
|
239
|
+
disabled={!canManage}
|
|
240
|
+
onCheckedChange={(on: boolean) => setRule({ allowSameLevel: on })}
|
|
241
|
+
/>
|
|
242
|
+
<Checkbox
|
|
243
|
+
checked={rules.allowSkipLevels}
|
|
244
|
+
label={t('hierarchy_skip_levels')}
|
|
245
|
+
disabled={!canManage}
|
|
246
|
+
onCheckedChange={(on: boolean) => setRule({ allowSkipLevels: on })}
|
|
247
|
+
/>
|
|
248
|
+
<label class="drow" data-testid="hierarchy-depth">
|
|
249
|
+
<span class="dlbl">{t('hierarchy_depth')}</span>
|
|
250
|
+
<Select
|
|
251
|
+
value={String(rules.maxSubItemDepth)}
|
|
252
|
+
options={[1, 2, 3, 4, 5].map((n) => ({ value: String(n), label: String(n) }))}
|
|
253
|
+
disabled={!canManage}
|
|
254
|
+
onValueChange={(v: string) => setRule({ maxSubItemDepth: Number(v) })}
|
|
255
|
+
/>
|
|
256
|
+
</label>
|
|
257
|
+
</div>
|
|
258
|
+
</SettingsSection>
|
|
259
|
+
{/if}
|
|
260
|
+
|
|
261
|
+
{#if selected}
|
|
262
|
+
<SettingsSection
|
|
263
|
+
title={t('layout_for', { name: selected.name })}
|
|
264
|
+
description={t('layout_hint')}
|
|
265
|
+
>
|
|
266
|
+
{#if layoutQuery.isPending}
|
|
267
|
+
<div class="state"><Spinner /></div>
|
|
268
|
+
{:else if layoutQuery.data}
|
|
269
|
+
<LayoutEditor
|
|
270
|
+
layout={layoutQuery.data}
|
|
271
|
+
disabled={!canManage}
|
|
272
|
+
onchange={(items) => (draft = items)}
|
|
273
|
+
/>
|
|
274
|
+
{/if}
|
|
275
|
+
|
|
276
|
+
{#snippet footer()}
|
|
277
|
+
<Button variant="ghost" size="sm" onclick={() => (draft = null)} disabled={!draft}>
|
|
278
|
+
{t('common.discard')}
|
|
279
|
+
</Button>
|
|
280
|
+
<Button
|
|
281
|
+
size="sm"
|
|
282
|
+
disabled={!draft || !canManage}
|
|
283
|
+
loading={save.isPending}
|
|
284
|
+
onclick={() => draft && save.mutate(draft)}
|
|
285
|
+
data-testid="save-layout"
|
|
286
|
+
>
|
|
287
|
+
{t('common.save')}
|
|
288
|
+
</Button>
|
|
289
|
+
{/snippet}
|
|
290
|
+
</SettingsSection>
|
|
291
|
+
{/if}
|
|
292
|
+
{/if}
|
|
293
|
+
</SettingsPage>
|
|
294
|
+
|
|
295
|
+
<TypeEditor
|
|
296
|
+
open={editorFor !== undefined}
|
|
297
|
+
type={editorFor ?? null}
|
|
298
|
+
busy={saveType.isPending}
|
|
299
|
+
onclose={() => (editorFor = undefined)}
|
|
300
|
+
onsave={(input) => saveType.mutate(input)}
|
|
301
|
+
/>
|
|
302
|
+
|
|
303
|
+
<style>
|
|
304
|
+
.rules {
|
|
305
|
+
display: grid;
|
|
306
|
+
gap: 10px;
|
|
307
|
+
justify-items: start;
|
|
308
|
+
}
|
|
309
|
+
.drow {
|
|
310
|
+
display: grid;
|
|
311
|
+
gap: 4px;
|
|
312
|
+
}
|
|
313
|
+
.dlbl {
|
|
314
|
+
font-size: 12px;
|
|
315
|
+
font-weight: 500;
|
|
316
|
+
color: var(--kern-ink-600);
|
|
317
|
+
}
|
|
318
|
+
.state {
|
|
319
|
+
display: grid;
|
|
320
|
+
place-items: center;
|
|
321
|
+
gap: 8px;
|
|
322
|
+
padding: 28px;
|
|
323
|
+
font-size: 13px;
|
|
324
|
+
}
|
|
325
|
+
.types {
|
|
326
|
+
list-style: none;
|
|
327
|
+
margin: 0;
|
|
328
|
+
padding: 0;
|
|
329
|
+
}
|
|
330
|
+
.types li {
|
|
331
|
+
display: flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
border-bottom: 1px solid var(--kern-border-hairline);
|
|
334
|
+
}
|
|
335
|
+
.types li.archived .tname {
|
|
336
|
+
color: var(--kern-ink-400);
|
|
337
|
+
}
|
|
338
|
+
.tactions {
|
|
339
|
+
display: flex;
|
|
340
|
+
gap: 2px;
|
|
341
|
+
padding-inline-end: 12px;
|
|
342
|
+
}
|
|
343
|
+
.confirm {
|
|
344
|
+
padding: 8px 18px;
|
|
345
|
+
font-size: 12.5px;
|
|
346
|
+
}
|
|
347
|
+
.confirm div {
|
|
348
|
+
display: flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
gap: 8px;
|
|
351
|
+
}
|
|
352
|
+
.types li:last-child {
|
|
353
|
+
border-bottom: 0;
|
|
354
|
+
}
|
|
355
|
+
.trow {
|
|
356
|
+
display: flex;
|
|
357
|
+
align-items: center;
|
|
358
|
+
gap: 10px;
|
|
359
|
+
width: 100%;
|
|
360
|
+
padding: 10px 18px;
|
|
361
|
+
border: 0;
|
|
362
|
+
background: none;
|
|
363
|
+
color: inherit;
|
|
364
|
+
font: inherit;
|
|
365
|
+
text-align: start;
|
|
366
|
+
cursor: pointer;
|
|
367
|
+
}
|
|
368
|
+
.trow:hover {
|
|
369
|
+
background: var(--kern-surface-hover);
|
|
370
|
+
}
|
|
371
|
+
.types li.on {
|
|
372
|
+
background: var(--kern-surface-active);
|
|
373
|
+
}
|
|
374
|
+
.tname {
|
|
375
|
+
font-size: 13px;
|
|
376
|
+
color: var(--kern-ink-800);
|
|
377
|
+
}
|
|
378
|
+
.tkey {
|
|
379
|
+
flex: 1;
|
|
380
|
+
font-family: var(--kern-font-mono);
|
|
381
|
+
font-size: 11.5px;
|
|
382
|
+
color: var(--kern-ink-400);
|
|
383
|
+
}
|
|
384
|
+
</style>
|