@neta-art/cohub 11.0.0 → 11.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 (71) hide show
  1. package/README.md +31 -5
  2. package/dist/board/editor/creation.d.ts +2 -2
  3. package/dist/board/editor/creation.js +3 -3
  4. package/dist/board/editor/editor.d.ts +4 -4
  5. package/dist/board/editor/editor.js +1 -1
  6. package/dist/board/editor/index.d.ts +2 -2
  7. package/dist/board/editor/interaction.d.ts +8 -2
  8. package/dist/board/editor/interaction.js +34 -1
  9. package/dist/board/editor/snapshots.d.ts +0 -2
  10. package/dist/board/editor/snapshots.js +4 -20
  11. package/dist/board/export/scene.js +4 -1
  12. package/dist/board/model/image-key.js +1 -1
  13. package/dist/board/model/index.d.ts +8 -7
  14. package/dist/board/model/index.js +6 -5
  15. package/dist/board/model/items/create.d.ts +16 -3
  16. package/dist/board/model/items/create.js +125 -5
  17. package/dist/board/model/items/index.d.ts +2 -2
  18. package/dist/board/model/items/index.js +2 -2
  19. package/dist/board/model/media-playback.js +1 -1
  20. package/dist/board/model/overlay-placement.d.ts +25 -0
  21. package/dist/board/model/overlay-placement.js +35 -0
  22. package/dist/board/model/replay.d.ts +1 -1
  23. package/dist/board/model/replay.js +1 -0
  24. package/dist/board/model/script-hit.d.ts +20 -3
  25. package/dist/board/model/script-hit.js +81 -24
  26. package/dist/board/model/script-observers.d.ts +18 -0
  27. package/dist/board/model/script-observers.js +92 -0
  28. package/dist/board/model/script-scene.d.ts +1 -1
  29. package/dist/board/model/script-scene.js +35 -8
  30. package/dist/board/model/script.d.ts +51 -7
  31. package/dist/board/render/board-background.js +5 -4
  32. package/dist/board/render/index.d.ts +2 -2
  33. package/dist/board/render/index.js +2 -2
  34. package/dist/board/render/media-interaction.js +1 -1
  35. package/dist/board/render/renderers/board-renderer-registry.d.ts +17 -2
  36. package/dist/board/render/renderers/board-renderer-registry.js +8 -1
  37. package/dist/board/render/renderers/custom-card-renderer.js +2 -1
  38. package/dist/board/render/renderers/task-card-renderer.d.ts +2 -1
  39. package/dist/board/render/renderers/task-card-renderer.js +220 -71
  40. package/dist/board/render/script-host.d.ts +1 -1
  41. package/dist/board/render/script-host.js +53 -28
  42. package/dist/board/render/script-runtime.js +13 -3
  43. package/dist/board/render/script-texture.d.ts +11 -0
  44. package/dist/board/render/script-texture.js +33 -0
  45. package/dist/board/stage/assets.js +1 -1
  46. package/dist/board/stage/scene.d.ts +2 -0
  47. package/dist/board/stage/scene.js +17 -1
  48. package/dist/board/stage/script-pointer.d.ts +1 -0
  49. package/dist/board/stage/script-pointer.js +8 -2
  50. package/dist/board/stage/stage.d.ts +11 -3
  51. package/dist/board/stage/stage.js +118 -21
  52. package/dist/chunks/environment.d.ts +40 -5
  53. package/dist/chunks/environment.js +12 -4
  54. package/dist/chunks/http.d.ts +3 -1
  55. package/dist/chunks/http.js +2 -1
  56. package/dist/chunks/transport.js +1 -1
  57. package/dist/chunks/websocket.d.ts +1 -1
  58. package/dist/http.d.ts +2 -2
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.js +151 -139
  61. package/dist/protocol/dist/board-model.d.ts +36 -1
  62. package/dist/protocol/dist/board-model.js +14 -4
  63. package/dist/protocol/dist/board-task.d.ts +22 -0
  64. package/dist/{board/model/task.js → protocol/dist/board-task.js} +34 -7
  65. package/dist/protocol/dist/generation/blocks.d.ts +8 -0
  66. package/dist/{generation-blocks.js → protocol/dist/generation/blocks.js} +14 -15
  67. package/dist/protocol/dist/index.d.ts +5 -2
  68. package/dist/protocol/dist/task/index.d.ts +4 -0
  69. package/package.json +1 -1
  70. package/dist/board/model/task.d.ts +0 -11
  71. package/dist/types.d.ts +0 -36
@@ -1,17 +1,21 @@
1
1
  import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
- import { featuredTaskArtifact } from "../../model/task.js";
2
+ import { featuredTaskArtifact } from "../../../protocol/dist/board-task.js";
3
3
  import { drawAudioWaveform } from "../audio-waveform.js";
4
4
  import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
5
5
  import { positionShell } from "./base-card-renderer.js";
6
6
  import { drawFarPlate } from "./far-plate.js";
7
7
  import { fitTextToLines } from "./file-card-renderer.js";
8
- import { Container, Graphics, Sprite, Text } from "pixi.js";
8
+ import { Container, FillGradient, Graphics, Sprite, Text } from "pixi.js";
9
9
  //#region src/board/render/renderers/task-card-renderer.ts
10
10
  const RADIUS = 4;
11
11
  const PADDING = 12;
12
12
  const META_HEIGHT = 28;
13
13
  const TASK_CARD_FULL_DETAIL_ZOOM = .55;
14
14
  const PLAY_BADGE_RADIUS = 15;
15
+ const SHIMMER_PERIOD_MS = 1600;
16
+ const SHIMMER_FRAME_MS = 33;
17
+ const REVEAL_MS = 180;
18
+ const SECOND = 1e3;
15
19
  const partsByContainer = /* @__PURE__ */ new WeakMap();
16
20
  function previewKindFor(artifact) {
17
21
  if (!artifact) return "empty";
@@ -19,36 +23,51 @@ function previewKindFor(artifact) {
19
23
  if (artifact.type === "audio") return artifact.previewUrl ? "texture" : "waveform";
20
24
  return artifact.textExcerpt ? "text" : "empty";
21
25
  }
22
- function stateSurfaceFor(item, kind, hasTexture, previewFailed) {
26
+ function liveStatus(item, context) {
27
+ const { snapshot } = item.props;
28
+ const live = snapshot.status === "completed" ? null : context.taskRun(item.props.taskRunId);
29
+ const liveStart = Date.parse(live?.startedAt ?? "");
30
+ const working = live?.status === "pending" || live?.status === "running";
31
+ const adopt = snapshot.status === "failed" ? working && liveStart > Date.parse(snapshot.updatedAt ?? "") : live?.status === "running" || live?.status === "pending" && snapshot.status === "pending";
32
+ const status = adopt && live ? live.status : snapshot.status;
33
+ const started = adopt ? liveStart : Date.parse(snapshot.startedAt ?? "");
34
+ return {
35
+ status,
36
+ startedAt: Number.isFinite(started) ? started : null
37
+ };
38
+ }
39
+ function stateSurfaceFor(status, kind, hasTexture, previewFailed) {
23
40
  if (kind === "texture" && !hasTexture) {
24
- if (item.props.snapshot.status === "failed") return "failed";
41
+ if (status === "failed") return "failed";
25
42
  return previewFailed ? "unavailable" : "media-loading";
26
43
  }
27
44
  if (kind !== "empty") return null;
28
- if (item.props.snapshot.status === "failed") return "failed";
29
- if (item.props.snapshot.status === "running") return "generating";
30
- if (item.props.snapshot.status === "pending") return "queued";
45
+ if (status === "failed") return "failed";
46
+ if (status === "running") return "generating";
47
+ if (status === "pending") return "queued";
31
48
  return "empty";
32
49
  }
33
- function stateLabel(surface) {
50
+ function stateLabel(surface, context) {
34
51
  switch (surface) {
35
- case "generating": return "Generating";
36
- case "queued": return "Queued";
37
- case "failed": return "Failed";
38
- case "unavailable": return "Preview unavailable";
39
- case "empty": return "No preview";
52
+ case "generating": return context.labels.generating;
53
+ case "queued": return context.labels.queued;
54
+ case "failed": return context.labels.failed;
55
+ case "unavailable": return context.labels.previewUnavailable;
56
+ case "empty": return context.labels.noPreview;
40
57
  default: return "";
41
58
  }
42
59
  }
43
- function statusColor(item, context) {
44
- if (item.props.snapshot.status === "failed") return context.colors.rose.stroke;
45
- if (item.props.snapshot.status === "running") return context.colors.brand.stroke;
60
+ function statusColor(status, context) {
61
+ if (status === "failed") return context.colors.rose.stroke;
62
+ if (status === "running") return context.colors.brand.stroke;
46
63
  return context.colors.neutral.stroke;
47
64
  }
48
- function metadata(item) {
49
- const { artifactCount, artifacts } = item.props.snapshot;
50
- const extra = artifactCount > artifacts.length ? `${artifacts.length}/${artifactCount}` : artifactCount > 1 ? `+${artifactCount - 1}` : null;
51
- return [item.props.snapshot.model, extra].filter(Boolean).join(" · ");
65
+ function formatTaskElapsed(ms) {
66
+ const total = Math.max(0, Math.floor(ms / SECOND));
67
+ const hours = Math.floor(total / 3600);
68
+ const minutes = Math.floor(total % 3600 / 60);
69
+ const seconds = String(total % 60).padStart(2, "0");
70
+ return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
52
71
  }
53
72
  function containTaskPreviewRect(width, height, imageWidth, imageHeight) {
54
73
  if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return {
@@ -150,15 +169,83 @@ function drawFailedBadge(graphics, width, color, context) {
150
169
  cap: "round"
151
170
  });
152
171
  }
172
+ const shimmerGradients = /* @__PURE__ */ new Map();
173
+ function shimmerGradient(color) {
174
+ let gradient = shimmerGradients.get(color);
175
+ if (!gradient) {
176
+ const rgb = `${color >> 16 & 255}, ${color >> 8 & 255}, ${color & 255}`;
177
+ gradient = new FillGradient({
178
+ type: "linear",
179
+ start: {
180
+ x: 0,
181
+ y: 0
182
+ },
183
+ end: {
184
+ x: 1,
185
+ y: 0
186
+ },
187
+ textureSpace: "local",
188
+ colorStops: [
189
+ {
190
+ offset: 0,
191
+ color: `rgba(${rgb}, 0)`
192
+ },
193
+ {
194
+ offset: .5,
195
+ color: `rgba(${rgb}, 0.07)`
196
+ },
197
+ {
198
+ offset: 1,
199
+ color: `rgba(${rgb}, 0)`
200
+ }
201
+ ]
202
+ });
203
+ shimmerGradients.set(color, gradient);
204
+ }
205
+ return gradient;
206
+ }
207
+ function drawShimmerBand(graphics, width, height, color) {
208
+ const band = Math.max(48, width * .45);
209
+ graphics.clear().rect(-band, 0, band, height).fill(shimmerGradient(color));
210
+ }
211
+ function advance(parts, clock) {
212
+ const { motion } = parts;
213
+ let next = null;
214
+ if (motion.shimmer) {
215
+ const travel = motion.width + parts.shimmer.width;
216
+ parts.shimmer.x = clock % SHIMMER_PERIOD_MS / SHIMMER_PERIOD_MS * travel;
217
+ next = SHIMMER_FRAME_MS;
218
+ }
219
+ if (motion.startedAt !== null) {
220
+ const elapsedMs = Date.now() - motion.startedAt;
221
+ const elapsed = formatTaskElapsed(elapsedMs);
222
+ if (elapsed !== motion.elapsed) {
223
+ motion.elapsed = elapsed;
224
+ parts.body.text = `${motion.label} · ${elapsed}`;
225
+ }
226
+ const untilNextSecond = SECOND - Math.max(0, elapsedMs) % SECOND;
227
+ next = next === null ? untilNextSecond : Math.min(next, untilNextSecond);
228
+ }
229
+ if (motion.revealFrom !== null) {
230
+ const progress = Math.min(1, (clock - motion.revealFrom) / REVEAL_MS);
231
+ parts.preview.alpha = progress * (2 - progress);
232
+ if (progress >= 1) motion.revealFrom = null;
233
+ else next = 0;
234
+ }
235
+ return next;
236
+ }
153
237
  function sync(container, item, context) {
154
238
  const parts = partsByContainer.get(container);
155
239
  if (!parts) return;
156
240
  positionShell(parts.root, item);
157
241
  const { width, height } = item.frame;
242
+ const { snapshot } = item.props;
243
+ const { status, startedAt } = liveStatus(item, context);
158
244
  const selected = context.selectedIds.has(item.id);
159
245
  const hovered = context.hoveredId === item.id;
160
246
  const full = context.zoom >= TASK_CARD_FULL_DETAIL_ZOOM;
161
- const color = statusColor(item, context);
247
+ const working = status === "pending" || status === "running";
248
+ const color = statusColor(status, context);
162
249
  const key = context.assetKey(item);
163
250
  if (key !== parts.assetKey) {
164
251
  if (parts.assetKey) context.releaseTexture(parts.assetKey);
@@ -167,20 +254,58 @@ function sync(container, item, context) {
167
254
  }
168
255
  const texture = key ? context.getTexture(key) : null;
169
256
  const previewFailed = Boolean(key && !texture && context.hasError(key));
170
- const artifact = featuredTaskArtifact(item.props.snapshot.artifacts);
257
+ const artifact = featuredTaskArtifact(snapshot.artifacts);
171
258
  const kind = previewKindFor(artifact);
172
- const surface = stateSurfaceFor(item, kind, Boolean(texture), previewFailed);
259
+ const surface = stateSurfaceFor(status, kind, Boolean(texture), previewFailed);
173
260
  const frame = {
174
261
  x: 1,
175
262
  y: 1,
176
263
  width: Math.max(1, width - 2),
177
264
  height: Math.max(1, height - 2)
178
265
  };
179
- const metaText = artifact ? metadata(item) : "";
266
+ const metaText = artifact || working ? snapshot.model ?? "" : "";
180
267
  const showMeta = full && Boolean(metaText);
181
- const failedWithOutput = item.props.snapshot.status === "failed" && kind !== "empty";
268
+ const failedWithOutput = status === "failed" && kind !== "empty";
269
+ const count = artifact && snapshot.artifactCount > 1 ? String(snapshot.artifactCount) : "";
270
+ const showBadge = full && Boolean(count);
271
+ const label = stateLabel(surface, context);
272
+ const counting = full && working && startedAt !== null && surface !== null;
273
+ const elapsed = counting ? formatTaskElapsed(Date.now() - startedAt) : "";
274
+ const detailText = !full || !surface ? "" : status === "failed" ? snapshot.errorExcerpt ?? "" : working ? snapshot.promptExcerpt ?? "" : "";
275
+ const textArtifact = kind === "text" && artifact?.type === "text";
276
+ const bodyText = textArtifact ? artifact.textExcerpt : counting ? `${label} · ${elapsed}` : label;
182
277
  syncTextResolution(parts.body, parts, context.zoom);
278
+ syncTextResolution(parts.detail, parts, context.zoom);
183
279
  syncTextResolution(parts.meta, parts, context.zoom);
280
+ syncTextResolution(parts.badge, parts, context.zoom);
281
+ const textSig = [
282
+ counting ? label : bodyText,
283
+ detailText,
284
+ metaText,
285
+ count,
286
+ kind,
287
+ surface,
288
+ full,
289
+ width,
290
+ height,
291
+ color,
292
+ context.palette.text,
293
+ context.palette.muted
294
+ ].join("|");
295
+ if (textSig !== parts.textSig) {
296
+ parts.textSig = textSig;
297
+ const wrap = Math.max(24, width - 24);
298
+ parts.body.style.fill = surface === "failed" ? color : context.palette.text;
299
+ parts.body.anchor.set(textArtifact ? 0 : .5, 0);
300
+ fitTextToLines(parts.body, bodyText, textArtifact ? Math.max(1, Math.floor((height - 24 - (showMeta ? META_HEIGHT : 0)) / 16)) : 1, wrap);
301
+ parts.detail.style.fill = context.palette.muted;
302
+ fitTextToLines(parts.detail, detailText, height - 24 > 120 ? 2 : 1, Math.min(wrap, 360));
303
+ parts.meta.style.fill = context.palette.text;
304
+ fitTextToLines(parts.meta, metaText, 1, wrap);
305
+ parts.badge.style.fill = context.palette.text;
306
+ if (parts.badge.text !== count) parts.badge.text = count;
307
+ }
308
+ const badgeWidth = Math.max(22, parts.badge.width + 12);
184
309
  const visualSig = [
185
310
  width,
186
311
  height,
@@ -191,6 +316,7 @@ function sync(container, item, context) {
191
316
  kind,
192
317
  surface,
193
318
  showMeta,
319
+ showBadge && badgeWidth,
194
320
  failedWithOutput,
195
321
  artifact?.type ?? "none",
196
322
  artifact?.id ?? "none",
@@ -199,7 +325,8 @@ function sync(container, item, context) {
199
325
  context.palette.surface,
200
326
  context.palette.hover,
201
327
  context.palette.border,
202
- context.palette.bg
328
+ context.palette.bg,
329
+ context.palette.text
203
330
  ].join("|");
204
331
  if (visualSig !== parts.visualSig) {
205
332
  parts.visualSig = visualSig;
@@ -217,6 +344,8 @@ function sync(container, item, context) {
217
344
  alpha: .5
218
345
  });
219
346
  parts.previewMask.clear().rect(frame.x, frame.y, frame.width, frame.height).fill({ color: 16777215 });
347
+ drawShimmerBand(parts.shimmer, frame.width, frame.height, context.palette.text);
348
+ parts.shimmer.y = frame.y;
220
349
  parts.previewArt.clear();
221
350
  if (kind === "waveform") {
222
351
  const bottomInset = showMeta ? META_HEIGHT : 0;
@@ -229,47 +358,52 @@ function sync(container, item, context) {
229
358
  }
230
359
  if (full && (artifact?.type === "audio" || artifact?.type === "video" && texture)) drawPlayBadge(parts.previewArt, frame, context);
231
360
  if (surface) {
232
- const markY = frame.y + frame.height / 2 - (full && stateLabel(surface) ? 10 : 0);
361
+ const markY = frame.y + frame.height / 2 - (full && label ? 18 : 0);
233
362
  drawStateMark(parts.previewArt, surface, frame.x + frame.width / 2, markY, surface === "failed" ? color : context.colors.neutral.stroke);
234
363
  }
235
364
  if (showMeta) parts.previewArt.rect(frame.x, frame.y + frame.height - META_HEIGHT, frame.width, META_HEIGHT).fill({
236
365
  color: context.palette.bg,
237
366
  alpha: .68
238
367
  });
368
+ if (showBadge) parts.previewArt.roundRect(width - badgeWidth - 8, 8, badgeWidth, 20, 10).fill({
369
+ color: context.palette.bg,
370
+ alpha: .72
371
+ });
239
372
  if (failedWithOutput) drawFailedBadge(parts.previewArt, width, color, context);
240
373
  }
241
- parts.preview.visible = kind === "texture" && Boolean(texture);
242
- if (parts.preview.visible && texture) {
374
+ const showPreview = kind === "texture" && Boolean(texture);
375
+ const revealKey = showPreview ? `${artifact?.id}:${texture?.uid}` : null;
376
+ const sameCard = parts.motion.itemId === item.id;
377
+ parts.motion.itemId = item.id;
378
+ if (revealKey !== parts.motion.revealKey || !sameCard) {
379
+ const appearing = sameCard && parts.motion.revealKey === null && revealKey !== null;
380
+ parts.motion.revealKey = revealKey;
381
+ parts.motion.revealFrom = appearing && !context.reducedMotion ? performance.now() : null;
382
+ parts.preview.alpha = parts.motion.revealFrom === null ? 1 : 0;
383
+ }
384
+ parts.preview.visible = showPreview;
385
+ if (showPreview && texture) {
243
386
  if (parts.preview.texture !== texture) parts.preview.texture = texture;
244
387
  const fitted = containTaskPreviewRect(frame.width, frame.height, texture.width, texture.height);
245
388
  parts.preview.position.set(frame.x + fitted.x, frame.y + fitted.y);
246
389
  parts.preview.width = fitted.width;
247
390
  parts.preview.height = fitted.height;
248
391
  }
249
- const bodyText = kind === "text" && artifact?.type === "text" ? artifact.textExcerpt : stateLabel(surface);
250
- const textSig = [
251
- bodyText,
252
- metaText,
253
- kind,
254
- surface,
255
- full,
256
- width,
257
- height,
258
- context.palette.text,
259
- context.palette.muted
260
- ].join("|");
261
- if (textSig !== parts.textSig) {
262
- parts.textSig = textSig;
263
- parts.body.style.fill = surface === "failed" ? color : context.palette.text;
264
- fitTextToLines(parts.body, bodyText, kind === "text" ? Math.max(1, Math.floor((height - 24 - (showMeta ? META_HEIGHT : 0)) / 16)) : 1, Math.max(24, width - 24));
265
- parts.meta.style.fill = context.palette.text;
266
- fitTextToLines(parts.meta, metaText, 1, Math.max(24, width - 24));
267
- }
268
392
  parts.body.visible = full && Boolean(bodyText);
393
+ parts.detail.visible = full && Boolean(detailText);
269
394
  parts.meta.visible = showMeta;
270
- if (kind === "text") parts.body.position.set(PADDING, PADDING);
271
- else parts.body.position.set(Math.max(PADDING, (width - parts.body.width) / 2), height / 2 + 10);
395
+ parts.badge.visible = showBadge;
396
+ if (textArtifact) parts.body.position.set(PADDING, PADDING);
397
+ else parts.body.position.set(width / 2, height / 2 + 2);
398
+ parts.detail.position.set(width / 2, height / 2 + 2 + parts.body.height + 4);
272
399
  parts.meta.position.set(PADDING, height - META_HEIGHT + 7);
400
+ parts.badge.position.set(width - 8 - badgeWidth / 2, 18);
401
+ parts.motion.width = frame.width;
402
+ parts.motion.shimmer = full && working && kind !== "texture" && !context.reducedMotion;
403
+ parts.shimmer.visible = parts.motion.shimmer;
404
+ parts.motion.startedAt = counting ? startedAt : null;
405
+ parts.motion.label = label;
406
+ parts.motion.elapsed = elapsed;
273
407
  }
274
408
  const taskCardRenderer = {
275
409
  id: "task-card",
@@ -281,35 +415,32 @@ const taskCardRenderer = {
281
415
  const previewBg = new Graphics();
282
416
  const preview = new Sprite();
283
417
  const previewMask = new Graphics();
418
+ const shimmer = new Graphics();
284
419
  const previewArt = new Graphics();
285
420
  const resolution = textResolutionForZoom(context.zoom);
286
- const body = new Text({
287
- text: "",
288
- style: {
289
- fontFamily: BOARD_FONT_STACK,
290
- fontSize: 11.5,
291
- fontWeight: "500",
292
- wordWrap: true,
293
- breakWords: true
294
- },
295
- resolution,
296
- roundPixels: true
297
- });
298
- const meta = new Text({
421
+ const text = (fontFamily, fontSize, fontWeight) => new Text({
299
422
  text: "",
300
423
  style: {
301
- fontFamily: BOARD_MONO_FONT_STACK,
302
- fontSize: 10,
303
- fontWeight: "500",
424
+ fontFamily,
425
+ fontSize,
426
+ fontWeight,
304
427
  wordWrap: true,
305
428
  breakWords: true
306
429
  },
307
430
  resolution,
308
431
  roundPixels: true
309
432
  });
433
+ const body = text(BOARD_FONT_STACK, 11.5, "500");
434
+ const detail = text(BOARD_FONT_STACK, 10.5, "400");
435
+ const meta = text(BOARD_MONO_FONT_STACK, 10, "500");
436
+ const badge = text(BOARD_MONO_FONT_STACK, 10, "500");
437
+ badge.style.wordWrap = false;
438
+ badge.anchor.set(.5);
439
+ detail.anchor.set(.5, 0);
440
+ detail.style.align = "center";
310
441
  preview.mask = previewMask;
311
442
  root.mask = clip;
312
- root.addChild(plate, clip, previewBg, preview, previewMask, previewArt, body, meta);
443
+ root.addChild(plate, clip, previewBg, shimmer, preview, previewMask, previewArt, body, detail, meta, badge);
313
444
  partsByContainer.set(root, {
314
445
  root,
315
446
  plate,
@@ -317,13 +448,26 @@ const taskCardRenderer = {
317
448
  previewBg,
318
449
  preview,
319
450
  previewMask,
451
+ shimmer,
320
452
  previewArt,
453
+ badge,
321
454
  body,
455
+ detail,
322
456
  meta,
323
457
  assetKey: null,
324
458
  visualSig: "",
325
459
  textSig: "",
326
- resolution
460
+ resolution,
461
+ motion: {
462
+ itemId: "",
463
+ width: 0,
464
+ shimmer: false,
465
+ startedAt: null,
466
+ label: "",
467
+ elapsed: "",
468
+ revealFrom: null,
469
+ revealKey: null
470
+ }
327
471
  });
328
472
  if (item.type === "task") sync(root, item, context);
329
473
  return root;
@@ -331,14 +475,19 @@ const taskCardRenderer = {
331
475
  update: (container, item, context) => {
332
476
  if (item.type === "task") sync(container, item, context);
333
477
  },
478
+ tick: (container, clock) => {
479
+ const parts = partsByContainer.get(container);
480
+ return parts ? advance(parts, clock) : null;
481
+ },
334
482
  renderFar: (graphics, item, context) => {
335
483
  if (item.type !== "task") return;
336
- const active = item.props.snapshot.status === "failed" || item.props.snapshot.status === "running" || item.props.snapshot.status === "pending";
484
+ const { status } = liveStatus(item, context);
485
+ const active = status === "failed" || status === "running" || status === "pending";
337
486
  drawFarPlate(graphics, item.frame, {
338
487
  fill: context.palette.surface,
339
488
  fillAlpha: .82,
340
489
  ...active ? {
341
- accent: statusColor(item, context),
490
+ accent: statusColor(status, context),
342
491
  accentAlpha: .9
343
492
  } : {}
344
493
  });
@@ -351,4 +500,4 @@ const taskCardRenderer = {
351
500
  }
352
501
  };
353
502
  //#endregion
354
- export { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, taskCardRenderer };
503
+ export { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, formatTaskElapsed, taskCardRenderer };
@@ -1,7 +1,7 @@
1
1
  import { BoardDocument } from "../../protocol/dist/board-model.js";
2
2
  import "../../protocol/dist/index.js";
3
- import { BoardScriptEvent, BoardScriptOutput, BoardScriptSnapshot } from "../model/script.js";
4
3
  import { BoardViewport } from "../model/geometry.js";
4
+ import { BoardScriptEvent, BoardScriptOutput, BoardScriptSnapshot } from "../model/script.js";
5
5
  import { BoardScene } from "../model/scene.js";
6
6
  import { BoardScriptHost } from "./renderers/board-renderer-registry.js";
7
7
  //#region src/board/render/script-host.d.ts
@@ -1,8 +1,9 @@
1
1
  import { boardScriptItems } from "../model/script-index.js";
2
2
  import { boardScriptScope } from "../model/script-scene.js";
3
+ import { boardScriptHitVisible, resolveBoardScriptHit } from "../model/script-hit.js";
4
+ import { BoardScriptTexture } from "./script-texture.js";
3
5
  import { createBoardScriptModuleReader, loadBoardScriptSources } from "../../protocol/dist/board-script-modules.js";
4
6
  import { boardScriptFrameSource } from "./script-sandbox.js";
5
- import { Texture } from "pixi.js";
6
7
  //#region src/board/render/script-host.ts
7
8
  function createBoardScriptHost(options) {
8
9
  const revisions = /* @__PURE__ */ new Map();
@@ -25,11 +26,27 @@ function createBoardScriptHost(options) {
25
26
  return value;
26
27
  }
27
28
  const focusTargets = /* @__PURE__ */ new Map();
29
+ const focusStyles = /* @__PURE__ */ new WeakMap();
28
30
  const layer = document.createElement("div");
31
+ let interactive = false;
29
32
  layer.style.cssText = "position:absolute;inset:0;pointer-events:none;overflow:hidden;z-index:2";
30
33
  layer.dataset.boardScripts = "";
31
34
  options.host.appendChild(layer);
32
35
  let disposed = false;
36
+ const retiredTextures = /* @__PURE__ */ new Set();
37
+ let retirementFrame = 0;
38
+ function retireTexture(texture) {
39
+ if (!texture.current) return;
40
+ retiredTextures.add(texture);
41
+ if (retirementFrame) return;
42
+ retirementFrame = requestAnimationFrame(() => {
43
+ retirementFrame = requestAnimationFrame(() => {
44
+ retirementFrame = 0;
45
+ for (const retired of retiredTextures) retired.destroy();
46
+ retiredTextures.clear();
47
+ });
48
+ });
49
+ }
33
50
  function ensureBroker() {
34
51
  if (broker) return;
35
52
  broker = document.createElement("iframe");
@@ -58,7 +75,7 @@ function createBoardScriptHost(options) {
58
75
  post(entry, { kind: "dispose" });
59
76
  entry.timer = setTimeout(() => removeFrame(entry), 100);
60
77
  } else removeFrame(entry);
61
- entry.texture?.destroy(true);
78
+ retireTexture(entry.texture);
62
79
  entries.delete(entry.id);
63
80
  revisions.set(entry.id, (revisions.get(entry.id) ?? 0) + 1);
64
81
  options.onOutput(entry.id, null);
@@ -99,7 +116,10 @@ function createBoardScriptHost(options) {
99
116
  removeFrame(entry);
100
117
  return;
101
118
  }
102
- if (entries.get(entry.id) !== entry) return;
119
+ if (entries.get(entry.id) !== entry) {
120
+ data.bitmap?.close?.();
121
+ return;
122
+ }
103
123
  if (data.kind === "ready") {
104
124
  entry.ready = true;
105
125
  options.onFrame();
@@ -149,11 +169,7 @@ function createBoardScriptHost(options) {
149
169
  clearTimeout(entry.timer);
150
170
  entry.pending = false;
151
171
  revisions.set(entry.id, (revisions.get(entry.id) ?? 0) + 1);
152
- if (data.bitmap) {
153
- const previous = entry.texture;
154
- entry.texture = Texture.from(data.bitmap);
155
- previous?.destroy(true);
156
- }
172
+ if (data.bitmap) entry.texture.update(data.bitmap);
157
173
  try {
158
174
  const output = data.output;
159
175
  if (output) {
@@ -170,7 +186,7 @@ function createBoardScriptHost(options) {
170
186
  return {
171
187
  revision: (id) => revisions.get(id) ?? 0,
172
188
  ready: (id) => entries.get(id)?.output !== null && entries.get(id)?.output !== void 0,
173
- frame: (item) => entries.get(item.id)?.texture ?? null,
189
+ frame: (item) => entries.get(item.id)?.texture.current ?? null,
174
190
  error: (id) => entries.get(id)?.error ?? null,
175
191
  release: () => {},
176
192
  fault(id, message) {
@@ -215,10 +231,12 @@ function createBoardScriptHost(options) {
215
231
  get animated() {
216
232
  return [...entries.values()].some((entry) => !entry.error && entry.output?.animated && (entry.visible || entry.output.updating) && !entry.pending);
217
233
  },
218
- sync(board, scene, camera, time, interactive, visibleIds) {
234
+ sync(board, scene, camera, time, inputEnabled, visibleIds) {
219
235
  if (disposed) return;
236
+ interactive = inputEnabled;
220
237
  const alive = /* @__PURE__ */ new Set();
221
238
  const liveFocusTargets = /* @__PURE__ */ new Set();
239
+ let focusSurface;
222
240
  for (const [id, item] of boardScriptItems(board)) {
223
241
  if (!item.script?.enabled) continue;
224
242
  alive.add(id);
@@ -255,7 +273,7 @@ function createBoardScriptHost(options) {
255
273
  inputKey: "",
256
274
  inputDocument: null,
257
275
  startedAt: (options.export ? 0 : time) - (input.snapshot?.time ?? 0),
258
- texture: null,
276
+ texture: new BoardScriptTexture(),
259
277
  error: null,
260
278
  output: null,
261
279
  events: [],
@@ -306,14 +324,15 @@ function createBoardScriptHost(options) {
306
324
  }
307
325
  if (entry.output && !options.export) {
308
326
  const hits = Object.entries(entry.output.hits).filter(([, hit]) => hit.label);
309
- if (!hits.length && entry.output.listens.length) hits.push([id, {
310
- x: 0,
311
- y: 0,
312
- width: box.width,
313
- height: box.height,
314
- label: "text" in item.props && typeof item.props.text === "string" ? item.props.text : item.script.src
315
- }]);
316
327
  for (const [target, hit] of hits) {
328
+ const resolved = resolveBoardScriptHit(scene, id, hit);
329
+ if (!resolved) continue;
330
+ const { rect } = resolved;
331
+ if (interactive) focusSurface ??= {
332
+ width: options.host.clientWidth,
333
+ height: options.host.clientHeight
334
+ };
335
+ const focusable = Boolean(focusSurface && interactive && boardScriptHitVisible(scene, resolved, camera, focusSurface));
317
336
  const key = `${id}/${target}`;
318
337
  liveFocusTargets.add(key);
319
338
  let button = focusTargets.get(key);
@@ -322,15 +341,11 @@ function createBoardScriptHost(options) {
322
341
  button.type = "button";
323
342
  const emit = (type, key) => {
324
343
  const current = entries.get(id);
325
- if (!current || current.error) return;
344
+ if (!interactive || !current || current.error || !current.output?.listens.includes(type)) return;
326
345
  current.events.push({
327
346
  type,
328
347
  target,
329
- key,
330
- point: {
331
- x: hit.x + hit.width / 2,
332
- y: hit.y + hit.height / 2
333
- }
348
+ key
334
349
  });
335
350
  current.dirty = true;
336
351
  options.onFrame();
@@ -343,10 +358,16 @@ function createBoardScriptHost(options) {
343
358
  focusTargets.set(key, button);
344
359
  layer.appendChild(button);
345
360
  }
346
- button.setAttribute("aria-label", hit.label ?? target);
347
- button.tabIndex = interactive && visible ? 0 : -1;
348
- const [a, b, c, d, x, y] = scene.layout.matrix(id);
349
- button.style.cssText = `position:absolute;pointer-events:none;background:transparent;border:0;padding:0;transform-origin:0 0;width:${hit.width}px;height:${hit.height}px;transform:matrix(${a * camera.zoom},${b * camera.zoom},${c * camera.zoom},${d * camera.zoom},${(x + a * hit.x + c * hit.y) * camera.zoom + camera.x},${(y + b * hit.x + d * hit.y) * camera.zoom + camera.y})`;
361
+ const label = hit.label ?? target;
362
+ if (button.getAttribute("aria-label") !== label) button.setAttribute("aria-label", label);
363
+ const tabIndex = focusable ? 0 : -1;
364
+ if (button.tabIndex !== tabIndex) button.tabIndex = tabIndex;
365
+ const [a, b, c, d, x, y] = scene.layout.matrix(resolved.id);
366
+ const style = `position:absolute;pointer-events:none;background:transparent;border:0;padding:0;transform-origin:0 0;width:${rect.width}px;height:${rect.height}px;transform:matrix(${a * camera.zoom},${b * camera.zoom},${c * camera.zoom},${d * camera.zoom},${(x + a * rect.x + c * rect.y) * camera.zoom + camera.x},${(y + b * rect.x + d * rect.y) * camera.zoom + camera.y})`;
367
+ if (focusStyles.get(button) !== style) {
368
+ button.style.cssText = style;
369
+ focusStyles.set(button, style);
370
+ }
350
371
  }
351
372
  }
352
373
  if (!entry.ready || !entry.sources || entry.pending || !entry.dirty && (!entry.output?.animated || !visible && !entry.output.updating || entry.time === time)) continue;
@@ -378,6 +399,10 @@ function createBoardScriptHost(options) {
378
399
  event(id, event) {
379
400
  const entry = entries.get(id);
380
401
  if (!entry || entry.error || entry.output && !entry.output.listens.includes(event.type)) return;
402
+ if (event.type === "viewchange" || event.type === "selectionchange") {
403
+ const queued = entry.events.findIndex((pending) => pending.type === event.type);
404
+ if (queued >= 0) entry.events.splice(queued, 1);
405
+ }
381
406
  if (event.type === "pointermove" && entry.events.at(-1)?.type === "pointermove") entry.events.pop();
382
407
  if (entry.events.length >= 128) return;
383
408
  entry.events.push(event);