@critical-path/svelte 0.4.0 → 0.5.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @critical-path/svelte
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 315425c: fix(core): generate Firebase Storage download tokens and construct valid public URLs
8
+ - Updated dependencies [315425c]
9
+ - @critical-path/core@0.8.1
10
+ - @critical-path/client@0.4.1
11
+
12
+ ## 0.5.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 1811922: Add attachment URL validation against large data URIs, sanitize undefined properties in FirebaseStore, provide direct attachment upload routes & SDK methods, and introduce TaskActivityState for combined comment & attachment threads.
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [1811922]
21
+ - @critical-path/core@0.8.0
22
+ - @critical-path/client@0.4.0
23
+
3
24
  ## 0.4.0
4
25
 
5
26
  ### Minor Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Svelte 5 Runes Reactive Integrations for Critical Path.**
4
4
 
5
- `@critical-path/svelte` provides Svelte 5 Runes reactive state classes and factories (`ProjectState`, `TaskState`) for building project management UIs in Svelte 5 and SvelteKit applications.
5
+ `@critical-path/svelte` provides Svelte 5 Runes reactive state classes and factories (`ProjectState`, `TaskState`, `WorkflowState`, `CommentState`, `AttachmentState`, `TaskActivityState`) for building project management UIs in Svelte 5 and SvelteKit applications.
6
6
 
7
7
  ---
8
8
 
@@ -16,7 +16,9 @@ pnpm add @critical-path/svelte svelte@^5.0.0
16
16
 
17
17
  ---
18
18
 
19
- ## 💡 Usage Example (Svelte 5 Runes)
19
+ ## 💡 Usage Examples (Svelte 5 Runes)
20
+
21
+ ### 1. Projects & Tasks
20
22
 
21
23
  ```svelte
22
24
  <script lang="ts">
@@ -50,8 +52,56 @@ pnpm add @critical-path/svelte svelte@^5.0.0
50
52
  {/if}
51
53
  ```
52
54
 
55
+ ### 2. Unified Task Activity & Threaded Discussions (`TaskActivityState`)
56
+
57
+ Combines threaded comments with inline attachments (`attachment.commentId === comment.id`) and standalone attachments in a single reactive store:
58
+
59
+ ```svelte
60
+ <script lang="ts">
61
+ import { onMount } from 'svelte';
62
+ import { createCriticalPathClient, createTaskActivityState } from '@critical-path/svelte';
63
+
64
+ const client = createCriticalPathClient({ baseUrl: '/api/critical-path' });
65
+ const activityState = createTaskActivityState(client, 'task_1');
66
+
67
+ onMount(() => {
68
+ activityState.fetch();
69
+ });
70
+
71
+ async function handleSend(text: string, fileUrl?: string) {
72
+ await activityState.addComment(
73
+ { content: text, authorId: 'user_1', authorType: 'user' },
74
+ fileUrl ? [{ filename: 'upload.png', url: fileUrl, uploaderId: 'user_1', mimeType: 'image/png', sizeBytes: 1024 }] : []
75
+ );
76
+ }
77
+ </script>
78
+
79
+ {#each activityState.threads as thread}
80
+ <div class="comment">
81
+ <p><strong>{thread.authorId}</strong>: {thread.content}</p>
82
+
83
+ <!-- Inline comment attachments -->
84
+ {#if thread.attachments.length > 0}
85
+ <div class="attachments">
86
+ {#each thread.attachments as att}
87
+ <a href={att.url} target="_blank">{att.filename}</a>
88
+ {/each}
89
+ </div>
90
+ {/if}
91
+
92
+ <!-- Threaded replies -->
93
+ {#each thread.replies as reply}
94
+ <div class="reply">
95
+ <p>↪ {reply.authorId}: {reply.content}</p>
96
+ </div>
97
+ {/each}
98
+ </div>
99
+ {/each}
100
+ ```
101
+
53
102
  ---
54
103
 
55
104
  ## 📄 License
56
105
 
57
106
  MIT © [Critical Path](https://github.com/Pixerate/Critical-Path)
107
+
@@ -0,0 +1,27 @@
1
+ import type { CriticalPathClient } from '@critical-path/client';
2
+ import type { Comment, Attachment } from '@critical-path/core';
3
+ export interface ThreadedCommentWithAttachments extends Comment {
4
+ attachments: Attachment[];
5
+ replies: ThreadedCommentWithAttachments[];
6
+ }
7
+ export declare class TaskActivityState {
8
+ private client;
9
+ taskId?: string | undefined;
10
+ comments: Comment[];
11
+ attachments: Attachment[];
12
+ loading: boolean;
13
+ error: Error | null;
14
+ threads: ThreadedCommentWithAttachments[];
15
+ standaloneAttachments: Attachment[];
16
+ constructor(client: CriticalPathClient, taskId?: string | undefined);
17
+ fetch(taskId?: string): Promise<void>;
18
+ addComment(input: Omit<Comment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>, attachmentInputs?: Array<Omit<Attachment, 'id' | 'taskId' | 'commentId' | 'createdAt' | 'updatedAt'>>): Promise<{
19
+ comment: Comment;
20
+ attachments: Attachment[];
21
+ }>;
22
+ addAttachment(input: Omit<Attachment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>): Promise<Attachment>;
23
+ deleteComment(id: string): Promise<void>;
24
+ deleteAttachment(id: string): Promise<void>;
25
+ }
26
+ export declare function createTaskActivityState(client: CriticalPathClient, taskId?: string): TaskActivityState;
27
+ //# sourceMappingURL=activity-state.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-state.svelte.d.ts","sourceRoot":"","sources":["../src/activity-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAE/D,MAAM,WAAW,8BAA+B,SAAQ,OAAO;IAC7D,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,OAAO,EAAE,8BAA8B,EAAE,CAAC;CAC3C;AAED,qBAAa,iBAAiB;IA6ChB,OAAO,CAAC,MAAM;IAA6B,MAAM,CAAC,EAAE,MAAM;IA5CtE,QAAQ,YAAyB;IACjC,WAAW,eAA4B;IACvC,OAAO,UAA0B;IACjC,KAAK,eAA8B;IAEnC,OAAO,mCAiCJ;IAEH,qBAAqB,eAElB;gBAEiB,MAAM,EAAE,kBAAkB,EAAS,MAAM,CAAC,EAAE,MAAM,YAAA;IAEhE,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM;IAwBrB,UAAU,CACd,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,CAAC,EACjE,gBAAgB,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC;;;;IAiCjG,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,CAAC;IAelF,aAAa,CAAC,EAAE,EAAE,MAAM;IAWxB,gBAAgB,CAAC,EAAE,EAAE,MAAM;CAUlC;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAEtG"}
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ export * from './task-state.svelte.js';
5
5
  export * from './workflow-state.svelte.js';
6
6
  export * from './comment-state.svelte.js';
7
7
  export * from './attachment-state.svelte.js';
8
+ export * from './activity-state.svelte.js';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE/E,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,aAAa,GAAG,kBAAkB,CAEnF;AAED,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE/E,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,aAAa,GAAG,kBAAkB,CAEnF;AAED,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -383,9 +383,141 @@ function d(e, t) {
383
383
  return new u(e, t);
384
384
  }
385
385
  //#endregion
386
+ //#region src/activity-state.svelte.ts
387
+ var f = class {
388
+ client;
389
+ taskId;
390
+ #e = t.state(t.proxy([]));
391
+ get comments() {
392
+ return t.get(this.#e);
393
+ }
394
+ set comments(e) {
395
+ t.set(this.#e, e, !0);
396
+ }
397
+ #t = t.state(t.proxy([]));
398
+ get attachments() {
399
+ return t.get(this.#t);
400
+ }
401
+ set attachments(e) {
402
+ t.set(this.#t, e, !0);
403
+ }
404
+ #n = t.state(!1);
405
+ get loading() {
406
+ return t.get(this.#n);
407
+ }
408
+ set loading(e) {
409
+ t.set(this.#n, e, !0);
410
+ }
411
+ #r = t.state(null);
412
+ get error() {
413
+ return t.get(this.#r);
414
+ }
415
+ set error(e) {
416
+ t.set(this.#r, e, !0);
417
+ }
418
+ #i = t.derived(() => {
419
+ let e = /* @__PURE__ */ new Map(), t = [], n = /* @__PURE__ */ new Map();
420
+ for (let e of this.attachments) e.commentId && (n.has(e.commentId) || n.set(e.commentId, []), n.get(e.commentId).push(e));
421
+ for (let t of this.comments) e.set(t.id, {
422
+ ...t,
423
+ attachments: n.get(t.id) || [],
424
+ replies: []
425
+ });
426
+ for (let n of this.comments) {
427
+ let r = e.get(n.id);
428
+ n.parentId && e.has(n.parentId) ? e.get(n.parentId).replies.push(r) : t.push(r);
429
+ }
430
+ return t;
431
+ });
432
+ get threads() {
433
+ return t.get(this.#i);
434
+ }
435
+ set threads(e) {
436
+ t.set(this.#i, e);
437
+ }
438
+ #a = t.derived(() => this.attachments.filter((e) => !e.commentId));
439
+ get standaloneAttachments() {
440
+ return t.get(this.#a);
441
+ }
442
+ set standaloneAttachments(e) {
443
+ t.set(this.#a, e);
444
+ }
445
+ constructor(e, t) {
446
+ this.client = e, this.taskId = t;
447
+ }
448
+ async fetch(e) {
449
+ let t = e || this.taskId;
450
+ if (!t) {
451
+ this.comments = [], this.attachments = [];
452
+ return;
453
+ }
454
+ this.taskId = t, this.loading = !0, this.error = null;
455
+ try {
456
+ let [e, n] = await Promise.all([this.client.getComments(t), this.client.getAttachments({ taskId: t })]);
457
+ this.comments = e, this.attachments = n;
458
+ } catch (e) {
459
+ this.error = e instanceof Error ? e : Error(String(e));
460
+ } finally {
461
+ this.loading = !1;
462
+ }
463
+ }
464
+ async addComment(e, t) {
465
+ if (!this.taskId) throw Error("TaskActivityState requires a taskId to add comments.");
466
+ try {
467
+ let n = await this.client.addComment({
468
+ ...e,
469
+ taskId: this.taskId
470
+ }), r = [];
471
+ return t && t.length > 0 && (r = await Promise.all(t.map((e) => this.client.createAttachment({
472
+ ...e,
473
+ taskId: this.taskId,
474
+ commentId: n.id
475
+ })))), this.comments = [...this.comments, n], r.length > 0 && (this.attachments = [...this.attachments, ...r]), {
476
+ comment: n,
477
+ attachments: r
478
+ };
479
+ } catch (e) {
480
+ let t = e instanceof Error ? e : Error(String(e));
481
+ throw this.error = t, t;
482
+ }
483
+ }
484
+ async addAttachment(e) {
485
+ if (!this.taskId) throw Error("TaskActivityState requires a taskId to add attachments.");
486
+ try {
487
+ let t = await this.client.createAttachment({
488
+ ...e,
489
+ taskId: this.taskId
490
+ });
491
+ return this.attachments = [t, ...this.attachments], t;
492
+ } catch (e) {
493
+ let t = e instanceof Error ? e : Error(String(e));
494
+ throw this.error = t, t;
495
+ }
496
+ }
497
+ async deleteComment(e) {
498
+ try {
499
+ await this.client.deleteComment(e), this.comments = this.comments.filter((t) => t.id !== e);
500
+ } catch (e) {
501
+ let t = e instanceof Error ? e : Error(String(e));
502
+ throw this.error = t, t;
503
+ }
504
+ }
505
+ async deleteAttachment(e) {
506
+ try {
507
+ await this.client.deleteAttachment(e), this.attachments = this.attachments.filter((t) => t.id !== e);
508
+ } catch (e) {
509
+ let t = e instanceof Error ? e : Error(String(e));
510
+ throw this.error = t, t;
511
+ }
512
+ }
513
+ };
514
+ function p(e, t) {
515
+ return new f(e, t);
516
+ }
517
+ //#endregion
386
518
  //#region src/index.ts
387
- function f(t) {
519
+ function m(t) {
388
520
  return new e(t);
389
521
  }
390
522
  //#endregion
391
- export { u as AttachmentState, c as CommentState, n as ProjectState, i as TaskState, o as WorkflowState, d as createAttachmentState, l as createCommentState, f as createCriticalPathClient, r as createProjectState, a as createTaskState, s as createWorkflowState };
523
+ export { u as AttachmentState, c as CommentState, n as ProjectState, f as TaskActivityState, i as TaskState, o as WorkflowState, d as createAttachmentState, l as createCommentState, m as createCriticalPathClient, r as createProjectState, p as createTaskActivityState, a as createTaskState, s as createWorkflowState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@critical-path/svelte",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Svelte 5 Runes state and reactive integrations for Critical Path headless project management framework",
5
5
  "author": "Jack James",
6
6
  "license": "MIT",
@@ -18,8 +18,8 @@
18
18
  }
19
19
  },
20
20
  "dependencies": {
21
- "@critical-path/client": "0.3.0",
22
- "@critical-path/core": "0.7.0"
21
+ "@critical-path/client": "0.4.1",
22
+ "@critical-path/core": "0.8.1"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "svelte": "^5.0.0"
@@ -0,0 +1,156 @@
1
+ /// <reference types="svelte" />
2
+ import type { CriticalPathClient } from '@critical-path/client';
3
+ import type { Comment, Attachment } from '@critical-path/core';
4
+
5
+ export interface ThreadedCommentWithAttachments extends Comment {
6
+ attachments: Attachment[];
7
+ replies: ThreadedCommentWithAttachments[];
8
+ }
9
+
10
+ export class TaskActivityState {
11
+ comments = $state<Comment[]>([]);
12
+ attachments = $state<Attachment[]>([]);
13
+ loading = $state<boolean>(false);
14
+ error = $state<Error | null>(null);
15
+
16
+ threads = $derived.by(() => {
17
+ const map = new Map<string, ThreadedCommentWithAttachments>();
18
+ const roots: ThreadedCommentWithAttachments[] = [];
19
+
20
+ // Group attachments by commentId
21
+ const attachmentsByComment = new Map<string, Attachment[]>();
22
+ for (const a of this.attachments) {
23
+ if (a.commentId) {
24
+ if (!attachmentsByComment.has(a.commentId)) {
25
+ attachmentsByComment.set(a.commentId, []);
26
+ }
27
+ attachmentsByComment.get(a.commentId)!.push(a);
28
+ }
29
+ }
30
+
31
+ for (const c of this.comments) {
32
+ map.set(c.id, {
33
+ ...c,
34
+ attachments: attachmentsByComment.get(c.id) || [],
35
+ replies: []
36
+ });
37
+ }
38
+
39
+ for (const c of this.comments) {
40
+ const threaded = map.get(c.id)!;
41
+ if (c.parentId && map.has(c.parentId)) {
42
+ map.get(c.parentId)!.replies.push(threaded);
43
+ } else {
44
+ roots.push(threaded);
45
+ }
46
+ }
47
+
48
+ return roots;
49
+ });
50
+
51
+ standaloneAttachments = $derived.by(() => {
52
+ return this.attachments.filter((a) => !a.commentId);
53
+ });
54
+
55
+ constructor(private client: CriticalPathClient, public taskId?: string) {}
56
+
57
+ async fetch(taskId?: string) {
58
+ const targetTaskId = taskId || this.taskId;
59
+ if (!targetTaskId) {
60
+ this.comments = [];
61
+ this.attachments = [];
62
+ return;
63
+ }
64
+ this.taskId = targetTaskId;
65
+ this.loading = true;
66
+ this.error = null;
67
+ try {
68
+ const [comments, attachments] = await Promise.all([
69
+ this.client.getComments(targetTaskId),
70
+ this.client.getAttachments({ taskId: targetTaskId })
71
+ ]);
72
+ this.comments = comments;
73
+ this.attachments = attachments;
74
+ } catch (err) {
75
+ this.error = err instanceof Error ? err : new Error(String(err));
76
+ } finally {
77
+ this.loading = false;
78
+ }
79
+ }
80
+
81
+ async addComment(
82
+ input: Omit<Comment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>,
83
+ attachmentInputs?: Array<Omit<Attachment, 'id' | 'taskId' | 'commentId' | 'createdAt' | 'updatedAt'>>
84
+ ) {
85
+ if (!this.taskId) {
86
+ throw new Error('TaskActivityState requires a taskId to add comments.');
87
+ }
88
+ try {
89
+ const comment = await this.client.addComment({ ...input, taskId: this.taskId });
90
+ let createdAttachments: Attachment[] = [];
91
+
92
+ if (attachmentInputs && attachmentInputs.length > 0) {
93
+ createdAttachments = await Promise.all(
94
+ attachmentInputs.map((att) =>
95
+ this.client.createAttachment({
96
+ ...att,
97
+ taskId: this.taskId,
98
+ commentId: comment.id
99
+ })
100
+ )
101
+ );
102
+ }
103
+
104
+ this.comments = [...this.comments, comment];
105
+ if (createdAttachments.length > 0) {
106
+ this.attachments = [...this.attachments, ...createdAttachments];
107
+ }
108
+ return { comment, attachments: createdAttachments };
109
+ } catch (err) {
110
+ const errorObj = err instanceof Error ? err : new Error(String(err));
111
+ this.error = errorObj;
112
+ throw errorObj;
113
+ }
114
+ }
115
+
116
+ async addAttachment(input: Omit<Attachment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>) {
117
+ if (!this.taskId) {
118
+ throw new Error('TaskActivityState requires a taskId to add attachments.');
119
+ }
120
+ try {
121
+ const created = await this.client.createAttachment({ ...input, taskId: this.taskId });
122
+ this.attachments = [created, ...this.attachments];
123
+ return created;
124
+ } catch (err) {
125
+ const errorObj = err instanceof Error ? err : new Error(String(err));
126
+ this.error = errorObj;
127
+ throw errorObj;
128
+ }
129
+ }
130
+
131
+ async deleteComment(id: string) {
132
+ try {
133
+ await this.client.deleteComment(id);
134
+ this.comments = this.comments.filter((c) => c.id !== id);
135
+ } catch (err) {
136
+ const errorObj = err instanceof Error ? err : new Error(String(err));
137
+ this.error = errorObj;
138
+ throw errorObj;
139
+ }
140
+ }
141
+
142
+ async deleteAttachment(id: string) {
143
+ try {
144
+ await this.client.deleteAttachment(id);
145
+ this.attachments = this.attachments.filter((a) => a.id !== id);
146
+ } catch (err) {
147
+ const errorObj = err instanceof Error ? err : new Error(String(err));
148
+ this.error = errorObj;
149
+ throw errorObj;
150
+ }
151
+ }
152
+ }
153
+
154
+ export function createTaskActivityState(client: CriticalPathClient, taskId?: string): TaskActivityState {
155
+ return new TaskActivityState(client, taskId);
156
+ }
package/src/index.test.ts CHANGED
@@ -10,10 +10,12 @@ import {
10
10
  CommentState,
11
11
  createCommentState,
12
12
  AttachmentState,
13
- createAttachmentState
13
+ createAttachmentState,
14
+ TaskActivityState,
15
+ createTaskActivityState
14
16
  } from './index.js';
15
17
  import type { CriticalPathClient } from '@critical-path/client';
16
- import type { Project, Task, Workflow } from '@critical-path/core';
18
+ import type { Project, Task, Workflow, Comment, Attachment } from '@critical-path/core';
17
19
 
18
20
  describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
19
21
  it('exports client factory and Svelte 5 Runes state factories', () => {
@@ -21,9 +23,15 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
21
23
  expect(createProjectState).toBeDefined();
22
24
  expect(createTaskState).toBeDefined();
23
25
  expect(createWorkflowState).toBeDefined();
26
+ expect(createCommentState).toBeDefined();
27
+ expect(createAttachmentState).toBeDefined();
28
+ expect(createTaskActivityState).toBeDefined();
24
29
  expect(ProjectState).toBeDefined();
25
30
  expect(TaskState).toBeDefined();
26
31
  expect(WorkflowState).toBeDefined();
32
+ expect(CommentState).toBeDefined();
33
+ expect(AttachmentState).toBeDefined();
34
+ expect(TaskActivityState).toBeDefined();
27
35
  });
28
36
 
29
37
  describe('WorkflowState', () => {
@@ -252,4 +260,51 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
252
260
  expect(attachmentState.data).toEqual([]);
253
261
  });
254
262
  });
263
+
264
+ describe('TaskActivityState', () => {
265
+ it('fetches comments and attachments and unifies them into threads with attachments', async () => {
266
+ const mockComment: Comment = {
267
+ id: 'cmt_1',
268
+ taskId: 'task_1',
269
+ content: 'Comment with attachment',
270
+ authorId: 'u1',
271
+ authorType: 'user',
272
+ createdAt: '2026-01-01',
273
+ updatedAt: '2026-01-01'
274
+ };
275
+
276
+ const mockAttachment: Attachment = {
277
+ id: 'att_1',
278
+ taskId: 'task_1',
279
+ commentId: 'cmt_1',
280
+ filename: 'screenshot.png',
281
+ mimeType: 'image/png',
282
+ sizeBytes: 2048,
283
+ url: 'https://storage.example.com/screenshot.png',
284
+ uploaderId: 'u1',
285
+ uploaderType: 'user',
286
+ createdAt: '2026-01-01',
287
+ updatedAt: '2026-01-01'
288
+ };
289
+
290
+ const mockClient = {
291
+ getComments: vi.fn().mockResolvedValue([mockComment]),
292
+ getAttachments: vi.fn().mockResolvedValue([mockAttachment]),
293
+ addComment: vi.fn().mockResolvedValue(mockComment),
294
+ createAttachment: vi.fn().mockResolvedValue(mockAttachment),
295
+ deleteComment: vi.fn().mockResolvedValue(true),
296
+ deleteAttachment: vi.fn().mockResolvedValue(true)
297
+ } as unknown as CriticalPathClient;
298
+
299
+ const activityState = createTaskActivityState(mockClient, 'task_1');
300
+ await activityState.fetch();
301
+
302
+ expect(activityState.comments).toEqual([mockComment]);
303
+ expect(activityState.attachments).toEqual([mockAttachment]);
304
+ expect(activityState.threads).toHaveLength(1);
305
+ expect(activityState.threads[0].attachments).toEqual([mockAttachment]);
306
+ expect(activityState.standaloneAttachments).toHaveLength(0);
307
+ });
308
+ });
255
309
  });
310
+
package/src/index.ts CHANGED
@@ -9,4 +9,5 @@ export * from './task-state.svelte.js';
9
9
  export * from './workflow-state.svelte.js';
10
10
  export * from './comment-state.svelte.js';
11
11
  export * from './attachment-state.svelte.js';
12
+ export * from './activity-state.svelte.js';
12
13