@neta-art/cohub 5.10.0 → 7.0.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.
- package/README.md +47 -50
- package/dist/board/animation.d.ts +157 -67
- package/dist/board/animation.js +161 -306
- package/dist/board/core/connections.js +4 -4
- package/dist/board/core/export-plan.js +1 -1
- package/dist/board/core/shape-types.js +0 -1
- package/dist/board/geometry.js +4 -4
- package/dist/board/index.d.ts +8 -7
- package/dist/board/index.js +8 -9
- package/dist/board/mutation.d.ts +2 -17
- package/dist/board/mutation.js +2 -93
- package/dist/board/render/renderers/text-card-renderer.js +1 -1
- package/dist/board/semantic-document.d.ts +30 -0
- package/dist/board/semantic-document.js +271 -0
- package/dist/board/semantic-mutation.d.ts +10 -0
- package/dist/board/semantic-mutation.js +203 -0
- package/dist/chunks/http.d.ts +27 -95
- package/dist/chunks/http.js +628 -1610
- package/dist/chunks/transport.js +1 -1
- package/dist/chunks/websocket.d.ts +3462 -533
- package/dist/chunks/websocket.js +1 -1
- package/dist/http.d.ts +3 -3
- package/dist/index.d.ts +158 -299
- package/dist/index.js +1207 -499
- package/dist/protocol/dist/board-authoring.d.ts +1305 -0
- package/dist/protocol/dist/board-authoring.js +291 -0
- package/dist/protocol/dist/board-capability-registry.d.ts +2 -0
- package/dist/protocol/dist/board-capability-registry.js +74 -0
- package/dist/protocol/dist/board-codec.d.ts +2 -0
- package/dist/protocol/dist/board-codec.js +19 -0
- package/dist/protocol/dist/board-composition.d.ts +383 -0
- package/dist/protocol/dist/board-composition.js +310 -0
- package/dist/protocol/dist/board-connection.d.ts +16 -16
- package/dist/protocol/dist/board-connection.js +16 -16
- package/dist/protocol/dist/board-constants.js +0 -25
- package/dist/protocol/dist/board-document.d.ts +34 -3
- package/dist/protocol/dist/board-document.js +3 -1
- package/dist/protocol/dist/board-effect.d.ts +94 -0
- package/dist/protocol/dist/board-effect.js +53 -0
- package/dist/protocol/dist/board-json.js +16 -0
- package/dist/protocol/dist/board-node.d.ts +1 -12
- package/dist/protocol/dist/board-node.js +1 -81
- package/dist/protocol/dist/board.d.ts +42 -245
- package/dist/protocol/dist/board.js +56 -117
- package/dist/protocol/dist/index.d.ts +9 -4
- package/dist/types.d.ts +4 -1
- package/docs/work-runtime-guide.md +19 -3
- package/package.json +2 -2
- package/dist/board/codec.d.ts +0 -27
- package/dist/board/codec.js +0 -277
- package/dist/board/nodes.d.ts +0 -113
- package/dist/board/nodes.js +0 -154
- package/dist/protocol/dist/board-content.js +0 -26
- package/dist/protocol/dist/identifiers.js +0 -10
- package/dist/protocol/dist/index.js +0 -14
- package/dist/protocol/dist/provenance.js +0 -15
- package/dist/protocol/dist/public-identifiers.js +0 -38
- package/dist/protocol/dist/realtime/board-awareness.js +0 -119
- package/dist/protocol/dist/ui-command.js +0 -2
- package/dist/protocol/dist/work-promotion-stats.js +0 -11
- package/dist/protocol/dist/work-surface.js +0 -2
- package/dist/protocol/dist/work-view-stats.js +0 -3
package/dist/chunks/http.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { D as
|
|
1
|
+
import { D as getRealtimeSpaceRoom, E as getRealtimeBoardRoom, n as HttpTransport, t as HttpError, y as isUuid } from "./transport.js";
|
|
2
2
|
import { a as resolveApiBaseUrl } from "./environment.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import "perfect-freehand";
|
|
5
4
|
//#region src/apis/channels.ts
|
|
6
5
|
var ChannelsApi = class {
|
|
7
6
|
transport;
|
|
@@ -454,7 +453,6 @@ const DEFAULT_BOARD_RENDER_LIMITS = {
|
|
|
454
453
|
simulationSteps: 1e5
|
|
455
454
|
};
|
|
456
455
|
const BOARD_BUILTIN_CLIP_KINDS = [
|
|
457
|
-
"motion.keyframes",
|
|
458
456
|
"motion.path",
|
|
459
457
|
"draw.reveal",
|
|
460
458
|
"draw.handwrite",
|
|
@@ -463,31 +461,14 @@ const BOARD_BUILTIN_CLIP_KINDS = [
|
|
|
463
461
|
"effects.trail",
|
|
464
462
|
"effects.impact",
|
|
465
463
|
"effects.flash",
|
|
466
|
-
"effects.color",
|
|
467
|
-
"camera.pan",
|
|
468
|
-
"camera.zoom",
|
|
469
464
|
"camera.focus",
|
|
470
465
|
"camera.shake"
|
|
471
466
|
];
|
|
472
467
|
const BOARD_BUILTIN_EFFECT_KINDS = ["effects.pulse", "effects.float"];
|
|
473
468
|
const BOARD_ARROW_STROKE_SIZE = 2.5;
|
|
474
469
|
const BOARD_CONNECTION_STROKE_SIZE = 2.5;
|
|
475
|
-
function clampBoardStrokeSize(size) {
|
|
476
|
-
if (!Number.isFinite(size)) return BOARD_CONNECTION_STROKE_SIZE;
|
|
477
|
-
return Math.min(64, Math.max(1, size));
|
|
478
|
-
}
|
|
479
470
|
function clipSchema(id) {
|
|
480
471
|
switch (id) {
|
|
481
|
-
case "motion.keyframes": return { params: {
|
|
482
|
-
x: {
|
|
483
|
-
coordinateSpace: "world-offset",
|
|
484
|
-
unit: "board"
|
|
485
|
-
},
|
|
486
|
-
y: {
|
|
487
|
-
coordinateSpace: "world-offset",
|
|
488
|
-
unit: "board"
|
|
489
|
-
}
|
|
490
|
-
} };
|
|
491
472
|
case "motion.path": return { params: { points: {
|
|
492
473
|
coordinateSpace: "world-offset",
|
|
493
474
|
unit: "board"
|
|
@@ -496,17 +477,6 @@ function clipSchema(id) {
|
|
|
496
477
|
coordinateSpace: "world",
|
|
497
478
|
unit: "board"
|
|
498
479
|
} } };
|
|
499
|
-
case "camera.pan": return { params: {
|
|
500
|
-
x: {
|
|
501
|
-
coordinateSpace: "screen-offset",
|
|
502
|
-
unit: "css-px"
|
|
503
|
-
},
|
|
504
|
-
y: {
|
|
505
|
-
coordinateSpace: "screen-offset",
|
|
506
|
-
unit: "css-px"
|
|
507
|
-
}
|
|
508
|
-
} };
|
|
509
|
-
case "camera.zoom": return { params: { scale: { unit: "ratio" } } };
|
|
510
480
|
case "camera.focus": return { params: {
|
|
511
481
|
focus: {
|
|
512
482
|
coordinateSpace: "world",
|
|
@@ -572,12 +542,11 @@ const BoardConnectionAnchorSchema = z.union([
|
|
|
572
542
|
ny: z.number().finite().min(0).max(1)
|
|
573
543
|
})
|
|
574
544
|
]);
|
|
575
|
-
const AUTO_BOARD_CONNECTION_ANCHOR = { kind: "auto" };
|
|
576
545
|
const BoardConnectionEndpointSchema = z.object({
|
|
577
|
-
|
|
546
|
+
itemId: z.string().min(1).max(160),
|
|
578
547
|
/** Optional semantic port. Older connections omit it and remain valid. */
|
|
579
548
|
portId: z.string().min(1).max(120).optional(),
|
|
580
|
-
anchor: BoardConnectionAnchorSchema.default(
|
|
549
|
+
anchor: BoardConnectionAnchorSchema.default({ kind: "auto" })
|
|
581
550
|
});
|
|
582
551
|
/**
|
|
583
552
|
* Which ends carry an arrowhead.
|
|
@@ -652,137 +621,148 @@ const BoardConnectionSchema = z.object({
|
|
|
652
621
|
routing: BoardConnectionRoutingSchema.default(DEFAULT_BOARD_CONNECTION_ROUTING),
|
|
653
622
|
style: BoardConnectionStyleSchema.default(DEFAULT_BOARD_CONNECTION_STYLE),
|
|
654
623
|
metadata: z.record(z.string(), z.unknown()).default({})
|
|
655
|
-
});
|
|
656
|
-
BoardConnectionSchema.omit({ id: true }).partial();
|
|
657
|
-
function normalizeBoardConnectionStyle(style) {
|
|
658
|
-
return {
|
|
659
|
-
color: style?.color?.trim() || DEFAULT_BOARD_CONNECTION_STYLE.color,
|
|
660
|
-
size: clampBoardStrokeSize(typeof style?.size === "number" && Number.isFinite(style.size) ? style.size : DEFAULT_BOARD_CONNECTION_STYLE.size),
|
|
661
|
-
line: style?.line ?? DEFAULT_BOARD_CONNECTION_STYLE.line
|
|
662
|
-
};
|
|
663
|
-
}
|
|
624
|
+
}).strict();
|
|
664
625
|
/**
|
|
665
|
-
*
|
|
626
|
+
* A patch to an existing connection.
|
|
666
627
|
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
* for the same intent.
|
|
628
|
+
* `id` is excluded: a connection's identity never changes, and re-pointing both
|
|
629
|
+
* endpoints is an edit of the same relation, not a new one.
|
|
670
630
|
*/
|
|
671
|
-
|
|
672
|
-
return {
|
|
673
|
-
id: input.id,
|
|
674
|
-
source: {
|
|
675
|
-
nodeId: input.sourceNodeId,
|
|
676
|
-
...input.sourcePortId ? { portId: input.sourcePortId } : {},
|
|
677
|
-
anchor: input.sourceAnchor ?? AUTO_BOARD_CONNECTION_ANCHOR
|
|
678
|
-
},
|
|
679
|
-
target: {
|
|
680
|
-
nodeId: input.targetNodeId,
|
|
681
|
-
...input.targetPortId ? { portId: input.targetPortId } : {},
|
|
682
|
-
anchor: input.targetAnchor ?? AUTO_BOARD_CONNECTION_ANCHOR
|
|
683
|
-
},
|
|
684
|
-
relation: input.relation ?? "related",
|
|
685
|
-
direction: input.direction ?? "forward",
|
|
686
|
-
label: input.label ?? "",
|
|
687
|
-
routing: {
|
|
688
|
-
...DEFAULT_BOARD_CONNECTION_ROUTING,
|
|
689
|
-
...input.routing
|
|
690
|
-
},
|
|
691
|
-
style: normalizeBoardConnectionStyle(input.style),
|
|
692
|
-
metadata: input.metadata ?? {}
|
|
693
|
-
};
|
|
694
|
-
}
|
|
631
|
+
const BoardConnectionPatchSchema = BoardConnectionSchema.omit({ id: true }).partial();
|
|
695
632
|
//#endregion
|
|
696
|
-
//#region
|
|
697
|
-
|
|
698
|
-
if (websocketClient.state === "open" || websocketClient.state === "connecting" || websocketClient.state === "reconnecting") return;
|
|
699
|
-
websocketClient.connect().catch((error) => {
|
|
700
|
-
console.error("[CohubClient] Failed to connect realtime websocket:", error);
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
//#endregion
|
|
704
|
-
//#region ../protocol/dist/board.js
|
|
705
|
-
const BOARD_DOCUMENT_KIND = "cohub.board";
|
|
706
|
-
const BOARD_MANIFEST_KIND = "cohub.board.manifest";
|
|
707
|
-
const idSchema$1 = z.string().min(1).max(160);
|
|
708
|
-
const extensionIdSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/).max(160);
|
|
709
|
-
const jsonObjectSchema = z.record(z.string(), z.unknown());
|
|
633
|
+
//#region ../protocol/dist/board-composition.js
|
|
634
|
+
const idSchema$2 = z.string().min(1).max(160);
|
|
710
635
|
const finiteSchema$1 = z.number().finite();
|
|
711
|
-
z.
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
z.object({
|
|
723
|
-
type: z.literal("effect"),
|
|
724
|
-
effectId: idSchema$1
|
|
725
|
-
}),
|
|
726
|
-
z.object({ type: z.literal("board") }),
|
|
727
|
-
z.object({ type: z.literal("camera") })
|
|
636
|
+
const jsonObjectSchema$2 = z.record(z.string(), z.unknown());
|
|
637
|
+
const BoardEasingSchema = z.enum([
|
|
638
|
+
"linear",
|
|
639
|
+
"ease-in-quad",
|
|
640
|
+
"ease-out-quad",
|
|
641
|
+
"ease-in-out-quad",
|
|
642
|
+
"ease-in-cubic",
|
|
643
|
+
"ease-out-cubic",
|
|
644
|
+
"ease-in-out-cubic",
|
|
645
|
+
"ease-out-quart",
|
|
646
|
+
"ease-out-expo"
|
|
728
647
|
]);
|
|
729
|
-
z.
|
|
730
|
-
centerX: finiteSchema$1,
|
|
731
|
-
centerY: finiteSchema$1,
|
|
732
|
-
zoom: finiteSchema$1.positive()
|
|
733
|
-
});
|
|
734
|
-
const BoardCameraFocusSchema = z.discriminatedUnion("type", [
|
|
648
|
+
const BoardAnimationTargetSchema = z.discriminatedUnion("type", [
|
|
735
649
|
z.object({
|
|
736
|
-
type: z.literal("
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
y: finiteSchema$1,
|
|
740
|
-
width: finiteSchema$1.positive(),
|
|
741
|
-
height: finiteSchema$1.positive()
|
|
742
|
-
})
|
|
743
|
-
}),
|
|
744
|
-
z.object({
|
|
745
|
-
type: z.literal("node"),
|
|
746
|
-
nodeId: idSchema$1
|
|
747
|
-
}),
|
|
748
|
-
z.object({
|
|
749
|
-
type: z.literal("nodes"),
|
|
750
|
-
nodeIds: z.array(idSchema$1).min(1).max(1e3)
|
|
751
|
-
}),
|
|
650
|
+
type: z.literal("item"),
|
|
651
|
+
itemId: idSchema$2
|
|
652
|
+
}).strict(),
|
|
752
653
|
z.object({
|
|
753
|
-
type: z.literal("
|
|
754
|
-
|
|
755
|
-
})
|
|
654
|
+
type: z.literal("effect"),
|
|
655
|
+
effectId: idSchema$2
|
|
656
|
+
}).strict(),
|
|
657
|
+
z.object({ type: z.literal("camera") }).strict(),
|
|
658
|
+
z.object({ type: z.literal("board") }).strict()
|
|
756
659
|
]);
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
660
|
+
const vector2Schema = z.object({
|
|
661
|
+
x: finiteSchema$1,
|
|
662
|
+
y: finiteSchema$1
|
|
663
|
+
}).strict();
|
|
664
|
+
const scaleSchema = z.union([finiteSchema$1.positive(), z.object({
|
|
665
|
+
x: finiteSchema$1.positive(),
|
|
666
|
+
y: finiteSchema$1.positive()
|
|
667
|
+
}).strict()]);
|
|
668
|
+
const BOARD_ANIMATION_CHANNELS = {
|
|
669
|
+
"transform.translation": {
|
|
670
|
+
targets: ["item"],
|
|
671
|
+
value: vector2Schema,
|
|
672
|
+
interpolations: ["linear", "step"],
|
|
673
|
+
coordinateSpace: "world-offset",
|
|
674
|
+
unit: "board"
|
|
675
|
+
},
|
|
676
|
+
"transform.rotation": {
|
|
677
|
+
targets: ["item"],
|
|
678
|
+
value: finiteSchema$1,
|
|
679
|
+
interpolations: ["linear", "step"],
|
|
680
|
+
unit: "radian"
|
|
681
|
+
},
|
|
682
|
+
"transform.scale": {
|
|
683
|
+
targets: ["item"],
|
|
684
|
+
value: scaleSchema,
|
|
685
|
+
interpolations: ["linear", "step"],
|
|
686
|
+
unit: "ratio"
|
|
687
|
+
},
|
|
688
|
+
"style.opacity": {
|
|
689
|
+
targets: ["item"],
|
|
690
|
+
value: finiteSchema$1.min(0).max(1),
|
|
691
|
+
interpolations: ["linear", "step"],
|
|
692
|
+
unit: "ratio"
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
const BoardTrackKeyframeSchema = z.object({
|
|
696
|
+
time: finiteSchema$1.nonnegative(),
|
|
697
|
+
value: z.unknown(),
|
|
698
|
+
easing: BoardEasingSchema.optional()
|
|
699
|
+
}).strict();
|
|
700
|
+
const BoardTrackSchema = z.object({
|
|
701
|
+
id: idSchema$2,
|
|
702
|
+
target: BoardAnimationTargetSchema,
|
|
703
|
+
channel: z.string().min(1).max(160),
|
|
704
|
+
channelVersion: z.number().int().positive().default(1),
|
|
705
|
+
interpolation: z.enum(["linear", "step"]).default("linear"),
|
|
706
|
+
fill: z.enum([
|
|
707
|
+
"none",
|
|
708
|
+
"backwards",
|
|
709
|
+
"forwards",
|
|
710
|
+
"both"
|
|
711
|
+
]).default("none"),
|
|
712
|
+
keyframes: z.array(BoardTrackKeyframeSchema).min(1).max(1e5),
|
|
713
|
+
metadata: jsonObjectSchema$2.default({})
|
|
714
|
+
}).strict().superRefine((track, context) => {
|
|
715
|
+
let previous = -1;
|
|
716
|
+
for (const [index, keyframe] of track.keyframes.entries()) {
|
|
717
|
+
if (keyframe.time <= previous) context.addIssue({
|
|
718
|
+
code: "custom",
|
|
719
|
+
message: "keyframe times must be strictly increasing",
|
|
720
|
+
path: [
|
|
721
|
+
"keyframes",
|
|
722
|
+
index,
|
|
723
|
+
"time"
|
|
724
|
+
]
|
|
725
|
+
});
|
|
726
|
+
previous = keyframe.time;
|
|
727
|
+
}
|
|
728
|
+
const channel = BOARD_ANIMATION_CHANNELS[track.channel];
|
|
729
|
+
if (!channel) {
|
|
730
|
+
if (!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+){2,}$/.test(track.channel)) context.addIssue({
|
|
731
|
+
code: "custom",
|
|
732
|
+
message: `unknown channel: ${track.channel}`,
|
|
733
|
+
path: ["channel"]
|
|
734
|
+
});
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (!channel.targets.includes(track.target.type)) context.addIssue({
|
|
765
738
|
code: "custom",
|
|
766
|
-
message:
|
|
767
|
-
path: ["
|
|
739
|
+
message: `${track.channel} cannot target ${track.target.type}`,
|
|
740
|
+
path: ["target"]
|
|
768
741
|
});
|
|
742
|
+
if (!channel.interpolations.includes(track.interpolation)) context.addIssue({
|
|
743
|
+
code: "custom",
|
|
744
|
+
message: `${track.channel} does not support ${track.interpolation} interpolation`,
|
|
745
|
+
path: ["interpolation"]
|
|
746
|
+
});
|
|
747
|
+
for (const [index, keyframe] of track.keyframes.entries()) {
|
|
748
|
+
const parsed = channel.value.safeParse(keyframe.value);
|
|
749
|
+
if (!parsed.success) context.addIssue({
|
|
750
|
+
code: "custom",
|
|
751
|
+
message: parsed.error.issues[0]?.message ?? `invalid value for ${track.channel}`,
|
|
752
|
+
path: [
|
|
753
|
+
"keyframes",
|
|
754
|
+
index,
|
|
755
|
+
"value"
|
|
756
|
+
]
|
|
757
|
+
});
|
|
758
|
+
}
|
|
769
759
|
});
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
});
|
|
775
|
-
const BoardKeyframeSchema = z.object({
|
|
776
|
-
at: finiteSchema$1.nonnegative(),
|
|
777
|
-
value: z.unknown(),
|
|
778
|
-
easing: z.string().min(1).max(80).optional()
|
|
779
|
-
});
|
|
780
|
-
const BoardClipSchema = z.object({
|
|
781
|
-
id: idSchema$1,
|
|
782
|
-
sequenceId: idSchema$1,
|
|
783
|
-
kind: extensionIdSchema,
|
|
760
|
+
const extensionKindSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/).max(160);
|
|
761
|
+
const BoardProceduralClipSchema = z.object({
|
|
762
|
+
id: idSchema$2,
|
|
763
|
+
kind: extensionKindSchema,
|
|
784
764
|
kindVersion: z.number().int().positive(),
|
|
785
|
-
target:
|
|
765
|
+
target: BoardAnimationTargetSchema,
|
|
786
766
|
start: finiteSchema$1.nonnegative(),
|
|
787
767
|
duration: finiteSchema$1.positive(),
|
|
788
768
|
layer: z.enum([
|
|
@@ -797,20 +777,207 @@ const BoardClipSchema = z.object({
|
|
|
797
777
|
"forwards",
|
|
798
778
|
"both"
|
|
799
779
|
]).default("none"),
|
|
800
|
-
easing:
|
|
801
|
-
params: jsonObjectSchema.default({}),
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
780
|
+
easing: BoardEasingSchema.default("linear"),
|
|
781
|
+
params: jsonObjectSchema$2.default({}),
|
|
782
|
+
assetRefs: z.array(z.object({
|
|
783
|
+
type: z.enum(["space-file", "extension"]),
|
|
784
|
+
ref: z.string().min(1).max(4096),
|
|
785
|
+
digest: z.string().min(16).max(160).optional()
|
|
786
|
+
}).strict()).default([]),
|
|
787
|
+
seed: idSchema$2,
|
|
788
|
+
metadata: jsonObjectSchema$2.default({})
|
|
789
|
+
}).strict();
|
|
790
|
+
const BoardTimelineMarkerSchema = z.object({
|
|
791
|
+
id: idSchema$2,
|
|
792
|
+
time: finiteSchema$1.nonnegative(),
|
|
793
|
+
duration: finiteSchema$1.nonnegative().optional(),
|
|
794
|
+
metadata: jsonObjectSchema$2.default({})
|
|
795
|
+
}).strict();
|
|
796
|
+
const BoardTimelineSchema = z.object({
|
|
797
|
+
duration: finiteSchema$1.nonnegative(),
|
|
798
|
+
tracks: z.array(BoardTrackSchema).max(5e4).default([]),
|
|
799
|
+
clips: z.array(BoardProceduralClipSchema).max(5e4).default([]),
|
|
800
|
+
markers: z.array(BoardTimelineMarkerSchema).max(1e4).default([])
|
|
801
|
+
}).strict().superRefine((timeline, context) => {
|
|
802
|
+
const ids = /* @__PURE__ */ new Set();
|
|
803
|
+
const channels = /* @__PURE__ */ new Set();
|
|
804
|
+
for (const [index, track] of timeline.tracks.entries()) {
|
|
805
|
+
if (ids.has(track.id)) context.addIssue({
|
|
806
|
+
code: "custom",
|
|
807
|
+
message: `duplicate timeline id: ${track.id}`,
|
|
808
|
+
path: [
|
|
809
|
+
"tracks",
|
|
810
|
+
index,
|
|
811
|
+
"id"
|
|
812
|
+
]
|
|
813
|
+
});
|
|
814
|
+
ids.add(track.id);
|
|
815
|
+
const targetId = track.target.type === "item" ? track.target.itemId : track.target.type === "effect" ? track.target.effectId : track.target.type;
|
|
816
|
+
const key = `${track.target.type}:${targetId}:${track.channel}`;
|
|
817
|
+
if (channels.has(key)) context.addIssue({
|
|
818
|
+
code: "custom",
|
|
819
|
+
message: `multiple tracks control ${key}`,
|
|
820
|
+
path: [
|
|
821
|
+
"tracks",
|
|
822
|
+
index,
|
|
823
|
+
"channel"
|
|
824
|
+
]
|
|
825
|
+
});
|
|
826
|
+
channels.add(key);
|
|
827
|
+
for (const [keyframeIndex, keyframe] of track.keyframes.entries()) if (keyframe.time > timeline.duration) context.addIssue({
|
|
828
|
+
code: "custom",
|
|
829
|
+
message: "keyframe exceeds timeline duration",
|
|
830
|
+
path: [
|
|
831
|
+
"tracks",
|
|
832
|
+
index,
|
|
833
|
+
"keyframes",
|
|
834
|
+
keyframeIndex,
|
|
835
|
+
"time"
|
|
836
|
+
]
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
for (const [index, clip] of timeline.clips.entries()) {
|
|
840
|
+
if (ids.has(clip.id)) context.addIssue({
|
|
841
|
+
code: "custom",
|
|
842
|
+
message: `duplicate timeline id: ${clip.id}`,
|
|
843
|
+
path: [
|
|
844
|
+
"clips",
|
|
845
|
+
index,
|
|
846
|
+
"id"
|
|
847
|
+
]
|
|
848
|
+
});
|
|
849
|
+
ids.add(clip.id);
|
|
850
|
+
if (clip.start + clip.duration > timeline.duration) context.addIssue({
|
|
851
|
+
code: "custom",
|
|
852
|
+
message: "clip exceeds timeline duration",
|
|
853
|
+
path: [
|
|
854
|
+
"clips",
|
|
855
|
+
index,
|
|
856
|
+
"duration"
|
|
857
|
+
]
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
for (const [index, marker] of timeline.markers.entries()) {
|
|
861
|
+
if (ids.has(marker.id)) context.addIssue({
|
|
862
|
+
code: "custom",
|
|
863
|
+
message: `duplicate timeline id: ${marker.id}`,
|
|
864
|
+
path: [
|
|
865
|
+
"markers",
|
|
866
|
+
index,
|
|
867
|
+
"id"
|
|
868
|
+
]
|
|
869
|
+
});
|
|
870
|
+
ids.add(marker.id);
|
|
871
|
+
if (marker.time + (marker.duration ?? 0) > timeline.duration) context.addIssue({
|
|
872
|
+
code: "custom",
|
|
873
|
+
message: "marker exceeds timeline duration",
|
|
874
|
+
path: ["markers", index]
|
|
875
|
+
});
|
|
876
|
+
}
|
|
806
877
|
});
|
|
878
|
+
const BoardCompositionPlaybackSchema = z.object({
|
|
879
|
+
loop: z.boolean().default(false),
|
|
880
|
+
endBehavior: z.enum(["hold", "reset"]).default("hold"),
|
|
881
|
+
reducedMotion: z.discriminatedUnion("mode", [
|
|
882
|
+
z.object({ mode: z.literal("base") }).strict(),
|
|
883
|
+
z.object({
|
|
884
|
+
mode: z.literal("time"),
|
|
885
|
+
time: finiteSchema$1.nonnegative()
|
|
886
|
+
}).strict(),
|
|
887
|
+
z.object({
|
|
888
|
+
mode: z.literal("marker"),
|
|
889
|
+
markerId: idSchema$2
|
|
890
|
+
}).strict()
|
|
891
|
+
]).default({ mode: "base" })
|
|
892
|
+
}).strict();
|
|
893
|
+
const compositionFields = {
|
|
894
|
+
id: idSchema$2,
|
|
895
|
+
name: z.string().min(1).max(255),
|
|
896
|
+
timeline: BoardTimelineSchema,
|
|
897
|
+
playback: BoardCompositionPlaybackSchema.default({
|
|
898
|
+
loop: false,
|
|
899
|
+
endBehavior: "hold",
|
|
900
|
+
reducedMotion: { mode: "base" }
|
|
901
|
+
}),
|
|
902
|
+
metadata: jsonObjectSchema$2.default({})
|
|
903
|
+
};
|
|
904
|
+
function validateCompositionFallback(composition, context) {
|
|
905
|
+
const fallback = composition.playback.reducedMotion;
|
|
906
|
+
if (fallback.mode === "time" && fallback.time > composition.timeline.duration) context.addIssue({
|
|
907
|
+
code: "custom",
|
|
908
|
+
message: "reduced-motion time exceeds timeline duration",
|
|
909
|
+
path: [
|
|
910
|
+
"playback",
|
|
911
|
+
"reducedMotion",
|
|
912
|
+
"time"
|
|
913
|
+
]
|
|
914
|
+
});
|
|
915
|
+
if (fallback.mode === "marker" && !composition.timeline.markers.some((marker) => marker.id === fallback.markerId)) context.addIssue({
|
|
916
|
+
code: "custom",
|
|
917
|
+
message: `marker does not exist: ${fallback.markerId}`,
|
|
918
|
+
path: [
|
|
919
|
+
"playback",
|
|
920
|
+
"reducedMotion",
|
|
921
|
+
"markerId"
|
|
922
|
+
]
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
const BoardCompositionInputSchema = z.object(compositionFields).strict().superRefine(validateCompositionFallback);
|
|
926
|
+
const BoardCompositionSchema = z.object({
|
|
927
|
+
...compositionFields,
|
|
928
|
+
revision: z.number().int().nonnegative().default(0)
|
|
929
|
+
}).strict().superRefine(validateCompositionFallback);
|
|
930
|
+
/**
|
|
931
|
+
* Accept inspect/get output as apply input while stripping server-owned
|
|
932
|
+
* `revision`. Anything else that looks server-owned (or simply unknown) fails
|
|
933
|
+
* with a pointer at the offending key, so a get → edit → apply round-trip that
|
|
934
|
+
* accidentally carries runtime fields names the culprit instead of a bare
|
|
935
|
+
* "Unrecognized key".
|
|
936
|
+
*/
|
|
937
|
+
function parseBoardCompositionInput(value) {
|
|
938
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
939
|
+
const { revision: _revision, ...input } = value;
|
|
940
|
+
const result = BoardCompositionInputSchema.safeParse(input);
|
|
941
|
+
if (!result.success) {
|
|
942
|
+
const issue = result.error.issues.find((item) => item.code === "unrecognized_keys");
|
|
943
|
+
if (issue) throw new Error(`unknown field in composition input: ${issue.message}. Server-owned fields such as revision are stripped automatically; remove other fields not part of the composition schema`);
|
|
944
|
+
throw result.error;
|
|
945
|
+
}
|
|
946
|
+
return result.data;
|
|
947
|
+
}
|
|
948
|
+
return BoardCompositionInputSchema.parse(value);
|
|
949
|
+
}
|
|
950
|
+
const BOARD_ANIMATION_CHANNEL_CAPABILITIES = Object.entries(BOARD_ANIMATION_CHANNELS).map(([id, definition]) => ({
|
|
951
|
+
id,
|
|
952
|
+
version: 1,
|
|
953
|
+
targets: definition.targets,
|
|
954
|
+
interpolations: definition.interpolations,
|
|
955
|
+
..."coordinateSpace" in definition ? { coordinateSpace: definition.coordinateSpace } : {},
|
|
956
|
+
..."unit" in definition ? { unit: definition.unit } : {},
|
|
957
|
+
valueSchema: z.toJSONSchema(definition.value)
|
|
958
|
+
}));
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region ../protocol/dist/board-effect.js
|
|
961
|
+
/**
|
|
962
|
+
* Board effect schema, isolated so `board-authoring.ts` can reuse the exact
|
|
963
|
+
* definition without a runtime cycle through `board.ts`. One definition, one
|
|
964
|
+
* strictness level — semantic mutations and Board create must not drift apart.
|
|
965
|
+
*/
|
|
966
|
+
const idSchema$1 = z.string().min(1).max(160);
|
|
967
|
+
const extensionIdSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/).max(160);
|
|
968
|
+
const jsonObjectSchema$1 = z.record(z.string(), z.unknown());
|
|
969
|
+
const BoardAssetRefSchema = z.object({
|
|
970
|
+
type: z.enum(["space-file", "extension"]),
|
|
971
|
+
ref: z.string().min(1).max(4096),
|
|
972
|
+
digest: z.string().min(16).max(160).optional()
|
|
973
|
+
}).strict();
|
|
807
974
|
const BoardEffectSchema = z.object({
|
|
808
975
|
id: idSchema$1,
|
|
809
976
|
boardId: z.string().uuid(),
|
|
810
977
|
target: z.discriminatedUnion("type", [z.object({
|
|
811
|
-
type: z.literal("
|
|
812
|
-
|
|
813
|
-
}), z.object({ type: z.literal("board") })]),
|
|
978
|
+
type: z.literal("item"),
|
|
979
|
+
itemId: idSchema$1
|
|
980
|
+
}).strict(), z.object({ type: z.literal("board") }).strict()]),
|
|
814
981
|
kind: extensionIdSchema,
|
|
815
982
|
kindVersion: z.number().int().positive(),
|
|
816
983
|
enabled: z.boolean().default(true),
|
|
@@ -827,610 +994,30 @@ const BoardEffectSchema = z.object({
|
|
|
827
994
|
layer: z.enum([
|
|
828
995
|
"behind",
|
|
829
996
|
"front",
|
|
830
|
-
"screen"
|
|
831
|
-
]).default("front"),
|
|
832
|
-
seed: z.string().min(1).max(160),
|
|
833
|
-
params: jsonObjectSchema.default({}),
|
|
834
|
-
assetRefs: z.array(BoardAssetRefSchema).default([]),
|
|
835
|
-
metadata: jsonObjectSchema.default({}),
|
|
836
|
-
revision: z.number().int().nonnegative()
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
boardId:
|
|
841
|
-
|
|
842
|
-
duration: finiteSchema$1.nonnegative(),
|
|
843
|
-
seed: z.string().min(1).max(160),
|
|
844
|
-
restPose: jsonObjectSchema.default({}),
|
|
845
|
-
metadata: jsonObjectSchema.default({}),
|
|
846
|
-
revision: z.number().int().nonnegative()
|
|
847
|
-
});
|
|
848
|
-
const BoardNodeInputSchema = z.object({
|
|
849
|
-
nodeId: idSchema$1,
|
|
850
|
-
type: z.string().min(1).max(40),
|
|
851
|
-
parentId: idSchema$1.nullable(),
|
|
852
|
-
orderKey: z.string().max(4096).nullable(),
|
|
853
|
-
x: finiteSchema$1,
|
|
854
|
-
y: finiteSchema$1,
|
|
855
|
-
width: finiteSchema$1.positive(),
|
|
856
|
-
height: finiteSchema$1.positive(),
|
|
857
|
-
rotation: finiteSchema$1,
|
|
858
|
-
refKind: z.string().max(40).nullable(),
|
|
859
|
-
refPath: z.string().max(4096).nullable(),
|
|
860
|
-
refUrl: z.string().max(4096).nullable(),
|
|
861
|
-
view: jsonObjectSchema,
|
|
862
|
-
style: jsonObjectSchema,
|
|
863
|
-
data: jsonObjectSchema
|
|
864
|
-
});
|
|
865
|
-
z.object({
|
|
866
|
-
path: z.string().min(1),
|
|
867
|
-
/** Reused by clients when board creation is interrupted and retried. */
|
|
868
|
-
mutationId: z.string().max(128).optional(),
|
|
869
|
-
title: z.string().min(1).max(255).optional(),
|
|
870
|
-
metadata: jsonObjectSchema.optional(),
|
|
871
|
-
nodes: z.array(BoardNodeInputSchema).max(5e4).optional(),
|
|
872
|
-
connections: z.array(BoardConnectionSchema).max(5e4).optional(),
|
|
873
|
-
effects: z.array(BoardEffectSchema.omit({
|
|
874
|
-
boardId: true,
|
|
875
|
-
revision: true
|
|
876
|
-
})).optional(),
|
|
877
|
-
sequences: z.array(z.object({
|
|
878
|
-
sequence: BoardSequenceSchema.omit({
|
|
879
|
-
boardId: true,
|
|
880
|
-
revision: true
|
|
881
|
-
}),
|
|
882
|
-
clips: z.array(BoardClipSchema.omit({ sequenceId: true }))
|
|
883
|
-
})).optional()
|
|
884
|
-
});
|
|
885
|
-
z.object({
|
|
886
|
-
include: z.array(z.enum([
|
|
887
|
-
"nodes",
|
|
888
|
-
"connections",
|
|
889
|
-
"effects",
|
|
890
|
-
"sequences",
|
|
891
|
-
"clips",
|
|
892
|
-
"playback"
|
|
893
|
-
])).optional(),
|
|
894
|
-
viewport: z.object({
|
|
895
|
-
x: finiteSchema$1,
|
|
896
|
-
y: finiteSchema$1,
|
|
897
|
-
width: finiteSchema$1.positive(),
|
|
898
|
-
height: finiteSchema$1.positive()
|
|
899
|
-
}).optional()
|
|
900
|
-
});
|
|
901
|
-
/** Persisted on `boards.metadata.playback`: how a Board plays when opened. */
|
|
902
|
-
const BoardPlaybackPolicySchema = z.object({
|
|
903
|
-
sequenceId: idSchema$1,
|
|
904
|
-
/** Delay before the first local playback after opening the Board, in milliseconds. */
|
|
905
|
-
delayMs: finiteSchema$1.nonnegative().default(0),
|
|
906
|
-
loop: z.boolean().default(false)
|
|
907
|
-
});
|
|
908
|
-
function parseBoardPlaybackPolicy(metadata) {
|
|
909
|
-
const parsed = BoardPlaybackPolicySchema.safeParse(metadata.playback);
|
|
910
|
-
return parsed.success ? parsed.data : null;
|
|
911
|
-
}
|
|
912
|
-
z.discriminatedUnion("type", [
|
|
913
|
-
z.object({
|
|
914
|
-
commandId: idSchema$1,
|
|
915
|
-
type: z.literal("play"),
|
|
916
|
-
sequenceId: idSchema$1,
|
|
917
|
-
position: finiteSchema$1.nonnegative().optional(),
|
|
918
|
-
timeScale: finiteSchema$1.positive().max(4).optional(),
|
|
919
|
-
shared: z.boolean().optional(),
|
|
920
|
-
seed: idSchema$1.optional()
|
|
921
|
-
}),
|
|
922
|
-
z.object({
|
|
923
|
-
commandId: idSchema$1,
|
|
924
|
-
type: z.literal("pause"),
|
|
925
|
-
playbackId: z.string().uuid()
|
|
926
|
-
}),
|
|
927
|
-
z.object({
|
|
928
|
-
commandId: idSchema$1,
|
|
929
|
-
type: z.literal("seek"),
|
|
930
|
-
playbackId: z.string().uuid(),
|
|
931
|
-
position: finiteSchema$1.nonnegative()
|
|
932
|
-
}),
|
|
933
|
-
z.object({
|
|
934
|
-
commandId: idSchema$1,
|
|
935
|
-
type: z.literal("stop"),
|
|
936
|
-
playbackId: z.string().uuid()
|
|
937
|
-
})
|
|
938
|
-
]);
|
|
939
|
-
const BoardContentKindSchema = z.enum([
|
|
940
|
-
"text",
|
|
941
|
-
"image",
|
|
942
|
-
"video",
|
|
943
|
-
"audio",
|
|
944
|
-
"file",
|
|
945
|
-
"json",
|
|
946
|
-
"collection"
|
|
947
|
-
]);
|
|
948
|
-
const BoardPortSchema = z.object({
|
|
949
|
-
id: z.string().min(1).max(120),
|
|
950
|
-
kind: BoardContentKindSchema,
|
|
951
|
-
role: z.string().min(1).max(80).optional(),
|
|
952
|
-
required: z.boolean().optional(),
|
|
953
|
-
multiple: z.boolean().optional(),
|
|
954
|
-
maxItems: z.number().int().positive().optional()
|
|
955
|
-
}).strict();
|
|
956
|
-
z.object({
|
|
957
|
-
inputs: z.array(BoardPortSchema),
|
|
958
|
-
outputs: z.array(BoardPortSchema)
|
|
959
|
-
}).strict();
|
|
960
|
-
//#endregion
|
|
961
|
-
//#region ../protocol/dist/board-url.js
|
|
962
|
-
const BOARD_REMOTE_URL_MAX_LENGTH = 4096;
|
|
963
|
-
function parseIpv4(host) {
|
|
964
|
-
const parts = host.split(".").map(Number);
|
|
965
|
-
return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
|
|
966
|
-
}
|
|
967
|
-
function isBlockedIpv4(host) {
|
|
968
|
-
const parts = parseIpv4(host);
|
|
969
|
-
if (!parts) return false;
|
|
970
|
-
const [first, second, third] = parts;
|
|
971
|
-
if (first === 0 || first === 10 || first === 127) return true;
|
|
972
|
-
if (first === 100 && second >= 64 && second <= 127) return true;
|
|
973
|
-
if (first === 169 && second === 254) return true;
|
|
974
|
-
if (first === 172 && second >= 16 && second <= 31) return true;
|
|
975
|
-
if (first === 192 && second === 168) return true;
|
|
976
|
-
if (first === 192 && second === 0 && (third === 0 || third === 2)) return true;
|
|
977
|
-
if (first === 192 && second === 88 && third === 99) return true;
|
|
978
|
-
if (first === 198 && (second === 18 || second === 19)) return true;
|
|
979
|
-
if (first === 198 && second === 51 && third === 100) return true;
|
|
980
|
-
if (first === 203 && second === 0 && third === 113) return true;
|
|
981
|
-
return first >= 224;
|
|
982
|
-
}
|
|
983
|
-
function expandIpv6(host) {
|
|
984
|
-
const [head, tail, extra] = host.toLowerCase().split("::");
|
|
985
|
-
if (extra !== void 0) return null;
|
|
986
|
-
const headParts = head ? head.split(":").filter(Boolean) : [];
|
|
987
|
-
const tailParts = tail ? tail.split(":").filter(Boolean) : [];
|
|
988
|
-
const missing = 8 - headParts.length - tailParts.length;
|
|
989
|
-
if (missing < 0 || tail === void 0 && missing !== 0) return null;
|
|
990
|
-
const parts = [
|
|
991
|
-
...headParts,
|
|
992
|
-
...Array.from({ length: missing }, () => "0"),
|
|
993
|
-
...tailParts
|
|
994
|
-
];
|
|
995
|
-
if (parts.length !== 8 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return null;
|
|
996
|
-
return parts.map((part) => part.padStart(4, "0"));
|
|
997
|
-
}
|
|
998
|
-
function isBlockedIpv6(host) {
|
|
999
|
-
const parts = expandIpv6(host);
|
|
1000
|
-
if (!parts) return true;
|
|
1001
|
-
if (parts.every((part) => part === "0000")) return true;
|
|
1002
|
-
if (parts.slice(0, 7).every((part) => part === "0000") && parts[7] === "0001") return true;
|
|
1003
|
-
if (parts.slice(0, 5).every((part) => part === "0000") && parts[5] === "ffff") {
|
|
1004
|
-
const high = Number.parseInt(parts[6] ?? "0", 16);
|
|
1005
|
-
const low = Number.parseInt(parts[7] ?? "0", 16);
|
|
1006
|
-
return isBlockedIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
1007
|
-
}
|
|
1008
|
-
if (parts.slice(0, 6).every((part) => part === "0000")) return true;
|
|
1009
|
-
const first = Number.parseInt(parts[0] ?? "0", 16);
|
|
1010
|
-
if ((first & 65024) === 64512) return true;
|
|
1011
|
-
if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
|
|
1012
|
-
if ((first & 65280) === 65280) return true;
|
|
1013
|
-
return parts[0] === "2001" && parts[1] === "0db8";
|
|
1014
|
-
}
|
|
1015
|
-
function isPublicBoardRemoteAddress(value) {
|
|
1016
|
-
const address = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
1017
|
-
if (parseIpv4(address)) return !isBlockedIpv4(address);
|
|
1018
|
-
return address.includes(":") && !isBlockedIpv6(address);
|
|
1019
|
-
}
|
|
1020
|
-
function isBlockedHost(hostname) {
|
|
1021
|
-
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
1022
|
-
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
1023
|
-
if (parseIpv4(host) || host.includes(":")) return !isPublicBoardRemoteAddress(host);
|
|
1024
|
-
return false;
|
|
1025
|
-
}
|
|
1026
|
-
/**
|
|
1027
|
-
* Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
|
|
1028
|
-
* addresses; any future server-side fetcher must additionally validate DNS
|
|
1029
|
-
* resolution to defend against rebinding.
|
|
1030
|
-
*/
|
|
1031
|
-
function normalizeBoardRemoteUrl(value) {
|
|
1032
|
-
if (typeof value !== "string") return void 0;
|
|
1033
|
-
const input = value.trim();
|
|
1034
|
-
if (!input || input.length > 4096) return void 0;
|
|
1035
|
-
try {
|
|
1036
|
-
const url = new URL(input);
|
|
1037
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
|
|
1038
|
-
if (url.username || url.password || isBlockedHost(url.hostname)) return;
|
|
1039
|
-
const normalized = url.toString();
|
|
1040
|
-
return normalized.length <= 4096 ? normalized : void 0;
|
|
1041
|
-
} catch {
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
const BoardRemoteUrlSchema = z.string().max(BOARD_REMOTE_URL_MAX_LENGTH).refine((value) => normalizeBoardRemoteUrl(value) !== void 0, { message: "URL must be a public HTTP(S) URL without credentials" });
|
|
1046
|
-
//#endregion
|
|
1047
|
-
//#region ../protocol/dist/board-document.js
|
|
1048
|
-
const BoardFrameSchema = z.object({
|
|
1049
|
-
x: z.number().finite(),
|
|
1050
|
-
y: z.number().finite(),
|
|
1051
|
-
width: z.number().finite().positive(),
|
|
1052
|
-
height: z.number().finite().positive(),
|
|
1053
|
-
rotation: z.number().finite().default(0)
|
|
1054
|
-
});
|
|
1055
|
-
/**
|
|
1056
|
-
* The board camera. This is local UI state, not synced content: semantic ops
|
|
1057
|
-
* (see diffBoardDocuments) never describe the viewport, and the editor holds
|
|
1058
|
-
* the live camera separately from the persisted document. Here it only serves
|
|
1059
|
-
* as an initial camera hint when a document is first loaded.
|
|
1060
|
-
*/
|
|
1061
|
-
const BoardViewportSchema = z.object({
|
|
1062
|
-
x: z.number().finite(),
|
|
1063
|
-
y: z.number().finite(),
|
|
1064
|
-
zoom: z.number().finite().min(.05).max(8)
|
|
1065
|
-
});
|
|
1066
|
-
const BoardAppearanceSchema = z.object({
|
|
1067
|
-
theme: z.string().min(1).default("clean"),
|
|
1068
|
-
background: z.object({
|
|
1069
|
-
kind: z.enum([
|
|
1070
|
-
"solid",
|
|
1071
|
-
"dots",
|
|
1072
|
-
"grid",
|
|
1073
|
-
"image",
|
|
1074
|
-
"shader",
|
|
1075
|
-
"custom"
|
|
1076
|
-
]).default("dots"),
|
|
1077
|
-
color: z.string().optional(),
|
|
1078
|
-
imageUrl: BoardRemoteUrlSchema.optional(),
|
|
1079
|
-
fit: z.enum([
|
|
1080
|
-
"cover",
|
|
1081
|
-
"contain",
|
|
1082
|
-
"repeat"
|
|
1083
|
-
]).optional(),
|
|
1084
|
-
position: z.enum([
|
|
1085
|
-
"center",
|
|
1086
|
-
"top",
|
|
1087
|
-
"bottom",
|
|
1088
|
-
"left",
|
|
1089
|
-
"right"
|
|
1090
|
-
]).optional(),
|
|
1091
|
-
opacity: z.number().finite().min(0).max(1).optional()
|
|
1092
|
-
}).default({ kind: "solid" }),
|
|
1093
|
-
grid: z.object({
|
|
1094
|
-
visible: z.boolean().default(false),
|
|
1095
|
-
size: z.number().finite().min(4).default(24),
|
|
1096
|
-
opacity: z.number().finite().min(0).max(1).default(.12)
|
|
1097
|
-
}).default({
|
|
1098
|
-
visible: false,
|
|
1099
|
-
size: 24,
|
|
1100
|
-
opacity: .12
|
|
1101
|
-
}),
|
|
1102
|
-
mood: z.enum([
|
|
1103
|
-
"clean",
|
|
1104
|
-
"playful",
|
|
1105
|
-
"arcane",
|
|
1106
|
-
"cyber",
|
|
1107
|
-
"natural"
|
|
1108
|
-
]).default("clean")
|
|
1109
|
-
});
|
|
1110
|
-
/** Optional visual chrome still carried by a few older shapes. */
|
|
1111
|
-
const BoardItemStyleSchema = z.object({
|
|
1112
|
-
variant: z.string().min(1).default("default"),
|
|
1113
|
-
theme: z.string().min(1).optional(),
|
|
1114
|
-
accentColor: z.string().optional(),
|
|
1115
|
-
size: z.enum([
|
|
1116
|
-
"sm",
|
|
1117
|
-
"md",
|
|
1118
|
-
"lg"
|
|
1119
|
-
]).default("md"),
|
|
1120
|
-
emphasis: z.enum([
|
|
1121
|
-
"normal",
|
|
1122
|
-
"rare",
|
|
1123
|
-
"epic",
|
|
1124
|
-
"legendary"
|
|
1125
|
-
]).default("normal"),
|
|
1126
|
-
effects: z.array(z.string().min(1)).default([])
|
|
1127
|
-
});
|
|
1128
|
-
const SpaceFileRefSchema = z.object({
|
|
1129
|
-
kind: z.literal("space-file"),
|
|
1130
|
-
path: z.string().min(1)
|
|
1131
|
-
});
|
|
1132
|
-
const BoardMediaSnapshotSchema = z.object({
|
|
1133
|
-
title: z.string().optional(),
|
|
1134
|
-
mimeType: z.string().optional(),
|
|
1135
|
-
size: z.number().finite().nonnegative().optional(),
|
|
1136
|
-
mtimeMs: z.number().finite().nonnegative().optional(),
|
|
1137
|
-
/** Intrinsic pixel size once known. */
|
|
1138
|
-
naturalWidth: z.number().finite().positive().optional(),
|
|
1139
|
-
naturalHeight: z.number().finite().positive().optional()
|
|
1140
|
-
});
|
|
1141
|
-
const BoardItemBaseSchema = z.object({
|
|
1142
|
-
id: z.string().min(1),
|
|
1143
|
-
frame: BoardFrameSchema,
|
|
1144
|
-
/** When true the shape cannot be moved, resized, or deleted. */
|
|
1145
|
-
locked: z.boolean().optional(),
|
|
1146
|
-
style: BoardItemStyleSchema.optional(),
|
|
1147
|
-
metadata: z.record(z.string(), z.unknown()).optional()
|
|
1148
|
-
});
|
|
1149
|
-
/** Freestanding text — no card chrome; its frame always follows the glyphs. */
|
|
1150
|
-
const BoardTextItemSchema = BoardItemBaseSchema.extend({
|
|
1151
|
-
type: z.literal("text"),
|
|
1152
|
-
text: z.string().default(""),
|
|
1153
|
-
color: z.string().min(1).default("neutral"),
|
|
1154
|
-
fontSize: z.number().finite().min(2).max(512).default(24)
|
|
1155
|
-
});
|
|
1156
|
-
const BoardGeoItemSchema = BoardItemBaseSchema.extend({
|
|
1157
|
-
type: z.literal("geo"),
|
|
1158
|
-
geo: z.string().min(1).default("rectangle"),
|
|
1159
|
-
text: z.string().default(""),
|
|
1160
|
-
color: z.string().min(1).default("brand"),
|
|
1161
|
-
fillOpacity: z.number().finite().min(0).max(1).default(0)
|
|
1162
|
-
});
|
|
1163
|
-
/** A raw freehand sample. Pressure defaults to 0.5 (mouse) when absent. */
|
|
1164
|
-
const DrawPointSchema = z.object({
|
|
1165
|
-
x: z.number().finite(),
|
|
1166
|
-
y: z.number().finite(),
|
|
1167
|
-
p: z.number().finite().min(0).max(1).default(.5)
|
|
1168
|
-
});
|
|
1169
|
-
/** A world-space point. */
|
|
1170
|
-
const BoardPointSchema = z.object({
|
|
1171
|
-
x: z.number().finite(),
|
|
1172
|
-
y: z.number().finite()
|
|
1173
|
-
});
|
|
1174
|
-
const BoardDrawItemSchema = BoardItemBaseSchema.extend({
|
|
1175
|
-
type: z.literal("draw"),
|
|
1176
|
-
points: z.array(DrawPointSchema).default([]),
|
|
1177
|
-
color: z.string().min(1).default("brand"),
|
|
1178
|
-
size: z.number().finite().positive().default(4)
|
|
1179
|
-
});
|
|
1180
|
-
/**
|
|
1181
|
-
* A free arrow — a standalone annotation stroke between two world points.
|
|
1182
|
-
*
|
|
1183
|
-
* Arrows do not relate nodes. A relation between two nodes is a
|
|
1184
|
-
* `BoardConnection`, which is stored separately and resolves its geometry from
|
|
1185
|
-
* the live node frames. Keeping the two apart is what lets an arrow be a plain
|
|
1186
|
-
* shape (its own frame, freely movable) while a connection stays purely
|
|
1187
|
-
* semantic — neither has to pretend to be the other.
|
|
1188
|
-
*/
|
|
1189
|
-
const BoardArrowItemSchema = BoardItemBaseSchema.extend({
|
|
1190
|
-
type: z.literal("arrow"),
|
|
1191
|
-
start: BoardPointSchema,
|
|
1192
|
-
end: BoardPointSchema,
|
|
1193
|
-
bend: z.number().finite().min(-.85).max(.85).default(0),
|
|
1194
|
-
color: z.string().min(1).default("brand"),
|
|
1195
|
-
size: z.number().finite().positive().default(BOARD_ARROW_STROKE_SIZE),
|
|
1196
|
-
arrowStart: z.boolean().default(false),
|
|
1197
|
-
arrowEnd: z.boolean().default(true),
|
|
1198
|
-
label: z.string().default("")
|
|
1199
|
-
});
|
|
1200
|
-
/** A frame container for organising shapes. */
|
|
1201
|
-
const BoardFrameItemSchema = BoardItemBaseSchema.extend({
|
|
1202
|
-
type: z.literal("frame"),
|
|
1203
|
-
label: z.string().default("Frame"),
|
|
1204
|
-
color: z.string().min(1).default("neutral")
|
|
1205
|
-
});
|
|
1206
|
-
/** Image node — space file only, natural aspect, no chrome. */
|
|
1207
|
-
const BoardImageItemSchema = BoardItemBaseSchema.extend({
|
|
1208
|
-
type: z.literal("image"),
|
|
1209
|
-
ref: SpaceFileRefSchema,
|
|
1210
|
-
snapshot: BoardMediaSnapshotSchema.optional(),
|
|
1211
|
-
/** Optional normalized crop in source image space (0..1). */
|
|
1212
|
-
crop: z.object({
|
|
1213
|
-
x: z.number().finite().min(0).max(1).default(0),
|
|
1214
|
-
y: z.number().finite().min(0).max(1).default(0),
|
|
1215
|
-
w: z.number().finite().min(0).max(1).default(1),
|
|
1216
|
-
h: z.number().finite().min(0).max(1).default(1)
|
|
1217
|
-
}).optional()
|
|
1218
|
-
});
|
|
1219
|
-
/** Video node — space file only. Playback state is local UI, never synced. */
|
|
1220
|
-
const BoardVideoItemSchema = BoardItemBaseSchema.extend({
|
|
1221
|
-
type: z.literal("video"),
|
|
1222
|
-
ref: SpaceFileRefSchema,
|
|
1223
|
-
snapshot: BoardMediaSnapshotSchema.optional()
|
|
1224
|
-
});
|
|
1225
|
-
/** Audio node — space file only. Playback state is local UI, never synced. */
|
|
1226
|
-
const BoardAudioItemSchema = BoardItemBaseSchema.extend({
|
|
1227
|
-
type: z.literal("audio"),
|
|
1228
|
-
ref: SpaceFileRefSchema,
|
|
1229
|
-
snapshot: BoardMediaSnapshotSchema.extend({ durationMs: z.number().finite().nonnegative().optional() }).optional()
|
|
1230
|
-
});
|
|
1231
|
-
/**
|
|
1232
|
-
* Cached display facts for a file card.
|
|
1233
|
-
*
|
|
1234
|
-
* Purely derived from the referenced file and versioned by `mtimeMs`, so it is a
|
|
1235
|
-
* cache and never a second source of truth: the workspace file remains
|
|
1236
|
-
* authoritative, and a stale snapshot is detectable rather than silently wrong.
|
|
1237
|
-
* It is stored so that opening a board renders complete cards immediately, with
|
|
1238
|
-
* no per-node file read on the first paint.
|
|
1239
|
-
*/
|
|
1240
|
-
const BoardFileSnapshotSchema = z.object({
|
|
1241
|
-
title: z.string().optional(),
|
|
1242
|
-
mimeType: z.string().optional(),
|
|
1243
|
-
size: z.number().finite().nonnegative().optional(),
|
|
1244
|
-
mtimeMs: z.number().finite().nonnegative().optional(),
|
|
1245
|
-
/** Cleaned leading prose. Capped when written (see FILE_EXCERPT_MAX_CHARS). */
|
|
1246
|
-
excerpt: z.string().optional(),
|
|
1247
|
-
/** Cover image inside the space, already resolved to a workspace path. */
|
|
1248
|
-
coverPath: z.string().optional(),
|
|
1249
|
-
/** Cover image at an absolute https URL, as declared by the file itself. */
|
|
1250
|
-
coverUrl: z.string().optional()
|
|
1251
|
-
});
|
|
1252
|
-
/**
|
|
1253
|
-
* File node — a thumbnail entry point to any workspace file.
|
|
1254
|
-
*
|
|
1255
|
-
* This is the fallback for every dropped file that is not natively an image or
|
|
1256
|
-
* video, including binaries and unknown extensions: a board should never refuse
|
|
1257
|
-
* a file, only present it with less detail. Presentation tiers are derived from
|
|
1258
|
-
* the snapshot (see filePreviewKind), not stored, so there is no display state
|
|
1259
|
-
* to drift from the facts.
|
|
1260
|
-
*/
|
|
1261
|
-
const BoardFileItemSchema = BoardItemBaseSchema.extend({
|
|
1262
|
-
type: z.literal("file"),
|
|
1263
|
-
ref: SpaceFileRefSchema,
|
|
1264
|
-
snapshot: BoardFileSnapshotSchema.optional()
|
|
1265
|
-
});
|
|
1266
|
-
const BoardTaskMediaArtifactFields = {
|
|
1267
|
-
id: z.string().min(1).max(240),
|
|
1268
|
-
title: z.string().max(240).optional(),
|
|
1269
|
-
url: BoardRemoteUrlSchema,
|
|
1270
|
-
mimeType: z.string().max(160).optional()
|
|
1271
|
-
};
|
|
1272
|
-
const BoardTaskArtifactSchema = z.discriminatedUnion("type", [
|
|
1273
|
-
z.object({
|
|
1274
|
-
...BoardTaskMediaArtifactFields,
|
|
1275
|
-
type: z.literal("image"),
|
|
1276
|
-
naturalWidth: z.number().positive().optional(),
|
|
1277
|
-
naturalHeight: z.number().positive().optional()
|
|
1278
|
-
}).strict(),
|
|
1279
|
-
z.object({
|
|
1280
|
-
...BoardTaskMediaArtifactFields,
|
|
1281
|
-
type: z.literal("video"),
|
|
1282
|
-
previewUrl: BoardRemoteUrlSchema.optional(),
|
|
1283
|
-
durationMs: z.number().int().positive().optional(),
|
|
1284
|
-
naturalWidth: z.number().positive().optional(),
|
|
1285
|
-
naturalHeight: z.number().positive().optional()
|
|
1286
|
-
}).strict(),
|
|
1287
|
-
z.object({
|
|
1288
|
-
...BoardTaskMediaArtifactFields,
|
|
1289
|
-
type: z.literal("audio"),
|
|
1290
|
-
previewUrl: BoardRemoteUrlSchema.optional(),
|
|
1291
|
-
durationMs: z.number().int().positive().optional()
|
|
1292
|
-
}).strict(),
|
|
1293
|
-
z.object({
|
|
1294
|
-
id: z.string().min(1).max(240),
|
|
1295
|
-
type: z.literal("text"),
|
|
1296
|
-
title: z.string().max(240).optional(),
|
|
1297
|
-
textExcerpt: z.string().min(1).max(480)
|
|
1298
|
-
}).strict()
|
|
1299
|
-
]);
|
|
1300
|
-
/**
|
|
1301
|
-
* Cached task facts used for an immediate first paint. The task run remains the
|
|
1302
|
-
* source of truth and live clients refresh this projection by `taskRunId`.
|
|
1303
|
-
*/
|
|
1304
|
-
const BoardTaskSnapshotSchema = z.object({
|
|
1305
|
-
taskType: z.string().min(1).max(120),
|
|
1306
|
-
status: z.enum([
|
|
1307
|
-
"pending",
|
|
1308
|
-
"running",
|
|
1309
|
-
"completed",
|
|
1310
|
-
"failed"
|
|
1311
|
-
]),
|
|
1312
|
-
title: z.string().min(1).max(240),
|
|
1313
|
-
model: z.string().max(160).optional(),
|
|
1314
|
-
promptExcerpt: z.string().max(480).optional(),
|
|
1315
|
-
artifactCount: z.number().int().nonnegative(),
|
|
1316
|
-
artifacts: z.array(BoardTaskArtifactSchema).max(6).default([]),
|
|
1317
|
-
updatedAt: z.string().optional()
|
|
1318
|
-
}).strict();
|
|
1319
|
-
/** A stable reference to a task run with a small, replaceable display cache. */
|
|
1320
|
-
const BoardTaskItemSchema = BoardItemBaseSchema.extend({
|
|
1321
|
-
type: z.literal("task"),
|
|
1322
|
-
taskRunId: z.string().min(1),
|
|
1323
|
-
snapshot: BoardTaskSnapshotSchema
|
|
1324
|
-
});
|
|
1325
|
-
/**
|
|
1326
|
-
* A forward-compatible carrier for shape types this client does not recognise.
|
|
1327
|
-
* Its discriminant is the literal `"unknown"` so the item union still narrows
|
|
1328
|
-
* cleanly on `type`. The real type string and every original field are preserved
|
|
1329
|
-
* verbatim in `raw`.
|
|
1330
|
-
*/
|
|
1331
|
-
const UNKNOWN_BOARD_ITEM_TYPE = "unknown";
|
|
1332
|
-
const BoardItemSchema = z.any().transform((raw) => parseBoardItemLoose(raw));
|
|
1333
|
-
/**
|
|
1334
|
-
* Parse a single item leniently: known types are validated and normalised;
|
|
1335
|
-
* anything else becomes a lossless unknown item. Never throws on shape data —
|
|
1336
|
-
* a malformed known item degrades to unknown rather than failing the document.
|
|
1337
|
-
*/
|
|
1338
|
-
function parseBoardItemLoose(raw) {
|
|
1339
|
-
if (!raw || typeof raw !== "object") return makeUnknownItem(raw);
|
|
1340
|
-
switch (raw.type) {
|
|
1341
|
-
case "image": {
|
|
1342
|
-
const parsed = BoardImageItemSchema.safeParse(raw);
|
|
1343
|
-
return parsed.success ? parsed.data : makeUnknownItem(raw);
|
|
1344
|
-
}
|
|
1345
|
-
case "video": {
|
|
1346
|
-
const parsed = BoardVideoItemSchema.safeParse(raw);
|
|
1347
|
-
return parsed.success ? parsed.data : makeUnknownItem(raw);
|
|
1348
|
-
}
|
|
1349
|
-
case "audio": {
|
|
1350
|
-
const parsed = BoardAudioItemSchema.safeParse(raw);
|
|
1351
|
-
return parsed.success ? parsed.data : makeUnknownItem(raw);
|
|
1352
|
-
}
|
|
1353
|
-
case "file": {
|
|
1354
|
-
const parsed = BoardFileItemSchema.safeParse(raw);
|
|
1355
|
-
return parsed.success ? parsed.data : makeUnknownItem(raw);
|
|
1356
|
-
}
|
|
1357
|
-
case "task": {
|
|
1358
|
-
const parsed = BoardTaskItemSchema.safeParse(raw);
|
|
1359
|
-
return parsed.success ? parsed.data : makeUnknownItem(raw);
|
|
1360
|
-
}
|
|
1361
|
-
case "text": {
|
|
1362
|
-
const parsed = BoardTextItemSchema.safeParse(raw);
|
|
1363
|
-
return parsed.success ? parsed.data : makeUnknownItem(raw);
|
|
1364
|
-
}
|
|
1365
|
-
case "geo": {
|
|
1366
|
-
const parsed = BoardGeoItemSchema.safeParse(raw);
|
|
1367
|
-
return parsed.success ? parsed.data : makeUnknownItem(raw);
|
|
1368
|
-
}
|
|
1369
|
-
case "draw": {
|
|
1370
|
-
const parsed = BoardDrawItemSchema.safeParse(raw);
|
|
1371
|
-
return parsed.success ? parsed.data : makeUnknownItem(raw);
|
|
1372
|
-
}
|
|
1373
|
-
case "arrow": {
|
|
1374
|
-
const parsed = BoardArrowItemSchema.safeParse(raw);
|
|
1375
|
-
return parsed.success ? parsed.data : makeUnknownItem(raw);
|
|
1376
|
-
}
|
|
1377
|
-
case "frame": {
|
|
1378
|
-
const parsed = BoardFrameItemSchema.safeParse(raw);
|
|
1379
|
-
return parsed.success ? parsed.data : makeUnknownItem(raw);
|
|
1380
|
-
}
|
|
1381
|
-
default: return makeUnknownItem(raw);
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
function makeUnknownItem(raw) {
|
|
1385
|
-
const record = raw && typeof raw === "object" ? raw : {};
|
|
1386
|
-
const frameParsed = BoardFrameSchema.safeParse(record.frame);
|
|
1387
|
-
const styleParsed = BoardItemStyleSchema.safeParse(record.style);
|
|
1388
|
-
return {
|
|
1389
|
-
id: typeof record.id === "string" && record.id ? record.id : "unknown",
|
|
1390
|
-
type: UNKNOWN_BOARD_ITEM_TYPE,
|
|
1391
|
-
frame: frameParsed.success ? frameParsed.data : {
|
|
1392
|
-
x: 0,
|
|
1393
|
-
y: 0,
|
|
1394
|
-
width: 120,
|
|
1395
|
-
height: 80,
|
|
1396
|
-
rotation: 0
|
|
1397
|
-
},
|
|
1398
|
-
...record.locked === true ? { locked: true } : {},
|
|
1399
|
-
...styleParsed.success && record.style ? { style: styleParsed.data } : {},
|
|
1400
|
-
...record.metadata && typeof record.metadata === "object" ? { metadata: record.metadata } : {},
|
|
1401
|
-
raw: record
|
|
1402
|
-
};
|
|
1403
|
-
}
|
|
1404
|
-
z.object({
|
|
1405
|
-
kind: z.literal(BOARD_DOCUMENT_KIND),
|
|
1406
|
-
version: z.literal(1),
|
|
1407
|
-
appearance: BoardAppearanceSchema.default({
|
|
1408
|
-
theme: "clean",
|
|
1409
|
-
background: { kind: "solid" },
|
|
1410
|
-
grid: {
|
|
1411
|
-
visible: false,
|
|
1412
|
-
size: 24,
|
|
1413
|
-
opacity: .12
|
|
1414
|
-
},
|
|
1415
|
-
mood: "clean"
|
|
1416
|
-
}),
|
|
1417
|
-
viewport: BoardViewportSchema,
|
|
1418
|
-
items: z.array(BoardItemSchema),
|
|
1419
|
-
/**
|
|
1420
|
-
* Node relations. Separate from `items` because a connection has no frame of
|
|
1421
|
-
* its own — its geometry is derived from the nodes it joins, so it is a
|
|
1422
|
-
* relation over the item set rather than a member of it.
|
|
1423
|
-
*
|
|
1424
|
-
* Connections referencing a missing node are dropped on parse: a relation to
|
|
1425
|
-
* nothing is not a relation, and keeping one would let an invisible dangling
|
|
1426
|
-
* edge accumulate silently. Callers that need to know write through the
|
|
1427
|
-
* transaction API, which reports the reference error instead.
|
|
1428
|
-
*/
|
|
1429
|
-
connections: z.array(BoardConnectionSchema).default([])
|
|
997
|
+
"screen"
|
|
998
|
+
]).default("front"),
|
|
999
|
+
seed: z.string().min(1).max(160),
|
|
1000
|
+
params: jsonObjectSchema$1.default({}),
|
|
1001
|
+
assetRefs: z.array(BoardAssetRefSchema).default([]),
|
|
1002
|
+
metadata: jsonObjectSchema$1.default({}),
|
|
1003
|
+
revision: z.number().int().nonnegative()
|
|
1004
|
+
}).strict();
|
|
1005
|
+
/** The client-authored effect form: no server-owned `boardId`/`revision`. */
|
|
1006
|
+
const BoardEffectInputSchema = BoardEffectSchema.omit({
|
|
1007
|
+
boardId: true,
|
|
1008
|
+
revision: true
|
|
1430
1009
|
});
|
|
1010
|
+
/** Accept a read projection in a write command while stripping server fields. */
|
|
1011
|
+
function parseBoardEffectInput(value) {
|
|
1012
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1013
|
+
const { boardId: _boardId, revision: _revision, ...input } = value;
|
|
1014
|
+
return BoardEffectInputSchema.parse(input);
|
|
1015
|
+
}
|
|
1016
|
+
return BoardEffectInputSchema.parse(value);
|
|
1017
|
+
}
|
|
1431
1018
|
//#endregion
|
|
1432
|
-
//#region ../protocol/dist/board-
|
|
1433
|
-
const
|
|
1019
|
+
//#region ../protocol/dist/board-authoring.js
|
|
1020
|
+
const BOARD_AUTHORING_COLOR_IDS = [
|
|
1434
1021
|
"brand",
|
|
1435
1022
|
"neutral",
|
|
1436
1023
|
"black",
|
|
@@ -1441,417 +1028,273 @@ const BOARD_COLOR_IDS = [
|
|
|
1441
1028
|
"violet",
|
|
1442
1029
|
"rose"
|
|
1443
1030
|
];
|
|
1444
|
-
const
|
|
1445
|
-
const BOARD_GEO_KINDS = [
|
|
1031
|
+
const BOARD_AUTHORING_GEO_KINDS = [
|
|
1446
1032
|
"rectangle",
|
|
1447
1033
|
"rounded",
|
|
1448
1034
|
"ellipse",
|
|
1449
1035
|
"diamond",
|
|
1450
1036
|
"triangle"
|
|
1451
1037
|
];
|
|
1452
|
-
const
|
|
1453
|
-
const
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
"frame"
|
|
1464
|
-
];
|
|
1465
|
-
const metadataSchema = z.record(z.string(), z.unknown());
|
|
1466
|
-
const commonData = {
|
|
1467
|
-
locked: z.boolean().optional(),
|
|
1468
|
-
metadata: metadataSchema.optional()
|
|
1469
|
-
};
|
|
1038
|
+
const idSchema = z.string().min(1).max(160);
|
|
1039
|
+
const jsonObjectSchema = z.record(z.string(), z.unknown());
|
|
1040
|
+
const finiteSchema = z.number().finite();
|
|
1041
|
+
const extensionTypeSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/).max(160);
|
|
1042
|
+
const BoardAuthoringFrameSchema = z.object({
|
|
1043
|
+
x: finiteSchema,
|
|
1044
|
+
y: finiteSchema,
|
|
1045
|
+
width: finiteSchema.positive(),
|
|
1046
|
+
height: finiteSchema.positive(),
|
|
1047
|
+
rotation: finiteSchema.default(0)
|
|
1048
|
+
}).strict();
|
|
1470
1049
|
const pointSchema = z.object({
|
|
1471
|
-
x:
|
|
1472
|
-
y:
|
|
1473
|
-
p:
|
|
1050
|
+
x: finiteSchema,
|
|
1051
|
+
y: finiteSchema,
|
|
1052
|
+
p: finiteSchema.min(0).max(1).default(.5)
|
|
1474
1053
|
}).strict();
|
|
1475
1054
|
const worldPointSchema = z.object({
|
|
1476
|
-
x:
|
|
1477
|
-
y:
|
|
1055
|
+
x: finiteSchema,
|
|
1056
|
+
y: finiteSchema
|
|
1478
1057
|
}).strict();
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
mtimeMs: z.number().finite().nonnegative().optional(),
|
|
1485
|
-
naturalWidth: z.number().finite().positive().optional(),
|
|
1486
|
-
naturalHeight: z.number().finite().positive().optional()
|
|
1058
|
+
const cropSchema = z.object({
|
|
1059
|
+
x: finiteSchema.min(0).max(1),
|
|
1060
|
+
y: finiteSchema.min(0).max(1),
|
|
1061
|
+
w: finiteSchema.min(0).max(1),
|
|
1062
|
+
h: finiteSchema.min(0).max(1)
|
|
1487
1063
|
}).strict();
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1064
|
+
const BoardItemStyleSchema = z.object({
|
|
1065
|
+
color: z.enum(BOARD_AUTHORING_COLOR_IDS).optional(),
|
|
1066
|
+
strokeWidth: finiteSchema.min(1).max(64).optional(),
|
|
1067
|
+
fillOpacity: finiteSchema.min(0).max(1).optional()
|
|
1068
|
+
}).strict();
|
|
1069
|
+
const sourceSnapshotSchema = z.record(z.string(), z.unknown());
|
|
1070
|
+
function isSafeBoardSourcePath(value) {
|
|
1071
|
+
return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !value.startsWith("\\") && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
1072
|
+
}
|
|
1073
|
+
const BoardItemSourceSchema = z.object({
|
|
1074
|
+
kind: z.literal("space-file"),
|
|
1075
|
+
path: z.string().refine(isSafeBoardSourcePath, "source path must be a safe relative Board file path"),
|
|
1076
|
+
snapshot: sourceSnapshotSchema.optional()
|
|
1077
|
+
}).strict();
|
|
1078
|
+
const baseFields = {
|
|
1079
|
+
id: idSchema,
|
|
1080
|
+
frame: BoardAuthoringFrameSchema,
|
|
1081
|
+
parentId: idSchema.nullable().optional(),
|
|
1082
|
+
locked: z.boolean().optional(),
|
|
1083
|
+
metadata: jsonObjectSchema.optional()
|
|
1084
|
+
};
|
|
1085
|
+
const styledBaseFields = {
|
|
1086
|
+
...baseFields,
|
|
1087
|
+
style: BoardItemStyleSchema.optional()
|
|
1088
|
+
};
|
|
1089
|
+
const BoardTextAuthoringItemSchema = z.object({
|
|
1090
|
+
...styledBaseFields,
|
|
1091
|
+
type: z.literal("text"),
|
|
1092
|
+
props: z.object({
|
|
1499
1093
|
text: z.string().default(""),
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1094
|
+
fontSize: finiteSchema.min(2).max(512).default(24)
|
|
1095
|
+
}).strict()
|
|
1096
|
+
}).strict();
|
|
1097
|
+
const BoardGeoAuthoringItemSchema = z.object({
|
|
1098
|
+
...styledBaseFields,
|
|
1099
|
+
type: z.literal("geo"),
|
|
1100
|
+
props: z.object({
|
|
1101
|
+
shape: z.enum(BOARD_AUTHORING_GEO_KINDS).default("rectangle"),
|
|
1102
|
+
text: z.string().default("")
|
|
1103
|
+
}).strict()
|
|
1104
|
+
}).strict();
|
|
1105
|
+
const BoardDrawAuthoringItemSchema = z.object({
|
|
1106
|
+
...styledBaseFields,
|
|
1107
|
+
type: z.literal("draw"),
|
|
1108
|
+
props: z.object({ points: z.array(pointSchema).min(1) }).strict()
|
|
1109
|
+
}).strict();
|
|
1110
|
+
const BoardArrowAuthoringItemSchema = z.object({
|
|
1111
|
+
...styledBaseFields,
|
|
1112
|
+
type: z.literal("arrow"),
|
|
1113
|
+
props: z.object({
|
|
1511
1114
|
start: worldPointSchema,
|
|
1512
1115
|
end: worldPointSchema,
|
|
1513
|
-
bend:
|
|
1514
|
-
color: BoardColorIdSchema.default("brand"),
|
|
1515
|
-
size: strokeSizeSchema.default(BOARD_ARROW_STROKE_SIZE),
|
|
1116
|
+
bend: finiteSchema.min(-.85).max(.85).default(0),
|
|
1516
1117
|
arrowStart: z.boolean().default(false),
|
|
1517
1118
|
arrowEnd: z.boolean().default(true),
|
|
1518
1119
|
label: z.string().default("")
|
|
1120
|
+
}).strict()
|
|
1121
|
+
}).strict();
|
|
1122
|
+
const BoardFrameAuthoringItemSchema = z.object({
|
|
1123
|
+
...styledBaseFields,
|
|
1124
|
+
type: z.literal("frame"),
|
|
1125
|
+
props: z.object({ label: z.string().default("Frame") }).strict()
|
|
1126
|
+
}).strict();
|
|
1127
|
+
const fileBackedFields = {
|
|
1128
|
+
...baseFields,
|
|
1129
|
+
style: z.object({}).strict().optional(),
|
|
1130
|
+
source: BoardItemSourceSchema
|
|
1131
|
+
};
|
|
1132
|
+
const builtinItemSchemas = [
|
|
1133
|
+
BoardTextAuthoringItemSchema,
|
|
1134
|
+
BoardGeoAuthoringItemSchema,
|
|
1135
|
+
BoardDrawAuthoringItemSchema,
|
|
1136
|
+
BoardArrowAuthoringItemSchema,
|
|
1137
|
+
BoardFrameAuthoringItemSchema,
|
|
1138
|
+
z.object({
|
|
1139
|
+
...fileBackedFields,
|
|
1140
|
+
type: z.literal("image"),
|
|
1141
|
+
props: z.object({ crop: cropSchema.optional() }).strict()
|
|
1142
|
+
}).strict(),
|
|
1143
|
+
z.object({
|
|
1144
|
+
...fileBackedFields,
|
|
1145
|
+
type: z.literal("video"),
|
|
1146
|
+
props: z.object({}).strict()
|
|
1519
1147
|
}).strict(),
|
|
1520
|
-
|
|
1521
|
-
...
|
|
1522
|
-
|
|
1523
|
-
|
|
1148
|
+
z.object({
|
|
1149
|
+
...fileBackedFields,
|
|
1150
|
+
type: z.literal("audio"),
|
|
1151
|
+
props: z.object({}).strict()
|
|
1524
1152
|
}).strict(),
|
|
1525
|
-
|
|
1526
|
-
...
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
y: z.number().finite().min(0).max(1),
|
|
1530
|
-
w: z.number().finite().min(0).max(1),
|
|
1531
|
-
h: z.number().finite().min(0).max(1)
|
|
1532
|
-
}).strict().optional()
|
|
1153
|
+
z.object({
|
|
1154
|
+
...fileBackedFields,
|
|
1155
|
+
type: z.literal("file"),
|
|
1156
|
+
props: z.object({}).strict()
|
|
1533
1157
|
}).strict(),
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1158
|
+
z.object({
|
|
1159
|
+
...baseFields,
|
|
1160
|
+
type: z.literal("task"),
|
|
1161
|
+
props: z.object({
|
|
1162
|
+
taskRunId: z.string().min(1),
|
|
1163
|
+
snapshot: z.record(z.string(), z.unknown())
|
|
1164
|
+
}).strict(),
|
|
1165
|
+
style: z.object({}).strict().optional()
|
|
1540
1166
|
}).strict()
|
|
1541
|
-
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1167
|
+
];
|
|
1168
|
+
const BoardBuiltinAuthoringItemSchema = z.discriminatedUnion("type", builtinItemSchemas);
|
|
1169
|
+
const BoardExtensionAuthoringItemSchema = z.object({
|
|
1170
|
+
...baseFields,
|
|
1171
|
+
type: extensionTypeSchema,
|
|
1172
|
+
kindVersion: z.number().int().positive(),
|
|
1173
|
+
props: jsonObjectSchema,
|
|
1174
|
+
style: jsonObjectSchema.optional(),
|
|
1175
|
+
source: z.object({
|
|
1176
|
+
kind: z.string().min(1).max(80),
|
|
1177
|
+
ref: z.string().min(1).max(4096),
|
|
1178
|
+
snapshot: jsonObjectSchema.optional()
|
|
1179
|
+
}).strict().optional()
|
|
1550
1180
|
}).strict();
|
|
1551
|
-
const
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
task: taskViewSchema,
|
|
1559
|
-
text: emptyViewSchema,
|
|
1560
|
-
geo: emptyViewSchema,
|
|
1561
|
-
draw: emptyViewSchema,
|
|
1562
|
-
arrow: emptyViewSchema,
|
|
1563
|
-
frame: emptyViewSchema
|
|
1564
|
-
};
|
|
1565
|
-
const nodeEnvelopeSchema = z.object({
|
|
1566
|
-
nodeId: z.string().min(1).max(160),
|
|
1567
|
-
type: z.string().min(1).max(40),
|
|
1568
|
-
parentId: z.string().min(1).max(160).nullable(),
|
|
1569
|
-
orderKey: z.string().max(4096).nullable(),
|
|
1570
|
-
x: z.number().finite(),
|
|
1571
|
-
y: z.number().finite(),
|
|
1572
|
-
width: z.number().finite().positive(),
|
|
1573
|
-
height: z.number().finite().positive(),
|
|
1574
|
-
rotation: z.number().finite(),
|
|
1575
|
-
refKind: z.string().max(40).nullable(),
|
|
1576
|
-
refPath: z.string().max(4096).nullable(),
|
|
1577
|
-
refUrl: z.string().max(4096).nullable(),
|
|
1578
|
-
view: z.record(z.string(), z.unknown()),
|
|
1579
|
-
style: z.record(z.string(), z.unknown()),
|
|
1580
|
-
data: z.record(z.string(), z.unknown())
|
|
1181
|
+
const BoardAuthoringItemSchema = z.union([BoardBuiltinAuthoringItemSchema, BoardExtensionAuthoringItemSchema]);
|
|
1182
|
+
const framePatchSchema = z.object({
|
|
1183
|
+
x: finiteSchema.optional(),
|
|
1184
|
+
y: finiteSchema.optional(),
|
|
1185
|
+
width: finiteSchema.positive().optional(),
|
|
1186
|
+
height: finiteSchema.positive().optional(),
|
|
1187
|
+
rotation: finiteSchema.optional()
|
|
1581
1188
|
}).strict();
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
"video",
|
|
1598
|
-
"audio",
|
|
1599
|
-
"file"
|
|
1600
|
-
],
|
|
1601
|
-
kind: "space_file",
|
|
1602
|
-
pathField: "refPath"
|
|
1603
|
-
},
|
|
1604
|
-
schemas: {
|
|
1605
|
-
envelope: jsonSchema(nodeEnvelopeSchema),
|
|
1606
|
-
data: Object.fromEntries(BOARD_NATIVE_NODE_TYPES.map((type) => [type, jsonSchema(dataSchemas[type])])),
|
|
1607
|
-
view: Object.fromEntries(BOARD_NATIVE_NODE_TYPES.map((type) => [type, jsonSchema(viewSchemas[type])]))
|
|
1608
|
-
}
|
|
1189
|
+
/** JSON Merge Patch semantics, constrained to the stable Item envelope. */
|
|
1190
|
+
const BoardItemPatchSchema = z.object({
|
|
1191
|
+
frame: framePatchSchema.optional(),
|
|
1192
|
+
parentId: idSchema.nullable().optional(),
|
|
1193
|
+
locked: z.boolean().nullable().optional(),
|
|
1194
|
+
props: jsonObjectSchema.optional(),
|
|
1195
|
+
style: jsonObjectSchema.nullable().optional(),
|
|
1196
|
+
source: jsonObjectSchema.nullable().optional(),
|
|
1197
|
+
metadata: jsonObjectSchema.nullable().optional()
|
|
1198
|
+
}).strict().refine((patch) => Object.keys(patch).length > 0, "item patch is empty");
|
|
1199
|
+
const BoardConnectionInputSchema = BoardConnectionSchema;
|
|
1200
|
+
const stripServerFields = (value) => {
|
|
1201
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1202
|
+
const { boardId: _boardId, revision: _revision, ...input } = value;
|
|
1203
|
+
return input;
|
|
1609
1204
|
};
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
return {
|
|
1614
|
-
severity: "error",
|
|
1615
|
-
code: "INVALID_BOARD_NODE",
|
|
1616
|
-
message: `${fullPath}: ${issue.message}`,
|
|
1617
|
-
path: fullPath,
|
|
1618
|
-
...values?.length ? { allowedValues: values } : {}
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
function drawGeometryDiagnostic(node, data, path) {
|
|
1622
|
-
let minX = Number.POSITIVE_INFINITY;
|
|
1623
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
1624
|
-
let maxX = Number.NEGATIVE_INFINITY;
|
|
1625
|
-
let maxY = Number.NEGATIVE_INFINITY;
|
|
1626
|
-
for (const point of data.points) {
|
|
1627
|
-
const radius = Math.max(.5, data.size / 2 * (.5 + point.p));
|
|
1628
|
-
minX = Math.min(minX, point.x - radius);
|
|
1629
|
-
minY = Math.min(minY, point.y - radius);
|
|
1630
|
-
maxX = Math.max(maxX, point.x + radius);
|
|
1631
|
-
maxY = Math.max(maxY, point.y + radius);
|
|
1632
|
-
}
|
|
1633
|
-
const width = Math.max(1, maxX - minX);
|
|
1634
|
-
const height = Math.max(1, maxY - minY);
|
|
1635
|
-
const tolerance = Math.max(.01, node.width * 1e-6, node.height * 1e-6);
|
|
1636
|
-
if (Math.abs(minX) <= tolerance && Math.abs(minY) <= tolerance && Math.abs(width - node.width) <= tolerance && Math.abs(height - node.height) <= tolerance) return null;
|
|
1637
|
-
return {
|
|
1638
|
-
severity: "error",
|
|
1639
|
-
code: "INVALID_BOARD_GEOMETRY",
|
|
1640
|
-
message: `${path}.data.points must use frame-local coordinates and match the node frame`,
|
|
1641
|
-
path: `${path}.data.points`,
|
|
1642
|
-
expected: "frame-local points with bounds matching width and height",
|
|
1643
|
-
coordinateSpace: "frame-local"
|
|
1644
|
-
};
|
|
1645
|
-
}
|
|
1646
|
-
function validateBoardNodeInput(node, path = "node") {
|
|
1647
|
-
const envelopeResult = nodeEnvelopeSchema.safeParse(node);
|
|
1648
|
-
if (!envelopeResult.success) return envelopeResult.error.issues.map((issue) => issueDiagnostic(issue, path));
|
|
1649
|
-
if (typeof node.type !== "string" || !BOARD_NATIVE_NODE_TYPES.includes(node.type)) return [{
|
|
1650
|
-
severity: "error",
|
|
1651
|
-
code: "INVALID_BOARD_NODE",
|
|
1652
|
-
message: `${path}.type is not supported`,
|
|
1653
|
-
path: `${path}.type`,
|
|
1654
|
-
expected: "BoardNativeNodeType",
|
|
1655
|
-
received: node.type,
|
|
1656
|
-
allowedValues: BOARD_NATIVE_NODE_TYPES
|
|
1657
|
-
}];
|
|
1658
|
-
const type = node.type;
|
|
1659
|
-
const dataResult = dataSchemas[type].safeParse(node.data ?? {});
|
|
1660
|
-
if (!dataResult.success) return dataResult.error.issues.map((issue) => issueDiagnostic(issue, `${path}.data`));
|
|
1661
|
-
const viewResult = viewSchemas[type].safeParse(node.view ?? {});
|
|
1662
|
-
if (!viewResult.success) return viewResult.error.issues.map((issue) => issueDiagnostic(issue, `${path}.view`));
|
|
1663
|
-
if (type === "image" || type === "video" || type === "audio" || type === "file") {
|
|
1664
|
-
if (node.refKind !== "space_file" || typeof node.refPath !== "string" || !node.refPath) return [{
|
|
1665
|
-
severity: "error",
|
|
1666
|
-
code: "INVALID_BOARD_NODE",
|
|
1667
|
-
message: `${path} requires a space file reference`,
|
|
1668
|
-
path: `${path}.refPath`,
|
|
1669
|
-
expected: "non-empty refPath with refKind space_file"
|
|
1670
|
-
}];
|
|
1671
|
-
}
|
|
1672
|
-
if (type === "draw") {
|
|
1673
|
-
const diagnostic = drawGeometryDiagnostic(node, dataResult.data, path);
|
|
1674
|
-
return diagnostic ? [diagnostic] : [];
|
|
1675
|
-
}
|
|
1676
|
-
if (type === "arrow") {
|
|
1677
|
-
const data = dataResult.data;
|
|
1678
|
-
const inside = (point) => point.x >= node.x && point.x <= node.x + node.width && point.y >= node.y && point.y <= node.y + node.height;
|
|
1679
|
-
if (!inside(data.start) || !inside(data.end)) return [{
|
|
1680
|
-
severity: "error",
|
|
1681
|
-
code: "INVALID_BOARD_GEOMETRY",
|
|
1682
|
-
message: `${path}.data endpoints must be covered by the node frame`,
|
|
1683
|
-
path: `${path}.data`,
|
|
1684
|
-
expected: "world-space endpoints inside the node frame",
|
|
1685
|
-
coordinateSpace: "world"
|
|
1686
|
-
}];
|
|
1687
|
-
}
|
|
1688
|
-
return [];
|
|
1689
|
-
}
|
|
1690
|
-
//#endregion
|
|
1691
|
-
//#region ../protocol/dist/realtime/board-awareness.js
|
|
1692
|
-
const idSchema = z.string().min(1).max(160);
|
|
1693
|
-
const finiteSchema = z.number().finite();
|
|
1694
|
-
const BoardAwarenessPointSchema = z.object({
|
|
1695
|
-
x: finiteSchema,
|
|
1696
|
-
y: finiteSchema
|
|
1697
|
-
});
|
|
1698
|
-
const BoardAwarenessDrawPointSchema = BoardAwarenessPointSchema.extend({ p: finiteSchema.min(0).max(1) });
|
|
1699
|
-
const BoardAwarenessFrameSchema = z.object({
|
|
1700
|
-
x: finiteSchema,
|
|
1701
|
-
y: finiteSchema,
|
|
1702
|
-
width: finiteSchema.positive(),
|
|
1703
|
-
height: finiteSchema.positive(),
|
|
1704
|
-
rotation: finiteSchema
|
|
1705
|
-
});
|
|
1706
|
-
const BoardAwarenessNodePreviewSchema = z.object({
|
|
1707
|
-
nodeId: idSchema,
|
|
1708
|
-
frame: BoardAwarenessFrameSchema,
|
|
1709
|
-
/** Live endpoints of a free arrow being dragged. */
|
|
1710
|
-
arrow: z.object({
|
|
1711
|
-
start: BoardAwarenessPointSchema,
|
|
1712
|
-
end: BoardAwarenessPointSchema,
|
|
1713
|
-
bend: finiteSchema
|
|
1714
|
-
}).optional()
|
|
1715
|
-
});
|
|
1716
|
-
const BoardAwarenessStateUpdateSchema = z.object({
|
|
1717
|
-
type: z.literal("state"),
|
|
1718
|
-
client: z.object({ formFactor: z.enum(["desktop", "mobile"]) }).optional(),
|
|
1719
|
-
cursor: BoardAwarenessPointSchema.extend({ pointerType: z.enum([
|
|
1720
|
-
"mouse",
|
|
1721
|
-
"pen",
|
|
1722
|
-
"touch"
|
|
1723
|
-
]) }).nullable(),
|
|
1724
|
-
tool: z.string().min(1).max(40),
|
|
1725
|
-
selection: z.object({
|
|
1726
|
-
ids: z.array(idSchema).max(64),
|
|
1727
|
-
count: z.number().int().nonnegative().max(5e4),
|
|
1728
|
-
bounds: BoardAwarenessFrameSchema.nullable()
|
|
1729
|
-
}),
|
|
1730
|
-
editingId: idSchema.nullable()
|
|
1731
|
-
});
|
|
1732
|
-
const BoardAwarenessGestureSchema = z.discriminatedUnion("kind", [
|
|
1205
|
+
const BoardEffectCommandInputSchema = z.preprocess(stripServerFields, BoardEffectInputSchema);
|
|
1206
|
+
const BoardCompositionCommandInputSchema = z.preprocess(stripServerFields, BoardCompositionInputSchema);
|
|
1207
|
+
const BoardSemanticCommandSchema = z.discriminatedUnion("type", [
|
|
1733
1208
|
z.object({
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
}),
|
|
1209
|
+
type: z.literal("board.patch"),
|
|
1210
|
+
patch: z.object({
|
|
1211
|
+
title: z.string().min(1).max(255).optional(),
|
|
1212
|
+
metadata: jsonObjectSchema.nullable().optional(),
|
|
1213
|
+
metadataPatch: jsonObjectSchema.optional()
|
|
1214
|
+
}).strict().refine((patch) => Object.keys(patch).length > 0, "board patch is empty")
|
|
1215
|
+
}).strict(),
|
|
1742
1216
|
z.object({
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
start: BoardAwarenessPointSchema,
|
|
1747
|
-
current: BoardAwarenessPointSchema,
|
|
1748
|
-
color: z.string().min(1).max(64),
|
|
1749
|
-
size: finiteSchema.positive().max(256).default(BOARD_ARROW_STROKE_SIZE)
|
|
1750
|
-
}),
|
|
1217
|
+
type: z.literal("item.create"),
|
|
1218
|
+
item: BoardAuthoringItemSchema
|
|
1219
|
+
}).strict(),
|
|
1751
1220
|
z.object({
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
start: BoardAwarenessPointSchema,
|
|
1757
|
-
current: BoardAwarenessPointSchema,
|
|
1758
|
-
color: z.string().min(1).max(64),
|
|
1759
|
-
geo: z.string().min(1).max(40)
|
|
1760
|
-
}),
|
|
1221
|
+
type: z.literal("item.patch"),
|
|
1222
|
+
itemId: idSchema,
|
|
1223
|
+
patch: BoardItemPatchSchema
|
|
1224
|
+
}).strict(),
|
|
1761
1225
|
z.object({
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
current: BoardAwarenessPointSchema,
|
|
1767
|
-
color: z.string().min(1).max(64),
|
|
1768
|
-
size: finiteSchema.positive().max(256).default(BOARD_CONNECTION_STROKE_SIZE)
|
|
1769
|
-
}),
|
|
1226
|
+
type: z.literal("item.replace"),
|
|
1227
|
+
itemId: idSchema,
|
|
1228
|
+
item: BoardAuthoringItemSchema
|
|
1229
|
+
}).strict(),
|
|
1770
1230
|
z.object({
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
"resize",
|
|
1776
|
-
"rotate",
|
|
1777
|
-
"arrow",
|
|
1778
|
-
"connection"
|
|
1779
|
-
]),
|
|
1780
|
-
nodes: z.array(BoardAwarenessNodePreviewSchema).max(64),
|
|
1781
|
-
bounds: BoardAwarenessFrameSchema.nullable()
|
|
1782
|
-
})
|
|
1783
|
-
]);
|
|
1784
|
-
const BoardAwarenessUpdateSchema = z.discriminatedUnion("type", [
|
|
1785
|
-
BoardAwarenessStateUpdateSchema,
|
|
1231
|
+
type: z.literal("item.delete"),
|
|
1232
|
+
itemId: idSchema,
|
|
1233
|
+
cascade: z.boolean().default(false)
|
|
1234
|
+
}).strict(),
|
|
1786
1235
|
z.object({
|
|
1787
|
-
type: z.literal("
|
|
1788
|
-
|
|
1789
|
-
|
|
1236
|
+
type: z.literal("item.reorder"),
|
|
1237
|
+
itemId: idSchema,
|
|
1238
|
+
index: z.number().int().nonnegative()
|
|
1239
|
+
}).strict(),
|
|
1790
1240
|
z.object({
|
|
1791
|
-
type: z.literal("
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
}),
|
|
1241
|
+
type: z.literal("connection.create"),
|
|
1242
|
+
connection: BoardConnectionInputSchema
|
|
1243
|
+
}).strict(),
|
|
1795
1244
|
z.object({
|
|
1796
|
-
type: z.literal("
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
"changelog",
|
|
1821
|
-
"docs",
|
|
1822
|
-
"explore",
|
|
1823
|
-
"invite",
|
|
1824
|
-
"landing",
|
|
1825
|
-
"login",
|
|
1826
|
-
"logout",
|
|
1827
|
-
"new",
|
|
1828
|
-
"org",
|
|
1829
|
-
"pricing",
|
|
1830
|
-
"pwa",
|
|
1831
|
-
"referrals",
|
|
1832
|
-
"sessions",
|
|
1833
|
-
"settings",
|
|
1834
|
-
"spaces",
|
|
1835
|
-
"static",
|
|
1836
|
-
"teams",
|
|
1837
|
-
"trending",
|
|
1838
|
-
"u",
|
|
1839
|
-
"user",
|
|
1840
|
-
"users",
|
|
1841
|
-
"work-auth"
|
|
1245
|
+
type: z.literal("connection.patch"),
|
|
1246
|
+
connectionId: idSchema,
|
|
1247
|
+
patch: BoardConnectionPatchSchema
|
|
1248
|
+
}).strict(),
|
|
1249
|
+
z.object({
|
|
1250
|
+
type: z.literal("connection.delete"),
|
|
1251
|
+
connectionId: idSchema
|
|
1252
|
+
}).strict(),
|
|
1253
|
+
z.object({
|
|
1254
|
+
type: z.literal("effect.apply"),
|
|
1255
|
+
effect: BoardEffectCommandInputSchema
|
|
1256
|
+
}).strict(),
|
|
1257
|
+
z.object({
|
|
1258
|
+
type: z.literal("effect.delete"),
|
|
1259
|
+
effectId: idSchema
|
|
1260
|
+
}).strict(),
|
|
1261
|
+
z.object({
|
|
1262
|
+
type: z.literal("composition.apply"),
|
|
1263
|
+
composition: BoardCompositionCommandInputSchema
|
|
1264
|
+
}).strict(),
|
|
1265
|
+
z.object({
|
|
1266
|
+
type: z.literal("composition.delete"),
|
|
1267
|
+
compositionId: idSchema
|
|
1268
|
+
}).strict()
|
|
1842
1269
|
]);
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1270
|
+
const BoardSemanticMutationSchema = z.object({
|
|
1271
|
+
mutationId: idSchema,
|
|
1272
|
+
baseVersion: z.number().int().nonnegative(),
|
|
1273
|
+
clientId: idSchema.optional(),
|
|
1274
|
+
undoGroupId: idSchema.optional(),
|
|
1275
|
+
/** Validate (including server-side reference checks) without writing. */
|
|
1276
|
+
dryRun: z.boolean().default(false),
|
|
1277
|
+
commands: z.array(BoardSemanticCommandSchema).min(1).max(5e4)
|
|
1278
|
+
}).strict();
|
|
1279
|
+
z.object({
|
|
1280
|
+
include: z.array(z.enum([
|
|
1281
|
+
"items",
|
|
1282
|
+
"connections",
|
|
1283
|
+
"effects",
|
|
1284
|
+
"compositions",
|
|
1285
|
+
"playback"
|
|
1286
|
+
])).optional(),
|
|
1287
|
+
itemIds: z.array(idSchema).max(5e4).optional(),
|
|
1288
|
+
connectionIds: z.array(idSchema).max(5e4).optional(),
|
|
1289
|
+
effectIds: z.array(idSchema).max(5e4).optional(),
|
|
1290
|
+
compositionIds: z.array(idSchema).max(5e4).optional(),
|
|
1291
|
+
viewport: z.object({
|
|
1292
|
+
x: finiteSchema,
|
|
1293
|
+
y: finiteSchema,
|
|
1294
|
+
width: finiteSchema.positive(),
|
|
1295
|
+
height: finiteSchema.positive()
|
|
1296
|
+
}).optional()
|
|
1297
|
+
}).strict();
|
|
1855
1298
|
//#endregion
|
|
1856
1299
|
//#region ../protocol/dist/ui-command.js
|
|
1857
1300
|
/**
|
|
@@ -1887,14 +1330,14 @@ const parseUiCommandId = (value) => {
|
|
|
1887
1330
|
const trimmed = value.trim();
|
|
1888
1331
|
return UI_COMMAND_ID_RE.test(trimmed) ? trimmed : null;
|
|
1889
1332
|
};
|
|
1890
|
-
const isRecord$
|
|
1333
|
+
const isRecord$1 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1891
1334
|
const asTrimmed = (value) => {
|
|
1892
1335
|
if (typeof value !== "string") return null;
|
|
1893
1336
|
const trimmed = value.trim();
|
|
1894
1337
|
return trimmed ? trimmed : null;
|
|
1895
1338
|
};
|
|
1896
1339
|
const parseLaunch = (value) => {
|
|
1897
|
-
if (!isRecord$
|
|
1340
|
+
if (!isRecord$1(value)) return void 0;
|
|
1898
1341
|
const search = asTrimmed(value.search);
|
|
1899
1342
|
const hash = asTrimmed(value.hash);
|
|
1900
1343
|
if (!search && !hash) return void 0;
|
|
@@ -1912,7 +1355,7 @@ const measureUiCommandPayload = (value) => {
|
|
|
1912
1355
|
}
|
|
1913
1356
|
};
|
|
1914
1357
|
const parseUiCommand = (input) => {
|
|
1915
|
-
if (!isRecord$
|
|
1358
|
+
if (!isRecord$1(input)) return {
|
|
1916
1359
|
command: null,
|
|
1917
1360
|
error: "command must be an object"
|
|
1918
1361
|
};
|
|
@@ -1921,7 +1364,7 @@ const parseUiCommand = (input) => {
|
|
|
1921
1364
|
error: "command.type must be one of: preview.show"
|
|
1922
1365
|
};
|
|
1923
1366
|
const preview = input.preview;
|
|
1924
|
-
if (!isRecord$
|
|
1367
|
+
if (!isRecord$1(preview)) return {
|
|
1925
1368
|
command: null,
|
|
1926
1369
|
error: "command.preview is required"
|
|
1927
1370
|
};
|
|
@@ -1978,7 +1421,7 @@ const parseUiCommand = (input) => {
|
|
|
1978
1421
|
}
|
|
1979
1422
|
let request;
|
|
1980
1423
|
if (input.request !== void 0 && input.request !== null) {
|
|
1981
|
-
if (!isRecord$
|
|
1424
|
+
if (!isRecord$1(input.request)) return {
|
|
1982
1425
|
command: null,
|
|
1983
1426
|
error: "command.request must be an object"
|
|
1984
1427
|
};
|
|
@@ -2030,303 +1473,12 @@ const parseUiCommand = (input) => {
|
|
|
2030
1473
|
};
|
|
2031
1474
|
};
|
|
2032
1475
|
//#endregion
|
|
2033
|
-
//#region
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
const WORK_COMPOSER_CHIP_LABEL_MAX_LENGTH = 120;
|
|
2039
|
-
const WORK_COMPOSER_CHIP_CONTENT_MAX_BYTES = 32 * 1024;
|
|
2040
|
-
const isRecord$1 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2041
|
-
const isSurfaceEnvelope = (value) => isRecord$1(value) && value.protocol === "cohub.surface" && value.version === 1;
|
|
2042
|
-
const parseWorkSurfaceReady = (value) => {
|
|
2043
|
-
if (!isSurfaceEnvelope(value) || value.type !== "ready") return null;
|
|
2044
|
-
const methods = Array.isArray(value.methods) ? value.methods.filter((method) => typeof method === "string" && Boolean(method)) : [];
|
|
2045
|
-
return {
|
|
2046
|
-
protocol: WORK_SURFACE_PROTOCOL,
|
|
2047
|
-
version: 1,
|
|
2048
|
-
type: "ready",
|
|
2049
|
-
methods
|
|
2050
|
-
};
|
|
2051
|
-
};
|
|
2052
|
-
const parseWorkSurfaceResponse = (value) => {
|
|
2053
|
-
if (!isSurfaceEnvelope(value) || value.type !== "response") return null;
|
|
2054
|
-
if (typeof value.requestId !== "string" || !value.requestId) return null;
|
|
2055
|
-
const error = isRecord$1(value.error) ? {
|
|
2056
|
-
code: typeof value.error.code === "string" && value.error.code ? value.error.code : "surface_error",
|
|
2057
|
-
message: typeof value.error.message === "string" ? value.error.message : "Work surface call failed"
|
|
2058
|
-
} : void 0;
|
|
2059
|
-
return {
|
|
2060
|
-
protocol: WORK_SURFACE_PROTOCOL,
|
|
2061
|
-
version: 1,
|
|
2062
|
-
type: "response",
|
|
2063
|
-
requestId: value.requestId,
|
|
2064
|
-
ok: value.ok === true,
|
|
2065
|
-
...error ? { error } : {}
|
|
2066
|
-
};
|
|
2067
|
-
};
|
|
2068
|
-
const parseComposerChipKey = (value) => {
|
|
2069
|
-
if (typeof value !== "string") return null;
|
|
2070
|
-
const key = value.trim();
|
|
2071
|
-
if (!key || key.length > 80) return null;
|
|
2072
|
-
return key;
|
|
2073
|
-
};
|
|
2074
|
-
const parseWorkComposerChipSet = (value) => {
|
|
2075
|
-
if (!isSurfaceEnvelope(value) || value.type !== "composer.chip.set" || !isRecord$1(value.chip)) return null;
|
|
2076
|
-
const key = parseComposerChipKey(value.chip.key);
|
|
2077
|
-
if (!key || typeof value.chip.label !== "string" || typeof value.chip.content !== "string") return null;
|
|
2078
|
-
const label = value.chip.label.trim();
|
|
2079
|
-
if (!label || label.length > 120) return null;
|
|
2080
|
-
if (!value.chip.content.trim()) return null;
|
|
2081
|
-
if (new TextEncoder().encode(value.chip.content).length > 32768) return null;
|
|
2082
|
-
return {
|
|
2083
|
-
protocol: WORK_SURFACE_PROTOCOL,
|
|
2084
|
-
version: 1,
|
|
2085
|
-
type: "composer.chip.set",
|
|
2086
|
-
chip: {
|
|
2087
|
-
key,
|
|
2088
|
-
label,
|
|
2089
|
-
content: value.chip.content
|
|
2090
|
-
}
|
|
2091
|
-
};
|
|
2092
|
-
};
|
|
2093
|
-
const parseWorkComposerChipClear = (value) => {
|
|
2094
|
-
if (!isSurfaceEnvelope(value) || value.type !== "composer.chip.clear") return null;
|
|
2095
|
-
const key = parseComposerChipKey(value.key);
|
|
2096
|
-
return key ? {
|
|
2097
|
-
protocol: WORK_SURFACE_PROTOCOL,
|
|
2098
|
-
version: 1,
|
|
2099
|
-
type: "composer.chip.clear",
|
|
2100
|
-
key
|
|
2101
|
-
} : null;
|
|
2102
|
-
};
|
|
2103
|
-
const parseWorkSurfaceRequest = (value) => {
|
|
2104
|
-
if (!isSurfaceEnvelope(value) || value.type !== "request") return null;
|
|
2105
|
-
if (typeof value.requestId !== "string" || !value.requestId) return null;
|
|
2106
|
-
if (typeof value.method !== "string" || !value.method) return null;
|
|
2107
|
-
const commandId = parseUiCommandId(value.commandId);
|
|
2108
|
-
if (!commandId) return null;
|
|
2109
|
-
return {
|
|
2110
|
-
protocol: WORK_SURFACE_PROTOCOL,
|
|
2111
|
-
version: 1,
|
|
2112
|
-
type: "request",
|
|
2113
|
-
requestId: value.requestId,
|
|
2114
|
-
method: value.method,
|
|
2115
|
-
...value.input === void 0 ? {} : { input: value.input },
|
|
2116
|
-
commandId
|
|
2117
|
-
};
|
|
2118
|
-
};
|
|
2119
|
-
const buildWorkSurfaceReady = (methods) => ({
|
|
2120
|
-
protocol: WORK_SURFACE_PROTOCOL,
|
|
2121
|
-
version: 1,
|
|
2122
|
-
type: "ready",
|
|
2123
|
-
methods: [...methods]
|
|
2124
|
-
});
|
|
2125
|
-
const buildWorkSurfaceRequest = (input) => ({
|
|
2126
|
-
protocol: WORK_SURFACE_PROTOCOL,
|
|
2127
|
-
version: 1,
|
|
2128
|
-
type: "request",
|
|
2129
|
-
...input
|
|
2130
|
-
});
|
|
2131
|
-
const buildWorkSurfaceResponse = (input) => ({
|
|
2132
|
-
protocol: WORK_SURFACE_PROTOCOL,
|
|
2133
|
-
version: 1,
|
|
2134
|
-
type: "response",
|
|
2135
|
-
...input
|
|
2136
|
-
});
|
|
2137
|
-
const buildWorkComposerChipSet = (chip) => ({
|
|
2138
|
-
protocol: WORK_SURFACE_PROTOCOL,
|
|
2139
|
-
version: 1,
|
|
2140
|
-
type: "composer.chip.set",
|
|
2141
|
-
chip
|
|
2142
|
-
});
|
|
2143
|
-
const buildWorkComposerChipClear = (key) => ({
|
|
2144
|
-
protocol: WORK_SURFACE_PROTOCOL,
|
|
2145
|
-
version: 1,
|
|
2146
|
-
type: "composer.chip.clear",
|
|
2147
|
-
key
|
|
2148
|
-
});
|
|
2149
|
-
//#endregion
|
|
2150
|
-
//#region src/board/core/draw-geometry.ts
|
|
2151
|
-
/** Radius of a sample in world units given the stroke size and pressure. */
|
|
2152
|
-
function sampleRadius(size, pressure) {
|
|
2153
|
-
const clamped = Math.min(1, Math.max(0, pressure));
|
|
2154
|
-
return Math.max(.5, size / 2 * (.5 + clamped));
|
|
2155
|
-
}
|
|
2156
|
-
/** Axis-aligned bounds of a stroke in its local space, padded by stroke width. */
|
|
2157
|
-
function computeDrawBounds(points, size) {
|
|
2158
|
-
if (points.length === 0) return {
|
|
2159
|
-
x: 0,
|
|
2160
|
-
y: 0,
|
|
2161
|
-
width: 1,
|
|
2162
|
-
height: 1
|
|
2163
|
-
};
|
|
2164
|
-
let minX = Number.POSITIVE_INFINITY;
|
|
2165
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
2166
|
-
let maxX = Number.NEGATIVE_INFINITY;
|
|
2167
|
-
let maxY = Number.NEGATIVE_INFINITY;
|
|
2168
|
-
for (const point of points) {
|
|
2169
|
-
const r = sampleRadius(size, point.p);
|
|
2170
|
-
minX = Math.min(minX, point.x - r);
|
|
2171
|
-
minY = Math.min(minY, point.y - r);
|
|
2172
|
-
maxX = Math.max(maxX, point.x + r);
|
|
2173
|
-
maxY = Math.max(maxY, point.y + r);
|
|
2174
|
-
}
|
|
2175
|
-
return {
|
|
2176
|
-
x: minX,
|
|
2177
|
-
y: minY,
|
|
2178
|
-
width: Math.max(1, maxX - minX),
|
|
2179
|
-
height: Math.max(1, maxY - minY)
|
|
2180
|
-
};
|
|
2181
|
-
}
|
|
2182
|
-
//#endregion
|
|
2183
|
-
//#region src/board/nodes.ts
|
|
2184
|
-
var BoardInputError = class extends Error {
|
|
2185
|
-
code = "INVALID_BOARD_NODE";
|
|
2186
|
-
diagnostics;
|
|
2187
|
-
body;
|
|
2188
|
-
constructor(diagnostics) {
|
|
2189
|
-
const message = diagnostics[0]?.message ?? "Invalid Board node";
|
|
2190
|
-
super(message);
|
|
2191
|
-
this.name = "BoardInputError";
|
|
2192
|
-
this.diagnostics = diagnostics;
|
|
2193
|
-
this.body = {
|
|
2194
|
-
code: this.code,
|
|
2195
|
-
message,
|
|
2196
|
-
diagnostics
|
|
2197
|
-
};
|
|
2198
|
-
}
|
|
2199
|
-
};
|
|
2200
|
-
function baseNode(spec, frame) {
|
|
2201
|
-
return {
|
|
2202
|
-
nodeId: spec.id,
|
|
2203
|
-
type: spec.type,
|
|
2204
|
-
parentId: spec.parentId ?? null,
|
|
2205
|
-
orderKey: spec.orderKey ?? null,
|
|
2206
|
-
x: frame.x,
|
|
2207
|
-
y: frame.y,
|
|
2208
|
-
width: frame.width,
|
|
2209
|
-
height: frame.height,
|
|
2210
|
-
rotation: frame.rotation ?? 0,
|
|
2211
|
-
refKind: null,
|
|
2212
|
-
refPath: null,
|
|
2213
|
-
refUrl: null,
|
|
2214
|
-
view: {},
|
|
2215
|
-
style: spec.style ?? {},
|
|
2216
|
-
data: {}
|
|
2217
|
-
};
|
|
2218
|
-
}
|
|
2219
|
-
function arrowFrame(start, end) {
|
|
2220
|
-
const padding = 16;
|
|
2221
|
-
return {
|
|
2222
|
-
x: Math.min(start.x, end.x) - padding,
|
|
2223
|
-
y: Math.min(start.y, end.y) - padding,
|
|
2224
|
-
width: Math.max(1, Math.abs(end.x - start.x) + padding * 2),
|
|
2225
|
-
height: Math.max(1, Math.abs(end.y - start.y) + padding * 2)
|
|
2226
|
-
};
|
|
2227
|
-
}
|
|
2228
|
-
/**
|
|
2229
|
-
* Create one validated wire node from semantic Board input.
|
|
2230
|
-
*
|
|
2231
|
-
* Box nodes take an explicit world-space frame. Draw samples and arrow endpoints
|
|
2232
|
-
* take world coordinates; the builder derives their frame and local storage form.
|
|
2233
|
-
*/
|
|
2234
|
-
function createBoardNode(spec) {
|
|
2235
|
-
let node;
|
|
2236
|
-
if (spec.type === "draw") {
|
|
2237
|
-
const size = spec.size ?? 4;
|
|
2238
|
-
const worldPoints = spec.points.map((point) => ({
|
|
2239
|
-
x: point.x,
|
|
2240
|
-
y: point.y,
|
|
2241
|
-
p: point.p ?? .5
|
|
2242
|
-
}));
|
|
2243
|
-
const bounds = computeDrawBounds(worldPoints, size);
|
|
2244
|
-
node = baseNode(spec, bounds);
|
|
2245
|
-
node.data = {
|
|
2246
|
-
points: worldPoints.map((point) => ({
|
|
2247
|
-
x: point.x - bounds.x,
|
|
2248
|
-
y: point.y - bounds.y,
|
|
2249
|
-
p: point.p
|
|
2250
|
-
})),
|
|
2251
|
-
color: spec.color ?? "brand",
|
|
2252
|
-
size
|
|
2253
|
-
};
|
|
2254
|
-
} else if (spec.type === "arrow") {
|
|
2255
|
-
node = baseNode(spec, arrowFrame(spec.start, spec.end));
|
|
2256
|
-
node.data = {
|
|
2257
|
-
start: spec.start,
|
|
2258
|
-
end: spec.end,
|
|
2259
|
-
bend: spec.bend ?? 0,
|
|
2260
|
-
color: spec.color ?? "brand",
|
|
2261
|
-
size: spec.size ?? 2.5,
|
|
2262
|
-
arrowStart: spec.arrowStart ?? false,
|
|
2263
|
-
arrowEnd: spec.arrowEnd ?? true,
|
|
2264
|
-
label: spec.label ?? ""
|
|
2265
|
-
};
|
|
2266
|
-
} else {
|
|
2267
|
-
node = baseNode(spec, spec.frame);
|
|
2268
|
-
switch (spec.type) {
|
|
2269
|
-
case "text":
|
|
2270
|
-
node.data = {
|
|
2271
|
-
text: spec.text ?? "",
|
|
2272
|
-
color: spec.color ?? "neutral",
|
|
2273
|
-
fontSize: spec.fontSize ?? 24
|
|
2274
|
-
};
|
|
2275
|
-
break;
|
|
2276
|
-
case "geo":
|
|
2277
|
-
node.data = {
|
|
2278
|
-
geo: spec.geo ?? "rectangle",
|
|
2279
|
-
text: spec.text ?? "",
|
|
2280
|
-
color: spec.color ?? "brand",
|
|
2281
|
-
fillOpacity: spec.fillOpacity ?? 0
|
|
2282
|
-
};
|
|
2283
|
-
break;
|
|
2284
|
-
case "frame":
|
|
2285
|
-
node.data = {
|
|
2286
|
-
label: spec.label ?? "Frame",
|
|
2287
|
-
color: spec.color ?? "neutral"
|
|
2288
|
-
};
|
|
2289
|
-
break;
|
|
2290
|
-
case "image":
|
|
2291
|
-
node.refKind = "space_file";
|
|
2292
|
-
node.refPath = spec.path;
|
|
2293
|
-
node.view = spec.snapshot ?? {};
|
|
2294
|
-
node.data = spec.crop ? { crop: spec.crop } : {};
|
|
2295
|
-
break;
|
|
2296
|
-
case "video":
|
|
2297
|
-
case "audio":
|
|
2298
|
-
node.refKind = "space_file";
|
|
2299
|
-
node.refPath = spec.path;
|
|
2300
|
-
node.view = spec.snapshot ?? {};
|
|
2301
|
-
break;
|
|
2302
|
-
case "file":
|
|
2303
|
-
node.refKind = "space_file";
|
|
2304
|
-
node.refPath = spec.path;
|
|
2305
|
-
node.view = spec.snapshot ?? {};
|
|
2306
|
-
break;
|
|
2307
|
-
case "task":
|
|
2308
|
-
node.view = spec.snapshot;
|
|
2309
|
-
node.data = { taskRunId: spec.taskRunId };
|
|
2310
|
-
break;
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
assertBoardNodes([node]);
|
|
2314
|
-
return node;
|
|
2315
|
-
}
|
|
2316
|
-
function validateBoardNodes(nodes, path = "nodes") {
|
|
2317
|
-
return nodes.flatMap((node, index) => validateBoardNodeInput(node, `${path}.${index}`));
|
|
2318
|
-
}
|
|
2319
|
-
function assertBoardNodes(nodes, path = "nodes") {
|
|
2320
|
-
const diagnostics = validateBoardNodes(nodes, path);
|
|
2321
|
-
if (diagnostics.length > 0) throw new BoardInputError(diagnostics);
|
|
2322
|
-
}
|
|
2323
|
-
function assertBoardTransactionNodeCreates(operations) {
|
|
2324
|
-
const diagnostics = operations.flatMap((operation, index) => {
|
|
2325
|
-
if (operation.type !== "node.create") return [];
|
|
2326
|
-
const payload = operation.payload;
|
|
2327
|
-
return payload.node ? validateBoardNodeInput(payload.node, `operations.${index}.payload.node`) : [];
|
|
1476
|
+
//#region src/realtime.ts
|
|
1477
|
+
function ensureRealtimeConnected(websocketClient) {
|
|
1478
|
+
if (websocketClient.state === "open" || websocketClient.state === "connecting" || websocketClient.state === "reconnecting") return;
|
|
1479
|
+
websocketClient.connect().catch((error) => {
|
|
1480
|
+
console.error("[CohubClient] Failed to connect realtime websocket:", error);
|
|
2328
1481
|
});
|
|
2329
|
-
if (diagnostics.length > 0) throw new BoardInputError(diagnostics);
|
|
2330
1482
|
}
|
|
2331
1483
|
//#endregion
|
|
2332
1484
|
//#region src/session-patch-reducer.ts
|
|
@@ -3339,26 +2491,6 @@ const getFilenameFromContentDisposition = (value) => {
|
|
|
3339
2491
|
return value.match(/filename="?([^";]+)"?/i)?.[1] ?? null;
|
|
3340
2492
|
};
|
|
3341
2493
|
/**
|
|
3342
|
-
* A board transaction rejected by the server. `status`/`code` let callers
|
|
3343
|
-
* distinguish a recoverable version conflict (409 / "VERSION_CONFLICT") from
|
|
3344
|
-
* transient failures, so they can rebase and retry instead of surfacing an error.
|
|
3345
|
-
*/
|
|
3346
|
-
var BoardTransactionError = class extends Error {
|
|
3347
|
-
status;
|
|
3348
|
-
code;
|
|
3349
|
-
body;
|
|
3350
|
-
constructor(message, status, code, body, options) {
|
|
3351
|
-
super(message, options);
|
|
3352
|
-
this.status = status;
|
|
3353
|
-
this.code = code;
|
|
3354
|
-
this.body = body;
|
|
3355
|
-
this.name = "BoardTransactionError";
|
|
3356
|
-
}
|
|
3357
|
-
get isVersionConflict() {
|
|
3358
|
-
return this.code === "VERSION_CONFLICT";
|
|
3359
|
-
}
|
|
3360
|
-
};
|
|
3361
|
-
/**
|
|
3362
2494
|
* Identifier for a client-authored Board entity (connection id, transaction id).
|
|
3363
2495
|
*
|
|
3364
2496
|
* `crypto.randomUUID` is used where available and falls back to a random string
|
|
@@ -3973,7 +3105,7 @@ var SpaceEventsApi = class {
|
|
|
3973
3105
|
handler(event);
|
|
3974
3106
|
return;
|
|
3975
3107
|
}
|
|
3976
|
-
if (type === "board.
|
|
3108
|
+
if (type === "board.changed" && event.type === "board.changed") {
|
|
3977
3109
|
handler(event);
|
|
3978
3110
|
return;
|
|
3979
3111
|
}
|
|
@@ -4331,10 +3463,10 @@ var BoardRealtimeClient = class {
|
|
|
4331
3463
|
const releaseRoom = this.websocketClient.retainRooms([getRealtimeSpaceRoom(this.spaceId), getRealtimeBoardRoom(this.boardId)]);
|
|
4332
3464
|
const unsubscribe = this.websocketClient.on("event", (event) => {
|
|
4333
3465
|
if (event.spaceId !== this.spaceId) return;
|
|
4334
|
-
if (event.type === "board.
|
|
4335
|
-
const
|
|
4336
|
-
handlers.event?.(
|
|
4337
|
-
handlers.
|
|
3466
|
+
if (event.type === "board.changed" && event.payload.boardId === this.boardId) {
|
|
3467
|
+
const changedEvent = event;
|
|
3468
|
+
handlers.event?.(changedEvent);
|
|
3469
|
+
handlers.changed?.(changedEvent);
|
|
4338
3470
|
}
|
|
4339
3471
|
if (event.type === "board.awareness.updated" && event.payload.boardId === this.boardId) {
|
|
4340
3472
|
const awarenessEvent = event;
|
|
@@ -4355,7 +3487,7 @@ var BoardRealtimeClient = class {
|
|
|
4355
3487
|
};
|
|
4356
3488
|
}
|
|
4357
3489
|
on(type, handler) {
|
|
4358
|
-
if (type === "
|
|
3490
|
+
if (type === "changed") return this.subscribe({ changed: handler });
|
|
4359
3491
|
if (type === "awareness") return this.subscribe({ awareness: handler });
|
|
4360
3492
|
return this.subscribe({ playback: handler });
|
|
4361
3493
|
}
|
|
@@ -4373,45 +3505,22 @@ var BoardClient = class {
|
|
|
4373
3505
|
this.boards = new SpaceBoardsApi(transport, spaceId, websocketClient);
|
|
4374
3506
|
this.realtime = new BoardRealtimeClient(websocketClient, spaceId, id);
|
|
4375
3507
|
}
|
|
4376
|
-
inspect(input = {}, customFetch) {
|
|
4377
|
-
return this.boards.inspect(this.id, input, customFetch);
|
|
4378
|
-
}
|
|
4379
3508
|
capabilities(customFetch) {
|
|
4380
3509
|
return this.boards.capabilities(this.id, customFetch);
|
|
4381
3510
|
}
|
|
4382
3511
|
summary(customFetch) {
|
|
4383
3512
|
return this.boards.summary(this.id, customFetch);
|
|
4384
3513
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
if (!Number.isSafeInteger(retries) || retries < 0 || retries > 3) throw new RangeError("Board mutation retries must be an integer from 0 to 3");
|
|
4388
|
-
for (let attempt = 0;; attempt += 1) {
|
|
4389
|
-
const current = await this.inspect({ include: input.include ?? [] });
|
|
4390
|
-
const operations = await input.build(current);
|
|
4391
|
-
if (operations.length === 0) return current;
|
|
4392
|
-
try {
|
|
4393
|
-
return await this.apply({
|
|
4394
|
-
txId: randomBoardId(),
|
|
4395
|
-
baseVersion: current.board.version,
|
|
4396
|
-
operations
|
|
4397
|
-
}, { compact: true });
|
|
4398
|
-
} catch (cause) {
|
|
4399
|
-
if (!(cause instanceof BoardTransactionError) || !cause.isVersionConflict || attempt >= retries) throw cause;
|
|
4400
|
-
}
|
|
4401
|
-
}
|
|
3514
|
+
authoring(input = {}, customFetch) {
|
|
3515
|
+
return this.boards.authoring(this.id, input, customFetch);
|
|
4402
3516
|
}
|
|
4403
|
-
|
|
4404
|
-
return this.boards.
|
|
4405
|
-
...
|
|
4406
|
-
|
|
3517
|
+
mutateSemantic(input) {
|
|
3518
|
+
return this.boards.mutateSemantic(this.id, {
|
|
3519
|
+
...input,
|
|
3520
|
+
mutationId: input.mutationId ?? randomBoardId(),
|
|
3521
|
+
dryRun: input.dryRun ?? false
|
|
4407
3522
|
});
|
|
4408
3523
|
}
|
|
4409
|
-
apply(transaction, options) {
|
|
4410
|
-
return this.boards.apply({
|
|
4411
|
-
...transaction,
|
|
4412
|
-
boardId: this.id
|
|
4413
|
-
}, options);
|
|
4414
|
-
}
|
|
4415
3524
|
updateAwareness(seq, update) {
|
|
4416
3525
|
if (!this.websocketClient) return Promise.resolve();
|
|
4417
3526
|
return this.websocketClient.updateBoardAwareness({
|
|
@@ -4421,91 +3530,6 @@ var BoardClient = class {
|
|
|
4421
3530
|
update
|
|
4422
3531
|
});
|
|
4423
3532
|
}
|
|
4424
|
-
playback(command) {
|
|
4425
|
-
return this.boards.playback(this.id, command);
|
|
4426
|
-
}
|
|
4427
|
-
/**
|
|
4428
|
-
* Read the Board's relations.
|
|
4429
|
-
*
|
|
4430
|
-
* A dedicated read rather than a filter over `inspect()`: a caller that wants
|
|
4431
|
-
* the graph should not have to fetch every node's geometry to get it.
|
|
4432
|
-
*/
|
|
4433
|
-
async connections(customFetch) {
|
|
4434
|
-
return (await this.boards.inspect(this.id, { include: ["connections"] }, customFetch)).connections;
|
|
4435
|
-
}
|
|
4436
|
-
/**
|
|
4437
|
-
* Connections touching a node, in either direction.
|
|
4438
|
-
*
|
|
4439
|
-
* Filtered client-side from the Board's relation set, which is a single read and
|
|
4440
|
-
* bounded by the Board rather than by the node's degree.
|
|
4441
|
-
*/
|
|
4442
|
-
async connectionsForNode(nodeId, customFetch) {
|
|
4443
|
-
return (await this.connections(customFetch)).filter((connection) => connection.source.nodeId === nodeId || connection.target.nodeId === nodeId);
|
|
4444
|
-
}
|
|
4445
|
-
/**
|
|
4446
|
-
* Connect two nodes.
|
|
4447
|
-
*
|
|
4448
|
-
* Wraps the transaction so the common case is one call: the caller supplies the
|
|
4449
|
-
* two nodes and, optionally, the relation. `baseVersion` still has to be the
|
|
4450
|
-
* version the caller last read, because a relation is only meaningful against
|
|
4451
|
-
* the node set it was authored on.
|
|
4452
|
-
*/
|
|
4453
|
-
connect(input) {
|
|
4454
|
-
const connection = createBoardConnection({
|
|
4455
|
-
id: input.id ?? randomBoardId(),
|
|
4456
|
-
sourceNodeId: input.sourceNodeId,
|
|
4457
|
-
targetNodeId: input.targetNodeId,
|
|
4458
|
-
...input.relation === void 0 ? {} : { relation: input.relation },
|
|
4459
|
-
...input.direction === void 0 ? {} : { direction: input.direction },
|
|
4460
|
-
...input.label === void 0 ? {} : { label: input.label },
|
|
4461
|
-
...input.sourcePortId === void 0 ? {} : { sourcePortId: input.sourcePortId },
|
|
4462
|
-
...input.targetPortId === void 0 ? {} : { targetPortId: input.targetPortId }
|
|
4463
|
-
});
|
|
4464
|
-
return this.apply({
|
|
4465
|
-
txId: input.txId ?? randomBoardId(),
|
|
4466
|
-
baseVersion: input.baseVersion,
|
|
4467
|
-
operations: [{
|
|
4468
|
-
type: "connection.create",
|
|
4469
|
-
payload: { connection }
|
|
4470
|
-
}]
|
|
4471
|
-
});
|
|
4472
|
-
}
|
|
4473
|
-
/** Remove a connection. The nodes it joined are untouched. */
|
|
4474
|
-
disconnect(input) {
|
|
4475
|
-
return this.apply({
|
|
4476
|
-
txId: input.txId ?? randomBoardId(),
|
|
4477
|
-
baseVersion: input.baseVersion,
|
|
4478
|
-
operations: [{
|
|
4479
|
-
type: "connection.delete",
|
|
4480
|
-
payload: { connectionId: input.connectionId }
|
|
4481
|
-
}]
|
|
4482
|
-
});
|
|
4483
|
-
}
|
|
4484
|
-
/**
|
|
4485
|
-
* Delete a node together with every relation that names it.
|
|
4486
|
-
*
|
|
4487
|
-
* The server refuses to orphan a relation, so the cascade is explicit and lands
|
|
4488
|
-
* in one transaction: one undo step restores the node and its edges together.
|
|
4489
|
-
* `connections` is the relation set the caller already read, so this stays a
|
|
4490
|
-
* single round-trip.
|
|
4491
|
-
*/
|
|
4492
|
-
deleteNodeWithConnections(input) {
|
|
4493
|
-
const incident = input.connections.filter((connection) => connection.source.nodeId === input.nodeId || connection.target.nodeId === input.nodeId);
|
|
4494
|
-
return this.apply({
|
|
4495
|
-
txId: input.txId ?? randomBoardId(),
|
|
4496
|
-
baseVersion: input.baseVersion,
|
|
4497
|
-
operations: [...incident.map((connection) => ({
|
|
4498
|
-
type: "connection.delete",
|
|
4499
|
-
payload: {
|
|
4500
|
-
connectionId: connection.id,
|
|
4501
|
-
reason: "node-cascade"
|
|
4502
|
-
}
|
|
4503
|
-
})), {
|
|
4504
|
-
type: "node.delete",
|
|
4505
|
-
payload: { nodeId: input.nodeId }
|
|
4506
|
-
}]
|
|
4507
|
-
});
|
|
4508
|
-
}
|
|
4509
3533
|
play(command) {
|
|
4510
3534
|
return this.boards.play(this.id, command);
|
|
4511
3535
|
}
|
|
@@ -4522,7 +3546,7 @@ var BoardClient = class {
|
|
|
4522
3546
|
return this.realtime.subscribe(handlers);
|
|
4523
3547
|
}
|
|
4524
3548
|
on(type, handler) {
|
|
4525
|
-
if (type === "
|
|
3549
|
+
if (type === "changed") return this.realtime.on("changed", handler);
|
|
4526
3550
|
if (type === "awareness") return this.realtime.on("awareness", handler);
|
|
4527
3551
|
return this.realtime.on("playback", handler);
|
|
4528
3552
|
}
|
|
@@ -4539,20 +3563,34 @@ var SpaceBoardsApi = class {
|
|
|
4539
3563
|
byId(boardId) {
|
|
4540
3564
|
return new BoardClient(this.spaceId, boardId, this.transport, this.websocketClient);
|
|
4541
3565
|
}
|
|
4542
|
-
create(input) {
|
|
4543
|
-
|
|
3566
|
+
async create(input) {
|
|
3567
|
+
for (const item of input.items ?? []) BoardAuthoringItemSchema.parse(item);
|
|
4544
3568
|
return this.transport.request(`/api/spaces/${this.spaceId}/boards`, {
|
|
4545
3569
|
method: "POST",
|
|
4546
3570
|
headers: { "Content-Type": "application/json" },
|
|
4547
3571
|
body: JSON.stringify(input)
|
|
4548
3572
|
});
|
|
4549
3573
|
}
|
|
4550
|
-
|
|
3574
|
+
authoring(boardId, input = {}, customFetch) {
|
|
4551
3575
|
const params = new URLSearchParams();
|
|
4552
|
-
|
|
3576
|
+
if (input.include) {
|
|
3577
|
+
for (const section of input.include) params.append("include", section);
|
|
3578
|
+
if (input.include.length === 0) params.set("include", "");
|
|
3579
|
+
}
|
|
3580
|
+
if (input.itemIds?.length) params.set("itemIds", input.itemIds.join(","));
|
|
3581
|
+
if (input.connectionIds?.length) params.set("connectionIds", input.connectionIds.join(","));
|
|
3582
|
+
if (input.effectIds?.length) params.set("effectIds", input.effectIds.join(","));
|
|
3583
|
+
if (input.compositionIds?.length) params.set("compositionIds", input.compositionIds.join(","));
|
|
4553
3584
|
if (input.viewport) params.set("viewport", JSON.stringify(input.viewport));
|
|
4554
3585
|
const query = params.toString();
|
|
4555
|
-
return this.transport.request(`/api/spaces/${this.spaceId}/boards/${boardId}${query ? `?${query}` : ""}`, { fetch: customFetch });
|
|
3586
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/boards/${boardId}/authoring${query ? `?${query}` : ""}`, { fetch: customFetch });
|
|
3587
|
+
}
|
|
3588
|
+
mutateSemantic(boardId, mutation) {
|
|
3589
|
+
return this.transport.request(`/api/spaces/${this.spaceId}/boards/${boardId}/mutations`, {
|
|
3590
|
+
method: "POST",
|
|
3591
|
+
headers: { "Content-Type": "application/json" },
|
|
3592
|
+
body: JSON.stringify(mutation)
|
|
3593
|
+
});
|
|
4556
3594
|
}
|
|
4557
3595
|
summary(boardId, customFetch) {
|
|
4558
3596
|
return this.transport.request(`/api/spaces/${this.spaceId}/boards/${boardId}/summary`, { fetch: customFetch });
|
|
@@ -4560,26 +3598,6 @@ var SpaceBoardsApi = class {
|
|
|
4560
3598
|
capabilities(boardId, customFetch) {
|
|
4561
3599
|
return this.transport.request(`/api/spaces/${this.spaceId}/boards/${boardId}/capabilities`, { fetch: customFetch });
|
|
4562
3600
|
}
|
|
4563
|
-
validate(transaction) {
|
|
4564
|
-
return this.transport.request(`/api/spaces/${this.spaceId}/boards/${transaction.boardId}/validate`, {
|
|
4565
|
-
method: "POST",
|
|
4566
|
-
headers: { "Content-Type": "application/json" },
|
|
4567
|
-
body: JSON.stringify(transaction)
|
|
4568
|
-
});
|
|
4569
|
-
}
|
|
4570
|
-
async apply(transaction, options) {
|
|
4571
|
-
assertBoardTransactionNodeCreates(transaction.operations);
|
|
4572
|
-
try {
|
|
4573
|
-
return await this.transport.request(`/api/spaces/${this.spaceId}/boards/${transaction.boardId}/transactions${options?.compact ? "?compact=1" : ""}`, {
|
|
4574
|
-
method: "POST",
|
|
4575
|
-
headers: { "Content-Type": "application/json" },
|
|
4576
|
-
body: JSON.stringify(transaction)
|
|
4577
|
-
});
|
|
4578
|
-
} catch (cause) {
|
|
4579
|
-
if (cause instanceof HttpError) throw new BoardTransactionError(cause.message, cause.status, cause.code ?? void 0, cause.body, { cause });
|
|
4580
|
-
throw cause;
|
|
4581
|
-
}
|
|
4582
|
-
}
|
|
4583
3601
|
playback(boardId, command) {
|
|
4584
3602
|
return this.transport.request(`/api/spaces/${this.spaceId}/boards/${boardId}/playback`, {
|
|
4585
3603
|
method: "POST",
|
|
@@ -5359,4 +4377,4 @@ var CohubHttpClient = class {
|
|
|
5359
4377
|
};
|
|
5360
4378
|
const createHttpClient = (options) => new CohubHttpClient(options);
|
|
5361
4379
|
//#endregion
|
|
5362
|
-
export {
|
|
4380
|
+
export { SessionAccessApi as $, isTerminalUiCommandStatus as A, BOARD_ANIMATION_CHANNELS as B, UI_COMMAND_DEFAULT_TIMEOUT_MS as C, UI_COMMAND_SETTLEMENT_GRACE_SECONDS as D, UI_COMMAND_PENDING_TTL_SECONDS as E, BoardItemPatchSchema as F, BoardTrackSchema as G, BoardCompositionInputSchema as H, BoardSemanticMutationSchema as I, BOARD_ARROW_STROKE_SIZE as J, parseBoardCompositionInput as K, BoardEffectInputSchema as L, parseUiCommand as M, parseUiCommandId as N, UI_COMMAND_TERMINAL_TTL_SECONDS as O, BoardAuthoringItemSchema as P, DEFAULT_BOARD_RENDER_LIMITS as Q, BoardEffectSchema as R, ensureRealtimeConnected as S, UI_COMMAND_PAYLOAD_MAX_BYTES as T, BoardCompositionSchema as U, BOARD_ANIMATION_CHANNEL_CAPABILITIES as V, BoardProceduralClipSchema as W, BOARD_BUILTIN_CLIP_KINDS as X, BOARD_BUILTIN_CAPABILITIES as Y, BOARD_BUILTIN_EFFECT_KINDS as Z, SessionGenerationStreamClient as _, ReferralsApi as a, ModelsApi as at, SessionPatchReducer as b, UiCommandsApi as c, ChannelsApi as ct, SpaceClient as d, ReferencesApi as et, SpacePublicFilesApi as f, buildSpacePath as g, buildSpaceInvitePath as h, WorksApi as i, PromptsApi as it, isUiSurfaceMethod as j, UI_COMMAND_VERSION as k, TasksApi as l, PublicInviteApi as m, createHttpClient as n, PublicAssetsApi as nt, UsersApi as o, GenerationsApi as ot, SpacesApi as p, BoardConnectionSchema as q, WorkCommerceApi as r, SkillsApi as rt, UserApi as s, CronJobsApi as st, CohubHttpClient as t, SearchApi as tt, BoardClient as u, createSessionGenerationStreamClient as v, UI_COMMAND_MAX_TIMEOUT_MS as w, createSessionPatchReducer as x, parseAssistantMessageCommit as y, parseBoardEffectInput as z };
|