From 23988e7ac5912d0e1beb12cef599537a32e84da1 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Fri, 10 Jul 2026 12:20:15 -0600 Subject: [PATCH 1/9] Add per-agent whiteboard with Excalidraw thin relay architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents construct raw Excalidraw element JSON directly via MCP tools (whiteboard_update, whiteboard_get, whiteboard_clear). The server is a thin relay — it merges elements by id, persists to Postgres with optimistic locking, and pushes updates via SSE. The frontend renders an interactive Excalidraw canvas with live sync in both directions. Key pieces: - Migration 0030: whiteboards table (agent_id PK, JSONB scene, version) - MCP tools with embedded Excalidraw element format cheat sheet (~200 lines) - REST endpoints for user-side scene save with optimistic locking - Whiteboard tab in center pane with lazy-loaded Excalidraw component - Self-hosted Excalidraw fonts via Vite plugin (excluding CJK for 12MB savings) - SSE-driven React Query invalidation with "agent drew" dot indicator - 31 unit tests + 4 E2E tests covering REST API, MCP tools, and UI Co-Authored-By: Claude Opus 4.6 --- .../src/db/migrations/0030_whiteboards.sql | 7 + apps/server/src/routes/mcp.ts | 9 + apps/server/src/routes/whiteboard.ts | 143 ++ apps/server/src/server.ts | 11 + apps/server/src/server/mcp-handlers.ts | 9 + .../src/server/mcp-whiteboard-handlers.ts | 199 +++ apps/server/src/server/ui-events.ts | 6 + apps/server/src/shared/mcp/server.ts | 26 + .../server/src/shared/mcp/whiteboard-tools.ts | 350 +++++ apps/server/src/shared/whiteboard-store.ts | 59 + apps/server/src/shared/whiteboard.ts | 87 ++ apps/server/test/whiteboard.test.ts | 459 ++++++ apps/web/package.json | 1 + apps/web/src/components/app/agents-view.tsx | 27 +- .../components/app/center-pane-tab-bar.tsx | 11 + .../src/components/app/whiteboard-pane.tsx | 44 + .../web/src/components/app/whiteboard-tab.tsx | 288 ++++ apps/web/src/hooks/use-agents-view-routing.ts | 9 +- apps/web/src/hooks/use-sse.ts | 26 +- apps/web/src/hooks/use-theme.ts | 4 + apps/web/src/hooks/use-whiteboard.ts | 22 + apps/web/src/lib/agent-routes.ts | 4 + apps/web/src/lib/store.ts | 6 +- apps/web/vite.config.ts | 43 +- e2e/whiteboard.spec.ts | 231 +++ pnpm-lock.yaml | 1367 ++++++++++++++++- 26 files changed, 3391 insertions(+), 57 deletions(-) create mode 100644 apps/server/src/db/migrations/0030_whiteboards.sql create mode 100644 apps/server/src/routes/whiteboard.ts create mode 100644 apps/server/src/server/mcp-whiteboard-handlers.ts create mode 100644 apps/server/src/shared/mcp/whiteboard-tools.ts create mode 100644 apps/server/src/shared/whiteboard-store.ts create mode 100644 apps/server/src/shared/whiteboard.ts create mode 100644 apps/server/test/whiteboard.test.ts create mode 100644 apps/web/src/components/app/whiteboard-pane.tsx create mode 100644 apps/web/src/components/app/whiteboard-tab.tsx create mode 100644 apps/web/src/hooks/use-whiteboard.ts create mode 100644 e2e/whiteboard.spec.ts diff --git a/apps/server/src/db/migrations/0030_whiteboards.sql b/apps/server/src/db/migrations/0030_whiteboards.sql new file mode 100644 index 00000000..3927b8f7 --- /dev/null +++ b/apps/server/src/db/migrations/0030_whiteboards.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS whiteboards ( + agent_id TEXT PRIMARY KEY, + scene JSONB NOT NULL DEFAULT '{"elements":[]}', + version INTEGER NOT NULL DEFAULT 1, + updated_by TEXT NOT NULL DEFAULT 'user', + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/apps/server/src/routes/mcp.ts b/apps/server/src/routes/mcp.ts index da6fdfa1..716c3c78 100644 --- a/apps/server/src/routes/mcp.ts +++ b/apps/server/src/routes/mcp.ts @@ -49,6 +49,9 @@ type McpRouteDeps = { mcpRenameSession: unknown; mcpShareMedia: unknown; mcpListMedia: unknown; + mcpGetWhiteboard: unknown; + mcpUpdateWhiteboard: unknown; + mcpClearWhiteboard: unknown; mcpSubmitFeedback: unknown; mcpListPersonas: unknown; mcpLaunchPersona: unknown; @@ -207,6 +210,9 @@ export async function registerMcpRoutes( renameSession: deps.mcpRenameSession, shareMedia: deps.mcpShareMedia, listMedia: deps.mcpListMedia, + getWhiteboard: deps.mcpGetWhiteboard, + updateWhiteboard: deps.mcpUpdateWhiteboard, + clearWhiteboard: deps.mcpClearWhiteboard, submitFeedback: deps.mcpSubmitFeedback, upsertPin: deps.mcpUpsertPin, deletePin: deps.mcpDeletePin, @@ -295,6 +301,9 @@ export async function registerMcpRoutes( renameSession: deps.mcpRenameSession, shareMedia: deps.mcpShareMedia, listMedia: deps.mcpListMedia, + getWhiteboard: deps.mcpGetWhiteboard, + updateWhiteboard: deps.mcpUpdateWhiteboard, + clearWhiteboard: deps.mcpClearWhiteboard, submitFeedback: deps.mcpSubmitFeedback, listPersonas: deps.mcpListPersonas, launchPersona: deps.mcpLaunchPersona, diff --git a/apps/server/src/routes/whiteboard.ts b/apps/server/src/routes/whiteboard.ts new file mode 100644 index 00000000..74e50d47 --- /dev/null +++ b/apps/server/src/routes/whiteboard.ts @@ -0,0 +1,143 @@ +import path from "node:path"; +import { mkdir, rm, writeFile } from "node:fs/promises"; + +import type { FastifyInstance } from "fastify"; +import type { Pool } from "pg"; + +import type { AgentManager } from "../agents/manager.js"; +import { resolveMediaDir } from "../shared/media.js"; +import { + EMPTY_SCENE, + isValidScene, + loadWhiteboard, + MAX_ELEMENTS, + saveWhiteboard, + WHITEBOARD_SNAPSHOT_FILENAME, +} from "../shared/whiteboard-store.js"; + +const SCENE_BODY_LIMIT = 8 * 1024 * 1024; + +type WhiteboardRouteDeps = { + pool: Pool; + mediaRoot: string; + agentManager: AgentManager; + publishUiEvent: (event: unknown) => void; +}; + +export async function registerWhiteboardRoutes( + app: FastifyInstance, + deps: WhiteboardRouteDeps +): Promise { + app.get("/api/v1/agents/:id/whiteboard", async (request, reply) => { + const params = request.params as { id?: string }; + const id = params.id ?? ""; + const agent = await deps.agentManager.getAgent(id); + if (!agent) { + return reply.code(404).send({ error: "Agent not found." }); + } + + const row = await loadWhiteboard(deps.pool, id); + if (!row) { + return { scene: EMPTY_SCENE, version: 0, updatedAt: null }; + } + return { + scene: row.scene, + version: Number(row.version), + updatedAt: row.updated_at.toISOString(), + }; + }); + + app.put( + "/api/v1/agents/:id/whiteboard", + { bodyLimit: SCENE_BODY_LIMIT }, + async (request, reply) => { + const params = request.params as { id?: string }; + const id = params.id ?? ""; + const agent = await deps.agentManager.getAgent(id); + if (!agent) { + return reply.code(404).send({ error: "Agent not found." }); + } + + const body = request.body as + | { scene?: unknown; baseVersion?: unknown } + | undefined; + if (!isValidScene(body?.scene)) { + return reply.code(400).send({ + error: `scene must be an object with an elements array (max ${MAX_ELEMENTS}).`, + }); + } + const baseVersion = + typeof body?.baseVersion === "number" && + Number.isInteger(body.baseVersion) && + body.baseVersion >= 0 + ? body.baseVersion + : null; + if (baseVersion === null) { + return reply + .code(400) + .send({ error: "baseVersion must be a non-negative integer." }); + } + + const saved = await saveWhiteboard( + deps.pool, + id, + body.scene, + baseVersion, + "user" + ); + if (!saved) { + const current = await loadWhiteboard(deps.pool, id); + return reply.code(409).send({ + error: "Whiteboard was modified by someone else.", + scene: current?.scene ?? EMPTY_SCENE, + version: current ? Number(current.version) : 0, + }); + } + + deps.publishUiEvent({ + type: "whiteboard.changed", + agentId: id, + version: saved.version, + source: "user", + }); + return { ok: true, version: saved.version }; + } + ); + + app.post("/api/v1/agents/:id/whiteboard/snapshot", async (request, reply) => { + const params = request.params as { id?: string }; + const id = params.id ?? ""; + const agent = await deps.agentManager.getAgent(id); + if (!agent) { + return reply.code(404).send({ error: "Agent not found." }); + } + + const data = await request.file(); + if (!data || data.mimetype !== "image/png") { + return reply.code(400).send({ error: "A PNG file field is required." }); + } + + const mediaDir = resolveMediaDir(id, agent.mediaDir, deps.mediaRoot); + await mkdir(mediaDir, { recursive: true }); + const buffer = await data.toBuffer(); + await writeFile(path.join(mediaDir, WHITEBOARD_SNAPSHOT_FILENAME), buffer); + return { ok: true, sizeBytes: buffer.length }; + }); + + app.delete( + "/api/v1/agents/:id/whiteboard/snapshot", + async (request, reply) => { + const params = request.params as { id?: string }; + const id = params.id ?? ""; + const agent = await deps.agentManager.getAgent(id); + if (!agent) { + return reply.code(404).send({ error: "Agent not found." }); + } + const mediaDir = resolveMediaDir(id, agent.mediaDir, deps.mediaRoot); + await rm(path.join(mediaDir, WHITEBOARD_SNAPSHOT_FILENAME), { + force: true, + }); + return { ok: true }; + } + ); +} diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 34f75103..ea57710c 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -105,6 +105,7 @@ import { registerFeedbackRoutes } from "./routes/feedback.js"; import { registerJobRoutes } from "./routes/jobs.js"; import { registerTemplateRoutes } from "./routes/templates.js"; import { registerMediaRoutes } from "./routes/media.js"; +import { registerWhiteboardRoutes } from "./routes/whiteboard.js"; import { registerMcpRoutes } from "./routes/mcp.js"; import { registerPersonaReviewRoutes } from "./routes/persona-reviews.js"; import { registerPersonalityRoutes } from "./routes/personalities.js"; @@ -476,6 +477,9 @@ async function registerRoutes() { mcpRenameSession: mcpHandlers.renameSession, mcpShareMedia: mcpHandlers.shareMedia, mcpListMedia: mcpHandlers.listMedia, + mcpGetWhiteboard: mcpHandlers.getWhiteboard, + mcpUpdateWhiteboard: mcpHandlers.updateWhiteboard, + mcpClearWhiteboard: mcpHandlers.clearWhiteboard, mcpSubmitFeedback: mcpHandlers.submitFeedback, mcpListPersonas: mcpHandlers.listPersonas, mcpLaunchPersona: mcpHandlers.launchPersona, @@ -581,6 +585,13 @@ async function registerRoutes() { publishUiEvent: (event) => uiEventBroker.publish(event as UiEvent), }); + await registerWhiteboardRoutes(app, { + pool, + mediaRoot: config.mediaRoot, + agentManager, + publishUiEvent: (event) => uiEventBroker.publish(event as UiEvent), + }); + await registerAgentRoutes(app, { pool, appLog: app.log, diff --git a/apps/server/src/server/mcp-handlers.ts b/apps/server/src/server/mcp-handlers.ts index 7afcfc73..32ce2c5f 100644 --- a/apps/server/src/server/mcp-handlers.ts +++ b/apps/server/src/server/mcp-handlers.ts @@ -28,6 +28,7 @@ import { resolveHeadSha } from "../shared/git/worktree.js"; import { isMediaFile, isTextFile, resolveMediaDir } from "../shared/media.js"; import type { PublishUiEvent, SendAgentPrompt } from "./mcp-handler-types.js"; import { createReviewHandlers } from "./mcp-review-handlers.js"; +import { createWhiteboardHandlers } from "./mcp-whiteboard-handlers.js"; const AGENT_LATEST_EVENT_TYPES = [ "working", @@ -151,8 +152,16 @@ export function createMcpHandlers(deps: CreateMcpHandlersDeps) { sendAgentPrompt, }); + const whiteboardHandlers = createWhiteboardHandlers({ + pool, + mediaRoot, + agentManager, + publishUiEvent, + }); + return { ...reviewHandlers, + ...whiteboardHandlers, async upsertEvent( agentId: string, diff --git a/apps/server/src/server/mcp-whiteboard-handlers.ts b/apps/server/src/server/mcp-whiteboard-handlers.ts new file mode 100644 index 00000000..d5ccd369 --- /dev/null +++ b/apps/server/src/server/mcp-whiteboard-handlers.ts @@ -0,0 +1,199 @@ +import path from "node:path"; +import { rm, stat } from "node:fs/promises"; + +import type { Pool } from "pg"; + +import type { AgentManager } from "../agents/manager.js"; +import { resolveMediaDir } from "../shared/media.js"; +import { + simplifyElements, + type WhiteboardGetResult, + type WhiteboardUpdateResult, +} from "../shared/whiteboard.js"; +import { + EMPTY_SCENE, + loadWhiteboard, + MAX_ELEMENTS, + saveWhiteboard, + WHITEBOARD_SNAPSHOT_FILENAME, +} from "../shared/whiteboard-store.js"; +import type { PublishUiEvent } from "./mcp-handler-types.js"; + +type CreateWhiteboardHandlersDeps = { + pool: Pool; + mediaRoot: string; + agentManager: AgentManager; + publishUiEvent: PublishUiEvent; +}; + +type RawElement = Record; + +function hasRequiredFields( + el: unknown +): el is RawElement & { id: string; type: string } { + if (typeof el !== "object" || el === null) return false; + const obj = el as RawElement; + return typeof obj.id === "string" && typeof obj.type === "string"; +} + +function mergeElements( + existing: unknown[], + incoming: unknown[], + deleteIds: string[] +): { elements: unknown[]; addedIds: string[]; updatedIds: string[] } { + const deleteSet = new Set(deleteIds); + const existingMap = new Map(); + for (const el of existing) { + if (typeof el === "object" && el !== null) { + const id = (el as RawElement).id; + if (typeof id === "string") { + existingMap.set(id, el); + } + } + } + + const addedIds: string[] = []; + const updatedIds: string[] = []; + + for (const el of incoming) { + if (!hasRequiredFields(el)) continue; + if (existingMap.has(el.id)) { + updatedIds.push(el.id); + } else { + addedIds.push(el.id); + } + existingMap.set(el.id, el); + } + + for (const id of deleteSet) { + if (existingMap.has(id)) { + existingMap.delete(id); + } + } + + return { + elements: Array.from(existingMap.values()), + addedIds, + updatedIds, + }; +} + +export function createWhiteboardHandlers(deps: CreateWhiteboardHandlersDeps) { + const { pool, mediaRoot, agentManager, publishUiEvent } = deps; + + return { + async getWhiteboard(agentId: string): Promise { + const agent = await agentManager.getAgent(agentId); + if (!agent) throw new Error("Agent not found."); + + const row = await loadWhiteboard(pool, agentId); + const snapshotFile = path.join( + resolveMediaDir(agentId, agent.mediaDir, mediaRoot), + WHITEBOARD_SNAPSHOT_FILENAME + ); + const snapshotStat = await stat(snapshotFile).catch(() => null); + const snapshotPath = snapshotStat?.isFile() ? snapshotFile : null; + return { + elements: row ? simplifyElements(row.scene.elements) : [], + version: row ? Number(row.version) : 0, + updatedAt: row ? row.updated_at.toISOString() : null, + updatedBy: row ? row.updated_by : null, + snapshotPath, + snapshotStale: + snapshotPath !== null && + row !== null && + snapshotStat !== null && + row.updated_at.getTime() > snapshotStat.mtime.getTime(), + }; + }, + + async updateWhiteboard( + agentId: string, + elements: unknown[], + deleteIds: string[] + ): Promise { + const agent = await agentManager.getAgent(agentId); + if (!agent) throw new Error("Agent not found."); + + for (let attempt = 0; attempt < 3; attempt++) { + const row = await loadWhiteboard(pool, agentId); + const baseVersion = row ? Number(row.version) : 0; + const existing = row ? row.scene.elements : []; + + const merged = mergeElements(existing, elements, deleteIds); + + if (merged.elements.length > MAX_ELEMENTS) { + throw new Error(`Board is full (max ${MAX_ELEMENTS} elements).`); + } + + const saved = await saveWhiteboard( + pool, + agentId, + { elements: merged.elements }, + baseVersion, + "agent" + ); + if (saved) { + publishUiEvent({ + type: "whiteboard.changed", + agentId, + version: saved.version, + source: "agent", + }); + return { + version: saved.version, + elementCount: merged.elements.length, + addedIds: merged.addedIds, + updatedIds: merged.updatedIds, + deletedIds: deleteIds.filter((id) => + existing.some( + (el) => + typeof el === "object" && + el !== null && + (el as RawElement).id === id + ) + ), + elements: simplifyElements(merged.elements), + }; + } + } + throw new Error( + "Whiteboard is being edited concurrently; try again in a moment." + ); + }, + + async clearWhiteboard(agentId: string): Promise { + const agent = await agentManager.getAgent(agentId); + if (!agent) throw new Error("Agent not found."); + + for (let attempt = 0; attempt < 3; attempt++) { + const row = await loadWhiteboard(pool, agentId); + const baseVersion = row ? Number(row.version) : 0; + const saved = await saveWhiteboard( + pool, + agentId, + EMPTY_SCENE, + baseVersion, + "agent" + ); + if (saved) { + publishUiEvent({ + type: "whiteboard.changed", + agentId, + version: saved.version, + source: "agent", + }); + const snapshotFile = path.join( + resolveMediaDir(agentId, agent.mediaDir, mediaRoot), + WHITEBOARD_SNAPSHOT_FILENAME + ); + await rm(snapshotFile, { force: true }); + return; + } + } + throw new Error( + "Whiteboard is being edited concurrently; try again in a moment." + ); + }, + }; +} diff --git a/apps/server/src/server/ui-events.ts b/apps/server/src/server/ui-events.ts index f020a79f..415ce82b 100644 --- a/apps/server/src/server/ui-events.ts +++ b/apps/server/src/server/ui-events.ts @@ -19,6 +19,12 @@ export type UiEvent = } | { type: "agent.deleted"; agentId: string } | { type: "media.changed"; agentId: string } + | { + type: "whiteboard.changed"; + agentId: string; + version: number; + source: "user" | "agent"; + } | { type: "media.seen"; agentId: string; keys: string[] } | { type: "stream.started"; agentId: string } | { type: "stream.stopped"; agentId: string } diff --git a/apps/server/src/shared/mcp/server.ts b/apps/server/src/shared/mcp/server.ts index 319c6c5d..3267f0f1 100644 --- a/apps/server/src/shared/mcp/server.ts +++ b/apps/server/src/shared/mcp/server.ts @@ -13,6 +13,11 @@ import { registerBrainTools } from "./brain-tools.js"; import { registerCrudTools, type CrudToolCallbacks } from "./crud-tools.js"; import { registerJobTools, type JobTools } from "./job-tools.js"; import { registerMessagingTools } from "./messaging-tools.js"; +import { registerWhiteboardTools } from "./whiteboard-tools.js"; +import type { + WhiteboardGetResult, + WhiteboardUpdateResult, +} from "../whiteboard.js"; import { registerPersonaInteractionTools, type LaunchPersonaAgentType, @@ -104,6 +109,9 @@ const AGENT_TOOLS = new Set([ "get_activity_summary", "get_agent_history", "get_feedback_summary", + "whiteboard_get", + "whiteboard_update", + "whiteboard_clear", "brain_get_object", "brain_store_object", "brain_list_objects", @@ -191,6 +199,7 @@ const PERSONA_TOOLS = new Set([ "dispatch_share", "dispatch_feedback", "get_parent_context", + "whiteboard_get", ]); type AgentType = "agent" | "job" | "persona"; @@ -405,6 +414,13 @@ export type McpRequestContext = { pin: { label: string; value: string; type: string } ) => Promise; deletePin?: (agentId: string, label: string) => Promise; + getWhiteboard?: (agentId: string) => Promise; + updateWhiteboard?: ( + agentId: string, + elements: unknown[], + deleteIds: string[] + ) => Promise; + clearWhiteboard?: (agentId: string) => Promise; getParentContext?: (parentAgentId: string) => Promise; getRecheckContext?: (agentId: string) => Promise; updateReviewStatus?: ( @@ -561,6 +577,16 @@ async function createDispatchMcpServer( }); } + // ── Whiteboard tools ────────────────────────────────────────────── + if (context.agent) { + registerWhiteboardTools(server, allowed, { + agentId: context.agent.id, + getWhiteboard: context.getWhiteboard, + updateWhiteboard: context.updateWhiteboard, + clearWhiteboard: context.clearWhiteboard, + }); + } + // ── Inter-agent messaging tools ─────────────────────────────────── if (context.agent) { registerMessagingTools(server, allowed, { diff --git a/apps/server/src/shared/mcp/whiteboard-tools.ts b/apps/server/src/shared/mcp/whiteboard-tools.ts new file mode 100644 index 00000000..36e4e6ee --- /dev/null +++ b/apps/server/src/shared/mcp/whiteboard-tools.ts @@ -0,0 +1,350 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import * as z from "zod/v4"; + +import { toToolError } from "./tool-error.js"; +import type { + WhiteboardGetResult, + WhiteboardUpdateResult, +} from "../whiteboard.js"; + +export type WhiteboardToolsContext = { + agentId: string; + getWhiteboard?: (agentId: string) => Promise; + updateWhiteboard?: ( + agentId: string, + elements: unknown[], + deleteIds: string[] + ) => Promise; + clearWhiteboard?: (agentId: string) => Promise; +}; + +// ── Excalidraw element format cheat sheet ────────────────────────────── +// Included in the whiteboard_update tool description so agents can +// construct valid Excalidraw element JSON without importing the library. +const EXCALIDRAW_CHEAT_SHEET = ` + +## Excalidraw Element Format Reference + +Every element is a JSON object. Fields marked (required) must be present; all others have sensible defaults the editor will apply if omitted. + +### Common fields (all element types) + +| Field | Type | Required | Default | Notes | +|-------|------|----------|---------|-------| +| id | string | YES | — | Unique id. Use readable slugs like "api-box", "db-node". | +| type | string | YES | — | One of: rectangle, ellipse, diamond, text, arrow, line, frame, freedraw, image | +| x | number | YES | — | Left edge, canvas pixels | +| y | number | YES | — | Top edge, canvas pixels | +| width | number | YES | — | Element width (use 0 for arrows/lines) | +| height | number | YES | — | Element height (use 0 for arrows/lines) | +| angle | number | no | 0 | Rotation in radians | +| strokeColor | string | no | "#1e1e1e" | Stroke/outline color (hex) | +| backgroundColor | string | no | "transparent" | Fill color (hex or "transparent") | +| fillStyle | string | no | "solid" | One of: solid, hachure, cross-hatch | +| strokeWidth | number | no | 2 | Stroke thickness in px | +| strokeStyle | string | no | "solid" | One of: solid, dashed, dotted | +| roughness | number | no | 1 | 0=smooth, 1=normal, 2=rough (hand-drawn look) | +| opacity | number | no | 100 | 0–100 | +| groupIds | string[] | no | [] | Group membership | +| frameId | string\\|null | no | null | Parent frame id | +| roundness | object\\|null | no | null | { type: 3 } for rounded corners on rectangles | +| seed | number | no | random | Random seed for roughness rendering | +| version | number | no | 1 | Bump on each edit | +| versionNonce | number | no | random | Random nonce, changes with version | +| isDeleted | boolean | no | false | Soft-delete flag | +| boundElements | array\\|null | no | null | Back-references: [{ id, type }] | +| updated | number | no | Date.now() | Timestamp ms | +| link | string\\|null | no | null | URL link | +| locked | boolean | no | false | Prevent editing | + +### Shape elements: rectangle, ellipse, diamond + +\`\`\`json +{ + "id": "api-box", + "type": "rectangle", + "x": 100, "y": 100, + "width": 160, "height": 70, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "roundness": { "type": 3 } +} +\`\`\` + +Ellipse and diamond use the same fields, just change \`type\`. + +### Text elements + +\`\`\`json +{ + "id": "title-text", + "type": "text", + "x": 100, "y": 50, + "width": 200, "height": 25, + "text": "API Server", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "originalText": "API Server" +} +\`\`\` + +**Bound text (label inside a shape):** Create a text element with \`containerId\` pointing to the shape, and add a back-reference in the shape's \`boundElements\`: + +\`\`\`json +[ + { + "id": "box1", + "type": "rectangle", + "x": 100, "y": 100, "width": 160, "height": 70, + "boundElements": [{ "id": "box1-label", "type": "text" }] + }, + { + "id": "box1-label", + "type": "text", + "x": 110, "y": 120, "width": 140, "height": 25, + "text": "API", + "originalText": "API", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "box1" + } +] +\`\`\` + +Text font families: 1=Virgil (handwritten), 3=Cascadia (monospace), 5=Excalifont (default). + +### Arrow and line elements + +\`\`\`json +{ + "id": "flow-arrow", + "type": "arrow", + "x": 260, "y": 135, + "width": 140, "height": 0, + "points": [[0, 0], [140, 0]], + "startArrowhead": null, + "endArrowhead": "arrow", + "startBinding": { + "elementId": "api-box", + "focus": 0, + "gap": 1, + "fixedPoint": [1, 0.5] + }, + "endBinding": { + "elementId": "db-node", + "focus": 0, + "gap": 1, + "fixedPoint": [0, 0.5] + }, + "elbowed": false +} +\`\`\` + +**Points:** Array of [x, y] offsets relative to the element's x, y. First point is always [0, 0]. Add intermediate points for bends. + +**Arrowheads:** \`startArrowhead\` and \`endArrowhead\` can be: null, "arrow", "bar", "dot", "triangle", "diamond". + +**Bindings:** Connect arrows to shapes. \`fixedPoint\` is [proportionX, proportionY] on the target shape (0-1 range): [0.5, 0] = top center, [1, 0.5] = right center, [0.5, 1] = bottom center, [0, 0.5] = left center. + +When binding an arrow, also add a back-reference in the target shape's \`boundElements\`: +\`\`\`json +{ "id": "flow-arrow", "type": "arrow" } +\`\`\` + +**Elbow routing:** Set \`elbowed: true\` for right-angle connector routing (auto-computed path). The \`points\` array will be overridden by the editor. + +**Lines** use the same format but \`type: "line"\` and no arrowheads. + +### Frame elements + +\`\`\`json +{ + "id": "frame1", + "type": "frame", + "x": 50, "y": 50, + "width": 400, "height": 300, + "name": "Backend Services" +} +\`\`\` + +Children are assigned to frames by setting their \`frameId\` to the frame's id. + +### Color reference + +**Stroke colors:** #1e1e1e (black/default), #e03131 (red), #2f9e44 (green), #1971c2 (blue), #f08c00 (orange), #6741d9 (violet), #0c8599 (cyan), #e8590c (dark orange), #868e96 (gray) + +**Pastel fills (good for shape backgrounds):** #a5d8ff (light blue), #b2f2bb (light green), #ffd8a8 (light orange), #d0bfff (light purple), #ffc9c9 (light red), #fff3bf (light yellow), #c3fae8 (light teal), #eebefa (light pink), #e5dbff (light violet) + +### Layout tips + +- Typical box: width 160, height 70 +- Leave ~80px gaps between shapes +- Center labels inside shapes using \`containerId\` + \`boundElements\` binding +- Keep labels short (2–5 words) — text wraps to shape width +- For arrows: set x, y to the start point, compute width/height from the last point offset +- Arrow width = last point's x offset, height = last point's y offset (can be negative) + +### Important notes + +- The editor auto-heals many issues (null fields, missing indices). Don't over-validate. +- Always provide \`id\`, \`type\`, \`x\`, \`y\` at minimum. Width and height default to 0 if omitted. +- Use readable, descriptive ids — you'll reference them in bindings and future updates. +- Elements are merged by id: sending an element with an existing id replaces it entirely. +`; + +export function registerWhiteboardTools( + server: McpServer, + allowed: Set, + context: WhiteboardToolsContext +): void { + if (allowed.has("whiteboard_get") && context.getWhiteboard) { + const agentId = context.agentId; + const getWhiteboard = context.getWhiteboard; + + server.registerTool( + "whiteboard_get", + { + description: + "Get the current state of this agent's shared whiteboard — a canvas the user sketches on " + + "(architecture diagrams, flows, ideas). Returns a simplified element list (geometry, text, " + + "arrow connections via from/to element ids) plus snapshotPath: a PNG rendering of the board. " + + "Read the snapshot file to SEE the drawing — freehand sketches are hard to interpret from " + + "elements alone. Use this whenever the user refers to the whiteboard/board/drawing.", + inputSchema: {}, + }, + async () => { + try { + const board = await getWhiteboard(agentId); + const summary = { + elementCount: board.elements.length, + version: board.version, + updatedAt: board.updatedAt, + updatedBy: board.updatedBy, + snapshotPath: board.snapshotPath, + snapshotStale: board.snapshotStale, + elements: board.elements, + }; + const snapshotNote = board.snapshotPath + ? board.snapshotStale + ? `\n\nNote: ${board.snapshotPath} was rendered BEFORE the latest edits — trust the element list over the image until a browser re-exports it.` + : `\n\nTip: Read ${board.snapshotPath} to view the board visually.` + : "\n\nNo snapshot has been rendered yet (the board may be empty or never opened)."; + return { + content: [ + { + type: "text", + text: JSON.stringify(summary, null, 2) + snapshotNote, + }, + ], + structuredContent: summary, + }; + } catch (error) { + return toToolError(error); + } + } + ); + } + + if (allowed.has("whiteboard_update") && context.updateWhiteboard) { + const agentId = context.agentId; + const updateWhiteboard = context.updateWhiteboard; + + server.registerTool( + "whiteboard_update", + { + description: + "Draw on the shared whiteboard — the user sees your edits live. You construct raw " + + "Excalidraw element JSON directly. Elements are merged by id: if an element with that " + + "id already exists on the board, it is replaced entirely; otherwise it is added. Use " + + "`deleteIds` to remove elements." + + "\n\n" + + "**Workflow:** Call `whiteboard_get` first to see current elements, their ids, and where " + + "free space is. Then construct your elements and send them here. Give elements readable " + + "ids (e.g. 'api-box', 'db-node') so you can reference them in arrow bindings." + + "\n\n" + + "**Labels:** To put text inside a shape, create BOTH a shape element (with a `boundElements` " + + "back-reference to the text) AND a text element (with `containerId` pointing to the shape). " + + "See the format reference below." + + "\n\n" + + "**Arrows:** Use `startBinding`/`endBinding` with `elementId` and `fixedPoint` to connect " + + "arrows to shapes. Add back-references in each target shape's `boundElements` array. " + + "Set `elbowed: true` for right-angle routing." + + "\n\n" + + "**Layout:** Typical box w=160, h=70 with ~80px gaps. Keep labels short (2–5 words)." + + EXCALIDRAW_CHEAT_SHEET, + inputSchema: { + elements: z + .array(z.record(z.string(), z.any())) + .max(500) + .describe( + "Array of Excalidraw element objects to add or update. Each must have at least " + + "'id' and 'type'. Elements are merged by id (upsert). See the format reference in " + + "the tool description for the full element schema." + ), + deleteIds: z + .array(z.string()) + .max(500) + .optional() + .describe("Array of element ids to remove from the board."), + }, + }, + async (args) => { + try { + const result = await updateWhiteboard( + agentId, + args.elements, + args.deleteIds ?? [] + ); + const summary = { + ok: true, + version: result.version, + elementCount: result.elementCount, + addedIds: result.addedIds, + updatedIds: result.updatedIds, + deletedIds: result.deletedIds, + elements: result.elements, + }; + return { + content: [{ type: "text", text: JSON.stringify(summary, null, 2) }], + structuredContent: summary, + }; + } catch (error) { + return toToolError(error); + } + } + ); + } + + if (allowed.has("whiteboard_clear") && context.clearWhiteboard) { + const agentId = context.agentId; + const clearWhiteboard = context.clearWhiteboard; + + server.registerTool( + "whiteboard_clear", + { + description: "Clear the whiteboard entirely, removing all elements.", + inputSchema: {}, + }, + async () => { + try { + await clearWhiteboard(agentId); + return { + content: [ + { + type: "text", + text: '{"ok": true, "message": "Whiteboard cleared."}', + }, + ], + }; + } catch (error) { + return toToolError(error); + } + } + ); + } +} diff --git a/apps/server/src/shared/whiteboard-store.ts b/apps/server/src/shared/whiteboard-store.ts new file mode 100644 index 00000000..742bb70d --- /dev/null +++ b/apps/server/src/shared/whiteboard-store.ts @@ -0,0 +1,59 @@ +import type { Pool } from "pg"; + +export const WHITEBOARD_SNAPSHOT_FILENAME = "whiteboard.png"; + +export const MAX_ELEMENTS = 20_000; + +export const EMPTY_SCENE = { elements: [] as unknown[] }; + +export type WhiteboardRow = { + scene: { elements: unknown[] }; + version: string; + updated_by: string; + updated_at: Date; +}; + +export async function loadWhiteboard( + pool: Pool, + agentId: string +): Promise { + const result = await pool.query( + "SELECT scene, version, updated_by, updated_at FROM whiteboards WHERE agent_id = $1", + [agentId] + ); + return result.rows[0] ?? null; +} + +export function isValidScene(scene: unknown): scene is { elements: unknown[] } { + return ( + typeof scene === "object" && + scene !== null && + Array.isArray((scene as { elements?: unknown }).elements) && + (scene as { elements: unknown[] }).elements.length <= MAX_ELEMENTS + ); +} + +export async function saveWhiteboard( + pool: Pool, + agentId: string, + scene: { elements: unknown[] }, + baseVersion: number, + updatedBy: "user" | "agent" +): Promise<{ version: number } | null> { + const result = await pool.query<{ version: string }>( + `INSERT INTO whiteboards (agent_id, scene, version, updated_by) + VALUES ($1, $2::jsonb, 1, $3) + ON CONFLICT (agent_id) DO UPDATE + SET scene = EXCLUDED.scene, + version = whiteboards.version + 1, + updated_by = EXCLUDED.updated_by, + updated_at = NOW() + WHERE whiteboards.version = $4 + RETURNING version`, + [agentId, JSON.stringify(scene), updatedBy, baseVersion] + ); + if (result.rows.length === 0) { + return null; + } + return { version: Number(result.rows[0].version) }; +} diff --git a/apps/server/src/shared/whiteboard.ts b/apps/server/src/shared/whiteboard.ts new file mode 100644 index 00000000..c59a9bc5 --- /dev/null +++ b/apps/server/src/shared/whiteboard.ts @@ -0,0 +1,87 @@ +type RawElement = Record; + +export type SimplifiedElement = { + id: string; + type: string; + x: number; + y: number; + width: number; + height: number; + angle?: number; + text?: string; + containerId?: string; + from?: string; + to?: string; + frameId?: string; + strokeColor?: string; + backgroundColor?: string; +}; + +export type WhiteboardGetResult = { + elements: SimplifiedElement[]; + version: number; + updatedAt: string | null; + updatedBy: string | null; + snapshotPath: string | null; + snapshotStale: boolean; +}; + +export type WhiteboardUpdateResult = { + version: number; + elementCount: number; + addedIds: string[]; + updatedIds: string[]; + deletedIds: string[]; + elements: SimplifiedElement[]; +}; + +function num(value: unknown): number { + return typeof value === "number" && Number.isFinite(value) + ? Math.round(value) + : 0; +} + +function str(value: unknown): string | undefined { + return typeof value === "string" && value ? value : undefined; +} + +export function simplifyElements(elements: unknown[]): SimplifiedElement[] { + const out: SimplifiedElement[] = []; + for (const raw of elements) { + if (typeof raw !== "object" || raw === null) continue; + const el = raw as RawElement; + if (el.isDeleted === true) continue; + + const simplified: SimplifiedElement = { + id: str(el.id) ?? "", + type: str(el.type) ?? "unknown", + x: num(el.x), + y: num(el.y), + width: num(el.width), + height: num(el.height), + }; + if (typeof el.angle === "number" && Math.abs(el.angle) > 0.01) { + simplified.angle = Number((el.angle as number).toFixed(2)); + } + const text = str(el.text) ?? str(el.originalText); + if (text) simplified.text = text; + const containerId = str((el as { containerId?: unknown }).containerId); + if (containerId) simplified.containerId = containerId; + const startBinding = el.startBinding as { elementId?: unknown } | null; + const endBinding = el.endBinding as { elementId?: unknown } | null; + const from = str(startBinding?.elementId); + const to = str(endBinding?.elementId); + if (from) simplified.from = from; + if (to) simplified.to = to; + const frameId = str(el.frameId); + if (frameId) simplified.frameId = frameId; + const strokeColor = str(el.strokeColor); + if (strokeColor) simplified.strokeColor = strokeColor; + const backgroundColor = str(el.backgroundColor); + if (backgroundColor && backgroundColor !== "transparent") { + simplified.backgroundColor = backgroundColor; + } + out.push(simplified); + } + return out; +} diff --git a/apps/server/test/whiteboard.test.ts b/apps/server/test/whiteboard.test.ts new file mode 100644 index 00000000..c8cc3e04 --- /dev/null +++ b/apps/server/test/whiteboard.test.ts @@ -0,0 +1,459 @@ +import { describe, expect, it, vi } from "vitest"; + +import { simplifyElements } from "../src/shared/whiteboard.js"; +import { isValidScene, MAX_ELEMENTS } from "../src/shared/whiteboard-store.js"; + +// ── mergeElements is not exported, so we test it indirectly through +// createWhiteboardHandlers. Import the module and extract the merge +// logic by testing the handlers with mocked deps. ── + +import { createWhiteboardHandlers } from "../src/server/mcp-whiteboard-handlers.js"; + +function rect(id: string, x = 0, y = 0) { + return { id, type: "rectangle", x, y, width: 100, height: 50 }; +} + +function text(id: string, t: string) { + return { + id, + type: "text", + x: 0, + y: 0, + width: 80, + height: 20, + text: t, + originalText: t, + }; +} + +function arrow(id: string, from: string, to: string) { + return { + id, + type: "arrow", + x: 100, + y: 50, + width: 100, + height: 0, + points: [ + [0, 0], + [100, 0], + ], + startBinding: { elementId: from, focus: 0, gap: 1 }, + endBinding: { elementId: to, focus: 0, gap: 1 }, + }; +} + +// ── simplifyElements ── + +describe("simplifyElements", () => { + it("converts raw elements to simplified format", () => { + const result = simplifyElements([ + rect("box1", 10, 20), + text("label1", "Hello"), + ]); + expect(result).toEqual([ + { id: "box1", type: "rectangle", x: 10, y: 20, width: 100, height: 50 }, + { + id: "label1", + type: "text", + x: 0, + y: 0, + width: 80, + height: 20, + text: "Hello", + }, + ]); + }); + + it("skips deleted elements", () => { + const result = simplifyElements([ + { ...rect("a"), isDeleted: true }, + rect("b"), + ]); + expect(result).toHaveLength(1); + expect(result[0].id).toBe("b"); + }); + + it("skips non-object values", () => { + const result = simplifyElements([ + null, + undefined, + 42, + "str", + rect("ok"), + ] as unknown[]); + expect(result).toHaveLength(1); + expect(result[0].id).toBe("ok"); + }); + + it("extracts arrow bindings as from/to", () => { + const result = simplifyElements([arrow("a1", "box1", "box2")]); + expect(result[0].from).toBe("box1"); + expect(result[0].to).toBe("box2"); + }); + + it("extracts containerId for bound text", () => { + const el = { ...text("lbl", "Hi"), containerId: "box1" }; + const result = simplifyElements([el]); + expect(result[0].containerId).toBe("box1"); + }); + + it("extracts frameId", () => { + const el = { ...rect("child"), frameId: "frame1" }; + const result = simplifyElements([el]); + expect(result[0].frameId).toBe("frame1"); + }); + + it("rounds angle and includes when non-zero", () => { + const el = { ...rect("rotated"), angle: 1.5708 }; + const result = simplifyElements([el]); + expect(result[0].angle).toBe(1.57); + }); + + it("omits angle when near zero", () => { + const el = { ...rect("flat"), angle: 0.005 }; + const result = simplifyElements([el]); + expect(result[0].angle).toBeUndefined(); + }); + + it("extracts colors (skips transparent bg)", () => { + const el = { + ...rect("colored"), + strokeColor: "#e03131", + backgroundColor: "transparent", + }; + const result = simplifyElements([el]); + expect(result[0].strokeColor).toBe("#e03131"); + expect(result[0].backgroundColor).toBeUndefined(); + }); + + it("includes non-transparent backgroundColor", () => { + const el = { ...rect("filled"), backgroundColor: "#a5d8ff" }; + const result = simplifyElements([el]); + expect(result[0].backgroundColor).toBe("#a5d8ff"); + }); +}); + +// ── isValidScene ── + +describe("isValidScene", () => { + it("accepts valid scene", () => { + expect(isValidScene({ elements: [rect("a")] })).toBe(true); + }); + + it("accepts empty scene", () => { + expect(isValidScene({ elements: [] })).toBe(true); + }); + + it("rejects null", () => { + expect(isValidScene(null)).toBe(false); + }); + + it("rejects non-object", () => { + expect(isValidScene("string")).toBe(false); + }); + + it("rejects missing elements", () => { + expect(isValidScene({ foo: "bar" })).toBe(false); + }); + + it("rejects non-array elements", () => { + expect(isValidScene({ elements: "not-array" })).toBe(false); + }); + + it("rejects oversized elements array", () => { + const elements = Array.from({ length: MAX_ELEMENTS + 1 }, (_, i) => + rect(`e${i}`) + ); + expect(isValidScene({ elements })).toBe(false); + }); + + it("accepts exactly MAX_ELEMENTS", () => { + const elements = Array.from({ length: MAX_ELEMENTS }, (_, i) => + rect(`e${i}`) + ); + expect(isValidScene({ elements })).toBe(true); + }); +}); + +// ── createWhiteboardHandlers (mergeElements + handler logic) ── + +describe("createWhiteboardHandlers", () => { + function createMockDeps() { + const publishedEvents: unknown[] = []; + return { + pool: { + query: vi.fn(), + } as unknown as import("pg").Pool, + mediaRoot: "/tmp/test-media", + agentManager: { + getAgent: vi.fn().mockResolvedValue({ id: "agt_test", mediaDir: null }), + } as unknown as import("../src/agents/manager.js").AgentManager, + publishUiEvent: vi.fn((e: unknown) => publishedEvents.push(e)), + publishedEvents, + }; + } + + function mockLoadReturn( + pool: { query: ReturnType }, + scene: { elements: unknown[] }, + version = 1 + ) { + pool.query.mockResolvedValueOnce({ + rows: [ + { + scene, + version: String(version), + updated_by: "agent", + updated_at: new Date(), + }, + ], + }); + } + + function mockSaveReturn( + pool: { query: ReturnType }, + version: number + ) { + pool.query.mockResolvedValueOnce({ + rows: [{ version: String(version) }], + }); + } + + function mockEmptyLoad(pool: { query: ReturnType }) { + pool.query.mockResolvedValueOnce({ rows: [] }); + } + + describe("updateWhiteboard", () => { + it("adds new elements to an empty board", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + + mockEmptyLoad( + deps.pool as unknown as { query: ReturnType } + ); + mockSaveReturn( + deps.pool as unknown as { query: ReturnType }, + 1 + ); + + const result = await handlers.updateWhiteboard( + "agt_test", + [rect("a"), rect("b")], + [] + ); + expect(result.addedIds).toEqual(["a", "b"]); + expect(result.updatedIds).toEqual([]); + expect(result.elementCount).toBe(2); + expect(result.version).toBe(1); + }); + + it("upserts existing elements", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + const pool = deps.pool as unknown as { query: ReturnType }; + + mockLoadReturn(pool, { elements: [rect("a", 0, 0)] }, 1); + mockSaveReturn(pool, 2); + + const result = await handlers.updateWhiteboard( + "agt_test", + [rect("a", 50, 50), rect("b")], + [] + ); + expect(result.addedIds).toEqual(["b"]); + expect(result.updatedIds).toEqual(["a"]); + expect(result.elementCount).toBe(2); + }); + + it("deletes specified elements", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + const pool = deps.pool as unknown as { query: ReturnType }; + + mockLoadReturn(pool, { elements: [rect("a"), rect("b"), rect("c")] }, 1); + mockSaveReturn(pool, 2); + + const result = await handlers.updateWhiteboard( + "agt_test", + [], + ["a", "c"] + ); + expect(result.deletedIds).toEqual(["a", "c"]); + expect(result.elementCount).toBe(1); + }); + + it("publishes whiteboard.changed event", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + const pool = deps.pool as unknown as { query: ReturnType }; + + mockEmptyLoad(pool); + mockSaveReturn(pool, 1); + + await handlers.updateWhiteboard("agt_test", [rect("a")], []); + expect(deps.publishUiEvent).toHaveBeenCalledWith({ + type: "whiteboard.changed", + agentId: "agt_test", + version: 1, + source: "agent", + }); + }); + + it("retries on optimistic lock conflict", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + const pool = deps.pool as unknown as { query: ReturnType }; + + // First attempt: load version 1, save fails (conflict) + mockLoadReturn(pool, { elements: [] }, 1); + pool.query.mockResolvedValueOnce({ rows: [] }); // save fails + + // Second attempt: load version 2, save succeeds + mockLoadReturn(pool, { elements: [] }, 2); + mockSaveReturn(pool, 3); + + const result = await handlers.updateWhiteboard( + "agt_test", + [rect("a")], + [] + ); + expect(result.version).toBe(3); + }); + + it("throws after 3 failed attempts", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + const pool = deps.pool as unknown as { query: ReturnType }; + + for (let i = 0; i < 3; i++) { + mockLoadReturn(pool, { elements: [] }, i + 1); + pool.query.mockResolvedValueOnce({ rows: [] }); // save fails + } + + await expect( + handlers.updateWhiteboard("agt_test", [rect("a")], []) + ).rejects.toThrow("concurrently"); + }); + + it("throws for unknown agent", async () => { + const deps = createMockDeps(); + ( + deps.agentManager.getAgent as ReturnType + ).mockResolvedValueOnce(null); + const handlers = createWhiteboardHandlers(deps); + + await expect( + handlers.updateWhiteboard("agt_missing", [rect("a")], []) + ).rejects.toThrow("Agent not found"); + }); + + it("throws when merged result exceeds MAX_ELEMENTS", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + const pool = deps.pool as unknown as { query: ReturnType }; + + const hugeScene = { + elements: Array.from({ length: MAX_ELEMENTS }, (_, i) => rect(`e${i}`)), + }; + mockLoadReturn(pool, hugeScene, 1); + + await expect( + handlers.updateWhiteboard("agt_test", [rect("new-one")], []) + ).rejects.toThrow("full"); + }); + + it("ignores incoming elements without required fields", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + const pool = deps.pool as unknown as { query: ReturnType }; + + mockEmptyLoad(pool); + mockSaveReturn(pool, 1); + + const result = await handlers.updateWhiteboard( + "agt_test", + [ + { id: "valid", type: "rectangle", x: 0, y: 0 }, + { type: "rectangle" }, // missing id + { id: "no-type" }, // missing type + null as unknown as Record, + ], + [] + ); + expect(result.addedIds).toEqual(["valid"]); + expect(result.elementCount).toBe(1); + }); + }); + + describe("clearWhiteboard", () => { + it("clears board and publishes event", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + const pool = deps.pool as unknown as { query: ReturnType }; + + mockLoadReturn(pool, { elements: [rect("a")] }, 1); + mockSaveReturn(pool, 2); + + await handlers.clearWhiteboard("agt_test"); + expect(deps.publishUiEvent).toHaveBeenCalledWith({ + type: "whiteboard.changed", + agentId: "agt_test", + version: 2, + source: "agent", + }); + }); + + it("throws for unknown agent", async () => { + const deps = createMockDeps(); + ( + deps.agentManager.getAgent as ReturnType + ).mockResolvedValueOnce(null); + const handlers = createWhiteboardHandlers(deps); + + await expect(handlers.clearWhiteboard("agt_missing")).rejects.toThrow( + "Agent not found" + ); + }); + }); + + describe("getWhiteboard", () => { + it("returns empty state for nonexistent whiteboard", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + const pool = deps.pool as unknown as { query: ReturnType }; + + mockEmptyLoad(pool); + + const result = await handlers.getWhiteboard("agt_test"); + expect(result.elements).toEqual([]); + expect(result.version).toBe(0); + expect(result.updatedAt).toBeNull(); + }); + + it("returns simplified elements for existing board", async () => { + const deps = createMockDeps(); + const handlers = createWhiteboardHandlers(deps); + const pool = deps.pool as unknown as { query: ReturnType }; + + const now = new Date(); + pool.query.mockResolvedValueOnce({ + rows: [ + { + scene: { elements: [rect("a", 10, 20)] }, + version: "3", + updated_by: "agent", + updated_at: now, + }, + ], + }); + + const result = await handlers.getWhiteboard("agt_test"); + expect(result.elements).toEqual([ + { id: "a", type: "rectangle", x: 10, y: 20, width: 100, height: 50 }, + ]); + expect(result.version).toBe(3); + expect(result.updatedAt).toBe(now.toISOString()); + }); + }); +}); diff --git a/apps/web/package.json b/apps/web/package.json index 22a2fd3f..9c8eaa15 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,6 +13,7 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { + "@excalidraw/excalidraw": "^0.18.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.16", diff --git a/apps/web/src/components/app/agents-view.tsx b/apps/web/src/components/app/agents-view.tsx index b5f9ba8d..624b049c 100644 --- a/apps/web/src/components/app/agents-view.tsx +++ b/apps/web/src/components/app/agents-view.tsx @@ -9,8 +9,12 @@ import { import { createPortal } from "react-dom"; import { Routes, Route, useNavigate, useParams } from "react-router-dom"; import { PanelLeftOpen, PanelRightOpen, Split } from "lucide-react"; +import { useAtomValue } from "jotai"; + +import { whiteboardAgentDrewAtomFamily } from "@/lib/store"; import { ChangesTab } from "@/components/app/changes-tab"; +import { WhiteboardPane } from "@/components/app/whiteboard-pane"; import { ChangesSettingsPopover } from "@/components/app/changes-settings-popover"; import { CenterPaneTabBar, @@ -136,6 +140,7 @@ export function AgentsView({ const { changesMatch, + whiteboardMatch, feedbackDetail, feedbackDetailRendered, handleFeedbackTransitionEnd, @@ -238,6 +243,10 @@ export function AgentsView({ ? (agents.find((agent) => agent.id === focusedAgentId) ?? null) : null; + const whiteboardAgentDrew = useAtomValue( + whiteboardAgentDrewAtomFamily(focusedAgentId ?? "") + ); + const { splitState, isSplit, exitSplit, updateSizes, handleTabDrop } = useSplitPane(focusedAgentId, isMobile); @@ -678,7 +687,13 @@ export function AgentsView({ {focusedAgent.name} { if (isSplit) { exitSplit(); @@ -686,6 +701,7 @@ export function AgentsView({ onTabChange(tab); }} diffStats={focusedDiffStats} + whiteboardAgentDrew={whiteboardAgentDrew} isSplit={isSplit} splitState={splitState} isMobile={isMobile} @@ -811,11 +827,18 @@ export function AgentsView({ <>
+ )} {stableTerminalContainerRef.current diff --git a/apps/web/src/components/app/center-pane-tab-bar.tsx b/apps/web/src/components/app/center-pane-tab-bar.tsx index e8ba9393..357c01aa 100644 --- a/apps/web/src/components/app/center-pane-tab-bar.tsx +++ b/apps/web/src/components/app/center-pane-tab-bar.tsx @@ -15,12 +15,14 @@ type TabDef = { const TABS: TabDef[] = [ { id: "terminal", label: "Terminal" }, { id: "changes", label: "Changes" }, + { id: "whiteboard", label: "Whiteboard" }, ]; type CenterPaneTabBarProps = { activeTab: CenterTab; onTabChange: (tab: CenterTab) => void; diffStats: DiffStats | null | undefined; + whiteboardAgentDrew?: boolean; isSplit: boolean; splitState: SplitPaneState; isMobile: boolean; @@ -30,6 +32,7 @@ export const CenterPaneTabBar = memo(function CenterPaneTabBar({ activeTab, onTabChange, diffStats, + whiteboardAgentDrew = false, isSplit, splitState, isMobile, @@ -81,6 +84,14 @@ export const CenterPaneTabBar = memo(function CenterPaneTabBar({ > {tab.label} + {tab.id === "whiteboard" && + whiteboardAgentDrew && + activeTab !== "whiteboard" ? ( + + ) : null} {activeTab === tab.id && !isSplit ? ( ) : null} diff --git a/apps/web/src/components/app/whiteboard-pane.tsx b/apps/web/src/components/app/whiteboard-pane.tsx new file mode 100644 index 00000000..3996cc16 --- /dev/null +++ b/apps/web/src/components/app/whiteboard-pane.tsx @@ -0,0 +1,44 @@ +import { lazy, Suspense, useEffect, useState } from "react"; +import { useAtom } from "jotai"; + +import { whiteboardAgentDrewAtomFamily } from "@/lib/store"; +import { cn } from "@/lib/utils"; + +const WhiteboardTab = lazy(() => import("@/components/app/whiteboard-tab")); + +type WhiteboardPaneProps = { + agentId: string | null; + active: boolean; +}; + +export function WhiteboardPane({ + agentId, + active, +}: WhiteboardPaneProps): JSX.Element | null { + const [opened, setOpened] = useState(false); + useEffect(() => { + if (active) setOpened(true); + }, [active]); + + const [agentDrew, setAgentDrew] = useAtom( + whiteboardAgentDrewAtomFamily(agentId ?? "") + ); + useEffect(() => { + if (active && agentId && agentDrew) setAgentDrew(false); + }, [active, agentId, agentDrew, setAgentDrew]); + + if (!opened || !agentId) return null; + return ( +
+ + Loading whiteboard… +
+ } + > + + +
+ ); +} diff --git a/apps/web/src/components/app/whiteboard-tab.tsx b/apps/web/src/components/app/whiteboard-tab.tsx new file mode 100644 index 00000000..e60fbd19 --- /dev/null +++ b/apps/web/src/components/app/whiteboard-tab.tsx @@ -0,0 +1,288 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { + Excalidraw, + exportToBlob, + getSceneVersion, +} from "@excalidraw/excalidraw"; +import type { + ExcalidrawImperativeAPI, + ExcalidrawInitialDataState, +} from "@excalidraw/excalidraw/types"; +import type { ExcalidrawElement } from "@excalidraw/excalidraw/element/types"; +import "@excalidraw/excalidraw/index.css"; + +import { + useWhiteboard, + whiteboardQueryKey, + type WhiteboardData, +} from "@/hooks/use-whiteboard"; +import { getThemeMode, useTheme } from "@/hooks/use-theme"; +import { api } from "@/lib/api"; + +declare global { + interface Window { + EXCALIDRAW_ASSET_PATH?: string; + } +} +window.EXCALIDRAW_ASSET_PATH = "/excalidraw/"; + +const SAVE_DEBOUNCE_MS = 1000; +const SNAPSHOT_DEBOUNCE_MS = 4000; + +type WhiteboardTabProps = { + agentId: string; + visible: boolean; +}; + +export default function WhiteboardTab({ + agentId, + visible, +}: WhiteboardTabProps): JSX.Element { + const { data, isLoading, isError } = useWhiteboard(agentId); + + if (isLoading || (!data && !isError)) { + return ( +
+ Loading whiteboard… +
+ ); + } + if (isError || !data) { + return ( +
+ Could not load the whiteboard. +
+ ); + } + return ( + + ); +} + +function WhiteboardCanvas({ + agentId, + initial, + visible, +}: { + agentId: string; + initial: WhiteboardData; + visible: boolean; +}): JSX.Element { + const queryClient = useQueryClient(); + const { theme } = useTheme(); + const [excalidrawAPI, setExcalidrawAPI] = + useState(null); + const [boardEmpty, setBoardEmpty] = useState( + initial.scene.elements.length === 0 + ); + const { data } = useWhiteboard(agentId); + + const versionRef = useRef(initial.version); + const sceneVersionRef = useRef( + getSceneVersion(initial.scene.elements as readonly ExcalidrawElement[]) + ); + const saveTimerRef = useRef(undefined); + const snapshotTimerRef = useRef(undefined); + const savingRef = useRef(false); + const pointerDownRef = useRef(false); + const pendingRemoteRef = useRef(null); + + const initialData = useMemo( + () => ({ + elements: initial.scene.elements as readonly ExcalidrawElement[], + scrollToContent: true, + }), + [initial] + ); + + const persistSnapshot = useCallback(async () => { + if (!excalidrawAPI) return; + const elements = excalidrawAPI.getSceneElements(); + if (elements.length === 0) { + try { + await api(`/api/v1/agents/${agentId}/whiteboard/snapshot`, { + method: "DELETE", + }); + } catch {} + return; + } + try { + const blob = await exportToBlob({ + elements, + appState: { + ...excalidrawAPI.getAppState(), + exportBackground: true, + }, + files: excalidrawAPI.getFiles(), + mimeType: "image/png", + }); + const form = new FormData(); + form.append("file", blob, "whiteboard.png"); + await api(`/api/v1/agents/${agentId}/whiteboard/snapshot`, { + method: "POST", + body: form, + }); + } catch { + // Snapshot is best-effort; the scene itself is already persisted. + } + }, [agentId, excalidrawAPI]); + + const persistScene = useCallback(async () => { + if (!excalidrawAPI || savingRef.current) return; + const elements = excalidrawAPI.getSceneElements(); + const sceneVersion = getSceneVersion(elements); + if (sceneVersion === sceneVersionRef.current) return; + savingRef.current = true; + try { + const res = await api<{ version: number }>( + `/api/v1/agents/${agentId}/whiteboard`, + { + method: "PUT", + body: JSON.stringify({ + scene: { elements }, + baseVersion: versionRef.current, + }), + } + ); + versionRef.current = res.version; + sceneVersionRef.current = sceneVersion; + queryClient.setQueryData( + whiteboardQueryKey(agentId), + (old) => + old + ? { + ...old, + scene: { elements: [...elements] }, + version: res.version, + } + : old + ); + if (snapshotTimerRef.current !== undefined) { + window.clearTimeout(snapshotTimerRef.current); + } + snapshotTimerRef.current = window.setTimeout(() => { + void persistSnapshot(); + }, SNAPSHOT_DEBOUNCE_MS); + } catch { + void queryClient.invalidateQueries({ + queryKey: whiteboardQueryKey(agentId), + exact: true, + }); + } finally { + savingRef.current = false; + } + }, [agentId, excalidrawAPI, persistSnapshot, queryClient]); + + const scheduleSave = useCallback(() => { + if (excalidrawAPI) { + setBoardEmpty(excalidrawAPI.getSceneElements().length === 0); + } + if (saveTimerRef.current !== undefined) { + window.clearTimeout(saveTimerRef.current); + } + saveTimerRef.current = window.setTimeout(() => { + saveTimerRef.current = undefined; + void persistScene(); + }, SAVE_DEBOUNCE_MS); + }, [excalidrawAPI, persistScene]); + + const applyRemote = useCallback( + (remote: WhiteboardData) => { + if (!excalidrawAPI) return; + versionRef.current = remote.version; + sceneVersionRef.current = getSceneVersion( + remote.scene.elements as readonly ExcalidrawElement[] + ); + excalidrawAPI.updateScene({ + elements: remote.scene.elements as ExcalidrawElement[], + }); + setBoardEmpty(remote.scene.elements.length === 0); + if (snapshotTimerRef.current !== undefined) { + window.clearTimeout(snapshotTimerRef.current); + } + snapshotTimerRef.current = window.setTimeout(() => { + void persistSnapshot(); + }, SNAPSHOT_DEBOUNCE_MS); + }, + [excalidrawAPI, persistSnapshot] + ); + + useEffect(() => { + if (!data || data.version <= versionRef.current) return; + if (pointerDownRef.current) { + pendingRemoteRef.current = data; + return; + } + applyRemote(data); + }, [data, applyRemote]); + + useEffect(() => { + const onPointerUp = () => { + pointerDownRef.current = false; + const pending = pendingRemoteRef.current; + if (pending) { + pendingRemoteRef.current = null; + applyRemote(pending); + } + }; + window.addEventListener("pointerup", onPointerUp); + return () => window.removeEventListener("pointerup", onPointerUp); + }, [applyRemote]); + + useEffect(() => { + if (visible) return; + if (saveTimerRef.current !== undefined) { + window.clearTimeout(saveTimerRef.current); + saveTimerRef.current = undefined; + void persistScene(); + } + }, [visible, persistScene]); + + useEffect(() => { + return () => { + if (saveTimerRef.current !== undefined) { + window.clearTimeout(saveTimerRef.current); + void persistScene(); + } + if (snapshotTimerRef.current !== undefined) { + window.clearTimeout(snapshotTimerRef.current); + void persistSnapshot(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
{ + pointerDownRef.current = true; + }} + > + {boardEmpty ? ( +
+

+ Sketch here — your agent can see this board. Ask it to “look + at the whiteboard” in the terminal. +

+
+ ) : null} + +
+ ); +} diff --git a/apps/web/src/hooks/use-agents-view-routing.ts b/apps/web/src/hooks/use-agents-view-routing.ts index 6fc17d42..d934e898 100644 --- a/apps/web/src/hooks/use-agents-view-routing.ts +++ b/apps/web/src/hooks/use-agents-view-routing.ts @@ -8,6 +8,7 @@ import { agentFeedbackRoute, agentReviewRoute, agentRoute, + agentWhiteboardRoute, } from "@/lib/agent-routes"; type UseAgentsViewRoutingOptions = { @@ -27,6 +28,7 @@ export function useAgentsViewRouting({ const feedbackMatch = useMatch("/agents/:agentId/feedback/:itemId"); const reviewMatch = useMatch("/agents/:agentId/review/:summaryAgentId"); const changesMatch = useMatch("/agents/:agentId/changes"); + const whiteboardMatch = useMatch("/agents/:agentId/whiteboard"); const itemId = feedbackMatch?.params.itemId; const summaryAgentId = reviewMatch?.params.summaryAgentId; @@ -74,12 +76,14 @@ export function useAgentsViewRouting({ }, [agents, agentsLoaded, navigate, routeAgentId, summaryAgentId]); const onTabChange = useCallback( - (tab: "terminal" | "changes") => { + (tab: "terminal" | "changes" | "whiteboard") => { if (!routeAgentId) return; navigate( tab === "changes" ? agentChangesRoute(routeAgentId) - : agentRoute(routeAgentId), + : tab === "whiteboard" + ? agentWhiteboardRoute(routeAgentId) + : agentRoute(routeAgentId), { replace: true } ); }, @@ -128,6 +132,7 @@ export function useAgentsViewRouting({ return { changesMatch: !!changesMatch, + whiteboardMatch: !!whiteboardMatch, feedbackDetail, feedbackDetailRendered, handleFeedbackTransitionEnd, diff --git a/apps/web/src/hooks/use-sse.ts b/apps/web/src/hooks/use-sse.ts index 1dce706a..a19a3a4a 100644 --- a/apps/web/src/hooks/use-sse.ts +++ b/apps/web/src/hooks/use-sse.ts @@ -1,5 +1,6 @@ import { useEffect, useRef } from "react"; import { useQueryClient } from "@tanstack/react-query"; +import { useStore } from "jotai"; import { type Agent, type AuthState, @@ -11,6 +12,7 @@ import { agentDiffQueryKey } from "@/hooks/use-agent-diff"; import { diffStatsQueryKey } from "@/hooks/use-agent-diff-stats"; import { sortAgentsByCreatedAtDesc } from "@/lib/agent-sort"; import { recordSSEEvent, recordSSEReconnect } from "@/lib/energy-metrics"; +import { whiteboardAgentDrewAtomFamily } from "@/lib/store"; import { showWebNotification } from "@/lib/web-notifications"; import { CACHED_RELEASE_INFO_QUERY_KEY, @@ -32,6 +34,12 @@ type UiEvent = } | { type: "agent.deleted"; agentId: string } | { type: "media.changed"; agentId: string } + | { + type: "whiteboard.changed"; + agentId: string; + version: number; + source: "user" | "agent"; + } | { type: "media.seen"; agentId: string; keys: string[] } | { type: "stream.started"; agentId: string } | { type: "stream.stopped"; agentId: string } @@ -70,6 +78,7 @@ function patchAgentHasStream( export function useSSE(authState: AuthState): void { const queryClient = useQueryClient(); + const jotaiStore = useStore(); const eventSourceRef = useRef(null); useEffect(() => { @@ -92,6 +101,7 @@ export function useSSE(authState: AuthState): void { void queryClient.invalidateQueries({ queryKey: ["jobs"] }); void queryClient.invalidateQueries({ queryKey: ["templates"] }); void queryClient.invalidateQueries({ queryKey: ["brain"] }); + void queryClient.invalidateQueries({ queryKey: ["whiteboard"] }); void queryClient.invalidateQueries({ queryKey: CACHED_RELEASE_INFO_QUERY_KEY, }); @@ -157,6 +167,20 @@ export function useSSE(authState: AuthState): void { return; } + if (payload.type === "whiteboard.changed") { + if (payload.source === "agent") { + void queryClient.invalidateQueries({ + queryKey: ["whiteboard", payload.agentId], + exact: true, + }); + jotaiStore.set( + whiteboardAgentDrewAtomFamily(payload.agentId), + true + ); + } + return; + } + if (payload.type === "media.seen") { const seen = new Set(payload.keys); queryClient.setQueryData( @@ -273,5 +297,5 @@ export function useSSE(authState: AuthState): void { document.removeEventListener("visibilitychange", onVisChange); closeSSE(); }; - }, [authState, queryClient]); + }, [authState, queryClient, jotaiStore]); } diff --git a/apps/web/src/hooks/use-theme.ts b/apps/web/src/hooks/use-theme.ts index df5085a2..36399a56 100644 --- a/apps/web/src/hooks/use-theme.ts +++ b/apps/web/src/hooks/use-theme.ts @@ -417,6 +417,10 @@ function applyTheme(themeId: ThemeId): void { } } +export function getThemeMode(themeId: ThemeId): "light" | "dark" { + return THEMES.find((t) => t.id === themeId)?.mode ?? "dark"; +} + export function useTheme(): { theme: ThemeId; setTheme: (id: ThemeId) => void; diff --git a/apps/web/src/hooks/use-whiteboard.ts b/apps/web/src/hooks/use-whiteboard.ts new file mode 100644 index 00000000..2150cc5f --- /dev/null +++ b/apps/web/src/hooks/use-whiteboard.ts @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; + +import { api } from "@/lib/api"; + +export type WhiteboardData = { + scene: { elements: unknown[] }; + version: number; + updatedAt: string | null; +}; + +export function whiteboardQueryKey(agentId: string): [string, string] { + return ["whiteboard", agentId]; +} + +export function useWhiteboard(agentId: string | null) { + return useQuery({ + queryKey: whiteboardQueryKey(agentId ?? ""), + queryFn: () => api(`/api/v1/agents/${agentId}/whiteboard`), + enabled: !!agentId, + staleTime: Infinity, + }); +} diff --git a/apps/web/src/lib/agent-routes.ts b/apps/web/src/lib/agent-routes.ts index 341813e7..914fb519 100644 --- a/apps/web/src/lib/agent-routes.ts +++ b/apps/web/src/lib/agent-routes.ts @@ -6,6 +6,10 @@ export function agentChangesRoute(agentId: string): string { return `/agents/${agentId}/changes`; } +export function agentWhiteboardRoute(agentId: string): string { + return `/agents/${agentId}/whiteboard`; +} + export function agentFeedbackRoute(agentId: string, itemId: number): string { return `/agents/${agentId}/feedback/${itemId}`; } diff --git a/apps/web/src/lib/store.ts b/apps/web/src/lib/store.ts index 55020051..19422f1a 100644 --- a/apps/web/src/lib/store.ts +++ b/apps/web/src/lib/store.ts @@ -92,6 +92,10 @@ export const dismissedReleaseToastAtomFamily = atomFamily((tag: string) => atomWithLocalStorage(`dispatch:dismissedReleaseToast:${tag}`, false) ); +export const whiteboardAgentDrewAtomFamily = atomFamily((_agentId: string) => + atom(false) +); + export type DiffViewType = "unified" | "split"; export const diffViewTypeAtom = atomWithLocalStorage( @@ -279,7 +283,7 @@ export function reconcileDiffViewStateStorage( // Split pane state — per-agent split/single mode and pane sizes // --------------------------------------------------------------------------- -export type CenterTab = "terminal" | "changes"; +export type CenterTab = "terminal" | "changes" | "whiteboard"; export type SplitPaneState = { mode: "single" | "split"; diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index debbf27f..bf90a2e5 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,11 +1,49 @@ -import { defineConfig } from "vite"; +import { defineConfig, type Plugin } from "vite"; import react from "@vitejs/plugin-react"; import { VitePWA } from "vite-plugin-pwa"; import path from "node:path"; -import { readFileSync } from "node:fs"; +import { readFileSync, cpSync, existsSync } from "node:fs"; const isProd = process.env.NODE_ENV === "production"; +function excalidrawAssets(): Plugin { + const fontsDir = path.resolve( + __dirname, + "node_modules/@excalidraw/excalidraw/dist/prod/fonts" + ); + const publicPrefix = "/excalidraw/fonts/"; + const skipFamilies = new Set(["Xiaolai"]); + return { + name: "excalidraw-assets", + configureServer(server) { + server.middlewares.use((req, res, next) => { + const url = (req.url ?? "").split("?")[0]; + if (!url.startsWith(publicPrefix)) return next(); + const rel = decodeURIComponent(url.slice(publicPrefix.length)); + const file = path.join(fontsDir, rel); + if (!file.startsWith(fontsDir) || !existsSync(file)) { + res.statusCode = 404; + return res.end(); + } + res.setHeader( + "Content-Type", + file.endsWith(".woff2") ? "font/woff2" : "font/woff" + ); + return res.end(readFileSync(file)); + }); + }, + writeBundle(options) { + const outDir = options.dir ?? path.resolve(__dirname, "dist"); + cpSync(fontsDir, path.join(outDir, "excalidraw/fonts"), { + recursive: true, + filter: (src) => + !skipFamilies.has(path.basename(path.dirname(src))) && + !skipFamilies.has(path.basename(src)), + }); + }, + }; +} + // Bake the workspace version into the bundle. The web client compares // this against the `X-Dispatch-Version` response header to detect a // stale bundle after a server self-update. @@ -35,6 +73,7 @@ export default defineConfig({ }, plugins: [ react(), + excalidrawAssets(), isProd && VitePWA({ registerType: "prompt", diff --git a/e2e/whiteboard.spec.ts b/e2e/whiteboard.spec.ts new file mode 100644 index 00000000..8ef4fdb0 --- /dev/null +++ b/e2e/whiteboard.spec.ts @@ -0,0 +1,231 @@ +import { test, expect } from "@playwright/test"; +import { + cleanupE2EAgents, + clickAgentRow, + createAgentViaAPI, + loadApp, +} from "./helpers"; + +const AUTH_HEADER = { + Authorization: `Bearer ${process.env.AUTH_TOKEN ?? "dev-token"}`, +}; + +async function callMcpTool( + request: Parameters[1]>[0]["request"], + agentId: string, + toolName: string, + args: Record +): Promise> { + const res = await request.fetch(`/api/mcp/${agentId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + }, + data: { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: toolName, arguments: args }, + }, + }); + const text = await res.text(); + const dataLine = text.split("\n").find((l) => l.startsWith("data: ")); + if (!dataLine) throw new Error(`No data line in MCP response: ${text}`); + return JSON.parse(dataLine.slice("data: ".length)) as Record; +} + +test.describe("Whiteboard", () => { + test.afterEach(async ({ request }) => { + await cleanupE2EAgents(request); + }); + + test("whiteboard tab is visible and navigates", async ({ page, request }) => { + const agent = await createAgentViaAPI(request, { + name: `e2e-agent-wb-${Date.now()}`, + }); + await loadApp(page); + await clickAgentRow(page, agent.id); + + const wbTab = page.getByTestId("center-tab-whiteboard"); + await expect(wbTab).toBeVisible(); + await wbTab.click(); + + await page.waitForURL(/\/agents\/[^/]+\/whiteboard/); + }); + + test("whiteboard REST API: PUT persists and GET retrieves scene", async ({ + request, + }) => { + const agent = await createAgentViaAPI(request, { + name: `e2e-agent-wb-api-${Date.now()}`, + }); + + // GET should return empty scene initially + const getRes1 = await request.get(`/api/v1/agents/${agent.id}/whiteboard`, { + headers: AUTH_HEADER, + }); + expect(getRes1.ok()).toBe(true); + const data1 = (await getRes1.json()) as { + scene: { elements: unknown[] }; + version: number; + }; + expect(data1.scene.elements).toEqual([]); + expect(data1.version).toBe(0); + + // PUT a scene + const scene = { + elements: [ + { + id: "test-box", + type: "rectangle", + x: 100, + y: 100, + width: 160, + height: 70, + }, + ], + }; + const putRes = await request.put(`/api/v1/agents/${agent.id}/whiteboard`, { + headers: { ...AUTH_HEADER, "content-type": "application/json" }, + data: { scene, baseVersion: 0 }, + }); + expect(putRes.ok()).toBe(true); + const putData = (await putRes.json()) as { ok: boolean; version: number }; + expect(putData.ok).toBe(true); + expect(putData.version).toBe(1); + + // GET should now return the scene + const getRes2 = await request.get(`/api/v1/agents/${agent.id}/whiteboard`, { + headers: AUTH_HEADER, + }); + const data2 = (await getRes2.json()) as { + scene: { elements: unknown[] }; + version: number; + }; + expect(data2.scene.elements).toHaveLength(1); + expect((data2.scene.elements[0] as { id: string }).id).toBe("test-box"); + expect(data2.version).toBe(1); + }); + + test("whiteboard REST API: PUT rejects stale baseVersion with 409", async ({ + request, + }) => { + const agent = await createAgentViaAPI(request, { + name: `e2e-agent-wb-conflict-${Date.now()}`, + }); + + const scene = { + elements: [ + { id: "a", type: "rectangle", x: 0, y: 0, width: 100, height: 50 }, + ], + }; + + // First PUT succeeds + const putRes1 = await request.put(`/api/v1/agents/${agent.id}/whiteboard`, { + headers: { ...AUTH_HEADER, "content-type": "application/json" }, + data: { scene, baseVersion: 0 }, + }); + expect(putRes1.ok()).toBe(true); + + // Second PUT with stale baseVersion=0 should get 409 + const putRes2 = await request.put(`/api/v1/agents/${agent.id}/whiteboard`, { + headers: { ...AUTH_HEADER, "content-type": "application/json" }, + data: { scene, baseVersion: 0 }, + }); + expect(putRes2.status()).toBe(409); + const conflictData = (await putRes2.json()) as { + error: string; + version: number; + }; + expect(conflictData.error).toContain("modified"); + expect(conflictData.version).toBe(1); + }); + + test("whiteboard MCP tool: agent can update and read whiteboard", async ({ + request, + }) => { + const agent = await createAgentViaAPI(request, { + name: `e2e-agent-wb-mcp-${Date.now()}`, + }); + + // Call whiteboard_update via MCP + const updateJson = await callMcpTool( + request, + agent.id, + "whiteboard_update", + { + elements: [ + { + id: "api-box", + type: "rectangle", + x: 100, + y: 100, + width: 160, + height: 70, + backgroundColor: "#a5d8ff", + }, + { + id: "api-label", + type: "text", + x: 110, + y: 120, + width: 140, + height: 25, + text: "API Server", + originalText: "API Server", + containerId: "api-box", + }, + ], + } + ); + + const updateResult = updateJson.result as { + content?: Array<{ text?: string }>; + }; + const updateContent = JSON.parse( + updateResult?.content?.[0]?.text ?? "{}" + ) as { + ok: boolean; + addedIds: string[]; + elementCount: number; + }; + expect(updateContent.ok).toBe(true); + expect(updateContent.addedIds).toEqual(["api-box", "api-label"]); + expect(updateContent.elementCount).toBe(2); + + // Call whiteboard_get via MCP + const getJson = await callMcpTool(request, agent.id, "whiteboard_get", {}); + const getResult = getJson.result as { + structuredContent?: { + elementCount: number; + elements: Array<{ id: string }>; + }; + }; + expect(getResult?.structuredContent?.elementCount).toBe(2); + expect( + getResult?.structuredContent?.elements?.map((e) => e.id).sort() + ).toEqual(["api-box", "api-label"]); + + // Call whiteboard_clear via MCP + const clearJson = await callMcpTool( + request, + agent.id, + "whiteboard_clear", + {} + ); + const clearResult = clearJson.result as { + content?: Array<{ text?: string }>; + }; + expect(clearResult?.content?.[0]?.text).toContain("ok"); + + // Verify board is empty via REST + const getRes2 = await request.get(`/api/v1/agents/${agent.id}/whiteboard`, { + headers: AUTH_HEADER, + }); + const data2 = (await getRes2.json()) as { + scene: { elements: unknown[] }; + }; + expect(data2.scene.elements).toEqual([]); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc467e22..1e2f099f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,16 +85,19 @@ importers: version: 8.18.1 "@vitest/coverage-v8": specifier: 4.1.2 - version: 4.1.2(vitest@4.1.2(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.2(vitest@4.1.2(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) vite: specifier: ^6.0.0 - version: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + version: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^4.0.18 - version: 4.1.2(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.2(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) apps/web: dependencies: + "@excalidraw/excalidraw": + specifier: ^0.18.1 + version: 0.18.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(immer@11.1.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) "@radix-ui/react-checkbox": specifier: ^1.3.3 version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -221,10 +224,10 @@ importers: version: 18.3.7(@types/react@18.3.28) "@vitejs/plugin-react": specifier: ^4.7.0 - version: 4.7.0(vite@5.4.21(@types/node@24.12.0)(terser@5.46.1)) + version: 4.7.0(vite@5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1)) "@vitest/coverage-v8": specifier: 2.1.9 - version: 2.1.9(vitest@2.1.9(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(terser@5.46.1)) + version: 2.1.9(vitest@2.1.9(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(sass@1.51.0)(terser@5.46.1)) autoprefixer: specifier: ^10.4.20 version: 10.4.27(postcss@8.5.8) @@ -257,13 +260,13 @@ importers: version: 8.57.2(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) vite: specifier: ^5.4.21 - version: 5.4.21(@types/node@24.12.0)(terser@5.46.1) + version: 5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1) vite-plugin-pwa: specifier: ^1.2.0 - version: 1.2.0(vite@5.4.21(@types/node@24.12.0)(terser@5.46.1))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0) + version: 1.2.0(vite@5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0) vitest: specifier: ^2.1.0 - version: 2.1.9(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(terser@5.46.1) + version: 2.1.9(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(sass@1.51.0)(terser@5.46.1) packages: "@alloc/quick-lru@5.2.0": @@ -1126,6 +1129,12 @@ packages: } engines: { node: ">=18" } + "@braintree/sanitize-url@6.0.2": + resolution: + { + integrity: sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==, + } + "@braintree/sanitize-url@7.1.2": resolution: { @@ -1139,12 +1148,42 @@ packages: } hasBin: true + "@chevrotain/cst-dts-gen@11.0.3": + resolution: + { + integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==, + } + + "@chevrotain/gast@11.0.3": + resolution: + { + integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==, + } + + "@chevrotain/regexp-to-ast@11.0.3": + resolution: + { + integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==, + } + + "@chevrotain/types@11.0.3": + resolution: + { + integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==, + } + "@chevrotain/types@11.1.2": resolution: { integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==, } + "@chevrotain/utils@11.0.3": + resolution: + { + integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==, + } + "@csstools/color-helpers@6.0.2": resolution: { @@ -1951,6 +1990,40 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + "@excalidraw/excalidraw@0.18.1": + resolution: + { + integrity: sha512-6i5Gt7IDTOH//qa0Z315Ly5iVRhjWpu2whrlQFqkuwrkKUWgRsMk0P5qdE7bpyDpai7jeLeWYkyj1eVAfni1lw==, + } + peerDependencies: + react: ^17.0.2 || ^18.2.0 || ^19.0.0 + react-dom: ^17.0.2 || ^18.2.0 || ^19.0.0 + + "@excalidraw/laser-pointer@1.3.1": + resolution: + { + integrity: sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g==, + } + + "@excalidraw/markdown-to-text@0.1.2": + resolution: + { + integrity: sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==, + } + + "@excalidraw/mermaid-to-excalidraw@2.2.2": + resolution: + { + integrity: sha512-5VKQq5CdRocC82vOIUpQ5ufJOVV9FpBTdHGA+ULqazeIVV+cr299877omQCibsdS3Bpitz2fsnTwnIXEmLVDSg==, + } + + "@excalidraw/random-username@1.1.0": + resolution: + { + integrity: sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA==, + } + engines: { node: ">=10" } + "@exodus/bytes@1.15.0": resolution: { @@ -2388,6 +2461,12 @@ packages: } engines: { node: ">=8" } + "@mermaid-js/parser@0.6.3": + resolution: + { + integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==, + } + "@mermaid-js/parser@1.1.1": resolution: { @@ -2455,12 +2534,40 @@ packages: integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==, } + "@radix-ui/primitive@1.0.0": + resolution: + { + integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==, + } + + "@radix-ui/primitive@1.1.1": + resolution: + { + integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==, + } + "@radix-ui/primitive@1.1.3": resolution: { integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==, } + "@radix-ui/react-arrow@1.1.2": + resolution: + { + integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-arrow@1.1.7": resolution: { @@ -2493,6 +2600,15 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-collection@1.0.1": + resolution: + { + integrity: sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + "@radix-ui/react-collection@1.1.7": resolution: { @@ -2509,6 +2625,26 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-compose-refs@1.0.0": + resolution: + { + integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + "@radix-ui/react-compose-refs@1.1.1": + resolution: + { + integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@radix-ui/react-compose-refs@1.1.2": resolution: { @@ -2521,6 +2657,26 @@ packages: "@types/react": optional: true + "@radix-ui/react-context@1.0.0": + resolution: + { + integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + "@radix-ui/react-context@1.1.1": + resolution: + { + integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@radix-ui/react-context@1.1.2": resolution: { @@ -2549,6 +2705,14 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-direction@1.0.0": + resolution: + { + integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + "@radix-ui/react-direction@1.1.1": resolution: { @@ -2577,6 +2741,22 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-dismissable-layer@1.1.5": + resolution: + { + integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-dropdown-menu@2.1.16": resolution: { @@ -2593,6 +2773,18 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-focus-guards@1.1.1": + resolution: + { + integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@radix-ui/react-focus-guards@1.1.3": resolution: { @@ -2605,6 +2797,22 @@ packages: "@types/react": optional: true + "@radix-ui/react-focus-scope@1.1.2": + resolution: + { + integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-focus-scope@1.1.7": resolution: { @@ -2621,6 +2829,26 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-id@1.0.0": + resolution: + { + integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + "@radix-ui/react-id@1.1.0": + resolution: + { + integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@radix-ui/react-id@1.1.1": resolution: { @@ -2665,6 +2893,38 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-popover@1.1.6": + resolution: + { + integrity: sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-popper@1.2.2": + resolution: + { + integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-popper@1.2.8": resolution: { @@ -2681,6 +2941,22 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-portal@1.1.4": + resolution: + { + integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-portal@1.1.9": resolution: { @@ -2697,6 +2973,31 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-presence@1.0.0": + resolution: + { + integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + "@radix-ui/react-presence@1.1.2": + resolution: + { + integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-presence@1.1.5": resolution: { @@ -2713,6 +3014,31 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-primitive@1.0.1": + resolution: + { + integrity: sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + "@radix-ui/react-primitive@2.0.2": + resolution: + { + integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-primitive@2.1.3": resolution: { @@ -2745,6 +3071,15 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-roving-focus@1.0.2": + resolution: + { + integrity: sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + "@radix-ui/react-roving-focus@1.1.11": resolution: { @@ -2793,6 +3128,26 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-slot@1.0.1": + resolution: + { + integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + "@radix-ui/react-slot@1.1.2": + resolution: + { + integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@radix-ui/react-slot@1.2.3": resolution: { @@ -2817,6 +3172,15 @@ packages: "@types/react": optional: true + "@radix-ui/react-tabs@1.0.2": + resolution: + { + integrity: sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + "@radix-ui/react-tooltip@1.2.8": resolution: { @@ -2824,19 +3188,107 @@ packages: } peerDependencies: "@types/react": "*" - "@types/react-dom": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@radix-ui/react-use-callback-ref@1.0.0": + resolution: + { + integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + "@radix-ui/react-use-callback-ref@1.1.0": + resolution: + { + integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-callback-ref@1.1.1": + resolution: + { + integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-controllable-state@1.0.0": + resolution: + { + integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + "@radix-ui/react-use-controllable-state@1.1.0": + resolution: + { + integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-controllable-state@1.2.2": + resolution: + { + integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-effect-event@0.0.2": + resolution: + { + integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + + "@radix-ui/react-use-escape-keydown@1.1.0": + resolution: + { + integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==, + } + peerDependencies: + "@types/react": "*" react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: "@types/react": optional: true - "@types/react-dom": - optional: true - "@radix-ui/react-use-callback-ref@1.1.1": + "@radix-ui/react-use-escape-keydown@1.1.1": resolution: { - integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==, + integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==, } peerDependencies: "@types/react": "*" @@ -2845,10 +3297,18 @@ packages: "@types/react": optional: true - "@radix-ui/react-use-controllable-state@1.2.2": + "@radix-ui/react-use-layout-effect@1.0.0": resolution: { - integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==, + integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==, + } + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + "@radix-ui/react-use-layout-effect@1.1.0": + resolution: + { + integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==, } peerDependencies: "@types/react": "*" @@ -2857,10 +3317,10 @@ packages: "@types/react": optional: true - "@radix-ui/react-use-effect-event@0.0.2": + "@radix-ui/react-use-layout-effect@1.1.1": resolution: { - integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==, + integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==, } peerDependencies: "@types/react": "*" @@ -2869,10 +3329,10 @@ packages: "@types/react": optional: true - "@radix-ui/react-use-escape-keydown@1.1.1": + "@radix-ui/react-use-previous@1.1.1": resolution: { - integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==, + integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==, } peerDependencies: "@types/react": "*" @@ -2881,10 +3341,10 @@ packages: "@types/react": optional: true - "@radix-ui/react-use-layout-effect@1.1.1": + "@radix-ui/react-use-rect@1.1.0": resolution: { - integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==, + integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==, } peerDependencies: "@types/react": "*" @@ -2893,10 +3353,10 @@ packages: "@types/react": optional: true - "@radix-ui/react-use-previous@1.1.1": + "@radix-ui/react-use-rect@1.1.1": resolution: { - integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==, + integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==, } peerDependencies: "@types/react": "*" @@ -2905,10 +3365,10 @@ packages: "@types/react": optional: true - "@radix-ui/react-use-rect@1.1.1": + "@radix-ui/react-use-size@1.1.0": resolution: { - integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==, + integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==, } peerDependencies: "@types/react": "*" @@ -2945,6 +3405,12 @@ packages: "@types/react-dom": optional: true + "@radix-ui/rect@1.1.0": + resolution: + { + integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==, + } + "@radix-ui/rect@1.1.1": resolution: { @@ -4290,6 +4756,12 @@ packages: } engines: { node: ">=8" } + browser-fs-access@0.29.1: + resolution: + { + integrity: sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw==, + } + browserslist@4.28.1: resolution: { @@ -4359,6 +4831,12 @@ packages: integrity: sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==, } + canvas-roundrect-polyfill@0.0.1: + resolution: + { + integrity: sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw==, + } + ccount@2.0.1: resolution: { @@ -4417,6 +4895,20 @@ packages: } engines: { node: ">= 16" } + chevrotain-allstar@0.3.1: + resolution: + { + integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==, + } + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: + { + integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==, + } + chokidar@3.6.0: resolution: { @@ -4457,6 +4949,13 @@ packages: } engines: { node: ">=12" } + clsx@1.1.1: + resolution: + { + integrity: sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==, + } + engines: { node: ">=6" } + clsx@2.1.1: resolution: { @@ -4611,6 +5110,13 @@ packages: integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==, } + crc-32@0.3.0: + resolution: + { + integrity: sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==, + } + engines: { node: ">=0.8" } + croner@10.0.1: resolution: { @@ -4625,6 +5131,14 @@ packages: } hasBin: true + cross-env@7.0.3: + resolution: + { + integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==, + } + engines: { node: ">=10.14", npm: ">=6", yarn: ">=1" } + hasBin: true + cross-spawn@7.0.6: resolution: { @@ -5284,6 +5798,13 @@ packages: integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==, } + es6-promise-pool@2.5.0: + resolution: + { + integrity: sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==, + } + engines: { node: ">=0.10.0" } + esbuild@0.21.5: resolution: { @@ -5677,6 +6198,13 @@ packages: integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==, } + fractional-indexing@3.2.0: + resolution: + { + integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==, + } + engines: { node: ^14.13.1 || >=16.0.0 } + framer-motion@12.38.0: resolution: { @@ -5743,6 +6271,13 @@ packages: integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==, } + fuzzy@0.1.3: + resolution: + { + integrity: sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==, + } + engines: { node: ">= 0.6.0" } + generator-function@2.0.1: resolution: { @@ -5869,6 +6404,12 @@ packages: } engines: { node: ">= 0.4" } + glur@1.1.2: + resolution: + { + integrity: sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==, + } + gopd@1.2.0: resolution: { @@ -6049,6 +6590,12 @@ packages: } engines: { node: ">= 4" } + image-blob-reduce@3.0.1: + resolution: + { + integrity: sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q==, + } + immer@10.2.0: resolution: { @@ -6061,6 +6608,12 @@ packages: integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==, } + immutable@4.3.9: + resolution: + { + integrity: sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==, + } + import-fresh@3.3.1: resolution: { @@ -6481,6 +7034,30 @@ packages: integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==, } + jotai-scope@0.7.2: + resolution: + { + integrity: sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg==, + } + peerDependencies: + jotai: ">=2.9.2" + react: ">=17.0.0" + + jotai@2.11.0: + resolution: + { + integrity: sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==, + } + engines: { node: ">=12.20.0" } + peerDependencies: + "@types/react": ">=17.0.0" + react: ">=17.0.0" + peerDependenciesMeta: + "@types/react": + optional: true + react: + optional: true + jotai@2.19.0: resolution: { @@ -6630,6 +7207,13 @@ packages: integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==, } + langium@3.3.1: + resolution: + { + integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==, + } + engines: { node: ">=16.0.0" } + layout-base@1.0.2: resolution: { @@ -6697,6 +7281,12 @@ packages: } engines: { node: ">=10" } + lodash-es@4.17.21: + resolution: + { + integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==, + } + lodash-es@4.18.1: resolution: { @@ -6721,6 +7311,12 @@ packages: integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==, } + lodash.throttle@4.1.1: + resolution: + { + integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==, + } + lodash@4.17.23: resolution: { @@ -7217,6 +7813,12 @@ packages: integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, } + multimath@2.0.0: + resolution: + { + integrity: sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==, + } + mz@2.7.0: resolution: { @@ -7231,6 +7833,22 @@ packages: engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true + nanoid@3.3.3: + resolution: + { + integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + hasBin: true + + nanoid@4.0.2: + resolution: + { + integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==, + } + engines: { node: ^14 || ^16 || >=18 } + hasBin: true + natural-compare@1.4.0: resolution: { @@ -7367,6 +7985,12 @@ packages: } engines: { node: ">=18" } + open-color@1.9.1: + resolution: + { + integrity: sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw==, + } + optionator@0.9.4: resolution: { @@ -7407,6 +8031,12 @@ packages: integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==, } + pako@2.0.3: + resolution: + { + integrity: sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==, + } + parent-module@1.0.1: resolution: { @@ -7498,6 +8128,12 @@ packages: } engines: { node: ">= 14.16" } + perfect-freehand@1.2.0: + resolution: + { + integrity: sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw==, + } + pg-cloudflare@1.3.0: resolution: { @@ -7556,6 +8192,12 @@ packages: integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==, } + pica@7.1.1: + resolution: + { + integrity: sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ==, + } + picocolors@1.1.1: resolution: { @@ -7632,12 +8274,36 @@ packages: engines: { node: ">=18" } hasBin: true + png-chunk-text@1.0.0: + resolution: + { + integrity: sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw==, + } + + png-chunks-encode@1.0.0: + resolution: + { + integrity: sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA==, + } + + png-chunks-extract@1.0.0: + resolution: + { + integrity: sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==, + } + points-on-curve@0.2.0: resolution: { integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==, } + points-on-curve@1.0.1: + resolution: + { + integrity: sha512-3nmX4/LIiyuwGLwuUrfhTlDeQFlAhi7lyK/zcRNGhalwapDWgAGR82bUpmn2mA03vII3fvNCG8jAONzKXwpxAg==, + } + points-on-path@0.2.1: resolution: { @@ -7828,6 +8494,12 @@ packages: } engines: { node: ">=6" } + pwacompat@2.0.17: + resolution: + { + integrity: sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w==, + } + qs@6.15.0: resolution: { @@ -8250,6 +8922,12 @@ packages: engines: { node: ">=18.0.0", npm: ">=8.0.0" } hasBin: true + roughjs@4.6.4: + resolution: + { + integrity: sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw==, + } + roughjs@4.6.6: resolution: { @@ -8322,6 +9000,14 @@ packages: integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, } + sass@1.51.0: + resolution: + { + integrity: sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==, + } + engines: { node: ">=12.0.0" } + hasBin: true + saxes@6.0.0: resolution: { @@ -8498,6 +9184,13 @@ packages: } engines: { node: ">=20" } + sliced@1.0.1: + resolution: + { + integrity: sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==, + } + deprecated: Unsupported + smob@1.6.1: resolution: { @@ -8992,6 +9685,12 @@ packages: engines: { node: ">=18.0.0" } hasBin: true + tunnel-rat@0.1.2: + resolution: + { + integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==, + } + type-check@0.4.0: resolution: { @@ -9430,6 +10129,44 @@ packages: jsdom: optional: true + vscode-jsonrpc@8.2.0: + resolution: + { + integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==, + } + engines: { node: ">=14.0.0" } + + vscode-languageserver-protocol@3.17.5: + resolution: + { + integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==, + } + + vscode-languageserver-textdocument@1.0.12: + resolution: + { + integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==, + } + + vscode-languageserver-types@3.17.5: + resolution: + { + integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==, + } + + vscode-languageserver@9.0.1: + resolution: + { + integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==, + } + hasBin: true + + vscode-uri@3.0.8: + resolution: + { + integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==, + } + w3c-xmlserializer@5.0.0: resolution: { @@ -9456,6 +10193,12 @@ packages: } engines: { node: ">=20" } + webworkify@1.5.0: + resolution: + { + integrity: sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==, + } + whatwg-mimetype@3.0.0: resolution: { @@ -9749,6 +10492,24 @@ packages: integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==, } + zustand@4.5.7: + resolution: + { + integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==, + } + engines: { node: ">=12.7.0" } + peerDependencies: + "@types/react": ">=16.8" + immer: ">=9.0.6" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + zwitch@2.0.4: resolution: { @@ -10466,14 +11227,33 @@ snapshots: "@bcoe/v8-coverage@1.0.2": {} + "@braintree/sanitize-url@6.0.2": {} + "@braintree/sanitize-url@7.1.2": {} "@bramus/specificity@2.4.2": dependencies: css-tree: 3.2.1 + "@chevrotain/cst-dts-gen@11.0.3": + dependencies: + "@chevrotain/gast": 11.0.3 + "@chevrotain/types": 11.0.3 + lodash-es: 4.17.21 + + "@chevrotain/gast@11.0.3": + dependencies: + "@chevrotain/types": 11.0.3 + lodash-es: 4.17.21 + + "@chevrotain/regexp-to-ast@11.0.3": {} + + "@chevrotain/types@11.0.3": {} + "@chevrotain/types@11.1.2": {} + "@chevrotain/utils@11.0.3": {} + "@csstools/color-helpers@6.0.2": {} "@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)": @@ -10776,6 +11556,59 @@ snapshots: "@eslint/core": 0.17.0 levn: 0.4.1 + "@excalidraw/excalidraw@0.18.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(immer@11.1.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@braintree/sanitize-url": 6.0.2 + "@excalidraw/laser-pointer": 1.3.1 + "@excalidraw/mermaid-to-excalidraw": 2.2.2 + "@excalidraw/random-username": 1.1.0 + "@radix-ui/react-popover": 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-tabs": 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + browser-fs-access: 0.29.1 + canvas-roundrect-polyfill: 0.0.1 + clsx: 1.1.1 + cross-env: 7.0.3 + es6-promise-pool: 2.5.0 + fractional-indexing: 3.2.0 + fuzzy: 0.1.3 + image-blob-reduce: 3.0.1 + jotai: 2.11.0(@types/react@18.3.28)(react@18.3.1) + jotai-scope: 0.7.2(jotai@2.11.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) + lodash.debounce: 4.0.8 + lodash.throttle: 4.1.1 + nanoid: 3.3.3 + open-color: 1.9.1 + pako: 2.0.3 + perfect-freehand: 1.2.0 + pica: 7.1.1 + png-chunk-text: 1.0.0 + png-chunks-encode: 1.0.0 + png-chunks-extract: 1.0.0 + points-on-curve: 1.0.1 + pwacompat: 2.0.17 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + roughjs: 4.6.4 + sass: 1.51.0 + tunnel-rat: 0.1.2(@types/react@18.3.28)(immer@11.1.4)(react@18.3.1) + transitivePeerDependencies: + - "@types/react" + - "@types/react-dom" + - immer + + "@excalidraw/laser-pointer@1.3.1": {} + + "@excalidraw/markdown-to-text@0.1.2": {} + + "@excalidraw/mermaid-to-excalidraw@2.2.2": + dependencies: + "@excalidraw/markdown-to-text": 0.1.2 + "@mermaid-js/parser": 0.6.3 + mermaid: 11.15.0 + nanoid: 4.0.2 + + "@excalidraw/random-username@1.1.0": {} + "@exodus/bytes@1.15.0": {} "@fastify/ajv-compiler@4.0.5": @@ -11008,6 +11841,10 @@ snapshots: "@lukeed/ms@2.0.2": {} + "@mermaid-js/parser@0.6.3": + dependencies: + langium: 3.3.1 + "@mermaid-js/parser@1.1.1": dependencies: "@chevrotain/types": 11.1.2 @@ -11057,8 +11894,23 @@ snapshots: "@radix-ui/number@1.1.1": {} + "@radix-ui/primitive@1.0.0": + dependencies: + "@babel/runtime": 7.29.2 + + "@radix-ui/primitive@1.1.1": {} + "@radix-ui/primitive@1.1.3": {} + "@radix-ui/react-arrow@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@radix-ui/react-primitive": 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + "@types/react": 18.3.28 + "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@radix-ui/react-primitive": 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11084,6 +11936,16 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-collection@1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + "@radix-ui/react-compose-refs": 1.0.0(react@18.3.1) + "@radix-ui/react-context": 1.0.0(react@18.3.1) + "@radix-ui/react-primitive": 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-slot": 1.0.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + "@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@radix-ui/react-compose-refs": 1.1.2(@types/react@18.3.28)(react@18.3.1) @@ -11096,12 +11958,34 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-compose-refs@1.0.0(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + react: 18.3.1 + + "@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.28)(react@18.3.1)": + dependencies: + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.28)(react@18.3.1)": dependencies: react: 18.3.1 optionalDependencies: "@types/react": 18.3.28 + "@radix-ui/react-context@1.0.0(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + react: 18.3.1 + + "@radix-ui/react-context@1.1.1(@types/react@18.3.28)(react@18.3.1)": + dependencies: + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-context@1.1.2(@types/react@18.3.28)(react@18.3.1)": dependencies: react: 18.3.1 @@ -11130,6 +12014,11 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-direction@1.0.0(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + react: 18.3.1 + "@radix-ui/react-direction@1.1.1(@types/react@18.3.28)(react@18.3.1)": dependencies: react: 18.3.1 @@ -11149,6 +12038,19 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@radix-ui/primitive": 1.1.1 + "@radix-ui/react-compose-refs": 1.1.1(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-primitive": 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-use-callback-ref": 1.1.0(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-use-escape-keydown": 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + "@types/react": 18.3.28 + "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@radix-ui/primitive": 1.1.3 @@ -11164,12 +12066,29 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.28)(react@18.3.1)": + dependencies: + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.28)(react@18.3.1)": dependencies: react: 18.3.1 optionalDependencies: "@types/react": 18.3.28 + "@radix-ui/react-focus-scope@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@radix-ui/react-compose-refs": 1.1.1(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-primitive": 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-use-callback-ref": 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + "@types/react": 18.3.28 + "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@radix-ui/react-compose-refs": 1.1.2(@types/react@18.3.28)(react@18.3.1) @@ -11181,6 +12100,19 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-id@1.0.0(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + "@radix-ui/react-use-layout-effect": 1.0.0(react@18.3.1) + react: 18.3.1 + + "@radix-ui/react-id@1.1.0(@types/react@18.3.28)(react@18.3.1)": + dependencies: + "@radix-ui/react-use-layout-effect": 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-id@1.1.1(@types/react@18.3.28)(react@18.3.1)": dependencies: "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@18.3.28)(react@18.3.1) @@ -11237,6 +12169,47 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-popover@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@radix-ui/primitive": 1.1.1 + "@radix-ui/react-compose-refs": 1.1.1(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-context": 1.1.1(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-dismissable-layer": 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-focus-guards": 1.1.1(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-focus-scope": 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-id": 1.1.0(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-popper": 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-portal": 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-presence": 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-primitive": 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-slot": 1.1.2(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-use-controllable-state": 1.1.0(@types/react@18.3.28)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + "@types/react": 18.3.28 + "@types/react-dom": 18.3.7(@types/react@18.3.28) + + "@radix-ui/react-popper@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@floating-ui/react-dom": 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-arrow": 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-compose-refs": 1.1.1(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-context": 1.1.1(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-primitive": 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-use-callback-ref": 1.1.0(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-use-layout-effect": 1.1.0(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-use-rect": 1.1.0(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-use-size": 1.1.0(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/rect": 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + "@types/react": 18.3.28 + "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@floating-ui/react-dom": 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11255,6 +12228,16 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-portal@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@radix-ui/react-primitive": 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-use-layout-effect": 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + "@types/react": 18.3.28 + "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@radix-ui/react-primitive": 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11265,6 +12248,24 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-presence@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + "@radix-ui/react-compose-refs": 1.0.0(react@18.3.1) + "@radix-ui/react-use-layout-effect": 1.0.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + "@radix-ui/react-presence@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@radix-ui/react-compose-refs": 1.1.1(@types/react@18.3.28)(react@18.3.1) + "@radix-ui/react-use-layout-effect": 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + "@types/react": 18.3.28 + "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@radix-ui/react-compose-refs": 1.1.2(@types/react@18.3.28)(react@18.3.1) @@ -11275,6 +12276,22 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-primitive@1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + "@radix-ui/react-slot": 1.0.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + "@radix-ui/react-primitive@2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@radix-ui/react-slot": 1.1.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + "@types/react": 18.3.28 + "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@radix-ui/react-slot": 1.2.3(@types/react@18.3.28)(react@18.3.1) @@ -11293,6 +12310,21 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-roving-focus@1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + "@radix-ui/primitive": 1.0.0 + "@radix-ui/react-collection": 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-compose-refs": 1.0.0(react@18.3.1) + "@radix-ui/react-context": 1.0.0(react@18.3.1) + "@radix-ui/react-direction": 1.0.0(react@18.3.1) + "@radix-ui/react-id": 1.0.0(react@18.3.1) + "@radix-ui/react-primitive": 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-use-callback-ref": 1.0.0(react@18.3.1) + "@radix-ui/react-use-controllable-state": 1.0.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + "@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@radix-ui/primitive": 1.1.3 @@ -11356,6 +12388,19 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-slot@1.0.1(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + "@radix-ui/react-compose-refs": 1.0.0(react@18.3.1) + react: 18.3.1 + + "@radix-ui/react-slot@1.1.2(@types/react@18.3.28)(react@18.3.1)": + dependencies: + "@radix-ui/react-compose-refs": 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-slot@1.2.3(@types/react@18.3.28)(react@18.3.1)": dependencies: "@radix-ui/react-compose-refs": 1.1.2(@types/react@18.3.28)(react@18.3.1) @@ -11370,6 +12415,20 @@ snapshots: optionalDependencies: "@types/react": 18.3.28 + "@radix-ui/react-tabs@1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + "@radix-ui/primitive": 1.0.0 + "@radix-ui/react-context": 1.0.0(react@18.3.1) + "@radix-ui/react-direction": 1.0.0(react@18.3.1) + "@radix-ui/react-id": 1.0.0(react@18.3.1) + "@radix-ui/react-presence": 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-primitive": 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-roving-focus": 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + "@radix-ui/react-use-controllable-state": 1.0.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + "@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@radix-ui/primitive": 1.1.3 @@ -11390,12 +12449,36 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/react-use-callback-ref@1.0.0(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + react: 18.3.1 + + "@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.28)(react@18.3.1)": + dependencies: + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.28)(react@18.3.1)": dependencies: react: 18.3.1 optionalDependencies: "@types/react": 18.3.28 + "@radix-ui/react-use-controllable-state@1.0.0(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + "@radix-ui/react-use-callback-ref": 1.0.0(react@18.3.1) + react: 18.3.1 + + "@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.28)(react@18.3.1)": + dependencies: + "@radix-ui/react-use-callback-ref": 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.28)(react@18.3.1)": dependencies: "@radix-ui/react-use-effect-event": 0.0.2(@types/react@18.3.28)(react@18.3.1) @@ -11411,6 +12494,13 @@ snapshots: optionalDependencies: "@types/react": 18.3.28 + "@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.28)(react@18.3.1)": + dependencies: + "@radix-ui/react-use-callback-ref": 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.28)(react@18.3.1)": dependencies: "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@18.3.28)(react@18.3.1) @@ -11418,6 +12508,17 @@ snapshots: optionalDependencies: "@types/react": 18.3.28 + "@radix-ui/react-use-layout-effect@1.0.0(react@18.3.1)": + dependencies: + "@babel/runtime": 7.29.2 + react: 18.3.1 + + "@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.28)(react@18.3.1)": + dependencies: + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.28)(react@18.3.1)": dependencies: react: 18.3.1 @@ -11430,6 +12531,13 @@ snapshots: optionalDependencies: "@types/react": 18.3.28 + "@radix-ui/react-use-rect@1.1.0(@types/react@18.3.28)(react@18.3.1)": + dependencies: + "@radix-ui/rect": 1.1.0 + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-use-rect@1.1.1(@types/react@18.3.28)(react@18.3.1)": dependencies: "@radix-ui/rect": 1.1.1 @@ -11437,6 +12545,13 @@ snapshots: optionalDependencies: "@types/react": 18.3.28 + "@radix-ui/react-use-size@1.1.0(@types/react@18.3.28)(react@18.3.1)": + dependencies: + "@radix-ui/react-use-layout-effect": 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + "@types/react": 18.3.28 + "@radix-ui/react-use-size@1.1.1(@types/react@18.3.28)(react@18.3.1)": dependencies: "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@18.3.28)(react@18.3.1) @@ -11453,6 +12568,8 @@ snapshots: "@types/react": 18.3.28 "@types/react-dom": 18.3.7(@types/react@18.3.28) + "@radix-ui/rect@1.1.0": {} + "@radix-ui/rect@1.1.1": {} "@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@18.3.28)(react@18.3.1)(redux@5.0.1))(react@18.3.1)": @@ -11958,7 +13075,7 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - "@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@24.12.0)(terser@5.46.1))": + "@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1))": dependencies: "@babel/core": 7.29.0 "@babel/plugin-transform-react-jsx-self": 7.27.1(@babel/core@7.29.0) @@ -11966,11 +13083,11 @@ snapshots: "@rolldown/pluginutils": 1.0.0-beta.27 "@types/babel__core": 7.20.5 react-refresh: 0.17.0 - vite: 5.4.21(@types/node@24.12.0)(terser@5.46.1) + vite: 5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1) transitivePeerDependencies: - supports-color - "@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(terser@5.46.1))": + "@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(sass@1.51.0)(terser@5.46.1))": dependencies: "@ampproject/remapping": 2.3.0 "@bcoe/v8-coverage": 0.2.3 @@ -11984,11 +13101,11 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(terser@5.46.1) + vitest: 2.1.9(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(sass@1.51.0)(terser@5.46.1) transitivePeerDependencies: - supports-color - "@vitest/coverage-v8@4.1.2(vitest@4.1.2(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))": + "@vitest/coverage-v8@4.1.2(vitest@4.1.2(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))": dependencies: "@bcoe/v8-coverage": 1.0.2 "@vitest/utils": 4.1.2 @@ -12000,7 +13117,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.2(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.2(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) "@vitest/expect@2.1.9": dependencies: @@ -12018,21 +13135,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - "@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.12.0)(terser@5.46.1))": + "@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1))": dependencies: "@vitest/spy": 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@24.12.0)(terser@5.46.1) + vite: 5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1) - "@vitest/mocker@4.1.2(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))": + "@vitest/mocker@4.1.2(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))": dependencies: "@vitest/spy": 4.1.2 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) "@vitest/pretty-format@2.1.9": dependencies: @@ -12321,6 +13438,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-fs-access@0.29.1: {} + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.10.12 @@ -12358,6 +13477,8 @@ snapshots: caniuse-lite@1.0.30001782: {} + canvas-roundrect-polyfill@0.0.1: {} + ccount@2.0.1: {} chai@5.3.3: @@ -12385,6 +13506,20 @@ snapshots: check-error@2.1.3: {} + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.18.1 + + chevrotain@11.0.3: + dependencies: + "@chevrotain/cst-dts-gen": 11.0.3 + "@chevrotain/gast": 11.0.3 + "@chevrotain/regexp-to-ast": 11.0.3 + "@chevrotain/types": 11.0.3 + "@chevrotain/utils": 11.0.3 + lodash-es: 4.17.21 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -12418,6 +13553,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clsx@1.1.1: {} + clsx@2.1.1: {} cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -12485,10 +13622,16 @@ snapshots: dependencies: layout-base: 2.0.1 + crc-32@0.3.0: {} + croner@10.0.1: {} cronstrue@3.14.0: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -12932,6 +14075,8 @@ snapshots: es-toolkit@1.45.1: {} + es6-promise-pool@2.5.0: {} + esbuild@0.21.5: optionalDependencies: "@esbuild/aix-ppc64": 0.21.5 @@ -13300,6 +14445,8 @@ snapshots: fraction.js@5.3.4: {} + fractional-indexing@3.2.0: {} + framer-motion@12.38.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: motion-dom: 12.38.0 @@ -13337,6 +14484,8 @@ snapshots: functions-have-names@1.2.3: {} + fuzzy@0.1.3: {} + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -13415,6 +14564,8 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + glur@1.1.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -13524,10 +14675,16 @@ snapshots: ignore@7.0.5: {} + image-blob-reduce@3.0.1: + dependencies: + pica: 7.1.1 + immer@10.2.0: {} immer@11.1.4: {} + immutable@4.3.9: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -13754,6 +14911,16 @@ snapshots: jose@6.2.2: {} + jotai-scope@0.7.2(jotai@2.11.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1): + dependencies: + jotai: 2.11.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + + jotai@2.11.0(@types/react@18.3.28)(react@18.3.1): + optionalDependencies: + "@types/react": 18.3.28 + react: 18.3.1 + jotai@2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@18.3.28)(react@18.3.1): optionalDependencies: "@babel/core": 7.29.0 @@ -13840,6 +15007,14 @@ snapshots: khroma@2.1.0: {} + langium@3.3.1: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + layout-base@1.0.2: {} layout-base@2.0.1: {} @@ -13883,6 +15058,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + lodash-es@4.18.1: {} lodash.debounce@4.0.8: {} @@ -13891,6 +15068,8 @@ snapshots: lodash.sortby@4.7.0: {} + lodash.throttle@4.1.1: {} + lodash@4.17.23: {} log-update@6.1.0: @@ -14370,6 +15549,11 @@ snapshots: ms@2.1.3: {} + multimath@2.0.0: + dependencies: + glur: 1.1.2 + object-assign: 4.1.1 + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -14378,6 +15562,10 @@ snapshots: nanoid@3.3.11: {} + nanoid@3.3.3: {} + + nanoid@4.0.2: {} + natural-compare@1.4.0: {} negotiator@1.0.0: {} @@ -14455,6 +15643,8 @@ snapshots: dependencies: mimic-function: 5.0.1 + open-color@1.9.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -14482,6 +15672,8 @@ snapshots: package-manager-detector@1.6.0: {} + pako@2.0.3: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -14528,6 +15720,8 @@ snapshots: pathval@2.0.1: {} + perfect-freehand@1.2.0: {} + pg-cloudflare@1.3.0: optional: true @@ -14563,6 +15757,14 @@ snapshots: dependencies: split2: 4.2.0 + pica@7.1.1: + dependencies: + glur: 1.1.2 + inherits: 2.0.4 + multimath: 2.0.0 + object-assign: 4.1.1 + webworkify: 1.5.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -14603,8 +15805,21 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + png-chunk-text@1.0.0: {} + + png-chunks-encode@1.0.0: + dependencies: + crc-32: 0.3.0 + sliced: 1.0.1 + + png-chunks-extract@1.0.0: + dependencies: + crc-32: 0.3.0 + points-on-curve@0.2.0: {} + points-on-curve@1.0.1: {} + points-on-path@0.2.1: dependencies: path-data-parser: 0.1.0 @@ -14699,6 +15914,8 @@ snapshots: punycode@2.3.1: {} + pwacompat@2.0.17: {} + qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -15030,6 +16247,13 @@ snapshots: "@rollup/rollup-win32-x64-msvc": 4.60.1 fsevents: 2.3.3 + roughjs@4.6.4: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -15082,6 +16306,12 @@ snapshots: safer-buffer@2.1.2: {} + sass@1.51.0: + dependencies: + chokidar: 3.6.0 + immutable: 4.3.9 + source-map-js: 1.2.1 + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -15234,6 +16464,8 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + sliced@1.0.1: {} + smob@1.6.1: {} sonic-boom@4.2.1: @@ -15536,6 +16768,14 @@ snapshots: fsevents: 2.3.3 optional: true + tunnel-rat@0.1.2(@types/react@18.3.28)(immer@11.1.4)(react@18.3.1): + dependencies: + zustand: 4.5.7(@types/react@18.3.28)(immer@11.1.4)(react@18.3.1) + transitivePeerDependencies: + - "@types/react" + - immer + - react + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -15724,13 +16964,13 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-node@2.1.9(@types/node@24.12.0)(terser@5.46.1): + vite-node@2.1.9(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 1.1.2 - vite: 5.4.21(@types/node@24.12.0)(terser@5.46.1) + vite: 5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1) transitivePeerDependencies: - "@types/node" - less @@ -15742,18 +16982,18 @@ snapshots: - supports-color - terser - vite-plugin-pwa@1.2.0(vite@5.4.21(@types/node@24.12.0)(terser@5.46.1))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0): + vite-plugin-pwa@1.2.0(vite@5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.15 - vite: 5.4.21(@types/node@24.12.0)(terser@5.46.1) + vite: 5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1) workbox-build: 7.4.0(@types/babel__core@7.20.5) workbox-window: 7.4.0 transitivePeerDependencies: - supports-color - vite@5.4.21(@types/node@24.12.0)(terser@5.46.1): + vite@5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1): dependencies: esbuild: 0.21.5 postcss: 8.5.8 @@ -15761,9 +17001,10 @@ snapshots: optionalDependencies: "@types/node": 24.12.0 fsevents: 2.3.3 + sass: 1.51.0 terser: 5.46.1 - vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -15775,14 +17016,15 @@ snapshots: "@types/node": 24.12.0 fsevents: 2.3.3 jiti: 1.21.7 + sass: 1.51.0 terser: 5.46.1 tsx: 4.21.0 yaml: 2.8.3 - vitest@2.1.9(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(terser@5.46.1): + vitest@2.1.9(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(sass@1.51.0)(terser@5.46.1): dependencies: "@vitest/expect": 2.1.9 - "@vitest/mocker": 2.1.9(vite@5.4.21(@types/node@24.12.0)(terser@5.46.1)) + "@vitest/mocker": 2.1.9(vite@5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1)) "@vitest/pretty-format": 2.1.9 "@vitest/runner": 2.1.9 "@vitest/snapshot": 2.1.9 @@ -15798,8 +17040,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 1.2.0 - vite: 5.4.21(@types/node@24.12.0)(terser@5.46.1) - vite-node: 2.1.9(@types/node@24.12.0)(terser@5.46.1) + vite: 5.4.21(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1) + vite-node: 2.1.9(@types/node@24.12.0)(sass@1.51.0)(terser@5.46.1) why-is-node-running: 2.3.0 optionalDependencies: "@types/node": 24.12.0 @@ -15816,10 +17058,10 @@ snapshots: - supports-color - terser - vitest@4.1.2(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.2(@types/node@24.12.0)(happy-dom@18.0.1)(jsdom@29.1.1)(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: "@vitest/expect": 4.1.2 - "@vitest/mocker": 4.1.2(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + "@vitest/mocker": 4.1.2(vite@6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) "@vitest/pretty-format": 4.1.2 "@vitest/runner": 4.1.2 "@vitest/snapshot": 4.1.2 @@ -15836,7 +17078,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@24.12.0)(jiti@1.21.7)(sass@1.51.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: "@types/node": 24.12.0 @@ -15845,6 +17087,23 @@ snapshots: transitivePeerDependencies: - msw + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -15857,6 +17116,8 @@ snapshots: webidl-conversions@8.0.1: {} + webworkify@1.5.0: {} + whatwg-mimetype@3.0.0: optional: true @@ -16095,4 +17356,12 @@ snapshots: zod@4.3.6: {} + zustand@4.5.7(@types/react@18.3.28)(immer@11.1.4)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + "@types/react": 18.3.28 + immer: 11.1.4 + react: 18.3.1 + zwitch@2.0.4: {} From f7d1e0a44477e14272ae4697a178608005bdfd87 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Fri, 10 Jul 2026 16:04:16 -0600 Subject: [PATCH 2/9] Fix whiteboard not repainting after tab switch When the whiteboard tab is hidden (display:none) and an agent updates the scene via MCP, Excalidraw's updateScene() updates internal state but the canvas can't repaint at zero dimensions. Call refresh() when the tab becomes visible so the canvas repaints with the latest content. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/components/app/whiteboard-tab.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/web/src/components/app/whiteboard-tab.tsx b/apps/web/src/components/app/whiteboard-tab.tsx index e60fbd19..33cb3086 100644 --- a/apps/web/src/components/app/whiteboard-tab.tsx +++ b/apps/web/src/components/app/whiteboard-tab.tsx @@ -230,6 +230,12 @@ function WhiteboardCanvas({ return () => window.removeEventListener("pointerup", onPointerUp); }, [applyRemote]); + useEffect(() => { + if (visible && excalidrawAPI) { + excalidrawAPI.refresh(); + } + }, [visible, excalidrawAPI]); + useEffect(() => { if (visible) return; if (saveTimerRef.current !== undefined) { From d907c9d704a688877f96b461caad4489682492ab Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Sat, 11 Jul 2026 13:33:32 -0600 Subject: [PATCH 3/9] Fix whiteboard going blank after agent updates via restoreElements hydration Raw server elements lack Excalidraw internal properties (seed, version, roughness, opacity, strokeWidth, etc.) needed for rendering. Use restoreElements() to hydrate sparse server elements before updateScene() so the canvas renders correctly without requiring a page reload. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/components/app/whiteboard-tab.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/app/whiteboard-tab.tsx b/apps/web/src/components/app/whiteboard-tab.tsx index 33cb3086..7186f803 100644 --- a/apps/web/src/components/app/whiteboard-tab.tsx +++ b/apps/web/src/components/app/whiteboard-tab.tsx @@ -4,6 +4,7 @@ import { Excalidraw, exportToBlob, getSceneVersion, + restoreElements, } from "@excalidraw/excalidraw"; import type { ExcalidrawImperativeAPI, @@ -190,14 +191,15 @@ function WhiteboardCanvas({ const applyRemote = useCallback( (remote: WhiteboardData) => { if (!excalidrawAPI) return; - versionRef.current = remote.version; - sceneVersionRef.current = getSceneVersion( - remote.scene.elements as readonly ExcalidrawElement[] + const hydrated = restoreElements( + remote.scene.elements as ExcalidrawElement[], + excalidrawAPI.getSceneElements(), + { repairBindings: true } ); - excalidrawAPI.updateScene({ - elements: remote.scene.elements as ExcalidrawElement[], - }); - setBoardEmpty(remote.scene.elements.length === 0); + versionRef.current = remote.version; + sceneVersionRef.current = getSceneVersion(hydrated); + excalidrawAPI.updateScene({ elements: hydrated }); + setBoardEmpty(hydrated.length === 0); if (snapshotTimerRef.current !== undefined) { window.clearTimeout(snapshotTimerRef.current); } From 819179bf7f8e4cd2a76fc53201a19926ca9107bd Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Sat, 11 Jul 2026 15:39:57 -0600 Subject: [PATCH 4/9] docs: add whiteboard libraries design spec and implementation plan Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-07-11-whiteboard-libraries.md | 1262 +++++++++++++++++ .../2026-07-11-whiteboard-libraries-design.md | 181 +++ 2 files changed, 1443 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-11-whiteboard-libraries.md create mode 100644 docs/superpowers/specs/2026-07-11-whiteboard-libraries-design.md diff --git a/docs/superpowers/plans/2026-07-11-whiteboard-libraries.md b/docs/superpowers/plans/2026-07-11-whiteboard-libraries.md new file mode 100644 index 00000000..7e186e66 --- /dev/null +++ b/docs/superpowers/plans/2026-07-11-whiteboard-libraries.md @@ -0,0 +1,1262 @@ +# Whiteboard Libraries Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add pre-loaded Excalidraw shape libraries (flowchart, architecture, wireframe, UI inputs) with agent MCP tools to browse and insert library items onto the whiteboard. + +**Architecture:** Four `.excalidrawlib` v2 files are checked into `apps/web/public/excalidraw/libraries/` as static assets. At server startup, a `LibraryCatalog` class parses these files, computes bounding boxes and anchor points for each item, and caches the catalog in memory. Two new MCP tools (`whiteboard_library`, `whiteboard_insert`) let agents browse and place library items. The client lazy-fetches the files on whiteboard mount and loads them into Excalidraw's built-in library panel via `updateLibrary()`. + +**Tech Stack:** Excalidraw library format v2 (JSON), Zod v4 schemas, Fastify, React Query, `@excalidraw/excalidraw` API. + +## Global Constraints + +- Library files are MIT licensed (Copyright 2020 Excalidraw) — include attribution. +- All `.excalidrawlib` files must be v2 format (containing `libraryItems` array with named items). +- Library items are static — no runtime fetching from external CDNs. +- The catalog is computed once at startup and cached in memory. +- `whiteboard_library` compact mode must be token-efficient (~30 tokens per item). +- `whiteboard_insert` reuses the existing `mergeElements` + `saveWhiteboard` persistence path. + +## File Structure + +### New files + +| File | Responsibility | +| ------------------------------------------------------------------------------------ | ---------------------------------------------------- | +| `apps/web/public/excalidraw/libraries/flow-chart-symbols.excalidrawlib` | Flowchart library (15 items) | +| `apps/web/public/excalidraw/libraries/architecture-diagram-components.excalidrawlib` | Architecture library (11 items) | +| `apps/web/public/excalidraw/libraries/universal-ui-kit.excalidrawlib` | UI wireframe library (22 items) | +| `apps/web/public/excalidraw/libraries/html-input-elements.excalidrawlib` | HTML input library (8 items) | +| `apps/web/public/excalidraw/libraries/NOTICES` | MIT attribution for library files | +| `apps/server/src/shared/whiteboard-library.ts` | `LibraryCatalog` class: parse, compute bounds, stamp | +| `apps/server/test/whiteboard-library.test.ts` | Unit tests for catalog parsing, bounds, stamp | + +### Modified files + +| File | Change | +| --------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `apps/server/src/shared/mcp/whiteboard-tools.ts` | Register `whiteboard_library` and `whiteboard_insert` tools; extend `WhiteboardToolsContext` | +| `apps/server/src/server/mcp-whiteboard-handlers.ts` | Add `libraryList`, `libraryGet`, `insertLibraryItem` handler methods | +| `apps/server/src/shared/mcp/server.ts` | Wire new handlers into MCP context object | +| `apps/web/src/components/app/whiteboard-tab.tsx` | Lazy-load libraries on mount via `updateLibrary()` | +| `e2e/whiteboard.spec.ts` | E2E tests for `whiteboard_library` and `whiteboard_insert` MCP tools | + +--- + +### Task 1: Download Library Files and Add Attribution + +**Files:** + +- Create: `apps/web/public/excalidraw/libraries/flow-chart-symbols.excalidrawlib` +- Create: `apps/web/public/excalidraw/libraries/architecture-diagram-components.excalidrawlib` +- Create: `apps/web/public/excalidraw/libraries/universal-ui-kit.excalidrawlib` +- Create: `apps/web/public/excalidraw/libraries/html-input-elements.excalidrawlib` +- Create: `apps/web/public/excalidraw/libraries/NOTICES` + +**Interfaces:** + +- Consumes: Nothing +- Produces: Static `.excalidrawlib` v2 JSON files at the paths above, served by Vite at `/excalidraw/libraries/` + +- [ ] **Step 1: Create the libraries directory** + +```bash +mkdir -p apps/web/public/excalidraw/libraries +``` + +- [ ] **Step 2: Download the four library files** + +Download each file from the excalidraw-libraries GitHub repo: + +```bash +curl -L -o apps/web/public/excalidraw/libraries/flow-chart-symbols.excalidrawlib \ + "https://raw.githubusercontent.com/excalidraw/excalidraw-libraries/main/libraries/finfin/flow-chart-symbols.excalidrawlib" + +curl -L -o apps/web/public/excalidraw/libraries/architecture-diagram-components.excalidrawlib \ + "https://raw.githubusercontent.com/excalidraw/excalidraw-libraries/main/libraries/anna-pastushko/architecture-diagram-components.excalidrawlib" + +curl -L -o apps/web/public/excalidraw/libraries/universal-ui-kit.excalidrawlib \ + "https://raw.githubusercontent.com/excalidraw/excalidraw-libraries/main/libraries/manuelernestog/universal-ui-kit.excalidrawlib" + +curl -L -o apps/web/public/excalidraw/libraries/html-input-elements.excalidrawlib \ + "https://raw.githubusercontent.com/excalidraw/excalidraw-libraries/main/libraries/marwinburesch/html-input-elements.excalidrawlib" +``` + +- [ ] **Step 3: Validate each file is v2 format** + +Each file must be valid JSON with `libraryItems` array where each item has a `name` field: + +```bash +for f in apps/web/public/excalidraw/libraries/*.excalidrawlib; do + echo "=== $(basename "$f") ===" + node -e " + const lib = JSON.parse(require('fs').readFileSync('$f', 'utf8')); + if (lib.version !== 2) throw new Error('Not v2'); + if (!Array.isArray(lib.libraryItems)) throw new Error('No libraryItems'); + const named = lib.libraryItems.filter(i => i.name); + console.log('Items: ' + lib.libraryItems.length + ', Named: ' + named.length); + named.forEach(i => console.log(' - ' + i.name)); + " +done +``` + +Expected: All files parse successfully, all items have names. If any file is v1 format (has `library` instead of `libraryItems`), it cannot be used — find an alternative v2 source or manually convert it. + +- [ ] **Step 4: Create NOTICES file** + +Create `apps/web/public/excalidraw/libraries/NOTICES`: + +``` +Excalidraw Libraries +==================== + +The .excalidrawlib files in this directory are sourced from the +excalidraw-libraries project: + + https://github.com/excalidraw/excalidraw-libraries + +Licensed under the MIT License: + + Copyright (c) 2020 Excalidraw + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Individual libraries: + - flow-chart-symbols.excalidrawlib — by finfin + - architecture-diagram-components.excalidrawlib — by anna-pastushko + - universal-ui-kit.excalidrawlib — by manuelernestog + - html-input-elements.excalidrawlib — by marwinburesch +``` + +- [ ] **Step 5: Commit** + +```bash +git add apps/web/public/excalidraw/libraries/ +git commit -m "feat: add excalidraw library files for whiteboard (flowchart, architecture, wireframe, UI inputs)" +``` + +--- + +### Task 2: LibraryCatalog Server-Side Class + +**Files:** + +- Create: `apps/server/src/shared/whiteboard-library.ts` +- Create: `apps/server/test/whiteboard-library.test.ts` + +**Interfaces:** + +- Consumes: `.excalidrawlib` v2 JSON files on disk +- Produces: + - `LibraryCatalog` class with methods: + - `static async fromDirectory(dir: string): Promise` — parse all `.excalidrawlib` files in a directory + - `list(category?: string): CompactLibraryItem[]` — compact list for browse mode + - `get(name: string): DetailedLibraryItem | null` — full detail for one item (case-insensitive) + - `stamp(name: string, x: number, y: number, scale: number): StampResult` — clone elements with fresh IDs, scale, and offset + - Exported types: `CompactLibraryItem`, `DetailedLibraryItem`, `StampResult` + +- [ ] **Step 1: Write the failing tests** + +Create `apps/server/test/whiteboard-library.test.ts`: + +```typescript +import { describe, expect, it } from "vitest"; +import path from "node:path"; +import { LibraryCatalog } from "../src/shared/whiteboard-library.js"; + +// Use the actual library files checked in during Task 1 +const LIBRARIES_DIR = path.resolve( + __dirname, + "../../web/public/excalidraw/libraries" +); + +describe("LibraryCatalog", () => { + describe("fromDirectory", () => { + it("loads all library files and builds catalog", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + expect(all.length).toBeGreaterThanOrEqual(40); + // Every item has required fields + for (const item of all) { + expect(item.name).toBeTruthy(); + expect(item.category).toBeTruthy(); + expect(item.width).toBeGreaterThan(0); + expect(item.height).toBeGreaterThan(0); + } + }); + + it("assigns correct categories from filenames", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const categories = new Set(catalog.list().map((i) => i.category)); + expect(categories).toContain("flowchart"); + expect(categories).toContain("architecture"); + expect(categories).toContain("wireframe"); + expect(categories).toContain("ui-input"); + }); + }); + + describe("list", () => { + it("filters by category", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const flowchart = catalog.list("flowchart"); + expect(flowchart.length).toBeGreaterThan(0); + expect(flowchart.every((i) => i.category === "flowchart")).toBe(true); + }); + + it("returns all items when no category specified", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + const flowchart = catalog.list("flowchart"); + expect(all.length).toBeGreaterThan(flowchart.length); + }); + }); + + describe("get", () => { + it("returns detailed item with anchors but no raw elements", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + const detail = catalog.get(all[0].name); + expect(detail).not.toBeNull(); + expect(detail!.anchors).toBeDefined(); + expect(detail!.anchors.top).toHaveLength(2); + expect(detail!.anchors.bottom).toHaveLength(2); + expect(detail!.anchors.left).toHaveLength(2); + expect(detail!.anchors.right).toHaveLength(2); + expect(detail!.elementCount).toBeGreaterThan(0); + expect(detail!.description).toBeTruthy(); + // Elements are internal — never leaked to callers + expect((detail as Record).elements).toBeUndefined(); + }); + + it("is case-insensitive", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + const name = all[0].name; + const lower = catalog.get(name.toLowerCase()); + const upper = catalog.get(name.toUpperCase()); + expect(lower).not.toBeNull(); + expect(upper).not.toBeNull(); + expect(lower!.name).toBe(upper!.name); + }); + + it("returns null for unknown name", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + expect(catalog.get("nonexistent-item-xyz")).toBeNull(); + }); + }); + + describe("stamp", () => { + it("returns elements offset to target position", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + const result = catalog.stamp(all[0].name, 200, 300, 1); + expect(result.elements.length).toBeGreaterThan(0); + // All elements should have fresh string IDs + const ids = result.elements.map( + (e) => (e as Record).id as string + ); + expect(new Set(ids).size).toBe(ids.length); // unique + // Bounding box top-left should be near (200, 300) + const xs = result.elements.map( + (e) => (e as Record).x as number + ); + const ys = result.elements.map( + (e) => (e as Record).y as number + ); + expect(Math.min(...xs)).toBeCloseTo(200, 0); + expect(Math.min(...ys)).toBeCloseTo(300, 0); + }); + + it("scales elements", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + const result1x = catalog.stamp(all[0].name, 0, 0, 1); + const result2x = catalog.stamp(all[0].name, 0, 0, 2); + // At 2x, bounding box should be roughly double + const width1 = Math.max( + ...result1x.elements.map( + (e) => + ((e as Record).x as number) + + ((e as Record).width as number) + ) + ); + const width2 = Math.max( + ...result2x.elements.map( + (e) => + ((e as Record).x as number) + + ((e as Record).width as number) + ) + ); + expect(width2).toBeCloseTo(width1 * 2, -1); + }); + + it("replaces text when label is provided", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + // Use the first item — stamp will apply label to any text element it contains + const textItemName = all[0].name; + const result = catalog.stamp(textItemName, 0, 0, 1, "Custom Label"); + const textEls = result.elements.filter( + (e) => (e as Record).type === "text" + ); + expect(textEls.length).toBeGreaterThan(0); + expect((textEls[0] as Record).text).toBe("Custom Label"); + expect((textEls[0] as Record).originalText).toBe( + "Custom Label" + ); + }); + + it("throws for unknown item", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + expect(() => catalog.stamp("nonexistent", 0, 0, 1)).toThrow(); + }); + + it("maintains internal bindings with remapped IDs", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + // Stamp each item until we find one that has containerId bindings + let boundItemName: string | null = null; + for (const item of all) { + const stamped = catalog.stamp(item.name, 0, 0, 1); + if ( + stamped.elements.some( + (e) => (e as Record).containerId + ) + ) { + boundItemName = item.name; + break; + } + } + if (!boundItemName) return; + const result = catalog.stamp(boundItemName, 0, 0, 1); + const idSet = new Set( + result.elements.map((e) => (e as Record).id as string) + ); + // Every containerId should reference an ID that exists in the stamped set + for (const el of result.elements) { + const containerId = (el as Record) + .containerId as string; + if (containerId) { + expect(idSet.has(containerId)).toBe(true); + } + } + }); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +pnpm run test -- apps/server/test/whiteboard-library.test.ts +``` + +Expected: FAIL — module `../src/shared/whiteboard-library.js` does not exist. + +- [ ] **Step 3: Implement LibraryCatalog** + +Create `apps/server/src/shared/whiteboard-library.ts`: + +```typescript +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { randomUUID } from "node:crypto"; + +type RawElement = Record; + +type LibraryFileItem = { + id: string; + name?: string; + status: string; + elements: RawElement[]; + created: number; +}; + +type Anchors = { + top: [number, number]; + bottom: [number, number]; + left: [number, number]; + right: [number, number]; +}; + +export type CompactLibraryItem = { + name: string; + category: string; + width: number; + height: number; +}; + +export type DetailedLibraryItem = { + name: string; + category: string; + description: string; + width: number; + height: number; + elementCount: number; + anchors: Anchors; +}; + +type CatalogEntry = DetailedLibraryItem & { + elements: RawElement[]; +}; + +export type StampResult = { + elements: RawElement[]; + addedIds: string[]; +}; + +const FILENAME_TO_CATEGORY: Record = { + "flow-chart-symbols": "flowchart", + "architecture-diagram-components": "architecture", + "universal-ui-kit": "wireframe", + "html-input-elements": "ui-input", +}; + +function computeBounds(elements: RawElement[]): { + minX: number; + minY: number; + maxX: number; + maxY: number; + width: number; + height: number; +} { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + for (const el of elements) { + const x = typeof el.x === "number" ? el.x : 0; + const y = typeof el.y === "number" ? el.y : 0; + const w = typeof el.width === "number" ? el.width : 0; + const h = typeof el.height === "number" ? el.height : 0; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x + w); + maxY = Math.max(maxY, y + h); + } + return { + minX, + minY, + maxX, + maxY, + width: Math.round(maxX - minX), + height: Math.round(maxY - minY), + }; +} + +function computeAnchors(width: number, height: number): Anchors { + return { + top: [Math.round(width / 2), 0], + bottom: [Math.round(width / 2), height], + left: [0, Math.round(height / 2)], + right: [width, Math.round(height / 2)], + }; +} + +export class LibraryCatalog { + private items: Map; + + private constructor(items: Map) { + this.items = items; + } + + static async fromDirectory(dir: string): Promise { + const items = new Map(); + const files = await readdir(dir); + for (const file of files) { + if (!file.endsWith(".excalidrawlib")) continue; + const filePath = path.join(dir, file); + const raw = JSON.parse(await readFile(filePath, "utf8")) as { + version?: number; + libraryItems?: LibraryFileItem[]; + }; + if (raw.version !== 2 || !Array.isArray(raw.libraryItems)) continue; + const stem = file.replace(/\.excalidrawlib$/, ""); + const category = FILENAME_TO_CATEGORY[stem] ?? stem; + for (const libItem of raw.libraryItems) { + const name = libItem.name?.trim(); + if ( + !name || + !Array.isArray(libItem.elements) || + libItem.elements.length === 0 + ) + continue; + const bounds = computeBounds(libItem.elements); + const anchors = computeAnchors(bounds.width, bounds.height); + items.set(name.toLowerCase(), { + name, + category, + description: name, + width: bounds.width, + height: bounds.height, + elementCount: libItem.elements.length, + anchors, + elements: libItem.elements, + }); + } + } + return new LibraryCatalog(items); + } + + list(category?: string): CompactLibraryItem[] { + const result: CompactLibraryItem[] = []; + for (const item of this.items.values()) { + if (category && item.category !== category) continue; + result.push({ + name: item.name, + category: item.category, + width: item.width, + height: item.height, + }); + } + return result; + } + + get(name: string): DetailedLibraryItem | null { + const entry = this.items.get(name.toLowerCase()); + if (!entry) return null; + const { elements: _, ...detail } = entry; + return detail; + } + + stamp( + name: string, + x: number, + y: number, + scale: number, + label?: string + ): StampResult { + const entry = this.items.get(name.toLowerCase()); + if (!entry) throw new Error(`Library item "${name}" not found.`); + + const bounds = computeBounds(entry.elements); + const idMap = new Map(); + const addedIds: string[] = []; + + // Build ID mapping first so we can remap bindings + for (const el of entry.elements) { + const oldId = el.id as string; + const newId = randomUUID().slice(0, 8); + idMap.set(oldId, newId); + addedIds.push(newId); + } + + let labelApplied = false; + const elements: RawElement[] = entry.elements.map((el) => { + const cloned: RawElement = { ...el }; + // Assign fresh ID + cloned.id = idMap.get(el.id as string)!; + // Scale and offset coordinates + const origX = typeof el.x === "number" ? el.x : 0; + const origY = typeof el.y === "number" ? el.y : 0; + cloned.x = x + (origX - bounds.minX) * scale; + cloned.y = y + (origY - bounds.minY) * scale; + if (typeof el.width === "number") cloned.width = el.width * scale; + if (typeof el.height === "number") cloned.height = el.height * scale; + + // Scale points for arrows/lines + if (Array.isArray(el.points)) { + cloned.points = (el.points as number[][]).map(([px, py]) => [ + px * scale, + py * scale, + ]); + } + + // Remap containerId + if (typeof el.containerId === "string" && idMap.has(el.containerId)) { + cloned.containerId = idMap.get(el.containerId)!; + } + // Remap frameId + if (typeof el.frameId === "string" && idMap.has(el.frameId)) { + cloned.frameId = idMap.get(el.frameId)!; + } + // Remap boundElements + if (Array.isArray(el.boundElements)) { + cloned.boundElements = ( + el.boundElements as Array<{ id: string; type: string }> + ).map((be) => ({ + ...be, + id: idMap.get(be.id) ?? be.id, + })); + } + // Remap arrow bindings + if (typeof el.startBinding === "object" && el.startBinding !== null) { + const sb = el.startBinding as Record; + if (typeof sb.elementId === "string" && idMap.has(sb.elementId)) { + cloned.startBinding = { ...sb, elementId: idMap.get(sb.elementId)! }; + } + } + if (typeof el.endBinding === "object" && el.endBinding !== null) { + const eb = el.endBinding as Record; + if (typeof eb.elementId === "string" && idMap.has(eb.elementId)) { + cloned.endBinding = { ...eb, elementId: idMap.get(eb.elementId)! }; + } + } + // Remap groupIds + if (Array.isArray(el.groupIds)) { + cloned.groupIds = (el.groupIds as string[]).map( + (gid) => idMap.get(gid) ?? gid + ); + } + + // Replace label text + if (label && !labelApplied && el.type === "text") { + cloned.text = label; + cloned.originalText = label; + labelApplied = true; + } + + return cloned; + }); + + return { elements, addedIds }; + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +pnpm run test -- apps/server/test/whiteboard-library.test.ts +``` + +Expected: All tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add apps/server/src/shared/whiteboard-library.ts apps/server/test/whiteboard-library.test.ts +git commit -m "feat: add LibraryCatalog class for parsing excalidraw libraries and stamping items" +``` + +--- + +### Task 3: MCP Tools — whiteboard_library and whiteboard_insert + +**Files:** + +- Modify: `apps/server/src/shared/mcp/whiteboard-tools.ts` — register new tools +- Modify: `apps/server/src/server/mcp-whiteboard-handlers.ts` — add handler methods +- Modify: `apps/server/src/shared/mcp/server.ts` — wire new handlers into context +- Modify: `e2e/whiteboard.spec.ts` — E2E tests for the new MCP tools + +**Interfaces:** + +- Consumes: + - `LibraryCatalog` from `apps/server/src/shared/whiteboard-library.ts`: `list(category?)`, `get(name)`, `stamp(name, x, y, scale, label?)` + - Existing whiteboard persistence: `loadWhiteboard`, `saveWhiteboard`, `mergeElements` from handlers + - `WhiteboardToolsContext` type and `registerWhiteboardTools` function +- Produces: + - `whiteboard_library` MCP tool (browse/detail) + - `whiteboard_insert` MCP tool (place library item) + +- [ ] **Step 1: Add handler methods to mcp-whiteboard-handlers.ts** + +Add imports at the top of `apps/server/src/server/mcp-whiteboard-handlers.ts`: + +```typescript +import type { LibraryCatalog } from "../shared/whiteboard-library.js"; +``` + +Extend `CreateWhiteboardHandlersDeps` to include an optional `libraryCatalog`: + +```typescript +type CreateWhiteboardHandlersDeps = { + pool: Pool; + mediaRoot: string; + agentManager: AgentManager; + publishUiEvent: PublishUiEvent; + libraryCatalog?: LibraryCatalog; +}; +``` + +Add two new methods to the returned object from `createWhiteboardHandlers`: + +```typescript + libraryList( + category?: string, + name?: string + ): CompactLibraryItem[] | DetailedLibraryItem | null { + if (!libraryCatalog) return []; + if (name) return libraryCatalog.get(name); + return libraryCatalog.list(category); + }, + + async insertLibraryItem( + agentId: string, + name: string, + x: number, + y: number, + scale: number, + label?: string + ): Promise { + const agent = await agentManager.getAgent(agentId); + if (!agent) throw new Error("Agent not found."); + if (!libraryCatalog) throw new Error("Library catalog not available."); + + const stamped = libraryCatalog.stamp(name, x, y, scale, label); + + for (let attempt = 0; attempt < 3; attempt++) { + const row = await loadWhiteboard(pool, agentId); + const baseVersion = row ? Number(row.version) : 0; + const existing = row ? row.scene.elements : []; + + const merged = mergeElements(existing, stamped.elements, []); + + if (merged.elements.length > MAX_ELEMENTS) { + throw new Error(`Board is full (max ${MAX_ELEMENTS} elements).`); + } + + const saved = await saveWhiteboard( + pool, + agentId, + { elements: merged.elements }, + baseVersion, + "agent" + ); + if (saved) { + publishUiEvent({ + type: "whiteboard.changed", + agentId, + version: saved.version, + source: "agent", + }); + return { + version: saved.version, + elementCount: merged.elements.length, + addedIds: stamped.addedIds, + updatedIds: [], + deletedIds: [], + elements: simplifyElements(merged.elements), + }; + } + } + throw new Error( + "Whiteboard is being edited concurrently; try again in a moment." + ); + }, +``` + +Note: `mergeElements` is a module-private function in this file. The `insertLibraryItem` method has access to it because it lives in the same closure returned by `createWhiteboardHandlers`. + +- [ ] **Step 2: Extend WhiteboardToolsContext and register new tools** + +In `apps/server/src/shared/mcp/whiteboard-tools.ts`, add to the imports: + +```typescript +import type { + WhiteboardGetResult, + WhiteboardUpdateResult, +} from "../whiteboard.js"; +import type { + CompactLibraryItem, + DetailedLibraryItem, +} from "../whiteboard-library.js"; +``` + +Extend `WhiteboardToolsContext`: + +```typescript +export type WhiteboardToolsContext = { + agentId: string; + getWhiteboard?: (agentId: string) => Promise; + updateWhiteboard?: ( + agentId: string, + elements: unknown[], + deleteIds: string[] + ) => Promise; + clearWhiteboard?: (agentId: string) => Promise; + libraryList?: ( + category?: string, + name?: string + ) => CompactLibraryItem[] | DetailedLibraryItem | null; + insertLibraryItem?: ( + agentId: string, + name: string, + x: number, + y: number, + scale: number, + label?: string + ) => Promise; +}; +``` + +Add tool registrations at the end of `registerWhiteboardTools`, before the closing `}`: + +```typescript +if (allowed.has("whiteboard_library") && context.libraryList) { + const libraryList = context.libraryList; + + server.registerTool( + "whiteboard_library", + { + description: + "Browse the library of pre-built shapes and icons available for the whiteboard. " + + "Call with no arguments for a compact list of all items (name, category, dimensions). " + + "Call with `name` to get full detail (anchors, element count) for layout planning. " + + "Call with `category` to filter: 'flowchart', 'architecture', 'wireframe', 'ui-input'.", + inputSchema: { + category: z + .string() + .optional() + .describe( + "Filter by category: 'flowchart', 'architecture', 'wireframe', 'ui-input'" + ), + name: z + .string() + .optional() + .describe( + "Get full detail for a specific item by name (case-insensitive). " + + "Takes priority over category." + ), + }, + }, + async (args) => { + try { + const result = libraryList(args.category, args.name); + if (args.name && result === null) { + return { + content: [ + { + type: "text", + text: `Library item "${args.name}" not found. Call whiteboard_library with no arguments to see all available items.`, + }, + ], + isError: true, + }; + } + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } catch (error) { + return toToolError(error); + } + } + ); +} + +if (allowed.has("whiteboard_insert") && context.insertLibraryItem) { + const agentId = context.agentId; + const insertLibraryItem = context.insertLibraryItem; + + server.registerTool( + "whiteboard_insert", + { + description: + "Place a pre-built library shape onto the whiteboard at a given position. " + + "Use `whiteboard_library` first to browse available items and get their dimensions " + + "for layout planning. Items are scaled and positioned, with optional text label replacement.", + inputSchema: { + name: z + .string() + .describe( + "Library item name (case-insensitive). Use whiteboard_library to see available names." + ), + x: z.number().describe("X coordinate for the item's top-left corner."), + y: z.number().describe("Y coordinate for the item's top-left corner."), + scale: z + .number() + .min(0.1) + .max(10) + .default(1) + .describe( + "Size multiplier. 1 = original size, 0.5 = half, 2 = double." + ), + label: z + .string() + .optional() + .describe( + "Replace the item's primary text label (e.g. place a 'Server' icon but label it 'Auth Service')." + ), + }, + }, + async (args) => { + try { + const result = await insertLibraryItem( + agentId, + args.name, + args.x, + args.y, + args.scale, + args.label + ); + const summary = { + ok: true, + version: result.version, + elementCount: result.elementCount, + addedIds: result.addedIds, + }; + return { + content: [{ type: "text", text: JSON.stringify(summary, null, 2) }], + }; + } catch (error) { + return toToolError(error); + } + } + ); +} +``` + +- [ ] **Step 3: Wire into MCP server context** + +In `apps/server/src/shared/mcp/server.ts`: + +1. Add to the context type (around line 423, after `clearWhiteboard`): + +```typescript + libraryList?: ( + category?: string, + name?: string + ) => CompactLibraryItem[] | DetailedLibraryItem | null; + insertLibraryItem?: ( + agentId: string, + name: string, + x: number, + y: number, + scale: number, + label?: string + ) => Promise; +``` + +2. Add the import for types at the top: + +```typescript +import type { + CompactLibraryItem, + DetailedLibraryItem, +} from "../whiteboard-library.js"; +``` + +3. In the whiteboard tools registration block (around line 582), add the new context fields: + +```typescript +registerWhiteboardTools(server, allowed, { + agentId: context.agent.id, + getWhiteboard: context.getWhiteboard, + updateWhiteboard: context.updateWhiteboard, + clearWhiteboard: context.clearWhiteboard, + libraryList: context.libraryList, + insertLibraryItem: context.insertLibraryItem, +}); +``` + +- [ ] **Step 4: Initialize LibraryCatalog at server startup and pass to handlers** + +Find where `createWhiteboardHandlers` is called in `apps/server/src/server/mcp-handlers.ts` (line ~155). The catalog needs to be initialized before this call. + +Add import at the top: + +```typescript +import { LibraryCatalog } from "../shared/whiteboard-library.js"; +``` + +The function that calls `createWhiteboardHandlers` receives `deps`. Add `libraryCatalog` to those deps. Initialize it near the top of the function: + +```typescript +const whiteboardHandlers = createWhiteboardHandlers({ + pool, + mediaRoot, + agentManager, + publishUiEvent, + libraryCatalog: deps.libraryCatalog, +}); +``` + +The `libraryCatalog` must be created once at app startup (before the MCP handlers factory is called) and passed through. Find the server bootstrap code that creates the deps object and add: + +```typescript +import path from "node:path"; + +// In the bootstrap/startup function: +const libraryCatalog = await LibraryCatalog.fromDirectory( + path.resolve(__dirname, "../../../web/public/excalidraw/libraries") +); +``` + +Pass it through the deps chain to `createWhiteboardHandlers`. The exact wiring depends on the server bootstrap structure — follow the existing pattern for how `pool`, `mediaRoot`, and `agentManager` are threaded through. + +- [ ] **Step 5: Add E2E tests** + +Add to `e2e/whiteboard.spec.ts`: + +```typescript +test("whiteboard MCP tool: whiteboard_library returns catalog", async ({ + request, +}) => { + const agent = await createAgentViaAPI(request, { + name: `e2e-agent-wb-lib-${Date.now()}`, + }); + + // List all items + const listJson = await callMcpTool( + request, + agent.id, + "whiteboard_library", + {} + ); + const listResult = listJson.result as { + content?: Array<{ text?: string }>; + }; + const items = JSON.parse(listResult?.content?.[0]?.text ?? "[]") as Array<{ + name: string; + category: string; + width: number; + height: number; + }>; + expect(items.length).toBeGreaterThanOrEqual(40); + expect(items[0].name).toBeTruthy(); + expect(items[0].width).toBeGreaterThan(0); + + // Filter by category + const archJson = await callMcpTool(request, agent.id, "whiteboard_library", { + category: "architecture", + }); + const archResult = archJson.result as { + content?: Array<{ text?: string }>; + }; + const archItems = JSON.parse( + archResult?.content?.[0]?.text ?? "[]" + ) as Array<{ category: string }>; + expect(archItems.length).toBeGreaterThan(0); + expect(archItems.every((i) => i.category === "architecture")).toBe(true); + + // Get detail for a specific item + const detailJson = await callMcpTool( + request, + agent.id, + "whiteboard_library", + { name: items[0].name } + ); + const detailResult = detailJson.result as { + content?: Array<{ text?: string }>; + }; + const detail = JSON.parse(detailResult?.content?.[0]?.text ?? "{}") as { + name: string; + anchors: { + top: number[]; + bottom: number[]; + left: number[]; + right: number[]; + }; + elementCount: number; + }; + expect(detail.name).toBe(items[0].name); + expect(detail.anchors.top).toHaveLength(2); + expect(detail.elementCount).toBeGreaterThan(0); +}); + +test("whiteboard MCP tool: whiteboard_insert places library item", async ({ + request, +}) => { + const agent = await createAgentViaAPI(request, { + name: `e2e-agent-wb-insert-${Date.now()}`, + }); + + // First get available items + const listJson = await callMcpTool( + request, + agent.id, + "whiteboard_library", + {} + ); + const listResult = listJson.result as { + content?: Array<{ text?: string }>; + }; + const items = JSON.parse(listResult?.content?.[0]?.text ?? "[]") as Array<{ + name: string; + }>; + const itemName = items[0].name; + + // Insert the item + const insertJson = await callMcpTool(request, agent.id, "whiteboard_insert", { + name: itemName, + x: 100, + y: 200, + scale: 1, + }); + const insertResult = insertJson.result as { + content?: Array<{ text?: string }>; + }; + const insertData = JSON.parse(insertResult?.content?.[0]?.text ?? "{}") as { + ok: boolean; + addedIds: string[]; + elementCount: number; + }; + expect(insertData.ok).toBe(true); + expect(insertData.addedIds.length).toBeGreaterThan(0); + expect(insertData.elementCount).toBeGreaterThan(0); + + // Verify the elements are on the board via REST + const getRes = await request.get(`/api/v1/agents/${agent.id}/whiteboard`, { + headers: AUTH_HEADER, + }); + const boardData = (await getRes.json()) as { + scene: { elements: Array<{ id: string }> }; + }; + expect(boardData.scene.elements.length).toBe(insertData.elementCount); +}); + +test("whiteboard MCP tool: whiteboard_insert with label and scale", async ({ + request, +}) => { + const agent = await createAgentViaAPI(request, { + name: `e2e-agent-wb-insert-label-${Date.now()}`, + }); + + // Find an item with text elements via library detail + const listJson = await callMcpTool( + request, + agent.id, + "whiteboard_library", + {} + ); + const items = JSON.parse( + (listJson.result as { content?: Array<{ text?: string }> })?.content?.[0] + ?.text ?? "[]" + ) as Array<{ name: string }>; + + // Insert at 2x scale with a custom label + const insertJson = await callMcpTool(request, agent.id, "whiteboard_insert", { + name: items[0].name, + x: 50, + y: 50, + scale: 2, + label: "My Service", + }); + const insertData = JSON.parse( + (insertJson.result as { content?: Array<{ text?: string }> })?.content?.[0] + ?.text ?? "{}" + ) as { ok: boolean }; + expect(insertData.ok).toBe(true); +}); +``` + +- [ ] **Step 6: Run type check and tests** + +```bash +pnpm run check +pnpm run test -- apps/server/test/whiteboard-library.test.ts apps/server/test/whiteboard.test.ts +pnpm run test:e2e -- --grep "whiteboard" +``` + +Expected: All pass. + +- [ ] **Step 7: Commit** + +```bash +git add apps/server/src/shared/mcp/whiteboard-tools.ts \ + apps/server/src/server/mcp-whiteboard-handlers.ts \ + apps/server/src/shared/mcp/server.ts \ + apps/server/src/server/mcp-handlers.ts \ + e2e/whiteboard.spec.ts +git commit -m "feat: add whiteboard_library and whiteboard_insert MCP tools for agent access to shape libraries" +``` + +--- + +### Task 4: Client-Side Library Loading + +**Files:** + +- Modify: `apps/web/src/components/app/whiteboard-tab.tsx` + +**Interfaces:** + +- Consumes: `.excalidrawlib` files at `/excalidraw/libraries/` (served by Vite from `public/`) +- Consumes: `excalidrawAPI.updateLibrary()` from `@excalidraw/excalidraw` +- Produces: Libraries loaded into Excalidraw's built-in library panel on mount + +- [ ] **Step 1: Add library loading to WhiteboardCanvas** + +In `apps/web/src/components/app/whiteboard-tab.tsx`, add a new `useEffect` inside `WhiteboardCanvas` that fires when `excalidrawAPI` becomes available. Place it after the existing `excalidrawAPI` state declaration (around line 76): + +```typescript +const LIBRARY_FILES = [ + "/excalidraw/libraries/flow-chart-symbols.excalidrawlib", + "/excalidraw/libraries/architecture-diagram-components.excalidrawlib", + "/excalidraw/libraries/universal-ui-kit.excalidrawlib", + "/excalidraw/libraries/html-input-elements.excalidrawlib", +]; + +// Inside WhiteboardCanvas, after excalidrawAPI state: +useEffect(() => { + if (!excalidrawAPI) return; + let cancelled = false; + void (async () => { + try { + const responses = await Promise.all( + LIBRARY_FILES.map((url) => fetch(url)) + ); + const blobs = await Promise.all(responses.map((r) => r.blob())); + if (cancelled) return; + for (const blob of blobs) { + await excalidrawAPI.updateLibrary({ + libraryItems: blob, + merge: true, + defaultStatus: "published", + openLibraryMenu: false, + }); + } + } catch { + // Library loading is best-effort — whiteboard works fine without them + } + })(); + return () => { + cancelled = true; + }; +}, [excalidrawAPI]); +``` + +Note: `updateLibrary` accepts a `Blob` directly (the `.excalidrawlib` file content). Excalidraw handles parsing internally. The `defaultStatus: "published"` makes items appear immediately without the user having to "accept" them. `openLibraryMenu: false` prevents the library panel from opening automatically. + +- [ ] **Step 2: Run finalize:web to verify build** + +```bash +pnpm run finalize:web +``` + +Expected: Type check and production build pass. + +- [ ] **Step 3: Validate in browser** + +Start the dev server: + +```bash +# Use repo_dev_up MCP tool +``` + +Open the whiteboard tab for any agent. The library panel (book icon in the Excalidraw toolbar) should show the pre-loaded shapes organized by source. Drag a shape onto the canvas to verify it works. Take a screenshot and share via `dispatch_share`. + +- [ ] **Step 4: Commit** + +```bash +git add apps/web/src/components/app/whiteboard-tab.tsx +git commit -m "feat: lazy-load excalidraw shape libraries on whiteboard mount" +``` + +- [ ] **Step 5: Run all pre-completion checks** + +```bash +pnpm run finalize:web +pnpm run test +pnpm run test:e2e +``` + +Expected: All pass. diff --git a/docs/superpowers/specs/2026-07-11-whiteboard-libraries-design.md b/docs/superpowers/specs/2026-07-11-whiteboard-libraries-design.md new file mode 100644 index 00000000..ac13fab3 --- /dev/null +++ b/docs/superpowers/specs/2026-07-11-whiteboard-libraries-design.md @@ -0,0 +1,181 @@ +# Whiteboard Libraries: Pre-loaded Excalidraw Libraries with Agent Catalog & Insert + +## Overview + +Add pre-loaded Excalidraw shape libraries to the whiteboard (flowchart, architecture, UI wireframe, HTML inputs). Users get drag-and-drop access via Excalidraw's built-in library panel. Agents get two new MCP tools to browse available shapes with dimensions and place them on the board. + +## Library Assets + +Four `.excalidrawlib` v2 files checked into the repo at `apps/web/public/excalidraw/libraries/`: + +| File | Items | Size | Category slug | Coverage | +| ----------------------------------------------- | ----- | ------ | -------------- | ----------------------------------------------------------------- | +| `flow-chart-symbols.excalidrawlib` | 15 | ~56KB | `flowchart` | Start/End, Process, Decision, Database, Document, I/O, Connectors | +| `architecture-diagram-components.excalidrawlib` | 11 | ~96KB | `architecture` | Server, Docker, GitHub, Slack, VPC, Subnet, User, Device, Email | +| `universal-ui-kit.excalidrawlib` | 22 | ~151KB | `wireframe` | Button, Checkbox, Toggle, Select, Calendar, Charts, Alerts | +| `html-input-elements.excalidrawlib` | 8 | ~32KB | `ui-input` | Button, Number, Date, Checkbox, Toggle, Switch, Select | + +Total: 56 named items, ~335KB. Licensed MIT (Excalidraw, Copyright 2020) — include attribution in LICENSE or NOTICES. + +Source: https://github.com/excalidraw/excalidraw-libraries (v2-format libraries from community contributors). + +## Server-Side Catalog + +### Startup parsing + +At server startup, read the `.excalidrawlib` files from disk (`apps/web/public/excalidraw/libraries/`), parse the v2 JSON, and build an in-memory catalog: + +```typescript +type LibraryCatalogItem = { + name: string; // from libraryItem.name + category: string; // mapped from filename + description: string; // derived from name or manually curated + width: number; // bounding box: max(x+width) - min(x) + height: number; // bounding box: max(y+height) - min(y) + elementCount: number; // number of primitives in the item + anchors: { + top: [number, number]; + bottom: [number, number]; + left: [number, number]; + right: [number, number]; + }; + elements: unknown[]; // raw Excalidraw elements (not exposed to agents via browse) +}; +``` + +Bounding boxes are computed from `min(x)` / `max(x + width)` across all elements in each library item. Anchors are midpoints of the bounding box edges. + +### Catalog storage + +A `LibraryCatalog` class in `apps/server/src/shared/whiteboard-library.ts`: + +- Parses all `.excalidrawlib` files from a given directory +- Exposes `list(category?: string): CompactItem[]` for browse mode +- Exposes `get(name: string): LibraryCatalogItem | null` for detail/insert +- Exposes `stamp(name: string, x: number, y: number, scale: number): StampedElements` to clone elements with fresh IDs, scale, and offset + +## MCP Tools + +### `whiteboard_library` — Browse available library items + +Registered alongside existing whiteboard tools in `apps/server/src/shared/mcp/whiteboard-tools.ts`. + +**Input schema:** + +```typescript +{ + category?: string; // filter by category slug + name?: string; // get full detail for one item +} +``` + +**Behavior** (evaluated in priority order): + +- `name` provided: returns full detail for that item (ignores `category`) +- `category` provided: returns filtered compact list +- No args: returns compact list of all items + +**Compact response** (no args / category filter): + +```json +[ + { "name": "Server", "category": "architecture", "width": 80, "height": 120 }, + { "name": "Database", "category": "flowchart", "width": 186, "height": 139 } +] +``` + +**Detail response** (`name` provided): + +```json +{ + "name": "Server", + "category": "architecture", + "description": "Server/host icon", + "width": 80, + "height": 120, + "elementCount": 5, + "anchors": { + "top": [40, 0], + "bottom": [40, 120], + "left": [0, 60], + "right": [80, 60] + } +} +``` + +Element data is never returned to agents — only metadata for layout planning. + +### `whiteboard_insert` — Place a library item on the board + +**Input schema:** + +```typescript +{ + name: string; // library item name (required) + x: number; // target x coordinate (required) + y: number; // target y coordinate (required) + scale?: number; // size multiplier, default 1.0 + label?: string; // replace text content if item contains a text element +} +``` + +**Behavior:** + +1. Look up item by `name` in the catalog (case-insensitive match) +2. Clone all elements with fresh UUIDs +3. Scale all coordinates and dimensions by `scale` factor relative to the item's local origin +4. Offset all coordinates to place the item's top-left at `(x, y)` +5. If `label` is provided and the item contains text elements, replace the first/primary text element's content +6. Merge onto the board using the existing `mergeElements` + `saveWhiteboard` logic (same as `whiteboard_update`) +7. Publish `whiteboard.changed` SSE event + +**Response:** Same shape as `whiteboard_update`: + +```json +{ + "ok": true, + "version": 4, + "addedIds": ["fresh-id-1", "fresh-id-2", "fresh-id-3"], + "elementCount": 15 +} +``` + +## Client-Side Library Loading + +### Lazy loading on whiteboard mount + +In `whiteboard-tab.tsx`, after `excalidrawAPI` is available: + +1. Fetch all 4 `.excalidrawlib` files in parallel from `/excalidraw/libraries/` +2. Parse each as JSON (v2 format: `{ type: "excalidrawlib", version: 2, libraryItems: [...] }`) +3. Call `excalidrawAPI.updateLibrary({ libraryItems: allItems, merge: true, defaultStatus: "published" })` + +Items appear immediately in Excalidraw's built-in library panel, grouped by source. Users drag and drop as normal. No custom UI needed. + +Fetches are cached by the browser. The loading happens once per component mount (whiteboard tab stays mounted via CSS hidden, so effectively once per session). + +### No bundle impact + +Library files are static assets in `public/`, not imported into the JS bundle. They're only fetched when the whiteboard component mounts. + +## File Changes + +### New files + +- `apps/web/public/excalidraw/libraries/*.excalidrawlib` — 4 library files +- `apps/server/src/shared/whiteboard-library.ts` — `LibraryCatalog` class (parse, compute dimensions, stamp) + +### Modified files + +- `apps/server/src/shared/mcp/whiteboard-tools.ts` — register `whiteboard_library` and `whiteboard_insert` tools +- `apps/server/src/server/mcp-whiteboard-handlers.ts` — add handler methods for the new tools +- `apps/web/src/components/app/whiteboard-tab.tsx` — lazy-load libraries on mount + +### Test files + +- `apps/server/test/whiteboard.test.ts` — unit tests for catalog parsing, bounding box computation, stamp logic +- `e2e/whiteboard.spec.ts` — E2E tests for MCP library/insert tools + +## Attribution + +Include MIT license attribution for the Excalidraw libraries in the repo (e.g. a `NOTICES` comment in the libraries directory or the project LICENSE file). From e8a35367c0044c7276adc64e51adae5a4a95f1d7 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Sat, 11 Jul 2026 17:15:23 -0600 Subject: [PATCH 5/9] feat: add excalidraw library files for whiteboard (flowchart, architecture, wireframe, UI inputs) --- apps/web/public/excalidraw/libraries/NOTICES | 36 + ...hitecture-diagram-components.excalidrawlib | 3555 +++++++++++ .../flow-chart-symbols.excalidrawlib | 2150 +++++++ .../html-input-elements.excalidrawlib | 1165 ++++ .../libraries/universal-ui-kit.excalidrawlib | 5533 +++++++++++++++++ 5 files changed, 12439 insertions(+) create mode 100644 apps/web/public/excalidraw/libraries/NOTICES create mode 100644 apps/web/public/excalidraw/libraries/architecture-diagram-components.excalidrawlib create mode 100644 apps/web/public/excalidraw/libraries/flow-chart-symbols.excalidrawlib create mode 100644 apps/web/public/excalidraw/libraries/html-input-elements.excalidrawlib create mode 100644 apps/web/public/excalidraw/libraries/universal-ui-kit.excalidrawlib diff --git a/apps/web/public/excalidraw/libraries/NOTICES b/apps/web/public/excalidraw/libraries/NOTICES new file mode 100644 index 00000000..520f6ac3 --- /dev/null +++ b/apps/web/public/excalidraw/libraries/NOTICES @@ -0,0 +1,36 @@ +Excalidraw Libraries +==================== + +The .excalidrawlib files in this directory are sourced from the +excalidraw-libraries project: + + https://github.com/excalidraw/excalidraw-libraries + +Licensed under the MIT License: + + Copyright (c) 2020 Excalidraw + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Individual libraries: + - flow-chart-symbols.excalidrawlib — by finfin + - architecture-diagram-components.excalidrawlib — by anna-pastushko + - universal-ui-kit.excalidrawlib — by manuelernestog + - html-input-elements.excalidrawlib — by marwinburesch diff --git a/apps/web/public/excalidraw/libraries/architecture-diagram-components.excalidrawlib b/apps/web/public/excalidraw/libraries/architecture-diagram-components.excalidrawlib new file mode 100644 index 00000000..528ea02e --- /dev/null +++ b/apps/web/public/excalidraw/libraries/architecture-diagram-components.excalidrawlib @@ -0,0 +1,3555 @@ +{ + "type": "excalidrawlib", + "version": 2, + "source": "https://excalidraw.com", + "libraryItems": [ + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 3035, + "versionNonce": 1280493655, + "isDeleted": false, + "id": "GUq90cfPCBI5_XleHcVf4", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1809.1495216187216, + "y": 1477.109035697224, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 64.46115236495699, + "height": 64.39942473426015, + "seed": 886726492, + "groupIds": [ + "6yKeiHwTZ9f1hENg1Rvln", + "T3ua4nOmCVl7yrAf5BCce", + "TCfHzaLQwx50O5BjXwJ-k", + "O-QUX1l0MwWQlYY-pidnL", + "32xgjoIfrxequxeS1yiKx" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "EUHZ_sUolb_30DI6OzWP4" + }, + { + "type": "arrow", + "id": "HjpGCBxUuBW46Wd5r_3cI" + }, + { + "type": "arrow", + "id": "qVpgRhAMH6njKSxkEUaob" + }, + { + "type": "arrow", + "id": "fXUqR5l9-mJxPJpngO-uK" + }, + { + "type": "arrow", + "id": "GST0HrCYDviCg-JDHslU7" + }, + { + "type": "arrow", + "id": "WwJfE-IGw-hz6uAt8MWQ6" + }, + { + "type": "arrow", + "id": "_xS_u95gyqYLnjH6Z_ekf" + } + ], + "updated": 1652788086502, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 2667, + "versionNonce": 1834863769, + "isDeleted": false, + "id": "RyexOyb07MMvy6EvnZWEe", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1829.0292782163615, + "y": 1525.8995920327388, + "strokeColor": "#e01e5a", + "backgroundColor": "#e01e5a", + "width": 9.812617686394493, + "height": 21.62230587269945, + "seed": 1677907044, + "groupIds": [ + "HCz7wqtztIfOhP5y_Go4g", + "O-QUX1l0MwWQlYY-pidnL", + "32xgjoIfrxequxeS1yiKx" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788086502, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 1.5493619039573798, + 7.856944476158105 + ], + [ + 9.210070077961463, + 7.039820663903555 + ], + [ + 9.812617686394493, + -13.576796391808415 + ], + [ + 0.7746982889836341, + -13.765361396541337 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 3068, + "versionNonce": 2040898935, + "isDeleted": false, + "id": "LqIPxdOsv3S99kOaoHOiG", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1817.9597283311568, + "y": 1510.4826671595124, + "strokeColor": "#e01e5a", + "backgroundColor": "#e01e5a", + "width": 10.809935258890052, + "height": 10.400984872948921, + "seed": 1905136604, + "groupIds": [ + "HCz7wqtztIfOhP5y_Go4g", + "O-QUX1l0MwWQlYY-pidnL", + "32xgjoIfrxequxeS1yiKx" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788086502, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -2.818991814833378, + 2.324732734179281 + ], + [ + -3.8436806131559678, + 6.762634497124866 + ], + [ + 1.1650667382148967, + 10.071035010913667 + ], + [ + 6.770132861029776, + 7.667134320273243 + ], + [ + 6.946438793826424, + -0.3210484014135393 + ], + [ + 6.966254645734084, + -0.329949862035255 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 2765, + "versionNonce": 420139385, + "isDeleted": false, + "id": "F6mCSafAu5tWLjM5CYatC", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.5707963267948957, + "x": 1823.1083833032508, + "y": 1505.83322408803, + "strokeColor": "#36c5f0", + "backgroundColor": "#36c5f0", + "width": 9.812617686394493, + "height": 21.62230587269945, + "seed": 1156753380, + "groupIds": [ + "HCz7wqtztIfOhP5y_Go4g", + "O-QUX1l0MwWQlYY-pidnL", + "32xgjoIfrxequxeS1yiKx" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788086502, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 1.5493619039573798, + 7.856944476158105 + ], + [ + 9.210070077961463, + 7.039820663903555 + ], + [ + 9.812617686394493, + -13.576796391808415 + ], + [ + 0.7746982889836341, + -13.765361396541337 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 3150, + "versionNonce": 1860165271, + "isDeleted": false, + "id": "FDcmhAvzwShOEBdo_dw0l", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.5707963267948957, + "x": 1832.77271399571, + "y": 1483.2516879256468, + "strokeColor": "#36c5f0", + "backgroundColor": "#36c5f0", + "width": 10.809935258890052, + "height": 10.400984872948921, + "seed": 920074332, + "groupIds": [ + "HCz7wqtztIfOhP5y_Go4g", + "O-QUX1l0MwWQlYY-pidnL", + "32xgjoIfrxequxeS1yiKx" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788086502, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -2.818991814833378, + 2.324732734179281 + ], + [ + -3.8436806131559678, + 6.762634497124866 + ], + [ + 1.1650667382148967, + 10.071035010913667 + ], + [ + 6.770132861029776, + 7.667134320273243 + ], + [ + 6.946438793826424, + -0.3210484014135393 + ], + [ + 6.966254645734084, + -0.329949862035255 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 2750, + "versionNonce": 1451506265, + "isDeleted": false, + "id": "nYFs-74rOPAlZ2nOvE97v", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 3.141592653589793, + "x": 1842.661901614898, + "y": 1498.7158578723952, + "strokeColor": "#2eb67d", + "backgroundColor": "#2eb67d", + "width": 9.812617686394493, + "height": 21.62230587269945, + "seed": 798755684, + "groupIds": [ + "HCz7wqtztIfOhP5y_Go4g", + "O-QUX1l0MwWQlYY-pidnL", + "32xgjoIfrxequxeS1yiKx" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788086502, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 1.5493619039573798, + 7.856944476158105 + ], + [ + 9.210070077961463, + 7.039820663903555 + ], + [ + 9.812617686394493, + -13.576796391808415 + ], + [ + 0.7746982889836341, + -13.765361396541337 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 3134, + "versionNonce": 1857418167, + "isDeleted": false, + "id": "WmaJXyLyLpOOjJhw-UoJj", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 3.141592653589793, + "x": 1861.4724630741243, + "y": 1496.5456409197295, + "strokeColor": "#2eb67d", + "backgroundColor": "#2eb67d", + "width": 10.809935258890052, + "height": 10.400984872948921, + "seed": 814206172, + "groupIds": [ + "HCz7wqtztIfOhP5y_Go4g", + "O-QUX1l0MwWQlYY-pidnL", + "32xgjoIfrxequxeS1yiKx" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788086502, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -2.818991814833378, + 2.324732734179281 + ], + [ + -3.8436806131559678, + 6.762634497124866 + ], + [ + 1.1650667382148967, + 10.071035010913667 + ], + [ + 6.770132861029776, + 7.667134320273243 + ], + [ + 6.946438793826424, + -0.3210484014135393 + ], + [ + 6.966254645734084, + -0.329949862035255 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 2819, + "versionNonce": 1990035257, + "isDeleted": false, + "id": "vkY1-p93k12hl8wGSNqf9", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 4.712388980384688, + "x": 1849.4061798638486, + "y": 1519.6810119128954, + "strokeColor": "#ecb22e", + "backgroundColor": "#ecb22e", + "width": 9.812617686394493, + "height": 21.62230587269945, + "seed": 1447886564, + "groupIds": [ + "HCz7wqtztIfOhP5y_Go4g", + "O-QUX1l0MwWQlYY-pidnL", + "32xgjoIfrxequxeS1yiKx" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788086502, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 1.5493619039573798, + 7.856944476158105 + ], + [ + 9.210070077961463, + 7.039820663903555 + ], + [ + 9.812617686394493, + -13.576796391808415 + ], + [ + 0.7746982889836341, + -13.765361396541337 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 3219, + "versionNonce": 1900831959, + "isDeleted": false, + "id": "KSOt5gdgvmOrCDQ87AB18", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 4.712388980384688, + "x": 1845.5079975038857, + "y": 1525.079826667898, + "strokeColor": "#ecb22e", + "backgroundColor": "#ecb22e", + "width": 10.809935258890052, + "height": 10.400984872948921, + "seed": 1077006684, + "groupIds": [ + "HCz7wqtztIfOhP5y_Go4g", + "O-QUX1l0MwWQlYY-pidnL", + "32xgjoIfrxequxeS1yiKx" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788086502, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -2.818991814833378, + 2.324732734179281 + ], + [ + -3.8436806131559678, + 6.762634497124866 + ], + [ + 1.1650667382148967, + 10.071035010913667 + ], + [ + 6.770132861029776, + 7.667134320273243 + ], + [ + 6.946438793826424, + -0.3210484014135393 + ], + [ + 6.966254645734084, + -0.329949862035255 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "text", + "version": 1858, + "versionNonce": 1673148441, + "isDeleted": false, + "id": "w5bRazofab7lU3wH7aSK_", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1815.3800978012005, + "y": 1545.8807498280685, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 52, + "height": 25, + "seed": 1905492580, + "groupIds": [ + "32xgjoIfrxequxeS1yiKx" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086502, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Slack", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Slack" + } + ], + "id": "mb6aDt8lLpnjb9BXfkKzo", + "created": 1652788300698, + "name": "Slack" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 3159, + "versionNonce": 1890483031, + "isDeleted": false, + "id": "uanIHxCIrMYntFa5JEEBR", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2000.8934435647661, + "y": 1476.3079086177738, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 64.46115236495699, + "height": 64.39942473426015, + "seed": 323870300, + "groupIds": [ + "ACroLnLjF7CL0w1OeLMWU", + "nj3uI0loSvxxD4oKwuvTC", + "4xoG6DV7mIB8Ajo2D1P48", + "_aLjJPj6ubf6pav06ALqJ", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "EUHZ_sUolb_30DI6OzWP4" + }, + { + "type": "arrow", + "id": "HjpGCBxUuBW46Wd5r_3cI" + }, + { + "type": "arrow", + "id": "qVpgRhAMH6njKSxkEUaob" + }, + { + "type": "arrow", + "id": "fXUqR5l9-mJxPJpngO-uK" + }, + { + "type": "arrow", + "id": "GST0HrCYDviCg-JDHslU7" + }, + { + "type": "arrow", + "id": "WwJfE-IGw-hz6uAt8MWQ6" + }, + { + "type": "arrow", + "id": "_xS_u95gyqYLnjH6Z_ekf" + } + ], + "updated": 1652788086506, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 2392, + "versionNonce": 336251289, + "isDeleted": false, + "id": "7_toODjCSHDISkrm2shFd", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2016.0359076430884, + "y": 1511.661855278454, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 56.62135476067269, + "height": 27.781375401353056, + "seed": 841461348, + "groupIds": [ + "eL4uAkeWwfpsKN2jOX7_g", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 26.49928798736996, + -0.5555399791810629 + ], + [ + 32.86897029257218, + -1.351746926303818 + ], + [ + 31.54797965057393, + -6.47281952059744 + ], + [ + 33.64708101980081, + -11.340553540806383 + ], + [ + 36.88620050527203, + -6.798543885447785 + ], + [ + 36.45191491873459, + -3.1432192393850187 + ], + [ + 41.67251763363534, + -6.7171136294920615 + ], + [ + 45.75309251293886, + -5.2242237812582175 + ], + [ + 43.32826283037451, + -1.9398610124836608 + ], + [ + 37.99003529362133, + 0.5844902862542796 + ], + [ + 24.0563885094017, + 14.925301714362618 + ], + [ + 0.1701084154921375, + 16.440821860546677 + ], + [ + -10.868262247733824, + 1.1146897259718145 + ], + [ + -3.582944626162501, + -0.5971552103421658 + ], + [ + -0.28719357028327863, + -0.019545320337599037 + ] + ] + }, + { + "type": "rectangle", + "version": 1298, + "versionNonce": 504559735, + "isDeleted": false, + "id": "6ldzdstTHWD2NSyZpjIOg", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2009.750948956059, + "y": 1503.8496173056888, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 5.566352494330056, + "height": 5.566352494330056, + "seed": 1835811300, + "groupIds": [ + "eL4uAkeWwfpsKN2jOX7_g", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1353, + "versionNonce": 990916217, + "isDeleted": false, + "id": "ivs_QGHAG6lFu1mrc09qQ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2016.7088876275266, + "y": 1503.5944970012197, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 5.566352494330056, + "height": 5.566352494330056, + "seed": 164783460, + "groupIds": [ + "eL4uAkeWwfpsKN2jOX7_g", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1373, + "versionNonce": 1613418903, + "isDeleted": false, + "id": "1fSQ-3ayqjVqHJK8VuyFh", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2023.991524160524, + "y": 1503.710450512912, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 5.566352494330056, + "height": 5.566352494330056, + "seed": 212343012, + "groupIds": [ + "eL4uAkeWwfpsKN2jOX7_g", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1394, + "versionNonce": 701888345, + "isDeleted": false, + "id": "E8EWStWQBSOsaK4qU9Ft2", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2031.2509668768762, + "y": 1503.3625666205105, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 5.566352494330056, + "height": 5.566352494330056, + "seed": 561732708, + "groupIds": [ + "eL4uAkeWwfpsKN2jOX7_g", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1436, + "versionNonce": 2786999, + "isDeleted": false, + "id": "wZ7pAlvrm5QFNU_NtlYnW", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2038.1393202055147, + "y": 1503.4553341013263, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 5.566352494330056, + "height": 5.566352494330056, + "seed": 1377447908, + "groupIds": [ + "eL4uAkeWwfpsKN2jOX7_g", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1340, + "versionNonce": 1387242553, + "isDeleted": false, + "id": "lJi4w4P0QseZ4KgNEaXCX", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2016.5117382395738, + "y": 1496.8453065724698, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 5.566352494330056, + "height": 5.566352494330056, + "seed": 1049793380, + "groupIds": [ + "eL4uAkeWwfpsKN2jOX7_g", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1364, + "versionNonce": 750163927, + "isDeleted": false, + "id": "-wnimA_jQMKWc_-g6eaOC", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2023.8175724821076, + "y": 1496.9380779461715, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 5.566352494330056, + "height": 5.566352494330056, + "seed": 1429374692, + "groupIds": [ + "eL4uAkeWwfpsKN2jOX7_g", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1388, + "versionNonce": 293647641, + "isDeleted": false, + "id": "kbQ8u03t5Yo9KkMQKkAPk", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2031.216178098346, + "y": 1496.9380701604036, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 5.566352494330056, + "height": 5.566352494330056, + "seed": 176687716, + "groupIds": [ + "eL4uAkeWwfpsKN2jOX7_g", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1407, + "versionNonce": 1273201911, + "isDeleted": false, + "id": "-zI2AUQ5W8JbYNUCkTieB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2030.9726410770982, + "y": 1489.8409763748195, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 5.566352494330056, + "height": 5.566352494330056, + "seed": 291577316, + "groupIds": [ + "eL4uAkeWwfpsKN2jOX7_g", + "544RHJd-Dhaeg2-NkUCY1", + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1917, + "versionNonce": 2042407417, + "isDeleted": false, + "id": "goGzbKzw9Qo8Immm60G38", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1999.6240197472443, + "y": 1546.0921712247273, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 67, + "height": 25, + "seed": 804287196, + "groupIds": [ + "0XWV8-iYC-gTs59lvQu5F" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788086506, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Docker", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Docker" + } + ], + "id": "Qsr_bCZKjzSUef9igg3F_", + "created": 1652788294057, + "name": "Docker" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 3143, + "versionNonce": 1591930393, + "isDeleted": false, + "id": "QtgIqDyZcHqpV8Ju_l857", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2131.4206261576473, + "y": 1482.2143172736266, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 64.46115236495699, + "height": 64.39942473426015, + "seed": 1157021412, + "groupIds": [ + "LrESEPdWDTzkPK50aCds2", + "h1Qh7Eu_XCbl35WTZ9Pq9", + "ZdJX-h95nCliReNZ8kLqG", + "I10Yt6AhGQErTZkizQcks", + "gR9JOi18bkX5JTHz6Te_0" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "type": "arrow", + "id": "EUHZ_sUolb_30DI6OzWP4" + }, + { + "type": "arrow", + "id": "HjpGCBxUuBW46Wd5r_3cI" + }, + { + "type": "arrow", + "id": "qVpgRhAMH6njKSxkEUaob" + }, + { + "type": "arrow", + "id": "fXUqR5l9-mJxPJpngO-uK" + }, + { + "type": "arrow", + "id": "GST0HrCYDviCg-JDHslU7" + }, + { + "type": "arrow", + "id": "WwJfE-IGw-hz6uAt8MWQ6" + }, + { + "type": "arrow", + "id": "_xS_u95gyqYLnjH6Z_ekf" + } + ], + "updated": 1652788096751, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 1098, + "versionNonce": 664624119, + "isDeleted": false, + "id": "kJiWasVkq7EzcAp_yYMnF", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2137.7099509559657, + "y": 1488.5865456369318, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "width": 51.88250276832002, + "height": 51.88250276832002, + "seed": 891096548, + "groupIds": [ + "YZQku5Chfcs1VbFqtc9Z-", + "gR9JOi18bkX5JTHz6Te_0" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788096752, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 4644, + "versionNonce": 812630777, + "isDeleted": false, + "id": "uygbaH3pY7f71kinGo1KC", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2153.457714886652, + "y": 1498.9945962464901, + "strokeColor": "#000000", + "backgroundColor": "#fff", + "width": 28.10264153823924, + "height": 32.45750960039449, + "seed": 965325156, + "groupIds": [ + "YZQku5Chfcs1VbFqtc9Z-", + "gR9JOi18bkX5JTHz6Te_0" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788096752, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 2.522383435627326, + 0.8876695614221044 + ], + [ + 4.222250533550099, + 2.710891079770457 + ], + [ + 6.53876438289814, + 1.9494891881139838 + ], + [ + 13.00273268947289, + 2.0374391115495714 + ], + [ + 15.018859724261578, + 2.2364052653721083 + ], + [ + 16.578273739956423, + 0.9941438562971401 + ], + [ + 18.744233429245106, + -0.5092823311506232 + ], + [ + 19.3382730064762, + 1.5588921397895759 + ], + [ + 19.283438583962543, + 3.566556278350808 + ], + [ + 21.26661686487244, + 5.46749782452002 + ], + [ + 22.153106695509578, + 10.909367329014938 + ], + [ + 21.45853734367018, + 16.608727547781946 + ], + [ + 19.438802781084537, + 18.8230883820351 + ], + [ + 16.687942584983702, + 20.009258415382803 + ], + [ + 13.71774469882833, + 20.664602961087855 + ], + [ + 14.50370475485716, + 21.40308436092641 + ], + [ + 15.390194585494283, + 23.040424434305827 + ], + [ + 15.572975993873055, + 28.176226896819482 + ], + [ + 15.536419712197318, + 31.28569415677526 + ], + [ + 14.567678247789736, + 31.823854903738514 + ], + [ + 7.240428539404906, + 31.948227269243866 + ], + [ + 6.324236729906197, + 31.05132655929166 + ], + [ + 6.068342758175913, + 27.392989048505843 + ], + [ + 4.377614730672068, + 27.862931757494 + ], + [ + 1.5810591824765403, + 28.041957551394276 + ], + [ + -1.0327149573401755, + 26.542616527479623 + ], + [ + -2.659469491911409, + 23.92436429168835 + ], + [ + -4.240528674387946, + 22.313132146586064 + ], + [ + -5.949534842729662, + 21.179302118551103 + ], + [ + -3.628210956319009, + 21.40308436092641 + ], + [ + -1.9831782809098755, + 22.671183734386556 + ], + [ + -0.11880791544621871, + 24.43906344915162 + ], + [ + 2.147681548450816, + 25.543055844869823 + ], + [ + 4.660925913659192, + 25.22976070554441 + ], + [ + 5.9038394906349865, + 24.252578247172192 + ], + [ + 6.022647406081191, + 22.71594018286161 + ], + [ + 6.689799546663803, + 21.380706136688904 + ], + [ + 7.558011236463052, + 20.776494082275484 + ], + [ + 4.277084956063728, + 19.93358096932851 + ], + [ + 1.1423838023674477, + 18.89984919335192 + ], + [ + -0.4569535209469784, + 16.571430507386058 + ], + [ + -1.6541717458280658, + 10.963038997107194 + ], + [ + -0.7037084222583516, + 5.6861115380996985 + ], + [ + 0.6671521405825978, + 4.240069736001822 + ], + [ + -0.11880791544621871, + 2.2772861946741387 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "text", + "version": 1975, + "versionNonce": 563922199, + "isDeleted": false, + "id": "fdfeFdIFPqv8ob7vcD8RU", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 2131.1512023401265, + "y": 1552.2135661651414, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 65, + "height": 25, + "seed": 1520252004, + "groupIds": [ + "gR9JOi18bkX5JTHz6Te_0" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788096752, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "GitHub", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "GitHub" + } + ], + "id": "K39d22cEulRClkwYv67oo", + "created": 1652788286899, + "name": "GitHub" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 300, + "versionNonce": 421369913, + "isDeleted": false, + "id": "f_JKahZnSxJc4KAt7ZsW-", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1442.384234036135, + "y": 1636.2963678444105, + "strokeColor": "#2b8a3e", + "backgroundColor": "transparent", + "width": 269.51410950501486, + "height": 164.54545632937743, + "seed": 452493049, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 2059, + "versionNonce": 1129440215, + "isDeleted": false, + "id": "LDxnjsBPCY93PCT2EGpUa", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1491.4371102665593, + "y": 1645.2696725568244, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 37, + "height": 25, + "seed": 1032660985, + "groupIds": [ + "SD0C2qiCphyCRHCZftNUU" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "bVaqFXvJckS26nzM_m6zD", + "type": "arrow" + } + ], + "updated": 1652788094454, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "VPC", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "VPC" + }, + { + "type": "rectangle", + "version": 409, + "versionNonce": 1041246489, + "isDeleted": false, + "id": "Td3722QsARfBJtcZHOuFS", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1442.721971221976, + "y": 1636.5078306958162, + "strokeColor": "#2b8a3e", + "backgroundColor": "#40c05788", + "width": 39.6243416500605, + "height": 42.52368372201621, + "seed": 34349401, + "groupIds": [ + "zrX79N-B9zMuBpQmVDD9X", + "SD0C2qiCphyCRHCZftNUU" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 3514, + "versionNonce": 154489079, + "isDeleted": false, + "id": "GPDOaIWIdOnW9UIZfR-hn", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1475.6296262085905, + "y": 1666.1668186165264, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 32.27574323253868, + "height": 18.361713520282862, + "seed": 847652599, + "groupIds": [ + "LYSo7W3gyad40JoNnPoy2", + "zrX79N-B9zMuBpQmVDD9X", + "SD0C2qiCphyCRHCZftNUU" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 3.549984432244912, + -3.8949537333214295 + ], + [ + 2.3636841312957984, + -8.495303540186455 + ], + [ + -2.7289433979513547, + -10.111388238124647 + ], + [ + -3.027690717076837, + -10.609548106174614 + ], + [ + -3.567000789869894, + -12.789455475380407 + ], + [ + -6.056547102486405, + -13.810998302611608 + ], + [ + -8.328438953196809, + -12.373992295078466 + ], + [ + -8.57536552902286, + -12.601775211762344 + ], + [ + -12.065771512081795, + -16.306198129166546 + ], + [ + -17.183143313379162, + -17.9479861601084 + ], + [ + -22.002391699683074, + -15.86580108709353 + ], + [ + -24.284559618469785, + -10.65550025600254 + ], + [ + -24.644339441920692, + -9.79120847277541 + ], + [ + -28.1253575027867, + -7.581853163765089 + ], + [ + -28.72575880029376, + -3.0476054391618987 + ], + [ + -25.849736907374307, + 0.09177212746608632 + ], + [ + -22.43830984753089, + 0.3704975936309154 + ], + [ + -18.24098378130509, + 0.41372736017446243 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "rectangle", + "version": 504, + "versionNonce": 1279106553, + "isDeleted": false, + "id": "S9yi14_dAkxISVpNQePuS", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1458.147217704677, + "y": 1657.4286516397065, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 6.723014307274803, + "height": 6.1627631150019475, + "seed": 58972761, + "groupIds": [ + "qVX3fdAHxGWkYZqsWhS1z", + "LYSo7W3gyad40JoNnPoy2", + "zrX79N-B9zMuBpQmVDD9X", + "SD0C2qiCphyCRHCZftNUU" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 648, + "versionNonce": 1450056215, + "isDeleted": false, + "id": "H3JdgEwAIzhcdcvKskrMI", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1458.3447672988902, + "y": 1657.3456514630645, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 3.735007948486001, + "height": 3.735007948486001, + "seed": 537867865, + "groupIds": [ + "qVX3fdAHxGWkYZqsWhS1z", + "LYSo7W3gyad40JoNnPoy2", + "zrX79N-B9zMuBpQmVDD9X", + "SD0C2qiCphyCRHCZftNUU" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.1660003532660483, + -0.8300017663302128 + ], + [ + 0.5810012364311606, + -2.0750044158255605 + ], + [ + 1.4940031793944009, + -2.9880063587888444 + ], + [ + 2.407005122357648, + -3.652007771852979 + ], + [ + 3.735007948486001, + -3.7350079484860075 + ] + ] + }, + { + "type": "line", + "version": 727, + "versionNonce": 581531353, + "isDeleted": false, + "id": "uBYTxi2Fk94vTgj17Ahqt", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1464.6627315703688, + "y": 1657.3713769309054, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 3.735007948486001, + "height": 3.735007948486001, + "seed": 361767031, + "groupIds": [ + "qVX3fdAHxGWkYZqsWhS1z", + "LYSo7W3gyad40JoNnPoy2", + "zrX79N-B9zMuBpQmVDD9X", + "SD0C2qiCphyCRHCZftNUU" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -0.16600035326605678, + -0.8300017663301986 + ], + [ + -0.5810012364311561, + -2.0750044158255605 + ], + [ + -1.494003179394383, + -2.9880063587888444 + ], + [ + -2.407005122357667, + -3.652007771852979 + ], + [ + -3.7350079484860075, + -3.7350079484860075 + ] + ] + } + ], + "id": "iuZ_6K7EwMGSXXV81NDXm", + "created": 1652788276526, + "name": "VPC" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 344, + "versionNonce": 1341177655, + "isDeleted": false, + "id": "dPEYeIjNa1nluI_vQK-H9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 10, + "angle": 0, + "x": 1753.0347076234943, + "y": 1636.2963678444105, + "strokeColor": "#0091e2", + "backgroundColor": "#0091e2", + "width": 269.51410950501486, + "height": 164.54545632937743, + "seed": 1970567865, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 2254, + "versionNonce": 1944531897, + "isDeleted": false, + "id": "WgyWtsyoLJ6f6YcSKYfC_", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1797.9846599072503, + "y": 1645.2736234765187, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 147, + "height": 25, + "seed": 1311030615, + "groupIds": [ + "jojPcobUJp1FYQLYaB99r" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "bVaqFXvJckS26nzM_m6zD", + "type": "arrow" + } + ], + "updated": 1652788094454, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Private subnet", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Private subnet" + }, + { + "type": "rectangle", + "version": 589, + "versionNonce": 136429655, + "isDeleted": false, + "id": "dpalnXDPuZPWrt4ocUhsM", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1753.269520862667, + "y": 1636.5117816155105, + "strokeColor": "#0091e2", + "backgroundColor": "#4c6ef588", + "width": 39.6243416500605, + "height": 42.52368372201621, + "seed": 563193753, + "groupIds": [ + "z8fpnbqG9D5h5BJpjr8L7", + "jojPcobUJp1FYQLYaB99r" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 461, + "versionNonce": 1411011737, + "isDeleted": false, + "id": "ijAkpwJaRQ3Sm6tTVLxq_", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1762.93827098891, + "y": 1655.1338121646274, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 20.286841397574015, + "height": 16.448790322357453, + "seed": 1976429719, + "groupIds": [ + "-y-OIIssX5nNaTN-ohqqU", + "z8fpnbqG9D5h5BJpjr8L7", + "jojPcobUJp1FYQLYaB99r" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 522, + "versionNonce": 1356176759, + "isDeleted": false, + "id": "CBU6E53PAYyGidh-Cw0-j", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.037253345120129566, + "x": 1767.2625911667262, + "y": 1654.8338126416122, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 12.858316444380746, + "height": 10.880113914476107, + "seed": 332567095, + "groupIds": [ + "-y-OIIssX5nNaTN-ohqqU", + "z8fpnbqG9D5h5BJpjr8L7", + "jojPcobUJp1FYQLYaB99r" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -0.329700421650787, + -3.9564050598095566 + ], + [ + 1.3188016866031462, + -8.90191138457145 + ], + [ + 6.2643080113649745, + -10.880113914476107 + ], + [ + 10.550413492825204, + -8.90191138457145 + ], + [ + 12.198915601079173, + -4.945506324761892 + ], + [ + 12.52861602272996, + -0.3297004216509265 + ] + ] + }, + { + "type": "ellipse", + "version": 442, + "versionNonce": 116808057, + "isDeleted": false, + "id": "nc52cTahwemUriesKHteX", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1770.3931057805657, + "y": 1659.2831289420449, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 5.275206746412619, + "height": 5.275206746412619, + "seed": 727656153, + "groupIds": [ + "-y-OIIssX5nNaTN-ohqqU", + "z8fpnbqG9D5h5BJpjr8L7", + "jojPcobUJp1FYQLYaB99r" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 420, + "versionNonce": 1297143447, + "isDeleted": false, + "id": "le0EGJ_WCnfaY7p2IsMOZ", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1773.0339879374565, + "y": 1664.7248252911374, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 0, + "height": 4.945506324761884, + "seed": 888331287, + "groupIds": [ + "-y-OIIssX5nNaTN-ohqqU", + "z8fpnbqG9D5h5BJpjr8L7", + "jojPcobUJp1FYQLYaB99r" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 4.945506324761884 + ] + ] + } + ], + "id": "tFo9OYIShjpGSvTwOAS2f", + "created": 1652788267525, + "name": "Private subnet" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 432, + "versionNonce": 994005687, + "isDeleted": false, + "id": "nN_FFSvlJJflT8rpZVgCX", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 10, + "angle": 0, + "x": 2068.196153089959, + "y": 1636.2963678444105, + "strokeColor": "#2b8a3e", + "backgroundColor": "#2b8a3e", + "width": 269.51410950501486, + "height": 164.54545632937743, + "seed": 2043392407, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 2349, + "versionNonce": 1806513719, + "isDeleted": false, + "id": "tc4qHpKR_vXNvf3-Pf1R4", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 2114.361939143105, + "y": 1645.9635823398605, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 129, + "height": 25, + "seed": 807566071, + "groupIds": [ + "PV8Gn0PijRZzjX2hMKwgp" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "bVaqFXvJckS26nzM_m6zD", + "type": "arrow" + } + ], + "updated": 1652788239861, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Public subnet", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Public subnet" + }, + { + "type": "rectangle", + "version": 665, + "versionNonce": 1145564087, + "isDeleted": false, + "id": "zmrROgE716GaUWwB116de", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2078.0676224585577, + "y": 1655.823771027969, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 20.286841397574015, + "height": 16.448790322357453, + "seed": 975569943, + "groupIds": [ + "xCQT5sKyDemXqnqKwHTF9", + "6tYLHZ6RYpPuaKiv1UfQv", + "PV8Gn0PijRZzjX2hMKwgp" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 726, + "versionNonce": 1967591225, + "isDeleted": false, + "id": "08gY5fDDsjcoAZHTdjq8H", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.037253345120129566, + "x": 2082.3919426363746, + "y": 1655.5237715049534, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 12.858316444380746, + "height": 10.880113914476107, + "seed": 1715285209, + "groupIds": [ + "xCQT5sKyDemXqnqKwHTF9", + "6tYLHZ6RYpPuaKiv1UfQv", + "PV8Gn0PijRZzjX2hMKwgp" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -0.329700421650787, + -3.9564050598095566 + ], + [ + 1.3188016866031462, + -8.90191138457145 + ], + [ + 6.2643080113649745, + -10.880113914476107 + ], + [ + 10.550413492825204, + -8.90191138457145 + ], + [ + 12.198915601079173, + -4.945506324761892 + ], + [ + 12.52861602272996, + -0.3297004216509265 + ] + ] + }, + { + "type": "ellipse", + "version": 646, + "versionNonce": 836721879, + "isDeleted": false, + "id": "NK5hWvP-BHfOMXbzjWyvQ", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2085.522457250214, + "y": 1659.9730878053863, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 5.275206746412619, + "height": 5.275206746412619, + "seed": 152791351, + "groupIds": [ + "xCQT5sKyDemXqnqKwHTF9", + "6tYLHZ6RYpPuaKiv1UfQv", + "PV8Gn0PijRZzjX2hMKwgp" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 624, + "versionNonce": 1676853273, + "isDeleted": false, + "id": "PO7G54ml-sCiqRZGCGtPE", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2088.163339407105, + "y": 1665.4147841544793, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 0, + "height": 4.945506324761884, + "seed": 932456889, + "groupIds": [ + "xCQT5sKyDemXqnqKwHTF9", + "6tYLHZ6RYpPuaKiv1UfQv", + "PV8Gn0PijRZzjX2hMKwgp" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 4.945506324761884 + ] + ] + }, + { + "type": "rectangle", + "version": 596, + "versionNonce": 430680567, + "isDeleted": false, + "id": "YwX1Ni6wuwrdk5CtH0XKI", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2068.3988723323146, + "y": 1637.2017404788526, + "strokeColor": "#2b8a3e", + "backgroundColor": "#40c05788", + "width": 39.6243416500605, + "height": 42.52368372201621, + "seed": 962056087, + "groupIds": [ + "6tYLHZ6RYpPuaKiv1UfQv", + "PV8Gn0PijRZzjX2hMKwgp" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788094454, + "link": null, + "locked": false + } + ], + "id": "RMcbGqneckVAijgb0ksgq", + "created": 1652788258854, + "name": "Public subnet" + }, + { + "status": "published", + "elements": [ + { + "type": "text", + "version": 884, + "versionNonce": 1252295383, + "isDeleted": false, + "id": "QJfRdycBHBpPbSJ2qkaHR", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1482.749129546251, + "y": 2105.0869523931538, + "strokeColor": "#000000", + "backgroundColor": "white", + "width": 46, + "height": 25, + "seed": 14301209, + "groupIds": [ + "Viu2YdNB2na1uSBiKAFRr" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "User", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "User" + }, + { + "type": "line", + "version": 1193, + "versionNonce": 33225241, + "isDeleted": false, + "id": "ossF4GzkYCgGsHklL1hl0", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1479.9619088977436, + "y": 2095.434026383175, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 49.26942071813747, + "height": 43.87300421060919, + "seed": 506339289, + "groupIds": [ + "1qNPPmx9UHXcelOK9AVB0", + "Viu2YdNB2na1uSBiKAFRr" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 5.518175120431392, + -29.072472669680792 + ], + [ + 23.649321944705985, + -43.87300421060919 + ], + [ + 41.780468768980576, + -32.244015142736835 + ], + [ + 49.26942071813747, + -3.0188078795298896 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "ellipse", + "version": 930, + "versionNonce": 1792806903, + "isDeleted": false, + "id": "2zyw5TfId7xzj1osFRw3u", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1492.4157797340592, + "y": 2028.7429785973186, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 25.225943407686408, + "height": 22.072700481725683, + "seed": 19327833, + "groupIds": [ + "1qNPPmx9UHXcelOK9AVB0", + "Viu2YdNB2na1uSBiKAFRr" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false + } + ], + "id": "UFgHjkyx6W38-mAJ-BNVT", + "created": 1652788139616, + "name": "User" + }, + { + "status": "published", + "elements": [ + { + "type": "text", + "version": 917, + "versionNonce": 1604126201, + "isDeleted": false, + "id": "SPeAjZMYCx8EoDI65dPCE", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1648.6634829780694, + "y": 2107.713080534573, + "strokeColor": "#000000", + "backgroundColor": "white", + "width": 57, + "height": 25, + "seed": 769940439, + "groupIds": [ + "3Au8BUSkq1LVGtylUmgjQ" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Users", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Users" + }, + { + "type": "ellipse", + "version": 1846, + "versionNonce": 957952535, + "isDeleted": false, + "id": "NmoZVFoqQX1Vu5MZKFy1V", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1689.1062999172887, + "y": 2040.453237200236, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 19.75034363276957, + "height": 17.281550678673437, + "seed": 1178681657, + "groupIds": [ + "wDTsFXl-vMcxnOc1Vhb-n", + "2Je6Qe5C2ipP25gzhXag3", + "3Au8BUSkq1LVGtylUmgjQ" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 1890, + "versionNonce": 1708908249, + "isDeleted": false, + "id": "h6PU4x9jiv-rRAUghe8Ai", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1645.9226257929668, + "y": 2041.03630498387, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 19.75034363276957, + "height": 17.281550678673437, + "seed": 987148825, + "groupIds": [ + "wDTsFXl-vMcxnOc1Vhb-n", + "2Je6Qe5C2ipP25gzhXag3", + "3Au8BUSkq1LVGtylUmgjQ" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 2073, + "versionNonce": 1568200503, + "isDeleted": false, + "id": "JAt8qoX76olLN6uxixJt7", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1656.3744458002311, + "y": 2099.9673407215196, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 41.80600213253467, + "height": 37.22704429756421, + "seed": 1632306937, + "groupIds": [ + "wDTsFXl-vMcxnOc1Vhb-n", + "2Je6Qe5C2ipP25gzhXag3", + "3Au8BUSkq1LVGtylUmgjQ" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 4.682272238843876, + -24.66852332971124 + ], + [ + 20.06688102361667, + -37.22704429756421 + ], + [ + 35.45148980838941, + -27.359634965679646 + ], + [ + 41.80600213253467, + -2.561513547547756 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "ellipse", + "version": 1784, + "versionNonce": 1955371961, + "isDeleted": false, + "id": "q84Lz9t5dvdU-A-7bgTEI", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1666.860567951041, + "y": 2043.3928294016825, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 21.404673091857735, + "height": 18.729088955375612, + "seed": 1679933401, + "groupIds": [ + "wDTsFXl-vMcxnOc1Vhb-n", + "2Je6Qe5C2ipP25gzhXag3", + "3Au8BUSkq1LVGtylUmgjQ" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 1186, + "versionNonce": 1558632535, + "isDeleted": false, + "id": "Vg-rpWJ3bCmlGFwLkz2RJ", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1685.525781254682, + "y": 2066.2982760364666, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 33.437317203044365, + "height": 37.41795020340682, + "seed": 808629655, + "groupIds": [ + "2Je6Qe5C2ipP25gzhXag3", + "3Au8BUSkq1LVGtylUmgjQ" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 5.174822900471103, + -5.57288620050744 + ], + [ + 9.553519200869804, + -8.3593293007612 + ], + [ + 17.116721901558428, + -8.757392600797402 + ], + [ + 22.291544802029577, + -7.563202700688624 + ], + [ + 25.874114502355752, + -3.5825697003262533 + ], + [ + 28.262494302573206, + 1.5922532001448921 + ], + [ + 30.650874102790677, + 7.961266000724821 + ], + [ + 32.64119060297189, + 14.33027880130475 + ], + [ + 33.437317203044365, + 19.50510170177589 + ], + [ + 32.64119060297189, + 24.67992460224705 + ], + [ + 29.45668420268195, + 27.068304402464427 + ], + [ + 25.077987902283255, + 28.26249430257321 + ], + [ + 18.708975101703373, + 28.660557602609412 + ], + [ + 14.728342101340951, + 27.86443100253699 + ], + [ + 14.330278801304704, + 22.291544802029662 + ], + [ + 12.738025601159729, + 18.70897510170331 + ], + [ + 9.951582500906056, + 13.136088901195967 + ], + [ + 5.970949500543595, + 5.572886200507347 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 1481, + "versionNonce": 1808972953, + "isDeleted": false, + "id": "mPuYUIgb4Y_xezXZi9asr", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1667.606994801348, + "y": 2066.286400133062, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 32.24312730293567, + "height": 39.01020340355187, + "seed": 1685387257, + "groupIds": [ + "2Je6Qe5C2ipP25gzhXag3", + "3Au8BUSkq1LVGtylUmgjQ" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -3.980633000362431, + -4.378696300398655 + ], + [ + -8.359329300761118, + -7.165139400652425 + ], + [ + -15.922532001449774, + -7.56320270068861 + ], + [ + -21.097354901920916, + -6.369012800579835 + ], + [ + -24.679924602247063, + -2.388379800217473 + ], + [ + -27.06830440246457, + 2.7864431002536705 + ], + [ + -29.45668420268202, + 9.155455900833594 + ], + [ + -31.4470007028632, + 15.524468701413529 + ], + [ + -32.24312730293567, + 20.699291601884667 + ], + [ + -31.4470007028632, + 25.87411450235582 + ], + [ + -28.262494302573295, + 28.262494302573188 + ], + [ + -24.281861302210807, + 30.25281080275446 + ], + [ + -17.912848501630954, + 31.44700070286316 + ], + [ + -11.145772401014788, + 31.447000702863257 + ], + [ + -10.349645800942298, + 24.281861302210825 + ], + [ + -9.553519200869822, + 20.699291601884582 + ], + [ + -8.359329300761118, + 14.728342101341028 + ], + [ + -4.7767596004349215, + 6.767076100616125 + ], + [ + 0, + 0 + ] + ] + } + ], + "id": "2WIQDCyDP0rYd2UMhMFNE", + "created": 1652788132269, + "name": "Users" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 1797, + "versionNonce": 1285967609, + "isDeleted": false, + "id": "XY68X70pWBB_KTuFENlCA", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1872.6978641000337, + "y": 2023.9314163933077, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "width": 44.89096291762419, + "height": 68.65473186014601, + "seed": 998136375, + "groupIds": [ + "Due5tD6SVG60wFltRvqxd", + "TDLQl-R1wAzxPa-KZXh88" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1959, + "versionNonce": 1927608599, + "isDeleted": false, + "id": "IXe55vQDHa7ParMVN5EnJ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1877.8373858616685, + "y": 2029.1656926777612, + "strokeColor": "#000000", + "backgroundColor": "#fff", + "width": 35.37268888043181, + "height": 52.508196731676236, + "seed": 88341335, + "groupIds": [ + "Due5tD6SVG60wFltRvqxd", + "TDLQl-R1wAzxPa-KZXh88" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "Q3sXfBVz75bTl5TIhEB0s", + "type": "arrow" + } + ], + "updated": 1652788090905, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 1995, + "versionNonce": 1423181785, + "isDeleted": false, + "id": "KRpxkVGxhpGc891OdiqcB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1891.9016475570904, + "y": 2083.9210188706206, + "strokeColor": "#000000", + "backgroundColor": "#fff", + "width": 7.24416548958712, + "height": 7.24416548958712, + "seed": 66615415, + "groupIds": [ + "Due5tD6SVG60wFltRvqxd", + "TDLQl-R1wAzxPa-KZXh88" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1648, + "versionNonce": 406044215, + "isDeleted": false, + "id": "i9cMOQK3OV6uebTtpDAVC", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1883.0859307662918, + "y": 2035.8496961999592, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef588", + "width": 24.875599071184233, + "height": 12.608047084964703, + "seed": 1835028887, + "groupIds": [ + "Due5tD6SVG60wFltRvqxd", + "TDLQl-R1wAzxPa-KZXh88" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1692, + "versionNonce": 77046969, + "isDeleted": false, + "id": "WLPcX4j7cDcAXBAGiH37S", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1883.0859307662918, + "y": 2058.3899714141685, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef588", + "width": 24.875599071184233, + "height": 12.608047084964703, + "seed": 1253050039, + "groupIds": [ + "Due5tD6SVG60wFltRvqxd", + "TDLQl-R1wAzxPa-KZXh88" + ], + "strokeSharpness": "sharp", + "boundElements": [ + { + "id": "Q3sXfBVz75bTl5TIhEB0s", + "type": "arrow" + } + ], + "updated": 1652788090905, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1164, + "versionNonce": 1840045911, + "isDeleted": false, + "id": "otYDQpWPH4kWxm8LQuC1i", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1863.6433455588458, + "y": 2100.8780236517937, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 63, + "height": 25, + "seed": 227676793, + "groupIds": [ + "TDLQl-R1wAzxPa-KZXh88" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Device", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Device" + } + ], + "id": "9_9pebw1kmiJPt-x9timk", + "created": 1652788125156, + "name": "Device" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 750, + "versionNonce": 1637405529, + "isDeleted": false, + "id": "7Pk-TLxXtuWZkXpaK8tS4", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2067.820080211402, + "y": 2024.956526167033, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 37.26835999695436, + "height": 69.56760532764793, + "seed": 372949593, + "groupIds": [ + "76AB9cfQxi8EevCVG_nyp" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1296, + "versionNonce": 1066625719, + "isDeleted": false, + "id": "NiaPt1_volLg4xDhmhVPw", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 2054.9542602098786, + "y": 2104.416032826593, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 63, + "height": 25, + "seed": 81738841, + "groupIds": [ + "76AB9cfQxi8EevCVG_nyp" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Server", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Server" + }, + { + "type": "line", + "version": 595, + "versionNonce": 1169621049, + "isDeleted": false, + "id": "GDASN8Ar7kqbTnHNqRkxb", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2072.3046375445324, + "y": 2032.6524768329887, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 28.572409330998198, + "height": 2.2737367544323206e-13, + "seed": 627417527, + "groupIds": [ + "76AB9cfQxi8EevCVG_nyp" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 28.572409330998198, + 2.2737367544323206e-13 + ] + ] + }, + { + "type": "line", + "version": 627, + "versionNonce": 1432550359, + "isDeleted": false, + "id": "BBQgQPZgkKsxQ6zNRzIUr", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2072.1680555443795, + "y": 2039.056274610597, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 28.572409330998198, + "height": 2.2737367544323206e-13, + "seed": 204892599, + "groupIds": [ + "76AB9cfQxi8EevCVG_nyp" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 28.572409330998198, + 2.2737367544323206e-13 + ] + ] + }, + { + "type": "line", + "version": 609, + "versionNonce": 1664913689, + "isDeleted": false, + "id": "Usz5re68bOeeSKgyCLVmq", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2072.1371702106403, + "y": 2045.4600723882063, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 28.572409330998198, + "height": 2.2737367544323206e-13, + "seed": 987848505, + "groupIds": [ + "76AB9cfQxi8EevCVG_nyp" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 28.572409330998198, + 2.2737367544323206e-13 + ] + ] + }, + { + "type": "line", + "version": 613, + "versionNonce": 1768122615, + "isDeleted": false, + "id": "BbKD57FKlc-NgH63VqGFw", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2072.1371702106403, + "y": 2051.863870165815, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 28.572409330998198, + "height": 2.2737367544323206e-13, + "seed": 769157465, + "groupIds": [ + "76AB9cfQxi8EevCVG_nyp" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 28.572409330998198, + 2.2737367544323206e-13 + ] + ] + } + ], + "id": "RK7rWRm_bqsgMR-yyEact", + "created": 1652788117720, + "name": "Server" + }, + { + "status": "published", + "elements": [ + { + "type": "text", + "version": 937, + "versionNonce": 932488601, + "isDeleted": false, + "id": "BR44CilwTIbONCvGKgLhq", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 2276.2302177969473, + "y": 2086.920421581179, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "width": 50, + "height": 25, + "seed": 2109994009, + "groupIds": [ + "z8UUgMUPNsq5axJkMXtT8" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Email", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Email" + }, + { + "type": "rectangle", + "version": 999, + "versionNonce": 899996791, + "isDeleted": false, + "id": "UeFyWyJ1YbSMu7K0pkCpY", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2269.9867164774682, + "y": 2035.3562941088205, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "width": 62.48700263895784, + "height": 41.463338199682276, + "seed": 1690575833, + "groupIds": [ + "UfI5AJ2N6H-8qqUQeyy3l", + "z8UUgMUPNsq5axJkMXtT8" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 826, + "versionNonce": 150899321, + "isDeleted": false, + "id": "e-Z3aRG8x2-hd5cKdX2t7", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2270.8896528022965, + "y": 2039.198838061454, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "width": 30.732287023896305, + "height": 23.925868693893968, + "seed": 1478770873, + "groupIds": [ + "UfI5AJ2N6H-8qqUQeyy3l", + "z8UUgMUPNsq5axJkMXtT8" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 30.732287023896305, + 23.925868693893968 + ] + ] + }, + { + "type": "line", + "version": 912, + "versionNonce": 1944073623, + "isDeleted": false, + "id": "au4tcwg8T7lC7un-8nooJ", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2302.757305018425, + "y": 2064.267967494489, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "width": 29.30307776200009, + "height": 29.23224875584021, + "seed": 657310105, + "groupIds": [ + "UfI5AJ2N6H-8qqUQeyy3l", + "z8UUgMUPNsq5axJkMXtT8" + ], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652788090905, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 29.30307776200009, + -29.23224875584021 + ] + ] + } + ], + "id": "yjClh1WmDx5TUhWugAREM", + "created": 1652788110401, + "name": "Email" + } + ] +} diff --git a/apps/web/public/excalidraw/libraries/flow-chart-symbols.excalidrawlib b/apps/web/public/excalidraw/libraries/flow-chart-symbols.excalidrawlib new file mode 100644 index 00000000..0acdcbd6 --- /dev/null +++ b/apps/web/public/excalidraw/libraries/flow-chart-symbols.excalidrawlib @@ -0,0 +1,2150 @@ +{ + "type": "excalidrawlib", + "version": 2, + "source": "https://app.excalidraw.com", + "libraryItems": [ + { + "status": "published", + "elements": [ + { + "id": "7lhy5sLQPzScfW-6lfBPd", + "type": "line", + "x": 1378.335226108522, + "y": 583.3618943263135, + "width": 102.95404895253068, + "height": 98.19215468747154, + "angle": 0, + "strokeColor": "#c2255c", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 80, + "groupIds": [ + "z7Qf9wPKTcV9QOIwh5C8u" + ], + "frameId": null, + "index": "ay", + "roundness": null, + "seed": 407059785, + "version": 864, + "versionNonce": 1017959239, + "isDeleted": false, + "boundElements": null, + "updated": 1717805906559, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.3759156430037365, + 60.37295808721399 + ], + [ + 54.58954101281756, + 98.19215468747154 + ], + [ + 102.8267215907565, + 60.73117425017426 + ], + [ + 102.95404895253068, + 0.7385631877812102 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + 6.087115954179808, + -0.5751962248369864 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "LaNgWQrBqLmN39HuAoljg", + "type": "text", + "x": 1389.2954603618482, + "y": 599.4595828483938, + "width": 78.62739167691734, + "height": 41.557855548965705, + "angle": 0, + "strokeColor": "#c2255c", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 80, + "groupIds": [ + "z7Qf9wPKTcV9QOIwh5C8u" + ], + "frameId": null, + "index": "az", + "roundness": null, + "seed": 404390247, + "version": 267, + "versionNonce": 1516294759, + "isDeleted": false, + "boundElements": null, + "updated": 1717805906559, + "link": null, + "locked": false, + "text": "Off-page\nConnector", + "fontSize": 16.623142219586274, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Off-page\nConnector", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "zB7s47_c8YdombAHNxSYv", + "created": 1717805908177, + "name": "Off-page Connector" + }, + { + "status": "published", + "elements": [ + { + "type": "ellipse", + "version": 591, + "versionNonce": 1421588297, + "index": "aU", + "isDeleted": false, + "id": "2Th0AKnbRgX9raD_PTxfu", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 70, + "angle": 0, + "x": 1130.4422480585324, + "y": 572.0977341520572, + "strokeColor": "#c2255c", + "backgroundColor": "transparent", + "width": 127.3416397353642, + "height": 119.42128278810515, + "seed": 1122082343, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "s8ubTI_Ir7Aj5dRqJaXAW" + } + ], + "updated": 1717805270630, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 221, + "versionNonce": 1823991337, + "index": "aV", + "isDeleted": false, + "id": "s8ubTI_Ir7Aj5dRqJaXAW", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 70, + "angle": 0, + "x": 1168.1110265947138, + "y": 606.5865761073771, + "strokeColor": "#c2255c", + "backgroundColor": "#e9ecef", + "width": 51.95994567871094, + "height": 50, + "seed": 2113390601, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717805270630, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Conn-\nector", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "2Th0AKnbRgX9raD_PTxfu", + "originalText": "Conn-\nector", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "l1p_mCp6TTfiDjPwoaFVb", + "created": 1717805905850, + "name": "Connector" + }, + { + "status": "published", + "elements": [ + { + "id": "_mgqBsosbDKsaJPnV6GUm", + "type": "line", + "x": 859.977602555591, + "y": 576.6007020879847, + "width": 189.69431320997705, + "height": 92.14981705069717, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 80, + "groupIds": [ + "oPEowtQlLQ7W75BeCNWVA" + ], + "frameId": null, + "index": "b02", + "roundness": { + "type": 2 + }, + "seed": 328260681, + "version": 3351, + "versionNonce": 8291625, + "isDeleted": false, + "boundElements": null, + "updated": 1717805902145, + "link": null, + "locked": false, + "points": [ + [ + -0.4146249824936916, + -0.2553866241213836 + ], + [ + 71.9452762472197, + -2.7525736277341424 + ], + [ + 139.2031290073635, + -0.6084149661238962 + ], + [ + 172.18904871522625, + 16.492317293055287 + ], + [ + 182.21459657555823, + 42.44909898776862 + ], + [ + 171.9221877693061, + 69.91639259900991 + ], + [ + 140.95789965612278, + 86.00019617494236 + ], + [ + 75.1616541535643, + 89.39724342296302 + ], + [ + -0.3128306307790344, + 87.31240126458516 + ], + [ + -6.345759049713115, + 78.78690165941359 + ], + [ + -7.4797166344188515, + 40.74704621264733 + ], + [ + -6.438257100429156, + 4.4733548714151 + ], + [ + -0.4146249824936916, + -0.2553866241213836 + ] + ], + "lastCommittedPoint": [ + -3.916043340218039, + -1.0156321879867392 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "sfg_Xl9_3KcBVN47Kgn2q", + "type": "text", + "x": 909.6639788403473, + "y": 608.3927966420889, + "width": 54.51995849609375, + "height": 25, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 80, + "groupIds": [ + "oPEowtQlLQ7W75BeCNWVA" + ], + "frameId": null, + "index": "b03", + "roundness": null, + "seed": 1913210377, + "version": 62, + "versionNonce": 379975689, + "isDeleted": false, + "boundElements": null, + "updated": 1717805902145, + "link": null, + "locked": false, + "text": "Delay", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Delay", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "2ubxnY8d_WiH_BL5_tMbc", + "created": 1717805903780, + "name": "Delay" + }, + { + "status": "published", + "elements": [ + { + "id": "LY-AGR6tKvOVZsu08SLJA", + "type": "line", + "x": 618.729185415738, + "y": 574.4141895154723, + "width": 189.17322239287762, + "height": 88.20047222171354, + "angle": 0, + "strokeColor": "#c2255c", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6vM38QQLsTDACmn_9ejck" + ], + "frameId": null, + "index": "b00", + "roundness": null, + "seed": 54329543, + "version": 679, + "versionNonce": 1472253961, + "isDeleted": false, + "boundElements": null, + "updated": 1717805402857, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 126.05329447414965, + 0.5620759022698394 + ], + [ + 157.6228256829138, + 45.45400190100463 + ], + [ + 123.55145875596122, + 88.16937866116234 + ], + [ + 1.6025142745565972, + 88.20047222171354 + ], + [ + -31.55039670996382, + 43.4185695910827 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + -2.332017041332392, + 3.4131162235705688 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ApXvjsobKnjz1PeaH9fbq", + "type": "text", + "x": 615.0594787913324, + "y": 597.0024653496696, + "width": 133.99989318847656, + "height": 50, + "angle": 0, + "strokeColor": "#c2255c", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "6vM38QQLsTDACmn_9ejck" + ], + "frameId": null, + "index": "b01", + "roundness": null, + "seed": 1111487687, + "version": 127, + "versionNonce": 689694441, + "isDeleted": false, + "boundElements": null, + "updated": 1717805402857, + "link": null, + "locked": false, + "text": "Preparation /\nInitialization", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Preparation /\nInitialization", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "D0V6_gzucPNKaYo0rlADU", + "created": 1717805901232, + "name": "Preparation / Initialization" + }, + { + "status": "published", + "elements": [ + { + "id": "PcPdOASZmABbn4P_js_u1", + "type": "line", + "x": 325.8174914447077, + "y": 575.5990708861827, + "width": 186.07918384898863, + "height": 137.1689186538997, + "angle": 0, + "strokeColor": "#846358", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "pq1vHdsYRVS64hUBstKRe" + ], + "frameId": null, + "index": "av", + "roundness": { + "type": 2 + }, + "seed": 830482247, + "version": 5980, + "versionNonce": 1329646151, + "isDeleted": false, + "boundElements": null, + "updated": 1717805897881, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.4137231248057631, + 80.70410765856475 + ], + [ + 3.537687343585804, + 89.49067391476437 + ], + [ + 34.212999425215436, + 100.23859036851506 + ], + [ + 79.32037154121512, + 105.32451869855666 + ], + [ + 133.28864837634546, + 102.45862074388079 + ], + [ + 169.41599316026085, + 90.50205816741571 + ], + [ + 185.22460029580316, + 77.58061102964352 + ], + [ + 186.07918384898863, + 71.4655388066998 + ], + [ + 182.7938970961902, + 1.1510503641975167 + ], + [ + 181.65322356746285, + -8.904186819871667 + ], + [ + 160.80284252697302, + -21.48425765324725 + ], + [ + 124.1751653366708, + -31.844399955343047 + ], + [ + 66.85648538745887, + -30.430901039527544 + ], + [ + 25.802679767707165, + -19.23584450747441 + ], + [ + 0.990108737970345, + -3.480782822171966 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + 165.27542937901814, + 5.7908338673858 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "XeHrvd1hySngOROXOqsws", + "type": "line", + "x": 329.4014018301715, + "y": 573.7817555724162, + "width": 176.5728110046411, + "height": 20.583490956468065, + "angle": 0, + "strokeColor": "#846358", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "pq1vHdsYRVS64hUBstKRe" + ], + "frameId": null, + "index": "aw", + "roundness": { + "type": 2 + }, + "seed": 758439497, + "version": 1098, + "versionNonce": 90040679, + "isDeleted": false, + "boundElements": null, + "updated": 1717805897881, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 41.77495465263375, + 13.540818396300015 + ], + [ + 72.60534601315386, + 17.609537309811437 + ], + [ + 120.04879176491201, + 17.660943678863863 + ], + [ + 151.77978451501266, + 8.879974268889638 + ], + [ + 176.5728110046411, + -2.9225472776042034 + ] + ], + "lastCommittedPoint": [ + 168.21214375070542, + -2.869206603446287 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "k_kAmvLrH2am0NW-1IwXH", + "type": "text", + "x": 365.69673515659207, + "y": 619.7601563630714, + "width": 98.87992858886719, + "height": 25, + "angle": 0, + "strokeColor": "#846358", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "pq1vHdsYRVS64hUBstKRe" + ], + "frameId": null, + "index": "ax", + "roundness": null, + "seed": 1785268391, + "version": 148, + "versionNonce": 885662855, + "isDeleted": false, + "boundElements": null, + "updated": 1717805897881, + "link": null, + "locked": false, + "text": "Database", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Database", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "uaLlMcw6E6ejv52GWDpoo", + "created": 1717805899229, + "name": "Database" + }, + { + "status": "published", + "elements": [ + { + "type": "ellipse", + "version": 500, + "versionNonce": 958375049, + "index": "aa", + "isDeleted": false, + "id": "vwFOAbBg_p9rIPsYtAkX0", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 70, + "angle": 0, + "x": 1361.1874136976912, + "y": 362.3681099059932, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "width": 127.93451698256467, + "height": 120.6590204032604, + "seed": 1823493767, + "groupIds": [ + "_1-JhdtfXiVOUwayr5yQq" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "id": "6S3Ew6Wg_BCchkcublazQ", + "type": "arrow" + }, + { + "id": "wruXLRErwk37BqH1JAtDs", + "type": "arrow" + } + ], + "updated": 1717805209495, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 780, + "versionNonce": 1415328583, + "index": "ab", + "isDeleted": false, + "id": "6S3Ew6Wg_BCchkcublazQ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 70, + "angle": 0, + "x": 1424.091075703831, + "y": 361.5205435240584, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "width": 4.640094860475074, + "height": 121.27151954645547, + "seed": 807862633, + "groupIds": [ + "_1-JhdtfXiVOUwayr5yQq" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": null, + "updated": 1717805209593, + "link": null, + "locked": false, + "startBinding": { + "elementId": "vwFOAbBg_p9rIPsYtAkX0", + "focus": -0.019952896255160824, + "gap": 1 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 4.640094860475074, + 121.27151954645547 + ] + ] + }, + { + "type": "line", + "version": 836, + "versionNonce": 86258279, + "index": "ac", + "isDeleted": false, + "id": "wruXLRErwk37BqH1JAtDs", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 70, + "angle": 0, + "x": 1490.025780767241, + "y": 423.8928211383988, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "width": 127.36505831970203, + "height": 1.9484094365963074, + "seed": 181593511, + "groupIds": [ + "_1-JhdtfXiVOUwayr5yQq" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": null, + "updated": 1717805209593, + "link": null, + "locked": false, + "startBinding": { + "elementId": "vwFOAbBg_p9rIPsYtAkX0", + "focus": -0.03625590054573358, + "gap": 1 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -127.36505831970203, + 1.9484094365963074 + ] + ] + }, + { + "id": "_6y_RrnVDdjKXSGaCm1Mr", + "type": "text", + "x": 1387.708106915793, + "y": 440.34587244709144, + "width": 28.079986572265625, + "height": 25, + "angle": 0, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "_1-JhdtfXiVOUwayr5yQq" + ], + "frameId": null, + "index": "ad", + "roundness": null, + "seed": 1819601993, + "version": 481, + "versionNonce": 96120553, + "isDeleted": false, + "boundElements": [], + "updated": 1717805209495, + "link": null, + "locked": false, + "text": "OR", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "OR", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "n3Fuxs_iH1zKmej-e7dtU", + "created": 1717805896882, + "name": "Or" + }, + { + "status": "published", + "elements": [ + { + "type": "ellipse", + "version": 281, + "versionNonce": 1055977737, + "index": "aW", + "isDeleted": false, + "id": "3S1O6CX7i9203WEuE40eq", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 70, + "angle": 0, + "x": 1112.9146365430734, + "y": 358.98774814889, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "width": 127.93451698256467, + "height": 120.6590204032604, + "seed": 25739399, + "groupIds": [ + "J7gGWiN0naYRqxU2D4LML" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1717805209495, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 202, + "versionNonce": 1470097385, + "index": "aX", + "isDeleted": false, + "id": "4_a6mJb9eCANog7sR_6ut", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 70, + "angle": 0, + "x": 1128.9869451411314, + "y": 380.7745082126496, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "width": 94.15767233998804, + "height": 78.09364075737852, + "seed": 2147450057, + "groupIds": [ + "J7gGWiN0naYRqxU2D4LML" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1717805209495, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 94.15767233998804, + 78.09364075737852 + ] + ] + }, + { + "type": "line", + "version": 166, + "versionNonce": 822936265, + "index": "aY", + "isDeleted": false, + "id": "z3NRRR-pDqZfCl_gMpDHG", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 70, + "angle": 0, + "x": 1220.22117562468, + "y": 377.402452118897, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "width": 85.4618399097069, + "height": 82.27849976818197, + "seed": 1424059111, + "groupIds": [ + "J7gGWiN0naYRqxU2D4LML" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1717805209495, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -85.4618399097069, + 82.27849976818197 + ] + ] + }, + { + "id": "LF0MxIcu9F1BvLad-RRp0", + "type": "text", + "x": 1156.5032073277193, + "y": 441.07091035248527, + "width": 41.5999755859375, + "height": 25, + "angle": 0, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "J7gGWiN0naYRqxU2D4LML" + ], + "frameId": null, + "index": "aZ", + "roundness": null, + "seed": 826740135, + "version": 198, + "versionNonce": 1473495465, + "isDeleted": false, + "boundElements": null, + "updated": 1717805209495, + "link": null, + "locked": false, + "text": "AND", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "AND", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "0sp_GmpRbdtlu1GZ7omMt", + "created": 1717805894879, + "name": "And" + }, + { + "status": "published", + "elements": [ + { + "type": "line", + "version": 2783, + "versionNonce": 1094955655, + "index": "aC", + "isDeleted": false, + "id": "c6tVey2vizAZfIpTpmyrn", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 863.8821959879904, + "y": 377.72227681403365, + "strokeColor": "#0c8599", + "backgroundColor": "transparent", + "width": 177.38720624737226, + "height": 68.61500430474813, + "seed": 298721961, + "groupIds": [ + "Swcr2QLDws17ylZpGabM-" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717805697298, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 141.1903133856958, + -0.8581778358546758 + ], + [ + 111.65315195251631, + 67.5555367658238 + ], + [ + -36.196892861676474, + 67.75682646889345 + ], + [ + -4.112523736907434, + 0.03207924491035474 + ] + ] + }, + { + "type": "text", + "version": 958, + "versionNonce": 736877929, + "index": "aD", + "isDeleted": false, + "id": "Osu8WLuVtjgbp_R-tvtGT", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 878.1033600594241, + "y": 385.57421875, + "strokeColor": "#0c8599", + "backgroundColor": "#eaddd7", + "width": 72.75994873046875, + "height": 50, + "seed": 1730536841, + "groupIds": [ + "Swcr2QLDws17ylZpGabM-" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717805697298, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Input /\nOutput", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Input /\nOutput", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "RQ9LMOAx9SsgXwRdUWp3m", + "created": 1717805892695, + "name": "Input / Output" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 1230, + "versionNonce": 1971044679, + "index": "a9", + "isDeleted": false, + "id": "0h-ZKB5IgZaejdvIPwoE8", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 580.4717245132821, + "y": 378.1846500970294, + "strokeColor": "#12b886", + "backgroundColor": "transparent", + "width": 159.67236311735425, + "height": 78.82038654907808, + "seed": 879538921, + "groupIds": [ + "rFBS465VVdQ92cc9YMl_1" + ], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "RpFMi6YrYnAcn8MY877bl" + } + ], + "updated": 1717805885404, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1446, + "versionNonce": 924975207, + "index": "aA", + "isDeleted": false, + "id": "RpFMi6YrYnAcn8MY877bl", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 602.8039311574084, + "y": 389.08192138632666, + "strokeColor": "#12b886", + "backgroundColor": "transparent", + "width": 115.00794982910156, + "height": 57.025843970483656, + "seed": 268987849, + "groupIds": [ + "rFBS465VVdQ92cc9YMl_1" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717805885404, + "link": null, + "locked": false, + "fontSize": 22.810337588193462, + "fontFamily": 1, + "text": "Predefined\nProcess", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "0h-ZKB5IgZaejdvIPwoE8", + "originalText": "Predefined\nProcess", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "version": 1395, + "versionNonce": 1676893063, + "index": "aB", + "isDeleted": false, + "id": "FgFiX0ABP0EDF6JlAkDxx", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 568.8490542276033, + "y": 378.4058291500633, + "strokeColor": "#12b886", + "backgroundColor": "transparent", + "width": 185.46152548156357, + "height": 78.82038654907808, + "seed": 975817897, + "groupIds": [ + "rFBS465VVdQ92cc9YMl_1" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717805885404, + "link": null, + "locked": false + } + ], + "id": "3PmSGt-bzcvgwCEo6QQlC", + "created": 1717805887161, + "name": "Predefined Process" + }, + { + "status": "published", + "elements": [ + { + "type": "line", + "version": 941, + "versionNonce": 1356696073, + "index": "a0", + "isDeleted": false, + "id": "PEJ-QNcFs7f2x-SImP4Fv", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 315.3350889273492, + "y": 413.1976522799753, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "width": 187.62872376742763, + "height": 99.60927275207422, + "seed": 1752890825, + "groupIds": [ + "kB67pLNC54wJH3yWPiTVB" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717805702754, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 1.6772038882485643, + 53.36744603397734 + ], + [ + 186.88413549416947, + 52.042222902854206 + ], + [ + 187.62872376742763, + -46.241826718096874 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "text", + "version": 35, + "versionNonce": 1562294023, + "index": "a1", + "isDeleted": false, + "id": "DXjbJ3yXQCFLHJn6g-Zme", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 344.90625, + "y": 423.91015625, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "width": 130.71990966796875, + "height": 25, + "seed": 257966985, + "groupIds": [ + "kB67pLNC54wJH3yWPiTVB" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717805702754, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Manual Input", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Manual Input", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "gHuOPWI1to2x4GZe-f8a6", + "created": 1717805884530, + "name": "Manaual Input" + }, + { + "status": "published", + "elements": [ + { + "type": "line", + "version": 5440, + "versionNonce": 1954191207, + "index": "aQ", + "isDeleted": false, + "id": "vHRbJ2BpfkCosy9NR5IEM", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1315.8953115682903, + "y": 274.03687989865256, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "width": 169.06445083090944, + "height": 109.53810588080518, + "seed": 1680859433, + "groupIds": [ + "YmGzfEKNBNtMT7U_3Ik1q", + "fwNdaia9ClByvlnRwspJv" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1717803982727, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -0.210460059221532, + 1.8477748276831107 + ], + [ + 0.061875343815700035, + -69.08591209470663 + ], + [ + 0.385487287172964, + -82.52264461609295 + ], + [ + 19.12193512037811, + -84.7286753285594 + ], + [ + 149.93796237532024, + -86.31029269671342 + ], + [ + 166.81888186029096, + -85.57758694053635 + ], + [ + 167.88556319979034, + -70.81765155100794 + ], + [ + 168.8539907716879, + 2.7251734065049504 + ], + [ + 167.63134224522514, + 10.144835962980778 + ], + [ + 161.05653820691145, + 6.321368101000758 + ], + [ + 126.67162974648454, + -6.659572950867641 + ], + [ + 85.15472426677707, + 9.034466823305706 + ], + [ + 45.334502532349916, + 23.227813184091758 + ], + [ + 0.25980113762557266, + 6.572512990447478 + ] + ] + }, + { + "type": "line", + "version": 5652, + "versionNonce": 941691527, + "index": "aR", + "isDeleted": false, + "id": "FlxDB4fsEGl-MMNwUWJDA", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1308.578191047576, + "y": 280.854699893971, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "width": 169.06445083090944, + "height": 109.53810588080518, + "seed": 1855634665, + "groupIds": [ + "WMezY1oOBs-zKbPEOyf9H", + "fwNdaia9ClByvlnRwspJv" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1717803982727, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -0.210460059221532, + 1.8477748276831107 + ], + [ + 0.061875343815700035, + -69.08591209470663 + ], + [ + 0.385487287172964, + -82.52264461609295 + ], + [ + 19.12193512037811, + -84.7286753285594 + ], + [ + 149.93796237532024, + -86.31029269671342 + ], + [ + 166.81888186029096, + -85.57758694053635 + ], + [ + 167.88556319979034, + -70.81765155100794 + ], + [ + 168.8539907716879, + 2.7251734065049504 + ], + [ + 167.63134224522514, + 10.144835962980778 + ], + [ + 161.05653820691145, + 6.321368101000758 + ], + [ + 126.67162974648454, + -6.659572950867641 + ], + [ + 85.15472426677707, + 9.034466823305706 + ], + [ + 45.334502532349916, + 23.227813184091758 + ], + [ + 0.25980113762557266, + 6.572512990447478 + ] + ] + }, + { + "type": "line", + "version": 5779, + "versionNonce": 864758183, + "index": "aS", + "isDeleted": false, + "id": "7dLPYxBfyAwoZfHj-ESpk", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1301.512959697645, + "y": 288.16753605061, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "width": 169.06445083090944, + "height": 109.53810588080518, + "seed": 1659378345, + "groupIds": [ + "NZ75EPZcHVPixfpUW6YqW", + "fwNdaia9ClByvlnRwspJv" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1717803982727, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -0.210460059221532, + 1.8477748276831107 + ], + [ + 0.061875343815700035, + -69.08591209470663 + ], + [ + 0.385487287172964, + -82.52264461609295 + ], + [ + 19.12193512037811, + -84.7286753285594 + ], + [ + 149.93796237532024, + -86.31029269671342 + ], + [ + 166.81888186029096, + -85.57758694053635 + ], + [ + 167.88556319979034, + -70.81765155100794 + ], + [ + 168.8539907716879, + 2.7251734065049504 + ], + [ + 167.63134224522514, + 10.144835962980778 + ], + [ + 161.05653820691145, + 6.321368101000758 + ], + [ + 126.67162974648454, + -6.659572950867641 + ], + [ + 85.15472426677707, + 9.034466823305706 + ], + [ + 45.334502532349916, + 23.227813184091758 + ], + [ + 0.25980113762557266, + 6.572512990447478 + ] + ] + }, + { + "type": "text", + "version": 324, + "versionNonce": 1783405639, + "index": "aT", + "isDeleted": false, + "id": "RprONJByu_K2I-ck4qX2a", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1337.6528397819125, + "y": 226.30550509373967, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e9ecef", + "width": 92.31991577148438, + "height": 50, + "seed": 1987618505, + "groupIds": [ + "fwNdaia9ClByvlnRwspJv" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717804505382, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Multiple\nDocument", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Multiple\nDocument", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "ZXLnsU-Sx_2nZTLLHK4bm", + "created": 1717805862639, + "name": "Multiple Document" + }, + { + "status": "published", + "elements": [ + { + "type": "line", + "version": 5183, + "versionNonce": 891804871, + "index": "aK", + "isDeleted": false, + "id": "x72qFrvXbZWSSl97nrosm", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1082.1957019064055, + "y": 280.36608085527246, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 169.06445083090944, + "height": 109.53810588080518, + "seed": 1229771881, + "groupIds": [ + "E9X55ngSHFvvwCLu79kUv" + ], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1717805859470, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -0.210460059221532, + 1.8477748276831107 + ], + [ + 0.061875343815700035, + -69.08591209470663 + ], + [ + 0.385487287172964, + -82.52264461609295 + ], + [ + 19.12193512037811, + -84.7286753285594 + ], + [ + 149.93796237532024, + -86.31029269671342 + ], + [ + 166.81888186029096, + -85.57758694053635 + ], + [ + 167.88556319979034, + -70.81765155100794 + ], + [ + 168.8539907716879, + 2.7251734065049504 + ], + [ + 167.63134224522514, + 10.144835962980778 + ], + [ + 161.05653820691145, + 6.321368101000758 + ], + [ + 126.67162974648454, + -6.659572950867641 + ], + [ + 85.15472426677707, + 9.034466823305706 + ], + [ + 45.334502532349916, + 23.227813184091758 + ], + [ + 0.25980113762557266, + 6.572512990447478 + ] + ] + }, + { + "type": "text", + "version": 83, + "versionNonce": 1908321255, + "index": "aL", + "isDeleted": false, + "id": "s6cEcwidKCnBX3gM0CNwM", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1123.1596518469416, + "y": 231.89641871848858, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 92.31991577148438, + "height": 25, + "seed": 1976372361, + "groupIds": [ + "E9X55ngSHFvvwCLu79kUv" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717805859470, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Document", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Document", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "EnBbCI8QaIPU6EmLx_POi", + "created": 1717805860607, + "name": "Document" + }, + { + "status": "published", + "elements": [ + { + "type": "diamond", + "version": 284, + "versionNonce": 494598825, + "index": "a7", + "isDeleted": false, + "id": "Q_oKTwLVNHjW9DQkiDUeo", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 809.5078125, + "y": 209.966604641718, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "width": 199.9609375, + "height": 81.8671875, + "seed": 269057129, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "8dwg41NA4-0FK5_3BvgL6" + } + ], + "updated": 1717805713870, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 136, + "versionNonce": 393723273, + "index": "a8", + "isDeleted": false, + "id": "8dwg41NA4-0FK5_3BvgL6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 871.1880874633789, + "y": 238.433401516718, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "width": 76.61991882324219, + "height": 25, + "seed": 2033436231, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717805713870, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Decision", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Q_oKTwLVNHjW9DQkiDUeo", + "originalText": "Decision", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "7drGFmKnMeVauqCwWXP1T", + "created": 1717805857819, + "name": "Decision" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 244, + "versionNonce": 359926087, + "index": "a5", + "isDeleted": false, + "id": "TpqBqrxZNxm6So_TyTxMG", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 571.77734375, + "y": 214.21484375, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "width": 167.30859374999997, + "height": 75.76562499999999, + "seed": 2040529417, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "VUPw28Em7aayRP1uWLDQd" + } + ], + "updated": 1717803160217, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 17, + "versionNonce": 427702215, + "index": "a6", + "isDeleted": false, + "id": "VUPw28Em7aayRP1uWLDQd", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 617.6316833496094, + "y": 239.59765625, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "width": 75.59991455078125, + "height": 25, + "seed": 913241927, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1717804505382, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Process", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "TpqBqrxZNxm6So_TyTxMG", + "originalText": "Process", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "349WwmMYPcxRe7MdAxOTo", + "created": 1717805855612, + "name": "Process" + }, + { + "status": "published", + "elements": [ + { + "id": "aOYKoBFtkBkWinkOTZSbx", + "type": "line", + "x": 341.79193029213565, + "y": 212.02306029032388, + "width": 215.3841423960141, + "height": 86.5470616870208, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 80, + "groupIds": [ + "iURYqzDDUS_HlukrruoXt" + ], + "frameId": null, + "index": "b04", + "roundness": { + "type": 2 + }, + "seed": 400689511, + "version": 4797, + "versionNonce": 106805319, + "isDeleted": false, + "boundElements": null, + "updated": 1717805847002, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 71.81544595290487, + -2.4136740813494826 + ], + [ + 138.82660765347347, + -1.2450510810007056 + ], + [ + 168.109388661726, + 16.07292491766025 + ], + [ + 177.73990742133736, + 41.00695112077255 + ], + [ + 167.85304263638204, + 67.39197167864695 + ], + [ + 138.9496417516002, + 83.97677862351297 + ], + [ + 74.80091837009296, + 84.13338760567132 + ], + [ + 2.404470760619684, + 84.10253853545282 + ], + [ + -25.363803827368663, + 71.44096570190817 + ], + [ + -37.64423497467675, + 43.52586048206696 + ], + [ + -27.748183191795828, + 13.57012300661859 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + -3.916043340218039, + -1.0156321879867392 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "bPfE_O7x6N99m2sVqdJ2A", + "type": "text", + "x": 351.0942276581047, + "y": 244.74988065652488, + "width": 116.13620399376578, + "height": 24.01494385587742, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 80, + "groupIds": [ + "iURYqzDDUS_HlukrruoXt" + ], + "frameId": null, + "index": "b05", + "roundness": null, + "seed": 1445651593, + "version": 379, + "versionNonce": 1722619751, + "isDeleted": false, + "boundElements": null, + "updated": 1717805847002, + "link": null, + "locked": false, + "text": "Start / End", + "fontSize": 19.211955084701934, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Start / End", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "id": "tVtn0WO3N-0HC-p2kIXOJ", + "created": 1717805851594, + "name": "Start / End" + } + ] +} diff --git a/apps/web/public/excalidraw/libraries/html-input-elements.excalidrawlib b/apps/web/public/excalidraw/libraries/html-input-elements.excalidrawlib new file mode 100644 index 00000000..4cea620b --- /dev/null +++ b/apps/web/public/excalidraw/libraries/html-input-elements.excalidrawlib @@ -0,0 +1,1165 @@ +{ + "type": "excalidrawlib", + "version": 2, + "source": "https://excalidraw.com", + "libraryItems": [ + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 204, + "versionNonce": 109672520, + "isDeleted": false, + "id": "k22PsWhRhSJGkuTxP5l9Y", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 461.9999999999999, + "y": 304, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef5", + "width": 209.00000000000017, + "height": 81, + "seed": 841086984, + "groupIds": [ + "J9pfMe7SYXD6thqgFLUGM" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839907, + "link": null + }, + { + "type": "text", + "version": 44, + "versionNonce": 455020600, + "isDeleted": false, + "id": "VJQw3xkenASRSundOHTVA", + "fillStyle": "cross-hatch", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 506, + "y": 323, + "strokeColor": "#fff", + "backgroundColor": "transparent", + "width": 124, + "height": 45, + "seed": 1647687432, + "groupIds": [ + "J9pfMe7SYXD6thqgFLUGM" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "fontSize": 36, + "fontFamily": 1, + "text": "Button", + "baseline": 32, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Button" + } + ], + "id": "GAwmmQaDaHWr5CQpMgrSD", + "created": 1647962839907, + "name": "button" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 602, + "versionNonce": 195427656, + "isDeleted": false, + "id": "_SwsgMaRNIZhFwM2sk7dl", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 779.6666666666667, + "y": 297.3333333333333, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef5", + "width": 279.00000000000017, + "height": 81, + "seed": 1036899076, + "groupIds": [ + "Ni8RRCo1KHeV_Vffhq2zB" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "text", + "version": 277, + "versionNonce": 1740200760, + "isDeleted": false, + "id": "xdr_tGOm4BkYrJjnrsLCY", + "fillStyle": "cross-hatch", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 804.6666666666667, + "y": 316.3333333333333, + "strokeColor": "#fff", + "backgroundColor": "transparent", + "width": 120, + "height": 45, + "seed": 222662588, + "groupIds": [ + "Ni8RRCo1KHeV_Vffhq2zB" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "fontSize": 36, + "fontFamily": 1, + "text": "Number", + "baseline": 32, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Number" + }, + { + "type": "rectangle", + "version": 286, + "versionNonce": 260510792, + "isDeleted": false, + "id": "okjTojr7z04fGOoogdW50", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 999.6666666666667, + "y": 297.3333333333333, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "width": 59, + "height": 82.00000000000001, + "seed": 681693700, + "groupIds": [ + "Ni8RRCo1KHeV_Vffhq2zB" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "line", + "version": 252, + "versionNonce": 1599681592, + "isDeleted": false, + "id": "VbGG85jIJC_dkM8EoPEHV", + "fillStyle": "cross-hatch", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1019.6666666666667, + "y": 359.3333333333333, + "strokeColor": "#fff", + "backgroundColor": "transparent", + "width": 27.623260931670757, + "height": 1.1375199034810066, + "seed": 206606140, + "groupIds": [ + "Ni8RRCo1KHeV_Vffhq2zB" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 27.623260931670757, + 1.1375199034810066 + ] + ] + }, + { + "type": "line", + "version": 317, + "versionNonce": 1160596296, + "isDeleted": false, + "id": "N0REOwLI0xUza0eDwOufb", + "fillStyle": "cross-hatch", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1018.6666666666667, + "y": 322.51197164040497, + "strokeColor": "#fff", + "backgroundColor": "transparent", + "width": 23.988219098175673, + "height": 0, + "seed": 104368900, + "groupIds": [ + "yyp2PdgHtBtiPxRoZ01zn", + "Ni8RRCo1KHeV_Vffhq2zB" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 23.988219098175673, + 0 + ] + ] + }, + { + "type": "line", + "version": 361, + "versionNonce": 1737878840, + "isDeleted": false, + "id": "LqfOKkvxCE6dipUTr-0lF", + "fillStyle": "cross-hatch", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 4.71238898038469, + "x": 1016.9028222923077, + "y": 322.3893295528446, + "strokeColor": "#fff", + "backgroundColor": "transparent", + "width": 23.988219098175673, + "height": 0, + "seed": 836099644, + "groupIds": [ + "yyp2PdgHtBtiPxRoZ01zn", + "Ni8RRCo1KHeV_Vffhq2zB" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 23.988219098175673, + 0 + ] + ] + } + ], + "id": "TckAMjJwM4-6hr7cyVcnU", + "created": 1647962839907, + "name": "number" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 459, + "versionNonce": 1944770872, + "isDeleted": false, + "id": "I5gimYSn5ACZBi5Me3qos", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1168.6666666666665, + "y": 297.33333333333354, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef5", + "width": 389.0000000000001, + "height": 81, + "seed": 64666884, + "groupIds": [ + "gEnPYnMfu3qbbL98khJvF", + "ojKlDqNrXA99rdHd96oUr" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "text", + "version": 248, + "versionNonce": 934573640, + "isDeleted": false, + "id": "urfeovHdD3MElco_hPkyt", + "fillStyle": "cross-hatch", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1192.6666666666665, + "y": 314.6666666666668, + "strokeColor": "#fff", + "backgroundColor": "transparent", + "width": 190, + "height": 45, + "seed": 1057258628, + "groupIds": [ + "gEnPYnMfu3qbbL98khJvF", + "ojKlDqNrXA99rdHd96oUr" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "fontSize": 36, + "fontFamily": 1, + "text": "dd/mm/yyyy", + "baseline": 32, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "dd/mm/yyyy" + }, + { + "type": "rectangle", + "version": 286, + "versionNonce": 615182904, + "isDeleted": false, + "id": "-8urCT5QIYTc-HGKdGio8", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1473.4070593841043, + "y": 318.4919723987164, + "strokeColor": "#343a40", + "backgroundColor": "#ced4da", + "width": 60.25647156923948, + "height": 46.31755141080755, + "seed": 1524189704, + "groupIds": [ + "Yy13x-nnF3ol_dv6cRiHU", + "ojKlDqNrXA99rdHd96oUr" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "rectangle", + "version": 343, + "versionNonce": 90074440, + "isDeleted": false, + "id": "OkK25Kmt2YYs0qERXCHQZ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1473.2516650016655, + "y": 317.8851919242895, + "strokeColor": "#343a40", + "backgroundColor": "#343a40", + "width": 60.46875082115287, + "height": 9.50335950515257, + "seed": 1079972872, + "groupIds": [ + "Yy13x-nnF3ol_dv6cRiHU", + "ojKlDqNrXA99rdHd96oUr" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "line", + "version": 319, + "versionNonce": 1476389688, + "isDeleted": false, + "id": "1l1UaOGfbA9pMj0jE5p1u", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1479.140610823897, + "y": 312.9308585962113, + "strokeColor": "#343a40", + "backgroundColor": "#343a40", + "width": 0, + "height": 8.767250802760055, + "seed": 807306360, + "groupIds": [ + "Yy13x-nnF3ol_dv6cRiHU", + "ojKlDqNrXA99rdHd96oUr" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 8.767250802760055 + ] + ] + }, + { + "type": "line", + "version": 398, + "versionNonce": 414286920, + "isDeleted": false, + "id": "3sNWHB0HaEu-IjlNBo95x", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1526.6834868861401, + "y": 312.65792540792614, + "strokeColor": "#343a40", + "backgroundColor": "#343a40", + "width": 0, + "height": 6.782212885153945, + "seed": 1054495864, + "groupIds": [ + "Yy13x-nnF3ol_dv6cRiHU", + "ojKlDqNrXA99rdHd96oUr" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 6.782212885153945 + ] + ] + } + ], + "id": "l2jzmGjj8xyTmiz_DgHDu", + "created": 1647962839907, + "name": "date" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 381, + "versionNonce": 1199582536, + "isDeleted": false, + "id": "-402Xjsn3gNeduUlZg2v3", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 471.13008971704664, + "y": 482.51725327812335, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "width": 91.84807256235844, + "height": 91.84807256235844, + "seed": 1651987076, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "rectangle", + "version": 400, + "versionNonce": 797879096, + "isDeleted": false, + "id": "TQ8OBbwND54FxSCKhbXQw", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 624.1459627329187, + "y": 482.57394262052696, + "strokeColor": "#000000", + "backgroundColor": "#12b886", + "width": 96.84807256235842, + "height": 96.84807256235842, + "seed": 1902078852, + "groupIds": [ + "ssKI2tVi4gFqDjyI136lr" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "line", + "version": 501, + "versionNonce": 1450099784, + "isDeleted": false, + "id": "3MQgXEcewu0rf4Z7c7MYO", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 5.497787143782138, + "x": 651.3873436280293, + "y": 506.58664173169706, + "strokeColor": "#fff", + "backgroundColor": "#12b886", + "width": 48.536273629427996, + "height": 50.882468429031995, + "seed": 619666180, + "groupIds": [ + "ssKI2tVi4gFqDjyI136lr" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 48.536273629427996, + 50.882468429031995 + ] + ] + }, + { + "type": "rectangle", + "version": 420, + "versionNonce": 1760430152, + "isDeleted": false, + "id": "S2B2-2uaJBmW9t_tq6P0Z", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 782.0031055900616, + "y": 483.7644188110031, + "strokeColor": "#000000", + "backgroundColor": "#12b886", + "width": 98.51473922902507, + "height": 98.51473922902507, + "seed": 376259388, + "groupIds": [ + "jOcdqEWxTWwsfsRXK-7uU" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "line", + "version": 421, + "versionNonce": 1795915832, + "isDeleted": false, + "id": "0lk8cM1etWjomFj_dh7gO", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 802.4888362033943, + "y": 502.75090059325555, + "strokeColor": "#fff", + "backgroundColor": "#12b886", + "width": 60.38000146295094, + "height": 61.96894886987046, + "seed": 1359350020, + "groupIds": [ + "jOcdqEWxTWwsfsRXK-7uU" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 60.38000146295094, + 61.96894886987046 + ] + ] + }, + { + "type": "line", + "version": 465, + "versionNonce": 1033224008, + "isDeleted": false, + "id": "1YAABfniIJ9raJCUv2zJo", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 4.71238898038469, + "x": 804.082806226778, + "y": 501.3017811065811, + "strokeColor": "#fff", + "backgroundColor": "#12b886", + "width": 60.38000146295094, + "height": 61.96894886987046, + "seed": 2109675836, + "groupIds": [ + "jOcdqEWxTWwsfsRXK-7uU" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 60.38000146295094, + 61.96894886987046 + ] + ] + } + ], + "id": "5WS9ADBFgotstBrMOou5f", + "created": 1647962839907, + "name": "chekbox" + }, + { + "status": "published", + "elements": [ + { + "type": "line", + "version": 1348, + "versionNonce": 1876964152, + "isDeleted": false, + "id": "n3WYiFQfwBLt2YAdJ_MU9", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1083.071891717442, + "y": 486.924992676318, + "strokeColor": "#000000", + "backgroundColor": "#12b886", + "width": 185.52583445234677, + "height": 94.06284022471463, + "seed": 1512825988, + "groupIds": [ + "QnrtcvWK88gC267KzOUwo" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -54.0885741214574, + 3.26847156143346 + ], + [ + -89.67460071146283, + 23.45332181394217 + ], + [ + -92.22253205774396, + 63.0437880022644 + ], + [ + -60.144768972084975, + 91.86290744298051 + ], + [ + 2.5001724247550996, + 94.06284022471463 + ], + [ + 59.384541551166066, + 91.45434849780133 + ], + [ + 88.5062786635112, + 71.24639451701566 + ], + [ + 93.3033023946028, + 31.081907444785514 + ], + [ + 64.52116000805307, + 1.822801447722543 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "ellipse", + "version": 774, + "versionNonce": 601472072, + "isDeleted": false, + "id": "55VhtrODf-39BZeQmSumD", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1085.1854189506882, + "y": 486.1091852034925, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "width": 93.04790406275514, + "height": 94.15561720635927, + "seed": 264549380, + "groupIds": [ + "QnrtcvWK88gC267KzOUwo" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + } + ], + "id": "BWDRsMWQdOyfQLv2O94Z-", + "created": 1647962839907, + "name": "toggle" + }, + { + "status": "published", + "elements": [ + { + "type": "ellipse", + "version": 799, + "versionNonce": 1616084792, + "isDeleted": false, + "id": "_bKueD4t26KAoK0x36dto", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1289.887799903069, + "y": 480.5734709177783, + "strokeColor": "#000000", + "backgroundColor": "#868e96", + "width": 93.04790406275514, + "height": 94.15561720635927, + "seed": 801562940, + "groupIds": [], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "ellipse", + "version": 855, + "versionNonce": 323833400, + "isDeleted": false, + "id": "JywcmKH07KzwKJ7WrUY6L", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1447.0306570459259, + "y": 482.47823282254024, + "strokeColor": "#000000", + "backgroundColor": "#12b886", + "width": 93.04790406275514, + "height": 94.15561720635927, + "seed": 1129273788, + "groupIds": [ + "cmZUI6etnFxQxV4ffbPxo" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "ellipse", + "version": 898, + "versionNonce": 228577608, + "isDeleted": false, + "id": "IEmC0Lz-6-MWX4vTCRvLH", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1465.6020856173543, + "y": 501.0496613939688, + "strokeColor": "#fff", + "backgroundColor": "#fff", + "width": 56.342021709813956, + "height": 57.01276006350216, + "seed": 808763964, + "groupIds": [ + "cmZUI6etnFxQxV4ffbPxo" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + } + ], + "id": "B0oUjXnDcywKHZei6xk4i", + "created": 1647962839907, + "name": "switch" + }, + { + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 477, + "versionNonce": 1784890936, + "isDeleted": false, + "id": "rsDBcsbUrznjIROoJWVY-", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 466.33333333333326, + "y": 665.3333333333331, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef5", + "width": 280.0000000000002, + "height": 81, + "seed": 1791643836, + "groupIds": [ + "r-CGb7V61ePHRBq-msY10" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "text", + "version": 242, + "versionNonce": 1426413896, + "isDeleted": false, + "id": "Nzq7E6nU6lBMiQSl2OFNv", + "fillStyle": "cross-hatch", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 491.33333333333337, + "y": 684.3333333333331, + "strokeColor": "#fff", + "backgroundColor": "transparent", + "width": 109, + "height": 45, + "seed": 993581372, + "groupIds": [ + "r-CGb7V61ePHRBq-msY10" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "fontSize": 36, + "fontFamily": 1, + "text": "Select", + "baseline": 32, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Select" + }, + { + "type": "line", + "version": 277, + "versionNonce": 1804451640, + "isDeleted": false, + "id": "XgNuRGYD3wVe9WRcTjGtr", + "fillStyle": "cross-hatch", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 691.3333333333336, + "y": 694.333333333333, + "strokeColor": "#fff", + "backgroundColor": "transparent", + "width": 38, + "height": 21, + "seed": 2109886908, + "groupIds": [ + "r-CGb7V61ePHRBq-msY10" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 16, + 21 + ], + [ + 38, + 1 + ] + ] + } + ], + "id": "bv6AOqfP6rvf3SMgBk4vS", + "created": 1647962839907, + "name": "select" + }, + { + "status": "published", + "elements": [ + { + "type": "ellipse", + "version": 949, + "versionNonce": 1524225848, + "isDeleted": false, + "id": "-XFi9OjODvOYo311gT6Nq", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 866.2806570459268, + "y": 641.1568042511118, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef5", + "width": 121.61933263418368, + "height": 123.06718183220954, + "seed": 214961156, + "groupIds": [ + "tRqxGU5SyRtDdgEKm5CJx", + "oAsLckZqtdTyJ2geE6DUb" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null + }, + { + "type": "line", + "version": 542, + "versionNonce": 968644680, + "isDeleted": false, + "id": "lFJJLa_cebMd2km50s4xm", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 5.497787143782138, + "x": 901.340088985371, + "y": 675.7137286005344, + "strokeColor": "#fff", + "backgroundColor": "#12b886", + "width": 52.59124040853669, + "height": 53.84770758771799, + "seed": 1746541828, + "groupIds": [ + "oAsLckZqtdTyJ2geE6DUb" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 52.59124040853669, + 53.84770758771799 + ] + ] + }, + { + "type": "line", + "version": 585, + "versionNonce": 285880376, + "isDeleted": false, + "id": "jDM-ygnwOVf9BUNXN7z0j", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 3.9269908169872405, + "x": 899.3518713892238, + "y": 672.7988431503037, + "strokeColor": "#fff", + "backgroundColor": "#12b886", + "width": 55.26947763608382, + "height": 57.18171102206827, + "seed": 554732932, + "groupIds": [ + "oAsLckZqtdTyJ2geE6DUb" + ], + "strokeSharpness": "round", + "boundElements": [], + "updated": 1647962839908, + "link": null, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 55.26947763608382, + 57.18171102206827 + ] + ] + } + ], + "id": "ou1D2OZuKQ7llStNyaYRC", + "created": 1647962839907, + "name": "action button" + } + ] +} diff --git a/apps/web/public/excalidraw/libraries/universal-ui-kit.excalidrawlib b/apps/web/public/excalidraw/libraries/universal-ui-kit.excalidrawlib new file mode 100644 index 00000000..a1b77e69 --- /dev/null +++ b/apps/web/public/excalidraw/libraries/universal-ui-kit.excalidrawlib @@ -0,0 +1,5533 @@ +{ + "type": "excalidrawlib", + "version": 2, + "source": "https://excalidraw.com", + "libraryItems": [ + { + "id": "efwT0ETYP68OJzEX_E8IU", + "status": "published", + "elements": [ + { + "id": "KG8neKgjYI7jiqrqnoIQC", + "type": "line", + "x": 1232.0583558339324, + "y": 711.6815886369704, + "width": 91.16819396087658, + "height": 40.05388963584642, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1381540613, + "version": 654, + "versionNonce": 82428267, + "isDeleted": false, + "boundElements": null, + "updated": 1670481287442, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 79.15616840522046, + 0.5715874229521618 + ], + [ + 79.46416906049366, + 28.293577436133948 + ], + [ + 25.38405344658713, + 28.007783724657884 + ], + [ + 29.749396550933398, + 40.05388963584642 + ], + [ + 7.35734824195174, + 27.876909864399977 + ], + [ + -11.396024245109626, + 28.00778372465779 + ], + [ + -11.70402490038292, + 0 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + -2.5149846609897395, + -2.5149846609897395 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "IJw_SvUiIOp5t5Sx1srTe", + "type": "text", + "x": 1236.4888603357447, + "y": 716.4424527253498, + "width": 55.58329984584186, + "height": 19.571584452761225, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1755160261, + "version": 64, + "versionNonce": 717108907, + "isDeleted": false, + "boundElements": null, + "updated": 1670481297548, + "link": null, + "locked": false, + "text": "Tooltip", + "fontSize": 15.657267562208974, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 13.571584452761225, + "containerId": null, + "originalText": "Tooltip" + } + ], + "created": 1670481302096, + "name": "Tooltip" + }, + { + "id": "svD0u-QcyVOjF6OyDJ7qc", + "status": "published", + "elements": [ + { + "id": "Ayh5aB2Ig5Md0qkxp9sHo", + "type": "ellipse", + "x": 1225.0657409542778, + "y": 760.6889061008446, + "width": 111.25692343761082, + "height": 108.18776692898712, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "OMcN4WFF7QMYq0irrOX5m" + ], + "strokeSharpness": "sharp", + "seed": 96437541, + "version": 120, + "versionNonce": 1733177413, + "isDeleted": false, + "boundElements": null, + "updated": 1670481201060, + "link": null, + "locked": false + }, + { + "id": "qC-Ll8MOu6htDgJYKA4QE", + "type": "text", + "x": 1250.1893111120598, + "y": 798.9589646030448, + "width": 62, + "height": 34, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "OMcN4WFF7QMYq0irrOX5m" + ], + "strokeSharpness": "sharp", + "seed": 2010811941, + "version": 50, + "versionNonce": 538944139, + "isDeleted": false, + "boundElements": null, + "updated": 1670481201060, + "link": null, + "locked": false, + "text": "65%", + "fontSize": 27.36597562069708, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 24, + "containerId": null, + "originalText": "65%" + }, + { + "id": "5xv8rRuZBk1deML6msWc1", + "type": "line", + "x": 1289.6573643933048, + "y": 761.1937112170393, + "width": 65.04920335369343, + "height": 108.29620118774267, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 4, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [ + "OMcN4WFF7QMYq0irrOX5m" + ], + "strokeSharpness": "round", + "seed": 828012139, + "version": 212, + "versionNonce": 755621797, + "isDeleted": false, + "boundElements": null, + "updated": 1670481201060, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -26.09116398252536, + 0.7148264104802138 + ], + [ + -55.04163360697157, + 23.231858340604845 + ], + [ + -65.04920335369343, + 45.034063860249375 + ], + [ + -63.61955053273323, + 67.19368258513407 + ], + [ + -53.611980786011145, + 89.35330131001865 + ], + [ + -39.67286578164817, + 100.07569746722095 + ], + [ + -24.66151116156516, + 106.5091351615423 + ], + [ + -7.863090515281556, + 108.29620118774267 + ] + ], + "lastCommittedPoint": [ + -7.863090515281556, + 108.29620118774267 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + } + ], + "created": 1670481202703, + "name": "Circle Progress Bar" + }, + { + "id": "EGtz9OOXb74RpkC0jHi9p", + "status": "published", + "elements": [ + { + "id": "gz0JGFkllMB9PVPBkSVkd", + "type": "ellipse", + "x": 1226.6003192085896, + "y": 732.2992083960748, + "width": 133.508308125133, + "height": 132.74101899797722, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "PMLwNXjxG2YXQX2Is_MbA" + ], + "strokeSharpness": "sharp", + "seed": 650245605, + "version": 45, + "versionNonce": 45659371, + "isDeleted": false, + "boundElements": null, + "updated": 1670480970116, + "link": null, + "locked": false + }, + { + "id": "z8kKi5XiJBhKFVrSN-d0p", + "type": "ellipse", + "x": 1272.6376668379457, + "y": 754.7141474478174, + "width": 37.597167230640935, + "height": 39.13174548495283, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "PMLwNXjxG2YXQX2Is_MbA" + ], + "strokeSharpness": "sharp", + "seed": 1628004715, + "version": 33, + "versionNonce": 705274693, + "isDeleted": false, + "boundElements": null, + "updated": 1670480970116, + "link": null, + "locked": false + }, + { + "id": "PHbQaPdvM5Afa80LxNsuy", + "type": "line", + "x": 1254.222727786203, + "y": 830.5122166720349, + "width": 77.49620184274954, + "height": 39.13174548495283, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "PMLwNXjxG2YXQX2Is_MbA" + ], + "strokeSharpness": "round", + "seed": 818769573, + "version": 148, + "versionNonce": 256434059, + "isDeleted": false, + "boundElements": null, + "updated": 1670480970116, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 37.597167230640935, + -23.7859629418341 + ], + [ + 77.49620184274954, + -0.7672891271560047 + ], + [ + 39.13174548495272, + 15.34578254311873 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + -3.0691565086237915, + 3.8364456357796826 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + } + ], + "created": 1670480971171, + "name": "User Icon" + }, + { + "id": "1TOJ67S86-pYl3x3LO-bV", + "status": "published", + "elements": [ + { + "type": "line", + "version": 276, + "versionNonce": 977636613, + "isDeleted": false, + "id": "iiZe9dq-kzAhxLKZTIXUM", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1366.9751567144779, + "y": 666.6293669043139, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 85.39136149270017, + "height": 42.07978670334516, + "seed": 1566526565, + "groupIds": [ + "mzJ43xI_t4SUAc24TnsK-" + ], + "strokeSharpness": "round", + "boundElements": null, + "updated": 1670480753825, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 72.81514702846026, + -0.9147779718117884 + ], + [ + 74.52844460560048, + 40.25023075972158 + ], + [ + -6.8531903085609756, + 41.16500873153337 + ], + [ + -10.862916887099686, + 5.783139948933467 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "ellipse", + "version": 134, + "versionNonce": 543781323, + "isDeleted": false, + "id": "vgIST6Us1mjPGwJozPW4I", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1351.6343079015057, + "y": 662.2653655628993, + "strokeColor": "#000000", + "backgroundColor": "#fff", + "width": 51.80388556015671, + "height": 49.10064195085418, + "seed": 609605739, + "groupIds": [ + "mzJ43xI_t4SUAc24TnsK-" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480753825, + "link": null, + "locked": false + } + ], + "created": 1670480761552, + "name": "Toggle Unchecked" + }, + { + "id": "WKn5Dggu6WveHBhW2xMQB", + "status": "published", + "elements": [ + { + "id": "V7FluXy8pgwwnZGK1kXtL", + "type": "ellipse", + "x": 1269.1667704967917, + "y": 459.86472125451473, + "width": 354.13953231335654, + "height": 354.13953231335654, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#ced4da", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "N0C5Kbfe0nyicbFeH54gp" + ], + "strokeSharpness": "sharp", + "seed": 2015934763, + "version": 52, + "versionNonce": 1290905067, + "isDeleted": false, + "boundElements": null, + "updated": 1670480550159, + "link": null, + "locked": false + }, + { + "id": "TnuLxzt45uL0W5ZBDzYge", + "type": "line", + "x": 1387.6161703831704, + "y": 482.8294008243229, + "width": 195.80411001625862, + "height": 314.2535099026371, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#868e96", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "N0C5Kbfe0nyicbFeH54gp" + ], + "strokeSharpness": "sharp", + "seed": 1779669355, + "version": 249, + "versionNonce": 1892958789, + "isDeleted": false, + "boundElements": null, + "updated": 1670480550159, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 76.1460427841007, + 163.17009168021548 + ], + [ + -36.26002037338117, + 303.3755037906228 + ], + [ + -80.98071216721792, + 268.32415076302095 + ], + [ + -103.94539173702606, + 222.39479162340479 + ], + [ + -119.65806723215792, + 160.75275698865676 + ], + [ + -116.03206519481978, + 105.15405908280559 + ], + [ + -68.8940387094242, + 30.21668364448442 + ], + [ + -39.886022410719306, + 7.252004074676279 + ], + [ + -4.834669383117443, + -10.878006112014305 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + 0, + 6.043336728896861 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + } + ], + "created": 1670480551630, + "name": "Pie chart" + }, + { + "id": "QgODomeA5ZOdzyh1Us7Ta", + "status": "published", + "elements": [ + { + "id": "_pREWmz2mIL5hqJZb4eyn", + "type": "line", + "x": 1251.0367603101013, + "y": 509.4200824314691, + "width": 1.208667345779304, + "height": 361.39153638803276, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "cPFxXXqWyStXkMqXs51a2" + ], + "strokeSharpness": "round", + "seed": 1134180997, + "version": 135, + "versionNonce": 1086824331, + "isDeleted": false, + "boundElements": null, + "updated": 1670480509927, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.208667345779304, + 361.39153638803276 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "IOTqq0ireBufePLVtXFc5", + "type": "line", + "x": 1249.8280929643217, + "y": 865.9769494363844, + "width": 448.4155852841477, + "height": 0, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "cPFxXXqWyStXkMqXs51a2" + ], + "strokeSharpness": "round", + "seed": 740241131, + "version": 85, + "versionNonce": 1219244709, + "isDeleted": false, + "boundElements": null, + "updated": 1670480509927, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 448.4155852841477, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "OldHkNBURLU1VIyXCymne", + "type": "line", + "x": 1253.4540950016599, + "y": 823.6735923341064, + "width": 377.1042118831647, + "height": 270.7414854545798, + "angle": 0, + "strokeColor": "#364fc7", + "backgroundColor": "#868e96", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "cPFxXXqWyStXkMqXs51a2" + ], + "strokeSharpness": "sharp", + "seed": 1629572741, + "version": 115, + "versionNonce": 224491051, + "isDeleted": false, + "boundElements": null, + "updated": 1670480509927, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 113.61473050326117, + -154.70942025976 + ], + [ + 195.80411001625862, + -74.93737543832128 + ], + [ + 377.1042118831647, + -270.7414854545798 + ] + ], + "lastCommittedPoint": [ + 377.1042118831647, + -270.7414854545798 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "gZZh4i6_nKwSiqu8hIUi3", + "type": "line", + "x": 1249.828092964322, + "y": 765.6575597366964, + "width": 395.23422206985515, + "height": 166.79609371755362, + "angle": 0, + "strokeColor": "#e67700", + "backgroundColor": "#868e96", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "cPFxXXqWyStXkMqXs51a2" + ], + "strokeSharpness": "sharp", + "seed": 2015346629, + "version": 128, + "versionNonce": 205035013, + "isDeleted": false, + "boundElements": null, + "updated": 1670480509927, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 123.28406926949606, + -38.67735506494 + ], + [ + 229.64679569808095, + -166.79609371755362 + ], + [ + 395.23422206985515, + -107.57139377436431 + ] + ], + "lastCommittedPoint": [ + 395.23422206985515, + -107.57139377436431 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + } + ], + "created": 1670480511509, + "name": "Line chart" + }, + { + "id": "f4tLnOQbKZB5qr3bBDleA", + "status": "published", + "elements": [ + { + "id": "_pREWmz2mIL5hqJZb4eyn", + "type": "line", + "x": 1251.0367603101013, + "y": 509.4200824314691, + "width": 1.208667345779304, + "height": 361.39153638803276, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "W_JT33kCqmb3NrvX82yGi" + ], + "strokeSharpness": "round", + "seed": 1134180997, + "version": 133, + "versionNonce": 585081637, + "isDeleted": false, + "boundElements": null, + "updated": 1670480431166, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.208667345779304, + 361.39153638803276 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "IOTqq0ireBufePLVtXFc5", + "type": "line", + "x": 1249.8280929643217, + "y": 865.9769494363844, + "width": 448.4155852841477, + "height": 0, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "W_JT33kCqmb3NrvX82yGi" + ], + "strokeSharpness": "round", + "seed": 740241131, + "version": 83, + "versionNonce": 918244779, + "isDeleted": false, + "boundElements": null, + "updated": 1670480431166, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 448.4155852841477, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "_Ox-KDABPjBjqwKoXWxcD", + "type": "rectangle", + "x": 1252.2454276558806, + "y": 576.5581089168646, + "width": 183.71743655846467, + "height": 39.886022410719306, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#868e96", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "W_JT33kCqmb3NrvX82yGi" + ], + "strokeSharpness": "sharp", + "seed": 791085157, + "version": 147, + "versionNonce": 531232389, + "isDeleted": false, + "boundElements": null, + "updated": 1670480431166, + "link": null, + "locked": false + }, + { + "id": "jZ370aU59Lg8ITq_xSumM", + "type": "rectangle", + "x": 1253.4540950016599, + "y": 638.2001435516128, + "width": 258.65481199678595, + "height": 41.09468975649861, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#ced4da", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "W_JT33kCqmb3NrvX82yGi" + ], + "strokeSharpness": "sharp", + "seed": 534368395, + "version": 202, + "versionNonce": 16408651, + "isDeleted": false, + "boundElements": null, + "updated": 1670480431166, + "link": null, + "locked": false + }, + { + "id": "QIeO1lL_xrQ-khl9k2Zhz", + "type": "rectangle", + "x": 1253.6627623474392, + "y": 727.3975393633151, + "width": 326.34018336043073, + "height": 41.09468975649861, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#868e96", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "W_JT33kCqmb3NrvX82yGi" + ], + "strokeSharpness": "sharp", + "seed": 815129739, + "version": 253, + "versionNonce": 1788009957, + "isDeleted": false, + "boundElements": null, + "updated": 1670480431166, + "link": null, + "locked": false + }, + { + "id": "-7EpOYY_SMHotwrlRvTUZ", + "type": "rectangle", + "x": 1253.2887643847773, + "y": 792.039573998063, + "width": 277.99348952925584, + "height": 37.468687719160464, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#ced4da", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "W_JT33kCqmb3NrvX82yGi" + ], + "strokeSharpness": "sharp", + "seed": 1767177419, + "version": 307, + "versionNonce": 1660821227, + "isDeleted": false, + "boundElements": null, + "updated": 1670480431166, + "link": null, + "locked": false + } + ], + "created": 1670480432680, + "name": "Bar Chart" + }, + { + "id": "84fkoxzqE6lYcScgkQyqT", + "status": "published", + "elements": [ + { + "id": "M6UQFq-DJ88ICC--Ay9uc", + "type": "rectangle", + "x": 1263.1746839620596, + "y": 522.6713486629604, + "width": 297.5749965150734, + "height": 292.3887759508462, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "seed": 259665643, + "version": 89, + "versionNonce": 2080883723, + "isDeleted": false, + "boundElements": null, + "updated": 1670480203922, + "link": null, + "locked": false + }, + { + "id": "bGL6KammPQsdgPOWMRmOx", + "type": "line", + "x": 1264.916664669991, + "y": 564.350517806695, + "width": 292.0622768105809, + "height": 1.9677215053246755, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "seed": 1401779525, + "version": 105, + "versionNonce": 2085525029, + "isDeleted": false, + "boundElements": null, + "updated": 1670480203922, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 292.0622768105809, + -1.9677215053246755 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "type": "line", + "version": 370, + "versionNonce": 640223915, + "isDeleted": false, + "id": "zm60He4vBRj4iXyHOidon", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 1.5375575269711668, + "x": 1282.1296686979365, + "y": 535.8536476858105, + "strokeColor": "#000000", + "backgroundColor": "#000", + "width": 21.95411118135862, + "height": 15.781689674230238, + "seed": 806865637, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203922, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 10.977055590679315, + 13.80897846495146 + ], + [ + 21.95411118135862, + -1.9727112092787782 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 460, + "versionNonce": 1958906245, + "isDeleted": false, + "id": "EVTiFKS7UqBav4B3a-07M", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 4.68150589574342, + "x": 1522.0214444794117, + "y": 535.9219162165431, + "strokeColor": "#000000", + "backgroundColor": "#000", + "width": 21.95411118135862, + "height": 15.781689674230236, + "seed": 1035346245, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203922, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + -0.46089649349504347 + ], + [ + 10.977055590679315, + 13.348081971456416 + ], + [ + 21.95411118135862, + -2.4336077027738217 + ], + [ + 0, + -0.46089649349504347 + ] + ] + }, + { + "id": "3oLMRGYhNIYPjYMlcFfHM", + "type": "text", + "x": 1338.6382457183336, + "y": 530.721257836406, + "width": 158, + "height": 25, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "seed": 900358123, + "version": 34, + "versionNonce": 363363659, + "isDeleted": false, + "boundElements": null, + "updated": 1670480203922, + "link": null, + "locked": false, + "text": "December 2022", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18, + "containerId": null, + "originalText": "December 2022" + }, + { + "id": "LvYW8Z0Ke5UyYRlBfEcpm", + "type": "text", + "x": 1290.0152503795039, + "y": 571.7315951113401, + "width": 13, + "height": 25, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "seed": 2126499275, + "version": 13, + "versionNonce": 1428230373, + "isDeleted": false, + "boundElements": null, + "updated": 1670480203922, + "link": null, + "locked": false, + "text": "s", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18, + "containerId": null, + "originalText": "s" + }, + { + "id": "jMYOTvn8pEYKIw-6neFQp", + "type": "line", + "x": 1309.0365582643087, + "y": 563.6780087318599, + "width": 3.279535842207679, + "height": 248.58881683934385, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "seed": 1213881349, + "version": 49, + "versionNonce": 1440783339, + "isDeleted": false, + "boundElements": null, + "updated": 1670480203922, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 3.279535842207679, + 248.58881683934385 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "A_E-YxfYQZKIvxCov45Hr", + "type": "line", + "x": 1349.0468955392425, + "y": 563.0221015634183, + "width": 1.3118143368833444, + "height": 247.9329096709023, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "seed": 897129733, + "version": 52, + "versionNonce": 1794616389, + "isDeleted": false, + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.3118143368833444, + 247.9329096709023 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "3tnQs8W3vaOk8yJwdkgDb", + "type": "line", + "x": 1389.0572328141766, + "y": 564.3339159003015, + "width": 1.9677215053247892, + "height": 251.86835268155153, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "seed": 443113323, + "version": 53, + "versionNonce": 584836747, + "isDeleted": false, + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.9677215053247892, + 251.86835268155153 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ujfnIT1tYW3_O1svjjhrA", + "type": "line", + "x": 1427.788034246903, + "y": 562.0221015634183, + "width": 3.9354430106491236, + "height": 252.5242598499931, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "seed": 80801707, + "version": 60, + "versionNonce": 1982061477, + "isDeleted": false, + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 3.9354430106491236, + 252.5242598499931 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "A2fk1HBI_qi_zk8gv0z7i", + "type": "line", + "x": 1468.1424643533953, + "y": 562.7102872265352, + "width": 3.935443010649351, + "height": 247.9329096709024, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "seed": 2003038507, + "version": 69, + "versionNonce": 1398070571, + "isDeleted": false, + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 3.935443010649351, + 247.9329096709024 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "Xk-9MpexuDEwgHGAF4l2f", + "type": "line", + "x": 1514.0023797648232, + "y": 563.0221015634183, + "width": 3.279535842207679, + "height": 247.9329096709023, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "seed": 1767212331, + "version": 74, + "versionNonce": 1590894341, + "isDeleted": false, + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 3.279535842207679, + 247.9329096709023 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "type": "line", + "version": 234, + "versionNonce": 45481931, + "isDeleted": false, + "id": "PEo23-FIwpvF11iLGz1Jd", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1558.5630005575767, + "y": 603.7106746548767, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 291.2227827880438, + "height": 2.623628673766234, + "seed": 719684965, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -291.2227827880438, + 2.623628673766234 + ] + ] + }, + { + "type": "line", + "version": 312, + "versionNonce": 730003045, + "isDeleted": false, + "id": "nYpR-qj49yimGBBd2tO46", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1559.8600918607117, + "y": 642.3965956531259, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 288.59915411427755, + "height": 2.623628673766234, + "seed": 1626187397, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -288.59915411427755, + 2.623628673766234 + ] + ] + }, + { + "type": "line", + "version": 361, + "versionNonce": 2065288811, + "isDeleted": false, + "id": "4vmT3vyGlYrdpBc_0ZmFV", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1556.0392065122326, + "y": 682.6721459193508, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 288.5991541142778, + "height": 1.9677215053246755, + "seed": 332205003, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -288.5991541142778, + 1.9677215053246755 + ] + ] + }, + { + "type": "line", + "version": 415, + "versionNonce": 922804677, + "isDeleted": false, + "id": "eNnSYgc-DOFdeRTYY3Ajb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1558.4011967230454, + "y": 723.810309371515, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 289.2550612827192, + "height": 1.9677215053246755, + "seed": 1987857253, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -289.2550612827192, + 1.9677215053246755 + ] + ] + }, + { + "type": "line", + "version": 461, + "versionNonce": 1598736651, + "isDeleted": false, + "id": "8cJOm8S0GTX7rKIdCLrTS", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1559.992018180589, + "y": 767.2620688861755, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 289.91096845116067, + "height": 2.623628673766234, + "seed": 1918413445, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "round", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -289.91096845116067, + 2.623628673766234 + ] + ] + }, + { + "type": "text", + "version": 38, + "versionNonce": 644505893, + "isDeleted": false, + "id": "Nrww6xcLvFoKhqaDCLPZy", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1324.1814948228796, + "y": 572.8229452904307, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 17, + "height": 25, + "seed": 1217802981, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "M", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "M" + }, + { + "type": "text", + "version": 63, + "versionNonce": 348352427, + "isDeleted": false, + "id": "i7LEI0-jY0-TF2-SoLr76", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1362.8477392662548, + "y": 573.4788524588724, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 18, + "height": 25, + "seed": 35082059, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "T", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "T" + }, + { + "type": "text", + "version": 89, + "versionNonce": 523039877, + "isDeleted": false, + "id": "EnMMMRd9y-L9CjEw5c8dl", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1402.2021693727472, + "y": 572.8229452904309, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 17, + "height": 25, + "seed": 191486283, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "W", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "W" + }, + { + "type": "text", + "version": 117, + "versionNonce": 1840136779, + "isDeleted": false, + "id": "PRV-lRA2ELRU7gyKn0B5Q", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1442.212506647681, + "y": 572.8229452904309, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 18, + "height": 25, + "seed": 47310405, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "T", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "T" + }, + { + "type": "text", + "version": 138, + "versionNonce": 1023810533, + "isDeleted": false, + "id": "V8jMROG3iez_gn4Zqd-dx", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1482.8787510910565, + "y": 572.1670381219893, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 13, + "height": 25, + "seed": 1025604267, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "F", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "F" + }, + { + "type": "text", + "version": 168, + "versionNonce": 317047019, + "isDeleted": false, + "id": "f7wOS9NLYZcPGcf06WtZo", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1532.0717887241722, + "y": 571.5111309535478, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 14, + "height": 25, + "seed": 1970427371, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "S", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "S" + }, + { + "type": "text", + "version": 215, + "versionNonce": 1208178501, + "isDeleted": false, + "id": "34rXlSD9Lt5_d7YvUvSis", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1527.4804385450816, + "y": 777.4659818441916, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 21, + "height": 25, + "seed": 944687307, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203923, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "31", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "31" + }, + { + "type": "text", + "version": 242, + "versionNonce": 770993035, + "isDeleted": false, + "id": "Vva74DvoxIt8K4sujd3xL", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1484.19056542794, + "y": 776.1541675073084, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 29, + "height": 25, + "seed": 10469739, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "30", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "30" + }, + { + "type": "text", + "version": 278, + "versionNonce": 713172645, + "isDeleted": false, + "id": "nKcmQwPTdL-nnjbcrk98s", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1439.5888779739153, + "y": 776.8100746757501, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 28, + "height": 25, + "seed": 1127213637, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "29", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "29" + }, + { + "type": "text", + "version": 297, + "versionNonce": 1393869355, + "isDeleted": false, + "id": "WMm3-BJ27tK-phf5Wk2i-", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1398.266726362098, + "y": 779.4337033495161, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 32, + "height": 25, + "seed": 331902987, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "28", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "28" + }, + { + "type": "text", + "version": 332, + "versionNonce": 657459717, + "isDeleted": false, + "id": "f-00jcaGB-y_Y0NGVcPXd", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1354.9768532449564, + "y": 779.4337033495161, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 27, + "height": 25, + "seed": 725845413, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "27", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "27" + }, + { + "type": "text", + "version": 356, + "versionNonce": 724649163, + "isDeleted": false, + "id": "w7VXTEKMvPHtvcMIDZnUh", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1315.6224231384638, + "y": 779.4337033495161, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 29, + "height": 25, + "seed": 1149747557, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "26", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "26" + }, + { + "type": "text", + "version": 380, + "versionNonce": 1326088549, + "isDeleted": false, + "id": "zKnE6J4Y6dn1KT759lcdH", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1274.9561786950883, + "y": 780.7455176863992, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 29, + "height": 25, + "seed": 111775531, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "25", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "25" + }, + { + "type": "text", + "version": 431, + "versionNonce": 1575252843, + "isDeleted": false, + "id": "ckAZn2av24IeIgYKMyK48", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1524.2009027028737, + "y": 734.1761087270498, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 29, + "height": 25, + "seed": 532718469, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "24", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "24" + }, + { + "type": "text", + "version": 456, + "versionNonce": 928976069, + "isDeleted": false, + "id": "QHga_G6e-SjJTpUtZnWFt", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1482.2228439226153, + "y": 732.2083872217253, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 30, + "height": 25, + "seed": 1606294187, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "23", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "23" + }, + { + "type": "text", + "version": 481, + "versionNonce": 997772811, + "isDeleted": false, + "id": "ue6_w-EwSBdJYD2YwMaQ1", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1438.2770636370321, + "y": 734.8320158954914, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 30, + "height": 25, + "seed": 1168972837, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "22", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "22" + }, + { + "type": "text", + "version": 500, + "versionNonce": 1875586085, + "isDeleted": false, + "id": "s2VVHFf7TVP1uEdRyIBTe", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1396.9549120252152, + "y": 733.5202015586084, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 22, + "height": 25, + "seed": 437427243, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "21", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "21" + }, + { + "type": "text", + "version": 526, + "versionNonce": 652793003, + "isDeleted": false, + "id": "hYSTA7-dNehZoTx--_VJB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1359.5682034240474, + "y": 733.5202015586084, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 30, + "height": 25, + "seed": 213870123, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "20", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "20" + }, + { + "type": "text", + "version": 555, + "versionNonce": 943480709, + "isDeleted": false, + "id": "0IxrNxGEforJSxSNYPTY2", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1320.8696804859962, + "y": 734.1761087270501, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 20, + "height": 25, + "seed": 1055154539, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "19", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "19" + }, + { + "type": "text", + "version": 571, + "versionNonce": 1071581003, + "isDeleted": false, + "id": "nC_1QKdal6-LEm8aUYaBS", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1279.5475288741793, + "y": 734.1761087270501, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 23, + "height": 25, + "seed": 1551393765, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "18", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "18" + }, + { + "type": "text", + "version": 616, + "versionNonce": 1995450085, + "isDeleted": false, + "id": "NRSV09eO2hnMRl7xiYe77", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1530.7599743872893, + "y": 692.1980499467915, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 18, + "height": 25, + "seed": 1242931269, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "17", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "17" + }, + { + "type": "text", + "version": 639, + "versionNonce": 1286989291, + "isDeleted": false, + "id": "N1Qhw_qoysAmAatRhzQKm", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1486.814194101706, + "y": 690.2303284414668, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 20, + "height": 25, + "seed": 1658147659, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "16", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "16" + }, + { + "type": "text", + "version": 668, + "versionNonce": 1853096517, + "isDeleted": false, + "id": "oX5nIa4tBUpOCaJSxqkNQ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1442.868413816123, + "y": 690.8862356099085, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 20, + "height": 25, + "seed": 164777029, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "15", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "15" + }, + { + "type": "text", + "version": 687, + "versionNonce": 1685581963, + "isDeleted": false, + "id": "ydYVlG2H9DWIq6kFV50Cx", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1401.5462622043058, + "y": 690.8862356099085, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 20, + "height": 25, + "seed": 487075339, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203924, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "14", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "14" + }, + { + "type": "text", + "version": 711, + "versionNonce": 866480549, + "isDeleted": false, + "id": "gxn0Pp0aRuXSmsv0vZHK9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1359.5682034240472, + "y": 692.1980499467916, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 21, + "height": 25, + "seed": 1268129131, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "13", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "13" + }, + { + "type": "text", + "version": 743, + "versionNonce": 1491052331, + "isDeleted": false, + "id": "3hS6WjpqNehLpAic25S_E", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1322.1814948228796, + "y": 691.54214277835, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 22, + "height": 25, + "seed": 2025386725, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "12", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "12" + }, + { + "type": "text", + "version": 771, + "versionNonce": 1672606981, + "isDeleted": false, + "id": "dEKzrCc04mq5G3R5R2-Mf", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1282.1711575479458, + "y": 692.1980499467915, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 13, + "height": 25, + "seed": 641426731, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "11", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "11" + }, + { + "type": "text", + "version": 814, + "versionNonce": 1616349643, + "isDeleted": false, + "id": "_I8pKCXD4o6HyaBkFZf0q", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1529.415881555731, + "y": 650.5640839980914, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 21, + "height": 25, + "seed": 897848363, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "10", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "10" + }, + { + "type": "text", + "version": 842, + "versionNonce": 437718117, + "isDeleted": false, + "id": "jgtRwe0ipUNG0huyqz5Yw", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1482.8464725963815, + "y": 649.9081768296498, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 14, + "height": 25, + "seed": 779739339, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "9", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "9" + }, + { + "type": "text", + "version": 871, + "versionNonce": 2147096683, + "isDeleted": false, + "id": "tX83Ioy91Z87My66Y12GS", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1440.2125066476813, + "y": 649.2522696612083, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 17, + "height": 25, + "seed": 1419577029, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "8", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "8" + }, + { + "type": "text", + "version": 903, + "versionNonce": 648165317, + "isDeleted": false, + "id": "8_2XCYwPhc-Y4O19eiK51", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1402.8257980465135, + "y": 650.5640839980914, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 13, + "height": 25, + "seed": 1589727813, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "7", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "7" + }, + { + "type": "text", + "version": 924, + "versionNonce": 898749195, + "isDeleted": false, + "id": "PRDzudXZatQhLqQFQrcFw", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1364.1272751084628, + "y": 652.5318055034161, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 15, + "height": 25, + "seed": 276662283, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "6", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "6" + }, + { + "type": "text", + "version": 939, + "versionNonce": 57734949, + "isDeleted": false, + "id": "pJ9giTkCvuhJUftvV8hpe", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1320.8696804859965, + "y": 652.5318055034161, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 14, + "height": 25, + "seed": 226579269, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "5", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "5" + }, + { + "type": "text", + "version": 958, + "versionNonce": 1637431723, + "isDeleted": false, + "id": "BTMcsNAl4pkKvotgKCSZO", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1282.8270647163872, + "y": 652.5318055034161, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 15, + "height": 25, + "seed": 1816943723, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "4", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "4" + }, + { + "type": "text", + "version": 991, + "versionNonce": 183552645, + "isDeleted": false, + "id": "eO5Xkd6JYPHhFHmCh1MFv", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1531.4158815557312, + "y": 611.209653891599, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 16, + "height": 25, + "seed": 1516880869, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "3", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "3" + }, + { + "type": "text", + "version": 1015, + "versionNonce": 1899678795, + "isDeleted": false, + "id": "-8DWEofL7OTPEdV4vvS0Z", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1484.8464725963818, + "y": 610.5537467231575, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 16, + "height": 25, + "seed": 1532588677, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "2", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "2" + }, + { + "type": "text", + "version": 1044, + "versionNonce": 1843343845, + "isDeleted": false, + "id": "C4hxI0WzneK8JwnC4pKz0", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1443.8684138161234, + "y": 610.5537467231575, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "width": 7, + "height": 25, + "seed": 1256278245, + "groupIds": [ + "0snuNBP8KeYVmBmPMDkqI" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670480203925, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "1", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1" + } + ], + "created": 1670480206794, + "name": "Calendar" + }, + { + "id": "WUvBairpZgzaZXlDs58W_", + "status": "published", + "elements": [ + { + "id": "xzf0xwxUnotMwGSMVN_io", + "type": "rectangle", + "x": 894.8717153091703, + "y": 482.79845583554464, + "width": 276.21447426482905, + "height": 43.80493864992883, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "jbt3pCldqMa0CL-tNQv3o" + ], + "strokeSharpness": "sharp", + "seed": 462208869, + "version": 183, + "versionNonce": 1207655851, + "isDeleted": false, + "boundElements": null, + "updated": 1670479764317, + "link": null, + "locked": false + }, + { + "id": "TG3OnRzco97bW4sNWUZ2v", + "type": "ellipse", + "x": 904.5744231954941, + "y": 491.01289227698146, + "width": 15.717345693056908, + "height": 16.431770497286855, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "gUHsObtaNlEaACaFm6G1L", + "jbt3pCldqMa0CL-tNQv3o" + ], + "strokeSharpness": "round", + "seed": 444728299, + "version": 84, + "versionNonce": 1311958661, + "isDeleted": false, + "boundElements": null, + "updated": 1670479764317, + "link": null, + "locked": false + }, + { + "id": "ooS_lNUdcCMCm69oJ6_RN", + "type": "line", + "x": 917.7377965821889, + "y": 504.3816146105126, + "width": 10.001947259218033, + "height": 11.43079686767781, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "gUHsObtaNlEaACaFm6G1L", + "jbt3pCldqMa0CL-tNQv3o" + ], + "strokeSharpness": "round", + "seed": 521162379, + "version": 114, + "versionNonce": 263745611, + "isDeleted": false, + "boundElements": null, + "updated": 1670479764317, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 10.001947259218033, + 11.43079686767781 + ] + ], + "lastCommittedPoint": [ + 19.647263004038223, + 22.454014861758083 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "OPtaxkNlYGVVJtIoyxX52", + "type": "text", + "x": 948.8816500788305, + "y": 490.592470931165, + "width": 76.22680427945444, + "height": 28.442837417706876, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "jbt3pCldqMa0CL-tNQv3o" + ], + "strokeSharpness": "round", + "seed": 1511444773, + "version": 61, + "versionNonce": 981323237, + "isDeleted": false, + "boundElements": null, + "updated": 1670479764317, + "link": null, + "locked": false, + "text": "Search", + "fontSize": 22.754269934165507, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 19.442837417706876, + "containerId": null, + "originalText": "Search" + } + ], + "created": 1670479766386, + "name": "Search Input" + }, + { + "id": "Vpd0rxn52ZVDTFz_xQvkP", + "status": "published", + "elements": [ + { + "id": "sJ__J9GvN3kcu-x9t5U_o", + "type": "line", + "x": 877.3050175258194, + "y": 482.40406025749184, + "width": 341.020350712951, + "height": 0, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "KgOL4t2n6YYJeY21s4hID" + ], + "strokeSharpness": "round", + "seed": 1861141701, + "version": 61, + "versionNonce": 1835191595, + "isDeleted": false, + "boundElements": null, + "updated": 1670479609043, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 341.020350712951, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ruBTrJAfkJE8N_Nb_gEUO", + "type": "ellipse", + "x": 893.1455286721379, + "y": 466.56354911117324, + "width": 28.06751857719769, + "height": 30.874270434917378, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "KgOL4t2n6YYJeY21s4hID" + ], + "strokeSharpness": "round", + "seed": 752053765, + "version": 19, + "versionNonce": 685518597, + "isDeleted": false, + "boundElements": null, + "updated": 1670479609043, + "link": null, + "locked": false + } + ], + "created": 1670479611431, + "name": "Slider" + }, + { + "id": "vRIqhXFjKIH7LHgPDdpss", + "status": "published", + "elements": [ + { + "id": "ita4BRJkzhHkyRhm7ziVw", + "type": "rectangle", + "x": 926.4231750359153, + "y": 458.5466694668739, + "width": 405.57564344050536, + "height": 266.6414264833773, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "_vaF9n9a8IjJOUBBNtgNu" + ], + "strokeSharpness": "round", + "seed": 50566693, + "version": 47, + "versionNonce": 1758022411, + "isDeleted": false, + "boundElements": null, + "updated": 1670479569627, + "link": null, + "locked": false + }, + { + "id": "ALMjLk0tbnCuPj1YQLgRh", + "type": "line", + "x": 925.0197991070553, + "y": 653.6159235783972, + "width": 402.76889158278584, + "height": 0, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "_vaF9n9a8IjJOUBBNtgNu" + ], + "strokeSharpness": "round", + "seed": 655151883, + "version": 61, + "versionNonce": 1535967013, + "isDeleted": false, + "boundElements": null, + "updated": 1670479569628, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 402.76889158278584, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "0iAlFKtVrTxyeq81SrhhS", + "type": "line", + "x": 1131.1059328628785, + "y": 653.6159235783972, + "width": 4.547473508864641e-13, + "height": 72.97554830071385, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "_vaF9n9a8IjJOUBBNtgNu" + ], + "strokeSharpness": "round", + "seed": 1876532683, + "version": 37, + "versionNonce": 790737323, + "isDeleted": false, + "boundElements": null, + "updated": 1670479569628, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -4.547473508864641e-13, + 72.97554830071385 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "AFGnrApC1IPrXBrL4kCF1", + "type": "text", + "x": 1019.0459863406672, + "y": 675.4901940133146, + "width": 34, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "_vaF9n9a8IjJOUBBNtgNu" + ], + "strokeSharpness": "round", + "seed": 2665125, + "version": 19, + "versionNonce": 306958981, + "isDeleted": false, + "boundElements": null, + "updated": 1670479569628, + "link": null, + "locked": false, + "text": "Yes", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18, + "containerId": null, + "originalText": "Yes" + }, + { + "id": "pzqyKmexAqQShchKBWncc", + "type": "text", + "x": 1198.678105234732, + "y": 676.8935699421745, + "width": 26, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "_vaF9n9a8IjJOUBBNtgNu" + ], + "strokeSharpness": "round", + "seed": 1966970795, + "version": 62, + "versionNonce": 1727284299, + "isDeleted": false, + "boundElements": null, + "updated": 1670479569628, + "link": null, + "locked": false, + "text": "No", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18, + "containerId": null, + "originalText": "No" + }, + { + "id": "T26gkhfc4w7KHdxqFrQEs", + "type": "text", + "x": 1087.8114068548016, + "y": 502.379645612187, + "width": 72.05063893289801, + "height": 35.31894065338138, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "_vaF9n9a8IjJOUBBNtgNu" + ], + "strokeSharpness": "round", + "seed": 981253221, + "version": 93, + "versionNonce": 1687763429, + "isDeleted": false, + "boundElements": null, + "updated": 1670479569628, + "link": null, + "locked": false, + "text": "Alert", + "fontSize": 28.2551525227051, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 24.318940653381382, + "containerId": null, + "originalText": "Alert" + }, + { + "id": "3AI-TEKsYncfvUoD42C70", + "type": "text", + "x": 1000.2223548386487, + "y": 559.6704041197611, + "width": 248.46158691674464, + "height": 29.23077493138172, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "_vaF9n9a8IjJOUBBNtgNu" + ], + "strokeSharpness": "round", + "seed": 959197003, + "version": 187, + "versionNonce": 941327083, + "isDeleted": false, + "boundElements": null, + "updated": 1670479569628, + "link": null, + "locked": false, + "text": "Alert Text Desription", + "fontSize": 22.942222334529312, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 20.23077493138172, + "containerId": null, + "originalText": "Alert Text Desription" + } + ], + "created": 1670479571783, + "name": "Alert" + }, + { + "id": "8LCQ4rGWlSL2qVUMaaEYj", + "status": "published", + "elements": [ + { + "id": "wljCrPpmqV6UF3lcEEKI0", + "type": "rectangle", + "x": 983.5570594593066, + "y": 335.2130938779404, + "width": 37.45162074812538, + "height": 545.1291464449341, + "angle": 1.5647488869799497, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "seed": 1263338187, + "version": 151, + "versionNonce": 453190187, + "isDeleted": false, + "boundElements": null, + "updated": 1670479436411, + "link": null, + "locked": false + }, + { + "id": "2JMjCJ8PGT-6s-g-fFM-a", + "type": "rectangle", + "x": 1235.821968454621, + "y": 586.1391842094348, + "width": 37.45162074812538, + "height": 40.22581487761602, + "angle": 1.5647488869799497, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "seed": 990766827, + "version": 128, + "versionNonce": 1675078149, + "isDeleted": false, + "boundElements": null, + "updated": 1670479436411, + "link": null, + "locked": false + }, + { + "id": "GkPu0bgsXCEQzYJP5RABf", + "type": "rectangle", + "x": 731.2508241315762, + "y": 589.2764453692502, + "width": 37.45162074812538, + "height": 40.22581487761602, + "angle": 1.5647488869799497, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "seed": 1576092299, + "version": 199, + "versionNonce": 643015883, + "isDeleted": false, + "boundElements": null, + "updated": 1670479436411, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 397, + "versionNonce": 1438523749, + "isDeleted": false, + "id": "raYQ1Dv3FmPZ72nvaqu7u", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 1.5965879461109562, + "x": 737.9446121577234, + "y": 603.5934510213455, + "strokeColor": "#000000", + "backgroundColor": "#000", + "width": 22.41597955723904, + "height": 18.025237460407283, + "seed": 1530985515, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670479436411, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 11.207989778619526, + 15.772082777856372 + ], + [ + 22.41597955723904, + -2.25315468255091 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 485, + "versionNonce": 96129899, + "isDeleted": false, + "id": "SBIxytnSwWxKJRXSzvpZt", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 4.717781918763523, + "x": 1241.2257481387528, + "y": 598.9343653655113, + "strokeColor": "#000000", + "backgroundColor": "#000", + "width": 22.41597955723904, + "height": 18.025237460407283, + "seed": 430759525, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670479436411, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 11.207989778619526, + 15.772082777856372 + ], + [ + 22.41597955723904, + -2.25315468255091 + ], + [ + 0, + 0 + ] + ] + }, + { + "id": "IIxh7sIcKoWg7w-UKnsXC", + "type": "rectangle", + "x": 1190.615892677032, + "y": 582.0985421841827, + "width": 18.459598986547917, + "height": 48.8636443761564, + "angle": 1.5647488869799497, + "strokeColor": "#000000", + "backgroundColor": "#000", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "seed": 1394705419, + "version": 101, + "versionNonce": 1721674949, + "isDeleted": false, + "boundElements": null, + "updated": 1670479436411, + "link": null, + "locked": false + } + ], + "created": 1670479439613, + "name": "Horizontal Scroll " + }, + { + "id": "T5384nxhOHMeLE5Xj2Ko1", + "status": "published", + "elements": [ + { + "id": "wljCrPpmqV6UF3lcEEKI0", + "type": "rectangle", + "x": 983.5143892065894, + "y": 335.25602277039843, + "width": 37.45162074812538, + "height": 545.1291464449341, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "seed": 1263338187, + "version": 74, + "versionNonce": 2139278059, + "isDeleted": false, + "boundElements": null, + "updated": 1670479428098, + "link": null, + "locked": false + }, + { + "id": "2JMjCJ8PGT-6s-g-fFM-a", + "type": "rectangle", + "x": 983.5143892065894, + "y": 335.43816663182065, + "width": 37.45162074812538, + "height": 40.22581487761602, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "seed": 990766827, + "version": 46, + "versionNonce": 1192488261, + "isDeleted": false, + "boundElements": null, + "updated": 1670479428098, + "link": null, + "locked": false + }, + { + "id": "GkPu0bgsXCEQzYJP5RABf", + "type": "rectangle", + "x": 983.6002479705039, + "y": 840.0190567964446, + "width": 37.45162074812538, + "height": 40.22581487761602, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "seed": 1576092299, + "version": 117, + "versionNonce": 1449145739, + "isDeleted": false, + "boundElements": null, + "updated": 1670479428098, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 315, + "versionNonce": 1729100965, + "isDeleted": false, + "id": "raYQ1Dv3FmPZ72nvaqu7u", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0.031839059131006486, + "x": 990.5501491836435, + "y": 854.5078422783215, + "strokeColor": "#000000", + "backgroundColor": "#000", + "width": 22.41597955723904, + "height": 18.025237460407283, + "seed": 1530985515, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670479428098, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 11.207989778619526, + 15.772082777856372 + ], + [ + 22.41597955723904, + -2.25315468255091 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "line", + "version": 403, + "versionNonce": 1333218347, + "isDeleted": false, + "id": "SBIxytnSwWxKJRXSzvpZt", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 3.153033031783572, + "x": 989.522820465324, + "y": 350.4585261179879, + "strokeColor": "#000000", + "backgroundColor": "#000", + "width": 22.41597955723904, + "height": 18.025237460407283, + "seed": 430759525, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670479428098, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 11.207989778619526, + 15.772082777856372 + ], + [ + 22.41597955723904, + -2.25315468255091 + ], + [ + 0, + 0 + ] + ] + }, + { + "id": "IIxh7sIcKoWg7w-UKnsXC", + "type": "rectangle", + "x": 992.9578621625155, + "y": 385.822021101814, + "width": 18.459598986547917, + "height": 48.8636443761564, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#000", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "76wYew9Fk1lQcYPo7XPBV" + ], + "strokeSharpness": "sharp", + "seed": 1394705419, + "version": 24, + "versionNonce": 2140348421, + "isDeleted": false, + "boundElements": null, + "updated": 1670479428098, + "link": null, + "locked": false + } + ], + "created": 1670479430230, + "name": "Vertical Scroll " + }, + { + "id": "IeSfUn1cG-oopAVVjTTPp", + "status": "published", + "elements": [ + { + "id": "JlfnB_ykNz7U-31f8mYkV", + "type": "rectangle", + "x": 814.2413905406195, + "y": 382.29551320972166, + "width": 367.8754217789495, + "height": 43.852699284907885, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "J-ABAu-U7dMd1uMSZljel" + ], + "strokeSharpness": "sharp", + "seed": 2033517477, + "version": 93, + "versionNonce": 1771172837, + "isDeleted": false, + "boundElements": null, + "updated": 1670479231416, + "link": null, + "locked": false + }, + { + "id": "y3Jv3INyRiSypvR3O4gTU", + "type": "rectangle", + "x": 815.4595210763114, + "y": 383.51364374541356, + "width": 218.04536588884753, + "height": 42.63456874921599, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#7950f2", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [ + "J-ABAu-U7dMd1uMSZljel" + ], + "strokeSharpness": "sharp", + "seed": 418179979, + "version": 94, + "versionNonce": 665020651, + "isDeleted": false, + "boundElements": null, + "updated": 1670479231416, + "link": null, + "locked": false + } + ], + "created": 1670479234343, + "name": "Linear Progress Bar" + }, + { + "id": "HGT3BGKkvXclK_eGY7wk2", + "status": "published", + "elements": [ + { + "id": "XnLJFolWBXRZ5n0Vn_q86", + "type": "line", + "x": 643.9288771818381, + "y": 718.600969515237, + "width": 85.39136149270017, + "height": 42.07978670334516, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "CP3ilmVf2tvU7qwwWNS2a" + ], + "strokeSharpness": "round", + "seed": 686565029, + "version": 256, + "versionNonce": 1380207371, + "isDeleted": false, + "boundElements": null, + "updated": 1670479073442, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 72.81514702846026, + -0.9147779718117884 + ], + [ + 74.52844460560048, + 40.25023075972158 + ], + [ + -6.8531903085609756, + 41.16500873153337 + ], + [ + -10.862916887099686, + 5.783139948933467 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + 1.0075701977275457, + -1.007570197727432 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "mZfNM81gkv1okLD9vOVOa", + "type": "ellipse", + "x": 677.1858131826609, + "y": 714.2369681738224, + "width": 51.80388556015671, + "height": 49.10064195085418, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#fff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "CP3ilmVf2tvU7qwwWNS2a" + ], + "strokeSharpness": "sharp", + "seed": 842074635, + "version": 74, + "versionNonce": 457309989, + "isDeleted": false, + "boundElements": null, + "updated": 1670479073442, + "link": null, + "locked": false + } + ], + "created": 1670479075002, + "name": "Toggle Checked" + }, + { + "id": "LMWqZquwvmqB27V3qXni0", + "status": "published", + "elements": [ + { + "type": "rectangle", + "version": 172, + "versionNonce": 1148237643, + "isDeleted": false, + "id": "rPzND8xY4cQY3HQczHhO_", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 289.4324454072679, + "y": 719.1647310325625, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 276.21447426482905, + "height": 43.80493864992883, + "seed": 2098720043, + "groupIds": [ + "XxPSIkHWUaiHFi74GCD6L" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670478955253, + "link": null, + "locked": false + }, + { + "type": "line", + "version": 166, + "versionNonce": 26310373, + "isDeleted": false, + "id": "A2Jr_MzrxW0YGKkYAHkl6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 515.7579617652335, + "y": 719.8948133433946, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 1.216803851386912, + "height": 43.80493864992883, + "seed": 1921584901, + "groupIds": [ + "XxPSIkHWUaiHFi74GCD6L" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670478955254, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 1.216803851386912, + 43.80493864992883 + ] + ] + }, + { + "type": "line", + "version": 332, + "versionNonce": 865179115, + "isDeleted": false, + "id": "ROSlH08hvHAQeo5wKsqrM", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0.07698597171669253, + "x": 527.329954841138, + "y": 744.2110033369908, + "strokeColor": "#000000", + "backgroundColor": "#000", + "width": 23.416075715097236, + "height": 10.901181258666599, + "seed": 563548107, + "groupIds": [ + "XxPSIkHWUaiHFi74GCD6L" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670478955254, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 11.708037857548625, + 9.538533601333274 + ], + [ + 23.416075715097236, + -1.3626476573333246 + ], + [ + 0, + 0 + ] + ] + }, + { + "type": "text", + "version": 178, + "versionNonce": 946557509, + "isDeleted": false, + "id": "N_nv7-dWgsRokJUaPfDE_", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 308.07388041051536, + "y": 731.0894087761542, + "strokeColor": "#000000", + "backgroundColor": "#000", + "width": 42, + "height": 26, + "seed": 855303781, + "groupIds": [ + "XxPSIkHWUaiHFi74GCD6L" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670478955254, + "link": null, + "locked": false, + "fontSize": 20.44230470330012, + "fontFamily": 1, + "text": "256", + "baseline": 18, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "256" + }, + { + "type": "line", + "version": 397, + "versionNonce": 1415422091, + "isDeleted": false, + "id": "N246kVCkR0OrjtJs2Pj2t", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 3.189410396262412, + "x": 526.7889205936648, + "y": 727.8544498670191, + "strokeColor": "#000000", + "backgroundColor": "#000", + "width": 23.416075715097236, + "height": 10.901181258666599, + "seed": 702367083, + "groupIds": [ + "XxPSIkHWUaiHFi74GCD6L" + ], + "strokeSharpness": "sharp", + "boundElements": null, + "updated": 1670478955254, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 11.708037857548625, + 9.538533601333274 + ], + [ + 23.416075715097236, + -1.3626476573333246 + ], + [ + 0, + 0 + ] + ] + } + ], + "created": 1670478959901, + "name": "Number Input" + }, + { + "id": "QiWTB9BNS7HcNaCElgbRn", + "status": "published", + "elements": [ + { + "id": "gTYfHz-fziFrtU5NiU7-O", + "type": "rectangle", + "x": 281.2896825396824, + "y": 613.741703977825, + "width": 276.21447426482905, + "height": 43.80493864992883, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "qRGYopWpK7rCA79_Dg9oV" + ], + "strokeSharpness": "sharp", + "seed": 1500793547, + "version": 154, + "versionNonce": 1520329195, + "isDeleted": false, + "boundElements": null, + "updated": 1670478871412, + "link": null, + "locked": false + }, + { + "id": "x4w66v57VxBxVlUUS9QZP", + "type": "line", + "x": 507.61519889764804, + "y": 614.4717862886571, + "width": 1.216803851386912, + "height": 43.80493864992883, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "qRGYopWpK7rCA79_Dg9oV" + ], + "strokeSharpness": "sharp", + "seed": 1709494859, + "version": 148, + "versionNonce": 1851868741, + "isDeleted": false, + "boundElements": null, + "updated": 1670478871412, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.216803851386912, + 43.80493864992883 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "PdUAOplOr8heuGNdcVAEL", + "type": "text", + "x": 299.9311175429299, + "y": 625.6663817214167, + "width": 91, + "height": 26, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#000", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "qRGYopWpK7rCA79_Dg9oV" + ], + "strokeSharpness": "sharp", + "seed": 48560299, + "version": 165, + "versionNonce": 401665163, + "isDeleted": false, + "boundElements": null, + "updated": 1670478871412, + "link": null, + "locked": false, + "text": "dd/mm/yy", + "fontSize": 20.44230470330012, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18, + "containerId": null, + "originalText": "dd/mm/yy" + }, + { + "id": "0oeT8t9x6Bej5TiYyZxxf", + "type": "line", + "x": 519.597152452057, + "y": 631.7084976087002, + "width": 24.13075290980819, + "height": 22.466563053959348, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "h9RGNA8jYBoc5XlOdhNqE", + "qRGYopWpK7rCA79_Dg9oV" + ], + "strokeSharpness": "sharp", + "seed": 1630041611, + "version": 173, + "versionNonce": 1296709029, + "isDeleted": false, + "boundElements": null, + "updated": 1670478871412, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 20.386325734148294 + ], + [ + 24.13075290980819, + 19.970278270186085 + ], + [ + 20.802373198110512, + -2.0802373198110504 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + 2.5, + -1.2500000000000002 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ZERXq57LcimcN41mtsTgy", + "type": "line", + "x": 525.421816947528, + "y": 630.0443077528515, + "width": 1.6641898558488406, + "height": 22.466563053959348, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "h9RGNA8jYBoc5XlOdhNqE", + "qRGYopWpK7rCA79_Dg9oV" + ], + "strokeSharpness": "sharp", + "seed": 717108709, + "version": 124, + "versionNonce": 735650603, + "isDeleted": false, + "boundElements": null, + "updated": 1670478871412, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.6641898558488406, + 22.466563053959348 + ] + ], + "lastCommittedPoint": [ + 5, + 67.5 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "-IXZIKWtiaf_9Bvbwi1l5", + "type": "line", + "x": 532.9106712988478, + "y": 630.0443077528515, + "width": 2.9123322477354714, + "height": 19.970278270186085, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "h9RGNA8jYBoc5XlOdhNqE", + "qRGYopWpK7rCA79_Dg9oV" + ], + "strokeSharpness": "sharp", + "seed": 184190789, + "version": 119, + "versionNonce": 1948137733, + "isDeleted": false, + "boundElements": null, + "updated": 1670478871412, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 2.9123322477354714, + 19.970278270186085 + ] + ], + "lastCommittedPoint": [ + 8.75, + 60 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "ckMd844TPkwF2TF2WN9NV", + "type": "line", + "x": 519.1811049880948, + "y": 637.9492095681334, + "width": 21.634468126034925, + "height": 1.2481423918866303, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "h9RGNA8jYBoc5XlOdhNqE", + "qRGYopWpK7rCA79_Dg9oV" + ], + "strokeSharpness": "sharp", + "seed": 1386388555, + "version": 117, + "versionNonce": 247563723, + "isDeleted": false, + "boundElements": null, + "updated": 1670478871412, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 21.634468126034925, + -1.2481423918866303 + ] + ], + "lastCommittedPoint": [ + 65, + -3.75 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "CVWkEqiJZVsoYNAkZiXS5", + "type": "line", + "x": 520.0131999160193, + "y": 645.8541113834154, + "width": 22.466563053959348, + "height": 1.2481423918866303, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "h9RGNA8jYBoc5XlOdhNqE", + "qRGYopWpK7rCA79_Dg9oV" + ], + "strokeSharpness": "sharp", + "seed": 556187077, + "version": 121, + "versionNonce": 1549455461, + "isDeleted": false, + "boundElements": null, + "updated": 1670478871412, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 22.466563053959348, + -1.2481423918866303 + ] + ], + "lastCommittedPoint": [ + 67.5, + -3.75 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "j1yekEMqebUwuzxJhz6bJ", + "type": "line", + "x": 518.3490100601703, + "y": 632.5405925366248, + "width": 22.88261051792156, + "height": 8.320949279244203, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#000", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "h9RGNA8jYBoc5XlOdhNqE", + "qRGYopWpK7rCA79_Dg9oV" + ], + "strokeSharpness": "sharp", + "seed": 257195115, + "version": 162, + "versionNonce": 1204065387, + "isDeleted": false, + "boundElements": null, + "updated": 1670478871412, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 22.88261051792156, + -0.41604746396221015 + ], + [ + 21.21842066207272, + -8.320949279244203 + ], + [ + 0, + -6.656759423395362 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + 1.25, + -2.5 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "Y9PxD1L9QYHpSPW2391mu", + "type": "line", + "x": 522.9255321637547, + "y": 628.7961653609648, + "width": 15.809803630563986, + "height": 8.736996743206413, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#000", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "h9RGNA8jYBoc5XlOdhNqE", + "qRGYopWpK7rCA79_Dg9oV" + ], + "strokeSharpness": "sharp", + "seed": 1260651243, + "version": 169, + "versionNonce": 1942846405, + "isDeleted": false, + "boundElements": null, + "updated": 1670478871413, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1.2481423918866303, + -7.488854351319783 + ], + [ + 3.328379711697681, + -7.904901815281993 + ], + [ + 2.9123322477354714, + -2.080237319811051 + ], + [ + 11.649328990941886, + -2.080237319811051 + ], + [ + 9.985139135093043, + -7.488854351319783 + ], + [ + 14.561661238677354, + -7.904901815281993 + ], + [ + 13.729566310752935, + 0.8320949279244203 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + 5, + 1.25 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + } + ], + "created": 1670478873756, + "name": "Date Input" + }, + { + "id": "V9vO3UBdck1KzojtHfuVc", + "status": "published", + "elements": [ + { + "id": "gTYfHz-fziFrtU5NiU7-O", + "type": "rectangle", + "x": 853.7896825396824, + "y": 391.265873015873, + "width": 283.75, + "height": 45, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "aveT88Ye3lzf7dEboBleK" + ], + "strokeSharpness": "sharp", + "seed": 1500793547, + "version": 25, + "versionNonce": 89689259, + "isDeleted": false, + "boundElements": null, + "updated": 1670478726539, + "link": null, + "locked": false + }, + { + "id": "x4w66v57VxBxVlUUS9QZP", + "type": "line", + "x": 1086.2896825396824, + "y": 392.015873015873, + "width": 1.25, + "height": 45, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "aveT88Ye3lzf7dEboBleK" + ], + "strokeSharpness": "sharp", + "seed": 1709494859, + "version": 19, + "versionNonce": 63186821, + "isDeleted": false, + "boundElements": null, + "updated": 1670478726539, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.25, + 45 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "NN-0X2zfwnM_EPeRAOW4B", + "type": "line", + "x": 1098.5396825396824, + "y": 409.015873015873, + "width": 27.5, + "height": 20, + "angle": 0.07698597171669253, + "strokeColor": "#000000", + "backgroundColor": "#000", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "aveT88Ye3lzf7dEboBleK" + ], + "strokeSharpness": "sharp", + "seed": 616202379, + "version": 55, + "versionNonce": 664256331, + "isDeleted": false, + "boundElements": null, + "updated": 1670478726540, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 13.750000000000004, + 17.5 + ], + [ + 27.499999999999996, + -2.5 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": [ + 0, + -2.5 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "PdUAOplOr8heuGNdcVAEL", + "type": "text", + "x": 872.9396825396824, + "y": 403.515873015873, + "width": 112.35000000000002, + "height": 26.25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#000", + "fillStyle": "cross-hatch", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "aveT88Ye3lzf7dEboBleK" + ], + "strokeSharpness": "sharp", + "seed": 48560299, + "version": 27, + "versionNonce": 239005413, + "isDeleted": false, + "boundElements": null, + "updated": 1670478726540, + "link": null, + "locked": false, + "text": "Combo Box", + "fontSize": 21, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18.25, + "containerId": null, + "originalText": "Combo Box" + } + ], + "created": 1670478728317, + "name": "Select" + }, + { + "id": "19HvoN8DWeO_nAfgUrSYn", + "status": "published", + "elements": [ + { + "id": "8mo6Pwf8BSHQ8k-EC1zFA", + "type": "rectangle", + "x": 943.7896825396824, + "y": 508.765873015873, + "width": 32.05562659846547, + "height": 30.32289002557545, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "UvB-9lHd6seH-I9U3ajJV", + "T2IvLH5v1vMrfnxydSyy6" + ], + "strokeSharpness": "sharp", + "seed": 1356230539, + "version": 64, + "versionNonce": 688279179, + "isDeleted": false, + "boundElements": [], + "updated": 1670478596899, + "link": null, + "locked": false + }, + { + "id": "pHKMYdrdmOtjranBmLlpL", + "type": "text", + "x": 991.4399382941581, + "y": 511.35907081870954, + "width": 87.84974424552435, + "height": 24.957313706114874, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "UvB-9lHd6seH-I9U3ajJV", + "T2IvLH5v1vMrfnxydSyy6" + ], + "strokeSharpness": "sharp", + "seed": 773611109, + "version": 85, + "versionNonce": 204731301, + "isDeleted": false, + "boundElements": null, + "updated": 1670478596899, + "link": null, + "locked": false, + "text": "Checkbox", + "fontSize": 19.9658509648919, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 17.957313706114874, + "containerId": null, + "originalText": "Checkbox" + }, + { + "id": "-hiTHjdB3f3E6-AfN3P-J", + "type": "line", + "x": 971.0396825396824, + "y": 516.265873015873, + "width": 21.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "T2IvLH5v1vMrfnxydSyy6" + ], + "strokeSharpness": "sharp", + "seed": 357849195, + "version": 45, + "versionNonce": 2012366123, + "isDeleted": false, + "boundElements": null, + "updated": 1670478596899, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -15, + 13.75 + ], + [ + -21.25, + 3.75 + ] + ], + "lastCommittedPoint": [ + -21.25, + 3.75 + ], + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + } + ], + "created": 1670478599229, + "name": "Checkbox Checked" + }, + { + "id": "jQambTGjMlteML0gOjjX1", + "status": "published", + "elements": [ + { + "id": "8mo6Pwf8BSHQ8k-EC1zFA", + "type": "rectangle", + "x": 943.7896825396824, + "y": 508.765873015873, + "width": 32.05562659846547, + "height": 30.32289002557545, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "UvB-9lHd6seH-I9U3ajJV" + ], + "strokeSharpness": "sharp", + "seed": 1356230539, + "version": 63, + "versionNonce": 288368107, + "isDeleted": false, + "boundElements": [], + "updated": 1670478544787, + "link": null, + "locked": false + }, + { + "id": "pHKMYdrdmOtjranBmLlpL", + "type": "text", + "x": 991.4399382941581, + "y": 511.35907081870954, + "width": 87.84974424552435, + "height": 24.957313706114874, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "UvB-9lHd6seH-I9U3ajJV" + ], + "strokeSharpness": "sharp", + "seed": 773611109, + "version": 84, + "versionNonce": 1975370309, + "isDeleted": false, + "boundElements": null, + "updated": 1670478544787, + "link": null, + "locked": false, + "text": "Checkbox", + "fontSize": 19.9658509648919, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 17.957313706114874, + "containerId": null, + "originalText": "Checkbox" + } + ], + "created": 1670478548817, + "name": "Checkbox Unchecked" + }, + { + "status": "published", + "elements": [ + { + "id": "tIYxcqQGVsF2qlCIk5nqi", + "type": "rectangle", + "x": 818.7896825396824, + "y": 420.265873015873, + "width": 170, + "height": 45, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#ced4da", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "FFiwayr2Q6y0DQrS0jxii" + ], + "strokeSharpness": "sharp", + "seed": 1085546123, + "version": 237, + "versionNonce": 1420898405, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "gYswTrtA5p1yIFUu6RTnD" + } + ], + "updated": 1670478468183, + "link": null, + "locked": false + }, + { + "id": "gYswTrtA5p1yIFUu6RTnD", + "type": "text", + "x": 885.2896825396824, + "y": 430.265873015873, + "width": 37, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "FFiwayr2Q6y0DQrS0jxii" + ], + "strokeSharpness": "sharp", + "seed": 2036733227, + "version": 190, + "versionNonce": 1854157931, + "isDeleted": false, + "boundElements": null, + "updated": 1670478468183, + "link": null, + "locked": false, + "text": "One", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 18, + "containerId": "tIYxcqQGVsF2qlCIk5nqi", + "originalText": "One" + }, + { + "id": "BsC5EY4e099Ds4QDqKyn6", + "type": "rectangle", + "x": 988.7896825396824, + "y": 420.015873015873, + "width": 170, + "height": 45, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "FFiwayr2Q6y0DQrS0jxii" + ], + "strokeSharpness": "sharp", + "seed": 1498805611, + "version": 251, + "versionNonce": 1381489605, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "7jWyZUB0DUYFx6cujKPBr" + } + ], + "updated": 1670478468183, + "link": null, + "locked": false + }, + { + "id": "7jWyZUB0DUYFx6cujKPBr", + "type": "text", + "x": 1053.2896825396824, + "y": 430.015873015873, + "width": 41, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "FFiwayr2Q6y0DQrS0jxii" + ], + "strokeSharpness": "sharp", + "seed": 292163595, + "version": 205, + "versionNonce": 75001611, + "isDeleted": false, + "boundElements": null, + "updated": 1670478468183, + "link": null, + "locked": false, + "text": "Two", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 18, + "containerId": "BsC5EY4e099Ds4QDqKyn6", + "originalText": "Two" + }, + { + "id": "3oWe5FzOseeI-yx_IywN_", + "type": "rectangle", + "x": 1160.0396825396824, + "y": 421.015873015873, + "width": 170, + "height": 45, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "FFiwayr2Q6y0DQrS0jxii" + ], + "strokeSharpness": "sharp", + "seed": 1158957573, + "version": 306, + "versionNonce": 1892817701, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "OLDt2vOCh3lNgAN1OJqUR" + } + ], + "updated": 1670478468183, + "link": null, + "locked": false + }, + { + "id": "OLDt2vOCh3lNgAN1OJqUR", + "type": "text", + "x": 1216.0396825396824, + "y": 431.015873015873, + "width": 58, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [ + "FFiwayr2Q6y0DQrS0jxii" + ], + "strokeSharpness": "sharp", + "seed": 351756037, + "version": 263, + "versionNonce": 1363957163, + "isDeleted": false, + "boundElements": null, + "updated": 1670478468183, + "link": null, + "locked": false, + "text": "Three", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 18, + "containerId": "3oWe5FzOseeI-yx_IywN_", + "originalText": "Three" + } + ], + "id": "1XCaoH5Ruq4V_TZYeqGx3", + "created": 1670478472137, + "name": "Button Group" + }, + { + "id": "AyVIJKUO5bYIZHegnhncd", + "status": "published", + "elements": [ + { + "id": "3oWe5FzOseeI-yx_IywN_", + "type": "rectangle", + "x": 921.2896825396824, + "y": 380.015873015873, + "width": 170, + "height": 45, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1158957573, + "version": 98, + "versionNonce": 1851678155, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "OLDt2vOCh3lNgAN1OJqUR" + } + ], + "updated": 1670478355798, + "link": null, + "locked": false + }, + { + "id": "OLDt2vOCh3lNgAN1OJqUR", + "type": "text", + "x": 970.7896825396824, + "y": 390.015873015873, + "width": 71, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 351756037, + "version": 46, + "versionNonce": 1939306597, + "isDeleted": false, + "boundElements": null, + "updated": 1670478355799, + "link": null, + "locked": false, + "text": "Button", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 18, + "containerId": "3oWe5FzOseeI-yx_IywN_", + "originalText": "Button" + } + ], + "created": 1670478361141, + "name": "Button" + } + ] +} From 356fe294e64d4b6c9630ea70120a270f251181dc Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Sat, 11 Jul 2026 17:21:46 -0600 Subject: [PATCH 6/9] feat: add LibraryCatalog class for parsing excalidraw libraries and stamping items --- apps/server/src/shared/whiteboard-library.ts | 254 +++++++++++++++++++ apps/server/test/whiteboard-library.test.ts | 183 +++++++++++++ 2 files changed, 437 insertions(+) create mode 100644 apps/server/src/shared/whiteboard-library.ts create mode 100644 apps/server/test/whiteboard-library.test.ts diff --git a/apps/server/src/shared/whiteboard-library.ts b/apps/server/src/shared/whiteboard-library.ts new file mode 100644 index 00000000..72ed047b --- /dev/null +++ b/apps/server/src/shared/whiteboard-library.ts @@ -0,0 +1,254 @@ +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { randomUUID } from "node:crypto"; + +type RawElement = Record; + +type LibraryFileItem = { + id: string; + name?: string; + status: string; + elements: RawElement[]; + created: number; +}; + +type Anchors = { + top: [number, number]; + bottom: [number, number]; + left: [number, number]; + right: [number, number]; +}; + +export type CompactLibraryItem = { + name: string; + category: string; + width: number; + height: number; +}; + +export type DetailedLibraryItem = { + name: string; + category: string; + description: string; + width: number; + height: number; + elementCount: number; + anchors: Anchors; +}; + +type CatalogEntry = DetailedLibraryItem & { + elements: RawElement[]; +}; + +export type StampResult = { + elements: RawElement[]; + addedIds: string[]; +}; + +const FILENAME_TO_CATEGORY: Record = { + "flow-chart-symbols": "flowchart", + "architecture-diagram-components": "architecture", + "universal-ui-kit": "wireframe", + "html-input-elements": "ui-input", +}; + +function computeBounds(elements: RawElement[]): { + minX: number; + minY: number; + maxX: number; + maxY: number; + width: number; + height: number; +} { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + for (const el of elements) { + const x = typeof el.x === "number" ? el.x : 0; + const y = typeof el.y === "number" ? el.y : 0; + const w = typeof el.width === "number" ? el.width : 0; + const h = typeof el.height === "number" ? el.height : 0; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x + w); + maxY = Math.max(maxY, y + h); + } + return { + minX, + minY, + maxX, + maxY, + width: Math.round(maxX - minX), + height: Math.round(maxY - minY), + }; +} + +function computeAnchors(width: number, height: number): Anchors { + return { + top: [Math.round(width / 2), 0], + bottom: [Math.round(width / 2), height], + left: [0, Math.round(height / 2)], + right: [width, Math.round(height / 2)], + }; +} + +export class LibraryCatalog { + private items: Map; + + private constructor(items: Map) { + this.items = items; + } + + static async fromDirectory(dir: string): Promise { + const items = new Map(); + const files = await readdir(dir); + for (const file of files) { + if (!file.endsWith(".excalidrawlib")) continue; + const filePath = path.join(dir, file); + const raw = JSON.parse(await readFile(filePath, "utf8")) as { + version?: number; + libraryItems?: LibraryFileItem[]; + }; + if (raw.version !== 2 || !Array.isArray(raw.libraryItems)) continue; + const stem = file.replace(/\.excalidrawlib$/, ""); + const category = FILENAME_TO_CATEGORY[stem] ?? stem; + for (const libItem of raw.libraryItems) { + const name = libItem.name?.trim(); + if ( + !name || + !Array.isArray(libItem.elements) || + libItem.elements.length === 0 + ) + continue; + const bounds = computeBounds(libItem.elements); + const anchors = computeAnchors(bounds.width, bounds.height); + items.set(name.toLowerCase(), { + name, + category, + description: name, + width: bounds.width, + height: bounds.height, + elementCount: libItem.elements.length, + anchors, + elements: libItem.elements, + }); + } + } + return new LibraryCatalog(items); + } + + list(category?: string): CompactLibraryItem[] { + const result: CompactLibraryItem[] = []; + for (const item of this.items.values()) { + if (category && item.category !== category) continue; + result.push({ + name: item.name, + category: item.category, + width: item.width, + height: item.height, + }); + } + return result; + } + + get(name: string): DetailedLibraryItem | null { + const entry = this.items.get(name.toLowerCase()); + if (!entry) return null; + const { elements: _, ...detail } = entry; + return detail; + } + + stamp( + name: string, + x: number, + y: number, + scale: number, + label?: string + ): StampResult { + const entry = this.items.get(name.toLowerCase()); + if (!entry) throw new Error(`Library item "${name}" not found.`); + + const bounds = computeBounds(entry.elements); + const idMap = new Map(); + const addedIds: string[] = []; + + // Build ID mapping first so we can remap bindings + for (const el of entry.elements) { + const oldId = el.id as string; + const newId = randomUUID().slice(0, 8); + idMap.set(oldId, newId); + addedIds.push(newId); + } + + let labelApplied = false; + const elements: RawElement[] = entry.elements.map((el) => { + const cloned: RawElement = { ...el }; + // Assign fresh ID + cloned.id = idMap.get(el.id as string)!; + // Scale and offset coordinates + const origX = typeof el.x === "number" ? el.x : 0; + const origY = typeof el.y === "number" ? el.y : 0; + cloned.x = x + (origX - bounds.minX) * scale; + cloned.y = y + (origY - bounds.minY) * scale; + if (typeof el.width === "number") cloned.width = el.width * scale; + if (typeof el.height === "number") cloned.height = el.height * scale; + + // Scale points for arrows/lines + if (Array.isArray(el.points)) { + cloned.points = (el.points as number[][]).map(([px, py]) => [ + px * scale, + py * scale, + ]); + } + + // Remap containerId + if (typeof el.containerId === "string" && idMap.has(el.containerId)) { + cloned.containerId = idMap.get(el.containerId)!; + } + // Remap frameId + if (typeof el.frameId === "string" && idMap.has(el.frameId)) { + cloned.frameId = idMap.get(el.frameId)!; + } + // Remap boundElements + if (Array.isArray(el.boundElements)) { + cloned.boundElements = ( + el.boundElements as Array<{ id: string; type: string }> + ).map((be) => ({ + ...be, + id: idMap.get(be.id) ?? be.id, + })); + } + // Remap arrow bindings + if (typeof el.startBinding === "object" && el.startBinding !== null) { + const sb = el.startBinding as Record; + if (typeof sb.elementId === "string" && idMap.has(sb.elementId)) { + cloned.startBinding = { ...sb, elementId: idMap.get(sb.elementId)! }; + } + } + if (typeof el.endBinding === "object" && el.endBinding !== null) { + const eb = el.endBinding as Record; + if (typeof eb.elementId === "string" && idMap.has(eb.elementId)) { + cloned.endBinding = { ...eb, elementId: idMap.get(eb.elementId)! }; + } + } + // Remap groupIds + if (Array.isArray(el.groupIds)) { + cloned.groupIds = (el.groupIds as string[]).map( + (gid) => idMap.get(gid) ?? gid + ); + } + + // Replace label text + if (label && !labelApplied && el.type === "text") { + cloned.text = label; + cloned.originalText = label; + labelApplied = true; + } + + return cloned; + }); + + return { elements, addedIds }; + } +} diff --git a/apps/server/test/whiteboard-library.test.ts b/apps/server/test/whiteboard-library.test.ts new file mode 100644 index 00000000..41317fee --- /dev/null +++ b/apps/server/test/whiteboard-library.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, it } from "vitest"; +import path from "node:path"; +import { LibraryCatalog } from "../src/shared/whiteboard-library.js"; + +// Use the actual library files checked in during Task 1 +const LIBRARIES_DIR = path.resolve( + __dirname, + "../../web/public/excalidraw/libraries" +); + +describe("LibraryCatalog", () => { + describe("fromDirectory", () => { + it("loads all library files and builds catalog", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + expect(all.length).toBeGreaterThanOrEqual(40); + // Every item has required fields + for (const item of all) { + expect(item.name).toBeTruthy(); + expect(item.category).toBeTruthy(); + expect(item.width).toBeGreaterThan(0); + expect(item.height).toBeGreaterThan(0); + } + }); + + it("assigns correct categories from filenames", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const categories = new Set(catalog.list().map((i) => i.category)); + expect(categories).toContain("flowchart"); + expect(categories).toContain("architecture"); + expect(categories).toContain("wireframe"); + expect(categories).toContain("ui-input"); + }); + }); + + describe("list", () => { + it("filters by category", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const flowchart = catalog.list("flowchart"); + expect(flowchart.length).toBeGreaterThan(0); + expect(flowchart.every((i) => i.category === "flowchart")).toBe(true); + }); + + it("returns all items when no category specified", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + const flowchart = catalog.list("flowchart"); + expect(all.length).toBeGreaterThan(flowchart.length); + }); + }); + + describe("get", () => { + it("returns detailed item with anchors but no raw elements", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + const detail = catalog.get(all[0].name); + expect(detail).not.toBeNull(); + expect(detail!.anchors).toBeDefined(); + expect(detail!.anchors.top).toHaveLength(2); + expect(detail!.anchors.bottom).toHaveLength(2); + expect(detail!.anchors.left).toHaveLength(2); + expect(detail!.anchors.right).toHaveLength(2); + expect(detail!.elementCount).toBeGreaterThan(0); + expect(detail!.description).toBeTruthy(); + // Elements are internal — never leaked to callers + expect((detail as Record).elements).toBeUndefined(); + }); + + it("is case-insensitive", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + const name = all[0].name; + const lower = catalog.get(name.toLowerCase()); + const upper = catalog.get(name.toUpperCase()); + expect(lower).not.toBeNull(); + expect(upper).not.toBeNull(); + expect(lower!.name).toBe(upper!.name); + }); + + it("returns null for unknown name", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + expect(catalog.get("nonexistent-item-xyz")).toBeNull(); + }); + }); + + describe("stamp", () => { + it("returns elements offset to target position", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + const result = catalog.stamp(all[0].name, 200, 300, 1); + expect(result.elements.length).toBeGreaterThan(0); + // All elements should have fresh string IDs + const ids = result.elements.map( + (e) => (e as Record).id as string + ); + expect(new Set(ids).size).toBe(ids.length); // unique + // Bounding box top-left should be near (200, 300) + const xs = result.elements.map( + (e) => (e as Record).x as number + ); + const ys = result.elements.map( + (e) => (e as Record).y as number + ); + expect(Math.min(...xs)).toBeCloseTo(200, 0); + expect(Math.min(...ys)).toBeCloseTo(300, 0); + }); + + it("scales elements", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + const result1x = catalog.stamp(all[0].name, 0, 0, 1); + const result2x = catalog.stamp(all[0].name, 0, 0, 2); + // At 2x, bounding box should be roughly double + const width1 = Math.max( + ...result1x.elements.map( + (e) => + ((e as Record).x as number) + + ((e as Record).width as number) + ) + ); + const width2 = Math.max( + ...result2x.elements.map( + (e) => + ((e as Record).x as number) + + ((e as Record).width as number) + ) + ); + expect(width2).toBeCloseTo(width1 * 2, -1); + }); + + it("replaces text when label is provided", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + // Use the first item — stamp will apply label to any text element it contains + const textItemName = all[0].name; + const result = catalog.stamp(textItemName, 0, 0, 1, "Custom Label"); + const textEls = result.elements.filter( + (e) => (e as Record).type === "text" + ); + expect(textEls.length).toBeGreaterThan(0); + expect((textEls[0] as Record).text).toBe("Custom Label"); + expect((textEls[0] as Record).originalText).toBe( + "Custom Label" + ); + }); + + it("throws for unknown item", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + expect(() => catalog.stamp("nonexistent", 0, 0, 1)).toThrow(); + }); + + it("maintains internal bindings with remapped IDs", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + // Stamp each item until we find one that has containerId bindings + let boundItemName: string | null = null; + for (const item of all) { + const stamped = catalog.stamp(item.name, 0, 0, 1); + if ( + stamped.elements.some( + (e) => (e as Record).containerId + ) + ) { + boundItemName = item.name; + break; + } + } + if (!boundItemName) return; + const result = catalog.stamp(boundItemName, 0, 0, 1); + const idSet = new Set( + result.elements.map((e) => (e as Record).id as string) + ); + // Every containerId should reference an ID that exists in the stamped set + for (const el of result.elements) { + const containerId = (el as Record) + .containerId as string; + if (containerId) { + expect(idSet.has(containerId)).toBe(true); + } + } + }); + }); +}); From e933c317c100d47673ab5faad41d44e0b9eb457d Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Sat, 11 Jul 2026 17:34:47 -0600 Subject: [PATCH 7/9] feat: add whiteboard_library and whiteboard_insert MCP tools for agent access to shape libraries --- apps/server/src/routes/mcp.ts | 6 + apps/server/src/server.ts | 17 ++ apps/server/src/server/mcp-handlers.ts | 4 + .../src/server/mcp-whiteboard-handlers.ts | 72 +++++++- apps/server/src/shared/mcp/server.ts | 20 +++ .../server/src/shared/mcp/whiteboard-tools.ts | 132 ++++++++++++++ e2e/whiteboard.spec.ts | 165 ++++++++++++++++++ 7 files changed, 415 insertions(+), 1 deletion(-) diff --git a/apps/server/src/routes/mcp.ts b/apps/server/src/routes/mcp.ts index 716c3c78..b471a5a1 100644 --- a/apps/server/src/routes/mcp.ts +++ b/apps/server/src/routes/mcp.ts @@ -52,6 +52,8 @@ type McpRouteDeps = { mcpGetWhiteboard: unknown; mcpUpdateWhiteboard: unknown; mcpClearWhiteboard: unknown; + mcpLibraryList: unknown; + mcpInsertLibraryItem: unknown; mcpSubmitFeedback: unknown; mcpListPersonas: unknown; mcpLaunchPersona: unknown; @@ -213,6 +215,8 @@ export async function registerMcpRoutes( getWhiteboard: deps.mcpGetWhiteboard, updateWhiteboard: deps.mcpUpdateWhiteboard, clearWhiteboard: deps.mcpClearWhiteboard, + libraryList: deps.mcpLibraryList, + insertLibraryItem: deps.mcpInsertLibraryItem, submitFeedback: deps.mcpSubmitFeedback, upsertPin: deps.mcpUpsertPin, deletePin: deps.mcpDeletePin, @@ -304,6 +308,8 @@ export async function registerMcpRoutes( getWhiteboard: deps.mcpGetWhiteboard, updateWhiteboard: deps.mcpUpdateWhiteboard, clearWhiteboard: deps.mcpClearWhiteboard, + libraryList: deps.mcpLibraryList, + insertLibraryItem: deps.mcpInsertLibraryItem, submitFeedback: deps.mcpSubmitFeedback, listPersonas: deps.mcpListPersonas, launchPersona: deps.mcpLaunchPersona, diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index ea57710c..b31375c5 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -1,5 +1,6 @@ import path from "node:path"; import os from "node:os"; +import { fileURLToPath } from "node:url"; import { mkdir, readFile, @@ -36,6 +37,7 @@ import { deleteSetting, getSetting, setSetting } from "./db/settings.js"; import { runCommand } from "./shared/lib/run-command.js"; import { shouldSkipAutomaticMacPathProbe } from "./shared/mac-path-privacy.js"; import { mimeType, resolveMediaDir } from "./shared/media.js"; +import { LibraryCatalog } from "./shared/whiteboard-library.js"; import { handleMcpRequest } from "./shared/mcp/server.js"; import { readReleaseStore, writeReleaseStore } from "./release-store.js"; import { @@ -147,7 +149,19 @@ import { createActivityMonitor } from "./agents/activity-monitor.js"; import { createAutoRenamePrompter } from "./agents/auto-rename-prompter.js"; import { DiffStatsRefresher } from "./agents/diff-stats-refresher.js"; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const config = loadConfig(); +const libraryCatalog = await LibraryCatalog.fromDirectory( + path.resolve( + __dirname, + "..", + "..", + "web", + "public", + "excalidraw", + "libraries" + ) +); const app = Fastify({ logger: true, ...(config.tls && { https: { cert: config.tls.cert, key: config.tls.key } }), @@ -320,6 +334,7 @@ const mcpHandlers = createMcpHandlers({ withStreamFlag, sendAgentPrompt: injectAgentPrompt, appLog: app.log, + libraryCatalog, }); const jobTerminalStatuses = new Set([ "completed", @@ -480,6 +495,8 @@ async function registerRoutes() { mcpGetWhiteboard: mcpHandlers.getWhiteboard, mcpUpdateWhiteboard: mcpHandlers.updateWhiteboard, mcpClearWhiteboard: mcpHandlers.clearWhiteboard, + mcpLibraryList: mcpHandlers.libraryList, + mcpInsertLibraryItem: mcpHandlers.insertLibraryItem, mcpSubmitFeedback: mcpHandlers.submitFeedback, mcpListPersonas: mcpHandlers.listPersonas, mcpLaunchPersona: mcpHandlers.launchPersona, diff --git a/apps/server/src/server/mcp-handlers.ts b/apps/server/src/server/mcp-handlers.ts index 32ce2c5f..16ad5b4c 100644 --- a/apps/server/src/server/mcp-handlers.ts +++ b/apps/server/src/server/mcp-handlers.ts @@ -29,6 +29,7 @@ import { isMediaFile, isTextFile, resolveMediaDir } from "../shared/media.js"; import type { PublishUiEvent, SendAgentPrompt } from "./mcp-handler-types.js"; import { createReviewHandlers } from "./mcp-review-handlers.js"; import { createWhiteboardHandlers } from "./mcp-whiteboard-handlers.js"; +import type { LibraryCatalog } from "../shared/whiteboard-library.js"; const AGENT_LATEST_EVENT_TYPES = [ "working", @@ -64,6 +65,7 @@ type CreateMcpHandlersDeps = { ) => T & { hasStream: boolean }; sendAgentPrompt: SendAgentPrompt; appLog: FastifyBaseLogger; + libraryCatalog?: LibraryCatalog; }; function isAgentLatestEventType(value: unknown): value is AgentLatestEventType { @@ -142,6 +144,7 @@ export function createMcpHandlers(deps: CreateMcpHandlersDeps) { withStreamFlag, sendAgentPrompt, appLog, + libraryCatalog, } = deps; const reviewHandlers = createReviewHandlers({ @@ -157,6 +160,7 @@ export function createMcpHandlers(deps: CreateMcpHandlersDeps) { mediaRoot, agentManager, publishUiEvent, + libraryCatalog, }); return { diff --git a/apps/server/src/server/mcp-whiteboard-handlers.ts b/apps/server/src/server/mcp-whiteboard-handlers.ts index d5ccd369..2cf55307 100644 --- a/apps/server/src/server/mcp-whiteboard-handlers.ts +++ b/apps/server/src/server/mcp-whiteboard-handlers.ts @@ -17,6 +17,11 @@ import { saveWhiteboard, WHITEBOARD_SNAPSHOT_FILENAME, } from "../shared/whiteboard-store.js"; +import type { + CompactLibraryItem, + DetailedLibraryItem, + LibraryCatalog, +} from "../shared/whiteboard-library.js"; import type { PublishUiEvent } from "./mcp-handler-types.js"; type CreateWhiteboardHandlersDeps = { @@ -24,6 +29,7 @@ type CreateWhiteboardHandlersDeps = { mediaRoot: string; agentManager: AgentManager; publishUiEvent: PublishUiEvent; + libraryCatalog?: LibraryCatalog; }; type RawElement = Record; @@ -79,7 +85,8 @@ function mergeElements( } export function createWhiteboardHandlers(deps: CreateWhiteboardHandlersDeps) { - const { pool, mediaRoot, agentManager, publishUiEvent } = deps; + const { pool, mediaRoot, agentManager, publishUiEvent, libraryCatalog } = + deps; return { async getWhiteboard(agentId: string): Promise { @@ -195,5 +202,68 @@ export function createWhiteboardHandlers(deps: CreateWhiteboardHandlersDeps) { "Whiteboard is being edited concurrently; try again in a moment." ); }, + + libraryList( + category?: string, + name?: string + ): CompactLibraryItem[] | DetailedLibraryItem | null { + if (!libraryCatalog) return []; + if (name) return libraryCatalog.get(name); + return libraryCatalog.list(category); + }, + + async insertLibraryItem( + agentId: string, + name: string, + x: number, + y: number, + scale: number, + label?: string + ): Promise { + const agent = await agentManager.getAgent(agentId); + if (!agent) throw new Error("Agent not found."); + if (!libraryCatalog) throw new Error("Library catalog not available."); + + const stamped = libraryCatalog.stamp(name, x, y, scale, label); + + for (let attempt = 0; attempt < 3; attempt++) { + const row = await loadWhiteboard(pool, agentId); + const baseVersion = row ? Number(row.version) : 0; + const existing = row ? row.scene.elements : []; + + const merged = mergeElements(existing, stamped.elements, []); + + if (merged.elements.length > MAX_ELEMENTS) { + throw new Error(`Board is full (max ${MAX_ELEMENTS} elements).`); + } + + const saved = await saveWhiteboard( + pool, + agentId, + { elements: merged.elements }, + baseVersion, + "agent" + ); + if (saved) { + publishUiEvent({ + type: "whiteboard.changed", + agentId, + version: saved.version, + source: "agent", + }); + return { + version: saved.version, + elementCount: merged.elements.length, + addedIds: stamped.addedIds, + updatedIds: [], + deletedIds: [], + elements: simplifyElements(merged.elements), + }; + } + } + throw new Error( + "Whiteboard is being edited concurrently; try again in a moment." + ); + }, }; } diff --git a/apps/server/src/shared/mcp/server.ts b/apps/server/src/shared/mcp/server.ts index 3267f0f1..63ed8346 100644 --- a/apps/server/src/shared/mcp/server.ts +++ b/apps/server/src/shared/mcp/server.ts @@ -18,6 +18,10 @@ import type { WhiteboardGetResult, WhiteboardUpdateResult, } from "../whiteboard.js"; +import type { + CompactLibraryItem, + DetailedLibraryItem, +} from "../whiteboard-library.js"; import { registerPersonaInteractionTools, type LaunchPersonaAgentType, @@ -112,6 +116,8 @@ const AGENT_TOOLS = new Set([ "whiteboard_get", "whiteboard_update", "whiteboard_clear", + "whiteboard_library", + "whiteboard_insert", "brain_get_object", "brain_store_object", "brain_list_objects", @@ -421,6 +427,18 @@ export type McpRequestContext = { deleteIds: string[] ) => Promise; clearWhiteboard?: (agentId: string) => Promise; + libraryList?: ( + category?: string, + name?: string + ) => CompactLibraryItem[] | DetailedLibraryItem | null; + insertLibraryItem?: ( + agentId: string, + name: string, + x: number, + y: number, + scale: number, + label?: string + ) => Promise; getParentContext?: (parentAgentId: string) => Promise; getRecheckContext?: (agentId: string) => Promise; updateReviewStatus?: ( @@ -584,6 +602,8 @@ async function createDispatchMcpServer( getWhiteboard: context.getWhiteboard, updateWhiteboard: context.updateWhiteboard, clearWhiteboard: context.clearWhiteboard, + libraryList: context.libraryList, + insertLibraryItem: context.insertLibraryItem, }); } diff --git a/apps/server/src/shared/mcp/whiteboard-tools.ts b/apps/server/src/shared/mcp/whiteboard-tools.ts index 36e4e6ee..ff9595cf 100644 --- a/apps/server/src/shared/mcp/whiteboard-tools.ts +++ b/apps/server/src/shared/mcp/whiteboard-tools.ts @@ -6,6 +6,10 @@ import type { WhiteboardGetResult, WhiteboardUpdateResult, } from "../whiteboard.js"; +import type { + CompactLibraryItem, + DetailedLibraryItem, +} from "../whiteboard-library.js"; export type WhiteboardToolsContext = { agentId: string; @@ -16,6 +20,18 @@ export type WhiteboardToolsContext = { deleteIds: string[] ) => Promise; clearWhiteboard?: (agentId: string) => Promise; + libraryList?: ( + category?: string, + name?: string + ) => CompactLibraryItem[] | DetailedLibraryItem | null; + insertLibraryItem?: ( + agentId: string, + name: string, + x: number, + y: number, + scale: number, + label?: string + ) => Promise; }; // ── Excalidraw element format cheat sheet ────────────────────────────── @@ -347,4 +363,120 @@ export function registerWhiteboardTools( } ); } + + if (allowed.has("whiteboard_library") && context.libraryList) { + const libraryList = context.libraryList; + + server.registerTool( + "whiteboard_library", + { + description: + "Browse the library of pre-built shapes and icons available for the whiteboard. " + + "Call with no arguments for a compact list of all items (name, category, dimensions). " + + "Call with `name` to get full detail (anchors, element count) for layout planning. " + + "Call with `category` to filter: 'flowchart', 'architecture', 'wireframe', 'ui-input'.", + inputSchema: { + category: z + .string() + .optional() + .describe( + "Filter by category: 'flowchart', 'architecture', 'wireframe', 'ui-input'" + ), + name: z + .string() + .optional() + .describe( + "Get full detail for a specific item by name (case-insensitive). " + + "Takes priority over category." + ), + }, + }, + async (args) => { + try { + const result = libraryList(args.category, args.name); + if (args.name && result === null) { + return { + content: [ + { + type: "text", + text: `Library item "${args.name}" not found. Call whiteboard_library with no arguments to see all available items.`, + }, + ], + isError: true, + }; + } + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + }; + } catch (error) { + return toToolError(error); + } + } + ); + } + + if (allowed.has("whiteboard_insert") && context.insertLibraryItem) { + const agentId = context.agentId; + const insertLibraryItem = context.insertLibraryItem; + + server.registerTool( + "whiteboard_insert", + { + description: + "Place a pre-built library shape onto the whiteboard at a given position. " + + "Use `whiteboard_library` first to browse available items and get their dimensions " + + "for layout planning. Items are scaled and positioned, with optional text label replacement.", + inputSchema: { + name: z + .string() + .describe( + "Library item name (case-insensitive). Use whiteboard_library to see available names." + ), + x: z + .number() + .describe("X coordinate for the item's top-left corner."), + y: z + .number() + .describe("Y coordinate for the item's top-left corner."), + scale: z + .number() + .min(0.1) + .max(10) + .default(1) + .describe( + "Size multiplier. 1 = original size, 0.5 = half, 2 = double." + ), + label: z + .string() + .optional() + .describe( + "Replace the item's primary text label (e.g. place a 'Server' icon but label it 'Auth Service')." + ), + }, + }, + async (args) => { + try { + const result = await insertLibraryItem( + agentId, + args.name, + args.x, + args.y, + args.scale, + args.label + ); + const summary = { + ok: true, + version: result.version, + elementCount: result.elementCount, + addedIds: result.addedIds, + }; + return { + content: [{ type: "text", text: JSON.stringify(summary, null, 2) }], + }; + } catch (error) { + return toToolError(error); + } + } + ); + } } diff --git a/e2e/whiteboard.spec.ts b/e2e/whiteboard.spec.ts index 8ef4fdb0..3a13f87f 100644 --- a/e2e/whiteboard.spec.ts +++ b/e2e/whiteboard.spec.ts @@ -228,4 +228,169 @@ test.describe("Whiteboard", () => { }; expect(data2.scene.elements).toEqual([]); }); + + test("whiteboard MCP tool: whiteboard_library returns catalog", async ({ + request, + }) => { + const agent = await createAgentViaAPI(request, { + name: `e2e-agent-wb-lib-${Date.now()}`, + }); + + // List all items + const listJson = await callMcpTool( + request, + agent.id, + "whiteboard_library", + {} + ); + const listResult = listJson.result as { + content?: Array<{ text?: string }>; + }; + const items = JSON.parse(listResult?.content?.[0]?.text ?? "[]") as Array<{ + name: string; + category: string; + width: number; + height: number; + }>; + expect(items.length).toBeGreaterThanOrEqual(40); + expect(items[0].name).toBeTruthy(); + expect(items[0].width).toBeGreaterThan(0); + + // Filter by category + const archJson = await callMcpTool( + request, + agent.id, + "whiteboard_library", + { + category: "architecture", + } + ); + const archResult = archJson.result as { + content?: Array<{ text?: string }>; + }; + const archItems = JSON.parse( + archResult?.content?.[0]?.text ?? "[]" + ) as Array<{ category: string }>; + expect(archItems.length).toBeGreaterThan(0); + expect(archItems.every((i) => i.category === "architecture")).toBe(true); + + // Get detail for a specific item + const detailJson = await callMcpTool( + request, + agent.id, + "whiteboard_library", + { name: items[0].name } + ); + const detailResult = detailJson.result as { + content?: Array<{ text?: string }>; + }; + const detail = JSON.parse(detailResult?.content?.[0]?.text ?? "{}") as { + name: string; + anchors: { + top: number[]; + bottom: number[]; + left: number[]; + right: number[]; + }; + elementCount: number; + }; + expect(detail.name).toBe(items[0].name); + expect(detail.anchors.top).toHaveLength(2); + expect(detail.elementCount).toBeGreaterThan(0); + }); + + test("whiteboard MCP tool: whiteboard_insert places library item", async ({ + request, + }) => { + const agent = await createAgentViaAPI(request, { + name: `e2e-agent-wb-insert-${Date.now()}`, + }); + + // First get available items + const listJson = await callMcpTool( + request, + agent.id, + "whiteboard_library", + {} + ); + const listResult = listJson.result as { + content?: Array<{ text?: string }>; + }; + const items = JSON.parse(listResult?.content?.[0]?.text ?? "[]") as Array<{ + name: string; + }>; + const itemName = items[0].name; + + // Insert the item + const insertJson = await callMcpTool( + request, + agent.id, + "whiteboard_insert", + { + name: itemName, + x: 100, + y: 200, + scale: 1, + } + ); + const insertResult = insertJson.result as { + content?: Array<{ text?: string }>; + }; + const insertData = JSON.parse(insertResult?.content?.[0]?.text ?? "{}") as { + ok: boolean; + addedIds: string[]; + elementCount: number; + }; + expect(insertData.ok).toBe(true); + expect(insertData.addedIds.length).toBeGreaterThan(0); + expect(insertData.elementCount).toBeGreaterThan(0); + + // Verify the elements are on the board via REST + const getRes = await request.get(`/api/v1/agents/${agent.id}/whiteboard`, { + headers: AUTH_HEADER, + }); + const boardData = (await getRes.json()) as { + scene: { elements: Array<{ id: string }> }; + }; + expect(boardData.scene.elements.length).toBe(insertData.elementCount); + }); + + test("whiteboard MCP tool: whiteboard_insert with label and scale", async ({ + request, + }) => { + const agent = await createAgentViaAPI(request, { + name: `e2e-agent-wb-insert-label-${Date.now()}`, + }); + + // Find an item with text elements via library detail + const listJson = await callMcpTool( + request, + agent.id, + "whiteboard_library", + {} + ); + const items = JSON.parse( + (listJson.result as { content?: Array<{ text?: string }> })?.content?.[0] + ?.text ?? "[]" + ) as Array<{ name: string }>; + + // Insert at 2x scale with a custom label + const insertJson = await callMcpTool( + request, + agent.id, + "whiteboard_insert", + { + name: items[0].name, + x: 50, + y: 50, + scale: 2, + label: "My Service", + } + ); + const insertData = JSON.parse( + (insertJson.result as { content?: Array<{ text?: string }> }) + ?.content?.[0]?.text ?? "{}" + ) as { ok: boolean }; + expect(insertData.ok).toBe(true); + }); }); From f7ab6ab5fca854b2201d443eeed8937f52fa5bac Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Sat, 11 Jul 2026 17:45:35 -0600 Subject: [PATCH 8/9] feat: lazy-load excalidraw shape libraries on whiteboard mount Add a useEffect in WhiteboardCanvas that fetches all four .excalidrawlib files in parallel and loads them into Excalidraw via updateLibrary(). Includes proper cleanup via cancelled flag to prevent state updates after unmount. Library loading is best-effort and fails silently. Co-Authored-By: Claude Opus 4.6 --- .../web/src/components/app/whiteboard-tab.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/apps/web/src/components/app/whiteboard-tab.tsx b/apps/web/src/components/app/whiteboard-tab.tsx index 7186f803..bd96d29d 100644 --- a/apps/web/src/components/app/whiteboard-tab.tsx +++ b/apps/web/src/components/app/whiteboard-tab.tsx @@ -31,6 +31,13 @@ window.EXCALIDRAW_ASSET_PATH = "/excalidraw/"; const SAVE_DEBOUNCE_MS = 1000; const SNAPSHOT_DEBOUNCE_MS = 4000; +const LIBRARY_FILES = [ + "/excalidraw/libraries/flow-chart-symbols.excalidrawlib", + "/excalidraw/libraries/architecture-diagram-components.excalidrawlib", + "/excalidraw/libraries/universal-ui-kit.excalidrawlib", + "/excalidraw/libraries/html-input-elements.excalidrawlib", +]; + type WhiteboardTabProps = { agentId: string; visible: boolean; @@ -74,6 +81,34 @@ function WhiteboardCanvas({ const { theme } = useTheme(); const [excalidrawAPI, setExcalidrawAPI] = useState(null); + + useEffect(() => { + if (!excalidrawAPI) return; + let cancelled = false; + void (async () => { + try { + const responses = await Promise.all( + LIBRARY_FILES.map((url) => fetch(url)) + ); + const blobs = await Promise.all(responses.map((r) => r.blob())); + if (cancelled) return; + for (const blob of blobs) { + await excalidrawAPI.updateLibrary({ + libraryItems: blob, + merge: true, + defaultStatus: "published", + openLibraryMenu: false, + }); + } + } catch { + // Library loading is best-effort — whiteboard works fine without them + } + })(); + return () => { + cancelled = true; + }; + }, [excalidrawAPI]); + const [boardEmpty, setBoardEmpty] = useState( initial.scene.elements.length === 0 ); From 72db728b8ec254e3abbb9becdcda07e120ab1980 Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Sat, 11 Jul 2026 17:54:31 -0600 Subject: [PATCH 9/9] fix: resolve duplicate names, groupId remapping, and startup crash in whiteboard library - Key the library catalog Map by category:name instead of name alone so duplicate item names across different .excalidrawlib files (e.g. "Button" in both html-input-elements and universal-ui-kit) no longer silently collide. get()/stamp() accept a bare name (first match) or category:name for an exact lookup. - Remap groupIds through a dedicated groupIdMap in stamp() instead of reusing the element idMap, so stamping the same item twice no longer shares groupIds between the two copies. - Wrap the LibraryCatalog.fromDirectory() startup call in try/catch so a missing/malformed library directory logs a warning and disables library support instead of crashing the server; all downstream consumers already treat libraryCatalog as optional. --- apps/server/src/server.ts | 32 +++++---- apps/server/src/shared/whiteboard-library.ts | 43 ++++++++++-- apps/server/test/whiteboard-library.test.ts | 69 ++++++++++++++++++++ 3 files changed, 128 insertions(+), 16 deletions(-) diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index b31375c5..97ab3ce3 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -151,17 +151,27 @@ import { DiffStatsRefresher } from "./agents/diff-stats-refresher.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const config = loadConfig(); -const libraryCatalog = await LibraryCatalog.fromDirectory( - path.resolve( - __dirname, - "..", - "..", - "web", - "public", - "excalidraw", - "libraries" - ) -); +let libraryCatalog: LibraryCatalog | undefined; +try { + libraryCatalog = await LibraryCatalog.fromDirectory( + path.resolve( + __dirname, + "..", + "..", + "web", + "public", + "excalidraw", + "libraries" + ) + ); +} catch (err) { + console.warn( + `Failed to load whiteboard library catalog, continuing without library support: ${ + err instanceof Error ? err.message : String(err) + }` + ); + libraryCatalog = undefined; +} const app = Fastify({ logger: true, ...(config.tls && { https: { cert: config.tls.cert, key: config.tls.key } }), diff --git a/apps/server/src/shared/whiteboard-library.ts b/apps/server/src/shared/whiteboard-library.ts index 72ed047b..c74fa477 100644 --- a/apps/server/src/shared/whiteboard-library.ts +++ b/apps/server/src/shared/whiteboard-library.ts @@ -123,7 +123,8 @@ export class LibraryCatalog { continue; const bounds = computeBounds(libItem.elements); const anchors = computeAnchors(bounds.width, bounds.height); - items.set(name.toLowerCase(), { + const key = `${category}:${name.toLowerCase()}`; + items.set(key, { name, category, description: name, @@ -138,6 +139,27 @@ export class LibraryCatalog { return new LibraryCatalog(items); } + /** + * Resolve a lookup string to a catalog entry. + * Accepts either a bare name (first case-insensitive match across all + * categories, in insertion order) or a "category:name" pair for an + * exact, unambiguous lookup. + */ + private find(input: string): CatalogEntry | null { + const separatorIndex = input.indexOf(":"); + if (separatorIndex !== -1) { + const category = input.slice(0, separatorIndex).toLowerCase(); + const name = input.slice(separatorIndex + 1).toLowerCase(); + const exact = this.items.get(`${category}:${name}`); + if (exact) return exact; + } + const target = input.toLowerCase(); + for (const entry of this.items.values()) { + if (entry.name.toLowerCase() === target) return entry; + } + return null; + } + list(category?: string): CompactLibraryItem[] { const result: CompactLibraryItem[] = []; for (const item of this.items.values()) { @@ -153,7 +175,7 @@ export class LibraryCatalog { } get(name: string): DetailedLibraryItem | null { - const entry = this.items.get(name.toLowerCase()); + const entry = this.find(name); if (!entry) return null; const { elements: _, ...detail } = entry; return detail; @@ -166,11 +188,12 @@ export class LibraryCatalog { scale: number, label?: string ): StampResult { - const entry = this.items.get(name.toLowerCase()); + const entry = this.find(name); if (!entry) throw new Error(`Library item "${name}" not found.`); const bounds = computeBounds(entry.elements); const idMap = new Map(); + const groupIdMap = new Map(); const addedIds: string[] = []; // Build ID mapping first so we can remap bindings @@ -179,6 +202,16 @@ export class LibraryCatalog { const newId = randomUUID().slice(0, 8); idMap.set(oldId, newId); addedIds.push(newId); + // Build a separate mapping for groupIds — they are a distinct + // namespace from element IDs, so each unique groupId needs its own + // fresh ID per stamp to avoid cross-linking separate stamps. + if (Array.isArray(el.groupIds)) { + for (const gid of el.groupIds as string[]) { + if (!groupIdMap.has(gid)) { + groupIdMap.set(gid, randomUUID().slice(0, 8)); + } + } + } } let labelApplied = false; @@ -232,10 +265,10 @@ export class LibraryCatalog { cloned.endBinding = { ...eb, elementId: idMap.get(eb.elementId)! }; } } - // Remap groupIds + // Remap groupIds (separate namespace from element IDs) if (Array.isArray(el.groupIds)) { cloned.groupIds = (el.groupIds as string[]).map( - (gid) => idMap.get(gid) ?? gid + (gid) => groupIdMap.get(gid) ?? gid ); } diff --git a/apps/server/test/whiteboard-library.test.ts b/apps/server/test/whiteboard-library.test.ts index 41317fee..310b4a3f 100644 --- a/apps/server/test/whiteboard-library.test.ts +++ b/apps/server/test/whiteboard-library.test.ts @@ -23,6 +23,32 @@ describe("LibraryCatalog", () => { } }); + it("keeps items with duplicate names across different libraries", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + // "Button" and "Select" exist in both html-input-elements + // (category ui-input) and universal-ui-kit (category wireframe). + // Both copies must survive — the catalog must not collapse them + // into a single entry keyed only by lowercase name. + expect(all.length).toBeGreaterThanOrEqual(54); + const buttons = all.filter((i) => i.name.toLowerCase() === "button"); + const selects = all.filter((i) => i.name.toLowerCase() === "select"); + expect(buttons.length).toBe(2); + expect(selects.length).toBe(2); + expect(new Set(buttons.map((i) => i.category)).size).toBe(2); + expect(new Set(selects.map((i) => i.category)).size).toBe(2); + }); + + it("makes every duplicate-named item retrievable via category:name", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const wireframeButton = catalog.get("wireframe:button"); + const uiInputButton = catalog.get("ui-input:button"); + expect(wireframeButton).not.toBeNull(); + expect(uiInputButton).not.toBeNull(); + expect(wireframeButton!.category).toBe("wireframe"); + expect(uiInputButton!.category).toBe("ui-input"); + }); + it("assigns correct categories from filenames", async () => { const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); const categories = new Set(catalog.list().map((i) => i.category)); @@ -149,6 +175,49 @@ describe("LibraryCatalog", () => { expect(() => catalog.stamp("nonexistent", 0, 0, 1)).toThrow(); }); + it("assigns fresh, non-overlapping groupIds on each stamp", async () => { + const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); + const all = catalog.list(); + // Find an item whose elements actually carry groupIds so the + // assertion exercises the remapping logic. + let groupedItemName: string | null = null; + for (const item of all) { + const stamped = catalog.stamp(item.name, 0, 0, 1); + const hasGroups = stamped.elements.some( + (e) => + Array.isArray((e as Record).groupIds) && + ((e as Record).groupIds as unknown[]).length > 0 + ); + if (hasGroups) { + groupedItemName = item.name; + break; + } + } + if (!groupedItemName) return; + + const first = catalog.stamp(groupedItemName, 0, 0, 1); + const second = catalog.stamp(groupedItemName, 0, 0, 1); + + const firstGroupIds = new Set( + first.elements.flatMap( + (e) => ((e as Record).groupIds as string[]) ?? [] + ) + ); + const secondGroupIds = new Set( + second.elements.flatMap( + (e) => ((e as Record).groupIds as string[]) ?? [] + ) + ); + + expect(firstGroupIds.size).toBeGreaterThan(0); + expect(secondGroupIds.size).toBeGreaterThan(0); + // No groupId should be shared between the two independent stamps — + // otherwise selecting one copy in the UI would also select the other. + for (const gid of firstGroupIds) { + expect(secondGroupIds.has(gid)).toBe(false); + } + }); + it("maintains internal bindings with remapped IDs", async () => { const catalog = await LibraryCatalog.fromDirectory(LIBRARIES_DIR); const all = catalog.list();