@critical-path/svelte 0.3.3 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # @critical-path/svelte
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a48a04a: Add threaded conversations, attachment metadata management, and storage adapters (S3 & Firebase Storage):
8
+
9
+ - **`@critical-path/core`**:
10
+ - Added `Attachment` entity, `CreateAttachmentInput`, `UploadFileInput`, and `FileStorageAdapter` contracts.
11
+ - Added `InMemoryFileStore`, duck-typed `S3StorageAdapter` (compatible with AWS SDK v3, v2, MinIO, and Cloudflare R2), and `FirebaseStorageAdapter` (compatible with Google Cloud Storage and Firebase Admin SDK).
12
+ - Extended `Comment` with `authorType` (`user`, `agent`, `system`) and `parentId` for threaded discussions.
13
+ - Implemented full comment and attachment repository methods across `InMemoryStore`, `SQLiteStore`, and `FirebaseStore`.
14
+ - Added engine operations for upload, presigning, and deleting attachments with domain events (`comment.*`, `attachment.*`) and webhooks.
15
+
16
+ - **`@critical-path/server`**:
17
+ - Added RESTful routes for comments (`GET/POST /tasks/:taskId/comments`, `GET/POST /comments`, `GET/PATCH/DELETE /comments/:id`).
18
+ - Added RESTful routes for attachments (`GET/POST /tasks/:taskId/attachments`, `GET/POST /attachments`, `GET/DELETE /attachments/:id`, `POST /attachments/presign`).
19
+
20
+ - **`@critical-path/client`**:
21
+ - Added SDK methods `getComments`, `getComment`, `addComment`, `updateComment`, `deleteComment`.
22
+ - Added SDK methods `getAttachments`, `getAttachment`, `createAttachment`, `deleteAttachment`, and `getPresignedAttachmentUploadUrl`.
23
+
24
+ - **`@critical-path/react`**:
25
+ - Added `useComments(taskId)` with reactive thread tree derivation (`threads` containing nested `replies`) and comment mutations.
26
+ - Added `useAttachments(filter)` with attachment creation and deletion.
27
+
28
+ - **`@critical-path/svelte`**:
29
+ - Added Svelte 5 Rune-based `CommentState` / `createCommentState(client, taskId)` with derived threaded hierarchy.
30
+ - Added Svelte 5 Rune-based `AttachmentState` / `createAttachmentState(client, filter)`.
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [a48a04a]
35
+ - @critical-path/core@0.7.0
36
+ - @critical-path/client@0.3.0
37
+
38
+ ## 0.3.4
39
+
40
+ ### Patch Changes
41
+
42
+ - Updated dependencies [6e9dece]
43
+ - @critical-path/core@0.6.0
44
+ - @critical-path/client@0.2.4
45
+
3
46
  ## 0.3.3
4
47
 
5
48
  ### Patch Changes
@@ -0,0 +1,20 @@
1
+ import type { CriticalPathClient } from '@critical-path/client';
2
+ import type { Attachment } from '@critical-path/core';
3
+ export interface AttachmentFilter {
4
+ taskId?: string;
5
+ projectId?: string;
6
+ commentId?: string;
7
+ }
8
+ export declare class AttachmentState {
9
+ private client;
10
+ filter?: AttachmentFilter | undefined;
11
+ data: Attachment[];
12
+ loading: boolean;
13
+ error: Error | null;
14
+ constructor(client: CriticalPathClient, filter?: AttachmentFilter | undefined);
15
+ fetch(filter?: AttachmentFilter): Promise<void>;
16
+ createAttachment(input: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment>;
17
+ deleteAttachment(id: string): Promise<void>;
18
+ }
19
+ export declare function createAttachmentState(client: CriticalPathClient, initialFilter?: AttachmentFilter): AttachmentState;
20
+ //# sourceMappingURL=attachment-state.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-state.svelte.d.ts","sourceRoot":"","sources":["../src/attachment-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,eAAe;IAKd,OAAO,CAAC,MAAM;IAA6B,MAAM,CAAC,EAAE,gBAAgB;IAJhF,IAAI,eAA4B;IAChC,OAAO,UAA0B;IACjC,KAAK,eAA8B;gBAEf,MAAM,EAAE,kBAAkB,EAAS,MAAM,CAAC,EAAE,gBAAgB,YAAA;IAE1E,KAAK,CAAC,MAAM,CAAC,EAAE,gBAAgB;IAe/B,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC;IAY1E,gBAAgB,CAAC,EAAE,EAAE,MAAM;CAUlC;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,EAAE,aAAa,CAAC,EAAE,gBAAgB,GAAG,eAAe,CAEnH"}
@@ -0,0 +1,20 @@
1
+ import type { CriticalPathClient } from '@critical-path/client';
2
+ import type { Comment } from '@critical-path/core';
3
+ export interface ThreadedComment extends Comment {
4
+ replies: ThreadedComment[];
5
+ }
6
+ export declare class CommentState {
7
+ private client;
8
+ taskId?: string | undefined;
9
+ data: Comment[];
10
+ loading: boolean;
11
+ error: Error | null;
12
+ threads: ThreadedComment[];
13
+ constructor(client: CriticalPathClient, taskId?: string | undefined);
14
+ fetch(taskId?: string): Promise<void>;
15
+ addComment(input: Omit<Comment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>): Promise<Comment>;
16
+ updateComment(id: string, updates: Partial<Comment>): Promise<Comment>;
17
+ deleteComment(id: string): Promise<void>;
18
+ }
19
+ export declare function createCommentState(client: CriticalPathClient, taskId?: string): CommentState;
20
+ //# sourceMappingURL=comment-state.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment-state.svelte.d.ts","sourceRoot":"","sources":["../src/comment-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,eAAgB,SAAQ,OAAO;IAC9C,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,qBAAa,YAAY;IAyBX,OAAO,CAAC,MAAM;IAA6B,MAAM,CAAC,EAAE,MAAM;IAxBtE,IAAI,YAAyB;IAC7B,OAAO,UAA0B;IACjC,KAAK,eAA8B;IAEnC,OAAO,oBAkBJ;gBAEiB,MAAM,EAAE,kBAAkB,EAAS,MAAM,CAAC,EAAE,MAAM,YAAA;IAEhE,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM;IAkBrB,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,GAAG,QAAQ,GAAG,WAAW,GAAG,WAAW,CAAC;IAe5E,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC;IAYnD,aAAa,CAAC,EAAE,EAAE,MAAM;CAU/B;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAE5F"}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,6 @@ export declare function createCriticalPathClient(options: ClientOptions): Critic
3
3
  export * from './project-state.svelte.js';
4
4
  export * from './task-state.svelte.js';
5
5
  export * from './workflow-state.svelte.js';
6
+ export * from './comment-state.svelte.js';
7
+ export * from './attachment-state.svelte.js';
6
8
  //# 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"}
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"}
package/dist/index.js CHANGED
@@ -227,9 +227,165 @@ function s(e) {
227
227
  return new o(e);
228
228
  }
229
229
  //#endregion
230
+ //#region src/comment-state.svelte.ts
231
+ var c = class {
232
+ client;
233
+ taskId;
234
+ #e = t.state(t.proxy([]));
235
+ get data() {
236
+ return t.get(this.#e);
237
+ }
238
+ set data(e) {
239
+ t.set(this.#e, e, !0);
240
+ }
241
+ #t = t.state(!1);
242
+ get loading() {
243
+ return t.get(this.#t);
244
+ }
245
+ set loading(e) {
246
+ t.set(this.#t, e, !0);
247
+ }
248
+ #n = t.state(null);
249
+ get error() {
250
+ return t.get(this.#n);
251
+ }
252
+ set error(e) {
253
+ t.set(this.#n, e, !0);
254
+ }
255
+ #r = t.derived(() => {
256
+ let e = /* @__PURE__ */ new Map(), t = [];
257
+ for (let t of this.data) e.set(t.id, {
258
+ ...t,
259
+ replies: []
260
+ });
261
+ for (let n of this.data) {
262
+ let r = e.get(n.id);
263
+ n.parentId && e.has(n.parentId) ? e.get(n.parentId).replies.push(r) : t.push(r);
264
+ }
265
+ return t;
266
+ });
267
+ get threads() {
268
+ return t.get(this.#r);
269
+ }
270
+ set threads(e) {
271
+ t.set(this.#r, e);
272
+ }
273
+ constructor(e, t) {
274
+ this.client = e, this.taskId = t;
275
+ }
276
+ async fetch(e) {
277
+ let t = e || this.taskId;
278
+ if (!t) {
279
+ this.data = [];
280
+ return;
281
+ }
282
+ this.taskId = t, this.loading = !0, this.error = null;
283
+ try {
284
+ this.data = await this.client.getComments(t);
285
+ } catch (e) {
286
+ this.error = e instanceof Error ? e : Error(String(e));
287
+ } finally {
288
+ this.loading = !1;
289
+ }
290
+ }
291
+ async addComment(e) {
292
+ if (!this.taskId) throw Error("CommentState requires a taskId to add comments.");
293
+ try {
294
+ let t = await this.client.addComment({
295
+ ...e,
296
+ taskId: this.taskId
297
+ });
298
+ return this.data = [...this.data, t], t;
299
+ } catch (e) {
300
+ let t = e instanceof Error ? e : Error(String(e));
301
+ throw this.error = t, t;
302
+ }
303
+ }
304
+ async updateComment(e, t) {
305
+ try {
306
+ let n = await this.client.updateComment(e, t);
307
+ return this.data = this.data.map((t) => t.id === e ? n : t), n;
308
+ } catch (e) {
309
+ let t = e instanceof Error ? e : Error(String(e));
310
+ throw this.error = t, t;
311
+ }
312
+ }
313
+ async deleteComment(e) {
314
+ try {
315
+ await this.client.deleteComment(e), this.data = this.data.filter((t) => t.id !== e);
316
+ } catch (e) {
317
+ let t = e instanceof Error ? e : Error(String(e));
318
+ throw this.error = t, t;
319
+ }
320
+ }
321
+ };
322
+ function l(e, t) {
323
+ return new c(e, t);
324
+ }
325
+ //#endregion
326
+ //#region src/attachment-state.svelte.ts
327
+ var u = class {
328
+ client;
329
+ filter;
330
+ #e = t.state(t.proxy([]));
331
+ get data() {
332
+ return t.get(this.#e);
333
+ }
334
+ set data(e) {
335
+ t.set(this.#e, e, !0);
336
+ }
337
+ #t = t.state(!1);
338
+ get loading() {
339
+ return t.get(this.#t);
340
+ }
341
+ set loading(e) {
342
+ t.set(this.#t, e, !0);
343
+ }
344
+ #n = t.state(null);
345
+ get error() {
346
+ return t.get(this.#n);
347
+ }
348
+ set error(e) {
349
+ t.set(this.#n, e, !0);
350
+ }
351
+ constructor(e, t) {
352
+ this.client = e, this.filter = t;
353
+ }
354
+ async fetch(e) {
355
+ e && (this.filter = e), this.loading = !0, this.error = null;
356
+ try {
357
+ this.data = await this.client.getAttachments(this.filter);
358
+ } catch (e) {
359
+ this.error = e instanceof Error ? e : Error(String(e));
360
+ } finally {
361
+ this.loading = !1;
362
+ }
363
+ }
364
+ async createAttachment(e) {
365
+ try {
366
+ let t = await this.client.createAttachment(e);
367
+ return this.data = [t, ...this.data], t;
368
+ } catch (e) {
369
+ let t = e instanceof Error ? e : Error(String(e));
370
+ throw this.error = t, t;
371
+ }
372
+ }
373
+ async deleteAttachment(e) {
374
+ try {
375
+ await this.client.deleteAttachment(e), this.data = this.data.filter((t) => t.id !== e);
376
+ } catch (e) {
377
+ let t = e instanceof Error ? e : Error(String(e));
378
+ throw this.error = t, t;
379
+ }
380
+ }
381
+ };
382
+ function d(e, t) {
383
+ return new u(e, t);
384
+ }
385
+ //#endregion
230
386
  //#region src/index.ts
231
- function c(t) {
387
+ function f(t) {
232
388
  return new e(t);
233
389
  }
234
390
  //#endregion
235
- export { n as ProjectState, i as TaskState, o as WorkflowState, c as createCriticalPathClient, r as createProjectState, a as createTaskState, s as createWorkflowState };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@critical-path/svelte",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
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.2.3",
22
- "@critical-path/core": "0.5.2"
21
+ "@critical-path/client": "0.3.0",
22
+ "@critical-path/core": "0.7.0"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "svelte": "^5.0.0"
@@ -0,0 +1,59 @@
1
+ /// <reference types="svelte" />
2
+ import type { CriticalPathClient } from '@critical-path/client';
3
+ import type { Attachment } from '@critical-path/core';
4
+
5
+ export interface AttachmentFilter {
6
+ taskId?: string;
7
+ projectId?: string;
8
+ commentId?: string;
9
+ }
10
+
11
+ export class AttachmentState {
12
+ data = $state<Attachment[]>([]);
13
+ loading = $state<boolean>(false);
14
+ error = $state<Error | null>(null);
15
+
16
+ constructor(private client: CriticalPathClient, public filter?: AttachmentFilter) {}
17
+
18
+ async fetch(filter?: AttachmentFilter) {
19
+ if (filter) {
20
+ this.filter = filter;
21
+ }
22
+ this.loading = true;
23
+ this.error = null;
24
+ try {
25
+ this.data = await this.client.getAttachments(this.filter);
26
+ } catch (err) {
27
+ this.error = err instanceof Error ? err : new Error(String(err));
28
+ } finally {
29
+ this.loading = false;
30
+ }
31
+ }
32
+
33
+ async createAttachment(input: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>) {
34
+ try {
35
+ const created = await this.client.createAttachment(input);
36
+ this.data = [created, ...this.data];
37
+ return created;
38
+ } catch (err) {
39
+ const errorObj = err instanceof Error ? err : new Error(String(err));
40
+ this.error = errorObj;
41
+ throw errorObj;
42
+ }
43
+ }
44
+
45
+ async deleteAttachment(id: string) {
46
+ try {
47
+ await this.client.deleteAttachment(id);
48
+ this.data = this.data.filter((a) => a.id !== id);
49
+ } catch (err) {
50
+ const errorObj = err instanceof Error ? err : new Error(String(err));
51
+ this.error = errorObj;
52
+ throw errorObj;
53
+ }
54
+ }
55
+ }
56
+
57
+ export function createAttachmentState(client: CriticalPathClient, initialFilter?: AttachmentFilter): AttachmentState {
58
+ return new AttachmentState(client, initialFilter);
59
+ }
@@ -0,0 +1,95 @@
1
+ /// <reference types="svelte" />
2
+ import type { CriticalPathClient } from '@critical-path/client';
3
+ import type { Comment } from '@critical-path/core';
4
+
5
+ export interface ThreadedComment extends Comment {
6
+ replies: ThreadedComment[];
7
+ }
8
+
9
+ export class CommentState {
10
+ data = $state<Comment[]>([]);
11
+ loading = $state<boolean>(false);
12
+ error = $state<Error | null>(null);
13
+
14
+ threads = $derived.by(() => {
15
+ const map = new Map<string, ThreadedComment>();
16
+ const roots: ThreadedComment[] = [];
17
+
18
+ for (const c of this.data) {
19
+ map.set(c.id, { ...c, replies: [] });
20
+ }
21
+
22
+ for (const c of this.data) {
23
+ const threaded = map.get(c.id)!;
24
+ if (c.parentId && map.has(c.parentId)) {
25
+ map.get(c.parentId)!.replies.push(threaded);
26
+ } else {
27
+ roots.push(threaded);
28
+ }
29
+ }
30
+
31
+ return roots;
32
+ });
33
+
34
+ constructor(private client: CriticalPathClient, public taskId?: string) {}
35
+
36
+ async fetch(taskId?: string) {
37
+ const targetTaskId = taskId || this.taskId;
38
+ if (!targetTaskId) {
39
+ this.data = [];
40
+ return;
41
+ }
42
+ this.taskId = targetTaskId;
43
+ this.loading = true;
44
+ this.error = null;
45
+ try {
46
+ this.data = await this.client.getComments(targetTaskId);
47
+ } catch (err) {
48
+ this.error = err instanceof Error ? err : new Error(String(err));
49
+ } finally {
50
+ this.loading = false;
51
+ }
52
+ }
53
+
54
+ async addComment(input: Omit<Comment, 'id' | 'taskId' | 'createdAt' | 'updatedAt'>) {
55
+ if (!this.taskId) {
56
+ throw new Error('CommentState requires a taskId to add comments.');
57
+ }
58
+ try {
59
+ const created = await this.client.addComment({ ...input, taskId: this.taskId });
60
+ this.data = [...this.data, created];
61
+ return created;
62
+ } catch (err) {
63
+ const errorObj = err instanceof Error ? err : new Error(String(err));
64
+ this.error = errorObj;
65
+ throw errorObj;
66
+ }
67
+ }
68
+
69
+ async updateComment(id: string, updates: Partial<Comment>) {
70
+ try {
71
+ const updated = await this.client.updateComment(id, updates);
72
+ this.data = this.data.map((c) => (c.id === id ? updated : c));
73
+ return updated;
74
+ } catch (err) {
75
+ const errorObj = err instanceof Error ? err : new Error(String(err));
76
+ this.error = errorObj;
77
+ throw errorObj;
78
+ }
79
+ }
80
+
81
+ async deleteComment(id: string) {
82
+ try {
83
+ await this.client.deleteComment(id);
84
+ this.data = this.data.filter((c) => c.id !== id);
85
+ } catch (err) {
86
+ const errorObj = err instanceof Error ? err : new Error(String(err));
87
+ this.error = errorObj;
88
+ throw errorObj;
89
+ }
90
+ }
91
+ }
92
+
93
+ export function createCommentState(client: CriticalPathClient, taskId?: string): CommentState {
94
+ return new CommentState(client, taskId);
95
+ }
package/src/index.test.ts CHANGED
@@ -6,7 +6,11 @@ import {
6
6
  TaskState,
7
7
  createTaskState,
8
8
  WorkflowState,
9
- createWorkflowState
9
+ createWorkflowState,
10
+ CommentState,
11
+ createCommentState,
12
+ AttachmentState,
13
+ createAttachmentState
10
14
  } from './index.js';
11
15
  import type { CriticalPathClient } from '@critical-path/client';
12
16
  import type { Project, Task, Workflow } from '@critical-path/core';
@@ -185,4 +189,67 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
185
189
  expect(taskState.data).toEqual([mockTask]);
186
190
  });
187
191
  });
192
+
193
+ describe('CommentState', () => {
194
+ it('fetches, creates, updates, and deletes comments', async () => {
195
+ const mockComment = {
196
+ id: 'cmt_1',
197
+ taskId: 'task_1',
198
+ content: 'Root comment',
199
+ authorId: 'u1',
200
+ authorType: 'user' as const,
201
+ createdAt: '2026-01-01',
202
+ updatedAt: '2026-01-01'
203
+ };
204
+ const updatedComment = { ...mockComment, content: 'Updated comment' };
205
+
206
+ const mockClient = {
207
+ getComments: vi.fn().mockResolvedValue([mockComment]),
208
+ addComment: vi.fn().mockResolvedValue(mockComment),
209
+ updateComment: vi.fn().mockResolvedValue(updatedComment),
210
+ deleteComment: vi.fn().mockResolvedValue(true)
211
+ } as unknown as CriticalPathClient;
212
+
213
+ const commentState = new CommentState(mockClient, 'task_1');
214
+ await commentState.fetch();
215
+
216
+ expect(commentState.data).toEqual([mockComment]);
217
+
218
+ await commentState.updateComment('cmt_1', { content: 'Updated comment' });
219
+ expect(commentState.data[0].content).toBe('Updated comment');
220
+
221
+ await commentState.deleteComment('cmt_1');
222
+ expect(commentState.data).toEqual([]);
223
+ });
224
+ });
225
+
226
+ describe('AttachmentState', () => {
227
+ it('fetches, creates, and deletes attachments', async () => {
228
+ const mockAttachment = {
229
+ id: 'att_1',
230
+ filename: 'spec.pdf',
231
+ mimeType: 'application/pdf',
232
+ sizeBytes: 1024,
233
+ url: 'https://storage.example.com/spec.pdf',
234
+ uploaderId: 'u1',
235
+ uploaderType: 'user' as const,
236
+ createdAt: '2026-01-01',
237
+ updatedAt: '2026-01-01'
238
+ };
239
+
240
+ const mockClient = {
241
+ getAttachments: vi.fn().mockResolvedValue([mockAttachment]),
242
+ createAttachment: vi.fn().mockResolvedValue(mockAttachment),
243
+ deleteAttachment: vi.fn().mockResolvedValue(true)
244
+ } as unknown as CriticalPathClient;
245
+
246
+ const attachmentState = new AttachmentState(mockClient, { taskId: 'task_1' });
247
+ await attachmentState.fetch();
248
+
249
+ expect(attachmentState.data).toEqual([mockAttachment]);
250
+
251
+ await attachmentState.deleteAttachment('att_1');
252
+ expect(attachmentState.data).toEqual([]);
253
+ });
254
+ });
188
255
  });
package/src/index.ts CHANGED
@@ -7,3 +7,6 @@ export function createCriticalPathClient(options: ClientOptions): CriticalPathCl
7
7
  export * from './project-state.svelte.js';
8
8
  export * from './task-state.svelte.js';
9
9
  export * from './workflow-state.svelte.js';
10
+ export * from './comment-state.svelte.js';
11
+ export * from './attachment-state.svelte.js';
12
+