@neta-art/cohub-cli 10.1.0 → 10.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5772,10 +5772,10 @@ const colorString$1 = string().max(120).refine(isBoardColorString$1, "expected a
5772
5772
  const BoardColorSchema$1 = union([colorString$1, object({
5773
5773
  light: colorString$1,
5774
5774
  dark: colorString$1
5775
- }).strict()]);
5775
+ }).strict()]).describe("Palette token (follows the theme), CSS color, or { light, dark }.");
5776
5776
  const BoardStyleSchema$1 = object({
5777
5777
  fill: BoardColorSchema$1.optional(),
5778
- fillOpacity: ratio$1.optional(),
5778
+ fillOpacity: ratio$1.optional().describe("Fill opacity, independent of the item's opacity."),
5779
5779
  stroke: BoardColorSchema$1.optional(),
5780
5780
  strokeWidth: finite$1.min(0).max(64).optional(),
5781
5781
  dash: _enum([
@@ -5783,8 +5783,8 @@ const BoardStyleSchema$1 = object({
5783
5783
  "dashed",
5784
5784
  "dotted"
5785
5785
  ]).optional(),
5786
- radius: nonNegative$1.optional(),
5787
- trim: ratio$1.optional()
5786
+ radius: nonNegative$1.optional().describe("Corner radius in board units."),
5787
+ trim: ratio$1.optional().describe("Fraction of the outline drawn; animate 0 → 1 to draw it in.")
5788
5788
  }).strict();
5789
5789
  function isSafeBoardSourcePath$1(value) {
5790
5790
  return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !/^[a-z][a-z\d+.-]*:/i.test(value.trimStart()) && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
@@ -5800,18 +5800,18 @@ function parseBoardMediaSource$1(src) {
5800
5800
  value: src
5801
5801
  } : null;
5802
5802
  }
5803
- const BoardMediaSourceSchema$1 = string().max(4096).refine((src) => parseBoardMediaSource$1(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a stable public HTTP(S) URL; relative Space file paths remain supported.");
5803
+ const BoardMediaSourceSchema$1 = string().max(4096).refine((src) => parseBoardMediaSource$1(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a public HTTP(S) URL; a Space path relative to the workspace root also works.");
5804
5804
  const BoardScriptSchema$1 = object({
5805
- src: srcSchema$1.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module"),
5806
- params: record(string(), json()).default({}),
5807
- targets: record(string().min(1).max(80), BoardIdSchema$1).default({}),
5808
- seed: string().min(1).max(160).optional(),
5805
+ src: srcSchema$1.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module").describe("A .js or .mjs Space file."),
5806
+ params: record(string(), json()).default({}).describe("Values the module reads as params."),
5807
+ targets: record(string().min(1).max(80), BoardIdSchema$1).default({}).describe("Named items the module can reach, as ctx.targets."),
5808
+ seed: string().min(1).max(160).optional().describe("Seed for ctx.random(); defaults to the item id."),
5809
5809
  enabled: boolean().default(true)
5810
- }).strict();
5810
+ }).strict().describe("A JavaScript module attached to the item; `cohub boards schema script` lists its API.");
5811
5811
  const itemEnvelope$1 = {
5812
5812
  script: BoardScriptSchema$1.optional(),
5813
- parent: BoardIdSchema$1.optional(),
5814
- z: finite$1.optional(),
5813
+ parent: BoardIdSchema$1.optional().describe("A frame id; position becomes relative to the frame, and the item moves with it."),
5814
+ z: finite$1.optional().describe("Order among siblings; new items go on top."),
5815
5815
  position: BoardVec2Schema$1.default({
5816
5816
  x: 0,
5817
5817
  y: 0
@@ -5821,7 +5821,7 @@ const itemEnvelope$1 = {
5821
5821
  origin: BoardVec2Schema$1.default({
5822
5822
  x: .5,
5823
5823
  y: .5
5824
- }),
5824
+ }).describe("Rotation and scale origin as fractions of the size."),
5825
5825
  opacity: ratio$1.default(1),
5826
5826
  style: BoardStyleSchema$1.default({}),
5827
5827
  locked: boolean().optional(),
@@ -5854,8 +5854,8 @@ const BoardFrameItemSchema$1 = object({
5854
5854
  type: literal("frame"),
5855
5855
  props: object({
5856
5856
  label: string().max(280).default(""),
5857
- decoration: _enum(["frame", "none"]).optional(),
5858
- clip: boolean().default(true)
5857
+ decoration: _enum(["frame", "none"]).optional().describe("none hides the title, border and background: an invisible group whose tracks move every child."),
5858
+ clip: boolean().default(true).describe("Hide children outside the frame.")
5859
5859
  }).strict().prefault({})
5860
5860
  }).strict();
5861
5861
  const BoardTextItemSchema$1 = object({
@@ -5865,12 +5865,12 @@ const BoardTextItemSchema$1 = object({
5865
5865
  text: string().default(""),
5866
5866
  fontSize: fontSize$1.default(24),
5867
5867
  fontWeight: finite$1.min(100).max(900).default(500),
5868
- font: string().min(1).max(120).default("sans"),
5868
+ font: string().min(1).max(120).default("sans").describe("sans, serif, mono, or a font family name."),
5869
5869
  align: _enum(BOARD_TEXT_ALIGNS$1).default("left"),
5870
5870
  lineHeight: finite$1.min(.5).max(4).default(4 / 3),
5871
- width: finite$1.positive().optional(),
5872
- reveal: ratio$1.default(1),
5873
- caret: boolean().default(false)
5871
+ width: finite$1.positive().optional().describe("Wrap width; the height follows the text."),
5872
+ reveal: ratio$1.default(1).describe("Fraction of the text shown; animate 0 → 1 to type it."),
5873
+ caret: boolean().default(false).describe("Blink a caret while the text is being revealed.")
5874
5874
  }).strict().prefault({})
5875
5875
  }).strict();
5876
5876
  const BoardShapeItemSchema$1 = object({
@@ -5878,7 +5878,7 @@ const BoardShapeItemSchema$1 = object({
5878
5878
  type: literal("shape"),
5879
5879
  props: object({
5880
5880
  geometry: _enum(BOARD_SHAPE_GEOMETRIES$1).default("rectangle"),
5881
- path: string().min(1).max(2e5).optional(),
5881
+ path: string().min(1).max(2e5).optional().describe("SVG path data for path geometry, fitted to the item size."),
5882
5882
  text: string().default(""),
5883
5883
  fontSize: fontSize$1.default(20),
5884
5884
  align: _enum(BOARD_TEXT_ALIGNS$1).default("center")
@@ -5890,12 +5890,12 @@ const BoardShapeItemSchema$1 = object({
5890
5890
  const BoardDrawPointSchema$1 = object({
5891
5891
  x: finite$1,
5892
5892
  y: finite$1,
5893
- p: ratio$1.default(.5)
5893
+ p: ratio$1.default(.5).describe("Pen pressure.")
5894
5894
  }).strict();
5895
5895
  const BoardDrawItemSchema$1 = object({
5896
5896
  ...unsized$1,
5897
5897
  type: literal("draw"),
5898
- props: object({ points: array(BoardDrawPointSchema$1).min(1).max(1e5) }).strict()
5898
+ props: object({ points: array(BoardDrawPointSchema$1).min(1).max(1e5).describe("Stroke points relative to position; the size follows them.") }).strict()
5899
5899
  }).strict();
5900
5900
  const BoardArrowEndSchema$1 = union([BoardVec2Schema$1, object({
5901
5901
  item: BoardIdSchema$1,
@@ -5914,9 +5914,9 @@ const BoardArrowEndSchema$1 = union([BoardVec2Schema$1, object({
5914
5914
  x: ratio$1,
5915
5915
  y: ratio$1
5916
5916
  }).strict()
5917
- ]).default("auto"),
5917
+ ]).default("auto").describe("auto uses the side facing the other end; or a side with an offset, or a point as fractions of the item."),
5918
5918
  port: string().min(1).max(120).optional()
5919
- }).strict()]);
5919
+ }).strict()]).describe("A point relative to position, or { item } to follow that item.");
5920
5920
  const BOARD_ARROW_ROUTES$1 = [
5921
5921
  "straight",
5922
5922
  "curve",
@@ -5929,13 +5929,13 @@ const BoardArrowItemSchema$1 = object({
5929
5929
  start: BoardArrowEndSchema$1,
5930
5930
  end: BoardArrowEndSchema$1,
5931
5931
  route: _enum(BOARD_ARROW_ROUTES$1).default("straight"),
5932
- bend: finite$1.min(-.85).max(.85).default(0),
5933
- waypoints: array(BoardVec2Schema$1).max(64).default([]),
5932
+ bend: finite$1.min(-.85).max(.85).default(0).describe("Sideways curve as a fraction of the length; the sign picks the side."),
5933
+ waypoints: array(BoardVec2Schema$1).max(64).default([]).describe("Points the route passes through."),
5934
5934
  arrowStart: boolean().default(false),
5935
5935
  arrowEnd: boolean().default(true),
5936
5936
  label: string().max(280).default(""),
5937
5937
  fontSize: fontSize$1.default(14),
5938
- relation: string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional()
5938
+ relation: string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional().describe("What the arrow means, as a slug such as triggers or depends-on.")
5939
5939
  }).strict()
5940
5940
  }).strict();
5941
5941
  const fileProps$1 = {
@@ -5951,26 +5951,26 @@ const fileProps$1 = {
5951
5951
  excerpt: string().optional(),
5952
5952
  coverPath: string().optional(),
5953
5953
  coverUrl: string().optional()
5954
- }).strict().optional()
5954
+ }).strict().describe("Cached facts for an instant preview; filled in when the media loads.").optional()
5955
5955
  };
5956
5956
  const BoardMediaOriginSchema$1 = object({
5957
5957
  taskRunId: string().min(1).max(160),
5958
5958
  artifactId: string().min(1).max(240)
5959
- }).strict();
5959
+ }).strict().describe("The generation run and result this media came from.");
5960
5960
  const mediaProps$1 = {
5961
5961
  ...fileProps$1,
5962
5962
  origin: BoardMediaOriginSchema$1.optional()
5963
5963
  };
5964
5964
  const timedFileProps$1 = {
5965
5965
  ...mediaProps$1,
5966
- time: nonNegative$1.optional()
5966
+ time: nonNegative$1.optional().describe("Playback position in milliseconds; animate props.time to play it on a timeline.")
5967
5967
  };
5968
5968
  const BoardCropSchema$1 = object({
5969
5969
  x: ratio$1,
5970
5970
  y: ratio$1,
5971
5971
  w: ratio$1,
5972
5972
  h: ratio$1
5973
- }).strict();
5973
+ }).strict().describe("Region shown, as fractions of the image. Without it the whole image fits inside size.");
5974
5974
  const BoardImageItemSchema$1 = object({
5975
5975
  ...boxed$1(640, 360),
5976
5976
  type: literal("image"),
@@ -6050,8 +6050,8 @@ const BoardTaskItemSchema$1 = object({
6050
6050
  ...boxed$1(420, 240),
6051
6051
  type: literal("task"),
6052
6052
  props: object({
6053
- taskRunId: string().min(1).max(160),
6054
- snapshot: BoardTaskSnapshotSchema$1.describe("Maintained by the server for tasks in the Board's Space; writers may omit it.")
6053
+ taskRunId: string().min(1).max(160).describe("The generation run shown; the server keeps the card's status and results up to date."),
6054
+ snapshot: BoardTaskSnapshotSchema$1.describe("Maintained by the server; omit it.")
6055
6055
  }).strict()
6056
6056
  }).strict();
6057
6057
  const BOARD_EFFECT_KINDS$1 = [
@@ -6074,8 +6074,8 @@ const BoardEffectItemSchema$1 = object({
6074
6074
  spread: finite$1.min(0).max(360).default(360),
6075
6075
  gravity: finite$1.default(0),
6076
6076
  intensity: nonNegative$1.max(10).default(1),
6077
- follow: BoardIdSchema$1.optional(),
6078
- seed: string().min(1).max(160).optional()
6077
+ follow: BoardIdSchema$1.optional().describe("trail: the item to follow."),
6078
+ seed: string().min(1).max(160).optional().describe("Makes procedural motion repeatable.")
6079
6079
  }).strict()
6080
6080
  }).strict();
6081
6081
  const BoardCustomItemSchema$1 = object({
@@ -6177,21 +6177,21 @@ function isBoardEase$1(value) {
6177
6177
  }
6178
6178
  const BoardEaseSchema$1 = string().max(120).refine(isBoardEase$1, "expected a CSS easing: linear, ease-in-out, cubic-bezier(…) or steps(…)");
6179
6179
  const BoardKeyframeSchema$1 = object({
6180
- at: nonNegative$1,
6180
+ at: nonNegative$1.describe("Time in milliseconds."),
6181
6181
  value: unknown(),
6182
- ease: BoardEaseSchema$1.optional()
6182
+ ease: BoardEaseSchema$1.optional().describe("Easing into this keyframe: a CSS keyword, cubic-bezier() or steps(); defaults to linear.")
6183
6183
  }).strict();
6184
6184
  const BoardTrackSchema$1 = object({
6185
- target: string().min(1).max(160),
6186
- property: string().min(1).max(200),
6187
- keyframes: array(BoardKeyframeSchema$1).min(1).max(1e5),
6188
- composite: _enum(["replace", "add"]).default("replace"),
6185
+ target: string().min(1).max(160).describe("An item id, camera, or another animation id (its time property nests that timeline)."),
6186
+ property: string().min(1).max(200).describe("JSON path in the target, e.g. position, opacity, style.fill, props.text."),
6187
+ keyframes: array(BoardKeyframeSchema$1).min(1).max(1e5).describe("Ordered by at. The value holds before the first and after the last keyframe."),
6188
+ composite: _enum(["replace", "add"]).default("replace").describe("replace: the latest-started track wins. add: positions and rotations add; scale and opacity multiply."),
6189
6189
  interpolation: _enum([
6190
6190
  "auto",
6191
6191
  "step",
6192
6192
  "spline"
6193
- ]).default("auto"),
6194
- orient: boolean().optional()
6193
+ ]).default("auto").describe("auto blends numbers, vectors and colors and steps other values; spline smooths position paths."),
6194
+ orient: boolean().optional().describe("Rotate the item along its path.")
6195
6195
  }).strict().superRefine((track, context) => {
6196
6196
  for (let index = 1; index < track.keyframes.length; index += 1) if ((track.keyframes[index]?.at ?? 0) < (track.keyframes[index - 1]?.at ?? 0)) context.addIssue({
6197
6197
  code: "custom",
@@ -6206,20 +6206,20 @@ const BoardTrackSchema$1 = object({
6206
6206
  const BoardMarkerSchema$1 = object({
6207
6207
  at: nonNegative$1,
6208
6208
  label: string().max(280).optional(),
6209
- pause: boolean().optional()
6209
+ pause: boolean().optional().describe("Hold shared playback here until next.")
6210
6210
  }).strict();
6211
6211
  const BoardAnimationHeaderSchema$1 = object({
6212
6212
  name: string().max(280).optional(),
6213
- duration: finite$1.positive().max(864e5),
6213
+ duration: finite$1.positive().max(864e5).describe("Milliseconds; a new animation without it ends at its last keyframe."),
6214
6214
  play: _enum([
6215
6215
  "manual",
6216
6216
  "auto",
6217
6217
  "always"
6218
- ]).default("manual"),
6218
+ ]).default("manual").describe("manual: only shared playback starts it. auto: plays when the Board opens. always: loops continuously."),
6219
6219
  delay: nonNegative$1.default(0),
6220
6220
  loop: boolean().default(false),
6221
- end: _enum(["hold", "reset"]).default("hold"),
6222
- markers: array(BoardMarkerSchema$1).max(1e4).default([])
6221
+ end: _enum(["hold", "reset"]).default("hold").describe("hold keeps the last frame; reset returns to the start."),
6222
+ markers: array(BoardMarkerSchema$1).max(1e4).default([]).describe("Labeled points on this timeline; a nested animation's markers do not carry over.")
6223
6223
  }).strict();
6224
6224
  BoardAnimationHeaderSchema$1.extend({ tracks: record(BoardKeySchema$1, BoardTrackSchema$1).default({}) }).strict();
6225
6225
  const BOARD_ENTER_PRESETS$1 = [
@@ -6237,7 +6237,7 @@ const BoardBackgroundSchema$1 = object({
6237
6237
  "image"
6238
6238
  ]).default("dots"),
6239
6239
  color: BoardColorSchema$1.optional(),
6240
- imageUrl: string().url().max(4096).optional(),
6240
+ imageUrl: string().url().max(4096).optional().describe("Background image for kind image."),
6241
6241
  fit: _enum([
6242
6242
  "cover",
6243
6243
  "contain",
@@ -6249,13 +6249,13 @@ const BoardSettingsSchema$1 = object({
6249
6249
  title: string().min(1).max(255).optional(),
6250
6250
  background: BoardBackgroundSchema$1.default({ kind: "dots" }),
6251
6251
  grid: object({
6252
- visible: boolean().default(false),
6252
+ visible: boolean().default(false).describe("Show the grid overlay."),
6253
6253
  size: finite$1.min(4).max(512).default(24)
6254
6254
  }).strict().optional(),
6255
6255
  enter: object({
6256
6256
  preset: _enum(BOARD_ENTER_PRESETS$1),
6257
6257
  duration: finite$1.positive().max(1e4).optional()
6258
- }).strict().optional()
6258
+ }).strict().optional().describe("Motion for items as they are added.")
6259
6259
  }).strict();
6260
6260
  const patchObject$1 = record(string(), unknown());
6261
6261
  const BoardPatchSchema$1 = object({
@@ -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,1197 +27908,306 @@ 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
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
27608
28080
  ].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;
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 "";
27784
- }
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();
28207
+ function replaceStroke(parts, next) {
28208
+ if (parts.stroke) {
28209
+ parts.root.removeChild(parts.stroke);
28210
+ parts.stroke.destroy();
28601
28211
  }
28602
28212
  parts.stroke = next;
28603
28213
  parts.root.addChild(next);
@@ -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);
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;
29444
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,
@@ -29974,10 +30341,10 @@ const colorString = string().max(120).refine(isBoardColorString, "expected a pal
29974
30341
  const BoardColorSchema = union([colorString, object({
29975
30342
  light: colorString,
29976
30343
  dark: colorString
29977
- }).strict()]);
30344
+ }).strict()]).describe("Palette token (follows the theme), CSS color, or { light, dark }.");
29978
30345
  const BoardStyleSchema = object({
29979
30346
  fill: BoardColorSchema.optional(),
29980
- fillOpacity: ratio.optional(),
30347
+ fillOpacity: ratio.optional().describe("Fill opacity, independent of the item's opacity."),
29981
30348
  stroke: BoardColorSchema.optional(),
29982
30349
  strokeWidth: finite.min(0).max(64).optional(),
29983
30350
  dash: _enum([
@@ -29985,8 +30352,8 @@ const BoardStyleSchema = object({
29985
30352
  "dashed",
29986
30353
  "dotted"
29987
30354
  ]).optional(),
29988
- radius: nonNegative.optional(),
29989
- trim: ratio.optional()
30355
+ radius: nonNegative.optional().describe("Corner radius in board units."),
30356
+ trim: ratio.optional().describe("Fraction of the outline drawn; animate 0 → 1 to draw it in.")
29990
30357
  }).strict();
29991
30358
  function isSafeBoardSourcePath(value) {
29992
30359
  return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !/^[a-z][a-z\d+.-]*:/i.test(value.trimStart()) && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
@@ -30002,18 +30369,18 @@ function parseBoardMediaSource(src) {
30002
30369
  value: src
30003
30370
  } : null;
30004
30371
  }
30005
- const BoardMediaSourceSchema = string().max(4096).refine((src) => parseBoardMediaSource(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a stable public HTTP(S) URL; relative Space file paths remain supported.");
30372
+ const BoardMediaSourceSchema = string().max(4096).refine((src) => parseBoardMediaSource(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a public HTTP(S) URL; a Space path relative to the workspace root also works.");
30006
30373
  const BoardScriptSchema = object({
30007
- src: srcSchema.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module"),
30008
- params: record(string(), json()).default({}),
30009
- targets: record(string().min(1).max(80), BoardIdSchema).default({}),
30010
- seed: string().min(1).max(160).optional(),
30374
+ src: srcSchema.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module").describe("A .js or .mjs Space file."),
30375
+ params: record(string(), json()).default({}).describe("Values the module reads as params."),
30376
+ targets: record(string().min(1).max(80), BoardIdSchema).default({}).describe("Named items the module can reach, as ctx.targets."),
30377
+ seed: string().min(1).max(160).optional().describe("Seed for ctx.random(); defaults to the item id."),
30011
30378
  enabled: boolean().default(true)
30012
- }).strict();
30379
+ }).strict().describe("A JavaScript module attached to the item; `cohub boards schema script` lists its API.");
30013
30380
  const itemEnvelope = {
30014
30381
  script: BoardScriptSchema.optional(),
30015
- parent: BoardIdSchema.optional(),
30016
- z: finite.optional(),
30382
+ parent: BoardIdSchema.optional().describe("A frame id; position becomes relative to the frame, and the item moves with it."),
30383
+ z: finite.optional().describe("Order among siblings; new items go on top."),
30017
30384
  position: BoardVec2Schema.default({
30018
30385
  x: 0,
30019
30386
  y: 0
@@ -30023,7 +30390,7 @@ const itemEnvelope = {
30023
30390
  origin: BoardVec2Schema.default({
30024
30391
  x: .5,
30025
30392
  y: .5
30026
- }),
30393
+ }).describe("Rotation and scale origin as fractions of the size."),
30027
30394
  opacity: ratio.default(1),
30028
30395
  style: BoardStyleSchema.default({}),
30029
30396
  locked: boolean().optional(),
@@ -30056,8 +30423,8 @@ object({
30056
30423
  type: literal("frame"),
30057
30424
  props: object({
30058
30425
  label: string().max(280).default(""),
30059
- decoration: _enum(["frame", "none"]).optional(),
30060
- clip: boolean().default(true)
30426
+ decoration: _enum(["frame", "none"]).optional().describe("none hides the title, border and background: an invisible group whose tracks move every child."),
30427
+ clip: boolean().default(true).describe("Hide children outside the frame.")
30061
30428
  }).strict().prefault({})
30062
30429
  }).strict();
30063
30430
  object({
@@ -30067,12 +30434,12 @@ object({
30067
30434
  text: string().default(""),
30068
30435
  fontSize: fontSize.default(24),
30069
30436
  fontWeight: finite.min(100).max(900).default(500),
30070
- font: string().min(1).max(120).default("sans"),
30437
+ font: string().min(1).max(120).default("sans").describe("sans, serif, mono, or a font family name."),
30071
30438
  align: _enum(BOARD_TEXT_ALIGNS).default("left"),
30072
30439
  lineHeight: finite.min(.5).max(4).default(4 / 3),
30073
- width: finite.positive().optional(),
30074
- reveal: ratio.default(1),
30075
- caret: boolean().default(false)
30440
+ width: finite.positive().optional().describe("Wrap width; the height follows the text."),
30441
+ reveal: ratio.default(1).describe("Fraction of the text shown; animate 0 → 1 to type it."),
30442
+ caret: boolean().default(false).describe("Blink a caret while the text is being revealed.")
30076
30443
  }).strict().prefault({})
30077
30444
  }).strict();
30078
30445
  object({
@@ -30080,7 +30447,7 @@ object({
30080
30447
  type: literal("shape"),
30081
30448
  props: object({
30082
30449
  geometry: _enum(BOARD_SHAPE_GEOMETRIES).default("rectangle"),
30083
- path: string().min(1).max(2e5).optional(),
30450
+ path: string().min(1).max(2e5).optional().describe("SVG path data for path geometry, fitted to the item size."),
30084
30451
  text: string().default(""),
30085
30452
  fontSize: fontSize.default(20),
30086
30453
  align: _enum(BOARD_TEXT_ALIGNS).default("center")
@@ -30092,12 +30459,12 @@ object({
30092
30459
  const BoardDrawPointSchema = object({
30093
30460
  x: finite,
30094
30461
  y: finite,
30095
- p: ratio.default(.5)
30462
+ p: ratio.default(.5).describe("Pen pressure.")
30096
30463
  }).strict();
30097
30464
  object({
30098
30465
  ...unsized,
30099
30466
  type: literal("draw"),
30100
- props: object({ points: array(BoardDrawPointSchema).min(1).max(1e5) }).strict()
30467
+ props: object({ points: array(BoardDrawPointSchema).min(1).max(1e5).describe("Stroke points relative to position; the size follows them.") }).strict()
30101
30468
  }).strict();
30102
30469
  const BoardArrowEndSchema = union([BoardVec2Schema, object({
30103
30470
  item: BoardIdSchema,
@@ -30116,9 +30483,9 @@ const BoardArrowEndSchema = union([BoardVec2Schema, object({
30116
30483
  x: ratio,
30117
30484
  y: ratio
30118
30485
  }).strict()
30119
- ]).default("auto"),
30486
+ ]).default("auto").describe("auto uses the side facing the other end; or a side with an offset, or a point as fractions of the item."),
30120
30487
  port: string().min(1).max(120).optional()
30121
- }).strict()]);
30488
+ }).strict()]).describe("A point relative to position, or { item } to follow that item.");
30122
30489
  const BOARD_ARROW_ROUTES = [
30123
30490
  "straight",
30124
30491
  "curve",
@@ -30131,13 +30498,13 @@ object({
30131
30498
  start: BoardArrowEndSchema,
30132
30499
  end: BoardArrowEndSchema,
30133
30500
  route: _enum(BOARD_ARROW_ROUTES).default("straight"),
30134
- bend: finite.min(-.85).max(.85).default(0),
30135
- waypoints: array(BoardVec2Schema).max(64).default([]),
30501
+ bend: finite.min(-.85).max(.85).default(0).describe("Sideways curve as a fraction of the length; the sign picks the side."),
30502
+ waypoints: array(BoardVec2Schema).max(64).default([]).describe("Points the route passes through."),
30136
30503
  arrowStart: boolean().default(false),
30137
30504
  arrowEnd: boolean().default(true),
30138
30505
  label: string().max(280).default(""),
30139
30506
  fontSize: fontSize.default(14),
30140
- relation: string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional()
30507
+ relation: string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional().describe("What the arrow means, as a slug such as triggers or depends-on.")
30141
30508
  }).strict()
30142
30509
  }).strict();
30143
30510
  const fileProps = {
@@ -30153,26 +30520,26 @@ const fileProps = {
30153
30520
  excerpt: string().optional(),
30154
30521
  coverPath: string().optional(),
30155
30522
  coverUrl: string().optional()
30156
- }).strict().optional()
30523
+ }).strict().describe("Cached facts for an instant preview; filled in when the media loads.").optional()
30157
30524
  };
30158
30525
  const BoardMediaOriginSchema = object({
30159
30526
  taskRunId: string().min(1).max(160),
30160
30527
  artifactId: string().min(1).max(240)
30161
- }).strict();
30528
+ }).strict().describe("The generation run and result this media came from.");
30162
30529
  const mediaProps = {
30163
30530
  ...fileProps,
30164
30531
  origin: BoardMediaOriginSchema.optional()
30165
30532
  };
30166
30533
  const timedFileProps = {
30167
30534
  ...mediaProps,
30168
- time: nonNegative.optional()
30535
+ time: nonNegative.optional().describe("Playback position in milliseconds; animate props.time to play it on a timeline.")
30169
30536
  };
30170
30537
  const BoardCropSchema = object({
30171
30538
  x: ratio,
30172
30539
  y: ratio,
30173
30540
  w: ratio,
30174
30541
  h: ratio
30175
- }).strict();
30542
+ }).strict().describe("Region shown, as fractions of the image. Without it the whole image fits inside size.");
30176
30543
  object({
30177
30544
  ...boxed(640, 360),
30178
30545
  type: literal("image"),
@@ -30252,8 +30619,8 @@ object({
30252
30619
  ...boxed(420, 240),
30253
30620
  type: literal("task"),
30254
30621
  props: object({
30255
- taskRunId: string().min(1).max(160),
30256
- snapshot: BoardTaskSnapshotSchema.describe("Maintained by the server for tasks in the Board's Space; writers may omit it.")
30622
+ taskRunId: string().min(1).max(160).describe("The generation run shown; the server keeps the card's status and results up to date."),
30623
+ snapshot: BoardTaskSnapshotSchema.describe("Maintained by the server; omit it.")
30257
30624
  }).strict()
30258
30625
  }).strict();
30259
30626
  const BOARD_EFFECT_KINDS = [
@@ -30276,8 +30643,8 @@ object({
30276
30643
  spread: finite.min(0).max(360).default(360),
30277
30644
  gravity: finite.default(0),
30278
30645
  intensity: nonNegative.max(10).default(1),
30279
- follow: BoardIdSchema.optional(),
30280
- seed: string().min(1).max(160).optional()
30646
+ follow: BoardIdSchema.optional().describe("trail: the item to follow."),
30647
+ seed: string().min(1).max(160).optional().describe("Makes procedural motion repeatable.")
30281
30648
  }).strict()
30282
30649
  }).strict();
30283
30650
  object({
@@ -30317,21 +30684,21 @@ function isBoardEase(value) {
30317
30684
  }
30318
30685
  const BoardEaseSchema = string().max(120).refine(isBoardEase, "expected a CSS easing: linear, ease-in-out, cubic-bezier(…) or steps(…)");
30319
30686
  const BoardKeyframeSchema = object({
30320
- at: nonNegative,
30687
+ at: nonNegative.describe("Time in milliseconds."),
30321
30688
  value: unknown(),
30322
- ease: BoardEaseSchema.optional()
30689
+ ease: BoardEaseSchema.optional().describe("Easing into this keyframe: a CSS keyword, cubic-bezier() or steps(); defaults to linear.")
30323
30690
  }).strict();
30324
30691
  const BoardTrackSchema = object({
30325
- target: string().min(1).max(160),
30326
- property: string().min(1).max(200),
30327
- keyframes: array(BoardKeyframeSchema).min(1).max(1e5),
30328
- composite: _enum(["replace", "add"]).default("replace"),
30692
+ target: string().min(1).max(160).describe("An item id, camera, or another animation id (its time property nests that timeline)."),
30693
+ property: string().min(1).max(200).describe("JSON path in the target, e.g. position, opacity, style.fill, props.text."),
30694
+ keyframes: array(BoardKeyframeSchema).min(1).max(1e5).describe("Ordered by at. The value holds before the first and after the last keyframe."),
30695
+ composite: _enum(["replace", "add"]).default("replace").describe("replace: the latest-started track wins. add: positions and rotations add; scale and opacity multiply."),
30329
30696
  interpolation: _enum([
30330
30697
  "auto",
30331
30698
  "step",
30332
30699
  "spline"
30333
- ]).default("auto"),
30334
- orient: boolean().optional()
30700
+ ]).default("auto").describe("auto blends numbers, vectors and colors and steps other values; spline smooths position paths."),
30701
+ orient: boolean().optional().describe("Rotate the item along its path.")
30335
30702
  }).strict().superRefine((track, context) => {
30336
30703
  for (let index = 1; index < track.keyframes.length; index += 1) if ((track.keyframes[index]?.at ?? 0) < (track.keyframes[index - 1]?.at ?? 0)) context.addIssue({
30337
30704
  code: "custom",
@@ -30346,20 +30713,20 @@ const BoardTrackSchema = object({
30346
30713
  const BoardMarkerSchema = object({
30347
30714
  at: nonNegative,
30348
30715
  label: string().max(280).optional(),
30349
- pause: boolean().optional()
30716
+ pause: boolean().optional().describe("Hold shared playback here until next.")
30350
30717
  }).strict();
30351
30718
  object({
30352
30719
  name: string().max(280).optional(),
30353
- duration: finite.positive().max(864e5),
30720
+ duration: finite.positive().max(864e5).describe("Milliseconds; a new animation without it ends at its last keyframe."),
30354
30721
  play: _enum([
30355
30722
  "manual",
30356
30723
  "auto",
30357
30724
  "always"
30358
- ]).default("manual"),
30725
+ ]).default("manual").describe("manual: only shared playback starts it. auto: plays when the Board opens. always: loops continuously."),
30359
30726
  delay: nonNegative.default(0),
30360
30727
  loop: boolean().default(false),
30361
- end: _enum(["hold", "reset"]).default("hold"),
30362
- markers: array(BoardMarkerSchema).max(1e4).default([])
30728
+ end: _enum(["hold", "reset"]).default("hold").describe("hold keeps the last frame; reset returns to the start."),
30729
+ markers: array(BoardMarkerSchema).max(1e4).default([]).describe("Labeled points on this timeline; a nested animation's markers do not carry over.")
30363
30730
  }).strict().extend({ tracks: record(BoardKeySchema, BoardTrackSchema).default({}) }).strict();
30364
30731
  const BOARD_ENTER_PRESETS = [
30365
30732
  "fade-in",
@@ -30376,7 +30743,7 @@ const BoardBackgroundSchema = object({
30376
30743
  "image"
30377
30744
  ]).default("dots"),
30378
30745
  color: BoardColorSchema.optional(),
30379
- imageUrl: string().url().max(4096).optional(),
30746
+ imageUrl: string().url().max(4096).optional().describe("Background image for kind image."),
30380
30747
  fit: _enum([
30381
30748
  "cover",
30382
30749
  "contain",
@@ -30388,13 +30755,13 @@ object({
30388
30755
  title: string().min(1).max(255).optional(),
30389
30756
  background: BoardBackgroundSchema.default({ kind: "dots" }),
30390
30757
  grid: object({
30391
- visible: boolean().default(false),
30758
+ visible: boolean().default(false).describe("Show the grid overlay."),
30392
30759
  size: finite.min(4).max(512).default(24)
30393
30760
  }).strict().optional(),
30394
30761
  enter: object({
30395
30762
  preset: _enum(BOARD_ENTER_PRESETS),
30396
30763
  duration: finite.positive().max(1e4).optional()
30397
- }).strict().optional()
30764
+ }).strict().optional().describe("Motion for items as they are added.")
30398
30765
  }).strict();
30399
30766
  const patchObject = record(string(), unknown());
30400
30767
  object({
@@ -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,