@neta-art/cohub 11.0.0 → 11.2.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 (115) hide show
  1. package/README.md +31 -5
  2. package/dist/board/editor/context.d.ts +1 -0
  3. package/dist/board/editor/context.js +1 -0
  4. package/dist/board/editor/creation.d.ts +2 -2
  5. package/dist/board/editor/creation.js +3 -3
  6. package/dist/board/editor/editor.d.ts +5 -4
  7. package/dist/board/editor/editor.js +8 -1
  8. package/dist/board/editor/index.d.ts +2 -2
  9. package/dist/board/editor/interaction.d.ts +12 -2
  10. package/dist/board/editor/interaction.js +50 -8
  11. package/dist/board/editor/selection-transform.d.ts +1 -1
  12. package/dist/board/editor/selection-transform.js +13 -2
  13. package/dist/board/editor/snapshots.d.ts +0 -2
  14. package/dist/board/editor/snapshots.js +4 -20
  15. package/dist/board/export/index.d.ts +2 -0
  16. package/dist/board/export/index.js +1 -0
  17. package/dist/board/export/scene.d.ts +2 -0
  18. package/dist/board/export/scene.js +8 -1
  19. package/dist/board/model/file-preview.d.ts +0 -1
  20. package/dist/board/model/file-preview.js +10 -1
  21. package/dist/board/model/image-key.js +1 -1
  22. package/dist/board/model/index.d.ts +8 -7
  23. package/dist/board/model/index.js +6 -5
  24. package/dist/board/model/items/create.d.ts +16 -3
  25. package/dist/board/model/items/create.js +125 -5
  26. package/dist/board/model/items/index.d.ts +2 -2
  27. package/dist/board/model/items/index.js +2 -2
  28. package/dist/board/model/media-playback.js +1 -1
  29. package/dist/board/model/overlay-placement.d.ts +25 -0
  30. package/dist/board/model/overlay-placement.js +35 -0
  31. package/dist/board/model/replay.d.ts +1 -1
  32. package/dist/board/model/replay.js +1 -0
  33. package/dist/board/model/script-hit.d.ts +20 -3
  34. package/dist/board/model/script-hit.js +81 -24
  35. package/dist/board/model/script-observers.d.ts +18 -0
  36. package/dist/board/model/script-observers.js +92 -0
  37. package/dist/board/model/script-scene.d.ts +1 -1
  38. package/dist/board/model/script-scene.js +35 -8
  39. package/dist/board/model/script.d.ts +51 -7
  40. package/dist/board/render/audio-waveform.d.ts +1 -1
  41. package/dist/board/render/audio-waveform.js +8 -5
  42. package/dist/board/render/board-background.js +5 -4
  43. package/dist/board/render/card-tag.d.ts +24 -0
  44. package/dist/board/render/card-tag.js +205 -0
  45. package/dist/board/render/index.d.ts +8 -5
  46. package/dist/board/render/index.js +8 -5
  47. package/dist/board/render/media-interaction.d.ts +2 -1
  48. package/dist/board/render/media-interaction.js +15 -7
  49. package/dist/board/render/palette.js +2 -2
  50. package/dist/board/render/renderers/arrow-card-renderer.js +20 -16
  51. package/dist/board/render/renderers/audio-card-renderer.d.ts +8 -1
  52. package/dist/board/render/renderers/audio-card-renderer.js +64 -60
  53. package/dist/board/render/renderers/base-card-renderer.d.ts +1 -26
  54. package/dist/board/render/renderers/base-card-renderer.js +6 -92
  55. package/dist/board/render/renderers/board-renderer-registry.d.ts +25 -2
  56. package/dist/board/render/renderers/board-renderer-registry.js +11 -4
  57. package/dist/board/render/renderers/custom-card-renderer.js +6 -8
  58. package/dist/board/render/renderers/effect-card-renderer.js +0 -5
  59. package/dist/board/render/renderers/file-card-renderer.d.ts +1 -11
  60. package/dist/board/render/renderers/file-card-renderer.js +43 -105
  61. package/dist/board/render/renderers/frame-card-renderer.js +14 -15
  62. package/dist/board/render/renderers/image-card-renderer.js +62 -67
  63. package/dist/board/render/renderers/media-key.d.ts +6 -0
  64. package/dist/board/render/renderers/media-key.js +39 -0
  65. package/dist/board/render/renderers/shape-card-renderer.js +6 -12
  66. package/dist/board/render/renderers/task-card-renderer.d.ts +1 -8
  67. package/dist/board/render/renderers/task-card-renderer.js +286 -111
  68. package/dist/board/render/renderers/unknown-card-renderer.js +17 -16
  69. package/dist/board/render/renderers/video-card-renderer.js +73 -80
  70. package/dist/board/render/script-host.d.ts +1 -1
  71. package/dist/board/render/script-host.js +53 -28
  72. package/dist/board/render/script-runtime.js +13 -3
  73. package/dist/board/render/script-texture.d.ts +11 -0
  74. package/dist/board/render/script-texture.js +33 -0
  75. package/dist/board/render/stroke.d.ts +1 -1
  76. package/dist/board/render/stroke.js +5 -4
  77. package/dist/board/render/style.d.ts +29 -0
  78. package/dist/board/render/style.js +54 -0
  79. package/dist/board/render/surface.d.ts +34 -0
  80. package/dist/board/render/surface.js +132 -0
  81. package/dist/board/render/text-fit.d.ts +8 -0
  82. package/dist/board/render/text-fit.js +60 -0
  83. package/dist/board/render/text-resolution.d.ts +2 -1
  84. package/dist/board/render/text-resolution.js +4 -1
  85. package/dist/board/stage/assets.js +1 -1
  86. package/dist/board/stage/index.d.ts +3 -2
  87. package/dist/board/stage/index.js +2 -1
  88. package/dist/board/stage/overlay.d.ts +41 -0
  89. package/dist/board/stage/overlay.js +182 -0
  90. package/dist/board/stage/scene.d.ts +4 -43
  91. package/dist/board/stage/scene.js +38 -130
  92. package/dist/board/stage/script-pointer.d.ts +1 -0
  93. package/dist/board/stage/script-pointer.js +8 -2
  94. package/dist/board/stage/stage.d.ts +13 -3
  95. package/dist/board/stage/stage.js +217 -53
  96. package/dist/chunks/environment.d.ts +1037 -8
  97. package/dist/chunks/environment.js +12 -4
  98. package/dist/chunks/http.d.ts +51 -2
  99. package/dist/chunks/http.js +10 -1
  100. package/dist/chunks/transport.js +1 -1
  101. package/dist/chunks/websocket.d.ts +1 -1
  102. package/dist/http.d.ts +3 -3
  103. package/dist/index.d.ts +3 -3
  104. package/dist/index.js +151 -139
  105. package/dist/protocol/dist/board-model.d.ts +36 -1
  106. package/dist/protocol/dist/board-model.js +14 -4
  107. package/dist/protocol/dist/board-task.d.ts +22 -0
  108. package/dist/{board/model/task.js → protocol/dist/board-task.js} +34 -7
  109. package/dist/protocol/dist/generation/blocks.d.ts +8 -0
  110. package/dist/{generation-blocks.js → protocol/dist/generation/blocks.js} +14 -15
  111. package/dist/protocol/dist/index.d.ts +5 -2
  112. package/dist/protocol/dist/task/index.d.ts +4 -0
  113. package/package.json +2 -1
  114. package/dist/board/model/task.d.ts +0 -11
  115. package/dist/types.d.ts +0 -36
package/README.md CHANGED
@@ -46,18 +46,39 @@ Development uses:
46
46
  - API: `https://api-dev.cohub.live`
47
47
  - WebSocket: `wss://gateway-dev.cohub.live/ws`
48
48
 
49
- Custom endpoints are still supported when needed:
49
+ Override endpoints for self-hosted deployments or proxies:
50
50
 
51
51
  ```ts
52
52
  const client = createCohubClient({
53
53
  baseUrl: "https://api.example.com",
54
+ webBaseUrl: "https://cohub.example.com",
54
55
  getAccessToken: async () => localStorage.getItem("token"),
55
56
  websocket: {
56
57
  url: "https://gateway.example.com",
57
58
  },
59
+ voice: {
60
+ url: "https://gateway.example.com",
61
+ },
58
62
  });
59
63
  ```
60
64
 
65
+ In Node.js, the SDK and CLI share these optional environment variables:
66
+
67
+ | Variable | Endpoint |
68
+ | --- | --- |
69
+ | `COHUB_BASE_URL` | API base URL |
70
+ | `COHUB_WS_URL` | Realtime gateway URL (`http(s)` or `ws(s)`; `/ws` is added when omitted) |
71
+ | `COHUB_WEB_URL` | Web origin for App authorization and CLI links |
72
+ | `COHUB_VOICE_WS_URL` | Voice input gateway URL; falls back to `COHUB_WS_URL` with `/asr/ws` |
73
+
74
+ Explicit client options take precedence over environment variables. Unconfigured
75
+ endpoints use the selected environment's defaults. API, web, and gateway addresses
76
+ are configured separately; changing `baseUrl` does not change the gateway.
77
+
78
+ ```bash
79
+ COHUB_BASE_URL=http://localhost:3000 COHUB_WS_URL=ws://localhost:8080 node app.js
80
+ ```
81
+
61
82
  ## Spaces and sessions
62
83
 
63
84
  A **Space** is a live, isolated working environment where users and agents create together.
@@ -101,10 +122,15 @@ const stop = board.subscribe({
101
122
  });
102
123
  ```
103
124
 
104
- Task Items keep a small, replaceable display snapshot beside their stable
105
- `taskRunId`; build it from an authoritative TaskRun with
106
- `taskRunToBoardTaskSnapshot(taskRun)` from `@neta-art/cohub/board`, and restore
107
- a Board's TaskRuns in one request with `client.tasks.getMany(ids, { spaceId })`.
125
+ Task Items show a run by its `taskRunId`. Write only the id; the server fills
126
+ the display snapshot from the run and rewrites it when the run finishes, on
127
+ every Board in the Space that shows it. Lay a run's results out as media with
128
+ `createTaskResultItems`; each result keeps an `origin` leading back to the run.
129
+
130
+ ```ts
131
+ const { taskRunId } = await client.generations.create({ spaceId, model, content });
132
+ await board.apply({ items: { hero: { type: "task", position: { x: 80, y: 80 }, size: { width: 480, height: 270 }, props: { taskRunId } } } });
133
+ ```
108
134
 
109
135
  ### Layers
110
136
 
@@ -40,6 +40,7 @@ type BoardEditorState = {
40
40
  width: number;
41
41
  height: number;
42
42
  };
43
+ controlOutset: number;
43
44
  cameraPolicy: BoardCameraPolicy;
44
45
  tool: BoardToolId;
45
46
  toolStyles: BoardToolStyleMap;
@@ -8,6 +8,7 @@ const STATE_CHANNELS = {
8
8
  selection: "selection",
9
9
  camera: "camera",
10
10
  surfaceSize: "camera",
11
+ controlOutset: "selection",
11
12
  cameraPolicy: "camera",
12
13
  tool: "tool",
13
14
  toolStyles: "tool",
@@ -28,8 +28,8 @@ declare function createCreationModule(ctx: EditorContext): {
28
28
  snapshot?: BoardMediaSnapshot;
29
29
  }) => string;
30
30
  addFile: (path: string, at: WorldPoint, snapshot?: BoardMediaSnapshot & BoardFileSnapshotFacts) => string;
31
- addTask: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, metadata?: Record<string, unknown>) => string;
32
- addTaskWithSources: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, sources: BoardTaskSource[], metadata?: Record<string, unknown>) => string | null;
31
+ addTask: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, metadata?: Record<string, unknown>, aspect?: number) => string;
32
+ addTaskWithSources: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, sources: BoardTaskSource[], metadata?: Record<string, unknown>, aspect?: number) => string | null;
33
33
  addText: (text: string, at: WorldPoint) => void;
34
34
  addShape: (at: WorldPoint, geometry?: ShapeKind) => void;
35
35
  addFrame: (at: WorldPoint) => void;
@@ -53,10 +53,10 @@ function createCreationModule(ctx) {
53
53
  if (options.readonly) return null;
54
54
  return addOne(ctx.registry.create(type, input));
55
55
  }
56
- function addTaskWithSources(taskRunId, snapshot, at, sources, metadata) {
56
+ function addTaskWithSources(taskRunId, snapshot, at, sources, metadata, aspect) {
57
57
  if (options.readonly) return null;
58
58
  const { arrow } = state.toolStyles;
59
- const task = createTaskBoardItem(taskRunId, snapshot, at.x, at.y, metadata);
59
+ const task = createTaskBoardItem(taskRunId, snapshot, at.x, at.y, metadata, void 0, aspect);
60
60
  add([task, ...sources.filter((source) => source.itemId !== task.id && state.scene.get(source.itemId)).map((source) => {
61
61
  const entry = createArrowBoardItem({
62
62
  x: 0,
@@ -199,7 +199,7 @@ function createCreationModule(ctx) {
199
199
  addApp: (app, at) => addOne(createAppBoardItem(app, at.x, at.y)),
200
200
  addMedia: (src, at, options = {}) => addOne(createMediaBoardItem(src, at.x, at.y, options.snapshot, void 0, options.kind)),
201
201
  addFile: (path, at, snapshot) => addOne(createFileNodeForPath(path, at.x, at.y, snapshot)),
202
- addTask: (taskRunId, snapshot, at, metadata) => addOne(createTaskBoardItem(taskRunId, snapshot, at.x, at.y, metadata)),
202
+ addTask: (taskRunId, snapshot, at, metadata, aspect) => addOne(createTaskBoardItem(taskRunId, snapshot, at.x, at.y, metadata, void 0, aspect)),
203
203
  addTaskWithSources,
204
204
  addText: (text, at) => {
205
205
  add([createTextBoardItem(text, at.x, at.y, styles().text.color)]);
@@ -11,7 +11,7 @@ import "../model/index.js";
11
11
  import { AlignMode, DistributeAxis } from "./align.js";
12
12
  import { BoardFocusOptions } from "./camera.js";
13
13
  import { BoardSelectionTransform, BoardTransformControl } from "./selection-transform.js";
14
- import { BoardInteraction, BoardPointerEvent } from "./interaction.js";
14
+ import { BoardInteraction, BoardPointerEvent, BoardTransformEnd } from "./interaction.js";
15
15
  import { BoardAppMetadata, BoardTaskSource } from "./creation.js";
16
16
  import { BoardClipboardPayload } from "./clipboard.js";
17
17
  import { BoardNaturalSize } from "./snapshots.js";
@@ -126,6 +126,7 @@ declare function createBoardEditor(options: BoardEditorOptions): {
126
126
  width: number;
127
127
  height: number;
128
128
  };
129
+ controlOutset: number;
129
130
  loadDocument: (next: BoardDocument, key?: string) => void;
130
131
  undo: () => void;
131
132
  redo: () => void;
@@ -178,8 +179,8 @@ declare function createBoardEditor(options: BoardEditorOptions): {
178
179
  kind?: BoardMediaItemKind;
179
180
  snapshot?: BoardMediaSnapshot;
180
181
  }) => string;
181
- addTask: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, metadata?: Record<string, unknown>) => string;
182
- addTaskWithSources: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, sources: BoardTaskSource[], metadata?: Record<string, unknown>) => string | null;
182
+ addTask: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, metadata?: Record<string, unknown>, aspect?: number) => string;
183
+ addTaskWithSources: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, sources: BoardTaskSource[], metadata?: Record<string, unknown>, aspect?: number) => string | null;
183
184
  addText: (text: string, at: WorldPoint) => void;
184
185
  addShape: (at: WorldPoint, geometry?: ShapeKind) => void;
185
186
  addFrame: (at: WorldPoint) => void;
@@ -204,7 +205,6 @@ declare function createBoardEditor(options: BoardEditorOptions): {
204
205
  snapshot: BoardFileSnapshotFacts;
205
206
  replace?: boolean;
206
207
  }>) => void;
207
- applyTaskSnapshots: (snapshots: ReadonlyMap<string, BoardTaskSnapshot>) => void;
208
208
  setPlayhead: (next: BoardPlayhead | null) => void;
209
209
  setRecording: (value: boolean) => void;
210
210
  keyframeState: (property: string) => "animated" | "keyframe" | "none";
@@ -217,6 +217,7 @@ declare function createBoardEditor(options: BoardEditorOptions): {
217
217
  pointerMove: (event: BoardPointerEvent) => void;
218
218
  pointerUp: (event: BoardPointerEvent) => void;
219
219
  pointerLeave: () => void;
220
+ subscribeTransforms: (listener: (event: BoardTransformEnd) => void) => () => void;
220
221
  cancelPointerInteraction: () => void;
221
222
  destroy(): void;
222
223
  };
@@ -53,6 +53,7 @@ function createBoardEditor(options) {
53
53
  width: 0,
54
54
  height: 0
55
55
  },
56
+ controlOutset: 0,
56
57
  cameraPolicy: preferences?.cameraPolicy ?? "follow",
57
58
  tool: options.initialTool ?? "select",
58
59
  toolStyles: preferences?.toolStyles ?? createBoardToolStyles(),
@@ -339,6 +340,12 @@ function createBoardEditor(options) {
339
340
  set surfaceSize(value) {
340
341
  state.surfaceSize = value;
341
342
  },
343
+ get controlOutset() {
344
+ return read("selection", () => state.controlOutset);
345
+ },
346
+ set controlOutset(value) {
347
+ if (value !== state.controlOutset) state.controlOutset = value;
348
+ },
342
349
  loadDocument: remote.loadDocument,
343
350
  undo: document.undo,
344
351
  redo: document.redo,
@@ -409,7 +416,6 @@ function createBoardEditor(options) {
409
416
  applyMediaFileChange: snapshots.applyMediaFileChange,
410
417
  adoptMediaNaturalSizes: snapshots.adoptMediaNaturalSizes,
411
418
  applyFileSnapshots: snapshots.applyFileSnapshots,
412
- applyTaskSnapshots: snapshots.applyTaskSnapshots,
413
419
  setPlayhead: animation.setPlayhead,
414
420
  setRecording: animation.setRecording,
415
421
  keyframeState: tracked([
@@ -427,6 +433,7 @@ function createBoardEditor(options) {
427
433
  pointerMove: interaction.pointerMove,
428
434
  pointerUp: interaction.pointerUp,
429
435
  pointerLeave: interaction.pointerLeave,
436
+ subscribeTransforms: interaction.subscribeTransforms,
430
437
  cancelPointerInteraction: interaction.cancel,
431
438
  destroy() {
432
439
  stopViewState();
@@ -2,7 +2,7 @@ import { BOARD_CLIPBOARD_MIME } from "../../protocol/dist/board.js";
2
2
  import { AlignMode, DistributeAxis, alignFrames, distributeFrames, framesBounds } from "./align.js";
3
3
  import { BoardFocusOptions } from "./camera.js";
4
4
  import { BoardSelectionTransform, BoardTransformControl, resizeCursorForHandle, resolveSelectionTransform, selectionTransformControlAt } from "./selection-transform.js";
5
- import { BoardInteraction, BoardPointerEvent } from "./interaction.js";
5
+ import { BoardInteraction, BoardPointerEvent, BoardTransformEnd } from "./interaction.js";
6
6
  import { BoardAppMetadata, BoardTaskSource } from "./creation.js";
7
7
  import { BOARD_CLIPBOARD_VERSION, BoardClipboardPayload, DUPLICATE_OFFSET, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, defaultPasteOffset, encodeClipboard, materializeClipboard, parseClipboard, remapItems } from "./clipboard.js";
8
8
  import { BoardNaturalSize } from "./snapshots.js";
@@ -15,4 +15,4 @@ import { BoardEditPlayhead, diffBoardEdits, isEmptyBoardPatch, readPath, routeBo
15
15
  import { BoardEditor, createBoardEditor } from "./editor.js";
16
16
  import { SpatialEntry, SpatialIndex, createSpatialIndex } from "./spatial.js";
17
17
  import { normalizeWheelDelta, wheelZoomFactor } from "./wheel.js";
18
- export { AlignMode, BOARD_CLIPBOARD_MIME, BOARD_CLIPBOARD_VERSION, BOARD_HAND_TAP_SLOP, type BoardAppMetadata, type BoardCameraPolicy, BoardClipboardPayload, BoardEditPlayhead, type BoardEditor, type BoardEditorChannel, type BoardEditorListener, type BoardEditorOptions, type BoardEditorPreferences, type BoardFocusOptions, type BoardInteraction, type BoardNaturalSize, type BoardPlayhead, type BoardPointerEvent, BoardSelectionTransform, type BoardTaskSource, BoardToolId, BoardTransformControl, type BoardViewState, CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, ConnectionPort, DUPLICATE_OFFSET, DistributeAxis, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, SnapAxis, SnapGuide, SnapOptions, SnapResult, SpatialEntry, SpatialIndex, alignFrames, canTapSelectWithHand, computeSnap, connectionPortAt, connectionPorts, createBoardEditor, createSpatialIndex, defaultBoardTool, defaultPasteOffset, diffBoardEdits, distributeFrames, encodeClipboard, framesBounds, isContinuousBoardTool, isCreationBoardTool, isEmptyBoardPatch, isWithinHandTapSlop, materializeClipboard, normalizeWheelDelta, parseClipboard, portHitRadius, readPath, remapItems, resizeCursorForHandle, resolveSelectionTransform, routeBoardEdits, sameJson, selectionTransformControlAt, wheelZoomFactor, writePath };
18
+ export { AlignMode, BOARD_CLIPBOARD_MIME, BOARD_CLIPBOARD_VERSION, BOARD_HAND_TAP_SLOP, type BoardAppMetadata, type BoardCameraPolicy, BoardClipboardPayload, BoardEditPlayhead, type BoardEditor, type BoardEditorChannel, type BoardEditorListener, type BoardEditorOptions, type BoardEditorPreferences, type BoardFocusOptions, type BoardInteraction, type BoardNaturalSize, type BoardPlayhead, type BoardPointerEvent, BoardSelectionTransform, type BoardTaskSource, BoardToolId, BoardTransformControl, type BoardTransformEnd, type BoardViewState, CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, ConnectionPort, DUPLICATE_OFFSET, DistributeAxis, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, SnapAxis, SnapGuide, SnapOptions, SnapResult, SpatialEntry, SpatialIndex, alignFrames, canTapSelectWithHand, computeSnap, connectionPortAt, connectionPorts, createBoardEditor, createSpatialIndex, defaultBoardTool, defaultPasteOffset, diffBoardEdits, distributeFrames, encodeClipboard, framesBounds, isContinuousBoardTool, isCreationBoardTool, isEmptyBoardPatch, isWithinHandTapSlop, materializeClipboard, normalizeWheelDelta, parseClipboard, portHitRadius, readPath, remapItems, resizeCursorForHandle, resolveSelectionTransform, routeBoardEdits, sameJson, selectionTransformControlAt, wheelZoomFactor, writePath };
@@ -28,6 +28,10 @@ type BoardInteraction = {
28
28
  single: BoardFrame | null;
29
29
  bounds: Rect;
30
30
  origin: Map<string, BoardFrame>;
31
+ grab: {
32
+ x: number;
33
+ y: number;
34
+ };
31
35
  moved: boolean;
32
36
  } | {
33
37
  type: "rotating";
@@ -90,6 +94,11 @@ type BoardPointerEvent = {
90
94
  pressure: number;
91
95
  samples?: readonly BoardDrawInputSample[];
92
96
  };
97
+ type BoardTransformEnd = {
98
+ ids: string[];
99
+ operation: "move" | "resize" | "rotate" | "arrow";
100
+ cancelled: boolean;
101
+ };
93
102
  declare function createInteractionModule(ctx: EditorContext): {
94
103
  marquee: () => Rect | null;
95
104
  hoveredControl: () => BoardTransformControl | null;
@@ -108,8 +117,9 @@ declare function createInteractionModule(ctx: EditorContext): {
108
117
  pointerUp: (event: BoardPointerEvent) => void;
109
118
  pointerLeave: () => void;
110
119
  cancel: () => void;
111
- destroy: () => void;
120
+ subscribeTransforms(listener: (event: BoardTransformEnd) => void): () => void;
121
+ destroy(): void;
112
122
  };
113
123
  type InteractionModule = ReturnType<typeof createInteractionModule>;
114
124
  //#endregion
115
- export { BoardInteraction, BoardPointerEvent, InteractionModule, createInteractionModule };
125
+ export { BoardInteraction, BoardPointerEvent, BoardTransformEnd, InteractionModule, createInteractionModule };
@@ -1,4 +1,4 @@
1
- import { clampZoom, normalizeRotation, panBy, rectCenter, rectsIntersect, resizeFrame, rotateFrames, scaleFrames, screenPoint, selectionBounds, worldPoint, zoomAround } from "../model/geometry.js";
1
+ import { clampZoom, frameHandlePosition, normalizeRotation, panBy, rectCenter, rectsIntersect, resizeFrame, rotateFrames, scaleFrames, screenPoint, selectionBounds, worldPoint, zoomAround } from "../model/geometry.js";
2
2
  import { resolveSceneArrow } from "../model/arrow-geometry.js";
3
3
  import { appendBoardDrawSamples } from "../model/draw-input.js";
4
4
  import { createBoardItemId } from "../model/items/id.js";
@@ -15,6 +15,23 @@ function angleFrom(center, point) {
15
15
  return Math.atan2(point.y - center.y, point.x - center.x) * 180 / Math.PI;
16
16
  }
17
17
  function createInteractionModule(ctx) {
18
+ const transformListeners = /* @__PURE__ */ new Set();
19
+ function transformEnd(gesture, cancelled) {
20
+ if (!transformListeners.size || !("moved" in gesture) || !gesture.moved) return;
21
+ let event;
22
+ if (gesture.type === "translating" || gesture.type === "resizing" || gesture.type === "rotating") event = {
23
+ ids: [...gesture.origin.keys()],
24
+ operation: gesture.type === "translating" ? "move" : gesture.type === "resizing" ? "resize" : "rotate",
25
+ cancelled
26
+ };
27
+ else if (gesture.type === "draggingArrowHandle") event = {
28
+ ids: [gesture.arrowId],
29
+ operation: "arrow",
30
+ cancelled
31
+ };
32
+ else return;
33
+ for (const listener of transformListeners) listener(event);
34
+ }
18
35
  const { state, options, registry } = ctx;
19
36
  const activePointers = /* @__PURE__ */ new Map();
20
37
  let pinch = null;
@@ -34,8 +51,9 @@ function createInteractionModule(ctx) {
34
51
  state.hoverPoint,
35
52
  state.hoverPointerType,
36
53
  state.camera.zoom,
37
- ctx.selection.transform()
38
- ], (interaction, tool, hoverPoint, pointerType, zoom, transform) => interaction.type === "idle" && !pinch && tool === "select" && hoverPoint ? selectionTransformControlAt(transform, hoverPoint, zoom, pointerType) : null);
54
+ ctx.selection.transform(),
55
+ state.controlOutset
56
+ ], (interaction, tool, hoverPoint, pointerType, zoom, transform, outset) => interaction.type === "idle" && !pinch && tool === "select" && hoverPoint ? selectionTransformControlAt(transform, hoverPoint, zoom, pointerType, outset) : null);
39
57
  function gridSnapSize() {
40
58
  const grid = state.base.board.grid;
41
59
  return grid?.visible === true ? grid.size ?? 0 : 0;
@@ -227,7 +245,8 @@ function createInteractionModule(ctx) {
227
245
  return;
228
246
  }
229
247
  const bounds = ctx.selection.bounds();
230
- const control = selectionTransformControlAt(ctx.selection.transform(), event.world, camera.zoom, event.pointerType);
248
+ const transform = ctx.selection.transform();
249
+ const control = selectionTransformControlAt(transform, event.world, camera.zoom, event.pointerType, state.controlOutset);
231
250
  if (control?.kind === "rotate" && bounds) {
232
251
  const pivot = rectCenter(bounds);
233
252
  state.interaction = {
@@ -240,15 +259,21 @@ function createInteractionModule(ctx) {
240
259
  };
241
260
  return;
242
261
  }
243
- if (control?.kind === "resize" && bounds) {
262
+ if (control?.kind === "resize" && bounds && transform) {
244
263
  const origin = ctx.selection.framesFor(ctx.selection.movable());
245
264
  const single = origin.size === 1 ? { ...[...origin.values()][0] } : null;
265
+ const edge = frameHandlePosition(transform.frame, control.handle);
266
+ const grab = {
267
+ x: edge.x - event.world.x,
268
+ y: edge.y - event.world.y
269
+ };
246
270
  state.interaction = {
247
271
  type: "resizing",
248
272
  handle: control.handle,
249
273
  single,
250
274
  bounds,
251
275
  origin,
276
+ grab,
252
277
  moved: false
253
278
  };
254
279
  return;
@@ -332,13 +357,14 @@ function createInteractionModule(ctx) {
332
357
  moved: true
333
358
  };
334
359
  const frames = /* @__PURE__ */ new Map();
360
+ const point = worldPoint(event.world.x + gesture.grab.x, event.world.y + gesture.grab.y);
335
361
  if (gesture.single) {
336
362
  const id = [...gesture.origin.keys()][0];
337
363
  const item = state.scene.get(id);
338
364
  const keepAspect = item ? registry.capabilities(item).aspectLocked || event.shiftKey : event.shiftKey;
339
- frames.set(id, resizeFrame(gesture.single, gesture.handle, event.world, void 0, keepAspect));
365
+ frames.set(id, resizeFrame(gesture.single, gesture.handle, point, void 0, keepAspect));
340
366
  } else {
341
- const scaled = scaleFrames([...gesture.origin.values()], gesture.bounds, gesture.handle, event.world);
367
+ const scaled = scaleFrames([...gesture.origin.values()], gesture.bounds, gesture.handle, point);
342
368
  [...gesture.origin.keys()].forEach((id, index) => {
343
369
  const frame = scaled[index];
344
370
  if (frame) frames.set(id, frame);
@@ -493,6 +519,10 @@ function createInteractionModule(ctx) {
493
519
  if (activePointers.size > 0) return;
494
520
  const gesture = state.interaction;
495
521
  if (gesture.type === "drawing" && gesture.pointerId !== event.pointerId) return;
522
+ if (event.cancelled) {
523
+ cancel();
524
+ return;
525
+ }
496
526
  state.snapGuides = [];
497
527
  state.interaction = { type: "idle" };
498
528
  switch (gesture.type) {
@@ -531,6 +561,7 @@ function createInteractionModule(ctx) {
531
561
  case "creatingBox": ctx.creation.commitBox(gesture);
532
562
  }
533
563
  ctx.remote.flushPending();
564
+ transformEnd(gesture, false);
534
565
  }
535
566
  function pointerLeave() {
536
567
  if (state.interaction.type !== "idle") return;
@@ -538,12 +569,14 @@ function createInteractionModule(ctx) {
538
569
  state.hoverId = null;
539
570
  }
540
571
  function cancel() {
572
+ const gesture = state.interaction;
541
573
  activePointers.clear();
542
574
  pinch = null;
543
575
  state.snapGuides = [];
544
576
  state.interaction = { type: "idle" };
545
577
  ctx.document.discardDraft();
546
578
  ctx.remote.flushPending();
579
+ transformEnd(gesture, true);
547
580
  }
548
581
  const ports = memo(() => [
549
582
  state.scene,
@@ -577,7 +610,16 @@ function createInteractionModule(ctx) {
577
610
  pointerUp,
578
611
  pointerLeave,
579
612
  cancel,
580
- destroy: () => activePointers.clear()
613
+ subscribeTransforms(listener) {
614
+ transformListeners.add(listener);
615
+ return () => {
616
+ transformListeners.delete(listener);
617
+ };
618
+ },
619
+ destroy() {
620
+ activePointers.clear();
621
+ transformListeners.clear();
622
+ }
581
623
  };
582
624
  }
583
625
  //#endregion
@@ -19,6 +19,6 @@ type BoardSelectionTransform = {
19
19
  canRotate: boolean;
20
20
  };
21
21
  declare function resolveSelectionTransform(registry: BoardRegistry, items: BoardSceneItem[], bounds: Rect | null): BoardSelectionTransform | null;
22
- declare function selectionTransformControlAt(transform: BoardSelectionTransform | null, point: WorldPoint, zoom: number, pointerType: string): BoardTransformControl | null;
22
+ declare function selectionTransformControlAt(selection: BoardSelectionTransform | null, point: WorldPoint, zoom: number, pointerType: string, outset?: number): BoardTransformControl | null;
23
23
  //#endregion
24
24
  export { BoardSelectionTransform, BoardTransformControl, resizeCursorForHandle, resolveSelectionTransform, selectionTransformControlAt };
@@ -51,10 +51,21 @@ function controlHitRadius(pointerType) {
51
51
  if (pointerType === "pen") return 12;
52
52
  return 8;
53
53
  }
54
- function selectionTransformControlAt(transform, point, zoom, pointerType) {
55
- if (!transform) return null;
54
+ function selectionTransformControlAt(selection, point, zoom, pointerType, outset = 0) {
55
+ if (!selection) return null;
56
56
  const screenRadius = controlHitRadius(pointerType);
57
57
  const radius = screenRadius / Math.max(zoom, 1e-4);
58
+ const grow = outset / Math.max(zoom, 1e-4);
59
+ const transform = grow > 0 ? {
60
+ ...selection,
61
+ frame: {
62
+ ...selection.frame,
63
+ x: selection.frame.x - grow,
64
+ y: selection.frame.y - grow,
65
+ width: selection.frame.width + grow * 2,
66
+ height: selection.frame.height + grow * 2
67
+ }
68
+ } : selection;
58
69
  if (transform.resizeMode !== "none") for (const handle of CORNER_RESIZE_HANDLES) {
59
70
  const position = frameHandlePosition(transform.frame, handle);
60
71
  if (Math.hypot(position.x - point.x, position.y - point.y) <= radius) return {
@@ -1,4 +1,3 @@
1
- import { BoardTaskSnapshot } from "../../protocol/dist/board-model.js";
2
1
  import { BoardFileSnapshotFacts } from "../model/file-snapshot.js";
3
2
  import "../model/index.js";
4
3
  import { EditorContext } from "./context.js";
@@ -20,7 +19,6 @@ declare function createSnapshotsModule(ctx: EditorContext): {
20
19
  snapshot: BoardFileSnapshotFacts;
21
20
  replace?: boolean;
22
21
  }>) => void;
23
- applyTaskSnapshots: (snapshots: ReadonlyMap<string, BoardTaskSnapshot>) => void;
24
22
  };
25
23
  type SnapshotsModule = ReturnType<typeof createSnapshotsModule>;
26
24
  //#endregion
@@ -1,15 +1,16 @@
1
+ import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
1
2
  import { mergeFileSnapshot } from "../model/file-preview.js";
2
- import { featuredTaskArtifact } from "../model/task.js";
3
3
  import { sameJson } from "./document-edits.js";
4
4
  //#region src/board/editor/snapshots.ts
5
5
  function fitHeight(item, natural) {
6
6
  const height = item.size.width * natural.height / natural.width;
7
7
  if (!Number.isFinite(height) || height <= 0) return null;
8
+ const shift = item.type === "task" ? 0 : (item.size.height - height) / 2;
8
9
  return {
9
10
  ...item,
10
11
  position: {
11
12
  x: item.position.x,
12
- y: item.position.y + (item.size.height - height) / 2
13
+ y: item.position.y + shift
13
14
  },
14
15
  size: {
15
16
  ...item.size,
@@ -105,27 +106,10 @@ function createSnapshotsModule(ctx) {
105
106
  }
106
107
  commit(changes);
107
108
  }
108
- function applyTaskSnapshots(snapshots) {
109
- if (snapshots.size === 0) return;
110
- const changes = /* @__PURE__ */ new Map();
111
- for (const [id, item] of Object.entries(state.base.items)) {
112
- if (item.type !== "task") continue;
113
- const snapshot = snapshots.get(item.props.taskRunId);
114
- if (snapshot && !sameJson(snapshot, item.props.snapshot)) changes.set(id, {
115
- ...item,
116
- props: {
117
- ...item.props,
118
- snapshot
119
- }
120
- });
121
- }
122
- commit(changes);
123
- }
124
109
  return {
125
110
  applyMediaFileChange,
126
111
  adoptMediaNaturalSizes,
127
- applyFileSnapshots,
128
- applyTaskSnapshots
112
+ applyFileSnapshots
129
113
  };
130
114
  }
131
115
  //#endregion
@@ -6,6 +6,7 @@ import { BoardEvaluation } from "../model/animation.js";
6
6
  import { BoardAnimationSampler } from "../model/sampling.js";
7
7
  import { BoardExportPlan, BoardExportPlanInput, BoardExportRegion } from "../model/export-plan.js";
8
8
  import { BoardShapeColors } from "../model/palette.js";
9
+ import { BoardRenderStyle } from "../render/style.js";
9
10
  import { BoardCardRendererResolver, BoardRenderPalette, BoardScriptHost } from "../render/renderers/board-renderer-registry.js";
10
11
  import "../render/index.js";
11
12
  import { BoardExportScene, BoardExportSceneInput, createBoardExportScene } from "./scene.js";
@@ -29,6 +30,7 @@ type BoardExportOptions = {
29
30
  background?: BoardExportBackground;
30
31
  palette?: Partial<BoardRenderPalette>;
31
32
  colors?: BoardShapeColors;
33
+ style?: Partial<BoardRenderStyle>;
32
34
  textures?: Map<string, Texture>;
33
35
  scripts?: BoardScriptHost;
34
36
  assetKey?: (item: BoardSceneItem) => string | null;
@@ -76,6 +76,7 @@ function renderBoardExport(renderer, input, options = {}) {
76
76
  colorScheme,
77
77
  palette,
78
78
  colors: options.colors,
79
+ style: options.style,
79
80
  textures: options.textures,
80
81
  scripts: options.scripts,
81
82
  assetKey: options.assetKey,
@@ -3,6 +3,7 @@ import "../../protocol/dist/index.js";
3
3
  import { Rect } from "../model/geometry.js";
4
4
  import { BoardScene, BoardSceneItem } from "../model/scene.js";
5
5
  import { BoardShapeColors } from "../model/palette.js";
6
+ import { BoardRenderStyle } from "../render/style.js";
6
7
  import { BoardCardRendererResolver, BoardRenderContext, BoardRenderPalette } from "../render/renderers/board-renderer-registry.js";
7
8
  import "../render/index.js";
8
9
  import { Container, Texture } from "pixi.js";
@@ -18,6 +19,7 @@ type BoardExportSceneInput = {
18
19
  colorScheme: "dark" | "light";
19
20
  palette?: Partial<BoardRenderPalette>;
20
21
  colors?: BoardShapeColors;
22
+ style?: Partial<BoardRenderStyle>;
21
23
  textures?: Map<string, Texture>;
22
24
  scripts?: BoardRenderContext["scripts"];
23
25
  assetKey?: (item: BoardSceneItem) => string | null;
@@ -2,7 +2,8 @@ import { buildFallbackShapeColors } from "../model/palette.js";
2
2
  import { imageAssetKey } from "../model/image-key.js";
3
3
  import { defaultBoardPalette } from "../render/palette.js";
4
4
  import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
5
- import { getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
5
+ import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
6
+ import { resolveBoardRenderStyle } from "../render/style.js";
6
7
  import { Container, Graphics, Sprite, TilingSprite } from "pixi.js";
7
8
  //#region src/board/export/scene.ts
8
9
  function buildContext(input) {
@@ -17,14 +18,20 @@ function buildContext(input) {
17
18
  selectedIds: /* @__PURE__ */ new Set(),
18
19
  hoveredId: null,
19
20
  resizingIds: /* @__PURE__ */ new Set(),
21
+ liftedIds: /* @__PURE__ */ new Set(),
20
22
  palette: {
21
23
  ...defaultBoardPalette(input.colorScheme),
22
24
  ...input.palette
23
25
  },
24
26
  colors: input.colors ?? buildFallbackShapeColors(input.colorScheme),
25
27
  colorScheme: input.colorScheme,
28
+ style: resolveBoardRenderStyle(input.colorScheme, input.style),
29
+ themeKey: "export",
26
30
  rendererType: "canvas",
27
31
  zoom: input.scale,
32
+ reducedMotion: true,
33
+ labels: DEFAULT_BOARD_RENDER_LABELS,
34
+ taskRun: () => null,
28
35
  scripts: input.scripts,
29
36
  assetKey: input.assetKey ?? imageAssetKey,
30
37
  getTexture: (key) => textures.get(key) ?? null,
@@ -7,7 +7,6 @@ type FilePreviewKind = "cover" | "text" | "blank";
7
7
  * sync with them.
8
8
  */
9
9
  declare function filePreviewKind(snapshot: BoardFileSnapshotFacts | undefined): FilePreviewKind;
10
- /** Short uppercase type label for a file card (`MD`, `JSON`, `FILE`). */
11
10
  declare function fileTypeLabel(path: string): string;
12
11
  /** Human-readable byte size. Kept as a public helper for custom card renderers. */
13
12
  declare function formatFileSize(bytes: number | undefined): string;
@@ -11,13 +11,22 @@ function filePreviewKind(snapshot) {
11
11
  if (snapshot.excerpt) return "text";
12
12
  return "blank";
13
13
  }
14
- /** Short uppercase type label for a file card (`MD`, `JSON`, `FILE`). */
15
14
  function fileTypeLabel(path) {
15
+ if (isWebPageUrl(path)) return "LINK";
16
16
  const name = fileBaseName(path);
17
17
  const dot = name.lastIndexOf(".");
18
18
  if (dot <= 0) return (name.replace(/^\./, "") || "file").toUpperCase();
19
19
  return name.slice(dot + 1).toUpperCase();
20
20
  }
21
+ function isWebPageUrl(path) {
22
+ if (!/^https?:\/\//i.test(path)) return false;
23
+ try {
24
+ const last = new URL(path).pathname.split("/").filter(Boolean).pop() ?? "";
25
+ return !/\.[a-z\d]{1,8}$/i.test(last);
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
21
30
  /** Human-readable byte size. Kept as a public helper for custom card renderers. */
22
31
  function formatFileSize(bytes) {
23
32
  if (bytes === void 0 || !Number.isFinite(bytes) || bytes < 0) return "";
@@ -1,5 +1,5 @@
1
1
  import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
2
- import { featuredTaskArtifact, taskArtifactPreviewUrl } from "./task.js";
2
+ import { featuredTaskArtifact, taskArtifactPreviewUrl } from "../../protocol/dist/board-task.js";
3
3
  //#region src/board/model/image-key.ts
4
4
  function imageAssetKey(item) {
5
5
  if (item.type === "image") {