truecanvas 0.1.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 (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +265 -0
  3. package/dist/catalog-worker.js +7 -0
  4. package/dist/chunk-753N4QRO.js +317 -0
  5. package/dist/chunk-J6CJBZO6.js +23 -0
  6. package/dist/chunk-WB35FXGP.js +3366 -0
  7. package/dist/cli.js +2931 -0
  8. package/dist/core/index.d.ts +820 -0
  9. package/dist/core/index.js +71 -0
  10. package/dist/editor/assets/controls-BQYSgX53.css +1 -0
  11. package/dist/editor/assets/controls-BYFjuGCq.js +9 -0
  12. package/dist/editor/assets/hub-C4_700Hv.js +2 -0
  13. package/dist/editor/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  14. package/dist/editor/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  15. package/dist/editor/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  16. package/dist/editor/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  17. package/dist/editor/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  18. package/dist/editor/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  19. package/dist/editor/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  20. package/dist/editor/assets/main-CfAJbDf1.js +25 -0
  21. package/dist/editor/assets/react-DB-4Zxce.js +1 -0
  22. package/dist/editor/favicon.svg +3 -0
  23. package/dist/editor/hub.html +16 -0
  24. package/dist/editor/index.html +16 -0
  25. package/dist/next/index.d.ts +11 -0
  26. package/dist/next/index.js +83 -0
  27. package/dist/next/loader.cjs +16210 -0
  28. package/dist/runtime/host.d.ts +23 -0
  29. package/dist/runtime/host.js +412 -0
  30. package/dist/runtime/index.d.ts +42 -0
  31. package/dist/runtime/index.js +11 -0
  32. package/package.json +108 -0
@@ -0,0 +1,820 @@
1
+ import MagicString from 'magic-string';
2
+
3
+ type PropValue = {
4
+ kind: "string";
5
+ value: string;
6
+ } | {
7
+ kind: "number";
8
+ value: number;
9
+ } | {
10
+ kind: "boolean";
11
+ value: boolean;
12
+ } | {
13
+ kind: "array";
14
+ value: (string | number | boolean)[];
15
+ } | {
16
+ kind: "expression";
17
+ code: string;
18
+ };
19
+ type NodeKind = "component" | "element" | "text" | "expression" | "fragment";
20
+ interface AttrRange {
21
+ name: string;
22
+ start: number;
23
+ end: number;
24
+ /**
25
+ * For `className={cn("a b", className)}` (cn, clsx, cx, twMerge, classNames):
26
+ * the range of the string literal, so class edits replace it and keep the merge.
27
+ */
28
+ literal?: {
29
+ start: number;
30
+ end: number;
31
+ };
32
+ }
33
+ interface CanvasNode {
34
+ id: string;
35
+ kind: NodeKind;
36
+ /** Tag or component name; for text the trimmed text, for expressions the code. */
37
+ name: string;
38
+ props: Record<string, PropValue>;
39
+ /** Spread attributes like {...rest}. When present, some props are not statically known. */
40
+ spreads: number;
41
+ children: CanvasNode[];
42
+ text?: string;
43
+ start: number;
44
+ end: number;
45
+ line: number;
46
+ col: number;
47
+ /** Index path from the canvas root: [frameIndex, childIndex, ...]. */
48
+ path: number[];
49
+ attrs: AttrRange[];
50
+ nameEnd: number;
51
+ openEnd: number;
52
+ selfClosing: boolean;
53
+ closeStart: number;
54
+ }
55
+ interface CanvasFrame extends CanvasNode {
56
+ frameName: string;
57
+ x: number;
58
+ y: number;
59
+ width: number;
60
+ height: number | null;
61
+ theme: "light" | "dark" | null;
62
+ device: string | null;
63
+ /** linked frame: the App Router page it shows (project-relative). Its layers are that page's JSX. */
64
+ page: string | null;
65
+ /** exploration copied from this page file ("Apply to page" writes it back) */
66
+ from: string | null;
67
+ /** main component frame: "components/button.tsx#Button". Its layers are that component's JSX. */
68
+ component: string | null;
69
+ /** set on linked frames once resolved */
70
+ link: FrameLink | null;
71
+ }
72
+ interface FrameLink {
73
+ /** "page": a linked App Router page; "component": a main component */
74
+ kind?: "page" | "component";
75
+ page: string;
76
+ /** URL path of the page, e.g. "/" */
77
+ route: string;
78
+ /** page and layout files whose JSX appears as layers */
79
+ files: string[];
80
+ /** false when the page has data or logic: shown as is, edit a copy instead */
81
+ editable: boolean;
82
+ reason?: string;
83
+ }
84
+ interface ImportInfo {
85
+ source: string;
86
+ typeOnly: boolean;
87
+ names: string[];
88
+ defaultName: string | null;
89
+ start: number;
90
+ end: number;
91
+ /** End of the last named specifier, for appending `, Name`. */
92
+ lastSpecifierEnd: number | null;
93
+ }
94
+ interface CanvasDoc {
95
+ name: string;
96
+ file: string;
97
+ frames: CanvasFrame[];
98
+ imports: ImportInfo[];
99
+ canvasNode: CanvasNode | null;
100
+ /** End of the "use client" directive prologue; imports must go after it. */
101
+ prologueEnd: number;
102
+ /** Top-level names declared in the canvas file itself. */
103
+ declared: string[];
104
+ error?: string;
105
+ }
106
+ interface PropSpec {
107
+ name: string;
108
+ type: "string" | "number" | "boolean" | "enum" | "node" | "function" | "color" | "colors" | "other";
109
+ options?: string[];
110
+ /** shown under "Advanced" in the inspector */
111
+ advanced?: boolean;
112
+ /** numeric range, from docs like "(0 to 1)" */
113
+ min?: number;
114
+ max?: number;
115
+ optional: boolean;
116
+ default?: string | number | boolean | (string | number | boolean)[];
117
+ description?: string;
118
+ typeText: string;
119
+ }
120
+ interface Preset {
121
+ name: string;
122
+ props: Record<string, string | number | boolean | (string | number | boolean)[]>;
123
+ }
124
+ interface ComponentSpec {
125
+ name: string;
126
+ /** project-relative file, or the package name for library components */
127
+ file: string;
128
+ /** set for components from npm packages: the import specifier */
129
+ library?: string;
130
+ presets?: Preset[];
131
+ description?: string;
132
+ props: PropSpec[];
133
+ acceptsClassName: boolean;
134
+ acceptsChildren: boolean;
135
+ }
136
+
137
+ declare function parseCanvas(name: string, file: string, source: string): CanvasDoc;
138
+ /** Every node in the doc, depth-first, including frames. */
139
+ declare function allNodes(doc: CanvasDoc): Generator<{
140
+ node: CanvasNode;
141
+ parent: CanvasNode | null;
142
+ frame: CanvasFrame;
143
+ }>;
144
+ declare function findNode(doc: CanvasDoc, id: string): {
145
+ node: CanvasNode;
146
+ parent: CanvasNode | null;
147
+ frame: CanvasFrame;
148
+ } | null;
149
+ declare function findFrame(doc: CanvasDoc, ref: string): CanvasFrame | null;
150
+
151
+ type Literal = string | number | boolean | null | (string | number | boolean)[];
152
+ declare class EditError extends Error {
153
+ }
154
+ /** Formats a literal as a JSX attribute: `name="v"`, `name={1}`, `name`. */
155
+ declare function attrCode(name: string, value: Literal): string;
156
+ /** Formats text as a JSX child; falls back to a string expression when it needs escaping. */
157
+ declare function textCode(text: string): string;
158
+ /**
159
+ * One editing session over a canvas source. Each method describes a change
160
+ * against the *original* positions; `result()` gives the new source.
161
+ */
162
+ declare class CanvasEditor {
163
+ readonly source: string;
164
+ readonly doc: CanvasDoc;
165
+ readonly s: MagicString;
166
+ /** Path of the node that should be selected after the edit (best effort, before formatting). */
167
+ selectPaths: number[][];
168
+ private usedFrameNames;
169
+ private placed;
170
+ constructor(source: string, doc: CanvasDoc);
171
+ node(id: string): {
172
+ node: CanvasNode;
173
+ parent: CanvasNode | null;
174
+ frame: CanvasFrame;
175
+ };
176
+ frame(ref: string): CanvasFrame;
177
+ /** A frame id/name or a node id: whatever an agent passes as "parent". */
178
+ container(ref: string): CanvasNode;
179
+ setProp(node: CanvasNode, name: string, value: Literal | undefined, opts?: {
180
+ raw?: string;
181
+ }): void;
182
+ setText(node: CanvasNode, text: string): void;
183
+ private unitCache;
184
+ /** The file's indent unit: tabs, or the smallest non-zero space indent. */
185
+ private unit;
186
+ lineIndent(pos: number): string;
187
+ /** Where a node's real content starts/ends (JSX text includes surrounding whitespace). */
188
+ contentStart(n: CanvasNode): number;
189
+ contentEnd(n: CanvasNode): number;
190
+ childIndent(parent: CanvasNode): string;
191
+ /** Re-indents lines 2..n of a snippet so it sits at `indent`. */
192
+ shift(code: string, indent: string): string;
193
+ private openToClose;
194
+ private insertAt;
195
+ insertChild(parent: CanvasNode, index: number | undefined, jsx: string): void;
196
+ /** Removes a node together with its line when it sits alone on it. */
197
+ remove(node: CanvasNode): void;
198
+ duplicate(node: CanvasNode): void;
199
+ move(node: CanvasNode, target: CanvasNode, index: number | undefined): void;
200
+ wrap(nodes: CanvasNode[], open: string, close: string): void;
201
+ /** Like wrap, but keeps content that sits on one line on one line: `<p><X>Hi</X></p>`. */
202
+ wrapInline(nodes: CanvasNode[], open: string, close: string): void;
203
+ /** Replaces a wrapper element with its children (re-indented to its place). */
204
+ unwrap(node: CanvasNode): void;
205
+ replace(node: CanvasNode, jsx: string): void;
206
+ private added;
207
+ ensureImport(name: string, from: string): void;
208
+ /** Adds a whole import statement unless every name it binds is already imported. */
209
+ addImportStatement(statement: string): void;
210
+ private importsPrepended;
211
+ /** First imports of a file: in order, with a blank line before the code. */
212
+ private prependImport;
213
+ insertFrame(code: string): void;
214
+ result(): string;
215
+ }
216
+
217
+ /**
218
+ * Stamps every JSX element in a canvas file with `data-tc="line:col"` so the
219
+ * in-frame runtime can map rendered DOM back to the exact JSX in the source.
220
+ * Inserted on the same line, so stack traces and line numbers stay intact.
221
+ * App files (pages, layouts) pass their project-relative path: their ids are
222
+ * `file#line:col`, so they never collide with the canvas file's own ids.
223
+ */
224
+ declare function injectIds(source: string, file?: string): string;
225
+
226
+ interface DarkMode {
227
+ /** "class": toggles a class on <html>. "attribute": sets an attribute on <html>. */
228
+ strategy: "class" | "attribute";
229
+ /** Class name (class strategy) or attribute value (attribute strategy). */
230
+ value: string;
231
+ /** Attribute name for the attribute strategy, e.g. "data-theme". */
232
+ attribute?: string;
233
+ /** Value written for light mode with the attribute strategy. */
234
+ lightValue?: string;
235
+ }
236
+ interface TruecanvasConfig {
237
+ root: string;
238
+ /** Where your Next.js dev server runs. */
239
+ appUrl: string;
240
+ /** Port of the Truecanvas editor + MCP server. */
241
+ port: number;
242
+ /** Folder holding *.canvas.tsx files. */
243
+ canvasDir: string;
244
+ /** Next.js app directory ("app" or "src/app"). */
245
+ appDir: string;
246
+ /** Globs of component files shown in the Components panel. */
247
+ components: string[];
248
+ darkMode: DarkMode;
249
+ /** npm packages whose components appear in the Components panel (auto-detects known ones). */
250
+ libraries: string[];
251
+ /** Editor used for "Open in editor" (code, zed, cursor, webstorm…). Defaults to auto-detect. */
252
+ editor?: string;
253
+ }
254
+ declare function loadConfig(root: string, overrides?: Partial<TruecanvasConfig>): TruecanvasConfig;
255
+
256
+ /**
257
+ * Reads component prop types with the TypeScript checker. The program is
258
+ * created on demand and dropped right after, so only the small JSON result
259
+ * stays in memory; it is rebuilt when component files change.
260
+ */
261
+ declare class Catalog {
262
+ private root;
263
+ private globs;
264
+ private libraries;
265
+ private cache;
266
+ private loading;
267
+ private generation;
268
+ constructor(root: string, globs: string[], libraries?: string[]);
269
+ invalidate(): void;
270
+ /** Cached components; call load() first. */
271
+ list(): ComponentSpec[];
272
+ get(name: string): ComponentSpec | undefined;
273
+ /**
274
+ * Analyzes components in a short-lived worker thread: the TypeScript program
275
+ * is large, and a worker gives all of that memory back when it exits.
276
+ */
277
+ load(): Promise<ComponentSpec[]>;
278
+ }
279
+
280
+ interface CommentAuthor {
281
+ name: string;
282
+ kind: "user" | "agent";
283
+ }
284
+ interface CommentMessage {
285
+ id: string;
286
+ author: CommentAuthor;
287
+ text: string;
288
+ at: number;
289
+ }
290
+ interface CommentThread {
291
+ id: string;
292
+ /** frame name + position in frame coordinates */
293
+ frame: string;
294
+ x: number;
295
+ y: number;
296
+ /** the layer under the pin when it was placed (tree path + name), best effort */
297
+ node: {
298
+ path: string;
299
+ name: string;
300
+ } | null;
301
+ resolved: boolean;
302
+ resolvedBy?: CommentAuthor;
303
+ messages: CommentMessage[];
304
+ createdAt: number;
305
+ }
306
+ /**
307
+ * Comments live next to the canvas in `<page>.comments.json`, so they are
308
+ * versioned, reviewed and merged with git like the design itself.
309
+ */
310
+ declare class Comments {
311
+ private config;
312
+ constructor(config: TruecanvasConfig);
313
+ file(canvas: string): string;
314
+ list(canvas: string): CommentThread[];
315
+ private save;
316
+ private update;
317
+ add(canvas: string, input: {
318
+ frame: string;
319
+ x: number;
320
+ y: number;
321
+ node?: CommentThread["node"];
322
+ text: string;
323
+ author: CommentAuthor;
324
+ }): CommentThread;
325
+ reply(canvas: string, id: string, text: string, author: CommentAuthor): CommentThread;
326
+ resolve(canvas: string, id: string, resolved: boolean, author: CommentAuthor): CommentThread;
327
+ remove(canvas: string, id: string): void;
328
+ /** Frames keep their comments when renamed. */
329
+ renameFrame(canvas: string, from: string, to: string): void;
330
+ renameCanvas(from: string, to: string): void;
331
+ }
332
+
333
+ interface GitFile {
334
+ path: string;
335
+ status: "modified" | "added" | "deleted" | "renamed" | "untracked";
336
+ staged: boolean;
337
+ }
338
+ interface GitStatus {
339
+ repo: boolean;
340
+ root: string | null;
341
+ branch: string | null;
342
+ /** no commits yet */
343
+ unborn: boolean;
344
+ upstream: string | null;
345
+ ahead: number;
346
+ behind: number;
347
+ files: GitFile[];
348
+ user: string | null;
349
+ /** main/master: the branch pull requests go into */
350
+ defaultBranch: string | null;
351
+ /** remote "origin" URL, if any */
352
+ remote: string | null;
353
+ }
354
+ interface PullRequest {
355
+ number: number;
356
+ url: string;
357
+ title: string;
358
+ state: "OPEN" | "CLOSED" | "MERGED";
359
+ draft: boolean;
360
+ review: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null;
361
+ checks: {
362
+ passed: number;
363
+ failed: number;
364
+ pending: number;
365
+ };
366
+ }
367
+ interface GitCommit {
368
+ hash: string;
369
+ short: string;
370
+ subject: string;
371
+ author: string;
372
+ at: number;
373
+ }
374
+ /** Thin wrapper over the git CLI for the editor's Git panel. */
375
+ declare class Git {
376
+ private cwd;
377
+ constructor(cwd: string);
378
+ status(): Promise<GitStatus>;
379
+ /** The branch PRs target: origin's HEAD, else a local main/master. */
380
+ defaultBranch(): Promise<string | null>;
381
+ /** When the last commit was made (ms), or 0 with no commits. */
382
+ lastCommitAt(): Promise<number>;
383
+ /** When a commit was made (ms). */
384
+ commitAt(ref: string): Promise<number>;
385
+ /** Commit where this branch left `base`. */
386
+ mergeBase(base: string): Promise<string | null>;
387
+ /** Files changed between a commit and HEAD, from the project root. */
388
+ changedSince(ref: string): Promise<string[]>;
389
+ /** Unified diff of these files against HEAD, including new files. */
390
+ diff(files: string[]): Promise<string>;
391
+ /**
392
+ * Adds files to a side branch without touching the working tree or index
393
+ * (used for PR screenshots), and pushes it. Returns the branch tip.
394
+ */
395
+ commitToSideBranch(branch: string, files: {
396
+ path: string;
397
+ data: Buffer;
398
+ }[], message: string): Promise<void>;
399
+ /** owner/name of the GitHub repo, if origin is on GitHub and gh is set up. */
400
+ githubRepo(): Promise<string | null>;
401
+ /** The open (or latest) pull request of the current branch. */
402
+ pullRequest(): Promise<PullRequest | null>;
403
+ createPullRequest(opts: {
404
+ title: string;
405
+ body: string;
406
+ base: string;
407
+ draft?: boolean;
408
+ }): Promise<string>;
409
+ /** Creates a GitHub repo for this project and pushes it (gh repo create). */
410
+ publish(name: string, isPrivate: boolean): Promise<string>;
411
+ /** git reports paths from the repo root; we show them from the project root. */
412
+ private rel;
413
+ branches(): Promise<{
414
+ current: string | null;
415
+ local: string[];
416
+ remote: string[];
417
+ }>;
418
+ /** Commits that touched these paths (newest first). */
419
+ log(paths: string[], limit?: number): Promise<GitCommit[]>;
420
+ /** A file's content at a ref, or null if it doesn't exist there. */
421
+ show(ref: string, file: string): Promise<string | null>;
422
+ commit(message: string, files: string[]): Promise<string>;
423
+ push(): Promise<void>;
424
+ pull(): Promise<void>;
425
+ fetch(): Promise<void>;
426
+ switch(branch: string, create?: boolean): Promise<void>;
427
+ }
428
+
429
+ type Actor = {
430
+ kind: "user";
431
+ } | {
432
+ kind: "agent";
433
+ name: string;
434
+ } | {
435
+ kind: "file";
436
+ };
437
+ type Command = {
438
+ op: "set_props";
439
+ canvas: string;
440
+ id: string;
441
+ props: Record<string, Literal>;
442
+ } | {
443
+ op: "set_text";
444
+ canvas: string;
445
+ id: string;
446
+ text: string;
447
+ } | {
448
+ op: "set_class";
449
+ canvas: string;
450
+ id: string;
451
+ className: string;
452
+ } | {
453
+ op: "insert_jsx";
454
+ canvas: string;
455
+ parent: string;
456
+ index?: number;
457
+ jsx: string;
458
+ } | {
459
+ op: "insert_component";
460
+ canvas: string;
461
+ parent: string;
462
+ index?: number;
463
+ component: string;
464
+ props?: Record<string, Literal>;
465
+ text?: string;
466
+ } | {
467
+ op: "replace";
468
+ canvas: string;
469
+ id: string;
470
+ jsx: string;
471
+ } | {
472
+ op: "duplicate";
473
+ canvas: string;
474
+ ids: string[];
475
+ } | {
476
+ op: "delete";
477
+ canvas: string;
478
+ ids: string[];
479
+ } | {
480
+ op: "move";
481
+ canvas: string;
482
+ id: string;
483
+ parent: string;
484
+ index?: number;
485
+ } | {
486
+ op: "reorder";
487
+ canvas: string;
488
+ id: string;
489
+ delta: number;
490
+ } | {
491
+ op: "wrap";
492
+ canvas: string;
493
+ ids: string[];
494
+ className?: string;
495
+ } | {
496
+ op: "create_frame";
497
+ canvas: string;
498
+ name?: string;
499
+ x?: number;
500
+ y?: number;
501
+ width?: number;
502
+ height?: number | null;
503
+ theme?: "light" | "dark" | null;
504
+ device?: string | null;
505
+ jsx?: string;
506
+ } | {
507
+ op: "update_frame";
508
+ canvas: string;
509
+ frame: string;
510
+ name?: string;
511
+ x?: number;
512
+ y?: number;
513
+ width?: number;
514
+ height?: number | null;
515
+ theme?: "light" | "dark" | null;
516
+ device?: string | null;
517
+ } | {
518
+ op: "add_background";
519
+ canvas: string;
520
+ parent: string;
521
+ component: string;
522
+ preset?: string;
523
+ } | {
524
+ op: "apply_preset";
525
+ canvas: string;
526
+ id: string;
527
+ preset: string;
528
+ } | {
529
+ op: "import_route";
530
+ canvas: string;
531
+ route: string;
532
+ name?: string;
533
+ width?: number;
534
+ x?: number;
535
+ y?: number;
536
+ copy?: boolean;
537
+ } | {
538
+ op: "explore_copy";
539
+ canvas: string;
540
+ frame: string;
541
+ name?: string;
542
+ } | {
543
+ op: "apply_to_page";
544
+ canvas: string;
545
+ frame: string;
546
+ } | {
547
+ op: "add_animation";
548
+ canvas: string;
549
+ id: string;
550
+ kind: "reveal" | "text";
551
+ effect?: string;
552
+ delay?: number;
553
+ duration?: number;
554
+ stagger?: number;
555
+ once?: boolean;
556
+ } | {
557
+ op: "remove_animation";
558
+ canvas: string;
559
+ id: string;
560
+ kind: "reveal" | "text";
561
+ } | {
562
+ op: "create_component";
563
+ canvas: string;
564
+ name: string;
565
+ from?: string;
566
+ parent?: string;
567
+ index?: number;
568
+ } | {
569
+ op: "add_component_frame";
570
+ canvas: string;
571
+ component: string;
572
+ x?: number;
573
+ y?: number;
574
+ width?: number;
575
+ } | {
576
+ op: "create_variants";
577
+ canvas: string;
578
+ component: string;
579
+ prop: string;
580
+ props?: Record<string, Literal>;
581
+ x?: number;
582
+ y?: number;
583
+ };
584
+ interface HistoryEntry {
585
+ id: number;
586
+ canvas: string;
587
+ label: string;
588
+ actor: Actor;
589
+ at: number;
590
+ before: string;
591
+ after: string;
592
+ ids: string[];
593
+ /** project-relative file this entry edited, when it isn't the canvas (a linked page or layout) */
594
+ file?: string;
595
+ /** frames the change touched (for "what changed" in the Git panel) */
596
+ frames?: string[];
597
+ }
598
+ type WorkspaceEvent = {
599
+ type: "doc";
600
+ canvas: string;
601
+ doc: CanvasDoc;
602
+ } | {
603
+ type: "canvases";
604
+ canvases: string[];
605
+ } | {
606
+ type: "catalog";
607
+ } | {
608
+ type: "change";
609
+ entry: PublicEntry;
610
+ ids: string[];
611
+ } | {
612
+ type: "history";
613
+ canvas: string;
614
+ undo: number;
615
+ redo: number;
616
+ } | {
617
+ type: "focus";
618
+ canvas: string;
619
+ ids: string[];
620
+ actor: string;
621
+ } | {
622
+ type: "agents";
623
+ agents: AgentInfo[];
624
+ } | {
625
+ type: "presence";
626
+ presence: Presence;
627
+ } | {
628
+ type: "canvas-renamed";
629
+ from: string;
630
+ to: string;
631
+ } | {
632
+ type: "comments";
633
+ canvas: string;
634
+ } | {
635
+ type: "motion";
636
+ canvas: string;
637
+ frame: string;
638
+ action: "play" | "replay" | "stop";
639
+ } | {
640
+ type: "git";
641
+ };
642
+ /** What an agent is doing right now, for live cursors in the editor. */
643
+ interface Presence {
644
+ session: string;
645
+ name: string;
646
+ canvas: string;
647
+ action: "reading" | "editing" | "looking";
648
+ label: string;
649
+ /** node or frame ids the agent is on */
650
+ ids: string[];
651
+ /** frame name when the target is a whole frame */
652
+ frame: string | null;
653
+ at: number;
654
+ }
655
+ type PublicEntry = Omit<HistoryEntry, "before" | "after">;
656
+ interface AgentInfo {
657
+ session: string;
658
+ name: string;
659
+ since: number;
660
+ lastSeen: number;
661
+ }
662
+ declare class Workspace {
663
+ readonly config: TruecanvasConfig;
664
+ readonly catalog: Catalog;
665
+ private sources;
666
+ private undoStacks;
667
+ private redoStacks;
668
+ readonly feed: HistoryEntry[];
669
+ private seq;
670
+ private queue;
671
+ private listeners;
672
+ selection: {
673
+ canvas: string | null;
674
+ ids: string[];
675
+ };
676
+ agents: Map<string, AgentInfo>;
677
+ readonly comments: Comments;
678
+ readonly git: Git;
679
+ constructor(config: TruecanvasConfig);
680
+ on(fn: (e: WorkspaceEvent) => void): () => boolean;
681
+ emit(e: WorkspaceEvent): void;
682
+ canvases(): string[];
683
+ file(canvas: string): string;
684
+ relFile(canvas: string): string;
685
+ read(canvas: string): string;
686
+ doc(canvas: string): CanvasDoc;
687
+ /** A canvas doc with linked frames resolved to their page's layers. */
688
+ docFrom(canvas: string, source: string): CanvasDoc;
689
+ /** Where a node's code lives: the canvas, or a linked page/layout file. */
690
+ nodeSource(canvas: string, id: string): {
691
+ node: CanvasNode;
692
+ rel: string;
693
+ abs: string;
694
+ source: string;
695
+ } | null;
696
+ /** Page and layout files shown by linked frames, across all canvases (the Git panel counts them as design files). */
697
+ linkedFiles(): string[];
698
+ /**
699
+ * Files that belong with design commits besides the canvas folder: linked
700
+ * pages and layouts, and the motion components animations import.
701
+ */
702
+ designFiles(): string[];
703
+ /** Canvases with a linked frame showing this page or layout file. */
704
+ private canvasesLinking;
705
+ /** Called by the app-dir watcher: a page or layout changed (in a code editor, or by git). */
706
+ linkedFileChanged(rel: string): void;
707
+ createCanvas(name: string, actor: Actor, content?: string): string;
708
+ userAuthor(): Promise<CommentAuthor>;
709
+ commentsChanged(canvas: string): void;
710
+ /** Pages panel: each canvas with its frame count. */
711
+ canvasInfos(): {
712
+ name: string;
713
+ frames: number;
714
+ }[];
715
+ private uniqueCanvasName;
716
+ private canvasesChanged;
717
+ renameCanvas(from: string, to: string): string;
718
+ duplicateCanvas(name: string): string;
719
+ /** Deleted pages are kept in memory for this session so they can be restored. */
720
+ private trash;
721
+ deleteCanvas(name: string): void;
722
+ restoreCanvas(name: string): string;
723
+ /** Called by the file watcher. Edits made in a code editor become undoable history too. */
724
+ externalChange(fileName: string): void;
725
+ run(cmd: Command, actor: Actor): Promise<{
726
+ doc: CanvasDoc;
727
+ ids: string[];
728
+ label: string;
729
+ }>;
730
+ private exec;
731
+ /**
732
+ * Linked frames: a frame passed as a parent means the page's root element,
733
+ * and view-only pages refuse edits on their layers.
734
+ */
735
+ private routeLinked;
736
+ /** The page/layout file the command's layers live in, or null for the canvas. */
737
+ private targetOf;
738
+ private apply;
739
+ private autoImport;
740
+ private motion;
741
+ /** work resolved (async) in exec before the synchronous apply */
742
+ private pending;
743
+ private cleanCache;
744
+ isPrettierClean(source: string, file: string): Promise<boolean>;
745
+ /** Pretty-prints an agent's JSX snippet so it lands in the file readable. */
746
+ formatSnippet(jsx: string): Promise<string>;
747
+ private prettier;
748
+ format(source: string, file: string): Promise<string>;
749
+ private record;
750
+ /** Changes still in effect (not undone) on a canvas since a time, oldest first. */
751
+ changesSince(canvas: string, since: number): PublicEntry[];
752
+ historyState(canvas: string): {
753
+ undo: number;
754
+ redo: number;
755
+ };
756
+ private emitHistory;
757
+ undo(canvas: string, actor: Actor): Promise<{
758
+ doc: CanvasDoc;
759
+ label: string;
760
+ }>;
761
+ redo(canvas: string, actor: Actor): Promise<{
762
+ doc: CanvasDoc;
763
+ label: string;
764
+ }>;
765
+ private step;
766
+ publicFeed(): PublicEntry[];
767
+ touchAgent(session: string, name: string): void;
768
+ /** Agents that went quiet for 15 minutes are shown as disconnected. */
769
+ pruneAgents(maxIdle?: number): void;
770
+ dropAgent(session: string): void;
771
+ ensureRoute(): boolean;
772
+ /** First canvas of a project: its homepage as a linked frame when there is one, else the placeholder. */
773
+ private homeCanvas;
774
+ findFrameOf(canvas: string, ref: string): CanvasFrame;
775
+ }
776
+ declare function componentJsx(spec: ComponentSpec, props?: Record<string, Literal>, text?: string): string;
777
+
778
+ /** Compact, token-cheap tree an agent can read: one line per node, ids in brackets. */
779
+ declare function outlineDoc(doc: CanvasDoc, opts?: {
780
+ frame?: string;
781
+ depth?: number;
782
+ }): string;
783
+ declare function describeComponent(c: ComponentSpec, verbose?: boolean): string;
784
+
785
+ /**
786
+ * Reads Tailwind v4 `@theme` blocks so agents use the project's real tokens
787
+ * (bg-surface, text-text-muted…) instead of guessing class names.
788
+ */
789
+ declare function readDesignTokens(root: string): string;
790
+
791
+ interface Device {
792
+ id: string;
793
+ name: string;
794
+ kind: "phone" | "tablet" | "desktop";
795
+ width: number;
796
+ height: number;
797
+ /** device pixel ratio */
798
+ dpr: number;
799
+ /** status bar height (phones/tablets) */
800
+ statusBar: number;
801
+ /** home indicator area at the bottom */
802
+ homeIndicator: number;
803
+ /** screen corner radius */
804
+ radius: number;
805
+ /** dynamic island / notch */
806
+ island?: boolean;
807
+ }
808
+ declare const DEVICES: Device[];
809
+ declare function findDevice(id: string | null | undefined): Device | undefined;
810
+
811
+ /**
812
+ * Writes an older version of a canvas next to the generated route so Next can
813
+ * render it like any canvas ("__compare__<name>"). Relative imports are
814
+ * rewritten for the new location; alias imports (@/…) work unchanged.
815
+ */
816
+ declare function writeCompare(config: TruecanvasConfig, canvas: string, source: string, files?: Record<string, string>, as?: string): string;
817
+ /** Per frame: same, changed, added (only now) or removed (only before). */
818
+ declare function frameChanges(before: string, after: string): Record<string, "changed" | "added" | "removed" | "same">;
819
+
820
+ export { type Actor, type AttrRange, type CanvasDoc, CanvasEditor, type CanvasFrame, type CanvasNode, Catalog, type Command, Comments, type ComponentSpec, DEVICES, type DarkMode, type Device, EditError, type FrameLink, Git, type HistoryEntry, type ImportInfo, type NodeKind, type Preset, type PropSpec, type PropValue, type TruecanvasConfig, Workspace, type WorkspaceEvent, allNodes, attrCode, componentJsx, describeComponent, findDevice, findFrame, findNode, frameChanges, injectIds, loadConfig, outlineDoc, parseCanvas, readDesignTokens, textCode, writeCompare };