@neta-art/cohub 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +33 -2
  2. package/dist/{board.d.ts → board/animation.d.ts} +4 -2
  3. package/dist/{chunks/board.js → board/animation.js} +5 -44
  4. package/dist/board/codec.d.ts +25 -0
  5. package/dist/board/codec.js +252 -0
  6. package/dist/board/core/bindings.d.ts +45 -0
  7. package/dist/board/core/bindings.js +162 -0
  8. package/dist/board/core/draw-geometry.d.ts +31 -0
  9. package/dist/board/core/draw-geometry.js +183 -0
  10. package/dist/board/core/export-assets.d.ts +14 -0
  11. package/dist/board/core/export-assets.js +22 -0
  12. package/dist/board/core/export-plan.d.ts +84 -0
  13. package/dist/board/core/export-plan.js +128 -0
  14. package/dist/board/core/file-preview.d.ts +176 -0
  15. package/dist/board/core/file-preview.js +271 -0
  16. package/dist/board/core/palette.d.ts +42 -0
  17. package/dist/board/core/palette.js +138 -0
  18. package/dist/board/core/shape-definition.d.ts +35 -0
  19. package/dist/board/core/shape-definition.js +64 -0
  20. package/dist/board/core/shape-types.d.ts +144 -0
  21. package/dist/board/core/shape-types.js +27 -0
  22. package/dist/board/core/text-metrics.d.ts +18 -0
  23. package/dist/board/core/text-metrics.js +43 -0
  24. package/dist/board/export/index.d.ts +56 -0
  25. package/dist/board/export/index.js +86 -0
  26. package/dist/board/export/scene.d.ts +35 -0
  27. package/dist/board/export/scene.js +69 -0
  28. package/dist/board/geometry.d.ts +131 -0
  29. package/dist/board/geometry.js +468 -0
  30. package/dist/board/headless/index.d.ts +69 -0
  31. package/dist/board/headless/index.js +150 -0
  32. package/dist/board/image-key.d.ts +19 -0
  33. package/dist/board/image-key.js +34 -0
  34. package/dist/board/index.d.ts +16 -0
  35. package/dist/board/index.js +16 -0
  36. package/dist/board/render/index.d.ts +6 -0
  37. package/dist/board/render/index.js +6 -0
  38. package/dist/board/render/palette.d.ts +5 -0
  39. package/dist/board/render/palette.js +30 -0
  40. package/dist/board/render/renderers/arrow-card-renderer.d.ts +5 -0
  41. package/dist/board/render/renderers/arrow-card-renderer.js +157 -0
  42. package/dist/board/render/renderers/base-card-renderer.d.ts +46 -0
  43. package/dist/board/render/renderers/base-card-renderer.js +153 -0
  44. package/dist/board/render/renderers/board-renderer-registry.d.ts +85 -0
  45. package/dist/board/render/renderers/board-renderer-registry.js +43 -0
  46. package/dist/board/render/renderers/draw-card-renderer.d.ts +5 -0
  47. package/dist/board/render/renderers/draw-card-renderer.js +82 -0
  48. package/dist/board/render/renderers/far-plate.d.ts +39 -0
  49. package/dist/board/render/renderers/far-plate.js +92 -0
  50. package/dist/board/render/renderers/file-card-renderer.d.ts +33 -0
  51. package/dist/board/render/renderers/file-card-renderer.js +371 -0
  52. package/dist/board/render/renderers/frame-card-renderer.d.ts +5 -0
  53. package/dist/board/render/renderers/frame-card-renderer.js +99 -0
  54. package/dist/board/render/renderers/geo-card-renderer.d.ts +5 -0
  55. package/dist/board/render/renderers/geo-card-renderer.js +128 -0
  56. package/dist/board/render/renderers/image-card-renderer.d.ts +8 -0
  57. package/dist/board/render/renderers/image-card-renderer.js +138 -0
  58. package/dist/board/render/renderers/note-card-renderer.d.ts +5 -0
  59. package/dist/board/render/renderers/note-card-renderer.js +112 -0
  60. package/dist/board/render/renderers/text-card-renderer.d.ts +7 -0
  61. package/dist/board/render/renderers/text-card-renderer.js +94 -0
  62. package/dist/board/render/renderers/unknown-card-renderer.d.ts +5 -0
  63. package/dist/board/render/renderers/unknown-card-renderer.js +102 -0
  64. package/dist/board/render/renderers/video-card-renderer.d.ts +7 -0
  65. package/dist/board/render/renderers/video-card-renderer.js +146 -0
  66. package/dist/board/render/text-measurement.d.ts +30 -0
  67. package/dist/board/render/text-measurement.js +57 -0
  68. package/dist/board/render/text-resolution.d.ts +16 -0
  69. package/dist/board/render/text-resolution.js +43 -0
  70. package/dist/board/render/themes/board-theme-registry.d.ts +20 -0
  71. package/dist/board/render/themes/board-theme-registry.js +13 -0
  72. package/dist/board/render/themes/clean-theme.d.ts +5 -0
  73. package/dist/board/render/themes/clean-theme.js +102 -0
  74. package/dist/chunks/http.d.ts +10 -40
  75. package/dist/chunks/http.js +26 -4
  76. package/dist/chunks/transport.js +4 -2
  77. package/dist/chunks/websocket.d.ts +832 -21
  78. package/dist/chunks/websocket.js +19 -2
  79. package/dist/http.d.ts +2 -3
  80. package/dist/index.d.ts +100 -5
  81. package/dist/index.js +391 -1
  82. package/dist/protocol/dist/board-constants.d.ts +23 -0
  83. package/dist/protocol/dist/board-constants.js +60 -0
  84. package/dist/protocol/dist/board-document.d.ts +1124 -0
  85. package/dist/protocol/dist/board-document.js +331 -0
  86. package/dist/protocol/dist/board.d.ts +186 -0
  87. package/dist/protocol/dist/board.js +207 -0
  88. package/dist/protocol/dist/index.d.ts +5 -0
  89. package/dist/protocol/dist/index.js +5 -0
  90. package/dist/protocol/dist/provenance.js +12 -0
  91. package/dist/protocol/dist/realtime/board-awareness.d.ts +1 -0
  92. package/dist/protocol/dist/realtime/board-awareness.js +117 -0
  93. package/dist/protocol/dist/realtime/types.d.ts +2 -0
  94. package/package.json +39 -4
  95. package/dist/board.js +0 -2
  96. package/dist/chunks/board.d.ts +0 -2589
@@ -0,0 +1,99 @@
1
+ import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
+ import { pickBoardColor } from "../../core/palette.js";
3
+ import { syncTextResolution } from "../text-resolution.js";
4
+ import { drawFarPlate } from "./far-plate.js";
5
+ import { createLabel, positionShell } from "./base-card-renderer.js";
6
+ import { Container, Graphics } from "pixi.js";
7
+ //#region src/board/render/renderers/frame-card-renderer.ts
8
+ const partsByContainer = /* @__PURE__ */ new WeakMap();
9
+ function sync(container, item, context) {
10
+ const parts = partsByContainer.get(container);
11
+ if (!parts || item.type !== "frame") return;
12
+ const { frame, label, color, locked } = item;
13
+ positionShell(parts.root, item);
14
+ syncTextResolution(parts.label, parts, context.zoom);
15
+ const selected = context.selectedIds.has(item.id);
16
+ const hovered = context.hoveredId === item.id;
17
+ const palette = pickBoardColor(context.colors, color, context.colorScheme);
18
+ const stroke = selected ? context.palette.brand : palette.stroke;
19
+ const alpha = selected ? 1 : hovered ? .85 : .55;
20
+ const visualSig = [
21
+ frame.width,
22
+ frame.height,
23
+ selected,
24
+ hovered,
25
+ palette.fill,
26
+ stroke
27
+ ].join("|");
28
+ if (visualSig !== parts.visualSig) {
29
+ parts.visualSig = visualSig;
30
+ parts.box.clear();
31
+ parts.box.roundRect(0, 0, frame.width, frame.height, 4).fill({
32
+ color: palette.fill,
33
+ alpha: .04
34
+ });
35
+ parts.box.roundRect(0, 0, frame.width, frame.height, 4).stroke({
36
+ color: stroke,
37
+ width: selected ? 2 : 1.5,
38
+ alpha
39
+ });
40
+ }
41
+ const title = locked ? `🔒 ${label || "Frame"}` : label || "Frame";
42
+ const textSig = [title, palette.stroke].join("|");
43
+ if (textSig !== parts.textSig) {
44
+ parts.textSig = textSig;
45
+ parts.label.text = title;
46
+ parts.label.style.fill = palette.stroke;
47
+ }
48
+ parts.label.position.set(4, -18);
49
+ }
50
+ const frameCardRenderer = {
51
+ id: "frame-card",
52
+ canRender: (item) => item.type === "frame",
53
+ create: (item, context) => {
54
+ const root = new Container();
55
+ const box = new Graphics();
56
+ const label = createLabel("", {
57
+ fill: context.palette.muted,
58
+ fontFamily: BOARD_FONT_STACK,
59
+ fontSize: 12,
60
+ fontWeight: "600"
61
+ });
62
+ root.addChild(box, label);
63
+ partsByContainer.set(root, {
64
+ root,
65
+ box,
66
+ label,
67
+ visualSig: "",
68
+ textSig: "",
69
+ resolution: label.resolution
70
+ });
71
+ sync(root, item, context);
72
+ return root;
73
+ },
74
+ update: (container, item, context) => {
75
+ sync(container, item, context);
76
+ },
77
+ /**
78
+ * Far LOD: a frame is a background region, so it draws as a faint wash with no
79
+ * label.
80
+ *
81
+ * Batching it is what keeps it *behind* its contents. As a live container it
82
+ * would be drawn above the whole far layer no matter where the document places
83
+ * it, so a frame would hide every card inside it.
84
+ */
85
+ renderFar: (graphics, item, context) => {
86
+ if (item.type !== "frame") return;
87
+ const palette = pickBoardColor(context.colors, item.color, context.colorScheme);
88
+ drawFarPlate(graphics, item.frame, {
89
+ fill: palette.fill,
90
+ fillAlpha: .07
91
+ });
92
+ },
93
+ destroy: (container) => {
94
+ partsByContainer.delete(container);
95
+ container.destroy({ children: true });
96
+ }
97
+ };
98
+ //#endregion
99
+ export { frameCardRenderer };
@@ -0,0 +1,5 @@
1
+ import { BoardCardRenderer } from "./board-renderer-registry.js";
2
+ //#region src/board/render/renderers/geo-card-renderer.d.ts
3
+ declare const geoCardRenderer: BoardCardRenderer;
4
+ //#endregion
5
+ export { geoCardRenderer };
@@ -0,0 +1,128 @@
1
+ import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
+ import { pickBoardColor } from "../../core/palette.js";
3
+ import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
4
+ import { drawFarPlate } from "./far-plate.js";
5
+ import { positionShell } from "./base-card-renderer.js";
6
+ import { Container, Graphics, Text } from "pixi.js";
7
+ //#region src/board/render/renderers/geo-card-renderer.ts
8
+ const RADIUS = 8;
9
+ const LABEL_PADDING = 8;
10
+ const partsByContainer = /* @__PURE__ */ new WeakMap();
11
+ function traceOutline(graphics, geo, width, height) {
12
+ switch (geo) {
13
+ case "ellipse":
14
+ graphics.ellipse(width / 2, height / 2, width / 2, height / 2);
15
+ return;
16
+ case "diamond":
17
+ graphics.moveTo(width / 2, 0).lineTo(width, height / 2).lineTo(width / 2, height).lineTo(0, height / 2).closePath();
18
+ return;
19
+ case "triangle":
20
+ graphics.moveTo(width / 2, 0).lineTo(width, height).lineTo(0, height).closePath();
21
+ return;
22
+ case "rounded":
23
+ graphics.roundRect(0, 0, width, height, RADIUS);
24
+ return;
25
+ default: graphics.rect(0, 0, width, height);
26
+ }
27
+ }
28
+ function sync(container, item, context) {
29
+ const parts = partsByContainer.get(container);
30
+ if (!parts) return;
31
+ positionShell(parts.root, item);
32
+ const { width, height } = item.frame;
33
+ const selected = context.selectedIds.has(item.id);
34
+ const hovered = context.hoveredId === item.id;
35
+ const color = pickBoardColor(context.colors, item.color, context.colorScheme);
36
+ syncTextResolution(parts.label, parts, context.zoom);
37
+ const resizing = context.resizingIds.has(item.id);
38
+ const visualSig = [
39
+ width,
40
+ height,
41
+ selected,
42
+ hovered,
43
+ item.geo,
44
+ item.fillOpacity,
45
+ color.fill,
46
+ color.stroke,
47
+ context.palette.muted
48
+ ].join("|");
49
+ if (visualSig !== parts.visualSig) {
50
+ parts.visualSig = visualSig;
51
+ parts.shape.clear();
52
+ traceOutline(parts.shape, item.geo, width, height);
53
+ if (item.fillOpacity > 0) parts.shape.fill({
54
+ color: color.fill,
55
+ alpha: item.fillOpacity
56
+ });
57
+ traceOutline(parts.shape, item.geo, width, height);
58
+ parts.shape.stroke({
59
+ color: selected ? color.stroke : hovered ? context.palette.muted : color.stroke,
60
+ width: selected ? 2.5 : 1.75,
61
+ alpha: selected ? 1 : hovered ? .95 : .85
62
+ });
63
+ }
64
+ const textSig = [item.text, color.label].join("|");
65
+ if (textSig !== parts.textSig) {
66
+ parts.textSig = textSig;
67
+ parts.label.text = item.text;
68
+ parts.label.visible = item.text.length > 0;
69
+ parts.label.style.fill = color.label;
70
+ }
71
+ syncTextWrapWidth(parts.label, parts, Math.max(1, width - LABEL_PADDING * 2), resizing);
72
+ parts.label.position.set(width / 2, height / 2);
73
+ }
74
+ const geoCardRenderer = {
75
+ id: "geo-card",
76
+ canRender: (item) => item.type === "geo",
77
+ create: (item, context) => {
78
+ const root = new Container();
79
+ const shape = new Graphics();
80
+ const resolution = textResolutionForZoom(context.zoom);
81
+ const label = new Text({
82
+ text: "",
83
+ style: {
84
+ fill: 16777215,
85
+ fontFamily: BOARD_FONT_STACK,
86
+ fontSize: 14,
87
+ fontWeight: "500",
88
+ align: "center",
89
+ wordWrap: true,
90
+ lineHeight: 20
91
+ },
92
+ resolution,
93
+ roundPixels: true
94
+ });
95
+ label.anchor.set(.5);
96
+ root.addChild(shape, label);
97
+ partsByContainer.set(root, {
98
+ root,
99
+ shape,
100
+ label,
101
+ visualSig: "",
102
+ textSig: "",
103
+ wrapWidth: 0,
104
+ resolution
105
+ });
106
+ if (item.type === "geo") sync(root, item, context);
107
+ return root;
108
+ },
109
+ update: (container, item, context) => {
110
+ if (item.type === "geo") sync(container, item, context);
111
+ },
112
+ renderFar: (graphics, item, context) => {
113
+ if (item.type !== "geo") return;
114
+ const color = pickBoardColor(context.colors, item.color, context.colorScheme);
115
+ drawFarPlate(graphics, item.frame, {
116
+ fill: color.fill,
117
+ fillAlpha: Math.max(.18, item.fillOpacity),
118
+ accent: color.stroke,
119
+ accentAlpha: .9
120
+ });
121
+ },
122
+ destroy: (container) => {
123
+ partsByContainer.get(container)?.root.destroy({ children: true });
124
+ partsByContainer.delete(container);
125
+ }
126
+ };
127
+ //#endregion
128
+ export { geoCardRenderer };
@@ -0,0 +1,8 @@
1
+ import { BoardImageItem, BoardItem } from "../../../protocol/dist/board-document.js";
2
+ import { BoardCardRenderer } from "./board-renderer-registry.js";
3
+ //#region src/board/render/renderers/image-card-renderer.d.ts
4
+ declare const imageCardRenderer: BoardCardRenderer;
5
+ /** Helper kept for type narrowing in tests. */
6
+ declare function isImageItem(item: BoardItem): item is BoardImageItem;
7
+ //#endregion
8
+ export { imageCardRenderer, isImageItem };
@@ -0,0 +1,138 @@
1
+ import { drawFarPlate } from "./far-plate.js";
2
+ import { positionShell } from "./base-card-renderer.js";
3
+ import { Container, Graphics, Sprite, Texture } from "pixi.js";
4
+ //#region src/board/render/renderers/image-card-renderer.ts
5
+ const partsByContainer = /* @__PURE__ */ new WeakMap();
6
+ const RADIUS = 2;
7
+ /**
8
+ * Lay the sprite out inside the frame.
9
+ *
10
+ * The frame normally already matches the image's pixel aspect (see the editor's
11
+ * media size adoption), so this fills it exactly. Until that correction lands
12
+ * for a freshly dropped file, `contain` keeps the whole image visible rather
13
+ * than cropping it — the brief letterbox is self-correcting.
14
+ */
15
+ function layoutContain(sprite, width, height) {
16
+ const texture = sprite.texture;
17
+ const tw = texture.width;
18
+ const th = texture.height;
19
+ if (!tw || !th) return;
20
+ const scale = Math.min(width / tw, height / th);
21
+ sprite.width = tw * scale;
22
+ sprite.height = th * scale;
23
+ sprite.x = (width - sprite.width) / 2;
24
+ sprite.y = (height - sprite.height) / 2;
25
+ }
26
+ function sync(container, item, context) {
27
+ const parts = partsByContainer.get(container);
28
+ if (!parts) return;
29
+ positionShell(parts.root, item);
30
+ const { width, height } = item.frame;
31
+ const selected = context.selectedIds.has(item.id);
32
+ const hovered = context.hoveredId === item.id;
33
+ const key = context.assetKey(item);
34
+ const texture = key ? context.getTexture(key) : null;
35
+ const failed = Boolean(key && !texture && context.hasError(key));
36
+ const texId = texture ? `${texture.width}x${texture.height}` : "none";
37
+ const sig = [
38
+ key ?? "",
39
+ width,
40
+ height,
41
+ selected,
42
+ hovered,
43
+ texId,
44
+ failed,
45
+ context.colorScheme
46
+ ].join("|");
47
+ if (sig === parts.sig) return;
48
+ parts.sig = sig;
49
+ if (texture && parts.sprite.texture !== texture) parts.sprite.texture = texture;
50
+ parts.sprite.visible = Boolean(texture);
51
+ if (texture) layoutContain(parts.sprite, width, height);
52
+ parts.placeholder.clear();
53
+ if (!texture) {
54
+ parts.placeholder.roundRect(0, 0, width, height, RADIUS).fill({
55
+ color: context.palette.surface,
56
+ alpha: .85
57
+ }).roundRect(0, 0, width, height, RADIUS).stroke({
58
+ color: failed ? context.palette.border : context.palette.hover,
59
+ width: 1,
60
+ alpha: .9
61
+ });
62
+ const cx = width / 2;
63
+ const cy = height / 2;
64
+ 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({
65
+ color: context.palette.muted,
66
+ width: 1.5,
67
+ alpha: .8
68
+ });
69
+ else parts.placeholder.circle(cx, cy, 6).stroke({
70
+ color: context.palette.muted,
71
+ width: 1.25,
72
+ alpha: .55
73
+ });
74
+ }
75
+ parts.placeholder.visible = !texture;
76
+ if (selected || hovered) {
77
+ const hasPixels = Boolean(texture) && parts.sprite.width > 0;
78
+ const ox = hasPixels ? parts.sprite.x : 0;
79
+ const oy = hasPixels ? parts.sprite.y : 0;
80
+ const ow = hasPixels ? parts.sprite.width : width;
81
+ const oh = hasPixels ? parts.sprite.height : height;
82
+ parts.placeholder.visible = true;
83
+ parts.placeholder.roundRect(ox, oy, ow, oh, RADIUS).stroke({
84
+ color: selected ? context.palette.brand : context.palette.muted,
85
+ width: selected ? 2 : 1,
86
+ alpha: selected ? .95 : .7
87
+ });
88
+ }
89
+ }
90
+ const imageCardRenderer = {
91
+ id: "image-card",
92
+ canRender: (item) => item.type === "image",
93
+ create: (item, context) => {
94
+ const root = new Container();
95
+ const sprite = new Sprite(Texture.EMPTY);
96
+ const placeholder = new Graphics();
97
+ const mask = new Graphics();
98
+ const maskBox = () => {
99
+ mask.clear();
100
+ if (item.type === "image") mask.roundRect(0, 0, item.frame.width, item.frame.height, RADIUS).fill({ color: 16777215 });
101
+ };
102
+ maskBox();
103
+ root.addChild(placeholder, sprite, mask);
104
+ sprite.mask = mask;
105
+ partsByContainer.set(root, {
106
+ root,
107
+ sprite,
108
+ placeholder,
109
+ sig: ""
110
+ });
111
+ if (item.type === "image") sync(root, item, context);
112
+ return root;
113
+ },
114
+ update: (container, item, context) => {
115
+ if (item.type !== "image") return;
116
+ const parts = partsByContainer.get(container);
117
+ if (!parts) return;
118
+ const mask = parts.root.children.find((child) => child !== parts.sprite && child !== parts.placeholder);
119
+ if (mask) mask.clear().roundRect(0, 0, item.frame.width, item.frame.height, RADIUS).fill({ color: 16777215 });
120
+ sync(container, item, context);
121
+ },
122
+ renderFar: (graphics, item, context) => {
123
+ drawFarPlate(graphics, item.frame, {
124
+ fill: context.palette.hover,
125
+ fillAlpha: .9
126
+ });
127
+ },
128
+ destroy: (container) => {
129
+ partsByContainer.get(container)?.root.destroy({ children: true });
130
+ partsByContainer.delete(container);
131
+ }
132
+ };
133
+ /** Helper kept for type narrowing in tests. */
134
+ function isImageItem(item) {
135
+ return item.type === "image";
136
+ }
137
+ //#endregion
138
+ export { imageCardRenderer, isImageItem };
@@ -0,0 +1,5 @@
1
+ import { BoardCardRenderer } from "./board-renderer-registry.js";
2
+ //#region src/board/render/renderers/note-card-renderer.d.ts
3
+ declare const noteCardRenderer: BoardCardRenderer;
4
+ //#endregion
5
+ export { noteCardRenderer };
@@ -0,0 +1,112 @@
1
+ import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
+ import { pickBoardColor } from "../../core/palette.js";
3
+ import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
4
+ import { drawFarPlate } from "./far-plate.js";
5
+ import { positionShell } from "./base-card-renderer.js";
6
+ import { Container, Graphics, Text } from "pixi.js";
7
+ //#region src/board/render/renderers/note-card-renderer.ts
8
+ const RADIUS = 8;
9
+ const PADDING = 12;
10
+ const partsByContainer = /* @__PURE__ */ new WeakMap();
11
+ function sync(container, item, context) {
12
+ const parts = partsByContainer.get(container);
13
+ if (!parts) return;
14
+ positionShell(parts.root, item);
15
+ const { width, height } = item.frame;
16
+ const selected = context.selectedIds.has(item.id);
17
+ const hovered = context.hoveredId === item.id;
18
+ const color = pickBoardColor(context.colors, item.color, context.colorScheme);
19
+ syncTextResolution(parts.body, parts, context.zoom);
20
+ const resizing = context.resizingIds.has(item.id);
21
+ const visualSig = [
22
+ width,
23
+ height,
24
+ selected,
25
+ hovered,
26
+ color.fill,
27
+ color.stroke,
28
+ context.palette.surface,
29
+ context.palette.muted,
30
+ context.palette.border
31
+ ].join("|");
32
+ if (visualSig !== parts.visualSig) {
33
+ parts.visualSig = visualSig;
34
+ parts.background.clear();
35
+ parts.background.roundRect(0, 0, width, height, RADIUS).fill({
36
+ color: color.fill,
37
+ alpha: .16
38
+ }).roundRect(0, 0, width, height, RADIUS).fill({
39
+ color: context.palette.surface,
40
+ alpha: .6
41
+ }).rect(1, 1, width - 2, 4).fill({
42
+ color: color.stroke,
43
+ alpha: .95
44
+ });
45
+ parts.background.roundRect(0, 0, width, height, RADIUS).stroke({
46
+ color: selected ? color.stroke : hovered ? context.palette.muted : context.palette.border,
47
+ width: selected ? 2 : 1,
48
+ alpha: selected ? .95 : .8
49
+ });
50
+ }
51
+ const textSig = [item.text, color.label].join("|");
52
+ if (textSig !== parts.textSig) {
53
+ parts.textSig = textSig;
54
+ parts.body.text = item.text;
55
+ parts.body.style.fill = color.label;
56
+ }
57
+ syncTextWrapWidth(parts.body, parts, Math.max(1, width - PADDING * 2), resizing);
58
+ parts.body.position.set(PADDING, 16);
59
+ }
60
+ const noteCardRenderer = {
61
+ id: "note-card",
62
+ canRender: (item) => item.type === "note",
63
+ create: (item, context) => {
64
+ const root = new Container();
65
+ const background = new Graphics();
66
+ const resolution = textResolutionForZoom(context.zoom);
67
+ const body = new Text({
68
+ text: "",
69
+ style: {
70
+ fill: 16777215,
71
+ fontFamily: BOARD_FONT_STACK,
72
+ fontSize: 14,
73
+ fontWeight: "500",
74
+ wordWrap: true,
75
+ lineHeight: 20
76
+ },
77
+ resolution,
78
+ roundPixels: true
79
+ });
80
+ root.addChild(background, body);
81
+ partsByContainer.set(root, {
82
+ root,
83
+ background,
84
+ body,
85
+ visualSig: "",
86
+ textSig: "",
87
+ wrapWidth: 0,
88
+ resolution
89
+ });
90
+ if (item.type === "note") sync(root, item, context);
91
+ return root;
92
+ },
93
+ update: (container, item, context) => {
94
+ if (item.type === "note") sync(container, item, context);
95
+ },
96
+ renderFar: (graphics, item, context) => {
97
+ if (item.type !== "note") return;
98
+ const color = pickBoardColor(context.colors, item.color, context.colorScheme);
99
+ drawFarPlate(graphics, item.frame, {
100
+ fill: color.fill,
101
+ fillAlpha: .28,
102
+ accent: color.stroke,
103
+ accentAlpha: .95
104
+ });
105
+ },
106
+ destroy: (container) => {
107
+ partsByContainer.get(container)?.root.destroy({ children: true });
108
+ partsByContainer.delete(container);
109
+ }
110
+ };
111
+ //#endregion
112
+ export { noteCardRenderer };
@@ -0,0 +1,7 @@
1
+ import { BoardItem, BoardTextItem } from "../../../protocol/dist/board-document.js";
2
+ import { BoardCardRenderer } from "./board-renderer-registry.js";
3
+ //#region src/board/render/renderers/text-card-renderer.d.ts
4
+ declare const textCardRenderer: BoardCardRenderer;
5
+ declare function isTextItem(item: BoardItem): item is BoardTextItem;
6
+ //#endregion
7
+ export { isTextItem, textCardRenderer };
@@ -0,0 +1,94 @@
1
+ import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
+ import { TEXT_FONT_SIZE, boardTextLineHeight, clampBoardTextFontSize, measureBoardText } from "../../core/text-metrics.js";
3
+ import { pickBoardColor } from "../../core/palette.js";
4
+ import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
5
+ import { drawFarPlate } from "./far-plate.js";
6
+ import { positionShell } from "./base-card-renderer.js";
7
+ import { Container, Text } from "pixi.js";
8
+ //#region src/board/render/renderers/text-card-renderer.ts
9
+ const partsByContainer = /* @__PURE__ */ new WeakMap();
10
+ function sync(container, item, context) {
11
+ const parts = partsByContainer.get(container);
12
+ if (!parts) return;
13
+ positionShell(parts.root, item);
14
+ const fontSize = clampBoardTextFontSize(item.fontSize);
15
+ const measured = measureBoardText(item.text, fontSize);
16
+ const previewScale = Math.max(1e-4, item.frame.width / Math.max(1e-4, measured.width));
17
+ parts.body.scale.set(previewScale);
18
+ parts.body.position.set(item.frame.width / 2, item.frame.height / 2);
19
+ syncTextResolution(parts.body, parts, context.zoom * Math.max(1, previewScale));
20
+ const color = pickBoardColor(context.colors, item.color || "neutral", context.colorScheme);
21
+ const ink = item.color === "neutral" || !item.color ? context.palette.text : color.stroke;
22
+ const contentSig = [
23
+ item.text,
24
+ item.color,
25
+ fontSize,
26
+ context.colorScheme,
27
+ context.colors.brand.stroke,
28
+ context.palette.text
29
+ ].join("|");
30
+ if (contentSig === parts.contentSig) return;
31
+ parts.contentSig = contentSig;
32
+ if (parts.body.text !== item.text) parts.body.text = item.text || "";
33
+ parts.body.style.fill = ink;
34
+ parts.body.style.fontSize = fontSize;
35
+ parts.body.style.lineHeight = boardTextLineHeight(fontSize);
36
+ }
37
+ const textCardRenderer = {
38
+ id: "text-card",
39
+ canRender: (item) => item.type === "text",
40
+ create: (item, context) => {
41
+ const root = new Container();
42
+ const resolution = textResolutionForZoom(context.zoom);
43
+ const color = pickBoardColor(context.colors, item.type === "text" ? item.color || "neutral" : "neutral", context.colorScheme);
44
+ const body = new Text({
45
+ text: "",
46
+ style: {
47
+ fill: item.type === "text" && (item.color === "neutral" || !item.color) ? context.palette.text : color.stroke,
48
+ fontFamily: BOARD_FONT_STACK,
49
+ fontSize: TEXT_FONT_SIZE,
50
+ fontWeight: "500",
51
+ lineHeight: boardTextLineHeight(TEXT_FONT_SIZE),
52
+ wordWrap: false
53
+ },
54
+ resolution,
55
+ roundPixels: true
56
+ });
57
+ body.anchor.set(.5);
58
+ root.addChild(body);
59
+ partsByContainer.set(root, {
60
+ root,
61
+ body,
62
+ resolution,
63
+ contentSig: ""
64
+ });
65
+ if (item.type === "text") sync(root, item, context);
66
+ return root;
67
+ },
68
+ update: (container, item, context) => {
69
+ if (item.type === "text") sync(container, item, context);
70
+ },
71
+ /**
72
+ * Far LOD: a low-alpha ink bar. Body text is well under a pixel tall at these
73
+ * zooms, so a rasterised glyph texture would only ever resolve to a smudge —
74
+ * this approximates that smudge for free and skips the texture entirely.
75
+ */
76
+ renderFar: (graphics, item, context) => {
77
+ if (item.type !== "text") return;
78
+ const color = pickBoardColor(context.colors, item.color || "neutral", context.colorScheme);
79
+ const ink = item.color === "neutral" || !item.color ? context.palette.text : color.stroke;
80
+ drawFarPlate(graphics, item.frame, {
81
+ fill: ink,
82
+ fillAlpha: .35
83
+ });
84
+ },
85
+ destroy: (container) => {
86
+ partsByContainer.get(container)?.root.destroy({ children: true });
87
+ partsByContainer.delete(container);
88
+ }
89
+ };
90
+ function isTextItem(item) {
91
+ return item.type === "text";
92
+ }
93
+ //#endregion
94
+ export { isTextItem, textCardRenderer };
@@ -0,0 +1,5 @@
1
+ import { BoardCardRenderer } from "./board-renderer-registry.js";
2
+ //#region src/board/render/renderers/unknown-card-renderer.d.ts
3
+ declare const unknownCardRenderer: BoardCardRenderer;
4
+ //#endregion
5
+ export { unknownCardRenderer };