@heyramzi/ui 2.23.0 → 2.24.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 (123) hide show
  1. package/COMPONENTS.md +3 -6
  2. package/dist/ai/confirmation/index.js +3 -6
  3. package/dist/ai/index.js +1 -5
  4. package/dist/charts/dashboard/index.js +1 -3
  5. package/dist/charts/primitives/chart-utils.js +14 -0
  6. package/dist/components/chip/index.js +1 -0
  7. package/dist/components/language-flag/flags.js +5 -0
  8. package/dist/components/language-flag/index.js +1 -0
  9. package/dist/components/list-page/index.js +1 -3
  10. package/dist/components/work-parts/index.js +1 -1
  11. package/dist/editor/doc-blocks.js +2 -1
  12. package/dist/effects/animated-beam/path-calculator.svelte.js +13 -6
  13. package/dist/lib/dates.js +41 -0
  14. package/dist/lib/panel.js +2 -2
  15. package/dist/lib/tone.js +13 -0
  16. package/dist/record/format.js +2 -18
  17. package/dist/record/logic.js +3 -6
  18. package/dist/status/index.js +1 -1
  19. package/dist/work/dates.js +2 -3
  20. package/dist/work/detail/detail-types.js +0 -8
  21. package/dist/work/docs/edited.js +8 -7
  22. package/dist/work/inbox/inbox-logic.js +5 -20
  23. package/dist/work/index.js +0 -2
  24. package/dist/work/time/duration.js +2 -3
  25. package/package.json +1 -21
  26. package/src/ai/confirmation/Confirmation.svelte +2 -2
  27. package/src/ai/confirmation/{ConfirmationRequest.svelte → ConfirmationState.svelte} +6 -3
  28. package/src/ai/confirmation/confirmation-context.svelte.ts +2 -2
  29. package/src/ai/confirmation/confirmation-parts.test.svelte +17 -0
  30. package/src/ai/confirmation/confirmation-parts.test.ts +23 -0
  31. package/src/ai/confirmation/index.ts +6 -21
  32. package/src/ai/index.ts +3 -13
  33. package/src/ai/typing-dots/TypingDots.svelte +32 -12
  34. package/src/ai/typing-dots/typing-dots.test.ts +36 -0
  35. package/src/canvas/ui/canvas-ui.css +12 -0
  36. package/src/charts/README.md +2 -2
  37. package/src/charts/area-chart.svelte +3 -10
  38. package/src/charts/bar-chart.svelte +4 -12
  39. package/src/charts/dashboard/data-table.svelte +21 -21
  40. package/src/charts/dashboard/index.ts +1 -3
  41. package/src/charts/dashboard/rating-chip.svelte +9 -11
  42. package/src/charts/dashboard/stat-tile.svelte +3 -4
  43. package/src/charts/line-chart.svelte +3 -10
  44. package/src/charts/primitives/chart-utils.test.ts +17 -0
  45. package/src/charts/primitives/chart-utils.ts +16 -0
  46. package/src/charts/time-series-chart.svelte +8 -4
  47. package/src/components/chip/count-chip.svelte +35 -0
  48. package/src/components/chip/index.ts +1 -0
  49. package/src/components/data-table/data-table.ts +1 -1
  50. package/src/components/data-table/row-context-menu.svelte +3 -4
  51. package/src/components/data-table/sortable-head.svelte +45 -10
  52. package/src/components/language-flag/flags.ts +5 -0
  53. package/src/components/language-flag/index.ts +1 -0
  54. package/src/components/language-flag/language-flag.svelte +3 -11
  55. package/src/components/list-page/empty-state.svelte +90 -12
  56. package/src/components/list-page/filter-popover.svelte +2 -5
  57. package/src/components/list-page/grouped-list.svelte +2 -4
  58. package/src/components/list-page/index.ts +1 -3
  59. package/src/components/section-header/section-header.svelte +2 -6
  60. package/src/components/segmented/segment.svelte +13 -19
  61. package/src/components/segmented/segmented-filter.svelte +1 -2
  62. package/src/components/table-view/table-view.svelte +31 -45
  63. package/src/components/table-view/table-view.test.ts +6 -3
  64. package/src/components/work-parts/group-head.svelte +1 -1
  65. package/src/components/work-parts/index.ts +1 -1
  66. package/src/editor/doc-blocks.ts +2 -1
  67. package/src/effects/animated-beam/path-calculator.svelte.ts +19 -5
  68. package/src/effects/marquee/Marquee.css +6 -3
  69. package/src/effects/marquee/Marquee.svelte +6 -2
  70. package/src/inbox/thread-inbox.svelte +6 -4
  71. package/src/lib/dates.test.ts +63 -0
  72. package/src/lib/dates.ts +63 -0
  73. package/src/lib/panel.svelte +116 -0
  74. package/src/lib/panel.ts +2 -2
  75. package/src/lib/tone.ts +15 -0
  76. package/src/panel/side-panel.svelte +4 -4
  77. package/src/record/format.ts +3 -15
  78. package/src/record/logic.ts +2 -6
  79. package/src/record/record-stats.svelte +4 -3
  80. package/src/record/steps-rail.svelte +7 -4
  81. package/src/status/index.ts +1 -1
  82. package/src/status/status-pill.svelte +2 -8
  83. package/src/thread/thread.svelte +3 -6
  84. package/src/work/README.md +0 -1
  85. package/src/work/dates.ts +3 -3
  86. package/src/work/demo.svelte +9 -1
  87. package/src/work/detail/detail-demo.svelte +30 -27
  88. package/src/work/detail/detail-types.ts +1 -29
  89. package/src/work/detail/task-detail.svelte +18 -17
  90. package/src/work/docs/doc-menu.svelte +2 -1
  91. package/src/work/docs/edited.ts +4 -7
  92. package/src/work/inbox/inbox-card.svelte +4 -9
  93. package/src/work/inbox/inbox-logic.ts +4 -16
  94. package/src/work/index.ts +1 -3
  95. package/src/work/time/duration.ts +2 -3
  96. package/dist/ai/thinking-dots/index.js +0 -3
  97. package/dist/components/icon/index.js +0 -1
  98. package/dist/components/locale-switch/index.js +0 -1
  99. package/dist/icons/trades.js +0 -49
  100. package/dist/status/pill-tint.js +0 -13
  101. package/dist/work/presets/sales.js +0 -39
  102. package/src/ai/confirmation/ConfirmationAccepted.svelte +0 -23
  103. package/src/ai/confirmation/ConfirmationFailed.svelte +0 -23
  104. package/src/ai/confirmation/ConfirmationRejected.svelte +0 -23
  105. package/src/ai/thinking-dots/ThinkingDots.svelte +0 -42
  106. package/src/ai/thinking-dots/index.ts +0 -5
  107. package/src/charts/dashboard/dashboard-empty.svelte +0 -55
  108. package/src/charts/dashboard/data-panel.svelte +0 -81
  109. package/src/charts/dashboard/data-sort-head.svelte +0 -102
  110. package/src/components/icon/icon.svelte +0 -23
  111. package/src/components/icon/index.ts +0 -1
  112. package/src/components/list-page/detail-empty.svelte +0 -45
  113. package/src/components/list-page/detail-panel.svelte +0 -82
  114. package/src/components/list-page/rail-empty.svelte +0 -44
  115. package/src/components/locale-switch/index.ts +0 -1
  116. package/src/components/locale-switch/locale-switch.svelte +0 -59
  117. package/src/components/work-parts/count-chip.svelte +0 -20
  118. package/src/icons/TradeIcon.svelte +0 -16
  119. package/src/icons/trades.ts +0 -89
  120. package/src/status/pill-tint.ts +0 -16
  121. package/src/work/detail/task-property.svelte +0 -17
  122. package/src/work/detail/task-thread.svelte +0 -221
  123. package/src/work/presets/sales.ts +0 -16
@@ -13,12 +13,12 @@
13
13
  import { Card } from "../../shadcn/card/index.js";
14
14
  import * as Avatar from "../../shadcn/avatar/index.js";
15
15
  import TaskDetail from "./task-detail.svelte";
16
- import TaskProperty from "./task-property.svelte";
16
+ import PropertyRow from "../../components/work-parts/property-row.svelte";
17
17
  import TaskChip from "./task-chip.svelte";
18
18
  import TimeTracker from "../time/time-tracker.svelte";
19
19
  import StatusDot from "../../status/status-dot.svelte";
20
20
  import { defineStatuses } from "../../status/define-statuses.js";
21
- import type { TaskComment } from "./detail-types.js";
21
+ import type { ThreadComment, ThreadPost } from "../../thread/types.js";
22
22
  import type { TimeLogInput, TimeStopInfo } from "../time/time-types.js";
23
23
 
24
24
  type Scenario =
@@ -53,32 +53,33 @@
53
53
  const hour = 3_600_000;
54
54
  const stamp = (agoHours: number) => new Date(now - agoHours * hour).toISOString();
55
55
 
56
- const seed: TaskComment[] = [
56
+ const person = (name: string) => ({ name, kind: "user" as const });
57
+ const planner = { name: "Planner", kind: "agent" as const };
58
+
59
+ const seed: ThreadComment[] = [
57
60
  {
58
61
  id: "c1",
59
- author: "Ada Lovelace",
62
+ author: person("Ada Lovelace"),
60
63
  body: "Calls with Yoan: the format that sold the template was simple. A board on why, then the system, then the demo.",
61
64
  createdAt: stamp(30),
62
65
  },
63
66
  {
64
67
  id: "c2",
65
- author: "Planner",
66
- agent: true,
68
+ author: planner,
67
69
  body: "Reused that shape for the outline. Three sections, the demo last, one call to action at the end.",
68
70
  createdAt: stamp(26),
69
71
  },
70
72
  {
71
73
  id: "c3",
72
- author: "Bo Chen",
74
+ author: person("Bo Chen"),
73
75
  body: "Looks right. I'll record Thursday.",
74
76
  createdAt: stamp(5),
75
77
  edited: true,
76
78
  },
77
79
  ];
78
- const crowd: TaskComment[] = Array.from({ length: 24 }, (_, i) => ({
80
+ const crowd: ThreadComment[] = Array.from({ length: 24 }, (_, i) => ({
79
81
  id: `b${i}`,
80
- author: i % 4 === 1 ? "Planner" : (people[i % people.length] ?? "Ada Lovelace"),
81
- agent: i % 4 === 1,
82
+ author: i % 4 === 1 ? planner : person(people[i % people.length] ?? "Ada Lovelace"),
82
83
  body:
83
84
  i % 5 === 0
84
85
  ? "Pushed the second cut. The intro runs 40 seconds, so I trimmed the first example and kept the one with numbers."
@@ -95,7 +96,7 @@
95
96
  let narrow = $state(false);
96
97
  let title = $state("Record the video and hand it off");
97
98
  let description = $state<string | null>(brief);
98
- let thread = $state<TaskComment[]>([...seed]);
99
+ let thread = $state<ThreadComment[]>([...seed]);
99
100
  let done = $state(false);
100
101
  let timer = $state<{ startedAt: number } | null>(null);
101
102
  let entries = $state([
@@ -140,13 +141,14 @@
140
141
  timer = null;
141
142
  }
142
143
 
143
- function onSend(body: string) {
144
+ function onPostComment({ body, parentId }: ThreadPost) {
144
145
  thread = [
145
146
  ...thread,
146
147
  {
147
148
  id: `n${thread.length}`,
148
- author: "Ada Lovelace",
149
+ author: person("Ada Lovelace"),
149
150
  body,
151
+ parentId,
150
152
  createdAt: new Date().toISOString(),
151
153
  },
152
154
  ];
@@ -191,27 +193,28 @@
191
193
  }}
192
194
  comments={thread}
193
195
  loadingComments={scenario === "loading"}
194
- {onSend}
196
+ {onPostComment}
195
197
  onDeleteComment={(comment) => {
196
198
  thread = thread.filter((row) => row.id !== comment.id);
197
199
  }}
198
- canEditComment={(comment) => !comment.agent && comment.author === "Ada Lovelace"}
200
+ canEditComment={(comment) =>
201
+ comment.author.kind === "user" && comment.author.name === "Ada Lovelace"}
199
202
  >
200
203
  {#snippet meta()}
201
204
  <span>From a call</span>
202
205
  <span class="tabular-nums">Created Sep 9, 2026</span>
203
206
  {/snippet}
204
207
  {#snippet properties()}
205
- <TaskProperty label="Status" icon={CircleHalf}>
208
+ <PropertyRow label="Status" icon={CircleHalf} layout="responsive">
206
209
  <TaskChip aria-label="Status: {statusLabel}"
207
210
  ><StatusDot {statuses} {status} size={14} animate={false} />{statusLabel}</TaskChip
208
211
  >
209
- </TaskProperty>
210
- <TaskProperty label="Priority" icon={Flag}>
212
+ </PropertyRow>
213
+ <PropertyRow label="Priority" icon={Flag} layout="responsive">
211
214
  <TaskChip><Flag weight="fill" class="text-destructive" aria-hidden="true" />High</TaskChip
212
215
  >
213
- </TaskProperty>
214
- <TaskProperty label="Assignee" icon={UserCircle}>
216
+ </PropertyRow>
217
+ <PropertyRow label="Assignee" icon={UserCircle} layout="responsive">
215
218
  {#if assignee}
216
219
  <TaskChip>
217
220
  <Avatar.Root class="size-4" aria-hidden="true">
@@ -222,14 +225,14 @@
222
225
  {:else}
223
226
  <TaskChip empty><CircleDashed aria-hidden="true" />Assign</TaskChip>
224
227
  {/if}
225
- </TaskProperty>
226
- <TaskProperty label="Due date" icon={CalendarBlank}>
228
+ </PropertyRow>
229
+ <PropertyRow label="Due date" icon={CalendarBlank} layout="responsive">
227
230
  <TaskChip><CalendarBlank aria-hidden="true" />Oct 9, 2026</TaskChip>
228
- </TaskProperty>
229
- <TaskProperty label="Project" icon={Folder}>
231
+ </PropertyRow>
232
+ <PropertyRow label="Project" icon={Folder} layout="responsive">
230
233
  <TaskChip><Folder aria-hidden="true" />Agency OS</TaskChip>
231
- </TaskProperty>
232
- <TaskProperty label="Time" icon={Timer}>
234
+ </PropertyRow>
235
+ <PropertyRow label="Time" icon={Timer} layout="responsive">
233
236
  <TimeTracker
234
237
  {entries}
235
238
  running={timer}
@@ -248,7 +251,7 @@
248
251
  entries = entries.filter((row) => row.id !== entry.id);
249
252
  }}
250
253
  />
251
- </TaskProperty>
254
+ </PropertyRow>
252
255
  {/snippet}
253
256
  </TaskDetail>
254
257
  </Card>
@@ -1,16 +1,4 @@
1
- /** One entry in a task's thread. The host resolves the author's name. */
2
- export type TaskComment = {
3
- id: string;
4
- author: string;
5
- body: string;
6
- /** ISO timestamp. */
7
- createdAt: string;
8
- /** An agent wrote it: the thread draws the agent avatar and an "AI" label. */
9
- agent?: boolean;
10
- /** Set when the body changed after posting. */
11
- edited?: boolean;
12
- };
13
-
1
+ /** The task page's own words. The comment thread's come through `threadLabels`. */
14
2
  export type TaskDetailLabels = {
15
3
  markComplete: string;
16
4
  markIncomplete: string;
@@ -21,14 +9,6 @@ export type TaskDetailLabels = {
21
9
  save: string;
22
10
  saving: string;
23
11
  cancel: string;
24
- comments: string;
25
- commentPlaceholder: string;
26
- commentSend: string;
27
- commentEdit: string;
28
- commentDelete: string;
29
- commentEdited: string;
30
- commentsEmpty: string;
31
- ai: string;
32
12
  properties: string;
33
13
  };
34
14
 
@@ -42,13 +22,5 @@ export const DEFAULT_TASK_DETAIL_LABELS: TaskDetailLabels = {
42
22
  save: "Save",
43
23
  saving: "Saving",
44
24
  cancel: "Cancel",
45
- comments: "Activity",
46
- commentPlaceholder: "Leave a comment",
47
- commentSend: "Comment",
48
- commentEdit: "Edit comment",
49
- commentDelete: "Delete comment",
50
- commentEdited: "edited",
51
- commentsEmpty: "No comments yet",
52
- ai: "AI",
53
25
  properties: "Properties",
54
26
  };
@@ -4,12 +4,12 @@
4
4
  import { cn } from "../../lib/utils.js";
5
5
  import { Button } from "../../shadcn/button/index.js";
6
6
  import { Textarea } from "../../shadcn/textarea/index.js";
7
+ import Thread from "../../thread/thread.svelte";
8
+ import type { ThreadComment, ThreadLabels, ThreadPost } from "../../thread/types.js";
7
9
  import {
8
10
  DEFAULT_TASK_DETAIL_LABELS,
9
- type TaskComment,
10
11
  type TaskDetailLabels,
11
12
  } from "./detail-types.js";
12
- import TaskThread from "./task-thread.svelte";
13
13
 
14
14
  interface Props {
15
15
  title: string;
@@ -21,22 +21,24 @@
21
21
  onToggleDone?: () => void;
22
22
  /** One 12px line under the title: origin, dates. */
23
23
  meta?: Snippet;
24
- /** The rail. A row of `TaskProperty` with a `TaskChip` or any picker trigger in each. */
24
+ /** The rail. A `PropertyRow layout="responsive"` with a `TaskChip` or any picker trigger in each. */
25
25
  properties?: Snippet;
26
26
  /** Under the rail's rows: ids, shortcuts, anything quiet. */
27
27
  railFooter?: Snippet;
28
28
  description?: string | null;
29
29
  onDescriptionChange?: (next: string | null) => void | Promise<void>;
30
- comments?: TaskComment[];
30
+ comments?: ThreadComment[];
31
31
  loadingComments?: boolean;
32
- agentAvatar?: Snippet<[TaskComment]>;
33
- avatar?: Snippet<[TaskComment]>;
34
- when?: (comment: TaskComment) => string;
35
- onSend?: (body: string) => void | boolean | Promise<void | boolean>;
36
- onEditComment?: (comment: TaskComment, body: string) => void | Promise<void>;
37
- onDeleteComment?: (comment: TaskComment) => void | Promise<void>;
38
- canEditComment?: (comment: TaskComment) => boolean;
32
+ /** Replaces the default avatar for a person or an agent, drawn in a 24px slot. */
33
+ avatar?: Snippet<[ThreadComment]>;
34
+ when?: (comment: ThreadComment) => string;
35
+ /** Returns false to keep the draft. Without it no composer shows. */
36
+ onPostComment?: (post: ThreadPost) => void | boolean | Promise<void | boolean>;
37
+ onEditComment?: (comment: ThreadComment, body: string) => void | Promise<void>;
38
+ onDeleteComment?: (comment: ThreadComment) => void | Promise<void>;
39
+ canEditComment?: (comment: ThreadComment) => boolean;
39
40
  labels?: Partial<TaskDetailLabels>;
41
+ threadLabels?: Partial<ThreadLabels>;
40
42
  class?: string;
41
43
  }
42
44
 
@@ -53,14 +55,14 @@
53
55
  onDescriptionChange,
54
56
  comments = [],
55
57
  loadingComments = false,
56
- agentAvatar,
57
58
  avatar,
58
59
  when,
59
- onSend,
60
+ onPostComment,
60
61
  onEditComment,
61
62
  onDeleteComment,
62
63
  canEditComment,
63
64
  labels: labelOverrides,
65
+ threadLabels,
64
66
  class: className,
65
67
  }: Props = $props();
66
68
 
@@ -260,14 +262,13 @@
260
262
  {/if}
261
263
  </div>
262
264
 
263
- <TaskThread
265
+ <Thread
264
266
  {comments}
265
267
  loading={loadingComments}
266
- labels={labelOverrides}
267
- {agentAvatar}
268
+ labels={threadLabels}
268
269
  {avatar}
269
270
  {when}
270
- {onSend}
271
+ onPost={onPostComment}
271
272
  onEdit={onEditComment}
272
273
  onDelete={onDeleteComment}
273
274
  canEdit={canEditComment}
@@ -29,6 +29,7 @@
29
29
  AlertDialogTitle,
30
30
  } from "../../shadcn/alert-dialog/index.js";
31
31
  import { buttonVariants } from "../../shadcn/button/index.js";
32
+ import { copyText } from "../../components/copy/clipboard.js";
32
33
  import {
33
34
  Command,
34
35
  CommandEmpty,
@@ -193,7 +194,7 @@
193
194
  async function copy(kind: CopyKind, text: string) {
194
195
  open = false;
195
196
  // A blocked clipboard still reports, so the host can say the copy didn't land.
196
- await navigator.clipboard.writeText(text).catch(() => {});
197
+ await copyText(text);
197
198
  oncopy?.(kind, text);
198
199
  }
199
200
 
@@ -1,5 +1,7 @@
1
1
  /** The "Edited Oct 23" chip: how a stamp reads, and what to show when it can't be read. */
2
2
 
3
+ import { shortDate as monthDay } from "../../lib/dates.js";
4
+
3
5
  export type DateInput = Date | string | number | null | undefined;
4
6
 
5
7
  export type StampOptions = {
@@ -24,13 +26,8 @@ export function shortDate(value: DateInput, options: StampOptions = {}): string
24
26
  const date = parseStamp(value);
25
27
  if (!date) return "";
26
28
  const { locale = "en-US", timeZone, now = new Date() } = options;
27
- const sameYear = yearOf(date, options) === yearOf(now, options);
28
- return new Intl.DateTimeFormat(locale, {
29
- month: "short",
30
- day: "numeric",
31
- year: sameYear ? undefined : "numeric",
32
- timeZone,
33
- }).format(date);
29
+ const year = yearOf(date, options) !== yearOf(now, options);
30
+ return monthDay(date, { locale, timeZone, year });
34
31
  }
35
32
 
36
33
  /** "Oct 23, 14:44" for the last-edit row. Empty when the date can't be read. */
@@ -29,6 +29,7 @@
29
29
  import { TagChip } from "../../components/chip/index.js";
30
30
  import { workLooks } from "../../components/work-parts/looks.js";
31
31
  import { FolderSimple } from "phosphor-svelte";
32
+ import { Copied } from "../../components/copy/copied.svelte.js";
32
33
  import type { InboxDraftEdits, InboxItem, InboxVerdict } from "./inbox-types.js";
33
34
 
34
35
  interface Props {
@@ -78,7 +79,7 @@
78
79
  let steering = $state(false);
79
80
  let note = $state("");
80
81
  let noteEl = $state<HTMLTextAreaElement | null>(null);
81
- let copied = $state(false);
82
+ const copied = new Copied();
82
83
 
83
84
  // WHY the selection is derived from the pick: a revised proposal brings new
84
85
  // drafts, and the old pick may not exist in them. The agent's choice wins then.
@@ -158,13 +159,7 @@
158
159
  async function copy() {
159
160
  const draft = current();
160
161
  if (!draft) return;
161
- try {
162
- await navigator.clipboard.writeText(`${draft.subject}\n\n${draft.body}`);
163
- copied = true;
164
- setTimeout(() => (copied = false), 1600);
165
- } catch {
166
- copied = false;
167
- }
162
+ if (!(await copied.copy(`${draft.subject}\n\n${draft.body}`, "", 1600))) copied.reset();
168
163
  }
169
164
 
170
165
  let sendLabel = $derived(
@@ -364,7 +359,7 @@
364
359
  <Button size="sm" variant="ghost" onclick={steer} aria-keyshortcuts="s">Steer</Button>
365
360
  {#if drafts.length}
366
361
  <Button size="sm" variant="ghost" onclick={copy} class="ml-auto" aria-live="polite">
367
- {copied ? "Copied" : "Copy"}
362
+ {copied.is() ? "Copied" : "Copy"}
368
363
  </Button>
369
364
  {/if}
370
365
  </div>
@@ -1,3 +1,4 @@
1
+ import { relativeTime, shortDate } from "../../lib/dates.js";
1
2
  import type { InboxDraft, InboxItem } from "./inbox-types.js";
2
3
 
3
4
  /** The draft to open on: the agent's pick, else the first. */
@@ -64,12 +65,6 @@ export function freshReasoning(item: InboxItem, facts: CardFacts): string {
64
65
  }
65
66
 
66
67
  const ISO = /^\d{4}-\d{2}-\d{2}T/;
67
- const UNITS: [number, Intl.RelativeTimeFormatUnit][] = [
68
- [604_800, "week"],
69
- [86_400, "day"],
70
- [3_600, "hour"],
71
- [60, "minute"],
72
- ];
73
68
 
74
69
  /**
75
70
  * A time as a person says it: "3 hr. ago", "Oct 12". A host-formatted string stays as written,
@@ -79,16 +74,9 @@ export function whenOf(at: string, now: number = Date.now(), locale = "en-GB"):
79
74
  if (!ISO.test(at)) return at;
80
75
  const time = Date.parse(at);
81
76
  if (Number.isNaN(time)) return "";
82
- const seconds = Math.round((time - now) / 1000);
83
- const age = Math.abs(seconds);
84
- if (age >= 4 * 604_800) {
85
- return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(time);
86
- }
87
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "short" });
88
- for (const [size, unit] of UNITS) {
89
- if (age >= size) return rtf.format(Math.round(seconds / size), unit);
90
- }
91
- return rtf.format(0, "second");
77
+ const age = Math.abs(Math.round((time - now) / 1000));
78
+ if (age >= 4 * 604_800) return shortDate(time, { locale });
79
+ return relativeTime(time, now, { locale, style: "short" });
92
80
  }
93
81
 
94
82
  /** True when a key press belongs to a text field and so isn't a shortcut. */
package/src/work/index.ts CHANGED
@@ -23,11 +23,9 @@ export type {
23
23
  TaskListRowContext,
24
24
  } from "./task-types.js";
25
25
  export { default as TaskDetail } from "./detail/task-detail.svelte";
26
- export { default as TaskThread } from "./detail/task-thread.svelte";
27
- export { default as TaskProperty } from "./detail/task-property.svelte";
28
26
  export { default as TaskChip } from "./detail/task-chip.svelte";
29
27
  export { DEFAULT_TASK_DETAIL_LABELS } from "./detail/detail-types.js";
30
- export type { TaskComment, TaskDetailLabels } from "./detail/detail-types.js";
28
+ export type { TaskDetailLabels } from "./detail/detail-types.js";
31
29
  export { default as TimeTracker } from "./time/time-tracker.svelte";
32
30
  export { default as TimeChip } from "./time/time-chip.svelte";
33
31
  export { default as TimeRing } from "./time/time-ring.svelte";
@@ -1,3 +1,4 @@
1
+ import { localDay as dayOf } from "../../lib/dates.js";
1
2
  import type { Tone } from "../../lib/tone.js";
2
3
 
3
4
  /** "90", "45m", "1h", "1.5h", "1h30", "1h 30m" and "1:30" to whole minutes. Null when it isn't a duration. */
@@ -35,9 +36,7 @@ export function formatClock(ms: number): { hours: string; minutes: string; secon
35
36
 
36
37
  /** The reader's own calendar day as `YYYY-MM-DD`. */
37
38
  export function localDay(date: Date = new Date()): string {
38
- const month = String(date.getMonth() + 1).padStart(2, "0");
39
- const day = String(date.getDate()).padStart(2, "0");
40
- return `${date.getFullYear()}-${month}-${day}`;
39
+ return dayOf(date);
41
40
  }
42
41
 
43
42
  /** "Today", "Yesterday", then "Mon, Oct 12", in the given locale. */
@@ -1,3 +0,0 @@
1
- import ThinkingDots from "../../../src/ai/thinking-dots/ThinkingDots.svelte";
2
- export { ThinkingDots };
3
- export default ThinkingDots;
@@ -1 +0,0 @@
1
- export { default as Icon } from "../../../src/components/icon/icon.svelte";
@@ -1 +0,0 @@
1
- export { default as LocaleSwitch } from "../../../src/components/locale-switch/locale-switch.svelte";
@@ -1,49 +0,0 @@
1
- /**
2
- * Trade icon map. Tabler covers the domain Lucide and Phosphor miss.
3
- */
4
- /*
5
- * Lucide/Phosphor gap audit (Sept 2026) against the 14 trade pills in the "NOT SURE? TAP WHAT YOU DO" strip. Lucide has `hammer`, `wrench`, `hard-hat`, `bolt`, `brick-wall`,
6
- */
7
- /*
8
- * `building`, `car`, but no `trowel`, no tiling grid, no `window` cleaning `spray/squeegee`, no `crane`, no dedicated `handyman` toolset. Phosphor has `hammer`, `wrench`, `hard-hat`, `paint-roller`, and also no `trowel`.
9
- */
10
- /*
11
- * Tabler 3.46 (6184 icons, MIT) has all 14 at a consistent 2px stroke on 24×24: droplet/pipe, bolt, home (roof), tools, key, window,
12
- */
13
- /*
14
- * car-garage, hammer / wave-saw-tool, helmet, trowel, grid-dots, leaf, car, crane. Svelte 5 port is ` @tabler/icons-svelte-runes` (runes, not the Svelte 4 ` @tabler/icons-svelte`).
15
- */
16
- import { IconBolt, IconCar, IconCarGarage, IconCrane, IconDroplet, IconGridDots, IconHammer, IconHelmet, IconHome, IconKey, IconLeaf, IconTools, IconTrowel, IconWindow } from "@tabler/icons-svelte-runes";
17
- export const tradeIconMap = {
18
- plumbers: IconDroplet,
19
- electricians: IconBolt,
20
- roofers: IconHome,
21
- handymen: IconTools,
22
- locksmiths: IconKey,
23
- "window-cleaners": IconWindow,
24
- garages: IconCarGarage,
25
- carpenters: IconHammer,
26
- builders: IconHelmet,
27
- plasterers: IconTrowel,
28
- tilers: IconGridDots,
29
- gardeners: IconLeaf,
30
- "mobile-mechanics": IconCar,
31
- "construction-companies": IconCrane
32
- };
33
- export const tradeLabels = {
34
- plumbers: "PLUMBERS",
35
- electricians: "ELECTRICIANS",
36
- roofers: "ROOFERS",
37
- handymen: "HANDYMEN",
38
- locksmiths: "LOCKSMITHS",
39
- "window-cleaners": "WINDOW CLEANERS",
40
- garages: "GARAGES",
41
- carpenters: "CARPENTERS",
42
- builders: "BUILDERS",
43
- plasterers: "PLASTERERS",
44
- tilers: "TILERS",
45
- gardeners: "GARDENERS",
46
- "mobile-mechanics": "MOBILE MECHANICS",
47
- "construction-companies": "CONSTRUCTION COMPANIES"
48
- };
49
- export const tradeSlugs = Object.keys(tradeIconMap);
@@ -1,13 +0,0 @@
1
- // A quiet tint and no border: the icon and the text carry the tone, the fill only groups
2
- // them. Neutral reads off the foreground, not `bg-muted`, because muted sits 0.005 L from
3
- // the canvas and vanishes there (tokens board, "what fights the new scale").
4
- // It lives in a .ts module, not status-pill.svelte's module script, so a plain tsc consumer
5
- // can import it: tsc's `*.svelte` shim only knows a default export.
6
- export const PILL_TINT = {
7
- neutral: "bg-foreground/6 text-muted-foreground",
8
- accent: "bg-[color-mix(in_oklab,var(--tone-accent,var(--color-primary))_12%,transparent)] text-[color:var(--tone-accent,var(--color-primary))]",
9
- info: "bg-info/12 text-info-800 dark:text-info-400",
10
- success: "bg-success/12 text-success-800 dark:text-success-400",
11
- warning: "bg-warning/14 text-warning-800 dark:text-warning-400",
12
- destructive: "bg-destructive/10 text-destructive-ink"
13
- };
@@ -1,39 +0,0 @@
1
- import { defineStatuses } from "../../status/define-statuses.js";
2
- /**
3
- * An example status map for a sales pipeline. Names are neutral on purpose:
4
- * swap the keys and labels for your own domain.
5
- */
6
- export const salesStatuses = defineStatuses({
7
- new: {
8
- label: "New",
9
- tone: "accent",
10
- variant: "open"
11
- },
12
- contacted: {
13
- label: "Contacted",
14
- tone: "info",
15
- variant: "open"
16
- },
17
- proposal: {
18
- label: "Proposal",
19
- tone: "accent",
20
- variant: "progress",
21
- progress: .25
22
- },
23
- negotiation: {
24
- label: "Negotiation",
25
- tone: "warning",
26
- variant: "progress",
27
- progress: .75
28
- },
29
- won: {
30
- label: "Won",
31
- tone: "success",
32
- variant: "completed"
33
- },
34
- lost: {
35
- label: "Lost",
36
- tone: "destructive",
37
- variant: "lost"
38
- }
39
- });
@@ -1,23 +0,0 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from "svelte";
3
-
4
- export interface ConfirmationAcceptedProps {
5
- children?: Snippet;
6
- }
7
- </script>
8
-
9
- <script lang="ts">
10
- import { getConfirmationContext } from "./confirmation-context.svelte.js";
11
-
12
- let { children }: ConfirmationAcceptedProps = $props();
13
-
14
- const ctx = getConfirmationContext();
15
- </script>
16
-
17
- {#if ctx.kind === "accepted"}
18
- <div
19
- class="flex flex-col gap-2 animate-in fade-in slide-in-from-top-1 duration-300 motion-reduce:animate-none"
20
- >
21
- {@render children?.()}
22
- </div>
23
- {/if}
@@ -1,23 +0,0 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from "svelte";
3
-
4
- export interface ConfirmationFailedProps {
5
- children?: Snippet;
6
- }
7
- </script>
8
-
9
- <script lang="ts">
10
- import { getConfirmationContext } from "./confirmation-context.svelte.js";
11
-
12
- let { children }: ConfirmationFailedProps = $props();
13
-
14
- const ctx = getConfirmationContext();
15
- </script>
16
-
17
- {#if ctx.kind === "failed"}
18
- <div
19
- class="flex flex-col gap-2 animate-in fade-in slide-in-from-top-1 duration-300 motion-reduce:animate-none"
20
- >
21
- {@render children?.()}
22
- </div>
23
- {/if}
@@ -1,23 +0,0 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from "svelte";
3
-
4
- export interface ConfirmationRejectedProps {
5
- children?: Snippet;
6
- }
7
- </script>
8
-
9
- <script lang="ts">
10
- import { getConfirmationContext } from "./confirmation-context.svelte.js";
11
-
12
- let { children }: ConfirmationRejectedProps = $props();
13
-
14
- const ctx = getConfirmationContext();
15
- </script>
16
-
17
- {#if ctx.kind === "rejected"}
18
- <div
19
- class="flex flex-col gap-2 animate-in fade-in slide-in-from-top-1 duration-300 motion-reduce:animate-none"
20
- >
21
- {@render children?.()}
22
- </div>
23
- {/if}
@@ -1,42 +0,0 @@
1
- <script lang="ts">
2
- // =========================
3
- // IMPORTS
4
- // =========================
5
-
6
- import { cn } from "../../lib/utils.js";
7
- import type { HTMLAttributes } from "svelte/elements";
8
- import TypingDots from "../typing-dots/TypingDots.svelte";
9
-
10
- // =========================
11
- // TYPES
12
- // =========================
13
-
14
- export interface ThinkingDotsProps extends HTMLAttributes<HTMLDivElement> {
15
- label?: string;
16
- class?: string;
17
- }
18
-
19
- // =========================
20
- // PROPS
21
- // =========================
22
-
23
- let { label = "Thinking", class: className, ...rest }: ThinkingDotsProps = $props();
24
- </script>
25
-
26
- <!-- =========================== -->
27
- <!-- MARKUP -->
28
- <!-- =========================== -->
29
-
30
- <!-- WHY: the dots animation is the shared chat TypingDots primitive; this only
31
- adds the AI "thinking" label so there is a single source for the dots. -->
32
- <div
33
- class={cn(
34
- "inline-flex items-center gap-2 text-sm text-[color:var(--ai-thinking-fg,inherit)]",
35
- className,
36
- )}
37
- role="status"
38
- {...rest}
39
- >
40
- {#if label}<span>{label}</span>{/if}
41
- <TypingDots />
42
- </div>
@@ -1,5 +0,0 @@
1
- import ThinkingDots from "./ThinkingDots.svelte";
2
- import type { ThinkingDotsProps } from "./ThinkingDots.svelte";
3
-
4
- export { ThinkingDots, type ThinkingDotsProps };
5
- export default ThinkingDots;