@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.
Files changed (39) hide show
  1. package/README.md +138 -18
  2. package/package.json +34 -1
  3. package/src/ambient.d.ts +3 -6
  4. package/src/demo.svelte +109 -19
  5. package/src/inbox/inbox-card.svelte +280 -0
  6. package/src/inbox/inbox-draft.svelte +84 -0
  7. package/src/inbox/inbox-logic.ts +56 -0
  8. package/src/inbox/inbox-statuses.ts +48 -0
  9. package/src/inbox/inbox-trace.svelte +45 -0
  10. package/src/inbox/inbox-types.ts +108 -0
  11. package/src/inbox/inbox.svelte +203 -0
  12. package/src/inbox/index.ts +23 -0
  13. package/src/index.ts +39 -7
  14. package/src/kanban-board.svelte +421 -104
  15. package/src/kanban-column.svelte +1 -1
  16. package/src/kanban-types.ts +18 -1
  17. package/src/side-panel.svelte +204 -0
  18. package/src/status-badge.svelte +37 -18
  19. package/src/status-dot.svelte +32 -12
  20. package/src/status-icon.svelte +2 -2
  21. package/src/status-types.ts +6 -5
  22. package/src/task-gantt.svelte +208 -36
  23. package/src/task-list.svelte +206 -101
  24. package/src/task-types.ts +11 -8
  25. package/src/time/duration.ts +69 -0
  26. package/src/time/time-chip.svelte +62 -0
  27. package/src/time/time-dot.svelte +58 -0
  28. package/src/time/time-ring.svelte +74 -0
  29. package/src/time/time-tracker.svelte +564 -0
  30. package/src/time/time-types.ts +74 -0
  31. package/src/use-kanban-dnd.svelte.ts +6 -1
  32. package/src/icons/board-icon.svelte +0 -9
  33. package/src/icons/chevron-right.svg +0 -1
  34. package/src/icons/gantt-icon.svelte +0 -11
  35. package/src/icons/list-icon.svelte +0 -16
  36. package/src/icons/spinner.svg +0 -1
  37. package/src/lib/cn.ts +0 -11
  38. package/src/task-view-switch.svelte +0 -118
  39. 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, config.boardOrder ?? (() => null)),
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>
@@ -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";