@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/src/status-badge.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
2
3
|
import { cn } from "./lib/cn.js";
|
|
3
4
|
import { TONE_CLASS } from "./tone.js";
|
|
4
5
|
import type { Tone, Statuses } from "./status-types.js";
|
|
@@ -7,13 +8,18 @@
|
|
|
7
8
|
type Props = {
|
|
8
9
|
/** A registry from `defineStatuses`. Supplies the label and tone. */
|
|
9
10
|
statuses?: Statuses;
|
|
10
|
-
status
|
|
11
|
+
/** The status id to look up in `statuses`. Leave it out when you pass `children`. */
|
|
12
|
+
status?: string;
|
|
11
13
|
/** Override the registry label. */
|
|
12
14
|
label?: string;
|
|
13
15
|
/** Override the registry tone. */
|
|
14
16
|
tone?: Tone;
|
|
15
17
|
/** Shows a spinner: a background job is still working on the record. */
|
|
16
18
|
busy?: boolean;
|
|
19
|
+
/** Capitalizes the first letter of each word. Turn off for a name or a number. */
|
|
20
|
+
capitalize?: boolean;
|
|
21
|
+
/** The chip's text, for a caller that already has the word. Wins over `label` and the registry. */
|
|
22
|
+
children?: Snippet;
|
|
17
23
|
class?: string;
|
|
18
24
|
};
|
|
19
25
|
|
|
@@ -23,16 +29,19 @@
|
|
|
23
29
|
label,
|
|
24
30
|
tone,
|
|
25
31
|
busy = false,
|
|
32
|
+
capitalize = true,
|
|
33
|
+
children,
|
|
26
34
|
class: className,
|
|
27
35
|
}: Props = $props();
|
|
28
36
|
|
|
29
|
-
let resolvedTone = $derived(tone ?? (statuses ? statuses.tone(status) : "neutral"));
|
|
30
|
-
let resolvedLabel = $derived(label ?? (statuses ? statuses.label(status) : status));
|
|
37
|
+
let resolvedTone = $derived(tone ?? (statuses && status ? statuses.tone(status) : "neutral"));
|
|
38
|
+
let resolvedLabel = $derived(label ?? (statuses && status ? statuses.label(status) : (status ?? "")));
|
|
31
39
|
</script>
|
|
32
40
|
|
|
33
41
|
<span
|
|
34
42
|
class={cn(
|
|
35
|
-
"inline-flex shrink-0 items-center gap-1 rounded-full border px-2
|
|
43
|
+
"inline-flex h-5 shrink-0 items-center justify-center gap-1 rounded-full border px-2 text-xs font-medium whitespace-nowrap",
|
|
44
|
+
capitalize && "capitalize",
|
|
36
45
|
TONE_CLASS[resolvedTone],
|
|
37
46
|
className,
|
|
38
47
|
)}
|
|
@@ -40,5 +49,9 @@
|
|
|
40
49
|
{#if busy}
|
|
41
50
|
<span class="size-3 animate-spin [&>svg]:size-full" aria-hidden="true">{@html spinnerIcon}</span>
|
|
42
51
|
{/if}
|
|
43
|
-
{
|
|
52
|
+
{#if children}
|
|
53
|
+
{@render children()}
|
|
54
|
+
{:else}
|
|
55
|
+
{resolvedLabel}
|
|
56
|
+
{/if}
|
|
44
57
|
</span>
|
package/src/status-dot.svelte
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { TONE_COLOR } from "./tone.js";
|
|
4
4
|
import type { StatusDotVariant, Statuses } from "./status-types.js";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
interface Props {
|
|
7
7
|
/** A registry from `defineStatuses`. Resolves color, variant and progress. */
|
|
8
8
|
statuses?: Statuses;
|
|
9
9
|
status?: string;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
size?: number;
|
|
15
15
|
animate?: boolean;
|
|
16
16
|
class?: string;
|
|
17
|
-
}
|
|
17
|
+
}
|
|
18
18
|
|
|
19
19
|
let {
|
|
20
20
|
statuses,
|
|
@@ -66,17 +66,16 @@
|
|
|
66
66
|
viewBox="0 0 14 14"
|
|
67
67
|
fill="none"
|
|
68
68
|
aria-hidden="true"
|
|
69
|
-
class={cn("
|
|
69
|
+
class={cn("sd", className)}
|
|
70
70
|
>
|
|
71
71
|
<circle cx="7" cy="7" r="6" fill={resolvedColor} class={animate ? "sd-pop" : ""}></circle>
|
|
72
72
|
<polyline
|
|
73
73
|
points="4.5,7 6.5,9 9.5,5"
|
|
74
74
|
fill="none"
|
|
75
|
-
|
|
75
|
+
class={cn("sd-mark", animate && "sd-check")}
|
|
76
76
|
stroke-width="1.5"
|
|
77
77
|
stroke-linecap="round"
|
|
78
78
|
stroke-linejoin="round"
|
|
79
|
-
class={animate ? "sd-check" : ""}
|
|
80
79
|
></polyline>
|
|
81
80
|
</svg>
|
|
82
81
|
{:else if resolvedVariant === "lost"}
|
|
@@ -86,10 +85,10 @@
|
|
|
86
85
|
viewBox="0 0 14 14"
|
|
87
86
|
fill="none"
|
|
88
87
|
aria-hidden="true"
|
|
89
|
-
class={cn("
|
|
88
|
+
class={cn("sd", className)}
|
|
90
89
|
>
|
|
91
90
|
<circle cx="7" cy="7" r="6" fill={resolvedColor} class={animate ? "sd-pop" : ""}></circle>
|
|
92
|
-
<g
|
|
91
|
+
<g class="sd-mark" stroke-width="1.5" stroke-linecap="round">
|
|
93
92
|
<line x1="5" y1="5" x2="9" y2="9" class={animate ? "sd-cross" : ""}></line>
|
|
94
93
|
<line x1="9" y1="5" x2="5" y2="9" class={animate ? "sd-cross sd-cross-second" : ""}></line>
|
|
95
94
|
</g>
|
|
@@ -101,7 +100,7 @@
|
|
|
101
100
|
viewBox="0 0 14 14"
|
|
102
101
|
fill="none"
|
|
103
102
|
aria-hidden="true"
|
|
104
|
-
class={cn("
|
|
103
|
+
class={cn("sd", className)}
|
|
105
104
|
>
|
|
106
105
|
<circle cx="7" cy="7" r="6" fill={resolvedColor} class={animate ? "sd-pop" : ""}></circle>
|
|
107
106
|
</svg>
|
|
@@ -112,9 +111,16 @@
|
|
|
112
111
|
viewBox="0 0 14 14"
|
|
113
112
|
fill="none"
|
|
114
113
|
aria-hidden="true"
|
|
115
|
-
class={cn("
|
|
114
|
+
class={cn("sd", className)}
|
|
116
115
|
>
|
|
117
|
-
<circle
|
|
116
|
+
<circle
|
|
117
|
+
cx="7"
|
|
118
|
+
cy="7"
|
|
119
|
+
r="5.5"
|
|
120
|
+
stroke={resolvedColor}
|
|
121
|
+
stroke-width="1.5"
|
|
122
|
+
class={resolvedVariant === "dashed" ? "sd-dashed" : ""}
|
|
123
|
+
></circle>
|
|
118
124
|
{#if dasharray}
|
|
119
125
|
<circle
|
|
120
126
|
cx="7"
|
|
@@ -133,6 +139,20 @@
|
|
|
133
139
|
{/if}
|
|
134
140
|
|
|
135
141
|
<style>
|
|
142
|
+
.sd {
|
|
143
|
+
flex-shrink: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* The tick and cross read as cut out of the disc. A host sets the mark to its own ground. */
|
|
147
|
+
.sd-mark {
|
|
148
|
+
stroke: var(--status-dot-mark, white);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* 12 dashes that close evenly: 2 * PI * 5.5 = 34.56, cut into 12 periods of 2.88. */
|
|
152
|
+
.sd-dashed {
|
|
153
|
+
stroke-dasharray: 1.3 1.58;
|
|
154
|
+
}
|
|
155
|
+
|
|
136
156
|
.sd-fill {
|
|
137
157
|
transform-origin: 7px 7px;
|
|
138
158
|
animation: sd-fill-in var(--duration-slow, 320ms) var(--ease-smooth-out, ease-out) forwards;
|
package/src/status-types.ts
CHANGED
|
@@ -3,8 +3,11 @@ import type { Component } from "svelte";
|
|
|
3
3
|
/** The one severity scale the kit ships. */
|
|
4
4
|
export type Tone = "neutral" | "accent" | "info" | "success" | "warning" | "destructive";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The shape the dot can take. `progress` needs a `progress` value between 0 and 1.
|
|
8
|
+
* `dashed` is an open ring cut into dashes, for an aggregate such as "All".
|
|
9
|
+
*/
|
|
10
|
+
export type StatusDotVariant = "open" | "dashed" | "progress" | "filled" | "completed" | "lost";
|
|
8
11
|
|
|
9
12
|
export type StatusDef = {
|
|
10
13
|
label: string;
|
package/src/task-gantt.svelte
CHANGED
|
@@ -3,16 +3,32 @@
|
|
|
3
3
|
import { cn } from "./lib/cn.js";
|
|
4
4
|
import { dayNumber, isoFromDay, todayIso, weekday } from "./dates.js";
|
|
5
5
|
import type { KanbanColumn as KanbanColumnDef } from "./kanban-types.js";
|
|
6
|
-
import type { GanttChange } from "./task-types.js";
|
|
6
|
+
import type { GanttChange, GanttLabelContext } from "./task-types.js";
|
|
7
|
+
import chevronIcon from "./icons/chevron-right.svg?raw";
|
|
8
|
+
import flagIcon from "./icons/milestone-flag.svg?raw";
|
|
7
9
|
|
|
8
10
|
type Props<T> = {
|
|
9
11
|
items: T[];
|
|
10
12
|
getId: (item: T) => string;
|
|
11
13
|
getTitle: (item: T) => string;
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* ISO date, inclusive. Null or empty means undated: with an end date alone the task
|
|
16
|
+
* starts the same day, and with neither it has no bar of its own.
|
|
17
|
+
*/
|
|
18
|
+
getStart: (item: T) => string | null | undefined;
|
|
19
|
+
/** ISO date, inclusive. See `getStart`. */
|
|
20
|
+
getEnd: (item: T) => string | null | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The id of the task this one sits under. Children nest under their parent, indented
|
|
23
|
+
* and foldable. A parent with no dates of its own draws the span of what is below it.
|
|
24
|
+
*/
|
|
25
|
+
getParent?: (item: T) => string | null | undefined;
|
|
26
|
+
/** Draws a flag at the end date instead of a bar. Milestones are not draggable. */
|
|
27
|
+
isMilestone?: (item: T) => boolean;
|
|
28
|
+
/** Parent ids that start folded. */
|
|
29
|
+
collapsed?: string[];
|
|
30
|
+
/** Shown in place of a bar for an undated task. */
|
|
31
|
+
noDatesLabel?: string;
|
|
16
32
|
/** Any CSS color for the bar. Falls back to the group hue, then the primary color. */
|
|
17
33
|
getHue?: (item: T) => string | undefined;
|
|
18
34
|
/** 0 to 1. Fills the bar from the left. */
|
|
@@ -27,7 +43,7 @@
|
|
|
27
43
|
/** Called on a click or Enter that did not move the bar. */
|
|
28
44
|
onSelect?: (id: string) => void;
|
|
29
45
|
/** The left column for a row. Defaults to the title. */
|
|
30
|
-
renderLabel?: Snippet<[T]>;
|
|
46
|
+
renderLabel?: Snippet<[T, GanttLabelContext]>;
|
|
31
47
|
/** First and last day shown. Default: the tasks' span, padded, starting on a Monday. */
|
|
32
48
|
from?: string;
|
|
33
49
|
to?: string;
|
|
@@ -39,6 +55,8 @@
|
|
|
39
55
|
labelWidth?: number;
|
|
40
56
|
labelHeading?: string;
|
|
41
57
|
locale?: string;
|
|
58
|
+
/** Drops the card chrome (border, radius, fill) so the chart sits flat on the page. */
|
|
59
|
+
flat?: boolean;
|
|
42
60
|
class?: string;
|
|
43
61
|
};
|
|
44
62
|
|
|
@@ -48,6 +66,10 @@
|
|
|
48
66
|
getTitle,
|
|
49
67
|
getStart,
|
|
50
68
|
getEnd,
|
|
69
|
+
getParent,
|
|
70
|
+
isMilestone,
|
|
71
|
+
collapsed = [],
|
|
72
|
+
noDatesLabel = "No dates",
|
|
51
73
|
getHue,
|
|
52
74
|
getProgress,
|
|
53
75
|
getDependsOn,
|
|
@@ -64,6 +86,7 @@
|
|
|
64
86
|
labelWidth = 240,
|
|
65
87
|
labelHeading = "Task",
|
|
66
88
|
locale = "en-US",
|
|
89
|
+
flat = false,
|
|
67
90
|
class: className,
|
|
68
91
|
}: Props<T> = $props();
|
|
69
92
|
|
|
@@ -71,13 +94,25 @@
|
|
|
71
94
|
type Drag = { id: string; mode: "move" | "start" | "end"; originX: number; dx: number };
|
|
72
95
|
type Row =
|
|
73
96
|
| { kind: "group"; group: KanbanColumnDef; count: number; span: Span | null }
|
|
74
|
-
| {
|
|
97
|
+
| {
|
|
98
|
+
kind: "task";
|
|
99
|
+
item: T;
|
|
100
|
+
id: string;
|
|
101
|
+
order: number;
|
|
102
|
+
depth: number;
|
|
103
|
+
hasChildren: boolean;
|
|
104
|
+
open: boolean;
|
|
105
|
+
/** The task's own dates, with a live drag applied. Null when undated. */
|
|
106
|
+
own: Span | null;
|
|
107
|
+
/** What the row draws: its own span, else the span of what is below it. */
|
|
108
|
+
span: Span | null;
|
|
109
|
+
};
|
|
75
110
|
|
|
76
111
|
// Bars past this row enter together, so a long chart never waits on its stagger.
|
|
77
112
|
const STAGGER_CAP = 10;
|
|
78
113
|
// A bar narrower than this shows its title outside, to the right.
|
|
79
114
|
const INLINE_TITLE_MIN = 72;
|
|
80
|
-
const
|
|
115
|
+
const INDENT = 14;
|
|
81
116
|
|
|
82
117
|
let scroller = $state<HTMLDivElement>();
|
|
83
118
|
let corner = $state<HTMLDivElement>();
|
|
@@ -87,13 +122,26 @@
|
|
|
87
122
|
|
|
88
123
|
let todayDay = $derived(dayNumber(today));
|
|
89
124
|
let editable = $derived(!!onChange);
|
|
125
|
+
// Under 18px a day is too narrow for a gap either side of a bar.
|
|
126
|
+
let inset = $derived(dayWidth >= 18 ? 3 : 0);
|
|
127
|
+
// svelte-ignore state_referenced_locally
|
|
128
|
+
let folded = $state(new Set(collapsed));
|
|
129
|
+
|
|
130
|
+
function toggle(id: string): void {
|
|
131
|
+
const next = new Set(folded);
|
|
132
|
+
if (next.has(id)) next.delete(id);
|
|
133
|
+
else next.add(id);
|
|
134
|
+
folded = next;
|
|
135
|
+
}
|
|
90
136
|
|
|
91
137
|
let range = $derived.by(() => {
|
|
92
138
|
let lo = todayDay;
|
|
93
139
|
let hi = todayDay;
|
|
94
140
|
for (const item of items) {
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
const span = spanOf(item);
|
|
142
|
+
if (!span) continue;
|
|
143
|
+
lo = Math.min(lo, span.start);
|
|
144
|
+
hi = Math.max(hi, span.end);
|
|
97
145
|
}
|
|
98
146
|
let first = from ? dayNumber(from) : lo - 7;
|
|
99
147
|
// Day 0 was a Thursday. Step back to the Monday on or before `first`.
|
|
@@ -128,15 +176,19 @@
|
|
|
128
176
|
return out;
|
|
129
177
|
});
|
|
130
178
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
179
|
+
/** The task's own dates. A lone date stands for a one-day task. */
|
|
180
|
+
function spanOf(item: T): Span | null {
|
|
181
|
+
const first = getStart(item) || getEnd(item);
|
|
182
|
+
const last = getEnd(item) || getStart(item);
|
|
183
|
+
if (!first || !last) return null;
|
|
184
|
+
const start = dayNumber(first);
|
|
185
|
+
return { start, end: Math.max(start, dayNumber(last)) };
|
|
134
186
|
}
|
|
135
187
|
|
|
136
188
|
/** A bar's span, with the live drag applied. Resizes never cross the other edge. */
|
|
137
|
-
function liveSpan(item: T, id: string): Span {
|
|
189
|
+
function liveSpan(item: T, id: string): Span | null {
|
|
138
190
|
const span = spanOf(item);
|
|
139
|
-
if (drag?.id !== id || drag.dx === 0) return span;
|
|
191
|
+
if (!span || drag?.id !== id || drag.dx === 0) return span;
|
|
140
192
|
if (drag.mode === "move") return { start: span.start + drag.dx, end: span.end + drag.dx };
|
|
141
193
|
if (drag.mode === "start") return { start: Math.min(span.start + drag.dx, span.end), end: span.end };
|
|
142
194
|
return { start: span.start, end: Math.max(span.end + drag.dx, span.start) };
|
|
@@ -145,20 +197,55 @@
|
|
|
145
197
|
let rows = $derived.by(() => {
|
|
146
198
|
const out: Row[] = [];
|
|
147
199
|
let order = 0;
|
|
148
|
-
const
|
|
200
|
+
const join = (a: Span | null, b: Span | null): Span | null =>
|
|
201
|
+
!a ? b : !b ? a : { start: Math.min(a.start, b.start), end: Math.max(a.end, b.end) };
|
|
202
|
+
|
|
203
|
+
// Depth first, so a parent's row comes before its children. A parent's span is known
|
|
204
|
+
// only after its children are, so the row is pushed first and filled in after.
|
|
205
|
+
const emit = (members: T[]) => {
|
|
206
|
+
if (!getParent) {
|
|
207
|
+
for (const item of members) {
|
|
208
|
+
const id = getId(item);
|
|
209
|
+
const own = liveSpan(item, id);
|
|
210
|
+
out.push({ kind: "task", item, id, order: order++, depth: 0, hasChildren: false, open: true, own, span: own });
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const ids = new Set(members.map((item) => getId(item)));
|
|
215
|
+
const kids = new Map<string | null, T[]>();
|
|
216
|
+
for (const item of members) {
|
|
217
|
+
const parent = getParent(item);
|
|
218
|
+
const key = parent && ids.has(parent) ? parent : null;
|
|
219
|
+
kids.set(key, [...(kids.get(key) ?? []), item]);
|
|
220
|
+
}
|
|
221
|
+
const walk = (item: T, depth: number, visible: boolean): Span | null => {
|
|
222
|
+
const id = getId(item);
|
|
223
|
+
const children = kids.get(id) ?? [];
|
|
224
|
+
const open = !folded.has(id);
|
|
225
|
+
const own = liveSpan(item, id);
|
|
226
|
+
const row: Row = { kind: "task", item, id, order: 0, depth, hasChildren: children.length > 0, open, own, span: own };
|
|
227
|
+
if (visible) {
|
|
228
|
+
row.order = order++;
|
|
229
|
+
out.push(row);
|
|
230
|
+
}
|
|
231
|
+
let below: Span | null = null;
|
|
232
|
+
for (const child of children) below = join(below, walk(child, depth + 1, visible && open));
|
|
233
|
+
if (!own) row.span = below;
|
|
234
|
+
return join(own, below);
|
|
235
|
+
};
|
|
236
|
+
for (const item of kids.get(null) ?? []) walk(item, 0, true);
|
|
237
|
+
};
|
|
238
|
+
|
|
149
239
|
if (!groups || !getGroup) {
|
|
150
|
-
items
|
|
240
|
+
emit(items);
|
|
151
241
|
return out;
|
|
152
242
|
}
|
|
153
243
|
for (const group of groups) {
|
|
154
244
|
const members = items.filter((item) => getGroup(item) === group.id);
|
|
155
245
|
let span: Span | null = null;
|
|
156
|
-
for (const item of members)
|
|
157
|
-
const s = liveSpan(item, getId(item));
|
|
158
|
-
span = span ? { start: Math.min(span.start, s.start), end: Math.max(span.end, s.end) } : s;
|
|
159
|
-
}
|
|
246
|
+
for (const item of members) span = join(span, liveSpan(item, getId(item)));
|
|
160
247
|
out.push({ kind: "group", group, count: members.length, span });
|
|
161
|
-
members
|
|
248
|
+
emit(members);
|
|
162
249
|
}
|
|
163
250
|
return out;
|
|
164
251
|
});
|
|
@@ -196,7 +283,7 @@
|
|
|
196
283
|
if (!getDependsOn) return [];
|
|
197
284
|
const at = new Map<string, { row: number; span: Span }>();
|
|
198
285
|
rows.forEach((row, index) => {
|
|
199
|
-
if (row.kind === "task") at.set(row.id, { row: index, span:
|
|
286
|
+
if (row.kind === "task" && row.span) at.set(row.id, { row: index, span: row.span });
|
|
200
287
|
});
|
|
201
288
|
const out: { key: string; d: string; head: string; from: string; to: string }[] = [];
|
|
202
289
|
for (const row of rows) {
|
|
@@ -206,9 +293,9 @@
|
|
|
206
293
|
for (const dep of getDependsOn(row.item) ?? []) {
|
|
207
294
|
const source = at.get(dep);
|
|
208
295
|
if (!source) continue;
|
|
209
|
-
const x1 = x(source.span.end + 1) -
|
|
296
|
+
const x1 = x(source.span.end + 1) - inset;
|
|
210
297
|
const y1 = source.row * rowHeight + rowHeight / 2;
|
|
211
|
-
const x2 = x(target.span.start) +
|
|
298
|
+
const x2 = x(target.span.start) + inset;
|
|
212
299
|
const y2 = target.row * rowHeight + rowHeight / 2;
|
|
213
300
|
// Room between the bars: one bend in the gap. No room: out, round the row gap, and back in.
|
|
214
301
|
const points: [number, number][] =
|
|
@@ -267,7 +354,7 @@
|
|
|
267
354
|
const moved = drag.dx !== 0;
|
|
268
355
|
const span = liveSpan(item, id);
|
|
269
356
|
drag = null;
|
|
270
|
-
if (moved) void onChange?.(id, { start: isoFromDay(span.start), end: isoFromDay(span.end) });
|
|
357
|
+
if (moved && span) void onChange?.(id, { start: isoFromDay(span.start), end: isoFromDay(span.end) });
|
|
271
358
|
else onSelect?.(id);
|
|
272
359
|
}
|
|
273
360
|
|
|
@@ -282,6 +369,7 @@
|
|
|
282
369
|
e.preventDefault();
|
|
283
370
|
const step = e.key === "ArrowRight" ? 1 : -1;
|
|
284
371
|
const span = spanOf(item);
|
|
372
|
+
if (!span) return;
|
|
285
373
|
const next = e.shiftKey
|
|
286
374
|
? { start: span.start, end: Math.max(span.start, span.end + step) }
|
|
287
375
|
: { start: span.start + step, end: span.end + step };
|
|
@@ -298,7 +386,8 @@
|
|
|
298
386
|
<div
|
|
299
387
|
bind:this={scroller}
|
|
300
388
|
class={cn(
|
|
301
|
-
"task-gantt relative min-h-0 flex-1 overflow-auto overscroll-x-contain
|
|
389
|
+
"task-gantt relative min-h-0 flex-1 overflow-auto overscroll-x-contain",
|
|
390
|
+
flat ? "bg-background" : "rounded-xl border border-border bg-card text-card-foreground",
|
|
302
391
|
drag && "select-none",
|
|
303
392
|
className,
|
|
304
393
|
)}
|
|
@@ -412,15 +501,16 @@
|
|
|
412
501
|
class="g-summary absolute top-1/2 h-1.5 -translate-y-1/2 rounded-full"
|
|
413
502
|
style:--g-hue={hue}
|
|
414
503
|
style:--g-i={Math.min(index, STAGGER_CAP)}
|
|
415
|
-
style:left="{x(row.span.start) +
|
|
416
|
-
style:width="{(row.span.end - row.span.start + 1) * dayWidth -
|
|
504
|
+
style:left="{x(row.span.start) + inset}px"
|
|
505
|
+
style:width="{Math.max((row.span.end - row.span.start + 1) * dayWidth - inset * 2, 8)}px"
|
|
417
506
|
></div>
|
|
418
507
|
{/if}
|
|
419
508
|
</div>
|
|
420
509
|
</div>
|
|
421
510
|
{:else}
|
|
422
|
-
{@const span =
|
|
423
|
-
{@const width = (span.end - span.start + 1) * dayWidth -
|
|
511
|
+
{@const span = row.span}
|
|
512
|
+
{@const width = span ? Math.max((span.end - span.start + 1) * dayWidth - inset * 2, 8) : 0}
|
|
513
|
+
{@const ctx = { depth: row.depth, hasChildren: row.hasChildren, open: row.open }}
|
|
424
514
|
{@const progress = Math.max(0, Math.min(1, getProgress?.(row.item) ?? 0))}
|
|
425
515
|
{@const title = getTitle(row.item)}
|
|
426
516
|
{@const dragging = drag?.id === row.id}
|
|
@@ -430,14 +520,74 @@
|
|
|
430
520
|
class:g-row-hot={hoveredId === row.id || dragging}
|
|
431
521
|
style:height="{rowHeight}px"
|
|
432
522
|
>
|
|
433
|
-
<div
|
|
523
|
+
<div
|
|
524
|
+
class="sticky left-0 z-10 flex w-(--g-label) shrink-0 items-center gap-2 border-r border-border bg-card px-3 text-sm"
|
|
525
|
+
style:padding-inline-start={getParent ? `${row.depth * INDENT + 12}px` : undefined}
|
|
526
|
+
>
|
|
527
|
+
{#if getParent}
|
|
528
|
+
{#if row.hasChildren}
|
|
529
|
+
<button
|
|
530
|
+
type="button"
|
|
531
|
+
aria-expanded={row.open}
|
|
532
|
+
aria-label="{row.open ? 'Fold' : 'Unfold'} {title}"
|
|
533
|
+
onclick={() => toggle(row.id)}
|
|
534
|
+
class="-ml-1 grid size-5 shrink-0 place-items-center rounded-md text-muted-foreground outline-none transition-colors duration-[var(--duration-quick,150ms)] hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60"
|
|
535
|
+
>
|
|
536
|
+
<span class="g-chevron size-3.5 [&>svg]:size-full" class:g-chevron-open={row.open} aria-hidden="true">
|
|
537
|
+
{@html chevronIcon}
|
|
538
|
+
</span>
|
|
539
|
+
</button>
|
|
540
|
+
{:else}
|
|
541
|
+
<span class="-ml-1 size-5 shrink-0" aria-hidden="true"></span>
|
|
542
|
+
{/if}
|
|
543
|
+
{/if}
|
|
434
544
|
{#if renderLabel}
|
|
435
|
-
{@render renderLabel(row.item)}
|
|
545
|
+
{@render renderLabel(row.item, ctx)}
|
|
436
546
|
{:else}
|
|
437
547
|
<span class="truncate text-foreground">{title}</span>
|
|
438
548
|
{/if}
|
|
439
549
|
</div>
|
|
440
550
|
<div class="relative shrink-0" style:width="{timelineWidth}px">
|
|
551
|
+
{#if !span}
|
|
552
|
+
<span
|
|
553
|
+
class="pointer-events-none absolute top-1/2 -translate-y-1/2 text-xs whitespace-nowrap text-muted-foreground/70"
|
|
554
|
+
style:left="{Math.max(x(todayDay) - 40, 0) + 8}px"
|
|
555
|
+
>
|
|
556
|
+
{noDatesLabel}
|
|
557
|
+
</span>
|
|
558
|
+
{:else if isMilestone?.(row.item) && row.own}
|
|
559
|
+
<button
|
|
560
|
+
type="button"
|
|
561
|
+
aria-label="{title}, {rangeLabel(span)}"
|
|
562
|
+
title="{title}, {rangeLabel(span)}"
|
|
563
|
+
class="g-flag absolute top-1/2 grid size-6 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
|
|
564
|
+
style:--g-hue={hueOf(row.item)}
|
|
565
|
+
style:left="{x(span.end) + dayWidth / 2}px"
|
|
566
|
+
onclick={() => onSelect?.(row.id)}
|
|
567
|
+
onpointerenter={() => (hoveredId = row.id)}
|
|
568
|
+
onpointerleave={() => (hoveredId = hoveredId === row.id ? null : hoveredId)}
|
|
569
|
+
>
|
|
570
|
+
<span class="size-5 [&>svg]:size-full" aria-hidden="true">{@html flagIcon}</span>
|
|
571
|
+
</button>
|
|
572
|
+
{:else if !row.own}
|
|
573
|
+
<!-- A parent with no dates of its own: the span of what is below it. Not draggable. -->
|
|
574
|
+
<div
|
|
575
|
+
role="button"
|
|
576
|
+
tabindex="0"
|
|
577
|
+
aria-label="{title}, {rangeLabel(span)}"
|
|
578
|
+
title="{title}, {rangeLabel(span)}"
|
|
579
|
+
class="g-derived absolute top-1/2 -translate-y-1/2 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
|
|
580
|
+
style:--g-hue={hueOf(row.item)}
|
|
581
|
+
style:--g-i={Math.min(row.order, STAGGER_CAP)}
|
|
582
|
+
style:left="{x(span.start) + inset}px"
|
|
583
|
+
style:width="{width}px"
|
|
584
|
+
style:height="{Math.max(rowHeight - 24, 10)}px"
|
|
585
|
+
onclick={() => onSelect?.(row.id)}
|
|
586
|
+
onkeydown={(e) => (e.key === "Enter" || e.key === " ") && (e.preventDefault(), onSelect?.(row.id))}
|
|
587
|
+
onpointerenter={() => (hoveredId = row.id)}
|
|
588
|
+
onpointerleave={() => (hoveredId = hoveredId === row.id ? null : hoveredId)}
|
|
589
|
+
></div>
|
|
590
|
+
{:else}
|
|
441
591
|
<div
|
|
442
592
|
data-bar
|
|
443
593
|
role="button"
|
|
@@ -451,7 +601,7 @@
|
|
|
451
601
|
)}
|
|
452
602
|
style:--g-hue={hueOf(row.item)}
|
|
453
603
|
style:--g-i={Math.min(row.order, STAGGER_CAP)}
|
|
454
|
-
style:left="{x(span.start) +
|
|
604
|
+
style:left="{x(span.start) + inset}px"
|
|
455
605
|
style:width="{width}px"
|
|
456
606
|
style:height="{rowHeight - 14}px"
|
|
457
607
|
onpointerdown={(e) => onPointerDown(e, row.id, "move")}
|
|
@@ -503,6 +653,7 @@
|
|
|
503
653
|
{title}
|
|
504
654
|
</span>
|
|
505
655
|
{/if}
|
|
656
|
+
{/if}
|
|
506
657
|
</div>
|
|
507
658
|
</div>
|
|
508
659
|
{/if}
|
|
@@ -581,6 +732,26 @@
|
|
|
581
732
|
opacity: 0.6;
|
|
582
733
|
}
|
|
583
734
|
|
|
735
|
+
.g-chevron {
|
|
736
|
+
transition: rotate var(--duration-fast, 250ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.g-chevron-open {
|
|
740
|
+
rotate: 90deg;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/* A summary of what is below: an outline, so it never reads as a task you can drag. */
|
|
744
|
+
.g-derived {
|
|
745
|
+
background: color-mix(in oklab, var(--g-hue) 10%, transparent);
|
|
746
|
+
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--g-hue) 45%, transparent);
|
|
747
|
+
animation: g-bar-in var(--duration-slow, 400ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) backwards;
|
|
748
|
+
animation-delay: calc(var(--g-i, 0) * var(--duration-stagger, 40ms));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.g-flag {
|
|
752
|
+
color: var(--g-hue);
|
|
753
|
+
}
|
|
754
|
+
|
|
584
755
|
.g-summary {
|
|
585
756
|
background: color-mix(in oklab, var(--g-hue) 45%, transparent);
|
|
586
757
|
animation: g-bar-in var(--duration-slow, 400ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)) backwards;
|
|
@@ -657,6 +828,7 @@
|
|
|
657
828
|
@media (prefers-reduced-motion: reduce) {
|
|
658
829
|
.g-bar,
|
|
659
830
|
.g-summary,
|
|
831
|
+
.g-derived,
|
|
660
832
|
.g-tip,
|
|
661
833
|
.g-today,
|
|
662
834
|
.g-arrow-line,
|
|
@@ -665,7 +837,8 @@
|
|
|
665
837
|
}
|
|
666
838
|
.g-bar,
|
|
667
839
|
.g-summary,
|
|
668
|
-
.g-progress
|
|
840
|
+
.g-progress,
|
|
841
|
+
.g-chevron {
|
|
669
842
|
transition: none;
|
|
670
843
|
}
|
|
671
844
|
}
|