@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernhq/module-tracker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Kern tracker module: projects, work item types, custom fields, workflows, issues, KQL, cycles, views, reports, intake, time tracking (server + client skeleton)",
|
|
5
5
|
"homepage": "https://kernaio.com",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -49,28 +49,41 @@
|
|
|
49
49
|
"./migrations": "./migrations"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@kernhq/contracts": "^0.5.
|
|
53
|
-
"@kernhq/kernel": "^0.
|
|
54
|
-
"@kernhq/sdk": "^0.1.
|
|
52
|
+
"@kernhq/contracts": "^0.5.1",
|
|
53
|
+
"@kernhq/kernel": "^0.7.0",
|
|
54
|
+
"@kernhq/sdk": "^0.1.5",
|
|
55
55
|
"@kernhq/workflow": "^0.1.0",
|
|
56
56
|
"@orpc/contract": "^1.15.0",
|
|
57
57
|
"@orpc/server": "^1.15.0",
|
|
58
58
|
"drizzle-orm": "^0.45.0",
|
|
59
|
+
"svelte-dnd-action": "^0.9.0",
|
|
59
60
|
"zod": "^4.1.0"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@kernhq/tsconfig": "^0.1.0",
|
|
64
|
+
"@kernhq/ui": "^0.8.0",
|
|
65
|
+
"@tanstack/svelte-query": "^6.1.0",
|
|
63
66
|
"@types/node": "^24.0.0",
|
|
64
67
|
"@types/pg": "^8.15.0",
|
|
65
68
|
"drizzle-kit": "^0.31.0",
|
|
66
69
|
"pg": "^8.16.0",
|
|
70
|
+
"svelte": "^5.46.0",
|
|
71
|
+
"svelte-check": "^4.0.0",
|
|
67
72
|
"typescript": "~5.9.3",
|
|
68
73
|
"vitest": "^4.0.0"
|
|
69
74
|
},
|
|
70
75
|
"peerDependencies": {
|
|
71
|
-
"
|
|
76
|
+
"@kernhq/ui": "^0.8.0",
|
|
77
|
+
"@tanstack/svelte-query": "^6.1.0",
|
|
78
|
+
"svelte": "^5.46.0"
|
|
72
79
|
},
|
|
73
80
|
"peerDependenciesMeta": {
|
|
81
|
+
"@kernhq/ui": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"@tanstack/svelte-query": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
74
87
|
"svelte": {
|
|
75
88
|
"optional": true
|
|
76
89
|
}
|
|
@@ -78,7 +91,7 @@
|
|
|
78
91
|
"scripts": {
|
|
79
92
|
"build": "tsc -p tsconfig.json",
|
|
80
93
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
81
|
-
"typecheck": "tsc -p tsconfig.json --noEmit &&
|
|
94
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && svelte-check --tsconfig ./tsconfig.client.json --threshold error",
|
|
82
95
|
"lint": "biome check .",
|
|
83
96
|
"test": "vitest run",
|
|
84
97
|
"test:unit": "vitest run --exclude 'src/**/*.int.test.ts'",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getHost } from '@kernhq/ui'
|
|
2
|
+
import { createTrackerClient, type TrackerApi } from './api.js'
|
|
3
|
+
import { createMockTrackerApi } from './mock.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The tracker API client.
|
|
7
|
+
*
|
|
8
|
+
* A module is reached at `/api/<module>`, so every module gets its own client built from its own
|
|
9
|
+
* contract; this is the tracker's, alongside `getApi()` for core. `PUBLIC_API_MOCK=1` swaps in the
|
|
10
|
+
* in-memory implementation from `./mock`, which satisfies the same contract types, so no view has a
|
|
11
|
+
* second code path for demos and end-to-end tests.
|
|
12
|
+
*/
|
|
13
|
+
export type { TrackerApi }
|
|
14
|
+
|
|
15
|
+
let cached: TrackerApi | null = null
|
|
16
|
+
|
|
17
|
+
export function getTrackerApi(): TrackerApi {
|
|
18
|
+
if (cached) return cached
|
|
19
|
+
cached = getHost().isMock ? (createMockTrackerApi() as unknown as TrackerApi) : createReal()
|
|
20
|
+
return cached
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createReal(): TrackerApi {
|
|
24
|
+
return createTrackerClient({
|
|
25
|
+
// empty base URL keeps requests same-origin: the dev server proxies /api, and in production the
|
|
26
|
+
// reverse proxy routes it, so the session cookie works without CORS in both
|
|
27
|
+
baseUrl: getHost().apiBaseUrl,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Test seam: lets tests install a fake without touching module state elsewhere. */
|
|
32
|
+
export function __setTrackerApi(api: TrackerApi | null) {
|
|
33
|
+
cached = api
|
|
34
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Avatar, Icon, realtime } from '@kernhq/ui'
|
|
3
|
+
import { getTrackerCatalogue } from '../context.svelte.js'
|
|
4
|
+
import { t } from '../i18n.js'
|
|
5
|
+
import { formatDuration, type Issue } from '../index.js'
|
|
6
|
+
import { estimateLabel, priorityLabel } from '../labels.js'
|
|
7
|
+
import DueDate from './DueDate.svelte'
|
|
8
|
+
import PriorityGlyph from './PriorityGlyph.svelte'
|
|
9
|
+
import StatusIcon from './StatusIcon.svelte'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A board card (DESIGN.md 3.3): status and key on top with the assignee on the far side, the title,
|
|
13
|
+
* a row of chips for the properties worth seeing without opening anything, and a footer of numbers.
|
|
14
|
+
* The comment count disappears when there are no comments rather than showing a zero.
|
|
15
|
+
*/
|
|
16
|
+
interface Props {
|
|
17
|
+
issue: Issue
|
|
18
|
+
onopen: (issue: Issue) => void
|
|
19
|
+
}
|
|
20
|
+
let { issue, onopen }: Props = $props()
|
|
21
|
+
|
|
22
|
+
const cat = getTrackerCatalogue()
|
|
23
|
+
const project = $derived(cat.project(issue.projectId))
|
|
24
|
+
const status = $derived(cat.status(issue.statusId))
|
|
25
|
+
const milestone = $derived(cat.milestone(issue.milestoneId))
|
|
26
|
+
const assignee = $derived(cat.person(issue.assigneeIds[0]))
|
|
27
|
+
const online = $derived(assignee ? realtime.online.has(assignee.id) : false)
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<article class="kcard" data-testid="board-card" data-issue-key={issue.key}>
|
|
31
|
+
<button type="button" class="body" onclick={() => onopen(issue)}>
|
|
32
|
+
<span class="top">
|
|
33
|
+
<StatusIcon
|
|
34
|
+
category={issue.statusCategory}
|
|
35
|
+
statusId={issue.statusId}
|
|
36
|
+
name={status?.name}
|
|
37
|
+
size={15}
|
|
38
|
+
label={status?.name}
|
|
39
|
+
/>
|
|
40
|
+
<span class="key">{issue.key}</span>
|
|
41
|
+
<span class="sp"></span>
|
|
42
|
+
{#if assignee}
|
|
43
|
+
<span class="dot" class:on={online}></span>
|
|
44
|
+
<Avatar name={assignee.name} src={assignee.avatarUrl} id={assignee.id} size={22} />
|
|
45
|
+
{/if}
|
|
46
|
+
</span>
|
|
47
|
+
|
|
48
|
+
<span class="title">{issue.title}</span>
|
|
49
|
+
|
|
50
|
+
<span class="chips">
|
|
51
|
+
{#if issue.priority !== 'none'}
|
|
52
|
+
<span class="chip pri" title={priorityLabel(issue.priority)}>
|
|
53
|
+
<PriorityGlyph priority={issue.priority} size={14} />
|
|
54
|
+
</span>
|
|
55
|
+
{/if}
|
|
56
|
+
{#if project}
|
|
57
|
+
<span class="chip">
|
|
58
|
+
<Icon name="diamond" size={12} strokeWidth={1.7} />
|
|
59
|
+
<span class="chip-t">{project.name}</span>
|
|
60
|
+
</span>
|
|
61
|
+
{/if}
|
|
62
|
+
{#if milestone}
|
|
63
|
+
<span class="chip">
|
|
64
|
+
<Icon name="flag" size={12} strokeWidth={1.7} />
|
|
65
|
+
<span class="chip-t">{milestone.name}</span>
|
|
66
|
+
</span>
|
|
67
|
+
{/if}
|
|
68
|
+
{#if issue.dueDate}
|
|
69
|
+
<span class="chip"><DueDate date={issue.dueDate} /></span>
|
|
70
|
+
{/if}
|
|
71
|
+
</span>
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
<footer class="foot">
|
|
75
|
+
<span class="stat"><Icon name="clock" size={13} strokeWidth={1.6} />{formatDuration(issue.timeSpentSec)}</span>
|
|
76
|
+
{#if issue.estimate !== null}
|
|
77
|
+
<span class="stat">{estimateLabel(issue.estimate, issue.estimateUnit)}</span>
|
|
78
|
+
{/if}
|
|
79
|
+
<span class="sp"></span>
|
|
80
|
+
{#if issue.commentCount > 0}
|
|
81
|
+
<span class="stat">
|
|
82
|
+
<Icon name="message-circle" size={13} strokeWidth={1.6} />{issue.commentCount}
|
|
83
|
+
</span>
|
|
84
|
+
{/if}
|
|
85
|
+
</footer>
|
|
86
|
+
</article>
|
|
87
|
+
|
|
88
|
+
<style>
|
|
89
|
+
.kcard {
|
|
90
|
+
background: var(--kern-surface-raised);
|
|
91
|
+
border: 1px solid var(--kern-border);
|
|
92
|
+
border-radius: var(--kern-r-card);
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
transition: border-color 80ms;
|
|
95
|
+
}
|
|
96
|
+
.kcard:hover {
|
|
97
|
+
border-color: var(--kern-border-hover);
|
|
98
|
+
}
|
|
99
|
+
.body {
|
|
100
|
+
display: block;
|
|
101
|
+
width: 100%;
|
|
102
|
+
padding: 13px 14px 14px;
|
|
103
|
+
text-align: start;
|
|
104
|
+
}
|
|
105
|
+
.top {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 7px;
|
|
109
|
+
}
|
|
110
|
+
.key {
|
|
111
|
+
font-family: var(--kern-font-mono);
|
|
112
|
+
font-size: 11.5px;
|
|
113
|
+
letter-spacing: -0.01em;
|
|
114
|
+
color: var(--kern-ink-250);
|
|
115
|
+
}
|
|
116
|
+
.sp {
|
|
117
|
+
flex: 1;
|
|
118
|
+
}
|
|
119
|
+
.dot {
|
|
120
|
+
width: 7px;
|
|
121
|
+
height: 7px;
|
|
122
|
+
border-radius: var(--kern-r-full);
|
|
123
|
+
background: var(--kern-success);
|
|
124
|
+
opacity: 0;
|
|
125
|
+
flex: none;
|
|
126
|
+
}
|
|
127
|
+
.dot.on {
|
|
128
|
+
opacity: 1;
|
|
129
|
+
}
|
|
130
|
+
.title {
|
|
131
|
+
display: block;
|
|
132
|
+
margin-top: 9px;
|
|
133
|
+
font-size: 14px;
|
|
134
|
+
line-height: 1.42;
|
|
135
|
+
letter-spacing: -0.005em;
|
|
136
|
+
color: var(--kern-ink-900);
|
|
137
|
+
text-wrap: pretty;
|
|
138
|
+
}
|
|
139
|
+
.chips {
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-wrap: wrap;
|
|
142
|
+
gap: 5px;
|
|
143
|
+
margin-top: 10px;
|
|
144
|
+
}
|
|
145
|
+
.chip {
|
|
146
|
+
display: inline-flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 6px;
|
|
149
|
+
height: 26px;
|
|
150
|
+
padding: 0 8px;
|
|
151
|
+
border-radius: var(--kern-r-md);
|
|
152
|
+
background: var(--kern-surface-chip);
|
|
153
|
+
color: var(--kern-ink-580);
|
|
154
|
+
font-size: 12.5px;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
}
|
|
157
|
+
.chip.pri {
|
|
158
|
+
padding: 0 7px;
|
|
159
|
+
}
|
|
160
|
+
.chip-t {
|
|
161
|
+
max-width: 120px;
|
|
162
|
+
overflow: hidden;
|
|
163
|
+
text-overflow: ellipsis;
|
|
164
|
+
}
|
|
165
|
+
.foot {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 14px;
|
|
169
|
+
height: 38px;
|
|
170
|
+
padding: 0 14px;
|
|
171
|
+
border-top: 1px solid var(--kern-surface-chip);
|
|
172
|
+
font-family: var(--kern-font-mono);
|
|
173
|
+
font-size: 11.5px;
|
|
174
|
+
letter-spacing: -0.01em;
|
|
175
|
+
color: var(--kern-ink-300);
|
|
176
|
+
}
|
|
177
|
+
.stat {
|
|
178
|
+
display: inline-flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
gap: 6px;
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Icon } from '@kernhq/ui'
|
|
3
|
+
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action'
|
|
4
|
+
import { t } from '../i18n.js'
|
|
5
|
+
import { byRank, type Issue, type StatusInfo, statusStyle } from '../index.js'
|
|
6
|
+
import BoardCard from './BoardCard.svelte'
|
|
7
|
+
import StatusIcon from './StatusIcon.svelte'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The board (DESIGN.md 3.3).
|
|
11
|
+
*
|
|
12
|
+
* One column per value of whatever the board is grouped by — a workflow status by default, or a
|
|
13
|
+
* custom field's options — each headed by a 2px rule that starts as a stub of the column's colour
|
|
14
|
+
* and fades to a hairline. Cards are dragged between columns with svelte-dnd-action; dropping one
|
|
15
|
+
* reports the column it landed in and the neighbours it landed between, so the page can apply
|
|
16
|
+
* whatever that column means and the new rank in one go.
|
|
17
|
+
*
|
|
18
|
+
* The board does not know what a column *is*: the page says how to read a card's column and what a
|
|
19
|
+
* drop means. That is what lets the same board group by status and by Severity without a second
|
|
20
|
+
* implementation.
|
|
21
|
+
*
|
|
22
|
+
* A WIP limit is shown as `used/limit` and turns red when a column is over it. It warns rather than
|
|
23
|
+
* refuses: a hard block just makes people rename columns.
|
|
24
|
+
*/
|
|
25
|
+
export interface BoardColumnInfo {
|
|
26
|
+
id: string
|
|
27
|
+
name: string
|
|
28
|
+
/** the colour of the header rule; a status derives one, a field option carries its own */
|
|
29
|
+
color: string
|
|
30
|
+
/** present only for a status column, which draws its glyph */
|
|
31
|
+
status?: StatusInfo
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Props {
|
|
35
|
+
issues: Issue[]
|
|
36
|
+
columns: BoardColumnInfo[]
|
|
37
|
+
/** which column a card belongs in — the page decides, because it knows the grouping */
|
|
38
|
+
columnOf: (issue: Issue) => string[]
|
|
39
|
+
wipLimits?: Record<string, number | undefined>
|
|
40
|
+
/** false when the viewer may not move work between statuses; cards stay readable, just not draggable */
|
|
41
|
+
draggable?: boolean
|
|
42
|
+
onopen: (issue: Issue) => void
|
|
43
|
+
onmove: (issue: Issue, toColumnId: string, afterId: string | null, beforeId: string | null) => void
|
|
44
|
+
oncreate?: (columnId: string) => void
|
|
45
|
+
}
|
|
46
|
+
let {
|
|
47
|
+
issues,
|
|
48
|
+
columns,
|
|
49
|
+
columnOf,
|
|
50
|
+
wipLimits = {},
|
|
51
|
+
draggable = true,
|
|
52
|
+
onopen,
|
|
53
|
+
onmove,
|
|
54
|
+
oncreate,
|
|
55
|
+
}: Props = $props()
|
|
56
|
+
|
|
57
|
+
const FLIP = 160
|
|
58
|
+
|
|
59
|
+
type Lane = { column: BoardColumnInfo; items: Issue[] }
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `$state.raw` matters here: the drag library tracks items by identity, and a deep-reactive proxy
|
|
63
|
+
* hands it a different object every read, which it reads as an endless stream of changes.
|
|
64
|
+
*/
|
|
65
|
+
let lanes = $state.raw<Lane[]>([])
|
|
66
|
+
let dragging = $state(false)
|
|
67
|
+
|
|
68
|
+
const replaceLane = (index: number, items: Issue[]) => {
|
|
69
|
+
lanes = lanes.map((lane, i) => (i === index ? { column: lane.column, items } : lane))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// mirror the query into local lists the drag library can reorder, but leave them alone mid-drag
|
|
73
|
+
$effect(() => {
|
|
74
|
+
if (dragging) return
|
|
75
|
+
const sorted = [...issues].sort(byRank)
|
|
76
|
+
lanes = columns.map((column) => ({
|
|
77
|
+
column,
|
|
78
|
+
items: sorted.filter((issue) => columnOf(issue).includes(column.id)),
|
|
79
|
+
}))
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const isShadow = (issue: Issue) =>
|
|
83
|
+
(issue as unknown as Record<string, unknown>)[SHADOW_ITEM_MARKER_PROPERTY_NAME] === true
|
|
84
|
+
|
|
85
|
+
function consider(index: number, event: CustomEvent<{ items: Issue[] }>) {
|
|
86
|
+
dragging = true
|
|
87
|
+
replaceLane(index, event.detail.items)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function finalize(index: number, event: CustomEvent<{ items: Issue[]; info: { id: string } }>) {
|
|
91
|
+
dragging = false
|
|
92
|
+
const lane = lanes[index]
|
|
93
|
+
if (!lane) return
|
|
94
|
+
const items = event.detail.items
|
|
95
|
+
replaceLane(index, items)
|
|
96
|
+
|
|
97
|
+
const at = items.findIndex((i) => i.id === event.detail.info.id)
|
|
98
|
+
const moved = items[at]
|
|
99
|
+
// the drop reports on both the source and the target lane; only the target holds the card
|
|
100
|
+
if (!moved) return
|
|
101
|
+
onmove(moved, lane.column.id, items[at - 1]?.id ?? null, items[at + 1]?.id ?? null)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const laneRule = (column: BoardColumnInfo) =>
|
|
105
|
+
`linear-gradient(to var(--kern-lane-dir, right), ${column.color} 0 34px, var(--kern-border) 34px)`
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<div class="kboard" data-testid="board">
|
|
109
|
+
{#each lanes as lane, index (lane.column.id)}
|
|
110
|
+
{@const limit = wipLimits[lane.column.id]}
|
|
111
|
+
{@const over = limit !== undefined && lane.items.length > limit}
|
|
112
|
+
<section class="col" data-column={lane.column.id}>
|
|
113
|
+
<header class="head">
|
|
114
|
+
{#if lane.column.status}
|
|
115
|
+
<StatusIcon
|
|
116
|
+
category={lane.column.status.category}
|
|
117
|
+
statusId={lane.column.status.id}
|
|
118
|
+
name={lane.column.status.name}
|
|
119
|
+
size={15}
|
|
120
|
+
/>
|
|
121
|
+
{:else}
|
|
122
|
+
<!-- A field's value has no glyph of its own, so the column's colour is the whole cue. -->
|
|
123
|
+
<span class="swatch" style:background={lane.column.color}></span>
|
|
124
|
+
{/if}
|
|
125
|
+
<h2 class="name">{lane.column.name}</h2>
|
|
126
|
+
<span class="count" class:over title={over ? t('wip_exceeded') : undefined}>
|
|
127
|
+
{limit === undefined ? lane.items.length : `${lane.items.length}/${limit}`}
|
|
128
|
+
</span>
|
|
129
|
+
<span class="sp"></span>
|
|
130
|
+
{#if oncreate}
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
class="add"
|
|
134
|
+
aria-label={t('new_issue')}
|
|
135
|
+
onclick={() => oncreate(lane.column.id)}
|
|
136
|
+
>
|
|
137
|
+
<Icon name="plus" size={13} strokeWidth={1.9} />
|
|
138
|
+
</button>
|
|
139
|
+
{/if}
|
|
140
|
+
</header>
|
|
141
|
+
<div class="rule" style:background={laneRule(lane.column)}></div>
|
|
142
|
+
|
|
143
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
144
|
+
<div
|
|
145
|
+
class="stack"
|
|
146
|
+
use:dndzone={{
|
|
147
|
+
items: lane.items,
|
|
148
|
+
flipDurationMs: FLIP,
|
|
149
|
+
type: 'tracker-issues',
|
|
150
|
+
dragDisabled: !draggable,
|
|
151
|
+
dropTargetStyle: {},
|
|
152
|
+
}}
|
|
153
|
+
onconsider={(e) => consider(index, e)}
|
|
154
|
+
onfinalize={(e) => finalize(index, e)}
|
|
155
|
+
>
|
|
156
|
+
{#each lane.items as issue (issue.id)}
|
|
157
|
+
<div class="slot" class:shadow={isShadow(issue)}>
|
|
158
|
+
<BoardCard {issue} {onopen} />
|
|
159
|
+
</div>
|
|
160
|
+
{/each}
|
|
161
|
+
</div>
|
|
162
|
+
</section>
|
|
163
|
+
{/each}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<style>
|
|
167
|
+
.kboard {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: flex-start;
|
|
170
|
+
gap: 20px;
|
|
171
|
+
padding: 22px 28px 30px;
|
|
172
|
+
overflow: auto;
|
|
173
|
+
height: 100%;
|
|
174
|
+
}
|
|
175
|
+
:global([dir='rtl']) .kboard {
|
|
176
|
+
--kern-lane-dir: left;
|
|
177
|
+
}
|
|
178
|
+
.col {
|
|
179
|
+
width: var(--kern-board-col-w);
|
|
180
|
+
flex: none;
|
|
181
|
+
}
|
|
182
|
+
.head {
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: 9px;
|
|
186
|
+
height: 30px;
|
|
187
|
+
}
|
|
188
|
+
.name {
|
|
189
|
+
margin: 0;
|
|
190
|
+
font-size: 13.5px;
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
letter-spacing: -0.01em;
|
|
193
|
+
color: var(--kern-ink-900);
|
|
194
|
+
white-space: nowrap;
|
|
195
|
+
}
|
|
196
|
+
.count {
|
|
197
|
+
font-family: var(--kern-font-mono);
|
|
198
|
+
font-size: 11.5px;
|
|
199
|
+
letter-spacing: -0.01em;
|
|
200
|
+
color: var(--kern-ink-250);
|
|
201
|
+
}
|
|
202
|
+
.count.over {
|
|
203
|
+
color: var(--kern-danger);
|
|
204
|
+
font-weight: 500;
|
|
205
|
+
}
|
|
206
|
+
.sp {
|
|
207
|
+
flex: 1;
|
|
208
|
+
}
|
|
209
|
+
.add {
|
|
210
|
+
width: 22px;
|
|
211
|
+
height: 22px;
|
|
212
|
+
border-radius: var(--kern-r-md);
|
|
213
|
+
display: grid;
|
|
214
|
+
place-items: center;
|
|
215
|
+
color: var(--kern-ink-250);
|
|
216
|
+
}
|
|
217
|
+
.add:hover {
|
|
218
|
+
background: var(--kern-surface-hover);
|
|
219
|
+
color: var(--kern-ink-900);
|
|
220
|
+
}
|
|
221
|
+
.swatch {
|
|
222
|
+
width: 9px;
|
|
223
|
+
height: 9px;
|
|
224
|
+
border-radius: 3px;
|
|
225
|
+
flex: none;
|
|
226
|
+
}
|
|
227
|
+
.rule {
|
|
228
|
+
height: 2px;
|
|
229
|
+
border-radius: 1px;
|
|
230
|
+
}
|
|
231
|
+
.stack {
|
|
232
|
+
display: flex;
|
|
233
|
+
flex-direction: column;
|
|
234
|
+
gap: 10px;
|
|
235
|
+
margin-top: 14px;
|
|
236
|
+
min-height: 80px;
|
|
237
|
+
padding-bottom: 4px;
|
|
238
|
+
}
|
|
239
|
+
.slot.shadow {
|
|
240
|
+
opacity: 0.35;
|
|
241
|
+
}
|
|
242
|
+
@media (max-width: 768px) {
|
|
243
|
+
.kboard {
|
|
244
|
+
padding: 16px;
|
|
245
|
+
gap: 14px;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
</style>
|