@heyramzi/tasks 0.3.0 → 0.5.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 +138 -18
- package/package.json +34 -1
- package/src/ambient.d.ts +3 -6
- package/src/demo.svelte +109 -19
- package/src/inbox/inbox-card.svelte +280 -0
- package/src/inbox/inbox-draft.svelte +84 -0
- package/src/inbox/inbox-logic.ts +56 -0
- package/src/inbox/inbox-statuses.ts +48 -0
- package/src/inbox/inbox-trace.svelte +45 -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 +39 -7
- package/src/kanban-board.svelte +421 -104
- package/src/kanban-column.svelte +1 -1
- package/src/kanban-types.ts +18 -1
- package/src/side-panel.svelte +204 -0
- package/src/status-badge.svelte +37 -18
- package/src/status-dot.svelte +32 -12
- package/src/status-icon.svelte +2 -2
- package/src/status-types.ts +6 -5
- package/src/task-gantt.svelte +208 -36
- package/src/task-list.svelte +206 -101
- package/src/task-types.ts +11 -8
- 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 +564 -0
- package/src/time/time-types.ts +74 -0
- package/src/use-kanban-dnd.svelte.ts +6 -1
- package/src/icons/board-icon.svelte +0 -9
- package/src/icons/chevron-right.svg +0 -1
- package/src/icons/gantt-icon.svelte +0 -11
- package/src/icons/list-icon.svelte +0 -16
- package/src/icons/spinner.svg +0 -1
- package/src/lib/cn.ts +0 -11
- package/src/task-view-switch.svelte +0 -118
- package/src/tone.ts +0 -51
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** A running timer. The host persists it, the tracker only reads it. */
|
|
2
|
+
export type TimeRunning = {
|
|
3
|
+
startedAt: Date | string | number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
/** What the tracker hands back when a manual entry is submitted. */
|
|
7
|
+
export type TimeLogInput = {
|
|
8
|
+
minutes: number;
|
|
9
|
+
/** The day worked, `YYYY-MM-DD`. */
|
|
10
|
+
day: string;
|
|
11
|
+
note?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** What the tracker hands back when the timer stops. */
|
|
15
|
+
export type TimeStopInfo = {
|
|
16
|
+
/** Elapsed whole minutes, never below 1. */
|
|
17
|
+
minutes: number;
|
|
18
|
+
startedAt: Date;
|
|
19
|
+
stoppedAt: Date;
|
|
20
|
+
/** The day it started, `YYYY-MM-DD`, in the reader's calendar. */
|
|
21
|
+
day: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type TimeLabels = {
|
|
25
|
+
title: string;
|
|
26
|
+
timer: string;
|
|
27
|
+
start: string;
|
|
28
|
+
stop: string;
|
|
29
|
+
running: string;
|
|
30
|
+
since: (time: string) => string;
|
|
31
|
+
started: string;
|
|
32
|
+
runningFor: (duration: string) => string;
|
|
33
|
+
stopped: (duration: string) => string;
|
|
34
|
+
elapsed: string;
|
|
35
|
+
logged: string;
|
|
36
|
+
duration: string;
|
|
37
|
+
durationPlaceholder: string;
|
|
38
|
+
date: string;
|
|
39
|
+
note: string;
|
|
40
|
+
notePlaceholder: string;
|
|
41
|
+
log: string;
|
|
42
|
+
saving: string;
|
|
43
|
+
invalid: string;
|
|
44
|
+
empty: string;
|
|
45
|
+
remaining: (left: string, estimate: string) => string;
|
|
46
|
+
over: (over: string, estimate: string) => string;
|
|
47
|
+
deleteEntry: (duration: string) => string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const DEFAULT_TIME_LABELS: TimeLabels = {
|
|
51
|
+
title: "Time",
|
|
52
|
+
timer: "Timer",
|
|
53
|
+
start: "Start timer",
|
|
54
|
+
stop: "Stop timer",
|
|
55
|
+
running: "Running",
|
|
56
|
+
since: (time) => `Since ${time}`,
|
|
57
|
+
started: "Timer started",
|
|
58
|
+
runningFor: (duration) => `Timer running, ${duration}`,
|
|
59
|
+
stopped: (duration) => `Timer stopped, ${duration} to log`,
|
|
60
|
+
elapsed: "Elapsed time",
|
|
61
|
+
logged: "Logged",
|
|
62
|
+
duration: "Time spent",
|
|
63
|
+
durationPlaceholder: "1h30",
|
|
64
|
+
date: "Date worked",
|
|
65
|
+
note: "Note",
|
|
66
|
+
notePlaceholder: "What was done",
|
|
67
|
+
log: "Log",
|
|
68
|
+
saving: "Saving",
|
|
69
|
+
invalid: "Try 45m, 1h30 or 1.5h",
|
|
70
|
+
empty: "Log your first hour",
|
|
71
|
+
remaining: (left, estimate) => `${left} left of ${estimate}`,
|
|
72
|
+
over: (over, estimate) => `${over} over ${estimate}`,
|
|
73
|
+
deleteEntry: (duration) => `Delete ${duration} entry`,
|
|
74
|
+
};
|
|
@@ -132,9 +132,14 @@ export function createKanbanDnd<T>(config: KanbanDndConfig<T>) {
|
|
|
132
132
|
.getColumnItems(targetColumnId)
|
|
133
133
|
.filter((entry) => config.getId(entry) !== movedId);
|
|
134
134
|
const index = manualInsertIndex(column, overId, targetColumnId);
|
|
135
|
+
const before = column[index - 1];
|
|
136
|
+
const after = column[index];
|
|
137
|
+
const orderOf = config.boardOrder;
|
|
135
138
|
void config.onMove(movedId, {
|
|
136
139
|
columnId: targetColumnId,
|
|
137
|
-
boardOrder: boardOrderAt(column, index,
|
|
140
|
+
...(orderOf ? { boardOrder: boardOrderAt(column, index, orderOf) } : {}),
|
|
141
|
+
beforeId: before === undefined ? null : config.getId(before),
|
|
142
|
+
afterId: after === undefined ? null : config.getId(after),
|
|
138
143
|
});
|
|
139
144
|
}
|
|
140
145
|
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
let { size = 16, class: className }: { size?: number; class?: string } = $props();
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true" class={className}>
|
|
6
|
-
<rect x="2" y="2.5" width="3.5" height="11" rx="1.25" stroke="currentColor" stroke-width="1.5"></rect>
|
|
7
|
-
<rect x="7" y="2.5" width="3.5" height="7" rx="1.25" stroke="currentColor" stroke-width="1.5"></rect>
|
|
8
|
-
<rect x="12" y="2.5" width="2" height="9" rx="1" stroke="currentColor" stroke-width="1.5"></rect>
|
|
9
|
-
</svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
let { size = 16, class: className }: { size?: number; class?: string } = $props();
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true" class={className}>
|
|
6
|
-
<g stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
|
7
|
-
<line x1="2" y1="4" x2="8" y2="4"></line>
|
|
8
|
-
<line x1="5" y1="8" x2="12" y2="8"></line>
|
|
9
|
-
<line x1="9" y1="12" x2="14" y2="12"></line>
|
|
10
|
-
</g>
|
|
11
|
-
</svg>
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
let { size = 16, class: className }: { size?: number; class?: string } = $props();
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true" class={className}>
|
|
6
|
-
<g stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
|
7
|
-
<line x1="6" y1="4" x2="14" y2="4"></line>
|
|
8
|
-
<line x1="6" y1="8" x2="14" y2="8"></line>
|
|
9
|
-
<line x1="6" y1="12" x2="14" y2="12"></line>
|
|
10
|
-
</g>
|
|
11
|
-
<g fill="currentColor">
|
|
12
|
-
<circle cx="2.75" cy="4" r="1"></circle>
|
|
13
|
-
<circle cx="2.75" cy="8" r="1"></circle>
|
|
14
|
-
<circle cx="2.75" cy="12" r="1"></circle>
|
|
15
|
-
</g>
|
|
16
|
-
</svg>
|
package/src/icons/spinner.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="3" opacity="0.25"/><path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>
|
package/src/lib/cn.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export type ClassValue = string | false | null | undefined;
|
|
2
|
-
|
|
3
|
-
/** Join class values, dropping empties. Deliberately dependency-free. */
|
|
4
|
-
export function cn(...values: ClassValue[]): string {
|
|
5
|
-
let out = "";
|
|
6
|
-
for (const value of values) {
|
|
7
|
-
if (!value) continue;
|
|
8
|
-
out = out ? out + " " + value : value;
|
|
9
|
-
}
|
|
10
|
-
return out;
|
|
11
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { cn } from "./lib/cn.js";
|
|
3
|
-
import type { TaskView } from "./task-types.js";
|
|
4
|
-
|
|
5
|
-
type Props = {
|
|
6
|
-
views: TaskView[];
|
|
7
|
-
/** The active view id. Bindable. */
|
|
8
|
-
value: string;
|
|
9
|
-
onchange?: (id: string) => void;
|
|
10
|
-
label?: string;
|
|
11
|
-
class?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
let {
|
|
15
|
-
views,
|
|
16
|
-
value = $bindable(),
|
|
17
|
-
onchange,
|
|
18
|
-
label = "View",
|
|
19
|
-
class: className,
|
|
20
|
-
}: Props = $props();
|
|
21
|
-
|
|
22
|
-
let track = $state<HTMLDivElement>();
|
|
23
|
-
let pill = $state<{ x: number; width: number } | null>(null);
|
|
24
|
-
// The pill's first position is written without a transition, or it slides in from 0.
|
|
25
|
-
let placed = $state(false);
|
|
26
|
-
|
|
27
|
-
function measure(): void {
|
|
28
|
-
const button = track?.querySelector<HTMLElement>(`[data-view="${CSS.escape(value)}"]`);
|
|
29
|
-
if (!button) return;
|
|
30
|
-
pill = { x: button.offsetLeft, width: button.offsetWidth };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
$effect(() => {
|
|
34
|
-
void value;
|
|
35
|
-
void views.length;
|
|
36
|
-
measure();
|
|
37
|
-
if (!placed) requestAnimationFrame(() => (placed = true));
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
$effect(() => {
|
|
41
|
-
if (!track) return;
|
|
42
|
-
const observer = new ResizeObserver(() => measure());
|
|
43
|
-
observer.observe(track);
|
|
44
|
-
return () => observer.disconnect();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
function select(id: string): void {
|
|
48
|
-
if (id === value) return;
|
|
49
|
-
value = id;
|
|
50
|
-
onchange?.(id);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function onKeyDown(e: KeyboardEvent): void {
|
|
54
|
-
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
55
|
-
e.preventDefault();
|
|
56
|
-
const at = views.findIndex((view) => view.id === value);
|
|
57
|
-
const next = views[(at + (e.key === "ArrowRight" ? 1 : -1) + views.length) % views.length];
|
|
58
|
-
select(next.id);
|
|
59
|
-
track?.querySelector<HTMLElement>(`[data-view="${CSS.escape(next.id)}"]`)?.focus();
|
|
60
|
-
}
|
|
61
|
-
</script>
|
|
62
|
-
|
|
63
|
-
<div
|
|
64
|
-
bind:this={track}
|
|
65
|
-
role="tablist"
|
|
66
|
-
aria-label={label}
|
|
67
|
-
tabindex="-1"
|
|
68
|
-
onkeydown={onKeyDown}
|
|
69
|
-
class={cn(
|
|
70
|
-
"task-view-switch relative inline-flex shrink-0 items-center gap-0.5 rounded-lg bg-muted/70 p-0.5 dark:bg-muted/40",
|
|
71
|
-
className,
|
|
72
|
-
)}
|
|
73
|
-
>
|
|
74
|
-
{#if pill}
|
|
75
|
-
<span
|
|
76
|
-
aria-hidden="true"
|
|
77
|
-
class="tvs-pill absolute inset-y-0.5 left-0 rounded-md bg-background shadow-sm ring-1 ring-foreground/5 dark:bg-foreground/10"
|
|
78
|
-
class:tvs-pill-placed={placed}
|
|
79
|
-
style:width="{pill.width}px"
|
|
80
|
-
style:transform="translateX({pill.x}px)"
|
|
81
|
-
></span>
|
|
82
|
-
{/if}
|
|
83
|
-
{#each views as view (view.id)}
|
|
84
|
-
{@const active = view.id === value}
|
|
85
|
-
<button
|
|
86
|
-
type="button"
|
|
87
|
-
role="tab"
|
|
88
|
-
data-view={view.id}
|
|
89
|
-
aria-selected={active}
|
|
90
|
-
tabindex={active ? 0 : -1}
|
|
91
|
-
onclick={() => select(view.id)}
|
|
92
|
-
class={cn(
|
|
93
|
-
"relative z-10 inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium outline-none transition-colors duration-[var(--duration-quick,150ms)] focus-visible:ring-2 focus-visible:ring-ring/60",
|
|
94
|
-
active ? "text-foreground" : "text-muted-foreground hover:text-foreground",
|
|
95
|
-
)}
|
|
96
|
-
>
|
|
97
|
-
{#if view.icon}
|
|
98
|
-
{@const Icon = view.icon}
|
|
99
|
-
<Icon size={14} class="shrink-0" />
|
|
100
|
-
{/if}
|
|
101
|
-
{view.label}
|
|
102
|
-
</button>
|
|
103
|
-
{/each}
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
<style>
|
|
107
|
-
.tvs-pill-placed {
|
|
108
|
-
transition:
|
|
109
|
-
transform var(--duration-fast, 250ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1)),
|
|
110
|
-
width var(--duration-fast, 250ms) var(--ease-smooth-out, cubic-bezier(0.22, 1, 0.36, 1));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
@media (prefers-reduced-motion: reduce) {
|
|
114
|
-
.tvs-pill-placed {
|
|
115
|
-
transition: none;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
</style>
|
package/src/tone.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { Tone } from "./status-types.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The severity scale, as Tailwind classes for a chip and as CSS colors for the
|
|
5
|
-
* dot. Tokens are plain CSS variables, so a host app can retheme them.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export const TONE_CLASS: Record<Tone, string> = {
|
|
9
|
-
neutral: "border-border bg-muted/40 text-muted-foreground",
|
|
10
|
-
accent: "border-primary/30 bg-primary/10 text-primary",
|
|
11
|
-
info: "border-info/30 bg-info/10 text-info-800 dark:text-info-400",
|
|
12
|
-
success: "border-success/30 bg-success/10 text-success-800 dark:text-success-400",
|
|
13
|
-
warning: "border-warning/30 bg-warning/10 text-warning-800 dark:text-warning-400",
|
|
14
|
-
destructive: "border-destructive/30 bg-destructive/10 text-destructive",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/** The same scale as text, for a verdict inside a line of muted facts. */
|
|
18
|
-
export const TONE_TEXT: Record<Tone, string> = {
|
|
19
|
-
neutral: "text-muted-foreground",
|
|
20
|
-
accent: "text-primary",
|
|
21
|
-
info: "text-info-800 dark:text-info-400",
|
|
22
|
-
success: "text-success-800 dark:text-success-400",
|
|
23
|
-
warning: "text-warning-800 dark:text-warning-400",
|
|
24
|
-
destructive: "text-destructive",
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/** The same scale as icon color. */
|
|
28
|
-
export const TONE_ICON: Record<Tone, string> = {
|
|
29
|
-
neutral: "text-muted-foreground",
|
|
30
|
-
accent: "text-primary",
|
|
31
|
-
info: "text-info",
|
|
32
|
-
success: "text-success",
|
|
33
|
-
warning: "text-warning",
|
|
34
|
-
destructive: "text-destructive",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/** The scale as CSS colors, for the SVG dot. */
|
|
38
|
-
export const TONE_COLOR: Record<Tone, string> = {
|
|
39
|
-
neutral: "var(--color-muted-foreground)",
|
|
40
|
-
accent: "var(--color-primary)",
|
|
41
|
-
info: "var(--color-info)",
|
|
42
|
-
success: "var(--color-success)",
|
|
43
|
-
warning: "var(--color-warning)",
|
|
44
|
-
destructive: "var(--color-destructive)",
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The inset well for a control block inside a panel. A visible step
|
|
49
|
-
* at the same depth, for filter rows and grouped controls.
|
|
50
|
-
*/
|
|
51
|
-
export const insetWellClass = "rounded-lg border border-border bg-muted/40 p-2";
|