@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,203 @@
1
+ <script lang="ts">
2
+ import { tick, type Snippet } from "svelte";
3
+ import { Badge } from "@heyramzi/ui/shadcn/badge";
4
+ import { Button } from "@heyramzi/ui/shadcn/button";
5
+ import * as Empty from "@heyramzi/ui/shadcn/empty";
6
+ import { Kbd, KbdGroup } from "@heyramzi/ui/shadcn/kbd";
7
+ import { Tabs, TabsList, TabsTrigger } from "@heyramzi/ui/shadcn/tabs";
8
+ import { ToggleGroup, ToggleGroupItem } from "@heyramzi/ui/shadcn/toggle-group";
9
+ import { cn } from "@heyramzi/utils";
10
+ import StatusDot from "../status-dot.svelte";
11
+ import InboxCard from "./inbox-card.svelte";
12
+ import { flatten, groupByProject, inLane, isTyping, laneOf } from "./inbox-logic.js";
13
+ import { inboxKinds, inboxStates } from "./inbox-statuses.js";
14
+ import type {
15
+ InboxItem,
16
+ InboxKindFilter,
17
+ InboxLane,
18
+ InboxProject,
19
+ InboxVerdict,
20
+ } from "./inbox-types.js";
21
+
22
+ type Props = {
23
+ items: InboxItem[];
24
+ /** The host applies the verdict and passes the updated items back. */
25
+ onverdict: (verdict: InboxVerdict) => void;
26
+ /** Replaces an item's summary lines. */
27
+ renderProposal?: Snippet<[InboxItem]>;
28
+ /** Trailing content in a project's header, such as a link to its timeline. */
29
+ renderGroupAction?: Snippet<[InboxProject]>;
30
+ class?: string;
31
+ };
32
+
33
+ let { items, onverdict, renderProposal, renderGroupAction, class: className }: Props = $props();
34
+
35
+ const LANES: { id: InboxLane; label: string }[] = [
36
+ { id: "needs", label: "Needs you" },
37
+ { id: "auto", label: "Done by the agent" },
38
+ { id: "decided", label: "Decided" },
39
+ ];
40
+
41
+ let lane = $state<InboxLane>("needs");
42
+ let kind = $state<InboxKindFilter>("all");
43
+ let cursor = $state(0);
44
+ let cards = $state<Record<string, ReturnType<typeof InboxCard> | null>>({});
45
+
46
+ let counts = $derived(
47
+ Object.fromEntries(LANES.map((l) => [l.id, items.filter((i) => laneOf(i) === l.id).length])) as Record<
48
+ InboxLane,
49
+ number
50
+ >,
51
+ );
52
+ let groups = $derived(groupByProject(inLane(items, lane, kind)));
53
+ let visible = $derived(flatten(groups));
54
+ // WHY an index and not an id: when a verdict removes the item under the cursor,
55
+ // the same index lands on the next one, so triage keeps moving down the queue.
56
+ let selected = $derived(visible[Math.min(cursor, visible.length - 1)]);
57
+
58
+ async function move(step: number) {
59
+ if (!visible.length) return;
60
+ cursor = Math.max(0, Math.min(visible.length - 1, cursor + step));
61
+ await tick();
62
+ document.querySelector<HTMLElement>(`[data-inbox-item="${CSS.escape(visible[cursor].id)}"]`)?.focus();
63
+ }
64
+
65
+ function onkeydown(event: KeyboardEvent) {
66
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
67
+ if (isTyping(event.target)) return;
68
+ const card = selected ? cards[selected.id] : null;
69
+ const actions: Record<string, (() => void) | undefined> = {
70
+ j: () => move(1),
71
+ k: () => move(-1),
72
+ a: () => card?.approve(),
73
+ d: () => card?.decline(),
74
+ s: () => card?.steer(),
75
+ e: () => card?.edit(),
76
+ u: () => card?.undo(),
77
+ };
78
+ const run = actions[event.key];
79
+ if (!run) return;
80
+ event.preventDefault();
81
+ run();
82
+ }
83
+
84
+ function setLane(next: string) {
85
+ lane = next as InboxLane;
86
+ cursor = 0;
87
+ }
88
+
89
+ function setKind(next: string | undefined) {
90
+ kind = (next || "all") as InboxKindFilter;
91
+ cursor = 0;
92
+ }
93
+ </script>
94
+
95
+ <!-- WHY the key handler sits on the region: the shortcuts work wherever focus is inside the inbox and nowhere else on the page. -->
96
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
97
+ <div
98
+ role="region"
99
+ aria-label="Inbox"
100
+ class={cn("flex h-full min-h-0 flex-col gap-3", className)}
101
+ {onkeydown}
102
+ >
103
+ <div class="flex flex-wrap items-center justify-between gap-2">
104
+ <Tabs value={lane} onValueChange={setLane}>
105
+ <TabsList>
106
+ {#each LANES as l (l.id)}
107
+ <TabsTrigger value={l.id} class="gap-2 px-3">
108
+ {l.label}
109
+ <span class="text-muted-foreground text-xs tabular-nums">{counts[l.id]}</span>
110
+ </TabsTrigger>
111
+ {/each}
112
+ </TabsList>
113
+ </Tabs>
114
+ <ToggleGroup
115
+ type="single"
116
+ variant="outline"
117
+ size="sm"
118
+ value={kind}
119
+ onValueChange={setKind}
120
+ aria-label="Filter by kind"
121
+ >
122
+ <ToggleGroupItem value="all">All</ToggleGroupItem>
123
+ {#each inboxKinds.ids as id (id)}
124
+ <ToggleGroupItem value={id}>{inboxKinds.label(id)}</ToggleGroupItem>
125
+ {/each}
126
+ </ToggleGroup>
127
+ </div>
128
+
129
+ <div class="-mx-1 min-h-0 flex-1 overflow-y-auto px-1 pb-1">
130
+ {#if !visible.length}
131
+ <Empty.Root class="h-full">
132
+ <Empty.Header>
133
+ <Empty.Media>
134
+ <StatusDot statuses={inboxStates} status="approved" size={28} />
135
+ </Empty.Media>
136
+ {#if kind !== "all"}
137
+ <Empty.Title>Nothing of this kind here</Empty.Title>
138
+ <Empty.Description>Show every kind to see the rest of this lane.</Empty.Description>
139
+ {:else if lane === "needs"}
140
+ <Empty.Title>Queue clear</Empty.Title>
141
+ <Empty.Description>The agent handled the rest, and nothing is waiting on you.</Empty.Description>
142
+ {:else if lane === "auto"}
143
+ <Empty.Title>The agent hasn't done anything on its own yet</Empty.Title>
144
+ <Empty.Description>Safe work it finishes by itself lands here, with an undo.</Empty.Description>
145
+ {:else}
146
+ <Empty.Title>No verdicts yet</Empty.Title>
147
+ <Empty.Description>What you approve or decline shows here and on the project timeline.</Empty.Description>
148
+ {/if}
149
+ </Empty.Header>
150
+ <Empty.Content>
151
+ {#if kind !== "all"}
152
+ <Button size="sm" variant="outline" onclick={() => setKind("all")}>Show every kind</Button>
153
+ {:else if lane === "needs" && counts.auto}
154
+ <Button size="sm" variant="outline" onclick={() => setLane("auto")}>See what the agent did</Button>
155
+ {:else if lane !== "needs" && counts.needs}
156
+ <Button size="sm" variant="outline" onclick={() => setLane("needs")}>Go to the queue</Button>
157
+ {/if}
158
+ </Empty.Content>
159
+ </Empty.Root>
160
+ {:else}
161
+ <div class="flex flex-col gap-5">
162
+ {#each groups as group (group.project.id)}
163
+ <section aria-label={group.project.name} class="flex flex-col gap-2">
164
+ <header class="flex items-center gap-2 px-1">
165
+ <h2 class="text-sm font-medium">{group.project.name}</h2>
166
+ {#if group.project.client}
167
+ <span class="text-muted-foreground text-sm">{group.project.client}</span>
168
+ {/if}
169
+ <Badge variant="outline" class="tabular-nums">{group.items.length}</Badge>
170
+ <div class="ml-auto flex items-center gap-1">
171
+ {#if group.project.href}
172
+ <Button size="sm" variant="ghost" href={group.project.href}>Timeline</Button>
173
+ {/if}
174
+ {@render renderGroupAction?.(group.project)}
175
+ </div>
176
+ </header>
177
+ <div class="flex flex-col gap-2">
178
+ {#each group.items as item (item.id)}
179
+ <InboxCard
180
+ bind:this={cards[item.id]}
181
+ {item}
182
+ selected={selected?.id === item.id}
183
+ onselect={() => (cursor = visible.findIndex((v) => v.id === item.id))}
184
+ {onverdict}
185
+ {renderProposal}
186
+ />
187
+ {/each}
188
+ </div>
189
+ </section>
190
+ {/each}
191
+ </div>
192
+ {/if}
193
+ </div>
194
+
195
+ <footer class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-1 text-xs max-sm:hidden">
196
+ <span class="inline-flex items-center gap-1.5"><KbdGroup><Kbd>j</Kbd><Kbd>k</Kbd></KbdGroup> move</span>
197
+ <span class="inline-flex items-center gap-1.5"><Kbd>a</Kbd> approve</span>
198
+ <span class="inline-flex items-center gap-1.5"><Kbd>d</Kbd> decline</span>
199
+ <span class="inline-flex items-center gap-1.5"><Kbd>s</Kbd> steer</span>
200
+ <span class="inline-flex items-center gap-1.5"><Kbd>e</Kbd> edit draft</span>
201
+ <span class="inline-flex items-center gap-1.5"><Kbd>u</Kbd> undo</span>
202
+ </footer>
203
+ </div>
@@ -0,0 +1,23 @@
1
+ export { default as Inbox } from "./inbox.svelte";
2
+ export { confidenceLevel, inboxConfidence, inboxKinds, inboxStates } from "./inbox-statuses.js";
3
+ export { flatten, groupByProject, inLane, laneOf } from "./inbox-logic.js";
4
+ export type {
5
+ InboxAgent,
6
+ InboxConfidence,
7
+ InboxDraft,
8
+ InboxDraftEdit,
9
+ InboxDraftEdits,
10
+ InboxEvidence,
11
+ InboxItem,
12
+ InboxKind,
13
+ InboxKindFilter,
14
+ InboxLane,
15
+ InboxOutcome,
16
+ InboxProject,
17
+ InboxProposal,
18
+ InboxSourceType,
19
+ InboxState,
20
+ InboxTier,
21
+ InboxTrace,
22
+ InboxVerdict,
23
+ } from "./inbox-types.js";
package/src/index.ts CHANGED
@@ -17,25 +17,57 @@ export { default as StatusBadge } from "./status-badge.svelte";
17
17
  export { default as StatusDot } from "./status-dot.svelte";
18
18
  export { default as StatusIcon } from "./status-icon.svelte";
19
19
  export { defineStatuses } from "./define-statuses.js";
20
- export { TONE_CLASS, TONE_COLOR, TONE_ICON, TONE_TEXT, insetWellClass } from "./tone.js";
21
20
  export type {
22
21
  StatusDef,
23
22
  StatusDotVariant,
24
23
  StatusId,
25
24
  StatusMap,
26
25
  Statuses,
27
- Tone,
28
26
  } from "./status-types.js";
27
+ export { default as SidePanel } from "./side-panel.svelte";
28
+ export type { PanelStat, SidePanelWidth } from "./side-panel.svelte";
29
29
  export { default as TaskList } from "./task-list.svelte";
30
30
  export { default as TaskGantt } from "./task-gantt.svelte";
31
- export { default as TaskViewSwitch } from "./task-view-switch.svelte";
32
- export { default as BoardIcon } from "./icons/board-icon.svelte";
33
- export { default as ListIcon } from "./icons/list-icon.svelte";
34
- export { default as GanttIcon } from "./icons/gantt-icon.svelte";
35
31
  export { addDays, dayNumber, isoFromDay, todayIso } from "./dates.js";
36
32
  export type {
37
33
  GanttChange,
34
+ GanttLabelContext,
38
35
  TaskListGroupContext,
39
36
  TaskListRowContext,
40
- TaskView,
41
37
  } from "./task-types.js";
38
+ export { default as TimeTracker } from "./time/time-tracker.svelte";
39
+ export { default as TimeChip } from "./time/time-chip.svelte";
40
+ export { default as TimeRing } from "./time/time-ring.svelte";
41
+ export { default as TimeDot } from "./time/time-dot.svelte";
42
+ export {
43
+ budgetTone,
44
+ dayLabel,
45
+ formatClock,
46
+ formatMinutes,
47
+ localDay,
48
+ parseMinutes,
49
+ personColor,
50
+ } from "./time/duration.js";
51
+ export { Inbox, confidenceLevel, inboxConfidence, inboxKinds, inboxStates } from "./inbox/index.js";
52
+ export type {
53
+ InboxAgent,
54
+ InboxConfidence,
55
+ InboxDraft,
56
+ InboxDraftEdit,
57
+ InboxDraftEdits,
58
+ InboxEvidence,
59
+ InboxItem,
60
+ InboxKind,
61
+ InboxKindFilter,
62
+ InboxLane,
63
+ InboxOutcome,
64
+ InboxProject,
65
+ InboxProposal,
66
+ InboxSourceType,
67
+ InboxState,
68
+ InboxTier,
69
+ InboxTrace,
70
+ InboxVerdict,
71
+ } from "./inbox/index.js";
72
+ export { DEFAULT_TIME_LABELS } from "./time/time-types.js";
73
+ export type { TimeLabels, TimeLogInput, TimeRunning, TimeStopInfo } from "./time/time-types.js";