@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
@@ -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
+ import { fitTextToLines } from "../text-fit.js";
4
+ import { boardCardDetailed, syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
5
+ import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "../card-tag.js";
3
6
  import { drawAudioWaveform } from "../audio-waveform.js";
4
- import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
7
+ import { cardElevation, containRect, drawCardSurface, traceCardOutline } from "../surface.js";
5
8
  import { positionShell } from "./base-card-renderer.js";
6
9
  import { drawFarPlate } from "./far-plate.js";
7
- import { fitTextToLines } from "./file-card-renderer.js";
8
- import { Container, Graphics, Sprite, Text } from "pixi.js";
10
+ import { Container, FillGradient, Graphics, Sprite, Text } from "pixi.js";
9
11
  //#region src/board/render/renderers/task-card-renderer.ts
10
- const RADIUS = 4;
11
12
  const PADDING = 12;
12
13
  const META_HEIGHT = 28;
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,54 +23,45 @@ 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(" · ");
52
- }
53
- function containTaskPreviewRect(width, height, imageWidth, imageHeight) {
54
- if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return {
55
- x: 0,
56
- y: 0,
57
- width: 0,
58
- height: 0
59
- };
60
- const scale = Math.min(width / imageWidth, height / imageHeight);
61
- const renderedWidth = imageWidth * scale;
62
- const renderedHeight = imageHeight * scale;
63
- return {
64
- x: (width - renderedWidth) / 2,
65
- y: (height - renderedHeight) / 2,
66
- width: renderedWidth,
67
- height: renderedHeight
68
- };
69
- }
70
65
  function drawPlayBadge(graphics, rect, context) {
71
66
  const radius = Math.min(PLAY_BADGE_RADIUS, Math.max(8, Math.min(rect.width, rect.height) * .16));
72
67
  const cx = rect.x + rect.width / 2;
@@ -150,15 +145,116 @@ function drawFailedBadge(graphics, width, color, context) {
150
145
  cap: "round"
151
146
  });
152
147
  }
148
+ const shimmerGradients = /* @__PURE__ */ new Map();
149
+ function shimmerGradient(color) {
150
+ let gradient = shimmerGradients.get(color);
151
+ if (!gradient) {
152
+ const rgb = `${color >> 16 & 255}, ${color >> 8 & 255}, ${color & 255}`;
153
+ gradient = new FillGradient({
154
+ type: "linear",
155
+ start: {
156
+ x: 0,
157
+ y: 0
158
+ },
159
+ end: {
160
+ x: 1,
161
+ y: 0
162
+ },
163
+ textureSpace: "local",
164
+ colorStops: [
165
+ {
166
+ offset: 0,
167
+ color: `rgba(${rgb}, 0)`
168
+ },
169
+ {
170
+ offset: .5,
171
+ color: `rgba(${rgb}, 0.07)`
172
+ },
173
+ {
174
+ offset: 1,
175
+ color: `rgba(${rgb}, 0)`
176
+ }
177
+ ]
178
+ });
179
+ shimmerGradients.set(color, gradient);
180
+ }
181
+ return gradient;
182
+ }
183
+ function drawShimmerBand(graphics, width, height, color) {
184
+ const band = Math.max(48, width * .45);
185
+ graphics.clear().rect(-band, 0, band, height).fill(shimmerGradient(color));
186
+ }
187
+ const MOSAIC_FRAME_MS = 160;
188
+ const MOSAIC_CELL = 8;
189
+ function drawMosaic(graphics, width, height, color, frame) {
190
+ const columns = Math.max(1, Math.floor(width / MOSAIC_CELL));
191
+ const rows = Math.max(1, Math.floor(height / MOSAIC_CELL));
192
+ const left = (width - columns * MOSAIC_CELL) / 2;
193
+ const top = (height - rows * MOSAIC_CELL) / 2;
194
+ graphics.clear();
195
+ for (const [threshold, alpha] of [[.08, .16], [.02, .3]]) {
196
+ for (let row = 0; row < rows; row += 1) for (let column = 0; column < columns; column += 1) {
197
+ let hash = Math.imul(column + 1, 2654435761) ^ Math.imul(row + 1, 2246822507) ^ Math.imul(frame + 1, 3266489909);
198
+ hash ^= hash >>> 15;
199
+ hash = Math.imul(hash, 739982445);
200
+ hash ^= hash >>> 12;
201
+ if ((hash >>> 0) / 4294967295 < threshold) graphics.rect(left + column * MOSAIC_CELL + 1, top + row * MOSAIC_CELL + 1, 6, 6);
202
+ }
203
+ graphics.fill({
204
+ color,
205
+ alpha
206
+ });
207
+ }
208
+ }
209
+ function advance(parts, clock) {
210
+ const { motion } = parts;
211
+ let next = null;
212
+ if (motion.mosaic) {
213
+ const frame = Math.floor(clock / MOSAIC_FRAME_MS);
214
+ if (frame !== motion.mosaic.step) {
215
+ motion.mosaic.step = frame;
216
+ drawMosaic(parts.shimmer, motion.width, motion.mosaic.height, motion.mosaic.color, frame);
217
+ }
218
+ next = MOSAIC_FRAME_MS - clock % MOSAIC_FRAME_MS;
219
+ } else if (motion.shimmer) {
220
+ const travel = motion.width + parts.shimmer.width;
221
+ parts.shimmer.x = clock % SHIMMER_PERIOD_MS / SHIMMER_PERIOD_MS * travel;
222
+ next = SHIMMER_FRAME_MS;
223
+ }
224
+ if (motion.startedAt !== null) {
225
+ const elapsedMs = Date.now() - motion.startedAt;
226
+ const elapsed = formatBoardDuration(elapsedMs);
227
+ if (elapsed !== motion.elapsed) {
228
+ motion.elapsed = elapsed;
229
+ parts.body.text = `${motion.label} · ${elapsed}`;
230
+ }
231
+ const untilNextSecond = SECOND - Math.max(0, elapsedMs) % SECOND;
232
+ next = next === null ? untilNextSecond : Math.min(next, untilNextSecond);
233
+ }
234
+ if (motion.revealFrom !== null) {
235
+ const progress = Math.min(1, (clock - motion.revealFrom) / REVEAL_MS);
236
+ parts.preview.alpha = progress * (2 - progress);
237
+ if (progress >= 1) motion.revealFrom = null;
238
+ else next = 0;
239
+ }
240
+ return next;
241
+ }
153
242
  function sync(container, item, context) {
154
243
  const parts = partsByContainer.get(container);
155
244
  if (!parts) return;
156
245
  positionShell(parts.root, item);
157
246
  const { width, height } = item.frame;
158
- const selected = context.selectedIds.has(item.id);
159
- const hovered = context.hoveredId === item.id;
160
- const full = context.zoom >= TASK_CARD_FULL_DETAIL_ZOOM;
161
- const color = statusColor(item, context);
247
+ const { snapshot } = item.props;
248
+ const { status, startedAt } = liveStatus(item, context);
249
+ const full = boardCardDetailed(context.zoom);
250
+ const elevation = cardElevation(context, item.id);
251
+ syncCardTag(parts.tag, {
252
+ glyph: "task",
253
+ text: snapshot.title,
254
+ width
255
+ }, context);
256
+ const working = status === "pending" || status === "running";
257
+ const color = statusColor(status, context);
162
258
  const key = context.assetKey(item);
163
259
  if (key !== parts.assetKey) {
164
260
  if (parts.assetKey) context.releaseTexture(parts.assetKey);
@@ -167,56 +263,100 @@ function sync(container, item, context) {
167
263
  }
168
264
  const texture = key ? context.getTexture(key) : null;
169
265
  const previewFailed = Boolean(key && !texture && context.hasError(key));
170
- const artifact = featuredTaskArtifact(item.props.snapshot.artifacts);
266
+ const artifact = featuredTaskArtifact(snapshot.artifacts);
171
267
  const kind = previewKindFor(artifact);
172
- const surface = stateSurfaceFor(item, kind, Boolean(texture), previewFailed);
268
+ const surface = stateSurfaceFor(status, kind, Boolean(texture), previewFailed);
173
269
  const frame = {
174
270
  x: 1,
175
271
  y: 1,
176
272
  width: Math.max(1, width - 2),
177
273
  height: Math.max(1, height - 2)
178
274
  };
179
- const metaText = artifact ? metadata(item) : "";
275
+ const metaText = artifact || working ? snapshot.model ?? "" : "";
180
276
  const showMeta = full && Boolean(metaText);
181
- const failedWithOutput = item.props.snapshot.status === "failed" && kind !== "empty";
277
+ const failedWithOutput = status === "failed" && kind !== "empty";
278
+ const count = artifact && snapshot.artifactCount > 1 ? String(snapshot.artifactCount) : "";
279
+ const showBadge = full && Boolean(count);
280
+ const label = stateLabel(surface, context);
281
+ const counting = full && working && startedAt !== null && surface !== null;
282
+ const elapsed = counting ? formatBoardDuration(Date.now() - startedAt) : "";
283
+ const detailText = !full || !surface ? "" : status === "failed" ? snapshot.errorExcerpt ?? "" : working ? snapshot.promptExcerpt ?? "" : "";
284
+ const textArtifact = kind === "text" && artifact?.type === "text";
285
+ const bodyText = textArtifact ? artifact.textExcerpt : counting ? `${label} · ${elapsed}` : label;
182
286
  syncTextResolution(parts.body, parts, context.zoom);
287
+ syncTextResolution(parts.detail, parts, context.zoom);
183
288
  syncTextResolution(parts.meta, parts, context.zoom);
289
+ syncTextResolution(parts.badge, parts, context.zoom);
290
+ const textSig = [
291
+ counting ? label : bodyText,
292
+ detailText,
293
+ metaText,
294
+ count,
295
+ kind,
296
+ surface,
297
+ full,
298
+ width,
299
+ height,
300
+ color,
301
+ context.palette.text,
302
+ context.palette.muted,
303
+ context.style.font
304
+ ].join("|");
305
+ if (textSig !== parts.textSig) {
306
+ parts.textSig = textSig;
307
+ parts.body.style.fontFamily = context.style.font;
308
+ parts.detail.style.fontFamily = context.style.font;
309
+ const wrap = Math.max(24, width - 24);
310
+ parts.body.style.fill = surface === "failed" ? color : context.palette.text;
311
+ parts.body.anchor.set(textArtifact ? 0 : .5, 0);
312
+ fitTextToLines(parts.body, bodyText, textArtifact ? Math.max(1, Math.floor((height - 24 - (showMeta ? META_HEIGHT : 0)) / 16)) : 1, wrap);
313
+ parts.detail.style.fill = context.palette.muted;
314
+ fitTextToLines(parts.detail, detailText, height - 24 > 120 ? 2 : 1, Math.min(wrap, 360));
315
+ parts.meta.style.fill = context.palette.text;
316
+ fitTextToLines(parts.meta, metaText, 1, wrap);
317
+ parts.badge.style.fill = context.palette.text;
318
+ if (parts.badge.text !== count) parts.badge.text = count;
319
+ }
320
+ const badgeWidth = Math.max(22, parts.badge.width + 12);
184
321
  const visualSig = [
185
322
  width,
186
323
  height,
187
- selected,
188
- hovered,
189
324
  full,
325
+ elevation,
190
326
  color,
191
327
  kind,
192
328
  surface,
193
329
  showMeta,
330
+ showBadge && badgeWidth,
194
331
  failedWithOutput,
195
332
  artifact?.type ?? "none",
196
333
  artifact?.id ?? "none",
197
334
  item.props.taskRunId,
198
335
  texture ? `${texture.width}x${texture.height}` : "none",
199
- context.palette.surface,
200
- context.palette.hover,
201
- context.palette.border,
202
- context.palette.bg
336
+ context.themeKey
203
337
  ].join("|");
204
338
  if (visualSig !== parts.visualSig) {
205
339
  parts.visualSig = visualSig;
206
- parts.plate.clear().roundRect(0, 0, width, height, RADIUS).fill({
207
- color: context.palette.surface,
208
- alpha: .98
209
- }).roundRect(0, 0, width, height, RADIUS).stroke({
210
- color: selected ? context.palette.brand : hovered ? context.palette.muted : context.palette.border,
211
- width: selected ? 2 : 1,
212
- alpha: selected ? .96 : .82
340
+ const rect = {
341
+ x: 0,
342
+ y: 0,
343
+ width,
344
+ height
345
+ };
346
+ drawCardSurface(parts.plate.clear(), rect, context.style, {
347
+ fill: context.palette.surface,
348
+ border: context.palette.border,
349
+ borderAlpha: .82,
350
+ elevation
213
351
  });
214
- parts.clip.clear().roundRect(1, 1, frame.width, frame.height, 3).fill({ color: 16777215 });
352
+ traceCardOutline(parts.clip.clear(), rect, context.style, -1).fill({ color: 16777215 });
215
353
  parts.previewBg.clear().rect(frame.x, frame.y, frame.width, frame.height).fill({
216
354
  color: context.palette.hover,
217
355
  alpha: .5
218
356
  });
219
357
  parts.previewMask.clear().rect(frame.x, frame.y, frame.width, frame.height).fill({ color: 16777215 });
358
+ if (context.style.progress === "shimmer") drawShimmerBand(parts.shimmer, frame.width, frame.height, context.palette.text);
359
+ parts.shimmer.position.set(context.style.progress === "shimmer" ? 0 : frame.x, frame.y);
220
360
  parts.previewArt.clear();
221
361
  if (kind === "waveform") {
222
362
  const bottomInset = showMeta ? META_HEIGHT : 0;
@@ -229,47 +369,58 @@ function sync(container, item, context) {
229
369
  }
230
370
  if (full && (artifact?.type === "audio" || artifact?.type === "video" && texture)) drawPlayBadge(parts.previewArt, frame, context);
231
371
  if (surface) {
232
- const markY = frame.y + frame.height / 2 - (full && stateLabel(surface) ? 10 : 0);
372
+ const markY = frame.y + frame.height / 2 - (full && label ? 18 : 0);
233
373
  drawStateMark(parts.previewArt, surface, frame.x + frame.width / 2, markY, surface === "failed" ? color : context.colors.neutral.stroke);
234
374
  }
235
375
  if (showMeta) parts.previewArt.rect(frame.x, frame.y + frame.height - META_HEIGHT, frame.width, META_HEIGHT).fill({
236
376
  color: context.palette.bg,
237
377
  alpha: .68
238
378
  });
379
+ if (showBadge) parts.previewArt.roundRect(width - badgeWidth - 8, 8, badgeWidth, 20, 10).fill({
380
+ color: context.palette.bg,
381
+ alpha: .72
382
+ });
239
383
  if (failedWithOutput) drawFailedBadge(parts.previewArt, width, color, context);
240
384
  }
241
- parts.preview.visible = kind === "texture" && Boolean(texture);
242
- if (parts.preview.visible && texture) {
385
+ const showPreview = kind === "texture" && Boolean(texture);
386
+ const revealKey = showPreview ? `${artifact?.id}:${texture?.uid}` : null;
387
+ const sameCard = parts.motion.itemId === item.id;
388
+ parts.motion.itemId = item.id;
389
+ if (revealKey !== parts.motion.revealKey || !sameCard) {
390
+ const appearing = sameCard && parts.motion.revealKey === null && revealKey !== null;
391
+ parts.motion.revealKey = revealKey;
392
+ parts.motion.revealFrom = appearing && !context.reducedMotion ? performance.now() : null;
393
+ parts.preview.alpha = parts.motion.revealFrom === null ? 1 : 0;
394
+ }
395
+ parts.preview.visible = showPreview;
396
+ if (showPreview && texture) {
243
397
  if (parts.preview.texture !== texture) parts.preview.texture = texture;
244
- const fitted = containTaskPreviewRect(frame.width, frame.height, texture.width, texture.height);
398
+ const fitted = containRect(frame.width, frame.height, texture.width, texture.height);
245
399
  parts.preview.position.set(frame.x + fitted.x, frame.y + fitted.y);
246
400
  parts.preview.width = fitted.width;
247
401
  parts.preview.height = fitted.height;
248
402
  }
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
403
  parts.body.visible = full && Boolean(bodyText);
404
+ parts.detail.visible = full && Boolean(detailText);
269
405
  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);
406
+ parts.badge.visible = showBadge;
407
+ if (textArtifact) parts.body.position.set(PADDING, PADDING);
408
+ else parts.body.position.set(width / 2, height / 2 + 2);
409
+ parts.detail.position.set(width / 2, height / 2 + 2 + parts.body.height + 4);
272
410
  parts.meta.position.set(PADDING, height - META_HEIGHT + 7);
411
+ parts.badge.position.set(width - 8 - badgeWidth / 2, 18);
412
+ parts.motion.width = frame.width;
413
+ parts.motion.shimmer = full && working && kind !== "texture" && !context.reducedMotion;
414
+ const mosaic = parts.motion.shimmer && context.style.progress === "mosaic";
415
+ parts.motion.mosaic = mosaic ? {
416
+ color: context.palette.brand,
417
+ height: frame.height,
418
+ step: -1
419
+ } : null;
420
+ parts.shimmer.visible = parts.motion.shimmer;
421
+ parts.motion.startedAt = counting ? startedAt : null;
422
+ parts.motion.label = label;
423
+ parts.motion.elapsed = elapsed;
273
424
  }
274
425
  const taskCardRenderer = {
275
426
  id: "task-card",
@@ -281,49 +432,64 @@ const taskCardRenderer = {
281
432
  const previewBg = new Graphics();
282
433
  const preview = new Sprite();
283
434
  const previewMask = new Graphics();
435
+ const shimmer = new Graphics();
284
436
  const previewArt = new Graphics();
285
437
  const resolution = textResolutionForZoom(context.zoom);
286
- const body = new Text({
438
+ const text = (fontFamily, fontSize, fontWeight) => new Text({
287
439
  text: "",
288
440
  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({
299
- text: "",
300
- style: {
301
- fontFamily: BOARD_MONO_FONT_STACK,
302
- fontSize: 10,
303
- fontWeight: "500",
441
+ fontFamily,
442
+ fontSize,
443
+ fontWeight,
304
444
  wordWrap: true,
305
445
  breakWords: true
306
446
  },
307
447
  resolution,
308
448
  roundPixels: true
309
449
  });
450
+ const body = text(BOARD_FONT_STACK, 11.5, "500");
451
+ const detail = text(BOARD_FONT_STACK, 10.5, "400");
452
+ const meta = text(BOARD_MONO_FONT_STACK, 10, "500");
453
+ const badge = text(BOARD_MONO_FONT_STACK, 10, "500");
454
+ badge.style.wordWrap = false;
455
+ badge.anchor.set(.5);
456
+ detail.anchor.set(.5, 0);
457
+ detail.style.align = "center";
310
458
  preview.mask = previewMask;
311
- root.mask = clip;
312
- root.addChild(plate, clip, previewBg, preview, previewMask, previewArt, body, meta);
459
+ const content = new Container();
460
+ content.mask = clip;
461
+ content.addChild(previewBg, shimmer, preview, previewMask, previewArt, body, detail, meta, badge);
462
+ const tag = createCardTag();
463
+ root.addChild(plate, content, clip, tag.root);
313
464
  partsByContainer.set(root, {
314
465
  root,
466
+ tag,
315
467
  plate,
316
468
  clip,
317
469
  previewBg,
318
470
  preview,
319
471
  previewMask,
472
+ shimmer,
320
473
  previewArt,
474
+ badge,
321
475
  body,
476
+ detail,
322
477
  meta,
323
478
  assetKey: null,
324
479
  visualSig: "",
325
480
  textSig: "",
326
- resolution
481
+ resolution,
482
+ motion: {
483
+ itemId: "",
484
+ width: 0,
485
+ shimmer: false,
486
+ mosaic: null,
487
+ startedAt: null,
488
+ label: "",
489
+ elapsed: "",
490
+ revealFrom: null,
491
+ revealKey: null
492
+ }
327
493
  });
328
494
  if (item.type === "task") sync(root, item, context);
329
495
  return root;
@@ -331,14 +497,23 @@ const taskCardRenderer = {
331
497
  update: (container, item, context) => {
332
498
  if (item.type === "task") sync(container, item, context);
333
499
  },
500
+ tick: (container, clock) => {
501
+ const parts = partsByContainer.get(container);
502
+ return parts ? advance(parts, clock) : null;
503
+ },
504
+ zoom: (container, zoom) => {
505
+ const tag = partsByContainer.get(container)?.tag;
506
+ if (tag) zoomCardTag(tag, zoom);
507
+ },
334
508
  renderFar: (graphics, item, context) => {
335
509
  if (item.type !== "task") return;
336
- const active = item.props.snapshot.status === "failed" || item.props.snapshot.status === "running" || item.props.snapshot.status === "pending";
510
+ const { status } = liveStatus(item, context);
511
+ const active = status === "failed" || status === "running" || status === "pending";
337
512
  drawFarPlate(graphics, item.frame, {
338
513
  fill: context.palette.surface,
339
514
  fillAlpha: .82,
340
515
  ...active ? {
341
- accent: statusColor(item, context),
516
+ accent: statusColor(status, context),
342
517
  accentAlpha: .9
343
518
  } : {}
344
519
  });
@@ -351,4 +526,4 @@ const taskCardRenderer = {
351
526
  }
352
527
  };
353
528
  //#endregion
354
- export { TASK_CARD_FULL_DETAIL_ZOOM, containTaskPreviewRect, taskCardRenderer };
529
+ export { taskCardRenderer };
@@ -1,40 +1,41 @@
1
1
  import { BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
3
+ import { cardElevation, drawCardSurface, traceCardOutline } from "../surface.js";
3
4
  import { positionShell } from "./base-card-renderer.js";
4
5
  import { drawFarPlate } from "./far-plate.js";
5
6
  import { Container, Graphics, Text } from "pixi.js";
6
7
  //#region src/board/render/renderers/unknown-card-renderer.ts
7
- const RADIUS = 10;
8
8
  const partsByContainer = /* @__PURE__ */ new WeakMap();
9
9
  function sync(container, item, context) {
10
10
  const parts = partsByContainer.get(container);
11
11
  if (!parts) return;
12
12
  positionShell(parts.root, item);
13
13
  const { width, height } = item.frame;
14
- const selected = context.selectedIds.has(item.id);
15
14
  const realType = item.type;
16
15
  syncTextResolution(parts.label, parts, context.zoom);
16
+ const elevation = cardElevation(context, item.id);
17
17
  const visualSig = [
18
18
  width,
19
19
  height,
20
- selected,
21
- context.palette.surface,
22
- context.palette.brand,
23
- context.palette.border,
24
- context.palette.muted
20
+ elevation,
21
+ context.themeKey
25
22
  ].join("|");
26
23
  if (visualSig !== parts.visualSig) {
27
24
  parts.visualSig = visualSig;
28
- parts.box.clear();
29
- parts.box.roundRect(0, 0, width, height, RADIUS).fill({
30
- color: context.palette.surface,
31
- alpha: .7
32
- }).roundRect(0, 0, width, height, RADIUS).stroke({
33
- color: selected ? context.palette.brand : context.palette.border,
34
- width: selected ? 2 : 1,
35
- alpha: .8
25
+ const rect = {
26
+ x: 0,
27
+ y: 0,
28
+ width,
29
+ height
30
+ };
31
+ drawCardSurface(parts.box.clear(), rect, context.style, {
32
+ fill: context.palette.surface,
33
+ fillAlpha: .7,
34
+ border: context.palette.border,
35
+ borderAlpha: .8,
36
+ elevation
36
37
  });
37
- parts.box.roundRect(6, 6, width - 12, height - 12, 6).stroke({
38
+ traceCardOutline(parts.box, rect, context.style, -6).stroke({
38
39
  color: context.palette.muted,
39
40
  width: 1,
40
41
  alpha: .4