@neta-art/cohub 12.2.0 → 12.3.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 (102) hide show
  1. package/README.md +77 -3
  2. package/dist/board/editor/clipboard.js +1 -1
  3. package/dist/board/editor/creation.js +1 -1
  4. package/dist/board/editor/document.d.ts +3 -1
  5. package/dist/board/editor/document.js +8 -6
  6. package/dist/board/editor/editor.d.ts +7 -3
  7. package/dist/board/editor/editor.js +4 -2
  8. package/dist/board/editor/index.d.ts +3 -3
  9. package/dist/board/editor/index.js +2 -2
  10. package/dist/board/editor/interaction.d.ts +23 -2
  11. package/dist/board/editor/interaction.js +93 -4
  12. package/dist/board/editor/query.d.ts +1 -1
  13. package/dist/board/editor/query.js +3 -7
  14. package/dist/board/editor/selection.js +1 -1
  15. package/dist/board/editor/tool.d.ts +4 -1
  16. package/dist/board/editor/tool.js +6 -1
  17. package/dist/board/export/index.js +1 -1
  18. package/dist/board/export/node/scripts.js +1 -1
  19. package/dist/board/export/scene.js +1 -1
  20. package/dist/board/model/asset-key.d.ts +9 -1
  21. package/dist/board/model/asset-key.js +15 -3
  22. package/dist/board/model/index.d.ts +5 -5
  23. package/dist/board/model/index.js +18 -18
  24. package/dist/board/model/items/create.js +2 -2
  25. package/dist/board/model/items/definition.d.ts +9 -1
  26. package/dist/board/model/items/index.d.ts +2 -2
  27. package/dist/board/model/items/registry.d.ts +2 -1
  28. package/dist/board/model/items/registry.js +1 -0
  29. package/dist/board/model/sampling.js +1 -1
  30. package/dist/board/model/text-metrics.d.ts +21 -1
  31. package/dist/board/model/text-metrics.js +24 -1
  32. package/dist/board/model/themes.d.ts +15 -8
  33. package/dist/board/model/themes.js +15 -10
  34. package/dist/board/render/index.d.ts +8 -6
  35. package/dist/board/render/index.js +7 -5
  36. package/dist/board/render/labels.d.ts +15 -0
  37. package/dist/board/render/labels.js +14 -0
  38. package/dist/board/render/lod.d.ts +9 -1
  39. package/dist/board/render/lod.js +7 -1
  40. package/dist/board/render/media-interaction.d.ts +5 -2
  41. package/dist/board/render/media-interaction.js +15 -3
  42. package/dist/board/render/renderers/arrow-card-renderer.js +7 -5
  43. package/dist/board/render/renderers/audio-card-renderer.js +1 -1
  44. package/dist/board/render/renderers/board-renderer-registry.d.ts +12 -1
  45. package/dist/board/render/renderers/board-renderer-registry.js +9 -4
  46. package/dist/board/render/renderers/draw-card-renderer.js +1 -1
  47. package/dist/board/render/renderers/far-plate.d.ts +4 -1
  48. package/dist/board/render/renderers/far-plate.js +5 -1
  49. package/dist/board/render/renderers/file-card-renderer.js +46 -26
  50. package/dist/board/render/renderers/frame-card-renderer.js +9 -3
  51. package/dist/board/render/renderers/image-card-renderer.js +2 -2
  52. package/dist/board/render/renderers/model-card-renderer.js +5 -5
  53. package/dist/board/render/renderers/shape-card-renderer.js +1 -1
  54. package/dist/board/render/renderers/task-card-renderer.js +2 -2
  55. package/dist/board/render/renderers/video-card-renderer.js +3 -3
  56. package/dist/board/render/script-export.js +1 -1
  57. package/dist/board/render/script-host.js +3 -3
  58. package/dist/board/render/style.d.ts +1 -0
  59. package/dist/board/render/style.js +3 -1
  60. package/dist/board/render/text-edit.d.ts +32 -0
  61. package/dist/board/render/text-edit.js +134 -0
  62. package/dist/board/render/text-fit.d.ts +1 -3
  63. package/dist/board/render/text-fit.js +4 -43
  64. package/dist/board/render/text-measurement.d.ts +12 -2
  65. package/dist/board/render/text-measurement.js +55 -10
  66. package/dist/board/replica/index.d.ts +4 -2
  67. package/dist/board/replica/index.js +28 -17
  68. package/dist/board/replica/indexeddb.d.ts +16 -0
  69. package/dist/board/replica/indexeddb.js +155 -0
  70. package/dist/board/stage/assets.d.ts +16 -4
  71. package/dist/board/stage/assets.js +155 -30
  72. package/dist/board/stage/bench.d.ts +35 -0
  73. package/dist/board/stage/bench.js +153 -0
  74. package/dist/board/stage/device.d.ts +41 -0
  75. package/dist/board/stage/device.js +62 -0
  76. package/dist/board/stage/frame-budget.d.ts +19 -0
  77. package/dist/board/stage/frame-budget.js +69 -0
  78. package/dist/board/stage/idle.d.ts +4 -0
  79. package/dist/board/stage/idle.js +26 -0
  80. package/dist/board/stage/index.d.ts +9 -5
  81. package/dist/board/stage/index.js +5 -2
  82. package/dist/board/stage/presses.d.ts +21 -0
  83. package/dist/board/stage/presses.js +66 -0
  84. package/dist/board/stage/scene.d.ts +4 -0
  85. package/dist/board/stage/scene.js +45 -13
  86. package/dist/board/stage/stage.d.ts +17 -2
  87. package/dist/board/stage/stage.js +314 -95
  88. package/dist/board/stage/texture-pool.d.ts +6 -4
  89. package/dist/board/stage/texture-pool.js +42 -8
  90. package/dist/chunks/environment.d.ts +6 -0
  91. package/dist/chunks/http.d.ts +9 -1
  92. package/dist/chunks/http.js +791 -4
  93. package/dist/index.d.ts +52 -1
  94. package/dist/index.js +65 -733
  95. package/dist/protocol/dist/board-model.d.ts +15 -1
  96. package/dist/protocol/dist/board-model.js +12 -2
  97. package/dist/protocol/dist/board-patch.js +1 -1
  98. package/dist/protocol/dist/board-schema.d.ts +2 -1
  99. package/dist/protocol/dist/board-schema.js +4 -1
  100. package/dist/protocol/dist/core/media.js +4 -0
  101. package/dist/protocol/dist/index.d.ts +2 -2
  102. package/package.json +2 -1
package/README.md CHANGED
@@ -112,7 +112,7 @@ const created = await space.boards.create({
112
112
  },
113
113
  });
114
114
 
115
- const board = space.board(created.id);
115
+ const board = space.board(created.id); // or await space.boards.open("boards/plan.board")
116
116
  const { items, version } = await board.get({ only: ["items"] });
117
117
  await board.apply({ items: { goal: { props: { text: "Shipped" } } } }, { baseVersion: version });
118
118
 
@@ -159,7 +159,7 @@ change, so they work directly with `useSyncExternalStore`:
159
159
  ```ts
160
160
  import { createBoardRegistry, defineBoardItem, worldPoint } from "@neta-art/cohub/board";
161
161
  import { createBoardEditor } from "@neta-art/cohub/board/editor";
162
- import { createBoardReplica } from "@neta-art/cohub/board/replica";
162
+ import { createBoardReplica, createIndexedDbBoardReplicaStorage } from "@neta-art/cohub/board/replica";
163
163
  import { createBoardAssetManager, mountBoardStage } from "@neta-art/cohub/board/stage";
164
164
  import { z } from "zod";
165
165
 
@@ -172,7 +172,13 @@ const order = defineBoardItem({
172
172
  capabilities: { canRotate: false },
173
173
  });
174
174
 
175
- const replica = createBoardReplica({ remote: space.board(boardId) });
175
+ // Cached documents and unsent writes survive a reload; clear them on sign-out
176
+ // with `clearBoardReplicaStorage({ userKey })`. The replica reads the Board again
177
+ // after a realtime reconnect and resends pending writes when the network returns.
178
+ const replica = createBoardReplica({
179
+ remote: space.board(boardId),
180
+ storage: createIndexedDbBoardReplicaStorage({ userKey, spaceId: space.id, boardId }),
181
+ });
176
182
  await replica.start();
177
183
 
178
184
  const editor = createBoardEditor({
@@ -230,6 +236,74 @@ rewritten by older ones. For a canvas look instead of DOM, pass renderers to
230
236
  `createBoardCardRendererResolver` from `board/render` and hand the result to
231
237
  the stage or to `renderBoardExport` as `renderers`.
232
238
 
239
+ ### Hosting a Board
240
+
241
+ Compose the SDK instead of wrapping it, and each release's loading, level of
242
+ detail and text work reaches your Board without code changes.
243
+
244
+ Pictures your own items show go through `itemAsset`. They share the built-in
245
+ loader: CDN variants sized to the screen, loads ordered by where the view is
246
+ heading, prefetch, compact copies when panned away, device budgets, one cache
247
+ and exports. Call `invalidateItemAssets()` when the data it reads changes:
248
+
249
+ ```ts
250
+ const assets = createBoardAssetManager({
251
+ resolveFileUrl: source.resolveFileUrl,
252
+ // undefined keeps the built-in rule; null shows no picture.
253
+ itemAsset: (item) => {
254
+ if (item.type !== "acme.order") return undefined;
255
+ const cover = covers.get(item.props.orderId as string);
256
+ return cover ? { url: cover, fit: "cover" } : null;
257
+ },
258
+ });
259
+ ```
260
+
261
+ - **Budgets.** Leave `lod`, `prefetch`, `concurrency` and `lruBudget` unset; they
262
+ follow `defaultBoardDeviceBudget()`. Force a tier with
263
+ `boardDeviceBudget(readBoardDeviceSignals(), "low")`.
264
+ - **Canvas cards.** Declare `lod` (`{ kind: "media" }` for pictures) and
265
+ `renderFar`, drawn with `drawFarPlate(graphics, item.frame, { fill:
266
+ farPictureTone(item, context) ?? paper })`. Wrap card text with
267
+ `layoutTextLines(text, font, width, { whiteSpace: "normal", maxLines: 2 })`
268
+ and apply `context.textResolution` in the `resolution` hook.
269
+ - **Opening cards.** Give a type `activation: "tap"` and handle `onActivate`: a
270
+ quick press opens the card without selecting it, a drag moves it (or pans with
271
+ the hand tool or read-only), and hovering shows a pointer instead of ports. A
272
+ press held in place past `BOARD_TAP_MAX_MS` does nothing: it was a pick-up. A
273
+ press on a selected card reports `selected: true`. `onLongPress` fires for a still
274
+ touch or pen; call `editor.pickUp()` there to pick the card up and drag it. The
275
+ stage drops the click a touch screen sends after a tap that opened something.
276
+ - **Card keys.** A renderer's `hitAt(item, point, context)` returns `{ action, cursor? }`
277
+ for keys it draws; the stage shows the cursor, passes the hovered key to renderers as
278
+ `context.hoveredHit` and sends presses to `onHit` (return `false` to let the editor
279
+ have them). Media cards report `BOARD_PLAY_MEDIA` this way.
280
+ - **Previews.** Write `preview: { title, excerpt, cover, src }` on your items so
281
+ clients without your renderer, exports and agents show them as cards. Keep it out
282
+ of undo with `editor.commitPatch(patch, { record: false })`.
283
+ - **Text editing.** Place the in-place editor with `boardTextEditLayout`, so its
284
+ caret sits on the canvas text:
285
+
286
+ ```ts
287
+ const layout = boardTextEditLayout(item, { viewport: editor.camera, scene: editor.scene, style: stage.theme.style, draft });
288
+ // A content-box textarea at layout.left/top, sized layout.width × height, padded by
289
+ // layout.padding, with `rotate(${layout.rotation}deg) scale(${layout.scale})` about its center.
290
+ ```
291
+
292
+ - **Themes.** Start from a built-in theme and override what differs:
293
+ `{ key: "neta-pixel", ...boardRenderTheme("neta-pixel") }`. `style.frameLabelSize`
294
+ sets frame titles; the editor's title hit box and text editor follow it.
295
+ - **Fonts.** Board text measures in `"Geist Variable"`; load
296
+ `@fontsource-variable/geist` so lines match the CLI export.
297
+ - **Agent context.** `onViewStateChange` on the editor reports `visibleRect` and
298
+ `selectedNodes`; send them with a prompt as
299
+ `buildViewportContentBlock([{ kind: "board", path, boardId, visibleRect, selectedNodes }])`
300
+ so the agent knows which Board, where and what the user is looking at.
301
+ - **Measuring.** `runBoardStageBench(stage)` drives the stage with wheel input and
302
+ reports frame and settle times; `stage.stats()` shows live, far and texture counts.
303
+
304
+ Release notes for Board changes end with `Hosts:`, naming what an embedding app
305
+ must change, or `Hosts: no action`.
306
+
233
307
  ### Images
234
308
 
235
309
  In the browser, `renderBoardExport(renderer, document, options)` from
@@ -1,6 +1,6 @@
1
1
  import { parseBoardItem } from "../../protocol/dist/board-model.js";
2
- import { arrowBindings, isArrowBinding } from "../../protocol/dist/board-layout.js";
3
2
  import { BOARD_CLIPBOARD_KIND, BOARD_CLIPBOARD_MIME } from "../../protocol/dist/board.js";
3
+ import { arrowBindings, isArrowBinding } from "../../protocol/dist/board-layout.js";
4
4
  import { createBoardItemId } from "../model/items/id.js";
5
5
  //#region src/board/editor/clipboard.ts
6
6
  /** How far a duplicate or repeated paste lands from its source. */
@@ -1,6 +1,6 @@
1
1
  import { IDENTITY_MATRIX, applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.js";
2
- import { rectCenter } from "../model/geometry.js";
3
2
  import { buildBoardScene } from "../model/scene.js";
3
+ import { rectCenter } from "../model/geometry.js";
4
4
  import { isShapeKind } from "../model/shape-types.js";
5
5
  import { createBoardItemId } from "../model/items/id.js";
6
6
  import { createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTextBoardItem } from "../model/items/create.js";
@@ -15,7 +15,9 @@ declare function createDocumentModule(ctx: EditorContext): {
15
15
  readonly hasScriptItems: boolean;
16
16
  setScriptItems: (items: ReadonlyMap<string, BoardItem>) => void;
17
17
  animationItems: () => Map<string, BoardItem>;
18
- commitPatch: (patch: BoardPatch) => Promise<void> | undefined;
18
+ commitPatch: (patch: BoardPatch, { record }?: {
19
+ record?: boolean;
20
+ }) => Promise<void> | undefined;
19
21
  rescene: (ids?: Iterable<string>) => void;
20
22
  reset: (next: BoardDocument) => void;
21
23
  setDraftItem: (id: string, item: BoardItem | null) => void;
@@ -206,7 +206,7 @@ function createDocumentModule(ctx) {
206
206
  for (const [id, item] of changes) setDraftItem(id, item);
207
207
  commit({}, record);
208
208
  }
209
- function commitPatch(patch) {
209
+ function commitPatch(patch, { record = true } = {}) {
210
210
  if (options.readonly) throw new Error("Board is read-only.");
211
211
  const before = state.base;
212
212
  const result = applyBoardPatchToDocument(before, patch);
@@ -214,11 +214,13 @@ function createDocumentModule(ctx) {
214
214
  const ids = Object.keys(patch.items ?? {});
215
215
  const redo = diffBoardEdits(before, result.document, ids);
216
216
  if (isEmptyBoardPatch(redo)) return;
217
- state.undoStack = [...state.undoStack.slice(-199), {
218
- undo: diffBoardEdits(result.document, before, ids),
219
- redo
220
- }];
221
- state.redoStack = [];
217
+ if (record) {
218
+ state.undoStack = [...state.undoStack.slice(-199), {
219
+ undo: diffBoardEdits(result.document, before, ids),
220
+ redo
221
+ }];
222
+ state.redoStack = [];
223
+ }
222
224
  adopt(result.document, ids);
223
225
  return send(redo).then((error) => {
224
226
  if (error) throw error;
@@ -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, BoardTransformEnd } from "./interaction.js";
14
+ import { BoardActivation, 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";
@@ -136,11 +136,13 @@ declare function createBoardEditor(options: BoardEditorOptions): {
136
136
  setPlayedItems: (items: ReadonlyMap<string, BoardItem>) => void;
137
137
  setScriptItems: (items: ReadonlyMap<string, BoardItem>) => void;
138
138
  animationItems: () => Map<string, BoardItem>;
139
- commitPatch: (patch: BoardPatch) => Promise<void> | undefined;
139
+ commitPatch: (patch: BoardPatch, { record }?: {
140
+ record?: boolean;
141
+ }) => Promise<void> | undefined;
140
142
  consumeRecentlyAdded: () => string[];
141
143
  itemAt: (point: WorldPoint, exclude?: ReadonlySet<string> | undefined, includeTemporary?: boolean | undefined) => BoardSceneItem | null;
142
144
  itemById: (id: string) => BoardSceneItem | null;
143
- labelItemAt: (point: WorldPoint) => BoardSceneItem | null;
145
+ labelItemAt: (point: WorldPoint, frameLabelSize?: number | undefined) => BoardSceneItem | null;
144
146
  idsInRect: (rect: Rect, includeTemporary?: boolean | undefined) => string[];
145
147
  setCamera: (viewport: BoardViewport) => void;
146
148
  setCameraPolicy: (policy: BoardCameraPolicy) => void;
@@ -219,6 +221,8 @@ declare function createBoardEditor(options: BoardEditorOptions): {
219
221
  pointerUp: (event: BoardPointerEvent) => void;
220
222
  pointerLeave: () => void;
221
223
  subscribeTransforms: (listener: (event: BoardTransformEnd) => void) => () => void;
224
+ subscribeActivations: (listener: (event: BoardActivation) => void) => () => void;
225
+ pickUp: () => boolean;
222
226
  cancelPointerInteraction: () => void;
223
227
  destroy(): void;
224
228
  };
@@ -1,8 +1,8 @@
1
1
  import { clampBoardStrokeSize } from "../../protocol/dist/board-constants.js";
2
- import { hasBoardScripts } from "../model/script-index.js";
3
- import { normalizeViewport } from "../model/geometry.js";
4
2
  import { buildBoardScene } from "../model/scene.js";
3
+ import { normalizeViewport } from "../model/geometry.js";
5
4
  import { isBoardColorId } from "../model/palette.js";
5
+ import { hasBoardScripts } from "../model/script-index.js";
6
6
  import { isShapeKind } from "../model/shape-types.js";
7
7
  import { createBoardToolStyles } from "../model/tool-styles.js";
8
8
  import { titleForBoardItem } from "../model/items/create.js";
@@ -438,6 +438,8 @@ function createBoardEditor(options) {
438
438
  pointerUp: interaction.pointerUp,
439
439
  pointerLeave: interaction.pointerLeave,
440
440
  subscribeTransforms: interaction.subscribeTransforms,
441
+ subscribeActivations: interaction.subscribeActivations,
442
+ pickUp: interaction.pickUp,
441
443
  cancelPointerInteraction: interaction.cancel,
442
444
  destroy() {
443
445
  stopViewState();
@@ -2,13 +2,13 @@ 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, BoardTransformEnd } from "./interaction.js";
5
+ import { BoardActivation, 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";
9
9
  import { SnapAxis, SnapGuide, SnapOptions, SnapResult, computeSnap } from "./snapping.js";
10
10
  import { BoardEditorChannel, BoardEditorListener } from "./state.js";
11
- import { BOARD_HAND_TAP_SLOP, BoardToolId, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
11
+ import { BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, BoardToolId, boardTapSlop, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
12
12
  import { BoardCameraPolicy, BoardEditorOptions, BoardEditorPreferences, BoardPlayhead, BoardViewState } from "./context.js";
13
13
  import { CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, ConnectionPort, connectionPortAt, connectionPorts, portHitRadius } from "./connection-ports.js";
14
14
  import { sameJson } from "../model/json.js";
@@ -16,4 +16,4 @@ import { BoardEditPlayhead, diffBoardEdits, isEmptyBoardPatch, readPath, routeBo
16
16
  import { BoardEditor, createBoardEditor } from "./editor.js";
17
17
  import { SpatialEntry, SpatialIndex, createSpatialIndex } from "./spatial.js";
18
18
  import { normalizeWheelDelta, wheelZoomFactor } from "./wheel.js";
19
- 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 };
19
+ export { AlignMode, BOARD_CLIPBOARD_MIME, BOARD_CLIPBOARD_VERSION, BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, type BoardActivation, 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, boardTapSlop, 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 };
@@ -5,9 +5,9 @@ import { diffBoardEdits, isEmptyBoardPatch, readPath, routeBoardEdits, writePath
5
5
  import { normalizeWheelDelta, wheelZoomFactor } from "./wheel.js";
6
6
  import { BOARD_CLIPBOARD_VERSION, DUPLICATE_OFFSET, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, defaultPasteOffset, encodeClipboard, materializeClipboard, parseClipboard, remapItems } from "./clipboard.js";
7
7
  import { CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, connectionPortAt, connectionPorts, portHitRadius } from "./connection-ports.js";
8
- import { BOARD_HAND_TAP_SLOP, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
8
+ import { BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, boardTapSlop, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
9
9
  import { resizeCursorForHandle, resolveSelectionTransform, selectionTransformControlAt } from "./selection-transform.js";
10
10
  import { computeSnap } from "./snapping.js";
11
11
  import { createSpatialIndex } from "./spatial.js";
12
12
  import { createBoardEditor } from "./editor.js";
13
- export { BOARD_CLIPBOARD_MIME, BOARD_CLIPBOARD_VERSION, BOARD_HAND_TAP_SLOP, CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, DUPLICATE_OFFSET, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, 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 };
13
+ export { BOARD_CLIPBOARD_MIME, BOARD_CLIPBOARD_VERSION, BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, CONNECTION_PORT_HIT_RADIUS, CONNECTION_PORT_OFFSET, CONNECTION_PORT_RADIUS, CONNECTION_PORT_TOUCH_HIT_RADIUS, CONNECTION_SIDES, DUPLICATE_OFFSET, MAX_CLIPBOARD_CHARS, MAX_CLIPBOARD_ITEMS, alignFrames, boardTapSlop, 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 };
@@ -6,9 +6,19 @@ import "../model/index.js";
6
6
  import { BoardTransformControl } from "./selection-transform.js";
7
7
  import { EditorContext } from "./context.js";
8
8
  //#region src/board/editor/interaction.d.ts
9
+ type BoardPress = {
10
+ type: "pressing";
11
+ itemId: string;
12
+ pointerId: number;
13
+ pointerType: string;
14
+ start: ScreenPoint;
15
+ world: WorldPoint;
16
+ camera: BoardViewport;
17
+ at: number;
18
+ };
9
19
  type BoardInteraction = {
10
20
  type: "idle";
11
- } | {
21
+ } | BoardPress | {
12
22
  type: "panning";
13
23
  start: ScreenPoint;
14
24
  origin: BoardViewport;
@@ -22,6 +32,10 @@ type BoardInteraction = {
22
32
  origin: Map<string, BoardFrame>;
23
33
  moved: boolean;
24
34
  duplicate: boolean;
35
+ tap?: {
36
+ itemId: string;
37
+ at: number;
38
+ };
25
39
  } | {
26
40
  type: "resizing";
27
41
  handle: ResizeHandle;
@@ -94,6 +108,11 @@ type BoardPointerEvent = {
94
108
  pressure: number;
95
109
  samples?: readonly BoardDrawInputSample[];
96
110
  };
111
+ type BoardActivation = {
112
+ itemId: string;
113
+ pointerType: string;
114
+ selected: boolean;
115
+ };
97
116
  type BoardTransformEnd = {
98
117
  ids: string[];
99
118
  operation: "move" | "resize" | "rotate" | "arrow";
@@ -117,9 +136,11 @@ declare function createInteractionModule(ctx: EditorContext): {
117
136
  pointerUp: (event: BoardPointerEvent) => void;
118
137
  pointerLeave: () => void;
119
138
  cancel: () => void;
139
+ pickUp: () => boolean;
120
140
  subscribeTransforms(listener: (event: BoardTransformEnd) => void): () => void;
141
+ subscribeActivations(listener: (event: BoardActivation) => void): () => void;
121
142
  destroy(): void;
122
143
  };
123
144
  type InteractionModule = ReturnType<typeof createInteractionModule>;
124
145
  //#endregion
125
- export { BoardInteraction, BoardPointerEvent, BoardTransformEnd, InteractionModule, createInteractionModule };
146
+ export { BoardActivation, BoardInteraction, BoardPointerEvent, BoardTransformEnd, InteractionModule, createInteractionModule };
@@ -3,7 +3,7 @@ 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";
5
5
  import { connectionPortAt, connectionPorts } from "./connection-ports.js";
6
- import { canTapSelectWithHand, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
6
+ import { boardTapSlop, canTapSelectWithHand, isCreationBoardTool, isWithinHandTapSlop } from "./tool.js";
7
7
  import { selectionTransformControlAt } from "./selection-transform.js";
8
8
  import { computeSnap } from "./snapping.js";
9
9
  import { memo } from "./state.js";
@@ -33,6 +33,15 @@ function createInteractionModule(ctx) {
33
33
  for (const listener of transformListeners) listener(event);
34
34
  }
35
35
  const { state, options, registry } = ctx;
36
+ const activationListeners = /* @__PURE__ */ new Set();
37
+ function activate(itemId, at, pointerType, selected) {
38
+ if (performance.now() - at > 300 || !state.scene.get(itemId)) return;
39
+ for (const listener of activationListeners) listener({
40
+ itemId,
41
+ pointerType,
42
+ selected
43
+ });
44
+ }
36
45
  const activePointers = /* @__PURE__ */ new Map();
37
46
  let pinch = null;
38
47
  const marquee = memo(() => [state.interaction], (interaction) => {
@@ -106,6 +115,7 @@ function createInteractionModule(ctx) {
106
115
  if (!candidateId) return null;
107
116
  const item = scene.get(candidateId);
108
117
  if (!item || ctx.query.isLocked(item) || item.type === "arrow") return null;
118
+ if (!selection.includes(item.id) && registry.activation(item) === "tap") return null;
109
119
  return registry.capabilities(item).canConnect ? item.id : null;
110
120
  }
111
121
  function visiblePorts() {
@@ -160,6 +170,66 @@ function createInteractionModule(ctx) {
160
170
  size: arrow.size
161
171
  };
162
172
  }
173
+ function plainPrimary(event) {
174
+ return event.button === 0 && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey;
175
+ }
176
+ function press(event) {
177
+ if (!plainPrimary(event)) return false;
178
+ const item = ctx.query.itemAt(event.world);
179
+ if (!item || state.selection.includes(item.id) || registry.activation(item) !== "tap") return false;
180
+ state.interaction = {
181
+ type: "pressing",
182
+ itemId: item.id,
183
+ pointerId: event.pointerId,
184
+ pointerType: event.pointerType,
185
+ start: event.screen,
186
+ world: event.world,
187
+ camera: { ...state.camera },
188
+ at: performance.now()
189
+ };
190
+ return true;
191
+ }
192
+ function releasePress(gesture, event) {
193
+ const dx = event.screen.x - gesture.start.x;
194
+ const dy = event.screen.y - gesture.start.y;
195
+ if (Math.hypot(dx, dy) <= boardTapSlop(gesture.pointerType)) return;
196
+ const item = state.scene.get(gesture.itemId);
197
+ if (!(!options.readonly && state.tool === "select" && item && !ctx.query.isLocked(item) && registry.capabilities(item).canMove)) {
198
+ state.interaction = {
199
+ type: "panning",
200
+ start: gesture.start,
201
+ origin: gesture.camera,
202
+ moved: true,
203
+ tapSelection: null
204
+ };
205
+ ctx.camera.setCamera(panBy(gesture.camera, dx, dy));
206
+ return;
207
+ }
208
+ const translating = {
209
+ type: "translating",
210
+ start: gesture.world,
211
+ origin: ctx.selection.framesFor([gesture.itemId]),
212
+ moved: false,
213
+ duplicate: false
214
+ };
215
+ state.interaction = translating;
216
+ translate(translating, event);
217
+ }
218
+ function pickUp() {
219
+ const gesture = state.interaction;
220
+ if (options.readonly || gesture.type !== "pressing") return false;
221
+ const item = state.scene.get(gesture.itemId);
222
+ if (!item || ctx.query.isLocked(item) || !registry.capabilities(item).canMove) return false;
223
+ state.selection = [item.id];
224
+ state.interaction = {
225
+ type: "translating",
226
+ start: state.hoverPoint ?? gesture.world,
227
+ origin: ctx.selection.framesFor([item.id]),
228
+ moved: false,
229
+ duplicate: false
230
+ };
231
+ return true;
232
+ }
163
233
  function pointerDown(event) {
164
234
  ctx.camera.cancelAnimation();
165
235
  state.hoverPoint = event.world;
@@ -173,6 +243,7 @@ function createInteractionModule(ctx) {
173
243
  }
174
244
  if (activePointers.size > 2) return;
175
245
  const additive = event.shiftKey || event.metaKey || event.ctrlKey;
246
+ if (!spaceHeld && (tool === "hand" || options.readonly) && press(event)) return;
176
247
  if (tool === "hand" || spaceHeld || event.button === 1) {
177
248
  const tapSelection = tool === "hand" && !spaceHeld && event.button !== 1 && canTapSelectWithHand(event.pointerType) ? { targetId: ctx.query.itemAt(event.world)?.id ?? null } : null;
178
249
  state.interaction = {
@@ -278,17 +349,23 @@ function createInteractionModule(ctx) {
278
349
  };
279
350
  return;
280
351
  }
352
+ if (press(event)) return;
281
353
  const item = ctx.query.itemAt(event.world);
282
354
  if (!item) return brush(event, additive);
283
355
  if (additive) ctx.selection.toggle(item.id);
284
356
  else if (!state.selection.includes(item.id)) state.selection = [item.id];
285
357
  const movable = ctx.selection.movable();
358
+ const tap = plainPrimary(event) && state.selection.length === 1 && registry.activation(item) === "tap" ? {
359
+ itemId: item.id,
360
+ at: performance.now()
361
+ } : void 0;
286
362
  state.interaction = movable.length === 0 ? { type: "idle" } : {
287
363
  type: "translating",
288
364
  start: event.world,
289
365
  origin: ctx.selection.framesFor(movable),
290
366
  moved: false,
291
- duplicate: event.altKey
367
+ duplicate: event.altKey,
368
+ ...tap ? { tap } : {}
292
369
  };
293
370
  }
294
371
  function pinchMove() {
@@ -472,6 +549,7 @@ function createInteractionModule(ctx) {
472
549
  ctx.camera.setCamera(panBy(interaction.origin, dx, dy));
473
550
  return;
474
551
  }
552
+ case "pressing": return releasePress(interaction, event);
475
553
  case "translating": return translate(interaction, event);
476
554
  case "resizing": return resize(interaction, event);
477
555
  case "rotating": return rotate(interaction, event);
@@ -532,11 +610,14 @@ function createInteractionModule(ctx) {
532
610
  state.selection = targetId && state.scene.get(targetId) ? [targetId] : [];
533
611
  }
534
612
  break;
613
+ case "pressing":
614
+ activate(gesture.itemId, gesture.at, gesture.pointerType, false);
615
+ break;
535
616
  case "translating":
536
617
  if (gesture.moved) {
537
- for (const [id, item] of reparentDropped(ctx.query.rootsOf(state.selection))) ctx.document.setDraftItem(id, item);
618
+ for (const [id, item] of reparentDropped([...gesture.origin.keys()])) ctx.document.setDraftItem(id, item);
538
619
  ctx.document.commit();
539
- }
620
+ } else if (gesture.tap) activate(gesture.tap.itemId, gesture.tap.at, event.pointerType, true);
540
621
  break;
541
622
  case "resizing":
542
623
  case "rotating":
@@ -610,15 +691,23 @@ function createInteractionModule(ctx) {
610
691
  pointerUp,
611
692
  pointerLeave,
612
693
  cancel,
694
+ pickUp,
613
695
  subscribeTransforms(listener) {
614
696
  transformListeners.add(listener);
615
697
  return () => {
616
698
  transformListeners.delete(listener);
617
699
  };
618
700
  },
701
+ subscribeActivations(listener) {
702
+ activationListeners.add(listener);
703
+ return () => {
704
+ activationListeners.delete(listener);
705
+ };
706
+ },
619
707
  destroy() {
620
708
  activePointers.clear();
621
709
  transformListeners.clear();
710
+ activationListeners.clear();
622
711
  }
623
712
  };
624
713
  }
@@ -18,7 +18,7 @@ declare function createQueryModule(ctx: EditorContext): {
18
18
  idsInRect: (rect: Rect, includeTemporary?: boolean) => string[];
19
19
  frameAt: (point: WorldPoint, exclude?: ReadonlySet<string>) => BoardSceneItem | null;
20
20
  itemAt: (point: WorldPoint, exclude?: ReadonlySet<string>, includeTemporary?: boolean) => BoardSceneItem | null;
21
- labelItemAt: (point: WorldPoint) => BoardSceneItem | null;
21
+ labelItemAt: (point: WorldPoint, frameLabelSize?: number) => BoardSceneItem | null;
22
22
  arrowHandleAt: (point: WorldPoint) => "start" | "end" | "mid" | null;
23
23
  connectTargetAt: (point: WorldPoint, exclude?: ReadonlySet<string>) => string | null;
24
24
  };
@@ -1,6 +1,7 @@
1
1
  import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
2
2
  import { rectContainsPoint, unionRects } from "../model/geometry.js";
3
3
  import { arrowHitRadius, arrowPathBounds, distanceToArrow, resolveSceneArrow } from "../model/arrow-geometry.js";
4
+ import { frameLabelHitRect } from "../model/text-metrics.js";
4
5
  import { createSpatialIndex } from "./spatial.js";
5
6
  //#region src/board/editor/query.ts
6
7
  const NO_IDS = /* @__PURE__ */ new Set();
@@ -139,7 +140,7 @@ function createQueryModule(ctx) {
139
140
  }
140
141
  return null;
141
142
  }
142
- function labelItemAt(point) {
143
+ function labelItemAt(point, frameLabelSize = 12) {
143
144
  ensureSpatial();
144
145
  const { scene, selection, camera } = state;
145
146
  const nearby = spatial.idsAtPoint(point);
@@ -147,12 +148,7 @@ function createQueryModule(ctx) {
147
148
  for (const id of new Set(candidates)) {
148
149
  const item = scene.get(id);
149
150
  if (!item || !state.base.items[id] || isLocked(item)) continue;
150
- if (item.type === "frame" && rectContainsPoint({
151
- x: item.frame.x,
152
- y: item.frame.y - 22,
153
- width: Math.max(48, item.frame.width * .5),
154
- height: 24
155
- }, point)) return item;
151
+ if (item.type === "frame" && rectContainsPoint(frameLabelHitRect(item.frame, frameLabelSize), point)) return item;
156
152
  if (item.type === "arrow") {
157
153
  const resolved = resolveSceneArrow(item, scene);
158
154
  const radius = Math.max(10, (item.props.fontSize ?? 14) * .9) / Math.min(1, camera.zoom);
@@ -1,5 +1,5 @@
1
- import { selectionBounds } from "../model/geometry.js";
2
1
  import { sceneItemToItem } from "../model/scene.js";
2
+ import { selectionBounds } from "../model/geometry.js";
3
3
  import { alignFrames, distributeFrames } from "./align.js";
4
4
  import { resolveSelectionTransform } from "./selection-transform.js";
5
5
  import { memo } from "./state.js";
@@ -1,10 +1,13 @@
1
1
  //#region src/board/editor/tool.d.ts
2
2
  type BoardToolId = "select" | "hand" | "text" | "shape" | "draw" | "arrow" | "frame";
3
3
  declare const BOARD_HAND_TAP_SLOP = 8;
4
+ declare const BOARD_MOUSE_TAP_SLOP = 4;
5
+ declare const BOARD_TAP_MAX_MS = 300;
6
+ declare function boardTapSlop(pointerType: string): number;
4
7
  declare function defaultBoardTool(isMobile: boolean): BoardToolId;
5
8
  declare function canTapSelectWithHand(pointerType: string): boolean;
6
9
  declare function isWithinHandTapSlop(dx: number, dy: number): boolean;
7
10
  declare function isContinuousBoardTool(tool: BoardToolId): boolean;
8
11
  declare function isCreationBoardTool(tool: BoardToolId): boolean;
9
12
  //#endregion
10
- export { BOARD_HAND_TAP_SLOP, BoardToolId, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop };
13
+ export { BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, BoardToolId, boardTapSlop, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop };
@@ -1,5 +1,10 @@
1
1
  //#region src/board/editor/tool.ts
2
2
  const BOARD_HAND_TAP_SLOP = 8;
3
+ const BOARD_MOUSE_TAP_SLOP = 4;
4
+ const BOARD_TAP_MAX_MS = 300;
5
+ function boardTapSlop(pointerType) {
6
+ return pointerType === "mouse" ? 4 : 8;
7
+ }
3
8
  function defaultBoardTool(isMobile) {
4
9
  return isMobile ? "hand" : "select";
5
10
  }
@@ -16,4 +21,4 @@ function isCreationBoardTool(tool) {
16
21
  return tool === "text" || tool === "shape" || tool === "draw" || tool === "arrow" || tool === "frame";
17
22
  }
18
23
  //#endregion
19
- export { BOARD_HAND_TAP_SLOP, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop };
24
+ export { BOARD_HAND_TAP_SLOP, BOARD_MOUSE_TAP_SLOP, BOARD_TAP_MAX_MS, boardTapSlop, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop };
@@ -1,9 +1,9 @@
1
1
  import { buildBoardScene } from "../model/scene.js";
2
2
  import { resolveBoardAnimationCamera } from "../model/sampling.js";
3
- import { normalizeBoardDocument, planBoardExport } from "../model/export-plan.js";
4
3
  import { parseBoardCssColor } from "../render/css-color.js";
5
4
  import { defaultBoardPalette } from "../render/palette.js";
6
5
  import { createBoardRenderSampler } from "../render/sampling.js";
6
+ import { normalizeBoardDocument, planBoardExport } from "../model/export-plan.js";
7
7
  import { ensureBoardTextMeasurement } from "../render/text-measurement.js";
8
8
  import { createBoardExportScene } from "./scene.js";
9
9
  import { Rectangle, RendererType } from "pixi.js";
@@ -1,5 +1,5 @@
1
- import { boardScriptScope, composeBoardScriptItems } from "../../model/script-scene.js";
2
1
  import { buildBoardScene } from "../../model/scene.js";
2
+ import { boardScriptScope, composeBoardScriptItems } from "../../model/script-scene.js";
3
3
  import { createBoardScriptModuleReader, loadBoardScriptSources } from "../../../protocol/dist/board-script-modules.js";
4
4
  import { createScriptRuntime } from "../../render/script-runtime.js";
5
5
  import { initializeBoardFonts } from "./fonts.js";
@@ -1,9 +1,9 @@
1
- import { boardAssetKey } from "../model/asset-key.js";
2
1
  import { buildFallbackShapeColors } from "../model/palette.js";
3
2
  import { defaultBoardPalette } from "../render/palette.js";
4
3
  import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
5
4
  import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
6
5
  import { resolveBoardRenderStyle } from "../render/style.js";
6
+ import { boardAssetKey } from "../model/asset-key.js";
7
7
  import { Container, Graphics, Sprite, TilingSprite } from "pixi.js";
8
8
  //#region src/board/export/scene.ts
9
9
  function buildContext(input) {
@@ -5,7 +5,15 @@ type BoardAssetKeySource = {
5
5
  kind: "file" | "url";
6
6
  media: BoardAssetMedia;
7
7
  value: string;
8
+ fit?: "cover";
8
9
  };
10
+ /** `fit` applies to images: `cover` bounds the short edge instead of the long one. */
11
+ type BoardAssetRef = {
12
+ url: string;
13
+ media?: "image" | "video";
14
+ fit?: "contain" | "cover";
15
+ };
16
+ declare function boardAssetRefKey({ url, media, fit }: BoardAssetRef): string;
9
17
  declare function imageAssetKey(item: BoardSceneItem): string | null;
10
18
  declare function boardAssetKey(item: BoardSceneItem): string | null;
11
19
  declare function boardAssetKeySource(key: string): BoardAssetKeySource | null;
@@ -13,4 +21,4 @@ declare function boardTexturePixels(item: BoardSceneItem, scale: number): number
13
21
  declare const BOARD_THUMBNAIL_EDGE = 64;
14
22
  declare function boardTextureEdge(item: BoardSceneItem, scale: number): number;
15
23
  //#endregion
16
- export { BOARD_THUMBNAIL_EDGE, BoardAssetKeySource, BoardAssetMedia, boardAssetKey, boardAssetKeySource, boardTextureEdge, boardTexturePixels, imageAssetKey };
24
+ export { BOARD_THUMBNAIL_EDGE, BoardAssetKeySource, BoardAssetMedia, BoardAssetRef, boardAssetKey, boardAssetKeySource, boardAssetRefKey, boardTextureEdge, boardTexturePixels, imageAssetKey };