@neta-art/cohub-cli 10.1.0 → 10.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.
@@ -9341,13 +9341,47 @@ function filePreviewKind(snapshot) {
9341
9341
  if (snapshot.excerpt) return "text";
9342
9342
  return "blank";
9343
9343
  }
9344
- /** Short uppercase type label for a file card (`MD`, `JSON`, `FILE`). */
9345
9344
  function fileTypeLabel(path) {
9345
+ if (isWebPageUrl(path)) return "LINK";
9346
9346
  const name = fileBaseName(path);
9347
9347
  const dot = name.lastIndexOf(".");
9348
9348
  if (dot <= 0) return (name.replace(/^\./, "") || "file").toUpperCase();
9349
9349
  return name.slice(dot + 1).toUpperCase();
9350
9350
  }
9351
+ function isWebPageUrl(path) {
9352
+ if (!/^https?:\/\//i.test(path)) return false;
9353
+ try {
9354
+ const last = new URL(path).pathname.split("/").filter(Boolean).pop() ?? "";
9355
+ return !/\.[a-z\d]{1,8}$/i.test(last);
9356
+ } catch {
9357
+ return false;
9358
+ }
9359
+ }
9360
+ /** Human-readable byte size. Kept as a public helper for custom card renderers. */
9361
+ function formatFileSize(bytes) {
9362
+ if (bytes === void 0 || !Number.isFinite(bytes) || bytes < 0) return "";
9363
+ if (bytes < 1024) return `${bytes} B`;
9364
+ const units = [
9365
+ "KB",
9366
+ "MB",
9367
+ "GB",
9368
+ "TB"
9369
+ ];
9370
+ let value = bytes / 1024;
9371
+ let unit = 0;
9372
+ while (value >= 1024 && unit < units.length - 1) {
9373
+ value /= 1024;
9374
+ unit += 1;
9375
+ }
9376
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
9377
+ }
9378
+ /** `MD · 12 KB` — empty when neither fact is available. */
9379
+ function fileMetaLine(path, size) {
9380
+ const type = fileTypeLabel(path);
9381
+ const formatted = formatFileSize(size);
9382
+ if (type && formatted) return `${type} · ${formatted}`;
9383
+ return formatted || type;
9384
+ }
9351
9385
  /** Map a file category onto an existing palette token. */
9352
9386
  function fileCategoryAccent(category, palette) {
9353
9387
  switch (category) {
@@ -27011,8 +27045,8 @@ const DARK = {
27011
27045
  legendary: 16096779
27012
27046
  };
27013
27047
  const LIGHT = {
27014
- bg: 16777215,
27015
- surface: 16053493,
27048
+ bg: 16251129,
27049
+ surface: 16777215,
27016
27050
  hover: 15329772,
27017
27051
  border: 14079706,
27018
27052
  brand: 15222030,
@@ -27195,6 +27229,298 @@ function inspectBoardScene(scene, limit = 100) {
27195
27229
  return diagnostics;
27196
27230
  }
27197
27231
  //#endregion
27232
+ //#region ../sdk/src/board/render/text-fit.ts
27233
+ function fitLineWithEllipsis(line, wrapWidth, measureWidth) {
27234
+ const trimmed = line.replace(/\s+$/u, "");
27235
+ if (!trimmed) return "…";
27236
+ const full = `${trimmed}…`;
27237
+ if (measureWidth(full) <= wrapWidth) return full;
27238
+ let lo = 0;
27239
+ let hi = trimmed.length;
27240
+ let best = "…";
27241
+ while (lo <= hi) {
27242
+ const mid = lo + hi >> 1;
27243
+ const candidate = mid > 0 ? `${trimmed.slice(0, mid)}…` : "…";
27244
+ if (measureWidth(candidate) <= wrapWidth) {
27245
+ best = candidate;
27246
+ lo = mid + 1;
27247
+ } else hi = mid - 1;
27248
+ }
27249
+ return best;
27250
+ }
27251
+ function fitTextToLines(text, value, maxLines, wrapWidth) {
27252
+ const width = Math.max(1, wrapWidth);
27253
+ text.style.wordWrapWidth = width;
27254
+ if (!value || maxLines <= 0) {
27255
+ if (text.text !== "") text.text = "";
27256
+ return;
27257
+ }
27258
+ const charCap = Math.max(maxLines * Math.ceil(width), maxLines * 4);
27259
+ const sample = value.length > charCap ? value.slice(0, charCap) : value;
27260
+ const metrics = CanvasTextMetrics.measureText(sample, text.style);
27261
+ if (!(metrics.lines.length > maxLines || sample !== value)) {
27262
+ if (text.text !== value) text.text = value;
27263
+ return;
27264
+ }
27265
+ const keptCount = Math.min(maxLines, metrics.lines.length);
27266
+ const kept = metrics.lines.slice(0, keptCount);
27267
+ const lastIndex = kept.length - 1;
27268
+ const probe = text.style.clone();
27269
+ probe.wordWrap = false;
27270
+ kept[lastIndex] = fitLineWithEllipsis(kept[lastIndex] ?? "", width, (candidate) => CanvasTextMetrics.measureText(candidate, probe).width);
27271
+ const fitted = kept.join("\n");
27272
+ if (text.text !== fitted) text.text = fitted;
27273
+ }
27274
+ function fitTextToWidth(text, value, width) {
27275
+ text.style.wordWrap = false;
27276
+ const measure = (candidate) => CanvasTextMetrics.measureText(candidate, text.style).width;
27277
+ const fitted = !value || measure(value) <= width ? value : fitLineWithEllipsis(value, width, measure);
27278
+ if (text.text !== fitted) text.text = fitted;
27279
+ }
27280
+ //#endregion
27281
+ //#region ../sdk/src/board/render/text-resolution.ts
27282
+ const MAX_BOARD_RESOLUTION = 2;
27283
+ function getBoardResolution() {
27284
+ return Math.min(globalThis.devicePixelRatio || 1, MAX_BOARD_RESOLUTION);
27285
+ }
27286
+ /**
27287
+ * Zoom buckets for text re-rasterisation. Pixi Text is a bitmap: at high zoom it
27288
+ * looks soft unless we raise its resolution. We quantise zoom into a few
27289
+ * buckets so small zoom changes don't thrash texture regeneration.
27290
+ */
27291
+ const TEXT_ZOOM_BUCKETS = [
27292
+ .5,
27293
+ 1,
27294
+ 1.5,
27295
+ 2,
27296
+ 3,
27297
+ 4
27298
+ ];
27299
+ function textZoomBucket(zoom) {
27300
+ const clamped = Math.max(.1, Math.min(4, zoom));
27301
+ for (const bucket of TEXT_ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
27302
+ return TEXT_ZOOM_BUCKETS[TEXT_ZOOM_BUCKETS.length - 1] ?? 4;
27303
+ }
27304
+ function boardCardDetailed(zoom) {
27305
+ return textZoomBucket(zoom) >= 1;
27306
+ }
27307
+ /** Effective text resolution for a given camera zoom. */
27308
+ function textResolutionForZoom(zoom) {
27309
+ const bucket = textZoomBucket(zoom);
27310
+ return Math.min(getBoardResolution() * Math.max(1, bucket), 6);
27311
+ }
27312
+ /** Update a Pixi text texture only when zoom crosses a resolution bucket. */
27313
+ function syncTextResolution(text, state, zoom) {
27314
+ const resolution = textResolutionForZoom(zoom);
27315
+ if (resolution === state.resolution) return;
27316
+ text.resolution = resolution;
27317
+ state.resolution = resolution;
27318
+ }
27319
+ /** Defer expensive Pixi word-wrap rasterisation during a live resize. */
27320
+ function syncTextWrapWidth(text, state, width, defer) {
27321
+ if (defer || width === state.wrapWidth) return;
27322
+ text.style.wordWrapWidth = width;
27323
+ state.wrapWidth = width;
27324
+ }
27325
+ const ICON = 12;
27326
+ const GAP$2 = 5;
27327
+ const MAX_GROWTH = 1.25;
27328
+ const MIN_READABLE = 7;
27329
+ const SECOND$1 = 1e3;
27330
+ function formatBoardDuration(ms) {
27331
+ const total = Math.max(0, Math.floor(ms / SECOND$1));
27332
+ const hours = Math.floor(total / 3600);
27333
+ const minutes = Math.floor(total % 3600 / 60);
27334
+ const seconds = String(total % 60).padStart(2, "0");
27335
+ return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
27336
+ }
27337
+ const PIXEL_GLYPHS = {
27338
+ image: [
27339
+ "######",
27340
+ "#....#",
27341
+ "#.#..#",
27342
+ "#.##.#",
27343
+ "######",
27344
+ "......"
27345
+ ],
27346
+ video: [
27347
+ "######",
27348
+ "#.#..#",
27349
+ "#.##.#",
27350
+ "#.#..#",
27351
+ "######",
27352
+ "......"
27353
+ ],
27354
+ audio: [
27355
+ "...#..",
27356
+ "...##.",
27357
+ "...#.#",
27358
+ "...#..",
27359
+ ".###..",
27360
+ ".###.."
27361
+ ],
27362
+ file: [
27363
+ "####..",
27364
+ "#..##.",
27365
+ "#...##",
27366
+ "#....#",
27367
+ "#....#",
27368
+ "######"
27369
+ ],
27370
+ task: [
27371
+ "..#...",
27372
+ ".###..",
27373
+ "#####.",
27374
+ ".###..",
27375
+ "..#...",
27376
+ "......"
27377
+ ]
27378
+ };
27379
+ function drawLineGlyph(graphics, glyph, color) {
27380
+ const line = {
27381
+ color,
27382
+ width: 1.25,
27383
+ alpha: 1,
27384
+ cap: "round",
27385
+ join: "round"
27386
+ };
27387
+ switch (glyph) {
27388
+ case "image":
27389
+ graphics.roundRect(1, 2, 10, 8, 1.5).stroke(line);
27390
+ graphics.moveTo(2.5, 9).lineTo(5, 6).lineTo(7, 8).lineTo(8.5, 6.5).lineTo(10, 8).stroke(line);
27391
+ return;
27392
+ case "video":
27393
+ graphics.roundRect(1, 2.5, 10, 7, 1.5).stroke(line);
27394
+ graphics.poly([
27395
+ 5,
27396
+ 4.5,
27397
+ 5,
27398
+ 7.5,
27399
+ 7.5,
27400
+ 6
27401
+ ]).fill({ color });
27402
+ return;
27403
+ case "audio":
27404
+ for (const [x, height] of [
27405
+ [2.5, 3],
27406
+ [5, 7],
27407
+ [7.5, 9],
27408
+ [10, 5]
27409
+ ]) graphics.moveTo(x, 6 - height / 2).lineTo(x, 6 + height / 2);
27410
+ graphics.stroke(line);
27411
+ return;
27412
+ case "file":
27413
+ graphics.poly([
27414
+ 2.5,
27415
+ 1,
27416
+ 7.5,
27417
+ 1,
27418
+ 9.5,
27419
+ 3,
27420
+ 9.5,
27421
+ 11,
27422
+ 2.5,
27423
+ 11
27424
+ ]).stroke(line);
27425
+ graphics.moveTo(7.5, 1).lineTo(7.5, 3).lineTo(9.5, 3).stroke(line);
27426
+ return;
27427
+ case "task":
27428
+ graphics.poly([
27429
+ 6,
27430
+ 1,
27431
+ 7.3,
27432
+ 4.7,
27433
+ 11,
27434
+ 6,
27435
+ 7.3,
27436
+ 7.3,
27437
+ 6,
27438
+ 11,
27439
+ 4.7,
27440
+ 7.3,
27441
+ 1,
27442
+ 6,
27443
+ 4.7,
27444
+ 4.7
27445
+ ]).fill({ color });
27446
+ return;
27447
+ }
27448
+ }
27449
+ function drawCardGlyph(graphics, glyph, color, pixel) {
27450
+ if (!pixel) {
27451
+ drawLineGlyph(graphics, glyph, color);
27452
+ return;
27453
+ }
27454
+ PIXEL_GLYPHS[glyph].forEach((row, y) => {
27455
+ for (let x = 0; x < row.length; x += 1) if (row[x] === "#") graphics.rect(x * 2, y * 2, 2, 2);
27456
+ });
27457
+ graphics.fill({ color });
27458
+ }
27459
+ function createCardTag() {
27460
+ const root = new Container({ label: "card-tag" });
27461
+ const icon = new Graphics();
27462
+ root.addChild(icon);
27463
+ root.visible = false;
27464
+ return {
27465
+ root,
27466
+ icon,
27467
+ text: null,
27468
+ sig: "",
27469
+ resolution: 0,
27470
+ width: 0
27471
+ };
27472
+ }
27473
+ function zoomCardTag(tag, zoom) {
27474
+ if (!tag.text || !tag.sig) return;
27475
+ const scale = Math.min(MAX_GROWTH, Math.max(1, 1 / Math.max(zoom, .01)));
27476
+ const span = 17 + tag.text.width;
27477
+ tag.root.scale.set(scale);
27478
+ tag.root.position.set(0, -18 * scale);
27479
+ tag.root.visible = 11 * scale * zoom >= MIN_READABLE && span * scale <= tag.width + .5;
27480
+ }
27481
+ function syncCardTag(tag, input, context) {
27482
+ tag.width = input.width;
27483
+ if (!input.text || input.width <= 24) {
27484
+ tag.sig = "";
27485
+ tag.root.visible = false;
27486
+ return;
27487
+ }
27488
+ if (!tag.text) {
27489
+ tag.resolution = textResolutionForZoom(context.zoom);
27490
+ tag.text = new Text({
27491
+ text: "",
27492
+ style: {
27493
+ fontFamily: context.style.font,
27494
+ fontSize: 11,
27495
+ fontWeight: "500"
27496
+ },
27497
+ resolution: tag.resolution,
27498
+ roundPixels: true
27499
+ });
27500
+ tag.root.addChild(tag.text);
27501
+ }
27502
+ syncTextResolution(tag.text, tag, context.zoom);
27503
+ const sig = [
27504
+ input.glyph,
27505
+ input.text,
27506
+ input.width,
27507
+ context.themeKey
27508
+ ].join("|");
27509
+ if (sig === tag.sig) {
27510
+ zoomCardTag(tag, context.zoom);
27511
+ return;
27512
+ }
27513
+ tag.sig = sig;
27514
+ const color = context.palette.muted;
27515
+ const pixel = context.style.icons === "pixel";
27516
+ drawCardGlyph(tag.icon.clear(), input.glyph, color, pixel);
27517
+ tag.text.style.fill = color;
27518
+ tag.text.style.fontFamily = context.style.font;
27519
+ fitTextToWidth(tag.text, input.text, Math.max(1, input.width - ICON - GAP$2));
27520
+ tag.text.position.set(17, (ICON - tag.text.height) / 2);
27521
+ zoomCardTag(tag, context.zoom);
27522
+ }
27523
+ //#endregion
27198
27524
  //#region ../sdk/src/board/render/clip.ts
27199
27525
  function isClippingFrame(item) {
27200
27526
  return item.type === "frame" && item.props.clip !== false;
@@ -27258,7 +27584,7 @@ function audioWaveformBars(seed, count) {
27258
27584
  }
27259
27585
  return bars;
27260
27586
  }
27261
- function drawAudioWaveform(graphics, seed, rect, color, alpha = .72) {
27587
+ function drawAudioWaveform(graphics, seed, rect, color, alpha = .72, square = false) {
27262
27588
  const count = Math.max(8, Math.min(DEFAULT_BARS, Math.floor(rect.width / 4)));
27263
27589
  const step = rect.width / count;
27264
27590
  const barWidth = Math.max(1.5, step - BAR_GAP);
@@ -27267,53 +27593,225 @@ function drawAudioWaveform(graphics, seed, rect, color, alpha = .72) {
27267
27593
  const bars = audioWaveformBars(seed, count);
27268
27594
  for (let index = 0; index < count; index += 1) {
27269
27595
  const height = Math.max(2, (bars[index] ?? .4) * maxHeight);
27270
- graphics.roundRect(rect.x + index * step, centerY - height / 2, barWidth, height, barWidth / 2).fill({
27271
- color,
27272
- alpha
27273
- });
27596
+ const x = rect.x + index * step;
27597
+ if (square) graphics.rect(x, centerY - height / 2, barWidth, height);
27598
+ else graphics.roundRect(x, centerY - height / 2, barWidth, height, barWidth / 2);
27274
27599
  }
27600
+ graphics.fill({
27601
+ color,
27602
+ alpha
27603
+ });
27275
27604
  }
27276
27605
  //#endregion
27277
- //#region ../sdk/src/board/render/text-resolution.ts
27278
- const MAX_BOARD_RESOLUTION = 2;
27279
- function getBoardResolution() {
27280
- return Math.min(globalThis.devicePixelRatio || 1, MAX_BOARD_RESOLUTION);
27606
+ //#region ../sdk/src/board/render/stroke.ts
27607
+ const HEAD_SPREAD = Math.PI / 6;
27608
+ function dashPattern(dash, width) {
27609
+ if (dash === "dashed") return [Math.max(6, width * 4), Math.max(4, width * 3)];
27610
+ if (dash === "dotted") return [Math.max(.5, width * .01), Math.max(3, width * 2)];
27611
+ return null;
27281
27612
  }
27282
- /**
27283
- * Zoom buckets for text re-rasterisation. Pixi Text is a bitmap: at high zoom it
27284
- * looks soft unless we raise its resolution. We quantise zoom into a few
27285
- * buckets so small zoom changes don't thrash texture regeneration.
27286
- */
27287
- const TEXT_ZOOM_BUCKETS = [
27288
- .5,
27289
- 1,
27290
- 1.5,
27291
- 2,
27292
- 3,
27293
- 4
27294
- ];
27295
- function textZoomBucket(zoom) {
27296
- const clamped = Math.max(.1, Math.min(4, zoom));
27297
- for (const bucket of TEXT_ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
27298
- return TEXT_ZOOM_BUCKETS[TEXT_ZOOM_BUCKETS.length - 1] ?? 4;
27613
+ function trimPolyline(path, trim) {
27614
+ if (trim === void 0 || trim >= 1 || path.length < 2) return path;
27615
+ if (trim <= 0) return [];
27616
+ let total = 0;
27617
+ for (let index = 1; index < path.length; index += 1) {
27618
+ const a = path[index - 1];
27619
+ const b = path[index];
27620
+ total += Math.hypot(b.x - a.x, b.y - a.y);
27621
+ }
27622
+ let remaining = total * trim;
27623
+ const out = [path[0]];
27624
+ for (let index = 1; index < path.length; index += 1) {
27625
+ const a = path[index - 1];
27626
+ const b = path[index];
27627
+ const length = Math.hypot(b.x - a.x, b.y - a.y);
27628
+ if (length >= remaining) {
27629
+ const t = length > 0 ? remaining / length : 0;
27630
+ out.push(worldPoint(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t));
27631
+ return out;
27632
+ }
27633
+ out.push(b);
27634
+ remaining -= length;
27635
+ }
27636
+ return out;
27299
27637
  }
27300
- /** Effective text resolution for a given camera zoom. */
27301
- function textResolutionForZoom(zoom) {
27302
- const bucket = textZoomBucket(zoom);
27303
- return Math.min(getBoardResolution() * Math.max(1, bucket), 6);
27638
+ function tracePolyline(graphics, path, pattern, closed = false, offset = 0) {
27639
+ const points = closed && path.length > 2 ? [...path, path[0]] : path;
27640
+ if (points.length < 2) return;
27641
+ if (!pattern) {
27642
+ const first = points[0];
27643
+ graphics.moveTo(first.x, first.y);
27644
+ for (let index = 1; index < points.length; index += 1) {
27645
+ const point = points[index];
27646
+ graphics.lineTo(point.x, point.y);
27647
+ }
27648
+ if (closed) graphics.closePath();
27649
+ return;
27650
+ }
27651
+ const [dash, gap] = pattern;
27652
+ const phase = Math.max(0, offset) % (dash + gap);
27653
+ let penDown = phase < dash;
27654
+ let fresh = penDown;
27655
+ let remaining = penDown ? dash - phase : dash + gap - phase;
27656
+ for (let index = 1; index < points.length; index += 1) {
27657
+ const a = points[index - 1];
27658
+ const b = points[index];
27659
+ const dx = b.x - a.x;
27660
+ const dy = b.y - a.y;
27661
+ const length = Math.hypot(dx, dy);
27662
+ let travelled = 0;
27663
+ while (length - travelled > 1e-4) {
27664
+ const step = Math.min(length - travelled, remaining);
27665
+ if (penDown) {
27666
+ const t0 = travelled / length;
27667
+ const t1 = (travelled + step) / length;
27668
+ if (fresh) graphics.moveTo(a.x + dx * t0, a.y + dy * t0);
27669
+ fresh = false;
27670
+ graphics.lineTo(a.x + dx * t1, a.y + dy * t1);
27671
+ }
27672
+ travelled += step;
27673
+ remaining -= step;
27674
+ if (remaining <= 1e-4) {
27675
+ penDown = !penDown;
27676
+ fresh = penDown;
27677
+ remaining = penDown ? dash : gap;
27678
+ }
27679
+ }
27680
+ }
27304
27681
  }
27305
- /** Update a Pixi text texture only when zoom crosses a resolution bucket. */
27306
- function syncTextResolution(text, state, zoom) {
27307
- const resolution = textResolutionForZoom(zoom);
27308
- if (resolution === state.resolution) return;
27309
- text.resolution = resolution;
27310
- state.resolution = resolution;
27682
+ function traceArrowhead(graphics, tip, angle, size) {
27683
+ graphics.moveTo(tip.x - size * Math.cos(angle - HEAD_SPREAD), tip.y - size * Math.sin(angle - HEAD_SPREAD)).lineTo(tip.x, tip.y).lineTo(tip.x - size * Math.cos(angle + HEAD_SPREAD), tip.y - size * Math.sin(angle + HEAD_SPREAD));
27311
27684
  }
27312
- /** Defer expensive Pixi word-wrap rasterisation during a live resize. */
27313
- function syncTextWrapWidth(text, state, width, defer) {
27314
- if (defer || width === state.wrapWidth) return;
27315
- text.style.wordWrapWidth = width;
27316
- state.wrapWidth = width;
27685
+ function endAngle(path, atStart) {
27686
+ if (path.length < 2) return null;
27687
+ const tip = atStart ? path[0] : path[path.length - 1];
27688
+ for (let offset = 1; offset < path.length; offset += 1) {
27689
+ const other = atStart ? path[offset] : path[path.length - 1 - offset];
27690
+ if (Math.hypot(tip.x - other.x, tip.y - other.y) > .001) return Math.atan2(tip.y - other.y, tip.x - other.x);
27691
+ }
27692
+ return null;
27693
+ }
27694
+ function roundRectPoints(width, height, radius) {
27695
+ const r = Math.min(radius, width / 2, height / 2);
27696
+ if (r <= 0) return [
27697
+ worldPoint(0, 0),
27698
+ worldPoint(width, 0),
27699
+ worldPoint(width, height),
27700
+ worldPoint(0, height),
27701
+ worldPoint(0, 0)
27702
+ ];
27703
+ const corner = (cx, cy, from) => Array.from({ length: 9 }, (_, index) => {
27704
+ const angle = from + index / 8 * (Math.PI / 2);
27705
+ return worldPoint(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r);
27706
+ });
27707
+ return [
27708
+ ...corner(width - r, r, -Math.PI / 2),
27709
+ ...corner(width - r, height - r, 0),
27710
+ ...corner(r, height - r, Math.PI / 2),
27711
+ ...corner(r, r, Math.PI),
27712
+ worldPoint(width - r, 0)
27713
+ ];
27714
+ }
27715
+ const SHADOW_LAYERS = 5;
27716
+ function traceCardOutline(graphics, rect, style, grow = 0) {
27717
+ const x = rect.x - grow;
27718
+ const y = rect.y - grow;
27719
+ const width = Math.max(0, rect.width + grow * 2);
27720
+ const height = Math.max(0, rect.height + grow * 2);
27721
+ if (style.corner === "pixel") return tracePixelRect(graphics, x, y, width, height, 2);
27722
+ const radius = Math.min(Math.max(0, style.radius + grow), width / 2, height / 2);
27723
+ return radius > 0 ? graphics.roundRect(x, y, width, height, radius) : graphics.rect(x, y, width, height);
27724
+ }
27725
+ function tracePixelRect(graphics, x, y, width, height, step) {
27726
+ const s = Math.min(step, width / 4, height / 4);
27727
+ if (s <= 0) return graphics.rect(x, y, width, height);
27728
+ return graphics.poly(pixelRectPoints(x, y, width, height, s).flatMap((point) => [point.x, point.y]));
27729
+ }
27730
+ function pixelRectPoints(x, y, width, height, s) {
27731
+ const r = x + width;
27732
+ const b = y + height;
27733
+ const p = worldPoint;
27734
+ return [
27735
+ p(x, y + s * 2),
27736
+ p(x + s, y + s * 2),
27737
+ p(x + s, y + s),
27738
+ p(x + s * 2, y + s),
27739
+ p(x + s * 2, y),
27740
+ p(r - s * 2, y),
27741
+ p(r - s * 2, y + s),
27742
+ p(r - s, y + s),
27743
+ p(r - s, y + s * 2),
27744
+ p(r, y + s * 2),
27745
+ p(r, b - s * 2),
27746
+ p(r - s, b - s * 2),
27747
+ p(r - s, b - s),
27748
+ p(r - s * 2, b - s),
27749
+ p(r - s * 2, b),
27750
+ p(x + s * 2, b),
27751
+ p(x + s * 2, b - s),
27752
+ p(x + s, b - s),
27753
+ p(x + s, b - s * 2),
27754
+ p(x, b - s * 2)
27755
+ ];
27756
+ }
27757
+ function drawCardShadow(graphics, rect, style, shadow) {
27758
+ if (!shadow || shadow.alpha <= 0 || shadow.blur <= 0) return;
27759
+ const offset = {
27760
+ ...rect,
27761
+ y: rect.y + shadow.y
27762
+ };
27763
+ const reach = (index) => (1 - index / SHADOW_LAYERS) ** 2;
27764
+ for (let index = SHADOW_LAYERS; index >= 1; index -= 1) {
27765
+ const alpha = shadow.alpha * (reach(index - 1) - reach(index));
27766
+ traceCardOutline(graphics, offset, style, shadow.blur * index / SHADOW_LAYERS).fill({
27767
+ color: shadow.color,
27768
+ alpha
27769
+ });
27770
+ traceCardOutline(graphics, rect, style).cut();
27771
+ }
27772
+ }
27773
+ function cardShadow(style, elevation) {
27774
+ const shadow = style.shadow;
27775
+ if (!shadow || elevation === 0) return null;
27776
+ if (elevation === 1) return shadow;
27777
+ return {
27778
+ ...shadow,
27779
+ alpha: Math.min(1, shadow.alpha * 1.6),
27780
+ blur: shadow.blur * 1.6,
27781
+ y: shadow.y * 2 + 4
27782
+ };
27783
+ }
27784
+ function drawCardSurface(graphics, rect, style, surface) {
27785
+ drawCardShadow(graphics, rect, style, cardShadow(style, surface.elevation ?? 0));
27786
+ traceCardOutline(graphics, rect, style).fill({
27787
+ color: surface.fill,
27788
+ alpha: surface.fillAlpha ?? 1
27789
+ });
27790
+ if (surface.border != null) traceCardOutline(graphics, rect, style).stroke({
27791
+ color: surface.border,
27792
+ width: 1,
27793
+ alpha: surface.borderAlpha ?? 1,
27794
+ alignment: 1
27795
+ });
27796
+ return graphics;
27797
+ }
27798
+ function containRect(width, height, imageWidth, imageHeight) {
27799
+ if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return {
27800
+ x: 0,
27801
+ y: 0,
27802
+ width: 0,
27803
+ height: 0
27804
+ };
27805
+ const scale = Math.min(width / imageWidth, height / imageHeight);
27806
+ return {
27807
+ x: (width - imageWidth * scale) / 2,
27808
+ y: (height - imageHeight * scale) / 2,
27809
+ width: imageWidth * scale,
27810
+ height: imageHeight * scale
27811
+ };
27812
+ }
27813
+ function cardElevation(context, id) {
27814
+ return context.liftedIds.has(id) ? 2 : 1;
27317
27815
  }
27318
27816
  //#endregion
27319
27817
  //#region ../sdk/src/board/render/renderers/base-card-renderer.ts
@@ -27325,7 +27823,10 @@ function createLabel(text, style) {
27325
27823
  return new Text({
27326
27824
  ...TEXT_OPTIONS,
27327
27825
  text,
27328
- style
27826
+ style: {
27827
+ fontFamily: BOARD_FONT_STACK,
27828
+ ...style
27829
+ }
27329
27830
  });
27330
27831
  }
27331
27832
  function positionShell(root, item) {
@@ -27407,1201 +27908,310 @@ function drawFarStroke(graphics, points, style) {
27407
27908
  });
27408
27909
  }
27409
27910
  //#endregion
27410
- //#region ../sdk/src/board/render/renderers/file-card-renderer.ts
27411
- const RADIUS$5 = 4;
27412
- const PADDING$1 = 10;
27413
- const COVER_RATIO = .56;
27414
- const MIN_BODY_FOR_COVER = 38;
27415
- const TITLE_SIZE = 13;
27416
- const TITLE_LINE = TITLE_SIZE * 1.35;
27417
- const TITLE_MAX_LINES = 2;
27418
- const EXCERPT_SIZE = 11;
27419
- const EXCERPT_LINE = EXCERPT_SIZE * 1.45;
27420
- const EXCERPT_MAX_LINES = 3;
27421
- const TYPE_MARK_SIZE = 28;
27422
- const GAP = 4;
27423
- const STRIPE = 2;
27424
- const LOD_TITLE_ZOOM = .35;
27425
- const LOD_BODY_ZOOM = .6;
27426
- const MAX_DASHES = 40;
27427
- const partsByContainer$12 = /* @__PURE__ */ new WeakMap();
27428
- function detailFor(zoom) {
27429
- if (zoom < LOD_TITLE_ZOOM) return "plate";
27430
- if (zoom < LOD_BODY_ZOOM) return "title";
27431
- return "full";
27432
- }
27433
- function coverHeight(item, height) {
27434
- if (filePreviewKind(item.props.snapshot) !== "cover") return 0;
27435
- const ideal = Math.round(height * COVER_RATIO);
27436
- return Math.max(0, Math.min(ideal, height - MIN_BODY_FOR_COVER));
27437
- }
27438
- function fitLineWithEllipsis(line, wrapWidth, measureWidth) {
27439
- const trimmed = line.replace(/\s+$/u, "");
27440
- if (!trimmed) return "…";
27441
- const full = `${trimmed}…`;
27442
- if (measureWidth(full) <= wrapWidth) return full;
27443
- let lo = 0;
27444
- let hi = trimmed.length;
27445
- let best = "…";
27446
- while (lo <= hi) {
27447
- const mid = lo + hi >> 1;
27448
- const candidate = mid > 0 ? `${trimmed.slice(0, mid)}…` : "…";
27449
- if (measureWidth(candidate) <= wrapWidth) {
27450
- best = candidate;
27451
- lo = mid + 1;
27452
- } else hi = mid - 1;
27453
- }
27454
- return best;
27455
- }
27456
- function fitTextToLines(text, value, maxLines, wrapWidth) {
27457
- const width = Math.max(1, wrapWidth);
27458
- text.style.wordWrapWidth = width;
27459
- if (!value || maxLines <= 0) {
27460
- if (text.text !== "") text.text = "";
27461
- return;
27462
- }
27463
- const charCap = Math.max(maxLines * Math.ceil(width), maxLines * 4);
27464
- const sample = value.length > charCap ? value.slice(0, charCap) : value;
27465
- const metrics = CanvasTextMetrics.measureText(sample, text.style);
27466
- if (!(metrics.lines.length > maxLines || sample !== value)) {
27467
- if (text.text !== value) text.text = value;
27911
+ //#region ../sdk/src/board/render/renderers/media-key.ts
27912
+ const MEDIA_SCRIM = 0;
27913
+ const MEDIA_INK = 16777215;
27914
+ function drawMediaPlayKey(graphics, cx, cy, radius, context, solid = false) {
27915
+ const face = solid ? context.palette.text : MEDIA_SCRIM;
27916
+ const ink = solid ? context.palette.surface : MEDIA_INK;
27917
+ if (context.style.corner === "pixel") {
27918
+ const size = Math.round(radius * 1.6);
27919
+ tracePixelRect(graphics, Math.round(cx - size / 2), Math.round(cy - size / 2), size, size, 2).fill({
27920
+ color: face,
27921
+ alpha: solid ? 1 : .62
27922
+ });
27923
+ const cell = Math.max(1, Math.round(size / 14));
27924
+ const x = Math.round(cx - cell * 1.5);
27925
+ const y = Math.round(cy - cell * 3.5);
27926
+ for (let row = 0; row < 7; row += 1) graphics.rect(x, y + row * cell, (4 - Math.abs(row - 3)) * cell, cell);
27927
+ graphics.fill({ color: ink });
27468
27928
  return;
27469
27929
  }
27470
- const keptCount = Math.min(maxLines, metrics.lines.length);
27471
- const kept = metrics.lines.slice(0, keptCount);
27472
- const lastIndex = kept.length - 1;
27473
- const probe = text.style.clone();
27474
- probe.wordWrap = false;
27475
- kept[lastIndex] = fitLineWithEllipsis(kept[lastIndex] ?? "", width, (candidate) => CanvasTextMetrics.measureText(candidate, probe).width);
27476
- const fitted = kept.join("\n");
27477
- if (text.text !== fitted) text.text = fitted;
27478
- }
27479
- function linesInRoom(room, lineHeight, max) {
27480
- if (room < lineHeight * .85) return 0;
27481
- return Math.max(0, Math.min(max, Math.floor((room + .5) / lineHeight)));
27930
+ graphics.circle(cx, cy, radius).fill({
27931
+ color: face,
27932
+ alpha: solid ? 1 : .45
27933
+ });
27934
+ const size = radius * .5;
27935
+ graphics.poly([
27936
+ cx - size * .45,
27937
+ cy - size * .75,
27938
+ cx + size * .8,
27939
+ cy,
27940
+ cx - size * .45,
27941
+ cy + size * .75
27942
+ ]).fill({
27943
+ color: ink,
27944
+ alpha: .96
27945
+ });
27482
27946
  }
27483
- function containCoverRect(width, height, imageWidth, imageHeight) {
27484
- if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return {
27485
- x: 0,
27486
- y: 0,
27487
- width: 0,
27488
- height: 0
27489
- };
27490
- const scale = Math.min(width / imageWidth, height / imageHeight);
27491
- const renderedWidth = imageWidth * scale;
27492
- const renderedHeight = imageHeight * scale;
27947
+ const GAP$1 = 12;
27948
+ const WAVE_HEIGHT = 16;
27949
+ const partsByContainer$12 = /* @__PURE__ */ new WeakMap();
27950
+ function audioKeyCenter(width, height) {
27951
+ const radius = Math.min(16, Math.max(8, height / 2 - 8), width / 4);
27493
27952
  return {
27494
- x: (width - renderedWidth) / 2,
27495
- y: (height - renderedHeight) / 2,
27496
- width: renderedWidth,
27497
- height: renderedHeight
27953
+ x: 14 + radius,
27954
+ y: height / 2,
27955
+ radius
27498
27956
  };
27499
27957
  }
27500
- function layoutCover(sprite, width, height, texture) {
27501
- const rect = containCoverRect(width, height, texture.width, texture.height);
27502
- sprite.position.set(rect.x, rect.y);
27503
- sprite.width = rect.width;
27504
- sprite.height = rect.height;
27505
- }
27506
- function syncClip(parts, width, height) {
27507
- parts.clip.clear().roundRect(0, 0, Math.max(1, width), Math.max(1, height), RADIUS$5).fill({ color: 16777215 });
27508
- }
27509
27958
  function sync$12(container, item, context) {
27510
27959
  const parts = partsByContainer$12.get(container);
27511
27960
  if (!parts) return;
27512
27961
  positionShell(parts.root, item);
27513
27962
  const { width, height } = item.frame;
27514
- const selected = context.selectedIds.has(item.id);
27515
- const hovered = context.hoveredId === item.id;
27516
- const detail = detailFor(context.zoom);
27517
- const key = context.assetKey(item);
27518
- const texture = key ? context.getTexture(key) : null;
27519
- const coverFailed = Boolean(key && !texture && context.hasError(key));
27520
- const fileState = context.fileState(item.props.src);
27521
- const band = coverHeight(item, height);
27522
- const kind = filePreviewKind(item.props.snapshot);
27523
- const category = fileCategory(item.props.src, item.props.snapshot?.mimeType);
27524
- const accent = fileCategoryAccent(category, context.palette);
27525
- syncTextResolution(parts.title, parts.titleRes, context.zoom);
27526
- syncTextResolution(parts.excerpt, parts.excerptRes, context.zoom);
27527
- syncTextResolution(parts.typeMark, parts.typeMarkRes, context.zoom);
27528
- const visualSig = [
27529
- key ?? "",
27963
+ const title = item.props.snapshot?.title ?? getMediaResourceTitle(item.props.src);
27964
+ const durationMs = item.props.snapshot?.durationMs;
27965
+ const detailed = boardCardDetailed(context.zoom);
27966
+ syncTextResolution(parts.title, parts, context.zoom);
27967
+ const elevation = cardElevation(context, item.id);
27968
+ const sig = [
27530
27969
  width,
27531
27970
  height,
27532
- selected,
27533
- hovered,
27534
- detail,
27535
- texture ? `${texture.width}x${texture.height}` : "none",
27536
- coverFailed,
27537
- fileState,
27538
- kind,
27539
- category,
27540
- context.colorScheme,
27541
- context.palette.surface,
27542
- context.palette.hover,
27543
- accent
27971
+ title,
27972
+ durationMs ?? "",
27973
+ detailed,
27974
+ elevation,
27975
+ item.props.src,
27976
+ item.props.snapshot?.mtimeMs ?? "",
27977
+ context.themeKey
27544
27978
  ].join("|");
27545
- if (visualSig !== parts.visualSig) {
27546
- parts.visualSig = visualSig;
27547
- syncClip(parts, width, height);
27548
- parts.plate.clear();
27549
- parts.plate.roundRect(0, 0, width, height, RADIUS$5).fill({
27550
- color: context.palette.surface,
27551
- alpha: .98
27552
- }).roundRect(0, 0, width, height, RADIUS$5).stroke({
27553
- color: selected ? context.palette.brand : hovered ? context.palette.muted : context.palette.border,
27554
- width: selected ? 2 : 1,
27555
- alpha: selected ? .95 : .85
27979
+ if (sig === parts.sig) return;
27980
+ parts.sig = sig;
27981
+ drawCardSurface(parts.plate.clear(), {
27982
+ x: 0,
27983
+ y: 0,
27984
+ width,
27985
+ height
27986
+ }, context.style, {
27987
+ fill: context.palette.surface,
27988
+ border: context.palette.border,
27989
+ borderAlpha: .85,
27990
+ elevation
27991
+ });
27992
+ const key = audioKeyCenter(width, height);
27993
+ drawMediaPlayKey(parts.plate, key.x, key.y, key.radius, context, true);
27994
+ const left = key.x + key.radius + GAP$1;
27995
+ const room = width - 14 - left;
27996
+ parts.title.visible = detailed && room > 24;
27997
+ parts.wave.clear();
27998
+ if (!parts.title.visible) return;
27999
+ parts.title.style.fontFamily = context.style.font;
28000
+ parts.title.style.fill = context.palette.text;
28001
+ fitTextToWidth(parts.title, durationMs ? `${title} · ${formatBoardDuration(durationMs)}` : title, room);
28002
+ const withWave = height >= 56;
28003
+ const top = (height - (parts.title.height + (withWave ? 22 : 0))) / 2;
28004
+ parts.title.position.set(left, top);
28005
+ if (withWave) drawAudioWaveform(parts.wave, `${item.props.src}:${item.props.snapshot?.mtimeMs ?? "unknown"}`, {
28006
+ x: left,
28007
+ y: top + parts.title.height + 6,
28008
+ width: room,
28009
+ height: WAVE_HEIGHT
28010
+ }, context.palette.muted, .45, context.style.corner === "pixel");
28011
+ }
28012
+ const audioCardRenderer = {
28013
+ id: "audio-card",
28014
+ canRender: (item) => item.type === "audio",
28015
+ create: (item, context) => {
28016
+ const root = new Container();
28017
+ const plate = new Graphics();
28018
+ const wave = new Graphics();
28019
+ const resolution = textResolutionForZoom(context.zoom);
28020
+ const title = new Text({
28021
+ text: "",
28022
+ style: {
28023
+ fontSize: 13,
28024
+ fontWeight: "600"
28025
+ },
28026
+ resolution,
28027
+ roundPixels: true
27556
28028
  });
27557
- if (fileState !== "ok") {
27558
- const dash = Math.max(6, width / MAX_DASHES / 2);
27559
- for (let x = 0; x < width; x += dash * 2) parts.plate.moveTo(x, .5).lineTo(Math.min(x + dash, width), .5);
27560
- parts.plate.stroke({
27561
- color: context.palette.muted,
27562
- width: 2,
27563
- alpha: fileState === "missing" ? .9 : .5
27564
- });
27565
- }
27566
- const showCover = band > 0 && Boolean(texture);
27567
- if (band > 0) parts.plate.rect(1, 1, width - 2, band - 1).fill({
27568
- color: context.palette.hover,
27569
- alpha: coverFailed ? .35 : .6
28029
+ root.addChild(plate, wave, title);
28030
+ partsByContainer$12.set(root, {
28031
+ root,
28032
+ plate,
28033
+ wave,
28034
+ title,
28035
+ sig: "",
28036
+ resolution
27570
28037
  });
27571
- if (showCover && texture) {
27572
- if (parts.cover.texture !== texture) parts.cover.texture = texture;
27573
- layoutCover(parts.cover, width, band, texture);
27574
- parts.coverMask.clear().roundRect(0, 0, width, band, RADIUS$5).fill({ color: 16777215 });
27575
- parts.coverMask.rect(0, band - RADIUS$5, width, RADIUS$5).fill({ color: 16777215 });
27576
- }
27577
- parts.cover.visible = showCover;
27578
- parts.coverMask.visible = showCover;
27579
- if (!band) parts.plate.rect(1, 1, STRIPE, height - 2).fill({
27580
- color: accent,
27581
- alpha: .55
28038
+ if (item.type === "audio") sync$12(root, item, context);
28039
+ return root;
28040
+ },
28041
+ update: (container, item, context) => {
28042
+ if (item.type === "audio") sync$12(container, item, context);
28043
+ },
28044
+ renderFar: (graphics, item, context) => {
28045
+ drawFarPlate(graphics, item.frame, {
28046
+ fill: context.palette.surface,
28047
+ fillAlpha: .96,
28048
+ accent: context.palette.brand,
28049
+ accentAlpha: .72
27582
28050
  });
27583
- parts.title.visible = detail !== "plate";
27584
- parts.excerpt.visible = detail === "full";
27585
- parts.typeMark.visible = kind === "blank" && detail !== "plate";
27586
- }
27587
- if (detail === "plate") {
27588
- parts.typeMark.visible = false;
27589
- return;
28051
+ },
28052
+ destroy: (container) => {
28053
+ partsByContainer$12.get(container)?.root.destroy({ children: true });
28054
+ partsByContainer$12.delete(container);
27590
28055
  }
27591
- const title = item.props.snapshot?.title || fileStem(item.props.src);
27592
- const excerpt = item.props.snapshot?.excerpt ?? "";
27593
- const mark = fileTypeLabel(item.props.src);
27594
- const innerWidth = Math.max(1, width - 20);
27595
- const showTypeMark = kind === "blank";
27596
- const textSig = [
27597
- title,
27598
- excerpt,
27599
- mark,
27600
- detail,
27601
- innerWidth,
27602
- band,
27603
- height,
27604
- kind,
27605
- context.palette.text,
27606
- context.palette.muted,
27607
- accent
27608
- ].join("|");
27609
- if (textSig === parts.textSig) return;
27610
- parts.textSig = textSig;
27611
- const top = band > 0 ? band + PADDING$1 * .8 : PADDING$1;
27612
- const contentBottom = height - PADDING$1;
27613
- let cursor = top;
27614
- if (showTypeMark) {
27615
- const markSize = Math.max(18, Math.min(TYPE_MARK_SIZE, Math.round(height * .22)));
27616
- parts.typeMark.style.fill = accent;
27617
- parts.typeMark.style.fontSize = markSize;
27618
- parts.typeMark.style.lineHeight = markSize * 1.1;
27619
- if (parts.typeMark.text !== mark) parts.typeMark.text = mark;
27620
- parts.typeMark.position.set(PADDING$1, cursor);
27621
- parts.typeMark.visible = true;
27622
- cursor += parts.typeMark.height + GAP;
27623
- } else parts.typeMark.visible = false;
27624
- const titleLines = linesInRoom(Math.max(0, contentBottom - cursor), TITLE_LINE, TITLE_MAX_LINES);
27625
- parts.title.style.fill = context.palette.text;
27626
- fitTextToLines(parts.title, title, titleLines, innerWidth);
27627
- parts.title.position.set(PADDING$1, cursor);
27628
- parts.title.visible = titleLines > 0;
27629
- if (detail !== "full") {
27630
- parts.excerpt.visible = false;
27631
- return;
28056
+ };
28057
+ //#endregion
28058
+ //#region ../sdk/src/board/render/renderers/arrow-card-renderer.ts
28059
+ const partsByContainer$11 = /* @__PURE__ */ new WeakMap();
28060
+ function sync$11(container, item, context) {
28061
+ const parts = partsByContainer$11.get(container);
28062
+ if (!parts) return;
28063
+ parts.root.position.set(0, 0);
28064
+ parts.root.rotation = 0;
28065
+ parts.root.scale.set(1);
28066
+ const { props, style } = item;
28067
+ parts.line.alpha = itemAlpha(context, style.stroke);
28068
+ const color = itemColor(context, style.stroke, "brand");
28069
+ const size = style.strokeWidth ?? 2.5;
28070
+ syncTextResolution(parts.label, parts, context.zoom);
28071
+ const resolved = resolveSceneArrow(item, context.scene);
28072
+ const path = trimPolyline(resolved.path, style.trim);
28073
+ const lineSig = [
28074
+ path.map((point) => `${point.x},${point.y}`).join(";"),
28075
+ color,
28076
+ size,
28077
+ style.dash,
28078
+ props.arrowStart,
28079
+ props.arrowEnd
28080
+ ].join("|");
28081
+ if (lineSig !== parts.lineSig) {
28082
+ parts.lineSig = lineSig;
28083
+ parts.line.clear();
28084
+ const width = size;
28085
+ const stroke = {
28086
+ color,
28087
+ width,
28088
+ alpha: .92,
28089
+ cap: "round",
28090
+ join: "round"
28091
+ };
28092
+ tracePolyline(parts.line, path, dashPattern(style.dash, width));
28093
+ parts.line.stroke(stroke);
28094
+ const first = path[0];
28095
+ const last = path[path.length - 1];
28096
+ if (first && last) {
28097
+ const span = Math.hypot(last.x - first.x, last.y - first.y);
28098
+ const headSize = Math.min(Math.max(14, size * 5.5), Math.max(10, span * .28));
28099
+ const complete = style.trim === void 0 || style.trim >= 1;
28100
+ const endDirection = endAngle(path, false);
28101
+ const startDirection = endAngle(path, true);
28102
+ if (props.arrowEnd && endDirection !== null && path.length > 1) traceArrowhead(parts.line, last, endDirection, headSize);
28103
+ if (props.arrowStart && startDirection !== null && complete) traceArrowhead(parts.line, first, startDirection, headSize);
28104
+ parts.line.stroke({
28105
+ ...stroke,
28106
+ width: Math.max(1.5, width)
28107
+ });
28108
+ }
27632
28109
  }
27633
- const excerptTop = cursor + (titleLines > 0 ? parts.title.height + GAP : 0);
27634
- const excerptLines = linesInRoom(contentBottom - excerptTop, EXCERPT_LINE, EXCERPT_MAX_LINES);
27635
- const showExcerpt = Boolean(excerpt) && excerptLines > 0;
27636
- if (showExcerpt) {
27637
- parts.excerpt.style.fill = context.palette.muted;
27638
- fitTextToLines(parts.excerpt, excerpt, excerptLines, innerWidth);
27639
- parts.excerpt.position.set(PADDING$1, excerptTop);
28110
+ const labelSig = [
28111
+ props.label,
28112
+ props.fontSize,
28113
+ context.themeKey
28114
+ ].join("|");
28115
+ if (labelSig !== parts.labelSig) {
28116
+ parts.labelSig = labelSig;
28117
+ parts.label.text = props.label;
28118
+ parts.label.style.fill = context.palette.text;
28119
+ parts.label.style.fontSize = props.fontSize;
28120
+ const padX = props.fontSize * .5;
28121
+ const padY = props.fontSize * .25;
28122
+ const { width, height } = parts.label;
28123
+ const plate = {
28124
+ x: -width / 2 - padX,
28125
+ y: -height / 2 - padY,
28126
+ width: width + padX * 2,
28127
+ height: height + padY * 2
28128
+ };
28129
+ drawCardSurface(parts.backdrop.clear(), plate, {
28130
+ ...context.style,
28131
+ radius: Math.min(context.style.radius, plate.height / 2)
28132
+ }, {
28133
+ fill: context.palette.surface,
28134
+ border: context.palette.border
28135
+ });
27640
28136
  }
27641
- parts.excerpt.visible = showExcerpt;
28137
+ const visible = props.label.length > 0 && (style.trim === void 0 || style.trim >= .5);
28138
+ parts.label.visible = visible;
28139
+ parts.backdrop.visible = visible;
28140
+ const mid = style.trim === void 0 ? resolved.mid : pathPointAt(resolved.path, .5);
28141
+ parts.label.position.set(mid.x, mid.y);
28142
+ parts.backdrop.position.set(mid.x, mid.y);
27642
28143
  }
27643
- const fileCardRenderer = {
27644
- id: "file-card",
27645
- canRender: (item) => item.type === "file",
28144
+ const arrowCardRenderer = {
28145
+ id: "arrow-card",
28146
+ canRender: (item) => item.type === "arrow",
27646
28147
  create: (item, context) => {
27647
28148
  const root = new Container();
27648
- const plate = new Graphics();
27649
- const body = new Container();
27650
- const clip = new Graphics();
27651
- const cover = new Sprite(Texture.EMPTY);
27652
- const coverMask = new Graphics();
27653
- cover.mask = coverMask;
27654
- const resolution = 1;
27655
- const title = new Text({
27656
- text: "",
27657
- style: {
27658
- fill: context.palette.text,
27659
- fontFamily: BOARD_FONT_STACK,
27660
- fontSize: TITLE_SIZE,
27661
- fontWeight: "600",
27662
- wordWrap: true,
27663
- breakWords: true,
27664
- lineHeight: TITLE_LINE
27665
- },
27666
- resolution,
27667
- roundPixels: true
27668
- });
27669
- const excerpt = new Text({
28149
+ const line = new Graphics();
28150
+ const resolution = textResolutionForZoom(context.zoom);
28151
+ const label = new Text({
27670
28152
  text: "",
27671
28153
  style: {
27672
- fill: context.palette.muted,
28154
+ fill: 16777215,
27673
28155
  fontFamily: BOARD_FONT_STACK,
27674
- fontSize: EXCERPT_SIZE,
27675
- fontWeight: "400",
27676
- wordWrap: true,
27677
- breakWords: true,
27678
- lineHeight: EXCERPT_LINE
27679
- },
27680
- resolution,
27681
- roundPixels: true
27682
- });
27683
- const typeMark = new Text({
27684
- text: "",
27685
- style: {
27686
- fill: context.palette.muted,
27687
- fontFamily: BOARD_MONO_FONT_STACK,
27688
- fontSize: TYPE_MARK_SIZE,
27689
- fontWeight: "700",
27690
- lineHeight: TYPE_MARK_SIZE * 1.1
28156
+ fontSize: 14,
28157
+ fontWeight: "500"
27691
28158
  },
27692
28159
  resolution,
27693
28160
  roundPixels: true
27694
28161
  });
27695
- body.mask = clip;
27696
- body.addChild(cover, coverMask, typeMark, title, excerpt);
27697
- root.addChild(plate, body, clip);
27698
- partsByContainer$12.set(root, {
28162
+ label.anchor.set(.5);
28163
+ const backdrop = new Graphics();
28164
+ root.addChild(line, backdrop, label);
28165
+ partsByContainer$11.set(root, {
27699
28166
  root,
27700
- plate,
27701
- body,
27702
- clip,
27703
- cover,
27704
- coverMask,
27705
- typeMark,
27706
- title,
27707
- excerpt,
27708
- visualSig: "",
27709
- textSig: "",
27710
- titleRes: { resolution },
27711
- excerptRes: { resolution },
27712
- typeMarkRes: { resolution }
28167
+ line,
28168
+ backdrop,
28169
+ label,
28170
+ lineSig: "",
28171
+ labelSig: "",
28172
+ resolution
27713
28173
  });
27714
- if (item.type === "file") sync$12(root, item, context);
28174
+ if (item.type === "arrow") sync$11(root, item, context);
27715
28175
  return root;
27716
28176
  },
27717
28177
  update: (container, item, context) => {
27718
- if (item.type === "file") sync$12(container, item, context);
28178
+ if (item.type === "arrow") sync$11(container, item, context);
27719
28179
  },
27720
28180
  renderFar: (graphics, item, context) => {
27721
- const category = fileCategory(item.type === "file" ? item.props.src : "", item.type === "file" ? item.props.snapshot?.mimeType : void 0);
27722
- drawFarPlate(graphics, item.frame, {
27723
- fill: context.palette.surface,
27724
- fillAlpha: .96,
27725
- accent: fileCategoryAccent(category, context.palette),
27726
- accentAlpha: .45
28181
+ if (item.type !== "arrow") return;
28182
+ const arrow = item;
28183
+ drawFarStroke(graphics, trimPolyline(resolveSceneArrow(arrow, context.scene).path, arrow.style.trim), {
28184
+ color: itemColor(context, arrow.style.stroke, "brand"),
28185
+ width: arrow.style.strokeWidth ?? 2.5,
28186
+ alpha: .85 * itemAlpha(context, arrow.style.stroke)
27727
28187
  });
27728
28188
  },
27729
28189
  destroy: (container) => {
27730
- partsByContainer$12.get(container)?.root.destroy({ children: true });
27731
- partsByContainer$12.delete(container);
28190
+ partsByContainer$11.get(container)?.root.destroy({ children: true });
28191
+ partsByContainer$11.delete(container);
27732
28192
  }
27733
28193
  };
27734
28194
  //#endregion
27735
- //#region ../sdk/src/board/render/renderers/task-card-renderer.ts
27736
- const RADIUS$4 = 4;
27737
- const PADDING = 12;
27738
- const META_HEIGHT = 28;
27739
- const TASK_CARD_FULL_DETAIL_ZOOM = .55;
27740
- const PLAY_BADGE_RADIUS = 15;
27741
- const SHIMMER_PERIOD_MS = 1600;
27742
- const SHIMMER_FRAME_MS = 33;
27743
- const REVEAL_MS = 180;
27744
- const SECOND = 1e3;
27745
- const partsByContainer$11 = /* @__PURE__ */ new WeakMap();
27746
- function previewKindFor(artifact) {
27747
- if (!artifact) return "empty";
27748
- if (artifact.type === "image" || artifact.type === "video") return "texture";
27749
- if (artifact.type === "audio") return artifact.previewUrl ? "texture" : "waveform";
27750
- return artifact.textExcerpt ? "text" : "empty";
27751
- }
27752
- function liveStatus(item, context) {
27753
- const { snapshot } = item.props;
27754
- const live = snapshot.status === "completed" ? null : context.taskRun(item.props.taskRunId);
27755
- const liveStart = Date.parse(live?.startedAt ?? "");
27756
- const working = live?.status === "pending" || live?.status === "running";
27757
- const adopt = snapshot.status === "failed" ? working && liveStart > Date.parse(snapshot.updatedAt ?? "") : live?.status === "running" || live?.status === "pending" && snapshot.status === "pending";
27758
- const status = adopt && live ? live.status : snapshot.status;
27759
- const started = adopt ? liveStart : Date.parse(snapshot.startedAt ?? "");
27760
- return {
27761
- status,
27762
- startedAt: Number.isFinite(started) ? started : null
27763
- };
27764
- }
27765
- function stateSurfaceFor(status, kind, hasTexture, previewFailed) {
27766
- if (kind === "texture" && !hasTexture) {
27767
- if (status === "failed") return "failed";
27768
- return previewFailed ? "unavailable" : "media-loading";
28195
+ //#region ../sdk/src/board/render/renderers/draw-card-renderer.ts
28196
+ const partsByContainer$10 = /* @__PURE__ */ new WeakMap();
28197
+ function fillRibbon(graphics, ribbon, color) {
28198
+ const { positions, indices } = ribbon;
28199
+ for (let offset = 0; offset < indices.length; offset += 3) {
28200
+ const a = (indices[offset] ?? 0) * 2;
28201
+ const b = (indices[offset + 1] ?? 0) * 2;
28202
+ const c = (indices[offset + 2] ?? 0) * 2;
28203
+ graphics.moveTo(positions[a] ?? 0, positions[a + 1] ?? 0).lineTo(positions[b] ?? 0, positions[b + 1] ?? 0).lineTo(positions[c] ?? 0, positions[c + 1] ?? 0).closePath();
27769
28204
  }
27770
- if (kind !== "empty") return null;
27771
- if (status === "failed") return "failed";
27772
- if (status === "running") return "generating";
27773
- if (status === "pending") return "queued";
27774
- return "empty";
28205
+ return graphics.fill({ color });
27775
28206
  }
27776
- function stateLabel(surface, context) {
27777
- switch (surface) {
27778
- case "generating": return context.labels.generating;
27779
- case "queued": return context.labels.queued;
27780
- case "failed": return context.labels.failed;
27781
- case "unavailable": return context.labels.previewUnavailable;
27782
- case "empty": return context.labels.noPreview;
27783
- default: return "";
28207
+ function replaceStroke(parts, next) {
28208
+ if (parts.stroke) {
28209
+ parts.root.removeChild(parts.stroke);
28210
+ parts.stroke.destroy();
27784
28211
  }
27785
- }
27786
- function statusColor(status, context) {
27787
- if (status === "failed") return context.colors.rose.stroke;
27788
- if (status === "running") return context.colors.brand.stroke;
27789
- return context.colors.neutral.stroke;
27790
- }
27791
- function formatTaskElapsed(ms) {
27792
- const total = Math.max(0, Math.floor(ms / SECOND));
27793
- const hours = Math.floor(total / 3600);
27794
- const minutes = Math.floor(total % 3600 / 60);
27795
- const seconds = String(total % 60).padStart(2, "0");
27796
- return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
27797
- }
27798
- function containTaskPreviewRect(width, height, imageWidth, imageHeight) {
27799
- if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return {
27800
- x: 0,
27801
- y: 0,
27802
- width: 0,
27803
- height: 0
27804
- };
27805
- const scale = Math.min(width / imageWidth, height / imageHeight);
27806
- const renderedWidth = imageWidth * scale;
27807
- const renderedHeight = imageHeight * scale;
27808
- return {
27809
- x: (width - renderedWidth) / 2,
27810
- y: (height - renderedHeight) / 2,
27811
- width: renderedWidth,
27812
- height: renderedHeight
27813
- };
27814
- }
27815
- function drawPlayBadge(graphics, rect, context) {
27816
- const radius = Math.min(PLAY_BADGE_RADIUS, Math.max(8, Math.min(rect.width, rect.height) * .16));
27817
- const cx = rect.x + rect.width / 2;
27818
- const cy = rect.y + rect.height / 2;
27819
- graphics.circle(cx, cy, radius).fill({
27820
- color: context.palette.bg,
27821
- alpha: .64
27822
- });
27823
- const size = radius * .72;
27824
- graphics.moveTo(cx - size * .34, cy - size * .56).lineTo(cx + size * .62, cy).lineTo(cx - size * .34, cy + size * .56).closePath().fill({
27825
- color: context.palette.text,
27826
- alpha: .94
27827
- });
27828
- }
27829
- function drawStateMark(graphics, surface, cx, cy, color) {
27830
- if (surface === "generating" || surface === "media-loading") {
27831
- graphics.circle(cx, cy, 4).fill({
27832
- color,
27833
- alpha: .9
27834
- });
27835
- graphics.circle(cx - 12, cy + 7, 2.5).fill({
27836
- color,
27837
- alpha: .48
27838
- });
27839
- graphics.circle(cx + 11, cy + 6, 3).fill({
27840
- color,
27841
- alpha: .68
27842
- });
27843
- graphics.circle(cx + 3, cy - 12, 2).fill({
27844
- color,
27845
- alpha: .36
27846
- });
27847
- return;
27848
- }
27849
- if (surface === "queued") {
27850
- graphics.circle(cx, cy, 10).stroke({
27851
- color,
27852
- width: 1.5,
27853
- alpha: .62
27854
- }).circle(cx, cy, 3).fill({
27855
- color,
27856
- alpha: .78
27857
- });
27858
- return;
27859
- }
27860
- if (surface === "failed") {
27861
- graphics.circle(cx, cy, 11).stroke({
27862
- color,
27863
- width: 1.5,
27864
- alpha: .86
27865
- });
27866
- graphics.moveTo(cx - 4, cy - 4).lineTo(cx + 4, cy + 4).moveTo(cx + 4, cy - 4).lineTo(cx - 4, cy + 4).stroke({
27867
- color,
27868
- width: 1.5,
27869
- alpha: .94,
27870
- cap: "round"
27871
- });
27872
- return;
27873
- }
27874
- if (surface === "unavailable") graphics.roundRect(cx - 12, cy - 9, 24, 18, 3).stroke({
27875
- color,
27876
- width: 1.25,
27877
- alpha: .62
27878
- }).moveTo(cx - 8, cy + 5).lineTo(cx + 8, cy - 5).stroke({
27879
- color,
27880
- width: 1.25,
27881
- alpha: .72,
27882
- cap: "round"
27883
- });
27884
- }
27885
- function drawFailedBadge(graphics, width, color, context) {
27886
- const cx = Math.max(12, width - 12);
27887
- graphics.circle(cx, 12, 8).fill({
27888
- color: context.palette.bg,
27889
- alpha: .72
27890
- });
27891
- graphics.moveTo(cx - 2.5, 9.5).lineTo(cx + 2.5, 14.5).moveTo(cx + 2.5, 9.5).lineTo(cx - 2.5, 14.5).stroke({
27892
- color,
27893
- width: 1.5,
27894
- alpha: .96,
27895
- cap: "round"
27896
- });
27897
- }
27898
- const shimmerGradients = /* @__PURE__ */ new Map();
27899
- function shimmerGradient(color) {
27900
- let gradient = shimmerGradients.get(color);
27901
- if (!gradient) {
27902
- const rgb = `${color >> 16 & 255}, ${color >> 8 & 255}, ${color & 255}`;
27903
- gradient = new FillGradient({
27904
- type: "linear",
27905
- start: {
27906
- x: 0,
27907
- y: 0
27908
- },
27909
- end: {
27910
- x: 1,
27911
- y: 0
27912
- },
27913
- textureSpace: "local",
27914
- colorStops: [
27915
- {
27916
- offset: 0,
27917
- color: `rgba(${rgb}, 0)`
27918
- },
27919
- {
27920
- offset: .5,
27921
- color: `rgba(${rgb}, 0.07)`
27922
- },
27923
- {
27924
- offset: 1,
27925
- color: `rgba(${rgb}, 0)`
27926
- }
27927
- ]
27928
- });
27929
- shimmerGradients.set(color, gradient);
27930
- }
27931
- return gradient;
27932
- }
27933
- function drawShimmerBand(graphics, width, height, color) {
27934
- const band = Math.max(48, width * .45);
27935
- graphics.clear().rect(-band, 0, band, height).fill(shimmerGradient(color));
27936
- }
27937
- function advance(parts, clock) {
27938
- const { motion } = parts;
27939
- let next = null;
27940
- if (motion.shimmer) {
27941
- const travel = motion.width + parts.shimmer.width;
27942
- parts.shimmer.x = clock % SHIMMER_PERIOD_MS / SHIMMER_PERIOD_MS * travel;
27943
- next = SHIMMER_FRAME_MS;
27944
- }
27945
- if (motion.startedAt !== null) {
27946
- const elapsedMs = Date.now() - motion.startedAt;
27947
- const elapsed = formatTaskElapsed(elapsedMs);
27948
- if (elapsed !== motion.elapsed) {
27949
- motion.elapsed = elapsed;
27950
- parts.body.text = `${motion.label} · ${elapsed}`;
27951
- }
27952
- const untilNextSecond = SECOND - Math.max(0, elapsedMs) % SECOND;
27953
- next = next === null ? untilNextSecond : Math.min(next, untilNextSecond);
27954
- }
27955
- if (motion.revealFrom !== null) {
27956
- const progress = Math.min(1, (clock - motion.revealFrom) / REVEAL_MS);
27957
- parts.preview.alpha = progress * (2 - progress);
27958
- if (progress >= 1) motion.revealFrom = null;
27959
- else next = 0;
27960
- }
27961
- return next;
27962
- }
27963
- function sync$11(container, item, context) {
27964
- const parts = partsByContainer$11.get(container);
27965
- if (!parts) return;
27966
- positionShell(parts.root, item);
27967
- const { width, height } = item.frame;
27968
- const { snapshot } = item.props;
27969
- const { status, startedAt } = liveStatus(item, context);
27970
- const selected = context.selectedIds.has(item.id);
27971
- const hovered = context.hoveredId === item.id;
27972
- const full = context.zoom >= TASK_CARD_FULL_DETAIL_ZOOM;
27973
- const working = status === "pending" || status === "running";
27974
- const color = statusColor(status, context);
27975
- const key = context.assetKey(item);
27976
- if (key !== parts.assetKey) {
27977
- if (parts.assetKey) context.releaseTexture(parts.assetKey);
27978
- parts.assetKey = key;
27979
- if (key) context.acquireTexture(key);
27980
- }
27981
- const texture = key ? context.getTexture(key) : null;
27982
- const previewFailed = Boolean(key && !texture && context.hasError(key));
27983
- const artifact = featuredTaskArtifact(snapshot.artifacts);
27984
- const kind = previewKindFor(artifact);
27985
- const surface = stateSurfaceFor(status, kind, Boolean(texture), previewFailed);
27986
- const frame = {
27987
- x: 1,
27988
- y: 1,
27989
- width: Math.max(1, width - 2),
27990
- height: Math.max(1, height - 2)
27991
- };
27992
- const metaText = artifact || working ? snapshot.model ?? "" : "";
27993
- const showMeta = full && Boolean(metaText);
27994
- const failedWithOutput = status === "failed" && kind !== "empty";
27995
- const count = artifact && snapshot.artifactCount > 1 ? String(snapshot.artifactCount) : "";
27996
- const showBadge = full && Boolean(count);
27997
- const label = stateLabel(surface, context);
27998
- const counting = full && working && startedAt !== null && surface !== null;
27999
- const elapsed = counting ? formatTaskElapsed(Date.now() - startedAt) : "";
28000
- const detailText = !full || !surface ? "" : status === "failed" ? snapshot.errorExcerpt ?? "" : working ? snapshot.promptExcerpt ?? "" : "";
28001
- const textArtifact = kind === "text" && artifact?.type === "text";
28002
- const bodyText = textArtifact ? artifact.textExcerpt : counting ? `${label} · ${elapsed}` : label;
28003
- syncTextResolution(parts.body, parts, context.zoom);
28004
- syncTextResolution(parts.detail, parts, context.zoom);
28005
- syncTextResolution(parts.meta, parts, context.zoom);
28006
- syncTextResolution(parts.badge, parts, context.zoom);
28007
- const textSig = [
28008
- counting ? label : bodyText,
28009
- detailText,
28010
- metaText,
28011
- count,
28012
- kind,
28013
- surface,
28014
- full,
28015
- width,
28016
- height,
28017
- color,
28018
- context.palette.text,
28019
- context.palette.muted
28020
- ].join("|");
28021
- if (textSig !== parts.textSig) {
28022
- parts.textSig = textSig;
28023
- const wrap = Math.max(24, width - 24);
28024
- parts.body.style.fill = surface === "failed" ? color : context.palette.text;
28025
- parts.body.anchor.set(textArtifact ? 0 : .5, 0);
28026
- fitTextToLines(parts.body, bodyText, textArtifact ? Math.max(1, Math.floor((height - 24 - (showMeta ? META_HEIGHT : 0)) / 16)) : 1, wrap);
28027
- parts.detail.style.fill = context.palette.muted;
28028
- fitTextToLines(parts.detail, detailText, height - 24 > 120 ? 2 : 1, Math.min(wrap, 360));
28029
- parts.meta.style.fill = context.palette.text;
28030
- fitTextToLines(parts.meta, metaText, 1, wrap);
28031
- parts.badge.style.fill = context.palette.text;
28032
- if (parts.badge.text !== count) parts.badge.text = count;
28033
- }
28034
- const badgeWidth = Math.max(22, parts.badge.width + 12);
28035
- const visualSig = [
28036
- width,
28037
- height,
28038
- selected,
28039
- hovered,
28040
- full,
28041
- color,
28042
- kind,
28043
- surface,
28044
- showMeta,
28045
- showBadge && badgeWidth,
28046
- failedWithOutput,
28047
- artifact?.type ?? "none",
28048
- artifact?.id ?? "none",
28049
- item.props.taskRunId,
28050
- texture ? `${texture.width}x${texture.height}` : "none",
28051
- context.palette.surface,
28052
- context.palette.hover,
28053
- context.palette.border,
28054
- context.palette.bg,
28055
- context.palette.text
28056
- ].join("|");
28057
- if (visualSig !== parts.visualSig) {
28058
- parts.visualSig = visualSig;
28059
- parts.plate.clear().roundRect(0, 0, width, height, RADIUS$4).fill({
28060
- color: context.palette.surface,
28061
- alpha: .98
28062
- }).roundRect(0, 0, width, height, RADIUS$4).stroke({
28063
- color: selected ? context.palette.brand : hovered ? context.palette.muted : context.palette.border,
28064
- width: selected ? 2 : 1,
28065
- alpha: selected ? .96 : .82
28066
- });
28067
- parts.clip.clear().roundRect(1, 1, frame.width, frame.height, 3).fill({ color: 16777215 });
28068
- parts.previewBg.clear().rect(frame.x, frame.y, frame.width, frame.height).fill({
28069
- color: context.palette.hover,
28070
- alpha: .5
28071
- });
28072
- parts.previewMask.clear().rect(frame.x, frame.y, frame.width, frame.height).fill({ color: 16777215 });
28073
- drawShimmerBand(parts.shimmer, frame.width, frame.height, context.palette.text);
28074
- parts.shimmer.y = frame.y;
28075
- parts.previewArt.clear();
28076
- if (kind === "waveform") {
28077
- const bottomInset = showMeta ? META_HEIGHT : 0;
28078
- drawAudioWaveform(parts.previewArt, item.props.taskRunId, {
28079
- x: frame.x + PADDING,
28080
- y: frame.y + PADDING,
28081
- width: Math.max(1, frame.width - 24),
28082
- height: Math.max(1, frame.height - 24 - bottomInset)
28083
- }, context.colors.brand.stroke);
28084
- }
28085
- if (full && (artifact?.type === "audio" || artifact?.type === "video" && texture)) drawPlayBadge(parts.previewArt, frame, context);
28086
- if (surface) {
28087
- const markY = frame.y + frame.height / 2 - (full && label ? 18 : 0);
28088
- drawStateMark(parts.previewArt, surface, frame.x + frame.width / 2, markY, surface === "failed" ? color : context.colors.neutral.stroke);
28089
- }
28090
- if (showMeta) parts.previewArt.rect(frame.x, frame.y + frame.height - META_HEIGHT, frame.width, META_HEIGHT).fill({
28091
- color: context.palette.bg,
28092
- alpha: .68
28093
- });
28094
- if (showBadge) parts.previewArt.roundRect(width - badgeWidth - 8, 8, badgeWidth, 20, 10).fill({
28095
- color: context.palette.bg,
28096
- alpha: .72
28097
- });
28098
- if (failedWithOutput) drawFailedBadge(parts.previewArt, width, color, context);
28099
- }
28100
- const showPreview = kind === "texture" && Boolean(texture);
28101
- const revealKey = showPreview ? `${artifact?.id}:${texture?.uid}` : null;
28102
- const sameCard = parts.motion.itemId === item.id;
28103
- parts.motion.itemId = item.id;
28104
- if (revealKey !== parts.motion.revealKey || !sameCard) {
28105
- const appearing = sameCard && parts.motion.revealKey === null && revealKey !== null;
28106
- parts.motion.revealKey = revealKey;
28107
- parts.motion.revealFrom = appearing && !context.reducedMotion ? performance.now() : null;
28108
- parts.preview.alpha = parts.motion.revealFrom === null ? 1 : 0;
28109
- }
28110
- parts.preview.visible = showPreview;
28111
- if (showPreview && texture) {
28112
- if (parts.preview.texture !== texture) parts.preview.texture = texture;
28113
- const fitted = containTaskPreviewRect(frame.width, frame.height, texture.width, texture.height);
28114
- parts.preview.position.set(frame.x + fitted.x, frame.y + fitted.y);
28115
- parts.preview.width = fitted.width;
28116
- parts.preview.height = fitted.height;
28117
- }
28118
- parts.body.visible = full && Boolean(bodyText);
28119
- parts.detail.visible = full && Boolean(detailText);
28120
- parts.meta.visible = showMeta;
28121
- parts.badge.visible = showBadge;
28122
- if (textArtifact) parts.body.position.set(PADDING, PADDING);
28123
- else parts.body.position.set(width / 2, height / 2 + 2);
28124
- parts.detail.position.set(width / 2, height / 2 + 2 + parts.body.height + 4);
28125
- parts.meta.position.set(PADDING, height - META_HEIGHT + 7);
28126
- parts.badge.position.set(width - 8 - badgeWidth / 2, 18);
28127
- parts.motion.width = frame.width;
28128
- parts.motion.shimmer = full && working && kind !== "texture" && !context.reducedMotion;
28129
- parts.shimmer.visible = parts.motion.shimmer;
28130
- parts.motion.startedAt = counting ? startedAt : null;
28131
- parts.motion.label = label;
28132
- parts.motion.elapsed = elapsed;
28133
- }
28134
- const taskCardRenderer = {
28135
- id: "task-card",
28136
- canRender: (item) => item.type === "task",
28137
- create: (item, context) => {
28138
- const root = new Container();
28139
- const plate = new Graphics();
28140
- const clip = new Graphics();
28141
- const previewBg = new Graphics();
28142
- const preview = new Sprite();
28143
- const previewMask = new Graphics();
28144
- const shimmer = new Graphics();
28145
- const previewArt = new Graphics();
28146
- const resolution = textResolutionForZoom(context.zoom);
28147
- const text = (fontFamily, fontSize, fontWeight) => new Text({
28148
- text: "",
28149
- style: {
28150
- fontFamily,
28151
- fontSize,
28152
- fontWeight,
28153
- wordWrap: true,
28154
- breakWords: true
28155
- },
28156
- resolution,
28157
- roundPixels: true
28158
- });
28159
- const body = text(BOARD_FONT_STACK, 11.5, "500");
28160
- const detail = text(BOARD_FONT_STACK, 10.5, "400");
28161
- const meta = text(BOARD_MONO_FONT_STACK, 10, "500");
28162
- const badge = text(BOARD_MONO_FONT_STACK, 10, "500");
28163
- badge.style.wordWrap = false;
28164
- badge.anchor.set(.5);
28165
- detail.anchor.set(.5, 0);
28166
- detail.style.align = "center";
28167
- preview.mask = previewMask;
28168
- root.mask = clip;
28169
- root.addChild(plate, clip, previewBg, shimmer, preview, previewMask, previewArt, body, detail, meta, badge);
28170
- partsByContainer$11.set(root, {
28171
- root,
28172
- plate,
28173
- clip,
28174
- previewBg,
28175
- preview,
28176
- previewMask,
28177
- shimmer,
28178
- previewArt,
28179
- badge,
28180
- body,
28181
- detail,
28182
- meta,
28183
- assetKey: null,
28184
- visualSig: "",
28185
- textSig: "",
28186
- resolution,
28187
- motion: {
28188
- itemId: "",
28189
- width: 0,
28190
- shimmer: false,
28191
- startedAt: null,
28192
- label: "",
28193
- elapsed: "",
28194
- revealFrom: null,
28195
- revealKey: null
28196
- }
28197
- });
28198
- if (item.type === "task") sync$11(root, item, context);
28199
- return root;
28200
- },
28201
- update: (container, item, context) => {
28202
- if (item.type === "task") sync$11(container, item, context);
28203
- },
28204
- tick: (container, clock) => {
28205
- const parts = partsByContainer$11.get(container);
28206
- return parts ? advance(parts, clock) : null;
28207
- },
28208
- renderFar: (graphics, item, context) => {
28209
- if (item.type !== "task") return;
28210
- const { status } = liveStatus(item, context);
28211
- const active = status === "failed" || status === "running" || status === "pending";
28212
- drawFarPlate(graphics, item.frame, {
28213
- fill: context.palette.surface,
28214
- fillAlpha: .82,
28215
- ...active ? {
28216
- accent: statusColor(status, context),
28217
- accentAlpha: .9
28218
- } : {}
28219
- });
28220
- },
28221
- destroy: (container, context) => {
28222
- const parts = partsByContainer$11.get(container);
28223
- if (parts?.assetKey) context.releaseTexture(parts.assetKey);
28224
- parts?.root.destroy({ children: true });
28225
- partsByContainer$11.delete(container);
28226
- }
28227
- };
28228
- //#endregion
28229
- //#region ../sdk/src/board/render/stroke.ts
28230
- const HEAD_SPREAD = Math.PI / 6;
28231
- function dashPattern(dash, width) {
28232
- if (dash === "dashed") return [Math.max(6, width * 4), Math.max(4, width * 3)];
28233
- if (dash === "dotted") return [Math.max(.5, width * .01), Math.max(3, width * 2)];
28234
- return null;
28235
- }
28236
- function trimPolyline(path, trim) {
28237
- if (trim === void 0 || trim >= 1 || path.length < 2) return path;
28238
- if (trim <= 0) return [];
28239
- let total = 0;
28240
- for (let index = 1; index < path.length; index += 1) {
28241
- const a = path[index - 1];
28242
- const b = path[index];
28243
- total += Math.hypot(b.x - a.x, b.y - a.y);
28244
- }
28245
- let remaining = total * trim;
28246
- const out = [path[0]];
28247
- for (let index = 1; index < path.length; index += 1) {
28248
- const a = path[index - 1];
28249
- const b = path[index];
28250
- const length = Math.hypot(b.x - a.x, b.y - a.y);
28251
- if (length >= remaining) {
28252
- const t = length > 0 ? remaining / length : 0;
28253
- out.push(worldPoint(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t));
28254
- return out;
28255
- }
28256
- out.push(b);
28257
- remaining -= length;
28258
- }
28259
- return out;
28260
- }
28261
- function tracePolyline(graphics, path, pattern, closed = false) {
28262
- const points = closed && path.length > 2 ? [...path, path[0]] : path;
28263
- if (points.length < 2) return;
28264
- if (!pattern) {
28265
- const first = points[0];
28266
- graphics.moveTo(first.x, first.y);
28267
- for (let index = 1; index < points.length; index += 1) {
28268
- const point = points[index];
28269
- graphics.lineTo(point.x, point.y);
28270
- }
28271
- if (closed) graphics.closePath();
28272
- return;
28273
- }
28274
- const [dash, gap] = pattern;
28275
- let penDown = true;
28276
- let fresh = true;
28277
- let remaining = dash;
28278
- for (let index = 1; index < points.length; index += 1) {
28279
- const a = points[index - 1];
28280
- const b = points[index];
28281
- const dx = b.x - a.x;
28282
- const dy = b.y - a.y;
28283
- const length = Math.hypot(dx, dy);
28284
- let travelled = 0;
28285
- while (length - travelled > 1e-4) {
28286
- const step = Math.min(length - travelled, remaining);
28287
- if (penDown) {
28288
- const t0 = travelled / length;
28289
- const t1 = (travelled + step) / length;
28290
- if (fresh) graphics.moveTo(a.x + dx * t0, a.y + dy * t0);
28291
- fresh = false;
28292
- graphics.lineTo(a.x + dx * t1, a.y + dy * t1);
28293
- }
28294
- travelled += step;
28295
- remaining -= step;
28296
- if (remaining <= 1e-4) {
28297
- penDown = !penDown;
28298
- fresh = penDown;
28299
- remaining = penDown ? dash : gap;
28300
- }
28301
- }
28302
- }
28303
- }
28304
- function traceArrowhead(graphics, tip, angle, size) {
28305
- graphics.moveTo(tip.x - size * Math.cos(angle - HEAD_SPREAD), tip.y - size * Math.sin(angle - HEAD_SPREAD)).lineTo(tip.x, tip.y).lineTo(tip.x - size * Math.cos(angle + HEAD_SPREAD), tip.y - size * Math.sin(angle + HEAD_SPREAD));
28306
- }
28307
- function endAngle(path, atStart) {
28308
- if (path.length < 2) return null;
28309
- const tip = atStart ? path[0] : path[path.length - 1];
28310
- for (let offset = 1; offset < path.length; offset += 1) {
28311
- const other = atStart ? path[offset] : path[path.length - 1 - offset];
28312
- if (Math.hypot(tip.x - other.x, tip.y - other.y) > .001) return Math.atan2(tip.y - other.y, tip.x - other.x);
28313
- }
28314
- return null;
28315
- }
28316
- function roundRectPoints(width, height, radius) {
28317
- const r = Math.min(radius, width / 2, height / 2);
28318
- if (r <= 0) return [
28319
- worldPoint(0, 0),
28320
- worldPoint(width, 0),
28321
- worldPoint(width, height),
28322
- worldPoint(0, height),
28323
- worldPoint(0, 0)
28324
- ];
28325
- const corner = (cx, cy, from) => Array.from({ length: 9 }, (_, index) => {
28326
- const angle = from + index / 8 * (Math.PI / 2);
28327
- return worldPoint(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r);
28328
- });
28329
- return [
28330
- ...corner(width - r, r, -Math.PI / 2),
28331
- ...corner(width - r, height - r, 0),
28332
- ...corner(r, height - r, Math.PI / 2),
28333
- ...corner(r, r, Math.PI),
28334
- worldPoint(width - r, 0)
28335
- ];
28336
- }
28337
- //#endregion
28338
- //#region ../sdk/src/board/render/renderers/arrow-card-renderer.ts
28339
- const partsByContainer$10 = /* @__PURE__ */ new WeakMap();
28340
- function sync$10(container, item, context) {
28341
- const parts = partsByContainer$10.get(container);
28342
- if (!parts) return;
28343
- parts.root.position.set(0, 0);
28344
- parts.root.rotation = 0;
28345
- parts.root.scale.set(1);
28346
- const { props, style } = item;
28347
- const selected = context.selectedIds.has(item.id);
28348
- const hovered = context.hoveredId === item.id;
28349
- parts.line.alpha = itemAlpha(context, style.stroke);
28350
- const color = itemColor(context, style.stroke, "brand");
28351
- const size = style.strokeWidth ?? 2.5;
28352
- syncTextResolution(parts.label, parts, context.zoom);
28353
- const resolved = resolveSceneArrow(item, context.scene);
28354
- const path = trimPolyline(resolved.path, style.trim);
28355
- const lineSig = [
28356
- path.map((point) => `${point.x},${point.y}`).join(";"),
28357
- selected,
28358
- hovered,
28359
- color,
28360
- size,
28361
- style.dash,
28362
- props.arrowStart,
28363
- props.arrowEnd
28364
- ].join("|");
28365
- if (lineSig !== parts.lineSig) {
28366
- parts.lineSig = lineSig;
28367
- parts.line.clear();
28368
- const width = selected ? size + 1 : size;
28369
- const stroke = {
28370
- color,
28371
- width,
28372
- alpha: selected || hovered ? 1 : .92,
28373
- cap: "round",
28374
- join: "round"
28375
- };
28376
- tracePolyline(parts.line, path, dashPattern(style.dash, width));
28377
- parts.line.stroke(stroke);
28378
- const first = path[0];
28379
- const last = path[path.length - 1];
28380
- if (first && last) {
28381
- const span = Math.hypot(last.x - first.x, last.y - first.y);
28382
- const headSize = Math.min(Math.max(14, size * 5.5), Math.max(10, span * .28));
28383
- const complete = style.trim === void 0 || style.trim >= 1;
28384
- const endDirection = endAngle(path, false);
28385
- const startDirection = endAngle(path, true);
28386
- if (props.arrowEnd && endDirection !== null && path.length > 1) traceArrowhead(parts.line, last, endDirection, headSize);
28387
- if (props.arrowStart && startDirection !== null && complete) traceArrowhead(parts.line, first, startDirection, headSize);
28388
- parts.line.stroke({
28389
- ...stroke,
28390
- width: Math.max(1.5, width)
28391
- });
28392
- }
28393
- }
28394
- const labelSig = [
28395
- props.label,
28396
- context.palette.text,
28397
- context.palette.bg,
28398
- props.fontSize
28399
- ].join("|");
28400
- if (labelSig !== parts.labelSig) {
28401
- parts.labelSig = labelSig;
28402
- parts.label.text = props.label;
28403
- parts.label.style.fill = context.palette.text;
28404
- parts.label.style.fontSize = props.fontSize;
28405
- const padX = props.fontSize * .4;
28406
- const padY = props.fontSize * .15;
28407
- const { width, height } = parts.label;
28408
- parts.backdrop.clear();
28409
- parts.backdrop.roundRect(-width / 2 - padX, -height / 2 - padY, width + padX * 2, height + padY * 2, props.fontSize * .3).fill({
28410
- color: context.palette.bg,
28411
- alpha: .92
28412
- });
28413
- }
28414
- const visible = props.label.length > 0 && (style.trim === void 0 || style.trim >= .5);
28415
- parts.label.visible = visible;
28416
- parts.backdrop.visible = visible;
28417
- const mid = style.trim === void 0 ? resolved.mid : pathPointAt(resolved.path, .5);
28418
- parts.label.position.set(mid.x, mid.y);
28419
- parts.backdrop.position.set(mid.x, mid.y);
28420
- }
28421
- const arrowCardRenderer = {
28422
- id: "arrow-card",
28423
- canRender: (item) => item.type === "arrow",
28424
- create: (item, context) => {
28425
- const root = new Container();
28426
- const line = new Graphics();
28427
- const resolution = textResolutionForZoom(context.zoom);
28428
- const label = new Text({
28429
- text: "",
28430
- style: {
28431
- fill: 16777215,
28432
- fontFamily: BOARD_FONT_STACK,
28433
- fontSize: 14,
28434
- fontWeight: "500"
28435
- },
28436
- resolution,
28437
- roundPixels: true
28438
- });
28439
- label.anchor.set(.5);
28440
- const backdrop = new Graphics();
28441
- root.addChild(line, backdrop, label);
28442
- partsByContainer$10.set(root, {
28443
- root,
28444
- line,
28445
- backdrop,
28446
- label,
28447
- lineSig: "",
28448
- labelSig: "",
28449
- resolution
28450
- });
28451
- if (item.type === "arrow") sync$10(root, item, context);
28452
- return root;
28453
- },
28454
- update: (container, item, context) => {
28455
- if (item.type === "arrow") sync$10(container, item, context);
28456
- },
28457
- renderFar: (graphics, item, context) => {
28458
- if (item.type !== "arrow") return;
28459
- const arrow = item;
28460
- drawFarStroke(graphics, trimPolyline(resolveSceneArrow(arrow, context.scene).path, arrow.style.trim), {
28461
- color: itemColor(context, arrow.style.stroke, "brand"),
28462
- width: arrow.style.strokeWidth ?? 2.5,
28463
- alpha: .85 * itemAlpha(context, arrow.style.stroke)
28464
- });
28465
- },
28466
- destroy: (container) => {
28467
- partsByContainer$10.get(container)?.root.destroy({ children: true });
28468
- partsByContainer$10.delete(container);
28469
- }
28470
- };
28471
- //#endregion
28472
- //#region ../sdk/src/board/render/renderers/audio-card-renderer.ts
28473
- const RADIUS$3 = 4;
28474
- const PLAY_RADIUS = 18;
28475
- const partsByContainer$9 = /* @__PURE__ */ new WeakMap();
28476
- function sync$9(container, item, context) {
28477
- const parts = partsByContainer$9.get(container);
28478
- if (!parts) return;
28479
- positionShell(parts.root, item);
28480
- const { width, height } = item.frame;
28481
- const selected = context.selectedIds.has(item.id);
28482
- const hovered = context.hoveredId === item.id;
28483
- const title = item.props.snapshot?.title ?? getMediaResourceTitle(item.props.src);
28484
- syncTextResolution(parts.label, parts, context.zoom);
28485
- if (parts.label.text !== title) parts.label.text = title;
28486
- syncTextWrapWidth(parts.label, parts, Math.max(1, width - 24), false);
28487
- parts.label.position.set(12, Math.max(8, height - parts.label.height - 8));
28488
- const sig = [
28489
- width,
28490
- height,
28491
- selected,
28492
- hovered,
28493
- title,
28494
- parts.label.height,
28495
- context.palette.surface,
28496
- context.palette.brand,
28497
- context.palette.border,
28498
- context.palette.muted,
28499
- context.palette.text
28500
- ].join("|");
28501
- if (sig === parts.sig) return;
28502
- parts.sig = sig;
28503
- parts.plate.clear().roundRect(0, 0, width, height, RADIUS$3).fill({
28504
- color: context.palette.surface,
28505
- alpha: .98
28506
- }).roundRect(0, 0, width, height, RADIUS$3).stroke({
28507
- color: selected ? context.palette.brand : hovered ? context.palette.muted : context.palette.border,
28508
- width: selected ? 2 : 1,
28509
- alpha: selected ? .96 : .84
28510
- });
28511
- const labelBand = Math.min(height * .38, parts.label.height + 16);
28512
- const waveformInset = Math.min(24, width * .08);
28513
- parts.waveform.clear();
28514
- drawAudioWaveform(parts.waveform, `${item.props.src}:${item.props.snapshot?.mtimeMs ?? "unknown"}`, {
28515
- x: waveformInset,
28516
- y: 10,
28517
- width: Math.max(1, width - waveformInset * 2),
28518
- height: Math.max(8, height - labelBand - 16)
28519
- }, context.palette.brand);
28520
- const cx = width / 2;
28521
- const cy = Math.max(24, (height - labelBand) / 2);
28522
- parts.chrome.clear().circle(cx, cy, PLAY_RADIUS).fill({
28523
- color: selected ? context.palette.brand : context.palette.surface,
28524
- alpha: .92
28525
- });
28526
- const triangle = PLAY_RADIUS * .58;
28527
- parts.chrome.moveTo(cx - triangle * .35, cy - triangle * .62).lineTo(cx - triangle * .35, cy + triangle * .62).lineTo(cx + triangle * .72, cy).closePath().fill({
28528
- color: context.palette.text,
28529
- alpha: .96
28530
- });
28531
- parts.label.style.fill = context.palette.text;
28532
- }
28533
- const audioCardRenderer = {
28534
- id: "audio-card",
28535
- canRender: (item) => item.type === "audio",
28536
- create: (item, context) => {
28537
- const root = new Container();
28538
- const plate = new Graphics();
28539
- const waveform = new Graphics();
28540
- const chrome = new Graphics();
28541
- const resolution = textResolutionForZoom(context.zoom);
28542
- const label = new Text({
28543
- text: "",
28544
- style: {
28545
- fill: context.palette.text,
28546
- fontFamily: BOARD_FONT_STACK,
28547
- fontSize: 12,
28548
- fontWeight: "500",
28549
- wordWrap: true
28550
- },
28551
- resolution,
28552
- roundPixels: true
28553
- });
28554
- root.addChild(plate, waveform, chrome, label);
28555
- partsByContainer$9.set(root, {
28556
- root,
28557
- plate,
28558
- waveform,
28559
- chrome,
28560
- label,
28561
- sig: "",
28562
- wrapWidth: 0,
28563
- resolution
28564
- });
28565
- if (item.type === "audio") sync$9(root, item, context);
28566
- return root;
28567
- },
28568
- update: (container, item, context) => {
28569
- if (item.type === "audio") sync$9(container, item, context);
28570
- },
28571
- renderFar: (graphics, item, context) => {
28572
- drawFarPlate(graphics, item.frame, {
28573
- fill: context.palette.surface,
28574
- fillAlpha: .96,
28575
- accent: context.palette.brand,
28576
- accentAlpha: .72
28577
- });
28578
- },
28579
- destroy: (container) => {
28580
- partsByContainer$9.get(container)?.root.destroy({ children: true });
28581
- partsByContainer$9.delete(container);
28582
- }
28583
- };
28584
- //#endregion
28585
- //#region ../sdk/src/board/render/renderers/draw-card-renderer.ts
28586
- const partsByContainer$8 = /* @__PURE__ */ new WeakMap();
28587
- function fillRibbon(graphics, ribbon, color) {
28588
- const { positions, indices } = ribbon;
28589
- for (let offset = 0; offset < indices.length; offset += 3) {
28590
- const a = (indices[offset] ?? 0) * 2;
28591
- const b = (indices[offset + 1] ?? 0) * 2;
28592
- const c = (indices[offset + 2] ?? 0) * 2;
28593
- graphics.moveTo(positions[a] ?? 0, positions[a + 1] ?? 0).lineTo(positions[b] ?? 0, positions[b + 1] ?? 0).lineTo(positions[c] ?? 0, positions[c + 1] ?? 0).closePath();
28594
- }
28595
- return graphics.fill({ color });
28596
- }
28597
- function replaceStroke(parts, next) {
28598
- if (parts.stroke) {
28599
- parts.root.removeChild(parts.stroke);
28600
- parts.stroke.destroy();
28601
- }
28602
- parts.stroke = next;
28603
- parts.root.addChild(next);
28604
- return next;
28212
+ parts.stroke = next;
28213
+ parts.root.addChild(next);
28214
+ return next;
28605
28215
  }
28606
28216
  function tessellate(parts, points, size, paint) {
28607
28217
  parts.ribbon ??= createStrokeRibbonBuilder(size);
@@ -28634,8 +28244,8 @@ function tessellate(parts, points, size, paint) {
28634
28244
  texture: Texture.WHITE
28635
28245
  }));
28636
28246
  }
28637
- function sync$8(container, item, context) {
28638
- const parts = partsByContainer$8.get(container);
28247
+ function sync$10(container, item, context) {
28248
+ const parts = partsByContainer$10.get(container);
28639
28249
  if (!parts) return;
28640
28250
  positionShell(parts.root, item);
28641
28251
  const { points } = item.props;
@@ -28668,7 +28278,7 @@ const drawCardRenderer = {
28668
28278
  canRender: (item) => item.type === "draw",
28669
28279
  create: (item, context) => {
28670
28280
  const root = new Container();
28671
- partsByContainer$8.set(root, {
28281
+ partsByContainer$10.set(root, {
28672
28282
  root,
28673
28283
  stroke: null,
28674
28284
  ribbon: null,
@@ -28685,11 +28295,11 @@ const drawCardRenderer = {
28685
28295
  height: 1
28686
28296
  }
28687
28297
  });
28688
- if (item.type === "draw") sync$8(root, item, context);
28298
+ if (item.type === "draw") sync$10(root, item, context);
28689
28299
  return root;
28690
28300
  },
28691
28301
  update: (container, item, context) => {
28692
- if (item.type === "draw") sync$8(container, item, context);
28302
+ if (item.type === "draw") sync$10(container, item, context);
28693
28303
  },
28694
28304
  renderFar: (graphics, item, context) => {
28695
28305
  if (item.type !== "draw") return;
@@ -28702,15 +28312,15 @@ const drawCardRenderer = {
28702
28312
  });
28703
28313
  },
28704
28314
  destroy: (container) => {
28705
- partsByContainer$8.get(container)?.root.destroy({ children: true });
28706
- partsByContainer$8.delete(container);
28315
+ partsByContainer$10.get(container)?.root.destroy({ children: true });
28316
+ partsByContainer$10.delete(container);
28707
28317
  }
28708
28318
  };
28709
28319
  //#endregion
28710
28320
  //#region ../sdk/src/board/render/renderers/effect-card-renderer.ts
28711
28321
  const BOARD_EFFECT_MAX_PARTICLES = 4e3;
28712
28322
  const TRAIL_SAMPLES = 64;
28713
- const partsByContainer$7 = /* @__PURE__ */ new WeakMap();
28323
+ const partsByContainer$9 = /* @__PURE__ */ new WeakMap();
28714
28324
  function hashString(value) {
28715
28325
  let hash = 2166136261;
28716
28326
  for (let index = 0; index < value.length; index += 1) {
@@ -28850,88 +28460,349 @@ function drawTrail(graphics, parts, item, context, color) {
28850
28460
  cap: "round"
28851
28461
  });
28852
28462
  }
28853
- }
28854
- function sync$7(container, item, context) {
28855
- const parts = partsByContainer$7.get(container);
28856
- if (!parts) return;
28857
- positionShell(parts.root, item);
28858
- const { width, height } = item.frame;
28859
- const { props } = item;
28860
- const color = itemColor(context, item.style.fill ?? item.style.stroke, "brand", "fill");
28861
- const seed = hashString(props.seed ?? item.id);
28862
- const time = Math.max(0, context.time);
28863
- const graphics = parts.graphics;
28864
- graphics.alpha = itemAlpha(context, item.style.fill ?? item.style.stroke);
28865
- graphics.clear();
28866
- switch (props.kind) {
28867
- case "particles":
28868
- drawParticles(graphics, props, width, height, time, seed, color);
28869
- break;
28870
- case "impact":
28871
- drawImpact(graphics, props, width, height, time, seed, color);
28872
- break;
28873
- case "flash":
28874
- drawFlash(graphics, props, width, height, time, color);
28875
- break;
28876
- case "glow":
28877
- drawGlow(graphics, props, width, height, time, color);
28878
- break;
28879
- case "trail": drawTrail(graphics, parts, item, context, color);
28463
+ }
28464
+ function sync$9(container, item, context) {
28465
+ const parts = partsByContainer$9.get(container);
28466
+ if (!parts) return;
28467
+ positionShell(parts.root, item);
28468
+ const { width, height } = item.frame;
28469
+ const { props } = item;
28470
+ const color = itemColor(context, item.style.fill ?? item.style.stroke, "brand", "fill");
28471
+ const seed = hashString(props.seed ?? item.id);
28472
+ const time = Math.max(0, context.time);
28473
+ const graphics = parts.graphics;
28474
+ graphics.alpha = itemAlpha(context, item.style.fill ?? item.style.stroke);
28475
+ graphics.clear();
28476
+ switch (props.kind) {
28477
+ case "particles":
28478
+ drawParticles(graphics, props, width, height, time, seed, color);
28479
+ break;
28480
+ case "impact":
28481
+ drawImpact(graphics, props, width, height, time, seed, color);
28482
+ break;
28483
+ case "flash":
28484
+ drawFlash(graphics, props, width, height, time, color);
28485
+ break;
28486
+ case "glow":
28487
+ drawGlow(graphics, props, width, height, time, color);
28488
+ break;
28489
+ case "trail": drawTrail(graphics, parts, item, context, color);
28490
+ }
28491
+ }
28492
+ const effectCardRenderer = {
28493
+ id: "effect-card",
28494
+ canRender: (item) => item.type === "effect",
28495
+ animated: true,
28496
+ create: (item, context) => {
28497
+ const root = new Container();
28498
+ const graphics = new Graphics();
28499
+ root.addChild(graphics);
28500
+ partsByContainer$9.set(root, {
28501
+ root,
28502
+ graphics,
28503
+ trail: []
28504
+ });
28505
+ if (item.type === "effect") sync$9(root, item, context);
28506
+ return root;
28507
+ },
28508
+ update: (container, item, context) => {
28509
+ if (item.type === "effect") sync$9(container, item, context);
28510
+ },
28511
+ renderFar: (graphics, item, context) => {
28512
+ if (item.type !== "effect") return;
28513
+ drawFarPlate(graphics, item.frame, {
28514
+ fill: itemColor(context, item.style.fill, "brand", "fill"),
28515
+ fillAlpha: .08
28516
+ });
28517
+ },
28518
+ destroy: (container) => {
28519
+ partsByContainer$9.get(container)?.root.destroy({ children: true });
28520
+ partsByContainer$9.delete(container);
28521
+ }
28522
+ };
28523
+ //#endregion
28524
+ //#region ../sdk/src/board/render/renderers/file-card-renderer.ts
28525
+ const PADDING$1 = 10;
28526
+ const COVER_RATIO = .56;
28527
+ const MIN_BODY_FOR_COVER = 38;
28528
+ const TITLE_SIZE = 13;
28529
+ const TITLE_LINE = TITLE_SIZE * 1.35;
28530
+ const TITLE_MAX_LINES = 2;
28531
+ const EXCERPT_SIZE = 11;
28532
+ const EXCERPT_LINE = EXCERPT_SIZE * 1.45;
28533
+ const EXCERPT_MAX_LINES = 3;
28534
+ const TYPE_MARK_SIZE = 28;
28535
+ const GAP = 4;
28536
+ const STRIPE = 2;
28537
+ const MAX_DASHES = 40;
28538
+ const partsByContainer$8 = /* @__PURE__ */ new WeakMap();
28539
+ function detailFor(zoom) {
28540
+ return boardCardDetailed(zoom) ? "full" : "title";
28541
+ }
28542
+ function coverHeight(item, height) {
28543
+ if (filePreviewKind(item.props.snapshot) !== "cover") return 0;
28544
+ const ideal = Math.round(height * COVER_RATIO);
28545
+ return Math.max(0, Math.min(ideal, height - MIN_BODY_FOR_COVER));
28546
+ }
28547
+ function linesInRoom(room, lineHeight, max) {
28548
+ if (room < lineHeight * .85) return 0;
28549
+ return Math.max(0, Math.min(max, Math.floor((room + .5) / lineHeight)));
28550
+ }
28551
+ function layoutCover(sprite, width, height, texture) {
28552
+ const rect = containRect(width, height, texture.width, texture.height);
28553
+ sprite.position.set(rect.x, rect.y);
28554
+ sprite.width = rect.width;
28555
+ sprite.height = rect.height;
28556
+ }
28557
+ function syncClip(parts, rect, style) {
28558
+ traceCardOutline(parts.clip.clear(), rect, style).fill({ color: 16777215 });
28559
+ }
28560
+ function sync$8(container, item, context) {
28561
+ const parts = partsByContainer$8.get(container);
28562
+ if (!parts) return;
28563
+ positionShell(parts.root, item);
28564
+ const { width, height } = item.frame;
28565
+ const detail = detailFor(context.zoom);
28566
+ const elevation = cardElevation(context, item.id);
28567
+ syncCardTag(parts.tag, {
28568
+ glyph: "file",
28569
+ text: fileMetaLine(item.props.src, item.props.snapshot?.size),
28570
+ width
28571
+ }, context);
28572
+ const key = context.assetKey(item);
28573
+ const texture = key ? context.getTexture(key) : null;
28574
+ const coverFailed = Boolean(key && !texture && context.hasError(key));
28575
+ const fileState = context.fileState(item.props.src);
28576
+ const band = coverHeight(item, height);
28577
+ const kind = filePreviewKind(item.props.snapshot);
28578
+ const category = fileCategory(item.props.src, item.props.snapshot?.mimeType);
28579
+ const accent = fileCategoryAccent(category, context.palette);
28580
+ syncTextResolution(parts.title, parts.titleRes, context.zoom);
28581
+ syncTextResolution(parts.excerpt, parts.excerptRes, context.zoom);
28582
+ syncTextResolution(parts.typeMark, parts.typeMarkRes, context.zoom);
28583
+ const visualSig = [
28584
+ key ?? "",
28585
+ width,
28586
+ height,
28587
+ detail,
28588
+ elevation,
28589
+ texture ? `${texture.width}x${texture.height}` : "none",
28590
+ coverFailed,
28591
+ fileState,
28592
+ kind,
28593
+ category,
28594
+ context.themeKey,
28595
+ accent
28596
+ ].join("|");
28597
+ if (visualSig !== parts.visualSig) {
28598
+ parts.visualSig = visualSig;
28599
+ const rect = {
28600
+ x: 0,
28601
+ y: 0,
28602
+ width,
28603
+ height
28604
+ };
28605
+ syncClip(parts, rect, context.style);
28606
+ drawCardSurface(parts.plate.clear(), rect, context.style, {
28607
+ fill: context.palette.surface,
28608
+ border: context.palette.border,
28609
+ borderAlpha: .85,
28610
+ elevation
28611
+ });
28612
+ if (fileState !== "ok") {
28613
+ const dash = Math.max(6, width / MAX_DASHES / 2);
28614
+ for (let x = 0; x < width; x += dash * 2) parts.plate.moveTo(x, .5).lineTo(Math.min(x + dash, width), .5);
28615
+ parts.plate.stroke({
28616
+ color: context.palette.muted,
28617
+ width: 2,
28618
+ alpha: fileState === "missing" ? .9 : .5
28619
+ });
28620
+ }
28621
+ const showCover = band > 0 && Boolean(texture);
28622
+ if (band > 0) parts.plate.rect(1, 1, width - 2, band - 1).fill({
28623
+ color: context.palette.hover,
28624
+ alpha: coverFailed ? .35 : .6
28625
+ });
28626
+ if (showCover && texture) {
28627
+ if (parts.cover.texture !== texture) parts.cover.texture = texture;
28628
+ layoutCover(parts.cover, width, band, texture);
28629
+ parts.coverMask.clear().rect(0, 0, width, band).fill({ color: 16777215 });
28630
+ }
28631
+ parts.cover.visible = showCover;
28632
+ parts.coverMask.visible = showCover;
28633
+ if (!band) parts.plate.rect(1, 1, STRIPE, height - 2).fill({
28634
+ color: accent,
28635
+ alpha: .55
28636
+ });
28637
+ parts.excerpt.visible = detail === "full";
28638
+ parts.typeMark.visible = kind === "blank";
28639
+ }
28640
+ const title = item.props.snapshot?.title || fileStem(item.props.src);
28641
+ const excerpt = item.props.snapshot?.excerpt ?? "";
28642
+ const mark = fileTypeLabel(item.props.src);
28643
+ const innerWidth = Math.max(1, width - 20);
28644
+ const showTypeMark = kind === "blank";
28645
+ const textSig = [
28646
+ title,
28647
+ excerpt,
28648
+ mark,
28649
+ detail,
28650
+ innerWidth,
28651
+ band,
28652
+ height,
28653
+ kind,
28654
+ context.palette.text,
28655
+ context.palette.muted,
28656
+ context.style.font,
28657
+ accent
28658
+ ].join("|");
28659
+ if (textSig === parts.textSig) return;
28660
+ parts.textSig = textSig;
28661
+ parts.title.style.fontFamily = context.style.font;
28662
+ parts.excerpt.style.fontFamily = context.style.font;
28663
+ const top = band > 0 ? band + PADDING$1 * .8 : PADDING$1;
28664
+ const contentBottom = height - PADDING$1;
28665
+ let cursor = top;
28666
+ if (showTypeMark) {
28667
+ const markSize = Math.max(18, Math.min(TYPE_MARK_SIZE, Math.round(height * .22)));
28668
+ parts.typeMark.style.fill = accent;
28669
+ parts.typeMark.style.fontSize = markSize;
28670
+ parts.typeMark.style.lineHeight = markSize * 1.1;
28671
+ if (parts.typeMark.text !== mark) parts.typeMark.text = mark;
28672
+ parts.typeMark.position.set(PADDING$1, cursor);
28673
+ parts.typeMark.visible = true;
28674
+ cursor += parts.typeMark.height + GAP;
28675
+ } else parts.typeMark.visible = false;
28676
+ const titleLines = linesInRoom(Math.max(0, contentBottom - cursor), TITLE_LINE, TITLE_MAX_LINES);
28677
+ parts.title.style.fill = context.palette.text;
28678
+ fitTextToLines(parts.title, title, titleLines, innerWidth);
28679
+ parts.title.position.set(PADDING$1, cursor);
28680
+ parts.title.visible = titleLines > 0;
28681
+ if (detail !== "full") {
28682
+ parts.excerpt.visible = false;
28683
+ return;
28684
+ }
28685
+ const excerptTop = cursor + (titleLines > 0 ? parts.title.height + GAP : 0);
28686
+ const excerptLines = linesInRoom(contentBottom - excerptTop, EXCERPT_LINE, EXCERPT_MAX_LINES);
28687
+ const showExcerpt = Boolean(excerpt) && excerptLines > 0;
28688
+ if (showExcerpt) {
28689
+ parts.excerpt.style.fill = context.palette.muted;
28690
+ fitTextToLines(parts.excerpt, excerpt, excerptLines, innerWidth);
28691
+ parts.excerpt.position.set(PADDING$1, excerptTop);
28880
28692
  }
28881
- if (context.selectedIds.has(item.id) || context.hoveredId === item.id) graphics.rect(0, 0, width, height).stroke({
28882
- color: context.palette.muted,
28883
- width: 1,
28884
- alpha: .6
28885
- });
28693
+ parts.excerpt.visible = showExcerpt;
28886
28694
  }
28887
- const effectCardRenderer = {
28888
- id: "effect-card",
28889
- canRender: (item) => item.type === "effect",
28890
- animated: true,
28695
+ const fileCardRenderer = {
28696
+ id: "file-card",
28697
+ canRender: (item) => item.type === "file",
28891
28698
  create: (item, context) => {
28892
28699
  const root = new Container();
28893
- const graphics = new Graphics();
28894
- root.addChild(graphics);
28895
- partsByContainer$7.set(root, {
28700
+ const plate = new Graphics();
28701
+ const body = new Container();
28702
+ const clip = new Graphics();
28703
+ const cover = new Sprite(Texture.EMPTY);
28704
+ const coverMask = new Graphics();
28705
+ cover.mask = coverMask;
28706
+ const resolution = 1;
28707
+ const title = new Text({
28708
+ text: "",
28709
+ style: {
28710
+ fill: context.palette.text,
28711
+ fontFamily: BOARD_FONT_STACK,
28712
+ fontSize: TITLE_SIZE,
28713
+ fontWeight: "600",
28714
+ wordWrap: true,
28715
+ breakWords: true,
28716
+ lineHeight: TITLE_LINE
28717
+ },
28718
+ resolution,
28719
+ roundPixels: true
28720
+ });
28721
+ const excerpt = new Text({
28722
+ text: "",
28723
+ style: {
28724
+ fill: context.palette.muted,
28725
+ fontFamily: BOARD_FONT_STACK,
28726
+ fontSize: EXCERPT_SIZE,
28727
+ fontWeight: "400",
28728
+ wordWrap: true,
28729
+ breakWords: true,
28730
+ lineHeight: EXCERPT_LINE
28731
+ },
28732
+ resolution,
28733
+ roundPixels: true
28734
+ });
28735
+ const typeMark = new Text({
28736
+ text: "",
28737
+ style: {
28738
+ fill: context.palette.muted,
28739
+ fontFamily: BOARD_MONO_FONT_STACK,
28740
+ fontSize: TYPE_MARK_SIZE,
28741
+ fontWeight: "700",
28742
+ lineHeight: TYPE_MARK_SIZE * 1.1
28743
+ },
28744
+ resolution,
28745
+ roundPixels: true
28746
+ });
28747
+ body.mask = clip;
28748
+ body.addChild(cover, coverMask, typeMark, title, excerpt);
28749
+ const tag = createCardTag();
28750
+ root.addChild(plate, body, clip, tag.root);
28751
+ partsByContainer$8.set(root, {
28752
+ tag,
28896
28753
  root,
28897
- graphics,
28898
- trail: []
28754
+ plate,
28755
+ body,
28756
+ clip,
28757
+ cover,
28758
+ coverMask,
28759
+ typeMark,
28760
+ title,
28761
+ excerpt,
28762
+ visualSig: "",
28763
+ textSig: "",
28764
+ titleRes: { resolution },
28765
+ excerptRes: { resolution },
28766
+ typeMarkRes: { resolution }
28899
28767
  });
28900
- if (item.type === "effect") sync$7(root, item, context);
28768
+ if (item.type === "file") sync$8(root, item, context);
28901
28769
  return root;
28902
28770
  },
28903
28771
  update: (container, item, context) => {
28904
- if (item.type === "effect") sync$7(container, item, context);
28772
+ if (item.type === "file") sync$8(container, item, context);
28773
+ },
28774
+ zoom: (container, zoom) => {
28775
+ const tag = partsByContainer$8.get(container)?.tag;
28776
+ if (tag) zoomCardTag(tag, zoom);
28905
28777
  },
28906
28778
  renderFar: (graphics, item, context) => {
28907
- if (item.type !== "effect") return;
28779
+ const category = fileCategory(item.type === "file" ? item.props.src : "", item.type === "file" ? item.props.snapshot?.mimeType : void 0);
28908
28780
  drawFarPlate(graphics, item.frame, {
28909
- fill: itemColor(context, item.style.fill, "brand", "fill"),
28910
- fillAlpha: .08
28781
+ fill: context.palette.surface,
28782
+ fillAlpha: .96,
28783
+ accent: fileCategoryAccent(category, context.palette),
28784
+ accentAlpha: .45
28911
28785
  });
28912
28786
  },
28913
28787
  destroy: (container) => {
28914
- partsByContainer$7.get(container)?.root.destroy({ children: true });
28915
- partsByContainer$7.delete(container);
28788
+ partsByContainer$8.get(container)?.root.destroy({ children: true });
28789
+ partsByContainer$8.delete(container);
28916
28790
  }
28917
28791
  };
28918
28792
  //#endregion
28919
28793
  //#region ../sdk/src/board/render/renderers/frame-card-renderer.ts
28920
- const partsByContainer$6 = /* @__PURE__ */ new WeakMap();
28794
+ const partsByContainer$7 = /* @__PURE__ */ new WeakMap();
28921
28795
  const DEFAULT_WASH = .04;
28922
- function sync$6(container, item, context) {
28923
- const parts = partsByContainer$6.get(container);
28796
+ function sync$7(container, item, context) {
28797
+ const parts = partsByContainer$7.get(container);
28924
28798
  if (!parts) return;
28925
28799
  const { frame, style, props, locked } = item;
28926
28800
  positionShell(parts.root, item);
28927
28801
  syncTextResolution(parts.label, parts, context.zoom);
28928
- const selected = context.selectedIds.has(item.id);
28929
- const hovered = context.hoveredId === item.id;
28930
28802
  const fill = itemColor(context, style.fill ?? style.stroke, "neutral", "fill");
28931
28803
  const fillAlpha = (style.fillOpacity ?? (style.fill ? 1 : DEFAULT_WASH)) * itemAlpha(context, style.fill ?? style.stroke);
28932
28804
  const baseStroke = itemColor(context, style.stroke, "neutral");
28933
- const stroke = selected ? context.palette.brand : hovered && style.stroke ? context.palette.muted : baseStroke;
28934
- const alpha = (selected || style.stroke ? 1 : hovered ? .85 : .55) * itemAlpha(context, style.stroke);
28805
+ const alpha = (style.stroke ? 1 : .55) * itemAlpha(context, style.stroke);
28935
28806
  const radius = style.radius ?? 4;
28936
28807
  const strokeWidth = style.strokeWidth ?? 1.5;
28937
28808
  const decorated = props.decoration !== "none";
@@ -28939,11 +28810,9 @@ function sync$6(container, item, context) {
28939
28810
  props.decoration,
28940
28811
  frame.width,
28941
28812
  frame.height,
28942
- selected,
28943
- hovered,
28944
28813
  fill,
28945
28814
  fillAlpha,
28946
- stroke,
28815
+ baseStroke,
28947
28816
  alpha,
28948
28817
  radius,
28949
28818
  strokeWidth,
@@ -28957,15 +28826,14 @@ function sync$6(container, item, context) {
28957
28826
  color: fill,
28958
28827
  alpha: fillAlpha
28959
28828
  });
28960
- const width = selected ? Math.max(2, strokeWidth) : strokeWidth;
28961
- const pattern = decorated ? dashPattern(style.dash, width) : null;
28962
- const trim = decorated ? style.trim ?? 1 : 1;
28963
- if ((decorated && strokeWidth > 0 || selected || hovered) && trim > 0) {
28829
+ const pattern = dashPattern(style.dash, strokeWidth);
28830
+ const trim = style.trim ?? 1;
28831
+ if (decorated && strokeWidth > 0 && trim > 0) {
28964
28832
  if (pattern || trim < 1) tracePolyline(parts.box, trimPolyline(roundRectPoints(frame.width, frame.height, radius), trim), pattern);
28965
28833
  else parts.box.roundRect(0, 0, frame.width, frame.height, radius);
28966
28834
  parts.box.stroke({
28967
- color: stroke,
28968
- width,
28835
+ color: baseStroke,
28836
+ width: strokeWidth,
28969
28837
  alpha,
28970
28838
  cap: pattern || trim < 1 ? "round" : "butt",
28971
28839
  join: "round"
@@ -28975,11 +28843,16 @@ function sync$6(container, item, context) {
28975
28843
  parts.label.visible = decorated;
28976
28844
  const title = locked ? `🔒 ${props.label}`.trim() : props.label;
28977
28845
  const labelColor = style.stroke ? baseStroke : context.palette.muted;
28978
- const textSig = [title, labelColor].join("|");
28846
+ const textSig = [
28847
+ title,
28848
+ labelColor,
28849
+ context.style.font
28850
+ ].join("|");
28979
28851
  if (textSig !== parts.textSig) {
28980
28852
  parts.textSig = textSig;
28981
28853
  parts.label.text = title;
28982
28854
  parts.label.style.fill = labelColor;
28855
+ parts.label.style.fontFamily = context.style.font;
28983
28856
  }
28984
28857
  parts.label.position.set(4, -18);
28985
28858
  }
@@ -28996,7 +28869,7 @@ const frameCardRenderer = {
28996
28869
  fontWeight: "600"
28997
28870
  });
28998
28871
  root.addChild(box, label);
28999
- partsByContainer$6.set(root, {
28872
+ partsByContainer$7.set(root, {
29000
28873
  root,
29001
28874
  box,
29002
28875
  label,
@@ -29004,11 +28877,11 @@ const frameCardRenderer = {
29004
28877
  textSig: "",
29005
28878
  resolution: label.resolution
29006
28879
  });
29007
- if (item.type === "frame") sync$6(root, item, context);
28880
+ if (item.type === "frame") sync$7(root, item, context);
29008
28881
  return root;
29009
28882
  },
29010
28883
  update: (container, item, context) => {
29011
- if (item.type === "frame") sync$6(container, item, context);
28884
+ if (item.type === "frame") sync$7(container, item, context);
29012
28885
  },
29013
28886
  renderFar: (graphics, item, context) => {
29014
28887
  if (item.type !== "frame" || item.props.decoration === "none") return;
@@ -29018,14 +28891,14 @@ const frameCardRenderer = {
29018
28891
  });
29019
28892
  },
29020
28893
  destroy: (container) => {
29021
- partsByContainer$6.delete(container);
28894
+ partsByContainer$7.delete(container);
29022
28895
  container.destroy({ children: true });
29023
28896
  }
29024
28897
  };
29025
28898
  //#endregion
29026
28899
  //#region ../sdk/src/board/render/renderers/shape-card-renderer.ts
29027
28900
  const DEFAULT_ROUNDED_RADIUS = 8;
29028
- const partsByContainer$5 = /* @__PURE__ */ new WeakMap();
28901
+ const partsByContainer$6 = /* @__PURE__ */ new WeakMap();
29029
28902
  const pathCache = /* @__PURE__ */ new Map();
29030
28903
  function parsedPath(d) {
29031
28904
  if (pathCache.has(d)) return pathCache.get(d) ?? null;
@@ -29106,14 +28979,12 @@ function outlinePoints(geometry, width, height, radius) {
29106
28979
  default: return roundRectPoints(width, height, radius);
29107
28980
  }
29108
28981
  }
29109
- function sync$5(container, item, context) {
29110
- const parts = partsByContainer$5.get(container);
28982
+ function sync$6(container, item, context) {
28983
+ const parts = partsByContainer$6.get(container);
29111
28984
  if (!parts) return;
29112
28985
  positionShell(parts.root, item);
29113
28986
  const { width, height } = item.frame;
29114
28987
  const { props, style } = item;
29115
- const selected = context.selectedIds.has(item.id);
29116
- const hovered = context.hoveredId === item.id;
29117
28988
  const stroke = itemColor(context, style.stroke ?? style.fill, "brand");
29118
28989
  const fill = itemColor(context, style.fill ?? style.stroke, "brand", "fill");
29119
28990
  const fillOpacity = (style.fillOpacity ?? (style.fill ? 1 : 0)) * itemAlpha(context, style.fill ?? style.stroke);
@@ -29123,8 +28994,6 @@ function sync$5(container, item, context) {
29123
28994
  const visualSig = [
29124
28995
  width,
29125
28996
  height,
29126
- selected,
29127
- hovered,
29128
28997
  props.geometry,
29129
28998
  props.path,
29130
28999
  fill,
@@ -29134,8 +29003,7 @@ function sync$5(container, item, context) {
29134
29003
  strokeWidth,
29135
29004
  style.dash,
29136
29005
  style.radius,
29137
- style.trim,
29138
- context.palette.muted
29006
+ style.trim
29139
29007
  ].join("|");
29140
29008
  if (visualSig !== parts.visualSig) {
29141
29009
  parts.visualSig = visualSig;
@@ -29147,8 +29015,7 @@ function sync$5(container, item, context) {
29147
29015
  alpha: fillOpacity
29148
29016
  });
29149
29017
  }
29150
- const lineWidth = selected ? Math.max(2.5, strokeWidth) : strokeWidth;
29151
- const pattern = dashPattern(style.dash, lineWidth);
29018
+ const pattern = dashPattern(style.dash, strokeWidth);
29152
29019
  const partial = style.trim !== void 0 && style.trim < 1;
29153
29020
  if (strokeWidth > 0 && (!partial || style.trim)) {
29154
29021
  if ((pattern || partial) && props.geometry !== "path") {
@@ -29156,9 +29023,9 @@ function sync$5(container, item, context) {
29156
29023
  tracePolyline(parts.shape, trimPolyline(outlinePoints(props.geometry, width, height, radius), style.trim), pattern);
29157
29024
  } else traceShapeOutline(parts.shape, props, width, height, style.radius);
29158
29025
  parts.shape.stroke({
29159
- color: hovered && !selected ? context.palette.muted : stroke,
29160
- width: lineWidth,
29161
- alpha: (selected ? 1 : hovered ? .95 : .85) * strokeAlpha,
29026
+ color: stroke,
29027
+ width: strokeWidth,
29028
+ alpha: .85 * strokeAlpha,
29162
29029
  cap: partial || pattern ? "round" : "butt",
29163
29030
  join: "round"
29164
29031
  });
@@ -29189,305 +29056,814 @@ const shapeCardRenderer = {
29189
29056
  canRender: (item) => item.type === "shape",
29190
29057
  create: (item, context) => {
29191
29058
  const root = new Container();
29192
- const shape = new Graphics();
29193
- const resolution = textResolutionForZoom(context.zoom);
29059
+ const shape = new Graphics();
29060
+ const resolution = textResolutionForZoom(context.zoom);
29061
+ const label = new Text({
29062
+ text: "",
29063
+ style: {
29064
+ fill: 16777215,
29065
+ fontFamily: BOARD_FONT_STACK,
29066
+ fontSize: 20,
29067
+ fontWeight: "500",
29068
+ align: "center",
29069
+ wordWrap: true,
29070
+ lineHeight: 28
29071
+ },
29072
+ resolution,
29073
+ roundPixels: true
29074
+ });
29075
+ label.anchor.set(.5);
29076
+ root.addChild(shape, label);
29077
+ partsByContainer$6.set(root, {
29078
+ root,
29079
+ shape,
29080
+ label,
29081
+ visualSig: "",
29082
+ textSig: "",
29083
+ wrapWidth: 0,
29084
+ resolution
29085
+ });
29086
+ if (item.type === "shape") sync$6(root, item, context);
29087
+ return root;
29088
+ },
29089
+ update: (container, item, context) => {
29090
+ if (item.type === "shape") sync$6(container, item, context);
29091
+ },
29092
+ renderFar: (graphics, item, context) => {
29093
+ if (item.type !== "shape") return;
29094
+ drawFarPlate(graphics, item.frame, {
29095
+ fill: itemColor(context, item.style.fill ?? item.style.stroke, "brand", "fill"),
29096
+ fillAlpha: Math.max(.18, item.style.fillOpacity ?? 0) * itemAlpha(context, item.style.fill ?? item.style.stroke),
29097
+ accent: itemColor(context, item.style.stroke, "brand"),
29098
+ accentAlpha: .9 * itemAlpha(context, item.style.stroke)
29099
+ });
29100
+ },
29101
+ destroy: (container) => {
29102
+ partsByContainer$6.get(container)?.root.destroy({ children: true });
29103
+ partsByContainer$6.delete(container);
29104
+ }
29105
+ };
29106
+ //#endregion
29107
+ //#region ../sdk/src/board/render/renderers/custom-card-renderer.ts
29108
+ const partsByContainer$5 = /* @__PURE__ */ new WeakMap();
29109
+ function sync$5(container, item, context) {
29110
+ const parts = partsByContainer$5.get(container);
29111
+ if (!parts) return;
29112
+ if (parts.id !== item.id) {
29113
+ context.scripts?.release(parts.id);
29114
+ parts.id = item.id;
29115
+ }
29116
+ positionShell(parts.root, item);
29117
+ const { width, height } = item.frame;
29118
+ const resolution = Math.min(4, Math.max(.25, context.zoom * getBoardResolution()));
29119
+ const texture = context.scripts?.frame(item, context.time, resolution) ?? null;
29120
+ const error = context.scripts?.error(item.id) ?? null;
29121
+ parts.sprite.visible = Boolean(texture);
29122
+ const display = texture ?? Texture.EMPTY;
29123
+ if (parts.sprite.texture !== display) parts.sprite.texture = display;
29124
+ parts.sprite.width = width;
29125
+ parts.sprite.height = height;
29126
+ const ready = Boolean(texture) || context.scripts?.ready?.(item.id) === true;
29127
+ const message = error ? `${item.script.src}\n${error}` : ready ? "" : item.script.src;
29128
+ const sig = [
29129
+ width,
29130
+ height,
29131
+ Boolean(texture),
29132
+ message,
29133
+ context.themeKey
29134
+ ].join("|");
29135
+ if (sig === parts.sig) return;
29136
+ parts.sig = sig;
29137
+ parts.placeholder.clear();
29138
+ if (!ready || error) parts.placeholder.rect(0, 0, width, height).stroke({
29139
+ color: context.palette.border,
29140
+ width: 1,
29141
+ alpha: .8
29142
+ });
29143
+ parts.label.visible = !ready || Boolean(error);
29144
+ parts.label.text = message;
29145
+ parts.label.style.fill = error ? context.colors.rose.stroke : context.palette.muted;
29146
+ parts.label.style.wordWrapWidth = Math.max(40, width - 16);
29147
+ parts.label.position.set(width / 2, height / 2);
29148
+ }
29149
+ const customCardRenderer = {
29150
+ id: "custom-card",
29151
+ canRender: (item) => item.type === "custom",
29152
+ animated: false,
29153
+ create: (item, context) => {
29154
+ const root = new Container();
29155
+ const sprite = new Sprite(Texture.EMPTY);
29156
+ const placeholder = new Graphics();
29194
29157
  const label = new Text({
29195
29158
  text: "",
29196
29159
  style: {
29197
- fill: 16777215,
29198
- fontFamily: BOARD_FONT_STACK,
29199
- fontSize: 20,
29200
- fontWeight: "500",
29160
+ fontFamily: BOARD_MONO_FONT_STACK,
29161
+ fontSize: 12,
29201
29162
  align: "center",
29202
29163
  wordWrap: true,
29203
- lineHeight: 28
29164
+ breakWords: true
29204
29165
  },
29205
- resolution,
29206
- roundPixels: true
29166
+ resolution: getBoardResolution()
29207
29167
  });
29208
29168
  label.anchor.set(.5);
29209
- root.addChild(shape, label);
29169
+ root.addChild(sprite, placeholder, label);
29210
29170
  partsByContainer$5.set(root, {
29171
+ id: item.id,
29211
29172
  root,
29212
- shape,
29173
+ sprite,
29174
+ placeholder,
29213
29175
  label,
29214
- visualSig: "",
29215
- textSig: "",
29216
- wrapWidth: 0,
29217
- resolution
29176
+ sig: ""
29218
29177
  });
29219
- if (item.type === "shape") sync$5(root, item, context);
29178
+ if (item.type === "custom") sync$5(root, item, context);
29220
29179
  return root;
29221
29180
  },
29222
29181
  update: (container, item, context) => {
29223
- if (item.type === "shape") sync$5(container, item, context);
29182
+ if (item.type === "custom") sync$5(container, item, context);
29224
29183
  },
29225
29184
  renderFar: (graphics, item, context) => {
29226
- if (item.type !== "shape") return;
29227
29185
  drawFarPlate(graphics, item.frame, {
29228
- fill: itemColor(context, item.style.fill ?? item.style.stroke, "brand", "fill"),
29229
- fillAlpha: Math.max(.18, item.style.fillOpacity ?? 0) * itemAlpha(context, item.style.fill ?? item.style.stroke),
29230
- accent: itemColor(context, item.style.stroke, "brand"),
29231
- accentAlpha: .9 * itemAlpha(context, item.style.stroke)
29186
+ fill: context.palette.muted,
29187
+ fillAlpha: .12
29232
29188
  });
29233
29189
  },
29234
- destroy: (container) => {
29235
- partsByContainer$5.get(container)?.root.destroy({ children: true });
29190
+ destroy: (container, context) => {
29191
+ const parts = partsByContainer$5.get(container);
29192
+ if (parts) context.scripts?.release(parts.id);
29193
+ parts?.root.destroy({ children: true });
29236
29194
  partsByContainer$5.delete(container);
29237
29195
  }
29238
29196
  };
29239
29197
  //#endregion
29240
- //#region ../sdk/src/board/render/renderers/custom-card-renderer.ts
29198
+ //#region ../sdk/src/board/render/renderers/image-card-renderer.ts
29241
29199
  const partsByContainer$4 = /* @__PURE__ */ new WeakMap();
29200
+ function cropRect(crop) {
29201
+ if (!crop) return null;
29202
+ const w = Math.min(crop.w, 1 - crop.x);
29203
+ const h = Math.min(crop.h, 1 - crop.y);
29204
+ return w > 0 && h > 0 ? {
29205
+ x: crop.x,
29206
+ y: crop.y,
29207
+ w,
29208
+ h
29209
+ } : null;
29210
+ }
29211
+ function showTexture(parts, texture, crop) {
29212
+ const rect = texture ? cropRect(crop) : null;
29213
+ const key = rect ? `${rect.x}|${rect.y}|${rect.w}|${rect.h}` : "";
29214
+ const previous = parts.cropped;
29215
+ const reusable = previous && parts.cropSource === texture && parts.cropKey === key;
29216
+ if (texture && rect && !reusable) {
29217
+ const { frame } = texture;
29218
+ const width = Math.max(1, frame.width * rect.w);
29219
+ const height = Math.max(1, frame.height * rect.h);
29220
+ const x = frame.x + Math.min(frame.width * rect.x, frame.width - width);
29221
+ const y = frame.y + Math.min(frame.height * rect.y, frame.height - height);
29222
+ parts.cropped = new Texture({
29223
+ source: texture.source,
29224
+ frame: new Rectangle(x, y, width, height)
29225
+ });
29226
+ } else if (!rect) parts.cropped = null;
29227
+ parts.cropSource = rect ? texture : null;
29228
+ parts.cropKey = key;
29229
+ const display = parts.cropped ?? texture ?? Texture.EMPTY;
29230
+ if (parts.sprite.texture !== display) parts.sprite.texture = display;
29231
+ if (previous && previous !== parts.cropped) previous.destroy(false);
29232
+ }
29233
+ const PRINT_MARGIN = 6;
29242
29234
  function sync$4(container, item, context) {
29243
29235
  const parts = partsByContainer$4.get(container);
29244
29236
  if (!parts) return;
29245
- if (parts.id !== item.id) {
29246
- context.scripts?.release(parts.id);
29247
- parts.id = item.id;
29237
+ positionShell(parts.root, item);
29238
+ const { width, height } = item.frame;
29239
+ const key = context.assetKey(item);
29240
+ const texture = key ? context.getTexture(key) : null;
29241
+ const failed = Boolean(key && !texture && context.hasError(key));
29242
+ const elevation = cardElevation(context, item.id);
29243
+ const sig = [
29244
+ key ?? "",
29245
+ width,
29246
+ height,
29247
+ texture ? `${texture.width}x${texture.height}` : "none",
29248
+ JSON.stringify(item.props.crop ?? null),
29249
+ failed,
29250
+ elevation,
29251
+ context.themeKey
29252
+ ].join("|");
29253
+ if (sig === parts.sig) return;
29254
+ parts.sig = sig;
29255
+ showTexture(parts, texture, item.props.crop);
29256
+ const display = parts.sprite.texture;
29257
+ const framed = context.style.media === "framed";
29258
+ const margin = framed ? Math.min(PRINT_MARGIN, width / 8, height / 8) : 0;
29259
+ const area = {
29260
+ x: margin,
29261
+ y: margin,
29262
+ width: width - margin * 2,
29263
+ height: height - margin * 2
29264
+ };
29265
+ const fitted = texture ? containRect(area.width, area.height, display.width, display.height) : {
29266
+ ...area,
29267
+ x: 0,
29268
+ y: 0
29269
+ };
29270
+ const picture = {
29271
+ x: area.x + fitted.x,
29272
+ y: area.y + fitted.y,
29273
+ width: fitted.width,
29274
+ height: fitted.height
29275
+ };
29276
+ const card = framed ? {
29277
+ x: picture.x - margin,
29278
+ y: picture.y - margin,
29279
+ width: picture.width + margin * 2,
29280
+ height: picture.height + margin * 2
29281
+ } : picture;
29282
+ parts.sprite.visible = Boolean(texture);
29283
+ if (texture) {
29284
+ parts.sprite.position.set(picture.x, picture.y);
29285
+ parts.sprite.width = picture.width;
29286
+ parts.sprite.height = picture.height;
29287
+ }
29288
+ traceCardOutline(parts.mask.clear(), picture, framed ? {
29289
+ corner: "round",
29290
+ radius: 0
29291
+ } : context.style).fill({ color: 16777215 });
29292
+ parts.placeholder.clear();
29293
+ if (texture && !framed) return;
29294
+ drawCardSurface(parts.placeholder, card, context.style, {
29295
+ fill: context.palette.surface,
29296
+ border: texture ? null : failed ? context.palette.border : context.palette.hover,
29297
+ elevation
29298
+ });
29299
+ if (texture) return;
29300
+ const cx = width / 2;
29301
+ const cy = height / 2;
29302
+ if (failed) parts.placeholder.moveTo(cx - 10, cy - 10).lineTo(cx + 10, cy + 10).moveTo(cx + 10, cy - 10).lineTo(cx - 10, cy + 10).stroke({
29303
+ color: context.palette.muted,
29304
+ width: 1.5,
29305
+ alpha: .8
29306
+ });
29307
+ else parts.placeholder.circle(cx, cy, 6).stroke({
29308
+ color: context.palette.muted,
29309
+ width: 1.25,
29310
+ alpha: .55
29311
+ });
29312
+ }
29313
+ const imageCardRenderer = {
29314
+ id: "image-card",
29315
+ canRender: (item) => item.type === "image",
29316
+ create: (item, context) => {
29317
+ const root = new Container();
29318
+ const sprite = new Sprite(Texture.EMPTY);
29319
+ const placeholder = new Graphics();
29320
+ const mask = new Graphics();
29321
+ root.addChild(placeholder, sprite, mask);
29322
+ sprite.mask = mask;
29323
+ partsByContainer$4.set(root, {
29324
+ root,
29325
+ sprite,
29326
+ placeholder,
29327
+ mask,
29328
+ sig: "",
29329
+ cropped: null,
29330
+ cropSource: null,
29331
+ cropKey: ""
29332
+ });
29333
+ if (item.type === "image") sync$4(root, item, context);
29334
+ return root;
29335
+ },
29336
+ update: (container, item, context) => {
29337
+ if (item.type === "image") sync$4(container, item, context);
29338
+ },
29339
+ renderFar: (graphics, item, context) => {
29340
+ drawFarPlate(graphics, item.frame, {
29341
+ fill: context.palette.hover,
29342
+ fillAlpha: .9
29343
+ });
29344
+ },
29345
+ destroy: (container) => {
29346
+ const parts = partsByContainer$4.get(container);
29347
+ parts?.root.destroy({ children: true });
29348
+ parts?.cropped?.destroy(false);
29349
+ partsByContainer$4.delete(container);
29350
+ }
29351
+ };
29352
+ //#endregion
29353
+ //#region ../sdk/src/board/render/renderers/task-card-renderer.ts
29354
+ const PADDING = 12;
29355
+ const META_HEIGHT = 28;
29356
+ const PLAY_BADGE_RADIUS = 15;
29357
+ const SHIMMER_PERIOD_MS = 1600;
29358
+ const SHIMMER_FRAME_MS = 33;
29359
+ const REVEAL_MS = 180;
29360
+ const SECOND = 1e3;
29361
+ const partsByContainer$3 = /* @__PURE__ */ new WeakMap();
29362
+ function previewKindFor(artifact) {
29363
+ if (!artifact) return "empty";
29364
+ if (artifact.type === "image" || artifact.type === "video") return "texture";
29365
+ if (artifact.type === "audio") return artifact.previewUrl ? "texture" : "waveform";
29366
+ return artifact.textExcerpt ? "text" : "empty";
29367
+ }
29368
+ function liveStatus(item, context) {
29369
+ const { snapshot } = item.props;
29370
+ const live = snapshot.status === "completed" ? null : context.taskRun(item.props.taskRunId);
29371
+ const liveStart = Date.parse(live?.startedAt ?? "");
29372
+ const working = live?.status === "pending" || live?.status === "running";
29373
+ const adopt = snapshot.status === "failed" ? working && liveStart > Date.parse(snapshot.updatedAt ?? "") : live?.status === "running" || live?.status === "pending" && snapshot.status === "pending";
29374
+ const status = adopt && live ? live.status : snapshot.status;
29375
+ const started = adopt ? liveStart : Date.parse(snapshot.startedAt ?? "");
29376
+ return {
29377
+ status,
29378
+ startedAt: Number.isFinite(started) ? started : null
29379
+ };
29380
+ }
29381
+ function stateSurfaceFor(status, kind, hasTexture, previewFailed) {
29382
+ if (kind === "texture" && !hasTexture) {
29383
+ if (status === "failed") return "failed";
29384
+ return previewFailed ? "unavailable" : "media-loading";
29385
+ }
29386
+ if (kind !== "empty") return null;
29387
+ if (status === "failed") return "failed";
29388
+ if (status === "running") return "generating";
29389
+ if (status === "pending") return "queued";
29390
+ return "empty";
29391
+ }
29392
+ function stateLabel(surface, context) {
29393
+ switch (surface) {
29394
+ case "generating": return context.labels.generating;
29395
+ case "queued": return context.labels.queued;
29396
+ case "failed": return context.labels.failed;
29397
+ case "unavailable": return context.labels.previewUnavailable;
29398
+ case "empty": return context.labels.noPreview;
29399
+ default: return "";
29400
+ }
29401
+ }
29402
+ function statusColor(status, context) {
29403
+ if (status === "failed") return context.colors.rose.stroke;
29404
+ if (status === "running") return context.colors.brand.stroke;
29405
+ return context.colors.neutral.stroke;
29406
+ }
29407
+ function drawPlayBadge(graphics, rect, context) {
29408
+ const radius = Math.min(PLAY_BADGE_RADIUS, Math.max(8, Math.min(rect.width, rect.height) * .16));
29409
+ const cx = rect.x + rect.width / 2;
29410
+ const cy = rect.y + rect.height / 2;
29411
+ graphics.circle(cx, cy, radius).fill({
29412
+ color: context.palette.bg,
29413
+ alpha: .64
29414
+ });
29415
+ const size = radius * .72;
29416
+ graphics.moveTo(cx - size * .34, cy - size * .56).lineTo(cx + size * .62, cy).lineTo(cx - size * .34, cy + size * .56).closePath().fill({
29417
+ color: context.palette.text,
29418
+ alpha: .94
29419
+ });
29420
+ }
29421
+ function drawStateMark(graphics, surface, cx, cy, color) {
29422
+ if (surface === "generating" || surface === "media-loading") {
29423
+ graphics.circle(cx, cy, 4).fill({
29424
+ color,
29425
+ alpha: .9
29426
+ });
29427
+ graphics.circle(cx - 12, cy + 7, 2.5).fill({
29428
+ color,
29429
+ alpha: .48
29430
+ });
29431
+ graphics.circle(cx + 11, cy + 6, 3).fill({
29432
+ color,
29433
+ alpha: .68
29434
+ });
29435
+ graphics.circle(cx + 3, cy - 12, 2).fill({
29436
+ color,
29437
+ alpha: .36
29438
+ });
29439
+ return;
29440
+ }
29441
+ if (surface === "queued") {
29442
+ graphics.circle(cx, cy, 10).stroke({
29443
+ color,
29444
+ width: 1.5,
29445
+ alpha: .62
29446
+ }).circle(cx, cy, 3).fill({
29447
+ color,
29448
+ alpha: .78
29449
+ });
29450
+ return;
29451
+ }
29452
+ if (surface === "failed") {
29453
+ graphics.circle(cx, cy, 11).stroke({
29454
+ color,
29455
+ width: 1.5,
29456
+ alpha: .86
29457
+ });
29458
+ graphics.moveTo(cx - 4, cy - 4).lineTo(cx + 4, cy + 4).moveTo(cx + 4, cy - 4).lineTo(cx - 4, cy + 4).stroke({
29459
+ color,
29460
+ width: 1.5,
29461
+ alpha: .94,
29462
+ cap: "round"
29463
+ });
29464
+ return;
29248
29465
  }
29249
- positionShell(parts.root, item);
29250
- const { width, height } = item.frame;
29251
- const resolution = Math.min(4, Math.max(.25, context.zoom * getBoardResolution()));
29252
- const texture = context.scripts?.frame(item, context.time, resolution) ?? null;
29253
- const error = context.scripts?.error(item.id) ?? null;
29254
- parts.sprite.visible = Boolean(texture);
29255
- const display = texture ?? Texture.EMPTY;
29256
- if (parts.sprite.texture !== display) parts.sprite.texture = display;
29257
- parts.sprite.width = width;
29258
- parts.sprite.height = height;
29259
- const selected = context.selectedIds.has(item.id);
29260
- const ready = Boolean(texture) || context.scripts?.ready?.(item.id) === true;
29261
- const message = error ? `${item.script.src}\n${error}` : ready ? "" : item.script.src;
29262
- const sig = [
29263
- width,
29264
- height,
29265
- selected,
29266
- Boolean(texture),
29267
- message,
29268
- context.palette.border,
29269
- context.palette.muted
29270
- ].join("|");
29271
- if (sig === parts.sig) return;
29272
- parts.sig = sig;
29273
- parts.placeholder.clear();
29274
- if (!ready || selected || error) parts.placeholder.rect(0, 0, width, height).stroke({
29275
- color: selected ? context.palette.brand : context.palette.border,
29276
- width: selected ? 2 : 1,
29277
- alpha: .8
29466
+ if (surface === "unavailable") graphics.roundRect(cx - 12, cy - 9, 24, 18, 3).stroke({
29467
+ color,
29468
+ width: 1.25,
29469
+ alpha: .62
29470
+ }).moveTo(cx - 8, cy + 5).lineTo(cx + 8, cy - 5).stroke({
29471
+ color,
29472
+ width: 1.25,
29473
+ alpha: .72,
29474
+ cap: "round"
29278
29475
  });
29279
- parts.label.visible = !ready || Boolean(error);
29280
- parts.label.text = message;
29281
- parts.label.style.fill = error ? context.colors.rose.stroke : context.palette.muted;
29282
- parts.label.style.wordWrapWidth = Math.max(40, width - 16);
29283
- parts.label.position.set(width / 2, height / 2);
29284
29476
  }
29285
- const customCardRenderer = {
29286
- id: "custom-card",
29287
- canRender: (item) => item.type === "custom",
29288
- animated: false,
29289
- create: (item, context) => {
29290
- const root = new Container();
29291
- const sprite = new Sprite(Texture.EMPTY);
29292
- const placeholder = new Graphics();
29293
- const label = new Text({
29294
- text: "",
29295
- style: {
29296
- fontFamily: BOARD_MONO_FONT_STACK,
29297
- fontSize: 12,
29298
- align: "center",
29299
- wordWrap: true,
29300
- breakWords: true
29477
+ function drawFailedBadge(graphics, width, color, context) {
29478
+ const cx = Math.max(12, width - 12);
29479
+ graphics.circle(cx, 12, 8).fill({
29480
+ color: context.palette.bg,
29481
+ alpha: .72
29482
+ });
29483
+ graphics.moveTo(cx - 2.5, 9.5).lineTo(cx + 2.5, 14.5).moveTo(cx + 2.5, 9.5).lineTo(cx - 2.5, 14.5).stroke({
29484
+ color,
29485
+ width: 1.5,
29486
+ alpha: .96,
29487
+ cap: "round"
29488
+ });
29489
+ }
29490
+ const shimmerGradients = /* @__PURE__ */ new Map();
29491
+ function shimmerGradient(color) {
29492
+ let gradient = shimmerGradients.get(color);
29493
+ if (!gradient) {
29494
+ const rgb = `${color >> 16 & 255}, ${color >> 8 & 255}, ${color & 255}`;
29495
+ gradient = new FillGradient({
29496
+ type: "linear",
29497
+ start: {
29498
+ x: 0,
29499
+ y: 0
29301
29500
  },
29302
- resolution: getBoardResolution()
29303
- });
29304
- label.anchor.set(.5);
29305
- root.addChild(sprite, placeholder, label);
29306
- partsByContainer$4.set(root, {
29307
- id: item.id,
29308
- root,
29309
- sprite,
29310
- placeholder,
29311
- label,
29312
- sig: ""
29313
- });
29314
- if (item.type === "custom") sync$4(root, item, context);
29315
- return root;
29316
- },
29317
- update: (container, item, context) => {
29318
- if (item.type === "custom") sync$4(container, item, context);
29319
- },
29320
- renderFar: (graphics, item, context) => {
29321
- drawFarPlate(graphics, item.frame, {
29322
- fill: context.palette.muted,
29323
- fillAlpha: .12
29501
+ end: {
29502
+ x: 1,
29503
+ y: 0
29504
+ },
29505
+ textureSpace: "local",
29506
+ colorStops: [
29507
+ {
29508
+ offset: 0,
29509
+ color: `rgba(${rgb}, 0)`
29510
+ },
29511
+ {
29512
+ offset: .5,
29513
+ color: `rgba(${rgb}, 0.07)`
29514
+ },
29515
+ {
29516
+ offset: 1,
29517
+ color: `rgba(${rgb}, 0)`
29518
+ }
29519
+ ]
29324
29520
  });
29325
- },
29326
- destroy: (container, context) => {
29327
- const parts = partsByContainer$4.get(container);
29328
- if (parts) context.scripts?.release(parts.id);
29329
- parts?.root.destroy({ children: true });
29330
- partsByContainer$4.delete(container);
29521
+ shimmerGradients.set(color, gradient);
29331
29522
  }
29332
- };
29333
- //#endregion
29334
- //#region ../sdk/src/board/render/renderers/image-card-renderer.ts
29335
- const partsByContainer$3 = /* @__PURE__ */ new WeakMap();
29336
- const RADIUS$2 = 2;
29337
- function layoutContain$1(sprite, width, height) {
29338
- const texture = sprite.texture;
29339
- const tw = texture.width;
29340
- const th = texture.height;
29341
- if (!tw || !th) return;
29342
- const scale = Math.min(width / tw, height / th);
29343
- sprite.width = tw * scale;
29344
- sprite.height = th * scale;
29345
- sprite.x = (width - sprite.width) / 2;
29346
- sprite.y = (height - sprite.height) / 2;
29523
+ return gradient;
29347
29524
  }
29348
- function cropRect(crop) {
29349
- if (!crop) return null;
29350
- const w = Math.min(crop.w, 1 - crop.x);
29351
- const h = Math.min(crop.h, 1 - crop.y);
29352
- return w > 0 && h > 0 ? {
29353
- x: crop.x,
29354
- y: crop.y,
29355
- w,
29356
- h
29357
- } : null;
29525
+ function drawShimmerBand(graphics, width, height, color) {
29526
+ const band = Math.max(48, width * .45);
29527
+ graphics.clear().rect(-band, 0, band, height).fill(shimmerGradient(color));
29358
29528
  }
29359
- function showTexture(parts, texture, crop) {
29360
- const rect = texture ? cropRect(crop) : null;
29361
- const key = rect ? `${rect.x}|${rect.y}|${rect.w}|${rect.h}` : "";
29362
- const previous = parts.cropped;
29363
- const reusable = previous && parts.cropSource === texture && parts.cropKey === key;
29364
- if (texture && rect && !reusable) {
29365
- const { frame } = texture;
29366
- const width = Math.max(1, frame.width * rect.w);
29367
- const height = Math.max(1, frame.height * rect.h);
29368
- const x = frame.x + Math.min(frame.width * rect.x, frame.width - width);
29369
- const y = frame.y + Math.min(frame.height * rect.y, frame.height - height);
29370
- parts.cropped = new Texture({
29371
- source: texture.source,
29372
- frame: new Rectangle(x, y, width, height)
29529
+ const MOSAIC_FRAME_MS = 160;
29530
+ const MOSAIC_CELL = 8;
29531
+ function drawMosaic(graphics, width, height, color, frame) {
29532
+ const columns = Math.max(1, Math.floor(width / MOSAIC_CELL));
29533
+ const rows = Math.max(1, Math.floor(height / MOSAIC_CELL));
29534
+ const left = (width - columns * MOSAIC_CELL) / 2;
29535
+ const top = (height - rows * MOSAIC_CELL) / 2;
29536
+ graphics.clear();
29537
+ for (const [threshold, alpha] of [[.08, .16], [.02, .3]]) {
29538
+ for (let row = 0; row < rows; row += 1) for (let column = 0; column < columns; column += 1) {
29539
+ let hash = Math.imul(column + 1, 2654435761) ^ Math.imul(row + 1, 2246822507) ^ Math.imul(frame + 1, 3266489909);
29540
+ hash ^= hash >>> 15;
29541
+ hash = Math.imul(hash, 739982445);
29542
+ hash ^= hash >>> 12;
29543
+ if ((hash >>> 0) / 4294967295 < threshold) graphics.rect(left + column * MOSAIC_CELL + 1, top + row * MOSAIC_CELL + 1, 6, 6);
29544
+ }
29545
+ graphics.fill({
29546
+ color,
29547
+ alpha
29373
29548
  });
29374
- } else if (!rect) parts.cropped = null;
29375
- parts.cropSource = rect ? texture : null;
29376
- parts.cropKey = key;
29377
- const display = parts.cropped ?? texture ?? Texture.EMPTY;
29378
- if (parts.sprite.texture !== display) parts.sprite.texture = display;
29379
- if (previous && previous !== parts.cropped) previous.destroy(false);
29549
+ }
29550
+ }
29551
+ function advance(parts, clock) {
29552
+ const { motion } = parts;
29553
+ let next = null;
29554
+ if (motion.mosaic) {
29555
+ const frame = Math.floor(clock / MOSAIC_FRAME_MS);
29556
+ if (frame !== motion.mosaic.step) {
29557
+ motion.mosaic.step = frame;
29558
+ drawMosaic(parts.shimmer, motion.width, motion.mosaic.height, motion.mosaic.color, frame);
29559
+ }
29560
+ next = MOSAIC_FRAME_MS - clock % MOSAIC_FRAME_MS;
29561
+ } else if (motion.shimmer) {
29562
+ const travel = motion.width + parts.shimmer.width;
29563
+ parts.shimmer.x = clock % SHIMMER_PERIOD_MS / SHIMMER_PERIOD_MS * travel;
29564
+ next = SHIMMER_FRAME_MS;
29565
+ }
29566
+ if (motion.startedAt !== null) {
29567
+ const elapsedMs = Date.now() - motion.startedAt;
29568
+ const elapsed = formatBoardDuration(elapsedMs);
29569
+ if (elapsed !== motion.elapsed) {
29570
+ motion.elapsed = elapsed;
29571
+ parts.body.text = `${motion.label} · ${elapsed}`;
29572
+ }
29573
+ const untilNextSecond = SECOND - Math.max(0, elapsedMs) % SECOND;
29574
+ next = next === null ? untilNextSecond : Math.min(next, untilNextSecond);
29575
+ }
29576
+ if (motion.revealFrom !== null) {
29577
+ const progress = Math.min(1, (clock - motion.revealFrom) / REVEAL_MS);
29578
+ parts.preview.alpha = progress * (2 - progress);
29579
+ if (progress >= 1) motion.revealFrom = null;
29580
+ else next = 0;
29581
+ }
29582
+ return next;
29380
29583
  }
29381
29584
  function sync$3(container, item, context) {
29382
29585
  const parts = partsByContainer$3.get(container);
29383
29586
  if (!parts) return;
29384
29587
  positionShell(parts.root, item);
29385
29588
  const { width, height } = item.frame;
29386
- const selected = context.selectedIds.has(item.id);
29387
- const hovered = context.hoveredId === item.id;
29589
+ const { snapshot } = item.props;
29590
+ const { status, startedAt } = liveStatus(item, context);
29591
+ const full = boardCardDetailed(context.zoom);
29592
+ const elevation = cardElevation(context, item.id);
29593
+ syncCardTag(parts.tag, {
29594
+ glyph: "task",
29595
+ text: snapshot.title,
29596
+ width
29597
+ }, context);
29598
+ const working = status === "pending" || status === "running";
29599
+ const color = statusColor(status, context);
29388
29600
  const key = context.assetKey(item);
29601
+ if (key !== parts.assetKey) {
29602
+ if (parts.assetKey) context.releaseTexture(parts.assetKey);
29603
+ parts.assetKey = key;
29604
+ if (key) context.acquireTexture(key);
29605
+ }
29389
29606
  const texture = key ? context.getTexture(key) : null;
29390
- const failed = Boolean(key && !texture && context.hasError(key));
29391
- const texId = texture ? `${texture.width}x${texture.height}` : "none";
29392
- const sig = [
29393
- key ?? "",
29607
+ const previewFailed = Boolean(key && !texture && context.hasError(key));
29608
+ const artifact = featuredTaskArtifact(snapshot.artifacts);
29609
+ const kind = previewKindFor(artifact);
29610
+ const surface = stateSurfaceFor(status, kind, Boolean(texture), previewFailed);
29611
+ const frame = {
29612
+ x: 1,
29613
+ y: 1,
29614
+ width: Math.max(1, width - 2),
29615
+ height: Math.max(1, height - 2)
29616
+ };
29617
+ const metaText = artifact || working ? snapshot.model ?? "" : "";
29618
+ const showMeta = full && Boolean(metaText);
29619
+ const failedWithOutput = status === "failed" && kind !== "empty";
29620
+ const count = artifact && snapshot.artifactCount > 1 ? String(snapshot.artifactCount) : "";
29621
+ const showBadge = full && Boolean(count);
29622
+ const label = stateLabel(surface, context);
29623
+ const counting = full && working && startedAt !== null && surface !== null;
29624
+ const elapsed = counting ? formatBoardDuration(Date.now() - startedAt) : "";
29625
+ const detailText = !full || !surface ? "" : status === "failed" ? snapshot.errorExcerpt ?? "" : working ? snapshot.promptExcerpt ?? "" : "";
29626
+ const textArtifact = kind === "text" && artifact?.type === "text";
29627
+ const bodyText = textArtifact ? artifact.textExcerpt : counting ? `${label} · ${elapsed}` : label;
29628
+ syncTextResolution(parts.body, parts, context.zoom);
29629
+ syncTextResolution(parts.detail, parts, context.zoom);
29630
+ syncTextResolution(parts.meta, parts, context.zoom);
29631
+ syncTextResolution(parts.badge, parts, context.zoom);
29632
+ const textSig = [
29633
+ counting ? label : bodyText,
29634
+ detailText,
29635
+ metaText,
29636
+ count,
29637
+ kind,
29638
+ surface,
29639
+ full,
29640
+ width,
29641
+ height,
29642
+ color,
29643
+ context.palette.text,
29644
+ context.palette.muted,
29645
+ context.style.font
29646
+ ].join("|");
29647
+ if (textSig !== parts.textSig) {
29648
+ parts.textSig = textSig;
29649
+ parts.body.style.fontFamily = context.style.font;
29650
+ parts.detail.style.fontFamily = context.style.font;
29651
+ const wrap = Math.max(24, width - 24);
29652
+ parts.body.style.fill = surface === "failed" ? color : context.palette.text;
29653
+ parts.body.anchor.set(textArtifact ? 0 : .5, 0);
29654
+ fitTextToLines(parts.body, bodyText, textArtifact ? Math.max(1, Math.floor((height - 24 - (showMeta ? META_HEIGHT : 0)) / 16)) : 1, wrap);
29655
+ parts.detail.style.fill = context.palette.muted;
29656
+ fitTextToLines(parts.detail, detailText, height - 24 > 120 ? 2 : 1, Math.min(wrap, 360));
29657
+ parts.meta.style.fill = context.palette.text;
29658
+ fitTextToLines(parts.meta, metaText, 1, wrap);
29659
+ parts.badge.style.fill = context.palette.text;
29660
+ if (parts.badge.text !== count) parts.badge.text = count;
29661
+ }
29662
+ const badgeWidth = Math.max(22, parts.badge.width + 12);
29663
+ const visualSig = [
29394
29664
  width,
29395
29665
  height,
29396
- selected,
29397
- hovered,
29398
- texId,
29399
- JSON.stringify(item.props.crop ?? null),
29400
- failed,
29401
- context.colorScheme
29666
+ full,
29667
+ elevation,
29668
+ color,
29669
+ kind,
29670
+ surface,
29671
+ showMeta,
29672
+ showBadge && badgeWidth,
29673
+ failedWithOutput,
29674
+ artifact?.type ?? "none",
29675
+ artifact?.id ?? "none",
29676
+ item.props.taskRunId,
29677
+ texture ? `${texture.width}x${texture.height}` : "none",
29678
+ context.themeKey
29402
29679
  ].join("|");
29403
- if (sig === parts.sig) return;
29404
- parts.sig = sig;
29405
- showTexture(parts, texture, item.props.crop);
29406
- parts.sprite.visible = Boolean(texture);
29407
- if (texture) layoutContain$1(parts.sprite, width, height);
29408
- parts.placeholder.clear();
29409
- if (!texture) {
29410
- parts.placeholder.roundRect(0, 0, width, height, RADIUS$2).fill({
29411
- color: context.palette.surface,
29412
- alpha: .85
29413
- }).roundRect(0, 0, width, height, RADIUS$2).stroke({
29414
- color: failed ? context.palette.border : context.palette.hover,
29415
- width: 1,
29416
- alpha: .9
29680
+ if (visualSig !== parts.visualSig) {
29681
+ parts.visualSig = visualSig;
29682
+ const rect = {
29683
+ x: 0,
29684
+ y: 0,
29685
+ width,
29686
+ height
29687
+ };
29688
+ drawCardSurface(parts.plate.clear(), rect, context.style, {
29689
+ fill: context.palette.surface,
29690
+ border: context.palette.border,
29691
+ borderAlpha: .82,
29692
+ elevation
29417
29693
  });
29418
- const cx = width / 2;
29419
- const cy = height / 2;
29420
- if (failed) parts.placeholder.moveTo(cx - 10, cy - 10).lineTo(cx + 10, cy + 10).moveTo(cx + 10, cy - 10).lineTo(cx - 10, cy + 10).stroke({
29421
- color: context.palette.muted,
29422
- width: 1.5,
29423
- alpha: .8
29694
+ traceCardOutline(parts.clip.clear(), rect, context.style, -1).fill({ color: 16777215 });
29695
+ parts.previewBg.clear().rect(frame.x, frame.y, frame.width, frame.height).fill({
29696
+ color: context.palette.hover,
29697
+ alpha: .5
29424
29698
  });
29425
- else parts.placeholder.circle(cx, cy, 6).stroke({
29426
- color: context.palette.muted,
29427
- width: 1.25,
29428
- alpha: .55
29699
+ parts.previewMask.clear().rect(frame.x, frame.y, frame.width, frame.height).fill({ color: 16777215 });
29700
+ if (context.style.progress === "shimmer") drawShimmerBand(parts.shimmer, frame.width, frame.height, context.palette.text);
29701
+ parts.shimmer.position.set(context.style.progress === "shimmer" ? 0 : frame.x, frame.y);
29702
+ parts.previewArt.clear();
29703
+ if (kind === "waveform") {
29704
+ const bottomInset = showMeta ? META_HEIGHT : 0;
29705
+ drawAudioWaveform(parts.previewArt, item.props.taskRunId, {
29706
+ x: frame.x + PADDING,
29707
+ y: frame.y + PADDING,
29708
+ width: Math.max(1, frame.width - 24),
29709
+ height: Math.max(1, frame.height - 24 - bottomInset)
29710
+ }, context.colors.brand.stroke);
29711
+ }
29712
+ if (full && (artifact?.type === "audio" || artifact?.type === "video" && texture)) drawPlayBadge(parts.previewArt, frame, context);
29713
+ if (surface) {
29714
+ const markY = frame.y + frame.height / 2 - (full && label ? 18 : 0);
29715
+ drawStateMark(parts.previewArt, surface, frame.x + frame.width / 2, markY, surface === "failed" ? color : context.colors.neutral.stroke);
29716
+ }
29717
+ if (showMeta) parts.previewArt.rect(frame.x, frame.y + frame.height - META_HEIGHT, frame.width, META_HEIGHT).fill({
29718
+ color: context.palette.bg,
29719
+ alpha: .68
29429
29720
  });
29430
- }
29431
- parts.placeholder.visible = !texture;
29432
- if (selected || hovered) {
29433
- const hasPixels = Boolean(texture) && parts.sprite.width > 0;
29434
- const ox = hasPixels ? parts.sprite.x : 0;
29435
- const oy = hasPixels ? parts.sprite.y : 0;
29436
- const ow = hasPixels ? parts.sprite.width : width;
29437
- const oh = hasPixels ? parts.sprite.height : height;
29438
- parts.placeholder.visible = true;
29439
- parts.placeholder.roundRect(ox, oy, ow, oh, RADIUS$2).stroke({
29440
- color: selected ? context.palette.brand : context.palette.muted,
29441
- width: selected ? 2 : 1,
29442
- alpha: selected ? .95 : .7
29721
+ if (showBadge) parts.previewArt.roundRect(width - badgeWidth - 8, 8, badgeWidth, 20, 10).fill({
29722
+ color: context.palette.bg,
29723
+ alpha: .72
29443
29724
  });
29725
+ if (failedWithOutput) drawFailedBadge(parts.previewArt, width, color, context);
29444
29726
  }
29727
+ const showPreview = kind === "texture" && Boolean(texture);
29728
+ const revealKey = showPreview ? `${artifact?.id}:${texture?.uid}` : null;
29729
+ const sameCard = parts.motion.itemId === item.id;
29730
+ parts.motion.itemId = item.id;
29731
+ if (revealKey !== parts.motion.revealKey || !sameCard) {
29732
+ const appearing = sameCard && parts.motion.revealKey === null && revealKey !== null;
29733
+ parts.motion.revealKey = revealKey;
29734
+ parts.motion.revealFrom = appearing && !context.reducedMotion ? performance.now() : null;
29735
+ parts.preview.alpha = parts.motion.revealFrom === null ? 1 : 0;
29736
+ }
29737
+ parts.preview.visible = showPreview;
29738
+ if (showPreview && texture) {
29739
+ if (parts.preview.texture !== texture) parts.preview.texture = texture;
29740
+ const fitted = containRect(frame.width, frame.height, texture.width, texture.height);
29741
+ parts.preview.position.set(frame.x + fitted.x, frame.y + fitted.y);
29742
+ parts.preview.width = fitted.width;
29743
+ parts.preview.height = fitted.height;
29744
+ }
29745
+ parts.body.visible = full && Boolean(bodyText);
29746
+ parts.detail.visible = full && Boolean(detailText);
29747
+ parts.meta.visible = showMeta;
29748
+ parts.badge.visible = showBadge;
29749
+ if (textArtifact) parts.body.position.set(PADDING, PADDING);
29750
+ else parts.body.position.set(width / 2, height / 2 + 2);
29751
+ parts.detail.position.set(width / 2, height / 2 + 2 + parts.body.height + 4);
29752
+ parts.meta.position.set(PADDING, height - META_HEIGHT + 7);
29753
+ parts.badge.position.set(width - 8 - badgeWidth / 2, 18);
29754
+ parts.motion.width = frame.width;
29755
+ parts.motion.shimmer = full && working && kind !== "texture" && !context.reducedMotion;
29756
+ const mosaic = parts.motion.shimmer && context.style.progress === "mosaic";
29757
+ parts.motion.mosaic = mosaic ? {
29758
+ color: context.palette.brand,
29759
+ height: frame.height,
29760
+ step: -1
29761
+ } : null;
29762
+ parts.shimmer.visible = parts.motion.shimmer;
29763
+ parts.motion.startedAt = counting ? startedAt : null;
29764
+ parts.motion.label = label;
29765
+ parts.motion.elapsed = elapsed;
29445
29766
  }
29446
- const imageCardRenderer = {
29447
- id: "image-card",
29448
- canRender: (item) => item.type === "image",
29767
+ const taskCardRenderer = {
29768
+ id: "task-card",
29769
+ canRender: (item) => item.type === "task",
29449
29770
  create: (item, context) => {
29450
29771
  const root = new Container();
29451
- const sprite = new Sprite(Texture.EMPTY);
29452
- const placeholder = new Graphics();
29453
- const mask = new Graphics();
29454
- const maskBox = () => {
29455
- mask.clear();
29456
- if (item.type === "image") mask.roundRect(0, 0, item.frame.width, item.frame.height, RADIUS$2).fill({ color: 16777215 });
29457
- };
29458
- maskBox();
29459
- root.addChild(placeholder, sprite, mask);
29460
- sprite.mask = mask;
29772
+ const plate = new Graphics();
29773
+ const clip = new Graphics();
29774
+ const previewBg = new Graphics();
29775
+ const preview = new Sprite();
29776
+ const previewMask = new Graphics();
29777
+ const shimmer = new Graphics();
29778
+ const previewArt = new Graphics();
29779
+ const resolution = textResolutionForZoom(context.zoom);
29780
+ const text = (fontFamily, fontSize, fontWeight) => new Text({
29781
+ text: "",
29782
+ style: {
29783
+ fontFamily,
29784
+ fontSize,
29785
+ fontWeight,
29786
+ wordWrap: true,
29787
+ breakWords: true
29788
+ },
29789
+ resolution,
29790
+ roundPixels: true
29791
+ });
29792
+ const body = text(BOARD_FONT_STACK, 11.5, "500");
29793
+ const detail = text(BOARD_FONT_STACK, 10.5, "400");
29794
+ const meta = text(BOARD_MONO_FONT_STACK, 10, "500");
29795
+ const badge = text(BOARD_MONO_FONT_STACK, 10, "500");
29796
+ badge.style.wordWrap = false;
29797
+ badge.anchor.set(.5);
29798
+ detail.anchor.set(.5, 0);
29799
+ detail.style.align = "center";
29800
+ preview.mask = previewMask;
29801
+ const content = new Container();
29802
+ content.mask = clip;
29803
+ content.addChild(previewBg, shimmer, preview, previewMask, previewArt, body, detail, meta, badge);
29804
+ const tag = createCardTag();
29805
+ root.addChild(plate, content, clip, tag.root);
29461
29806
  partsByContainer$3.set(root, {
29462
29807
  root,
29463
- sprite,
29464
- placeholder,
29465
- sig: "",
29466
- cropped: null,
29467
- cropSource: null,
29468
- cropKey: ""
29808
+ tag,
29809
+ plate,
29810
+ clip,
29811
+ previewBg,
29812
+ preview,
29813
+ previewMask,
29814
+ shimmer,
29815
+ previewArt,
29816
+ badge,
29817
+ body,
29818
+ detail,
29819
+ meta,
29820
+ assetKey: null,
29821
+ visualSig: "",
29822
+ textSig: "",
29823
+ resolution,
29824
+ motion: {
29825
+ itemId: "",
29826
+ width: 0,
29827
+ shimmer: false,
29828
+ mosaic: null,
29829
+ startedAt: null,
29830
+ label: "",
29831
+ elapsed: "",
29832
+ revealFrom: null,
29833
+ revealKey: null
29834
+ }
29469
29835
  });
29470
- if (item.type === "image") sync$3(root, item, context);
29836
+ if (item.type === "task") sync$3(root, item, context);
29471
29837
  return root;
29472
29838
  },
29473
29839
  update: (container, item, context) => {
29474
- if (item.type !== "image") return;
29840
+ if (item.type === "task") sync$3(container, item, context);
29841
+ },
29842
+ tick: (container, clock) => {
29475
29843
  const parts = partsByContainer$3.get(container);
29476
- if (!parts) return;
29477
- const mask = parts.root.children.find((child) => child !== parts.sprite && child !== parts.placeholder);
29478
- if (mask) mask.clear().roundRect(0, 0, item.frame.width, item.frame.height, RADIUS$2).fill({ color: 16777215 });
29479
- sync$3(container, item, context);
29844
+ return parts ? advance(parts, clock) : null;
29845
+ },
29846
+ zoom: (container, zoom) => {
29847
+ const tag = partsByContainer$3.get(container)?.tag;
29848
+ if (tag) zoomCardTag(tag, zoom);
29480
29849
  },
29481
29850
  renderFar: (graphics, item, context) => {
29851
+ if (item.type !== "task") return;
29852
+ const { status } = liveStatus(item, context);
29853
+ const active = status === "failed" || status === "running" || status === "pending";
29482
29854
  drawFarPlate(graphics, item.frame, {
29483
- fill: context.palette.hover,
29484
- fillAlpha: .9
29855
+ fill: context.palette.surface,
29856
+ fillAlpha: .82,
29857
+ ...active ? {
29858
+ accent: statusColor(status, context),
29859
+ accentAlpha: .9
29860
+ } : {}
29485
29861
  });
29486
29862
  },
29487
- destroy: (container) => {
29863
+ destroy: (container, context) => {
29488
29864
  const parts = partsByContainer$3.get(container);
29865
+ if (parts?.assetKey) context.releaseTexture(parts.assetKey);
29489
29866
  parts?.root.destroy({ children: true });
29490
- parts?.cropped?.destroy(false);
29491
29867
  partsByContainer$3.delete(container);
29492
29868
  }
29493
29869
  };
@@ -29591,37 +29967,37 @@ const textCardRenderer = {
29591
29967
  };
29592
29968
  //#endregion
29593
29969
  //#region ../sdk/src/board/render/renderers/unknown-card-renderer.ts
29594
- const RADIUS$1 = 10;
29595
29970
  const partsByContainer$1 = /* @__PURE__ */ new WeakMap();
29596
29971
  function sync$1(container, item, context) {
29597
29972
  const parts = partsByContainer$1.get(container);
29598
29973
  if (!parts) return;
29599
29974
  positionShell(parts.root, item);
29600
29975
  const { width, height } = item.frame;
29601
- const selected = context.selectedIds.has(item.id);
29602
29976
  const realType = item.type;
29603
29977
  syncTextResolution(parts.label, parts, context.zoom);
29978
+ const elevation = cardElevation(context, item.id);
29604
29979
  const visualSig = [
29605
29980
  width,
29606
29981
  height,
29607
- selected,
29608
- context.palette.surface,
29609
- context.palette.brand,
29610
- context.palette.border,
29611
- context.palette.muted
29982
+ elevation,
29983
+ context.themeKey
29612
29984
  ].join("|");
29613
29985
  if (visualSig !== parts.visualSig) {
29614
29986
  parts.visualSig = visualSig;
29615
- parts.box.clear();
29616
- parts.box.roundRect(0, 0, width, height, RADIUS$1).fill({
29617
- color: context.palette.surface,
29618
- alpha: .7
29619
- }).roundRect(0, 0, width, height, RADIUS$1).stroke({
29620
- color: selected ? context.palette.brand : context.palette.border,
29621
- width: selected ? 2 : 1,
29622
- alpha: .8
29987
+ const rect = {
29988
+ x: 0,
29989
+ y: 0,
29990
+ width,
29991
+ height
29992
+ };
29993
+ drawCardSurface(parts.box.clear(), rect, context.style, {
29994
+ fill: context.palette.surface,
29995
+ fillAlpha: .7,
29996
+ border: context.palette.border,
29997
+ borderAlpha: .8,
29998
+ elevation
29623
29999
  });
29624
- parts.box.roundRect(6, 6, width - 12, height - 12, 6).stroke({
30000
+ traceCardOutline(parts.box, rect, context.style, -6).stroke({
29625
30001
  color: context.palette.muted,
29626
30002
  width: 1,
29627
30003
  alpha: .4
@@ -29683,86 +30059,87 @@ const unknownCardRenderer = {
29683
30059
  //#endregion
29684
30060
  //#region ../sdk/src/board/render/renderers/video-card-renderer.ts
29685
30061
  const partsByContainer = /* @__PURE__ */ new WeakMap();
29686
- const RADIUS = 4;
29687
- function layoutContain(sprite, width, height) {
29688
- const texture = sprite.texture;
29689
- if (!texture.width || !texture.height) return;
29690
- const scale = Math.min(width / texture.width, height / texture.height);
29691
- sprite.width = texture.width * scale;
29692
- sprite.height = texture.height * scale;
29693
- sprite.x = (width - sprite.width) / 2;
29694
- sprite.y = (height - sprite.height) / 2;
30062
+ const FILM_RAIL = 10;
30063
+ const FILM_HOLE = 4;
30064
+ function videoTitle(item) {
30065
+ return item.props.snapshot?.title ?? getMediaResourceTitle(item.props.src);
29695
30066
  }
29696
30067
  function sync(container, item, context) {
29697
30068
  const parts = partsByContainer.get(container);
29698
30069
  if (!parts) return;
29699
30070
  positionShell(parts.root, item);
29700
30071
  const { width, height } = item.frame;
29701
- const selected = context.selectedIds.has(item.id);
29702
- const hovered = context.hoveredId === item.id;
29703
- const resizing = context.resizingIds.has(item.id);
29704
- const title = item.props.snapshot?.title ?? getMediaResourceTitle(item.props.src);
30072
+ const title = videoTitle(item);
30073
+ const durationMs = item.props.snapshot?.durationMs;
30074
+ const duration = durationMs ? formatBoardDuration(durationMs) : "";
30075
+ syncCardTag(parts.tag, {
30076
+ glyph: "video",
30077
+ text: duration ? `${title} · ${duration}` : title,
30078
+ width
30079
+ }, context);
29705
30080
  const key = context.assetKey(item);
29706
30081
  const texture = key ? context.getTexture(key) : null;
29707
- const failed = Boolean(key && !texture && context.hasError(key));
29708
- syncTextResolution(parts.label, parts, context.zoom);
29709
- if (parts.label.text !== title) parts.label.text = title;
29710
- syncTextWrapWidth(parts.label, parts, Math.max(1, width - 16), resizing);
29711
- parts.label.position.set(8, Math.max(8, height - parts.label.height - 8));
30082
+ const elevation = cardElevation(context, item.id);
29712
30083
  const sig = [
29713
30084
  key ?? "",
29714
30085
  texture ? `${texture.width}x${texture.height}` : "none",
29715
30086
  width,
29716
30087
  height,
29717
- selected,
29718
- hovered,
29719
- failed,
29720
- title,
29721
- item.props.time ?? 0,
29722
- parts.label.height,
29723
- context.palette.surface,
29724
- context.palette.brand,
29725
- context.palette.muted,
29726
- context.palette.border,
29727
- context.palette.hover,
29728
- context.palette.text
30088
+ elevation,
30089
+ boardCardDetailed(context.zoom),
30090
+ context.themeKey
29729
30091
  ].join("|");
29730
30092
  if (sig === parts.sig) return;
29731
30093
  parts.sig = sig;
30094
+ const framed = context.style.media === "framed";
30095
+ const rect = {
30096
+ x: 0,
30097
+ y: 0,
30098
+ width,
30099
+ height
30100
+ };
30101
+ const rail = framed ? Math.min(FILM_RAIL, height / 6) : 0;
30102
+ const screen = {
30103
+ x: framed ? rail * .6 : 0,
30104
+ y: rail,
30105
+ width: width - (framed ? rail * 1.2 : 0),
30106
+ height: height - rail * 2
30107
+ };
30108
+ drawCardSurface(parts.plate.clear(), rect, context.style, {
30109
+ fill: framed ? context.palette.text : context.palette.surface,
30110
+ border: framed ? null : context.palette.border,
30111
+ borderAlpha: .85,
30112
+ elevation
30113
+ });
30114
+ if (framed) drawFilmRails(parts.plate, width, height, rail, context.palette.bg);
30115
+ traceCardOutline(parts.mask.clear(), framed ? screen : rect, framed ? {
30116
+ corner: "round",
30117
+ radius: 1
30118
+ } : context.style).fill({ color: 16777215 });
29732
30119
  if (texture && parts.sprite.texture !== texture) parts.sprite.texture = texture;
29733
30120
  parts.sprite.visible = Boolean(texture);
29734
- if (texture) layoutContain(parts.sprite, width, height);
29735
- parts.mask.clear().roundRect(0, 0, width, height, RADIUS).fill({ color: 16777215 });
29736
- parts.plate.clear().roundRect(0, 0, width, height, RADIUS).fill({
29737
- color: context.palette.surface,
29738
- alpha: .96
29739
- });
29740
- parts.chrome.clear();
29741
30121
  if (texture) {
29742
- const bandHeight = Math.min(height, parts.label.height + 16);
29743
- parts.chrome.rect(0, height - bandHeight, width, bandHeight).fill({
29744
- color: context.palette.surface,
29745
- alpha: .78
29746
- });
30122
+ const fitted = containRect(screen.width, screen.height, texture.width, texture.height);
30123
+ parts.sprite.position.set(screen.x + fitted.x, screen.y + fitted.y);
30124
+ parts.sprite.width = fitted.width;
30125
+ parts.sprite.height = fitted.height;
29747
30126
  }
29748
- const cx = width / 2;
29749
- const cy = height / 2;
29750
- const radius = Math.min(22, Math.min(width, height) * .18);
29751
- parts.chrome.circle(cx, cy, radius).fill({
29752
- color: selected ? context.palette.brand : context.palette.surface,
29753
- alpha: texture ? .88 : .96
29754
- });
29755
- const triangle = radius * .55;
29756
- parts.chrome.moveTo(cx - triangle * .35, cy - triangle * .6).lineTo(cx - triangle * .35, cy + triangle * .6).lineTo(cx + triangle * .7, cy).closePath().fill({
29757
- color: context.palette.text,
29758
- alpha: .94
29759
- });
29760
- parts.chrome.roundRect(0, 0, width, height, RADIUS).stroke({
29761
- color: selected ? context.palette.brand : hovered ? context.palette.muted : context.palette.border,
29762
- width: selected ? 2 : 1,
29763
- alpha: selected ? .95 : .85
30127
+ parts.chrome.clear();
30128
+ if (boardCardDetailed(context.zoom)) drawMediaPlayKey(parts.chrome, screen.x + screen.width / 2, screen.y + screen.height / 2, Math.min(18, Math.min(screen.width, screen.height) * .16), context);
30129
+ }
30130
+ function drawFilmRails(graphics, width, height, rail, hole) {
30131
+ const size = Math.min(FILM_HOLE, rail * .45);
30132
+ const step = size * 2.5;
30133
+ const count = Math.max(1, Math.floor((width - step) / step));
30134
+ const start = (width - (count - 1) * step - size) / 2;
30135
+ for (let index = 0; index < count; index += 1) {
30136
+ const x = start + index * step;
30137
+ graphics.rect(x, (rail - size) / 2, size, size).rect(x, height - rail + (rail - size) / 2, size, size);
30138
+ }
30139
+ graphics.fill({
30140
+ color: hole,
30141
+ alpha: .9
29764
30142
  });
29765
- parts.label.style.fill = texture ? context.palette.text : context.palette.muted;
29766
30143
  }
29767
30144
  const videoCardRenderer = {
29768
30145
  id: "video-card",
@@ -29773,20 +30150,8 @@ const videoCardRenderer = {
29773
30150
  const sprite = new Sprite(Texture.EMPTY);
29774
30151
  const mask = new Graphics();
29775
30152
  const chrome = new Graphics();
29776
- const resolution = textResolutionForZoom(context.zoom);
29777
- const label = new Text({
29778
- text: "",
29779
- style: {
29780
- fill: context.palette.muted,
29781
- fontFamily: BOARD_FONT_STACK,
29782
- fontSize: 12,
29783
- fontWeight: "500",
29784
- wordWrap: true
29785
- },
29786
- resolution,
29787
- roundPixels: true
29788
- });
29789
- root.addChild(plate, sprite, mask, chrome, label);
30153
+ const tag = createCardTag();
30154
+ root.addChild(plate, sprite, mask, chrome, tag.root);
29790
30155
  sprite.mask = mask;
29791
30156
  partsByContainer.set(root, {
29792
30157
  root,
@@ -29794,10 +30159,8 @@ const videoCardRenderer = {
29794
30159
  sprite,
29795
30160
  mask,
29796
30161
  chrome,
29797
- label,
29798
- sig: "",
29799
- wrapWidth: 0,
29800
- resolution
30162
+ tag,
30163
+ sig: ""
29801
30164
  });
29802
30165
  if (item.type === "video") sync(root, item, context);
29803
30166
  return root;
@@ -29805,6 +30168,10 @@ const videoCardRenderer = {
29805
30168
  update: (container, item, context) => {
29806
30169
  if (item.type === "video") sync(container, item, context);
29807
30170
  },
30171
+ zoom: (container, zoom) => {
30172
+ const tag = partsByContainer.get(container)?.tag;
30173
+ if (tag) zoomCardTag(tag, zoom);
30174
+ },
29808
30175
  renderFar: (graphics, item, context) => {
29809
30176
  drawFarPlate(graphics, item.frame, {
29810
30177
  fill: context.palette.surface,
@@ -30729,6 +31096,58 @@ function createScriptRuntime(module, initial, host) {
30729
31096
  };
30730
31097
  }
30731
31098
  //#endregion
31099
+ //#region ../sdk/src/board/render/style.ts
31100
+ const BASE = {
31101
+ corner: "round",
31102
+ radius: 6,
31103
+ selection: {
31104
+ width: 1.5,
31105
+ outset: 3,
31106
+ dash: 0,
31107
+ handle: "square",
31108
+ handleSize: 8
31109
+ },
31110
+ media: "plain",
31111
+ progress: "shimmer",
31112
+ icons: "line",
31113
+ motion: "smooth",
31114
+ font: BOARD_FONT_STACK
31115
+ };
31116
+ const SHADOW = {
31117
+ light: {
31118
+ color: 1447704,
31119
+ alpha: .06,
31120
+ blur: 12,
31121
+ y: 2
31122
+ },
31123
+ dark: {
31124
+ color: 0,
31125
+ alpha: .24,
31126
+ blur: 12,
31127
+ y: 2
31128
+ }
31129
+ };
31130
+ function defaultBoardRenderStyle(mode) {
31131
+ return {
31132
+ ...BASE,
31133
+ selection: { ...BASE.selection },
31134
+ shadow: { ...SHADOW[mode] }
31135
+ };
31136
+ }
31137
+ function resolveBoardRenderStyle(mode, style) {
31138
+ const base = defaultBoardRenderStyle(mode);
31139
+ if (!style) return base;
31140
+ return {
31141
+ ...base,
31142
+ ...style,
31143
+ selection: {
31144
+ ...base.selection,
31145
+ ...style.selection
31146
+ },
31147
+ shadow: style.shadow === void 0 ? base.shadow : style.shadow
31148
+ };
31149
+ }
31150
+ //#endregion
30732
31151
  //#region ../sdk/src/board/export/scene.ts
30733
31152
  function buildContext(input) {
30734
31153
  const missing = /* @__PURE__ */ new Set();
@@ -30742,12 +31161,15 @@ function buildContext(input) {
30742
31161
  selectedIds: /* @__PURE__ */ new Set(),
30743
31162
  hoveredId: null,
30744
31163
  resizingIds: /* @__PURE__ */ new Set(),
31164
+ liftedIds: /* @__PURE__ */ new Set(),
30745
31165
  palette: {
30746
31166
  ...defaultBoardPalette(input.colorScheme),
30747
31167
  ...input.palette
30748
31168
  },
30749
31169
  colors: input.colors ?? buildFallbackShapeColors(input.colorScheme),
30750
31170
  colorScheme: input.colorScheme,
31171
+ style: resolveBoardRenderStyle(input.colorScheme, input.style),
31172
+ themeKey: "export",
30751
31173
  rendererType: "canvas",
30752
31174
  zoom: input.scale,
30753
31175
  reducedMotion: true,
@@ -30907,6 +31329,7 @@ function renderBoardExport(renderer, input, options = {}) {
30907
31329
  colorScheme,
30908
31330
  palette,
30909
31331
  colors: options.colors,
31332
+ style: options.style,
30910
31333
  textures: options.textures,
30911
31334
  scripts: options.scripts,
30912
31335
  assetKey: options.assetKey,