@heyramzi/tasks 0.3.0 → 0.4.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/README.md +102 -9
- package/package.json +24 -1
- package/src/demo.svelte +96 -1
- package/src/icons/board-icon.svelte +5 -3
- package/src/icons/chevron-down.svg +1 -0
- package/src/icons/clock.svg +1 -0
- package/src/icons/close.svg +1 -0
- package/src/icons/milestone-flag.svg +1 -0
- package/src/icons/play.svg +1 -0
- package/src/inbox/icons/call.svg +1 -0
- package/src/inbox/icons/email.svg +1 -0
- package/src/inbox/icons/form.svg +1 -0
- package/src/inbox/icons/slack.svg +1 -0
- package/src/inbox/inbox-card.svelte +279 -0
- package/src/inbox/inbox-draft.svelte +84 -0
- package/src/inbox/inbox-logic.ts +56 -0
- package/src/inbox/inbox-statuses.ts +50 -0
- package/src/inbox/inbox-trace.svelte +44 -0
- package/src/inbox/inbox-types.ts +108 -0
- package/src/inbox/inbox.svelte +203 -0
- package/src/inbox/index.ts +23 -0
- package/src/index.ts +37 -0
- package/src/kanban-board.svelte +420 -103
- package/src/kanban-types.ts +18 -1
- package/src/status-badge.svelte +18 -5
- package/src/status-dot.svelte +30 -10
- package/src/status-types.ts +5 -2
- package/src/task-gantt.svelte +208 -35
- package/src/task-list.svelte +204 -99
- package/src/task-types.ts +13 -0
- package/src/time/duration.ts +69 -0
- package/src/time/time-chip.svelte +62 -0
- package/src/time/time-dot.svelte +58 -0
- package/src/time/time-ring.svelte +74 -0
- package/src/time/time-tracker.svelte +567 -0
- package/src/time/time-types.ts +74 -0
- package/src/tone.ts +8 -5
- package/src/use-kanban-dnd.svelte.ts +6 -1
package/README.md
CHANGED
|
@@ -73,12 +73,26 @@ isn't a drop target (an Overdue lane, say).
|
|
|
73
73
|
| `sortKey` | `string` | Active sort. Defaults to `"manual"`. |
|
|
74
74
|
| `columnHeader` | `Snippet<[KanbanColumn, { count, active }]>` | Optional header. Defaults to label plus count. |
|
|
75
75
|
| `emptyLabel` | `string` | Shown in an empty column. |
|
|
76
|
-
| `dropAtTop` | `(columnId: string) => boolean` | Drops on a capped column go to the top.
|
|
76
|
+
| `dropAtTop` | `(columnId: string) => boolean` | Drops on a capped column go to the top. Defaults to "is `limit` hiding its tail". |
|
|
77
77
|
| `isDroppable` | `(columnId: string) => boolean` | Overrides the per-column `droppable` flag. |
|
|
78
|
-
|
|
79
|
-
`
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
| `manualTiebreak` | `(a: T, b: T) => number` | Settles two cards with the same manual key, or none. |
|
|
79
|
+
| `getLabel` | `(item: T) => string` | The screen-reader name of a card. Defaults to a keyboard hint. |
|
|
80
|
+
| `onSelect` | `(id: string) => void` | Enter or Space on a focused card. |
|
|
81
|
+
| `columnFooter` | `Snippet<[Column, KanbanColumnContext]>` | Under a column's cards: a "Show all" or an add button. |
|
|
82
|
+
| `wrapColumn` | `Snippet<[Column, Snippet<[props]>]>` | Wraps a whole column, for a context menu. Render the inner snippet once with the props you are handed. |
|
|
83
|
+
| `limit` | `(columnId: string) => number \| undefined` | Cards a column shows before the rest fold. `expanded` and `onExpandedChange` lift it per column. |
|
|
84
|
+
| `hidden` | `string[]` | Columns folded into a rail on the right. Fold and unfold animate, and a card dropped on a rail entry brings the column back. `onHiddenChange`, `hiddenLabel`, `showAllLabel` and `hiddenItem` go with it. |
|
|
85
|
+
|
|
86
|
+
`columns` can be your own type, as long as it extends `KanbanColumn`: the
|
|
87
|
+
header, footer and wrap snippets get it back as you passed it, so a column can
|
|
88
|
+
carry a status or a stage for your own markup. The header and footer context is
|
|
89
|
+
`{ count, active, shown, capped, hide, expand }`.
|
|
90
|
+
|
|
91
|
+
`onMove` gives `{ columnId, boardOrder?, beforeId?, afterId? }`. On a manual
|
|
92
|
+
drop `beforeId` and `afterId` name the cards above and below the new slot (null
|
|
93
|
+
at an end), for a backend that orders by neighbour. `boardOrder` is set when you
|
|
94
|
+
pass a `boardOrder` accessor. It's a fractional key that places the card in its
|
|
95
|
+
new slot, so a move writes one row instead of renumbering a column.
|
|
82
96
|
|
|
83
97
|
## List
|
|
84
98
|
|
|
@@ -107,8 +121,31 @@ group. Rows slide into their new place when the order changes.
|
|
|
107
121
|
```
|
|
108
122
|
|
|
109
123
|
Beyond the board's props it takes `head` (a sticky header row), `groupHeader`
|
|
110
|
-
(`Snippet<[group, { count, active, open }]>`), `
|
|
111
|
-
`
|
|
124
|
+
(`Snippet<[group, { count, active, open, depth, hasChildren }]>`), `actions` (controls at the end of
|
|
125
|
+
a group header, shown on hover), `groupFooter` (an add-task row under the rows),
|
|
126
|
+
`collapsible` (default true), `collapsed` (group ids that start folded) and
|
|
127
|
+
`onCollapsedChange`. Pass `flat` to drop the card border, radius and fill so the list sits on the page (the gantt takes it too); group dividers stay. Pass `draggable={false}` for a list that only groups: rows
|
|
128
|
+
lose the drag handlers and the keyboard move, so links and buttons inside them
|
|
129
|
+
keep every key.
|
|
130
|
+
|
|
131
|
+
Pass `getParent` to nest one group under another, such as a project under its
|
|
132
|
+
client. It takes a group and returns the id of its parent, or nothing for a
|
|
133
|
+
top-level group. A child draws inside its parent's section, indented, with its
|
|
134
|
+
own sticky header under the parent's, and folds when the parent folds. A parent
|
|
135
|
+
takes no drops and its `count` adds up its children. A child whose parent id
|
|
136
|
+
matches no group is drawn at the top level.
|
|
137
|
+
|
|
138
|
+
```svelte
|
|
139
|
+
<TaskList
|
|
140
|
+
groups={[
|
|
141
|
+
{ id: "acme", label: "Acme" },
|
|
142
|
+
{ id: "acme-site", label: "Website", parent: "acme" },
|
|
143
|
+
{ id: "acme-audit", label: "Audit", parent: "acme" },
|
|
144
|
+
]}
|
|
145
|
+
getParent={(group) => group.parent}
|
|
146
|
+
...
|
|
147
|
+
/>
|
|
148
|
+
```
|
|
112
149
|
|
|
113
150
|
## Gantt
|
|
114
151
|
|
|
@@ -135,14 +172,18 @@ its end. Leave out `onChange` and the chart is read only.
|
|
|
135
172
|
| Prop | Type | Notes |
|
|
136
173
|
| -------------- | ----------------------------------------- | ----------------------------------------------------------- |
|
|
137
174
|
| `items`, `getId`, `getTitle` | | Required. |
|
|
138
|
-
| `getStart`, `getEnd` | `(item: T) => string`
|
|
175
|
+
| `getStart`, `getEnd` | `(item: T) => string \| null \| undefined` | ISO dates, inclusive. A one-day task has the same start and end. One date alone is a one-day task, none is undated. A parent with no dates draws the span of its children as an outline bar. |
|
|
139
176
|
| `onChange` | `(id, { start, end }) => void \| Promise<void>` | Called after a drag, a resize or a key nudge. |
|
|
140
177
|
| `onSelect` | `(id) => void` | A click or Enter that didn't move the bar. |
|
|
141
178
|
| `getHue` | `(item: T) => string \| undefined` | Bar color. Falls back to the group hue, then `--color-primary`. |
|
|
142
179
|
| `getProgress` | `(item: T) => number \| undefined` | 0 to 1, filled from the left. |
|
|
143
180
|
| `getDependsOn` | `(item: T) => string[] \| undefined` | Draws an arrow from each listed task's end to this one's start. |
|
|
144
181
|
| `groups`, `getGroup` | | Optional. Groups rows under a header with a span line. |
|
|
145
|
-
| `
|
|
182
|
+
| `getParent` | `(item: T) => string \| null \| undefined` | Nests a task under another by id: indented, foldable, depth first. |
|
|
183
|
+
| `collapsed` | `string[]` | Parent ids that start folded. |
|
|
184
|
+
| `isMilestone` | `(item: T) => boolean` | Draws a flag at the end date instead of a bar. Not draggable. |
|
|
185
|
+
| `noDatesLabel` | `string` | Shown for a task with no dates. Default "No dates". |
|
|
186
|
+
| `renderLabel` | `Snippet<[T, { depth, hasChildren, open }]>` | The left column. Defaults to the title. |
|
|
146
187
|
| `from`, `to` | `string` | The shown range. Defaults to the tasks' span, padded, from a Monday. |
|
|
147
188
|
| `today` | `string` | Where the today line sits. Defaults to the viewer's today. |
|
|
148
189
|
| `dayWidth` | `number` | Default 32. Under 18 the header shows week starts only. |
|
|
@@ -158,6 +199,54 @@ with a pill that slides to the active option. Pass `views`
|
|
|
158
199
|
(`{ id, label, icon? }[]`) and `bind:value`. `BoardIcon`, `ListIcon` and
|
|
159
200
|
`GanttIcon` are exported for it.
|
|
160
201
|
|
|
202
|
+
## Time tracking
|
|
203
|
+
|
|
204
|
+
Optional. `TimeTracker` logs time against one task: a start and stop timer, quick
|
|
205
|
+
entry that reads `1h30`, `90m`, `1.5h` or `1:30`, a date and a note, entries
|
|
206
|
+
grouped by day with day totals, a per-person split and an optional estimate shown
|
|
207
|
+
as a ring that turns warning at 90% and destructive past 100%. It holds no data
|
|
208
|
+
and fetches nothing. You pass entries in and persist what it hands back.
|
|
209
|
+
|
|
210
|
+
```svelte
|
|
211
|
+
<TimeTracker
|
|
212
|
+
entries={entries}
|
|
213
|
+
getId={(e) => e.id}
|
|
214
|
+
getMinutes={(e) => e.minutes}
|
|
215
|
+
getDay={(e) => e.day}
|
|
216
|
+
getPerson={(e) => e.person}
|
|
217
|
+
estimate={480}
|
|
218
|
+
{running}
|
|
219
|
+
onStart={() => (running = { startedAt: Date.now() })}
|
|
220
|
+
onStop={(info) => save(info.minutes, info.day)}
|
|
221
|
+
onLog={(input) => save(input.minutes, input.day, input.note)}
|
|
222
|
+
onDelete={(entry) => remove(entry.id)}
|
|
223
|
+
/>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| Prop | Type | Notes |
|
|
227
|
+
| --- | --- | --- |
|
|
228
|
+
| `entries` | `T[]` | Every entry on the task. |
|
|
229
|
+
| `getId`, `getMinutes`, `getDay`, `getPerson` | accessors | `getDay` returns `YYYY-MM-DD`. |
|
|
230
|
+
| `getPersonId`, `getNote` | accessors | Optional. The id keeps two people with one name apart. |
|
|
231
|
+
| `running` | `{ startedAt } \| null` | The host stores it, so it survives a reload. |
|
|
232
|
+
| `estimate` | `number` | Minutes budgeted. Draws the ring. |
|
|
233
|
+
| `onStart` | `() => void` | The host sets `running`. |
|
|
234
|
+
| `onStop` | `(info) => void` | `info` is `{ minutes, startedAt, stoppedAt, day }`. Clear `running` after saving. |
|
|
235
|
+
| `onLog` | `(input) => void \| boolean` | `input` is `{ minutes, day, note? }`. Return `false` to keep the fields. |
|
|
236
|
+
| `onDelete`, `canDelete` | callback | The delete button shows when `onDelete` is set and `canDelete` allows it. |
|
|
237
|
+
| `renderMeta` | `Snippet<[T]>` | Extra text after a row's note. |
|
|
238
|
+
| `flat` | `boolean` | Drops the card (border, radius, background, shadow, padding) for a host that already has a panel. The running state then shows through the dot, readout and button. |
|
|
239
|
+
| `locale`, `labels`, `class` | | `labels` overrides any string in `DEFAULT_TIME_LABELS`. |
|
|
240
|
+
|
|
241
|
+
Alt+S starts and stops the timer. A screen reader hears the start, one update a
|
|
242
|
+
minute and the stop. Import it alone from `@heyramzi/tasks/time`.
|
|
243
|
+
|
|
244
|
+
For boards and lists: `TimeChip` (`minutes`, `estimate?`, `running?`) shows the
|
|
245
|
+
logged total, with a pulsing dot while a timer runs and a small ring against an
|
|
246
|
+
estimate. `TimeRing` and `TimeDot` are the pieces under it.
|
|
247
|
+
`parseMinutes`, `formatMinutes`, `formatClock`, `budgetTone` and `localDay` are
|
|
248
|
+
exported helpers.
|
|
249
|
+
|
|
161
250
|
## Also exported
|
|
162
251
|
|
|
163
252
|
- `KanbanColumn`: the column shell, if you want to build your own board.
|
|
@@ -167,12 +256,16 @@ with a pill that slides to the active option. Pass `views`
|
|
|
167
256
|
- `reorderByIds`: move an item to another item's index in a flat list.
|
|
168
257
|
- `addDays`, `dayNumber`, `isoFromDay`, `todayIso`: the date helpers the gantt uses.
|
|
169
258
|
- `StatusBadge`, `StatusDot`, `StatusIcon`: chips, dots and icons from one config.
|
|
259
|
+
A badge also takes its text as children, for a caller that already has the word.
|
|
170
260
|
- `defineStatuses`: builds a status registry from a plain map.
|
|
171
261
|
- `TONE_CLASS`, `TONE_COLOR`, `TONE_ICON`, `TONE_TEXT`, `insetWellClass`: the severity scale.
|
|
172
262
|
- `salesStatuses`: an example pipeline map.
|
|
173
263
|
|
|
174
264
|
## Styling
|
|
175
265
|
|
|
266
|
+
The accent tone reads `--tone-accent` and falls back to `--color-primary`. Set
|
|
267
|
+
`--tone-accent` when your primary is a neutral and your accent is a brand color.
|
|
268
|
+
|
|
176
269
|
Classes are Tailwind, on shadcn-style tokens (`bg-card`, `border-border`,
|
|
177
270
|
`text-muted-foreground`, `--color-primary`). Point Tailwind at the package with
|
|
178
271
|
`@source` so it generates them. Motion reads the `--duration-*` and `--ease-*`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyramzi/tasks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Svelte 5 task views: a drag-and-drop board, a grouped list and a gantt timeline from one config, plus status badges, dots and icons.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -54,6 +54,26 @@
|
|
|
54
54
|
"types": "./src/status-badge.svelte",
|
|
55
55
|
"default": "./src/status-badge.svelte"
|
|
56
56
|
},
|
|
57
|
+
"./status-dot": {
|
|
58
|
+
"svelte": "./src/status-dot.svelte",
|
|
59
|
+
"types": "./src/status-dot.svelte",
|
|
60
|
+
"default": "./src/status-dot.svelte"
|
|
61
|
+
},
|
|
62
|
+
"./statuses": {
|
|
63
|
+
"svelte": "./src/define-statuses.ts",
|
|
64
|
+
"types": "./src/define-statuses.ts",
|
|
65
|
+
"default": "./src/define-statuses.ts"
|
|
66
|
+
},
|
|
67
|
+
"./time": {
|
|
68
|
+
"svelte": "./src/time/time-tracker.svelte",
|
|
69
|
+
"types": "./src/time/time-tracker.svelte",
|
|
70
|
+
"default": "./src/time/time-tracker.svelte"
|
|
71
|
+
},
|
|
72
|
+
"./inbox": {
|
|
73
|
+
"svelte": "./src/inbox/index.ts",
|
|
74
|
+
"types": "./src/inbox/index.ts",
|
|
75
|
+
"default": "./src/inbox/index.ts"
|
|
76
|
+
},
|
|
57
77
|
"./presets/sales": {
|
|
58
78
|
"svelte": "./src/presets/sales.ts",
|
|
59
79
|
"types": "./src/presets/sales.ts",
|
|
@@ -66,9 +86,12 @@
|
|
|
66
86
|
"./demo.svelte": "./src/demo.svelte"
|
|
67
87
|
},
|
|
68
88
|
"peerDependencies": {
|
|
89
|
+
"@heyramzi/ui": "*",
|
|
69
90
|
"svelte": "^5.0.0"
|
|
70
91
|
},
|
|
71
92
|
"devDependencies": {
|
|
93
|
+
"@heyramzi/ui": "7.0.0",
|
|
94
|
+
"@types/node": "^26.6.4",
|
|
72
95
|
"svelte-check": "^4.7.6",
|
|
73
96
|
"typescript": "~7.0.2"
|
|
74
97
|
},
|
package/src/demo.svelte
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
import { salesStatuses } from "./presets/sales.js";
|
|
14
14
|
import type { KanbanMove } from "./kanban-types.js";
|
|
15
15
|
import type { GanttChange } from "./task-types.js";
|
|
16
|
+
import TimeTracker from "./time/time-tracker.svelte";
|
|
17
|
+
import TimeChip from "./time/time-chip.svelte";
|
|
18
|
+
import type { TimeLogInput, TimeStopInfo } from "./time/time-types.js";
|
|
16
19
|
|
|
17
20
|
type Priority = "low" | "medium" | "high";
|
|
18
21
|
|
|
@@ -107,6 +110,52 @@
|
|
|
107
110
|
tasks = tasks.map((task) => (task.id === id ? { ...task, ...change } : task));
|
|
108
111
|
}
|
|
109
112
|
|
|
113
|
+
// ---- time tracking, an option the host turns on
|
|
114
|
+
type Entry = { id: string; taskId: string; person: string; day: string; minutes: number; note?: string };
|
|
115
|
+
|
|
116
|
+
const estimates: Record<string, number> = { t3: 360, t4: 480, t5: 180, t6: 240 };
|
|
117
|
+
|
|
118
|
+
let tracking = $state(true);
|
|
119
|
+
let timeTaskId = $state("t4");
|
|
120
|
+
let nextEntry = 100;
|
|
121
|
+
let entries = $state<Entry[]>([
|
|
122
|
+
{ id: "e1", taskId: "t4", person: "Dana", day: on(-3), minutes: 150, note: "Signup and email step" },
|
|
123
|
+
{ id: "e2", taskId: "t4", person: "Ada", day: on(-2), minutes: 95, note: "Copy for the welcome screen" },
|
|
124
|
+
{ id: "e3", taskId: "t4", person: "Dana", day: on(-2), minutes: 125, note: "Progress bar and empty states" },
|
|
125
|
+
{ id: "e4", taskId: "t4", person: "Dana", day: on(-1), minutes: 80 },
|
|
126
|
+
{ id: "e5", taskId: "t3", person: "Ada", day: on(-5), minutes: 210, note: "First draft" },
|
|
127
|
+
{ id: "e6", taskId: "t3", person: "Bo", day: on(-3), minutes: 95, note: "Pricing table" },
|
|
128
|
+
{ id: "e7", taskId: "t3", person: "Ada", day: on(-1), minutes: 85, note: "Review changes" },
|
|
129
|
+
{ id: "e8", taskId: "t5", person: "Bo", day: on(0), minutes: 40 },
|
|
130
|
+
{ id: "e9", taskId: "t1", person: "Bo", day: on(-12), minutes: 60 },
|
|
131
|
+
]);
|
|
132
|
+
// The host keeps the running timer. A reload would read it back from storage.
|
|
133
|
+
let timer = $state<{ taskId: string; startedAt: number } | null>({
|
|
134
|
+
taskId: "t4",
|
|
135
|
+
startedAt: Date.now() - (12 * 60 + 41) * 1000,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let minutesByTask = $derived.by(() => {
|
|
139
|
+
const totals: Record<string, number> = {};
|
|
140
|
+
for (const entry of entries) totals[entry.taskId] = (totals[entry.taskId] ?? 0) + entry.minutes;
|
|
141
|
+
return totals;
|
|
142
|
+
});
|
|
143
|
+
let timeEntries = $derived(entries.filter((entry) => entry.taskId === timeTaskId));
|
|
144
|
+
let timeRunning = $derived(timer?.taskId === timeTaskId ? { startedAt: timer.startedAt } : null);
|
|
145
|
+
|
|
146
|
+
function addEntry(taskId: string, day: string, minutes: number, note?: string) {
|
|
147
|
+
entries = [{ id: `e${nextEntry++}`, taskId, person: "Dana", day, minutes, note }, ...entries];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function onLog(input: TimeLogInput) {
|
|
151
|
+
addEntry(timeTaskId, input.day, input.minutes, input.note);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function onStop(info: TimeStopInfo) {
|
|
155
|
+
if (timer) addEntry(timer.taskId, info.day, info.minutes);
|
|
156
|
+
timer = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
110
159
|
const listGrid = "grid grid-cols-[minmax(0,1fr)_7rem_5.5rem_4.5rem] items-center gap-3 max-sm:grid-cols-[minmax(0,1fr)_4.5rem]";
|
|
111
160
|
</script>
|
|
112
161
|
|
|
@@ -124,7 +173,11 @@
|
|
|
124
173
|
<div class="flex h-[600px] flex-col gap-3">
|
|
125
174
|
<div class="flex flex-wrap items-center gap-2">
|
|
126
175
|
<TaskViewSwitch {views} bind:value={view} label="Task view" />
|
|
127
|
-
<div class="ml-auto flex items-center gap-
|
|
176
|
+
<div class="ml-auto flex items-center gap-3">
|
|
177
|
+
<label class="flex cursor-pointer items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
|
178
|
+
<input type="checkbox" bind:checked={tracking} class="size-3.5 accent-[var(--tone-accent,var(--color-primary))]" />
|
|
179
|
+
Time tracking
|
|
180
|
+
</label>
|
|
128
181
|
{#if view === "gantt"}
|
|
129
182
|
<TaskViewSwitch
|
|
130
183
|
views={[
|
|
@@ -170,6 +223,9 @@
|
|
|
170
223
|
<div class="mt-2.5 flex items-center gap-2 text-xs">
|
|
171
224
|
{@render avatar(task.assignee)}
|
|
172
225
|
<span class={d.late ? "font-medium text-destructive" : "text-muted-foreground"}>{d.label}</span>
|
|
226
|
+
{#if tracking}
|
|
227
|
+
<TimeChip minutes={minutesByTask[task.id] ?? 0} estimate={estimates[task.id]} running={timer?.taskId === task.id} />
|
|
228
|
+
{/if}
|
|
173
229
|
<span class="ml-auto rounded-full px-1.5 py-0.5 font-medium capitalize {priorityClass[task.priority]}">
|
|
174
230
|
{task.priority}
|
|
175
231
|
</span>
|
|
@@ -206,6 +262,9 @@
|
|
|
206
262
|
<span class="truncate" class:text-muted-foreground={task.status === "done"} class:line-through={task.status === "done"}>
|
|
207
263
|
{task.title}
|
|
208
264
|
</span>
|
|
265
|
+
{#if tracking}
|
|
266
|
+
<TimeChip minutes={minutesByTask[task.id] ?? 0} estimate={estimates[task.id]} running={timer?.taskId === task.id} />
|
|
267
|
+
{/if}
|
|
209
268
|
</span>
|
|
210
269
|
<span class="flex min-w-0 items-center gap-2 text-muted-foreground max-sm:hidden">
|
|
211
270
|
{@render avatar(task.assignee)}
|
|
@@ -259,6 +318,42 @@
|
|
|
259
318
|
</p>
|
|
260
319
|
</div>
|
|
261
320
|
|
|
321
|
+
{#if tracking}
|
|
322
|
+
<section class="flex flex-col gap-3 border-t border-border pt-4">
|
|
323
|
+
<div class="flex items-center justify-between gap-3">
|
|
324
|
+
<h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Task time</h3>
|
|
325
|
+
<select
|
|
326
|
+
bind:value={timeTaskId}
|
|
327
|
+
aria-label="Task"
|
|
328
|
+
class="h-8 max-w-[60%] rounded-md border border-border bg-background px-2 text-xs text-foreground"
|
|
329
|
+
>
|
|
330
|
+
{#each tasks as task (task.id)}
|
|
331
|
+
<option value={task.id}>{task.title}</option>
|
|
332
|
+
{/each}
|
|
333
|
+
</select>
|
|
334
|
+
</div>
|
|
335
|
+
<TimeTracker
|
|
336
|
+
class="mx-auto w-full max-w-xl"
|
|
337
|
+
entries={timeEntries}
|
|
338
|
+
getId={(entry) => entry.id}
|
|
339
|
+
getMinutes={(entry) => entry.minutes}
|
|
340
|
+
getDay={(entry) => entry.day}
|
|
341
|
+
getPerson={(entry) => entry.person}
|
|
342
|
+
getNote={(entry) => entry.note}
|
|
343
|
+
estimate={estimates[timeTaskId]}
|
|
344
|
+
running={timeRunning}
|
|
345
|
+
onStart={() => {
|
|
346
|
+
timer = { taskId: timeTaskId, startedAt: Date.now() };
|
|
347
|
+
}}
|
|
348
|
+
{onStop}
|
|
349
|
+
{onLog}
|
|
350
|
+
onDelete={(entry) => {
|
|
351
|
+
entries = entries.filter((row) => row.id !== entry.id);
|
|
352
|
+
}}
|
|
353
|
+
/>
|
|
354
|
+
</section>
|
|
355
|
+
{/if}
|
|
356
|
+
|
|
262
357
|
<section class="flex flex-col gap-2 border-t border-border pt-4">
|
|
263
358
|
<h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Status badges</h3>
|
|
264
359
|
<div class="flex flex-wrap items-center gap-2">
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true" class={className}>
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
<g stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
|
7
|
+
<line x1="4" y1="3" x2="4" y2="11"></line>
|
|
8
|
+
<line x1="8" y1="3" x2="8" y2="7.5"></line>
|
|
9
|
+
<line x1="12" y1="3" x2="12" y2="13"></line>
|
|
10
|
+
</g>
|
|
9
11
|
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 3.5 5 6.5 8 3.5"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M8 4.75V8l2.25 1.5"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="m4 4 8 8M12 4l-8 8"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 2v12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/><path d="M4.75 2.75h8l-2 3 2 3h-8z" fill="currentColor"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.6v12.8a1 1 0 0 0 1.5.86l10.6-6.4a1 1 0 0 0 0-1.72L9.5 4.74A1 1 0 0 0 8 5.6Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 2.5h2l1 3-1.5 1a7 7 0 0 0 4 4l1-1.5 3 1v2a1.5 1.5 0 0 1-1.5 1.5A10.5 10.5 0 0 1 2 4a1.5 1.5 0 0 1 1.5-1.5Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3.5" width="12" height="9" rx="1.5"/><path d="m2.5 4.5 5.5 4 5.5-4"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="2" width="10" height="12" rx="1.5"/><path d="M5.5 5.5h5M5.5 8h5M5.5 10.5h3"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 2.5 5.5 13.5M10.5 2.5l-1 11M3 6h10.5M2.5 10H13"/></svg>
|