excalidrop 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +411 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +533 -0
- package/dist/cli.js.map +1 -0
- package/dist/frontend/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
- package/dist/frontend/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
- package/dist/frontend/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
- package/dist/frontend/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
- package/dist/frontend/assets/Tableau10-B-NsZVaP.js +1 -0
- package/dist/frontend/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
- package/dist/frontend/assets/advancedFormat-bKUmsig-.js +1 -0
- package/dist/frontend/assets/ar-SA-G6X2FPQ2-C7K_UR3P.js +10 -0
- package/dist/frontend/assets/arc-BI_FJWE7.js +1 -0
- package/dist/frontend/assets/array-BKyUJesY.js +1 -0
- package/dist/frontend/assets/az-AZ-76LH7QW2-PkhuRuSr.js +1 -0
- package/dist/frontend/assets/band-dPffDWoQ.js +1 -0
- package/dist/frontend/assets/bg-BG-XCXSNQG7-CgLRSYTl.js +5 -0
- package/dist/frontend/assets/blockDiagram-38ab4fdb-BFdBRYSn.js +118 -0
- package/dist/frontend/assets/blockDiagram-68f4deed-Ynbqr7bQ.js +118 -0
- package/dist/frontend/assets/bn-BD-2XOGV67Q-CEvkU0mz.js +5 -0
- package/dist/frontend/assets/c4Diagram-15b5d702-Dm7F1Pn_.js +10 -0
- package/dist/frontend/assets/c4Diagram-3d4e48cf-COkK-e67.js +10 -0
- package/dist/frontend/assets/ca-ES-6MX7JW3Y-DzP6q9bE.js +8 -0
- package/dist/frontend/assets/channel-BqCiG5n3.js +1 -0
- package/dist/frontend/assets/classDiagram-70f12bd4-CRzulwvH.js +2 -0
- package/dist/frontend/assets/classDiagram-d40c83e7-DNbA-n-a.js +2 -0
- package/dist/frontend/assets/classDiagram-v2-d5a6b087-l-GglywH.js +2 -0
- package/dist/frontend/assets/classDiagram-v2-f2320105-B_TY1zoc.js +2 -0
- package/dist/frontend/assets/clone-B6Jqcrku.js +1 -0
- package/dist/frontend/assets/createText-2e5e7dd3-DB_k52Pn.js +5 -0
- package/dist/frontend/assets/createText-d213de94-D-G49giw.js +5 -0
- package/dist/frontend/assets/cs-CZ-2BRQDIVT-Bf4TOzpR.js +11 -0
- package/dist/frontend/assets/cytoscape-cose-bilkent-DQYypj0P.js +331 -0
- package/dist/frontend/assets/da-DK-5WZEPLOC-jO-5kyXb.js +5 -0
- package/dist/frontend/assets/de-DE-XR44H4JA-BkNXWOVh.js +8 -0
- package/dist/frontend/assets/directory-open-01563666-DWU9wJ6I.js +1 -0
- package/dist/frontend/assets/directory-open-4ed118d0-BzWybGaI.js +1 -0
- package/dist/frontend/assets/edges-332bd1c7-CVvMtkTa.js +4 -0
- package/dist/frontend/assets/edges-e0da2a9e-DQBAvC8-.js +4 -0
- package/dist/frontend/assets/el-GR-BZB4AONW-Dv7Y9hMk.js +10 -0
- package/dist/frontend/assets/elk.bundled-YVS3PsMH.js +26 -0
- package/dist/frontend/assets/erDiagram-880f2ed8-BjBeR38f.js +51 -0
- package/dist/frontend/assets/erDiagram-9861fffd-B2Bi3R45.js +51 -0
- package/dist/frontend/assets/es-ES-U4NZUMDT-CWhs0SQY.js +9 -0
- package/dist/frontend/assets/eu-ES-A7QVB2H4-BfkFwc3e.js +11 -0
- package/dist/frontend/assets/fa-IR-HGAKTJCU-BgWzSj46.js +8 -0
- package/dist/frontend/assets/fi-FI-Z5N7JZ37-CUyvkum-.js +6 -0
- package/dist/frontend/assets/file-open-002ab408-DIuFHtCF.js +1 -0
- package/dist/frontend/assets/file-open-7c801643-684qeFg4.js +1 -0
- package/dist/frontend/assets/file-save-3189631c-x92wctJd.js +1 -0
- package/dist/frontend/assets/file-save-745eba88-Bb9F9Kg7.js +1 -0
- package/dist/frontend/assets/flowDb-7c981674-BSZyyVBf.js +10 -0
- package/dist/frontend/assets/flowDb-956e92f1-BC3-CLkF.js +10 -0
- package/dist/frontend/assets/flowDiagram-66a62f08-H57DAvmM.js +4 -0
- package/dist/frontend/assets/flowDiagram-cbd28bf7-Bw4Z2nBD.js +4 -0
- package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-CBjbm_oB.js +1 -0
- package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CoQufPp2.js +1 -0
- package/dist/frontend/assets/flowchart-elk-definition-36e2d292-1kZNdf3X.js +114 -0
- package/dist/frontend/assets/flowchart-elk-definition-4a651766-DDhG2Esi.js +114 -0
- package/dist/frontend/assets/fr-FR-RHASNOE6-7xzoRwbd.js +9 -0
- package/dist/frontend/assets/ganttDiagram-04f9e578-CtUSsXCz.js +257 -0
- package/dist/frontend/assets/ganttDiagram-c361ad54-K42AklFY.js +257 -0
- package/dist/frontend/assets/gitGraphDiagram-21fc4d3e-uW6XA6Y6.js +70 -0
- package/dist/frontend/assets/gitGraphDiagram-72cf32ee-CCNbhnBg.js +70 -0
- package/dist/frontend/assets/gl-ES-HMX3MZ6V-CXuDDwNP.js +10 -0
- package/dist/frontend/assets/graph-CYOmcSYN.js +1 -0
- package/dist/frontend/assets/graph-o42dOfI7.js +1 -0
- package/dist/frontend/assets/he-IL-6SHJWFNN-O9dzh9EM.js +10 -0
- package/dist/frontend/assets/hi-IN-IWLTKZ5I-DlrOXhWs.js +4 -0
- package/dist/frontend/assets/hu-HU-A5ZG7DT2-C3eNg-uI.js +7 -0
- package/dist/frontend/assets/id-ID-SAP4L64H-6iM4pI5c.js +10 -0
- package/dist/frontend/assets/image-blob-reduce.esm-B6b2_-a4.js +7 -0
- package/dist/frontend/assets/index-3862675e-DrvLhypC.js +1 -0
- package/dist/frontend/assets/index-6079d271-BgVTRqg2.js +1 -0
- package/dist/frontend/assets/index-BRA5OktX.css +1 -0
- package/dist/frontend/assets/index-D-cr7jpJ.js +349 -0
- package/dist/frontend/assets/index-DGmpr33w.js +3 -0
- package/dist/frontend/assets/index-HkUXslJy.js +87 -0
- package/dist/frontend/assets/infoDiagram-4a4f5b27-ZxmAcaW1.js +7 -0
- package/dist/frontend/assets/infoDiagram-f8f76790-Bn-PhiFG.js +7 -0
- package/dist/frontend/assets/init-Gi6I4Gst.js +1 -0
- package/dist/frontend/assets/it-IT-JPQ66NNP-B26NQzGp.js +11 -0
- package/dist/frontend/assets/ja-JP-DBVTYXUO-CB0fvnYJ.js +8 -0
- package/dist/frontend/assets/journeyDiagram-29694f62-ggDKs06f.js +139 -0
- package/dist/frontend/assets/journeyDiagram-49397b02-pdDS-n-z.js +139 -0
- package/dist/frontend/assets/kaa-6HZHGXH3-YNdBdcwJ.js +1 -0
- package/dist/frontend/assets/kab-KAB-ZGHBKWFO-D86pwZO0.js +8 -0
- package/dist/frontend/assets/katex-ChWnQ-fc.js +261 -0
- package/dist/frontend/assets/kk-KZ-P5N5QNE5-CpG5oheM.js +1 -0
- package/dist/frontend/assets/km-KH-HSX4SM5Z-DYOZ3Azw.js +11 -0
- package/dist/frontend/assets/ko-KR-MTYHY66A-BHorm6lC.js +9 -0
- package/dist/frontend/assets/ku-TR-6OUDTVRD-CM9RgEHv.js +9 -0
- package/dist/frontend/assets/layout-CCtkxrAf.js +1 -0
- package/dist/frontend/assets/layout-DW9Qf-At.js +1 -0
- package/dist/frontend/assets/line-DvZfgg9P.js +1 -0
- package/dist/frontend/assets/linear-FwFrGRfa.js +1 -0
- package/dist/frontend/assets/lt-LT-XHIRWOB4-DvriKXBj.js +3 -0
- package/dist/frontend/assets/lv-LV-5QDEKY6T-DzQpwk87.js +7 -0
- package/dist/frontend/assets/mindmap-definition-ac74a2e8-DsRo8f2w.js +95 -0
- package/dist/frontend/assets/mindmap-definition-fc14e90a-dQ9reluI.js +95 -0
- package/dist/frontend/assets/mr-IN-CRQNXWMA-C7RzxxP5.js +13 -0
- package/dist/frontend/assets/my-MM-5M5IBNSE-B-RRuy29.js +1 -0
- package/dist/frontend/assets/nb-NO-T6EIAALU-B47x16Ik.js +10 -0
- package/dist/frontend/assets/nl-NL-IS3SIHDZ-BxAKoQne.js +8 -0
- package/dist/frontend/assets/nn-NO-6E72VCQL-dfpbEDSZ.js +8 -0
- package/dist/frontend/assets/oc-FR-POXYY2M6-gv6geBjV.js +8 -0
- package/dist/frontend/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/frontend/assets/pa-IN-N4M65BXN-CY30-qH7.js +4 -0
- package/dist/frontend/assets/path-CbwjOpE9.js +1 -0
- package/dist/frontend/assets/pica-DWYGHLQV.js +7 -0
- package/dist/frontend/assets/pie-CDiWh1rj.js +1 -0
- package/dist/frontend/assets/pieDiagram-421022e6-Bgy-duyf.js +35 -0
- package/dist/frontend/assets/pieDiagram-8a3498a8-DH7FsbPK.js +35 -0
- package/dist/frontend/assets/pl-PL-T2D74RX3-TrHugsIA.js +9 -0
- package/dist/frontend/assets/pt-BR-5N22H2LF-CJxdFp9c.js +9 -0
- package/dist/frontend/assets/pt-PT-UZXXM6DQ-CB-1q6YJ.js +9 -0
- package/dist/frontend/assets/quadrantDiagram-0957ecba-DcwYI1oi.js +7 -0
- package/dist/frontend/assets/quadrantDiagram-120e2f19-CvHU5a9v.js +7 -0
- package/dist/frontend/assets/requirementDiagram-23d650b8-tdagN8HP.js +52 -0
- package/dist/frontend/assets/requirementDiagram-deff3bca-BMGM-fSI.js +52 -0
- package/dist/frontend/assets/ro-RO-JPDTUUEW-DQq9DBOd.js +11 -0
- package/dist/frontend/assets/roundRect-0PYZxl1G.js +1 -0
- package/dist/frontend/assets/ru-RU-B4JR7IUQ-CWX32rfh.js +9 -0
- package/dist/frontend/assets/sankeyDiagram-04a897e0-3i6zWpY2.js +8 -0
- package/dist/frontend/assets/sankeyDiagram-23345273-ncj1Ol7S.js +8 -0
- package/dist/frontend/assets/sankeyLinkHorizontal-DgqkLiUE.js +1 -0
- package/dist/frontend/assets/selectAll-DJwesbwT.js +1 -0
- package/dist/frontend/assets/sequenceDiagram-17ac3bff-ChyKKRZA.js +122 -0
- package/dist/frontend/assets/sequenceDiagram-704730f1-D57_wP9k.js +122 -0
- package/dist/frontend/assets/si-LK-N5RQ5JYF-Lkc5ZxyG.js +1 -0
- package/dist/frontend/assets/sk-SK-C5VTKIMK-DyvYXjqZ.js +6 -0
- package/dist/frontend/assets/sl-SI-NN7IZMDC-D_MvP9Rh.js +6 -0
- package/dist/frontend/assets/stateDiagram-587899a1-OjFTnFLS.js +1 -0
- package/dist/frontend/assets/stateDiagram-9c5f0230-BzU36S5C.js +1 -0
- package/dist/frontend/assets/stateDiagram-v2-51a3dcff-CfX1qo7U.js +1 -0
- package/dist/frontend/assets/stateDiagram-v2-d93cdb3a-BH7Ud4sU.js +1 -0
- package/dist/frontend/assets/styles-2ab5d517-CA8x9Nh5.js +116 -0
- package/dist/frontend/assets/styles-5f03d8d2-BnO2pLhV.js +160 -0
- package/dist/frontend/assets/styles-6aaf32cf-COnot1ea.js +207 -0
- package/dist/frontend/assets/styles-9a916d00-C3QkBgTF.js +160 -0
- package/dist/frontend/assets/styles-c10674c1-CPAG2_rr.js +116 -0
- package/dist/frontend/assets/styles-edf9a4b0-B8Q9fyu0.js +207 -0
- package/dist/frontend/assets/subset-shared.chunk.js +84 -0
- package/dist/frontend/assets/subset-worker.chunk.js +1 -0
- package/dist/frontend/assets/sv-SE-XGPEYMSR-t6voGQSI.js +10 -0
- package/dist/frontend/assets/svgDrawCommon-08f97a94-CdT21LWf.js +1 -0
- package/dist/frontend/assets/svgDrawCommon-3ba9043b-D1MU7jAE.js +1 -0
- package/dist/frontend/assets/ta-IN-2NMHFXQM-7VmfDDyV.js +9 -0
- package/dist/frontend/assets/th-TH-HPSO5L25-B5rLEY9P.js +2 -0
- package/dist/frontend/assets/timeline-definition-7e6b55e7-BIcL44O_.js +61 -0
- package/dist/frontend/assets/timeline-definition-85554ec2-TgrYYwpU.js +61 -0
- package/dist/frontend/assets/tr-TR-DEFEU3FU-0CgOHXTn.js +7 -0
- package/dist/frontend/assets/uk-UA-QMV73CPH-CzSFsIsx.js +6 -0
- package/dist/frontend/assets/union-CnQAJu0l.js +1 -0
- package/dist/frontend/assets/vi-VN-M7AON7JQ-Bhx3rYU9.js +5 -0
- package/dist/frontend/assets/xml-BOsq7VnW.js +1 -0
- package/dist/frontend/assets/xychartDiagram-b6496bcd-BMKDop6v.js +7 -0
- package/dist/frontend/assets/xychartDiagram-e933f94c-DAWithrN.js +7 -0
- package/dist/frontend/assets/zh-CN-LNUGB5OW-Br52_wH4.js +10 -0
- package/dist/frontend/assets/zh-HK-E62DVLB3-Cj6mnIQ9.js +1 -0
- package/dist/frontend/assets/zh-TW-RAJ6MFWO-yLEsVBbC.js +9 -0
- package/dist/frontend/assets/zipObject-BeXj5PVd.js +1 -0
- package/dist/frontend/index.html +339 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2305 -0
- package/dist/index.js.map +1 -0
- package/dist/remote.d.ts +4 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +133 -0
- package/dist/remote.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1217 -0
- package/dist/server.js.map +1 -0
- package/dist/target.d.ts +41 -0
- package/dist/target.d.ts.map +1 -0
- package/dist/target.js +244 -0
- package/dist/target.js.map +1 -0
- package/dist/types.d.ts +228 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +53 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/auth.d.ts +15 -0
- package/dist/utils/auth.d.ts.map +1 -0
- package/dist/utils/auth.js +68 -0
- package/dist/utils/auth.js.map +1 -0
- package/dist/utils/github.d.ts +26 -0
- package/dist/utils/github.d.ts.map +1 -0
- package/dist/utils/github.js +111 -0
- package/dist/utils/github.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +25 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/r2store.d.ts +19 -0
- package/dist/utils/r2store.d.ts.map +1 -0
- package/dist/utils/r2store.js +75 -0
- package/dist/utils/r2store.js.map +1 -0
- package/frontend/index.html +338 -0
- package/frontend/src/App.tsx +1091 -0
- package/frontend/src/main.tsx +15 -0
- package/frontend/src/utils/ghSync.ts +171 -0
- package/frontend/src/utils/mermaidConverter.ts +49 -0
- package/frontend/src/utils/webmcp.ts +285 -0
- package/package.json +101 -0
- package/scripts/check-local-bind.mjs +164 -0
- package/scripts/postinstall.mjs +23 -0
- package/scripts/publish-pages.sh +39 -0
- package/scripts/seed-r2.mjs +9 -0
- package/skills/excalidraw-skill/SKILL.md +303 -0
- package/skills/excalidraw-skill/evals/evals.json +23 -0
- package/skills/excalidraw-skill/references/cheatsheet.md +142 -0
- package/skills/excalidraw-skill/scripts/clear-canvas.cjs +38 -0
- package/skills/excalidraw-skill/scripts/create-element.cjs +68 -0
- package/skills/excalidraw-skill/scripts/delete-element.cjs +48 -0
- package/skills/excalidraw-skill/scripts/export-elements.cjs +53 -0
- package/skills/excalidraw-skill/scripts/healthcheck.cjs +35 -0
- package/skills/excalidraw-skill/scripts/import-elements.cjs +81 -0
- package/skills/excalidraw-skill/scripts/update-element.cjs +70 -0
- package/src/cli.ts +559 -0
- package/src/index.ts +2557 -0
- package/src/remote.ts +96 -0
- package/src/server.ts +1349 -0
- package/src/target.ts +208 -0
- package/src/types.ts +338 -0
- package/src/utils/auth.ts +61 -0
- package/src/utils/github.ts +93 -0
- package/src/utils/logger.ts +35 -0
- package/src/utils/r2store.ts +76 -0
- package/tsconfig.json +50 -0
- package/vite.config.js +37 -0
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Excalidraw,
|
|
4
|
+
MainMenu,
|
|
5
|
+
convertToExcalidrawElements,
|
|
6
|
+
CaptureUpdateAction,
|
|
7
|
+
ExcalidrawImperativeAPI,
|
|
8
|
+
exportToBlob,
|
|
9
|
+
exportToSvg
|
|
10
|
+
} from '@excalidraw/excalidraw'
|
|
11
|
+
import type { ExcalidrawElement, NonDeleted, NonDeletedExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
|
|
12
|
+
import { convertMermaidToExcalidraw, DEFAULT_MERMAID_CONFIG } from './utils/mermaidConverter'
|
|
13
|
+
import type { MermaidConfig } from '@excalidraw/mermaid-to-excalidraw'
|
|
14
|
+
import { APP_INSTALL_URL } from './utils/ghSync'
|
|
15
|
+
import { registerCanvasWebMCP } from './utils/webmcp'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
type ExcalidrawAPIRefValue = ExcalidrawImperativeAPI;
|
|
19
|
+
|
|
20
|
+
interface ServerElement {
|
|
21
|
+
id: string;
|
|
22
|
+
type: string;
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width?: number;
|
|
26
|
+
height?: number;
|
|
27
|
+
backgroundColor?: string;
|
|
28
|
+
strokeColor?: string;
|
|
29
|
+
strokeWidth?: number;
|
|
30
|
+
roughness?: number;
|
|
31
|
+
opacity?: number;
|
|
32
|
+
text?: string;
|
|
33
|
+
fontSize?: number;
|
|
34
|
+
fontFamily?: string | number;
|
|
35
|
+
label?: {
|
|
36
|
+
text: string;
|
|
37
|
+
};
|
|
38
|
+
createdAt?: string;
|
|
39
|
+
updatedAt?: string;
|
|
40
|
+
version?: number;
|
|
41
|
+
syncedAt?: string;
|
|
42
|
+
source?: string;
|
|
43
|
+
syncTimestamp?: string;
|
|
44
|
+
boundElements?: any[] | null;
|
|
45
|
+
containerId?: string | null;
|
|
46
|
+
locked?: boolean;
|
|
47
|
+
|
|
48
|
+
start?: { id: string };
|
|
49
|
+
end?: { id: string };
|
|
50
|
+
strokeStyle?: string;
|
|
51
|
+
endArrowhead?: string;
|
|
52
|
+
startArrowhead?: string;
|
|
53
|
+
|
|
54
|
+
fileId?: string;
|
|
55
|
+
status?: string;
|
|
56
|
+
scale?: [number, number];
|
|
57
|
+
angle?: number;
|
|
58
|
+
link?: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface WebSocketMessage {
|
|
62
|
+
type: string;
|
|
63
|
+
element?: ServerElement;
|
|
64
|
+
elements?: ServerElement[];
|
|
65
|
+
elementId?: string;
|
|
66
|
+
count?: number;
|
|
67
|
+
timestamp?: string;
|
|
68
|
+
source?: string;
|
|
69
|
+
mermaidDiagram?: string;
|
|
70
|
+
config?: MermaidConfig;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ApiResponse {
|
|
74
|
+
success: boolean;
|
|
75
|
+
elements?: ServerElement[];
|
|
76
|
+
element?: ServerElement;
|
|
77
|
+
files?: Record<string, unknown>;
|
|
78
|
+
count?: number;
|
|
79
|
+
error?: string;
|
|
80
|
+
message?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type SyncStatus = 'idle' | 'syncing' | 'success' | 'error';
|
|
84
|
+
const authHeaders = (): Record<string, string> => {
|
|
85
|
+
const t = localStorage.getItem('excalidrop_token');
|
|
86
|
+
return t ? { Authorization: `Bearer ${t}` } : {};
|
|
87
|
+
};
|
|
88
|
+
const AUTO_SYNC_DEBOUNCE_MS = 1200;
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
const cleanElementForExcalidraw = (element: ServerElement): Partial<ExcalidrawElement> => {
|
|
92
|
+
const {
|
|
93
|
+
createdAt,
|
|
94
|
+
updatedAt,
|
|
95
|
+
version,
|
|
96
|
+
syncedAt,
|
|
97
|
+
source,
|
|
98
|
+
syncTimestamp,
|
|
99
|
+
...cleanElement
|
|
100
|
+
} = element;
|
|
101
|
+
return cleanElement;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const validateAndFixBindings = (elements: Partial<ExcalidrawElement>[]): Partial<ExcalidrawElement>[] => {
|
|
105
|
+
const elementMap = new Map(elements.map(el => [el.id!, el]));
|
|
106
|
+
|
|
107
|
+
return elements.map(element => {
|
|
108
|
+
const fixedElement = { ...element };
|
|
109
|
+
|
|
110
|
+
if (fixedElement.boundElements) {
|
|
111
|
+
if (Array.isArray(fixedElement.boundElements)) {
|
|
112
|
+
fixedElement.boundElements = fixedElement.boundElements.filter((binding: any) => {
|
|
113
|
+
if (!binding || typeof binding !== 'object') return false;
|
|
114
|
+
if (!binding.id || !binding.type) return false;
|
|
115
|
+
|
|
116
|
+
const referencedElement = elementMap.get(binding.id);
|
|
117
|
+
if (!referencedElement) return false;
|
|
118
|
+
|
|
119
|
+
if (!['text', 'arrow'].includes(binding.type)) return false;
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (fixedElement.boundElements.length === 0) {
|
|
125
|
+
fixedElement.boundElements = null;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
fixedElement.boundElements = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (fixedElement.containerId) {
|
|
133
|
+
const containerElement = elementMap.get(fixedElement.containerId);
|
|
134
|
+
if (!containerElement) {
|
|
135
|
+
fixedElement.containerId = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return fixedElement;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const isImageElement = (element: Partial<ExcalidrawElement>): boolean => {
|
|
144
|
+
return element.type === 'image'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const isShapeContainerType = (type: string | undefined): boolean => {
|
|
148
|
+
return type === 'rectangle' || type === 'ellipse' || type === 'diamond'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const recenterBoundShapeTextElements = (
|
|
152
|
+
elements: Partial<ExcalidrawElement>[]
|
|
153
|
+
): Partial<ExcalidrawElement>[] => {
|
|
154
|
+
const elementMap = new Map(elements.map((el) => [el.id, el]))
|
|
155
|
+
|
|
156
|
+
return elements.map((element) => {
|
|
157
|
+
if (element.type !== 'text' || !element.containerId) {
|
|
158
|
+
return element
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const textElement = element as ExcalidrawElement & { type: 'text'; containerId: string; autoResize?: boolean }
|
|
162
|
+
const container = elementMap.get(textElement.containerId) as (ExcalidrawElement & { x: number; y: number; width: number; height: number }) | undefined
|
|
163
|
+
if (!container || !isShapeContainerType(container.type)) {
|
|
164
|
+
return element
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (textElement.autoResize === false) {
|
|
168
|
+
return element
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
typeof container.x !== 'number' ||
|
|
173
|
+
typeof container.y !== 'number' ||
|
|
174
|
+
typeof container.width !== 'number' ||
|
|
175
|
+
typeof container.height !== 'number' ||
|
|
176
|
+
typeof textElement.width !== 'number' ||
|
|
177
|
+
typeof textElement.height !== 'number'
|
|
178
|
+
) {
|
|
179
|
+
return element
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
...element,
|
|
184
|
+
x: container.x + (container.width - textElement.width) / 2,
|
|
185
|
+
y: container.y + (container.height - textElement.height) / 2,
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const normalizeImageElement = (element: Partial<ExcalidrawElement>): Partial<ExcalidrawElement> => {
|
|
191
|
+
const img = element as any
|
|
192
|
+
return {
|
|
193
|
+
...img,
|
|
194
|
+
angle: img.angle || 0,
|
|
195
|
+
strokeColor: img.strokeColor || 'transparent',
|
|
196
|
+
backgroundColor: img.backgroundColor || 'transparent',
|
|
197
|
+
fillStyle: img.fillStyle || 'solid',
|
|
198
|
+
strokeWidth: img.strokeWidth || 1,
|
|
199
|
+
strokeStyle: img.strokeStyle || 'solid',
|
|
200
|
+
roughness: img.roughness ?? 0,
|
|
201
|
+
opacity: img.opacity ?? 100,
|
|
202
|
+
groupIds: img.groupIds || [],
|
|
203
|
+
roundness: null,
|
|
204
|
+
seed: img.seed || Math.floor(Math.random() * 1000000),
|
|
205
|
+
version: img.version || 1,
|
|
206
|
+
versionNonce: img.versionNonce || Math.floor(Math.random() * 1000000),
|
|
207
|
+
isDeleted: img.isDeleted ?? false,
|
|
208
|
+
boundElements: img.boundElements || null,
|
|
209
|
+
link: img.link || null,
|
|
210
|
+
locked: img.locked || false,
|
|
211
|
+
status: img.status || 'saved',
|
|
212
|
+
fileId: img.fileId,
|
|
213
|
+
scale: img.scale || [1, 1],
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const restoreBindings = (
|
|
218
|
+
convertedElements: readonly any[],
|
|
219
|
+
originalElements: Partial<ExcalidrawElement>[]
|
|
220
|
+
): any[] => {
|
|
221
|
+
const originalMap = new Map<string, any>();
|
|
222
|
+
for (const el of originalElements) {
|
|
223
|
+
if (el.id) originalMap.set(el.id, el);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return convertedElements.map((el: any) => {
|
|
227
|
+
const orig = originalMap.get(el.id);
|
|
228
|
+
if (!orig) return el;
|
|
229
|
+
|
|
230
|
+
const patched = { ...el };
|
|
231
|
+
|
|
232
|
+
if (orig.startBinding && !el.startBinding) {
|
|
233
|
+
patched.startBinding = orig.startBinding;
|
|
234
|
+
}
|
|
235
|
+
if (orig.endBinding && !el.endBinding) {
|
|
236
|
+
patched.endBinding = orig.endBinding;
|
|
237
|
+
}
|
|
238
|
+
if (orig.boundElements && (!el.boundElements || el.boundElements.length === 0)) {
|
|
239
|
+
patched.boundElements = orig.boundElements;
|
|
240
|
+
}
|
|
241
|
+
if (orig.elbowed !== undefined && el.elbowed === undefined) {
|
|
242
|
+
patched.elbowed = orig.elbowed;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return patched;
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const convertElementsPreservingImageProps = (
|
|
250
|
+
elements: Partial<ExcalidrawElement>[]
|
|
251
|
+
): Partial<ExcalidrawElement>[] => {
|
|
252
|
+
if (elements.length === 0) return []
|
|
253
|
+
|
|
254
|
+
const validatedElements = validateAndFixBindings(elements)
|
|
255
|
+
const imageElements = validatedElements.filter(isImageElement).map(normalizeImageElement)
|
|
256
|
+
const nonImageElements = validatedElements.filter(el => !isImageElement(el))
|
|
257
|
+
const convertedNonImageElements = convertToExcalidrawElements(nonImageElements as any, { regenerateIds: false })
|
|
258
|
+
const restoredNonImageElements = restoreBindings(convertedNonImageElements, nonImageElements)
|
|
259
|
+
return recenterBoundShapeTextElements([...restoredNonImageElements, ...imageElements])
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function App(): JSX.Element {
|
|
263
|
+
const [excalidrawAPI, setExcalidrawAPI] = useState<ExcalidrawAPIRefValue | null>(null)
|
|
264
|
+
const excalidrawAPIRef = useRef<ExcalidrawAPIRefValue | null>(null)
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
excalidrawAPIRef.current = excalidrawAPI
|
|
267
|
+
}, [excalidrawAPI])
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
if (!excalidrawAPI) return
|
|
270
|
+
const handle = registerCanvasWebMCP(() => excalidrawAPIRef.current)
|
|
271
|
+
return () => handle.cleanup()
|
|
272
|
+
}, [excalidrawAPI])
|
|
273
|
+
const [isConnected, setIsConnected] = useState<boolean>(false)
|
|
274
|
+
const websocketRef = useRef<WebSocket | null>(null)
|
|
275
|
+
|
|
276
|
+
const [syncStatus, setSyncStatus] = useState<SyncStatus>('idle')
|
|
277
|
+
const [lastSyncTime, setLastSyncTime] = useState<Date | null>(null)
|
|
278
|
+
const autoSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
279
|
+
const syncInFlightRef = useRef<boolean>(false)
|
|
280
|
+
const suppressAutoSyncCountRef = useRef<number>(0)
|
|
281
|
+
const userInteractedRef = useRef<boolean>(false)
|
|
282
|
+
|
|
283
|
+
const applySceneUpdateWithoutAutoSync = (
|
|
284
|
+
scene: Parameters<ExcalidrawImperativeAPI['updateScene']>[0]
|
|
285
|
+
): void => {
|
|
286
|
+
suppressAutoSyncCountRef.current += 1
|
|
287
|
+
api.updateScene(scene)
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
suppressAutoSyncCountRef.current = Math.max(0, suppressAutoSyncCountRef.current - 1)
|
|
290
|
+
}, 0)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
return () => {
|
|
295
|
+
if (autoSyncTimerRef.current) {
|
|
296
|
+
clearTimeout(autoSyncTimerRef.current)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}, [])
|
|
300
|
+
|
|
301
|
+
const [serverMode, setServerMode] = useState<boolean>(false)
|
|
302
|
+
const [ghToken, setGhToken] = useState<string | null>(null)
|
|
303
|
+
const [access, setAccess] = useState<'unknown' | 'editor' | 'viewer' | 'denied'>('unknown')
|
|
304
|
+
const [ghLogin, setGhLogin] = useState<string>('')
|
|
305
|
+
const [ghRepo, setGhRepo] = useState<import('./utils/ghSync').RepoRef | null>(null)
|
|
306
|
+
const [accessDetail, setAccessDetail] = useState<import('./utils/ghSync').AccessDetail>(null)
|
|
307
|
+
const [ghDirty, setGhDirty] = useState<boolean>(false)
|
|
308
|
+
const ghShaRef = useRef<string | null>(null)
|
|
309
|
+
const ghPushInFlightRef = useRef<boolean>(false)
|
|
310
|
+
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
fetch('/api/health', { headers: authHeaders() }).then(r => {
|
|
313
|
+
if (r.ok) { setServerMode(true); connectWebSocket() }
|
|
314
|
+
else { initStaticMode() }
|
|
315
|
+
}).catch(() => initStaticMode())
|
|
316
|
+
return () => {
|
|
317
|
+
if (websocketRef.current) {
|
|
318
|
+
websocketRef.current.close()
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
322
|
+
}, [])
|
|
323
|
+
|
|
324
|
+
const initStaticMode = async (): Promise<void> => {
|
|
325
|
+
const gh = await import('./utils/ghSync')
|
|
326
|
+
setGhRepo(gh.detectRepo())
|
|
327
|
+
setGhToken(gh.getToken())
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (serverMode || !ghRepo) return
|
|
332
|
+
if (!ghToken) { setAccess('denied'); setGhLogin(''); setAccessDetail(null); return }
|
|
333
|
+
setAccess('unknown')
|
|
334
|
+
void (async () => {
|
|
335
|
+
try {
|
|
336
|
+
const gh = await import('./utils/ghSync')
|
|
337
|
+
const { access: a, login, detail } = await gh.checkAccess(ghRepo, ghToken)
|
|
338
|
+
setAccess(a)
|
|
339
|
+
setGhLogin(login)
|
|
340
|
+
setAccessDetail(detail)
|
|
341
|
+
} catch {
|
|
342
|
+
setAccess('denied')
|
|
343
|
+
setGhLogin('')
|
|
344
|
+
setAccessDetail(null)
|
|
345
|
+
}
|
|
346
|
+
})()
|
|
347
|
+
}, [serverMode, ghToken, ghRepo])
|
|
348
|
+
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (serverMode) return
|
|
351
|
+
const id = setInterval(() => { void pushToGitHub(false) }, 10000)
|
|
352
|
+
const flush = () => { void pushToGitHub(true) }
|
|
353
|
+
const guard = (e: BeforeUnloadEvent) => {
|
|
354
|
+
if (ghDirty || ghPushInFlightRef.current) { e.preventDefault() }
|
|
355
|
+
}
|
|
356
|
+
document.addEventListener('visibilitychange', () => { if (document.hidden) flush() })
|
|
357
|
+
window.addEventListener('pagehide', flush)
|
|
358
|
+
window.addEventListener('beforeunload', guard)
|
|
359
|
+
return () => {
|
|
360
|
+
clearInterval(id)
|
|
361
|
+
document.removeEventListener('visibilitychange', () => { })
|
|
362
|
+
window.removeEventListener('pagehide', flush)
|
|
363
|
+
window.removeEventListener('beforeunload', guard)
|
|
364
|
+
}
|
|
365
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
366
|
+
}, [serverMode, ghToken, ghRepo, ghDirty, excalidrawAPI])
|
|
367
|
+
|
|
368
|
+
const pushToGitHub = async (isClosing: boolean): Promise<void> => {
|
|
369
|
+
if (serverMode || !excalidrawAPI || !ghToken || !ghRepo) return
|
|
370
|
+
if (access !== 'editor') return
|
|
371
|
+
if ((!ghDirty && !isClosing) || ghPushInFlightRef.current) return
|
|
372
|
+
ghPushInFlightRef.current = true
|
|
373
|
+
if (!isClosing) setSyncStatus('syncing')
|
|
374
|
+
try {
|
|
375
|
+
const gh = await import('./utils/ghSync')
|
|
376
|
+
const elements = excalidrawAPI.getSceneElements().filter(el => !el.isDeleted)
|
|
377
|
+
const files = excalidrawAPI.getFiles()
|
|
378
|
+
if (isClosing) {
|
|
379
|
+
// Best-effort synchronous flush on close
|
|
380
|
+
const cur = await fetch(`https://api.github.com/repos/${ghRepo.owner}/${ghRepo.repo}/contents/canvas.excalidraw?ref=${ghRepo.branch}`, {
|
|
381
|
+
headers: { Authorization: `Bearer ${ghToken}`, Accept: 'application/vnd.github+json' },
|
|
382
|
+
}).then(r => r.json()).catch(() => null)
|
|
383
|
+
await fetch(`https://api.github.com/repos/${ghRepo.owner}/${ghRepo.repo}/contents/canvas.excalidraw`, {
|
|
384
|
+
method: 'PUT',
|
|
385
|
+
keepalive: true,
|
|
386
|
+
headers: { Authorization: `Bearer ${ghToken}`, Accept: 'application/vnd.github+json', 'Content-Type': 'application/json' },
|
|
387
|
+
body: JSON.stringify({
|
|
388
|
+
message: `excalidrop: force-save on close (${elements.length} elements)`,
|
|
389
|
+
content: btoa(unescape(encodeURIComponent(JSON.stringify({ type: 'excalidraw', version: 2, source: 'excalidrop', elements }, null, 2)))),
|
|
390
|
+
branch: ghRepo.branch,
|
|
391
|
+
...(cur?.sha ? { sha: cur.sha } : {}),
|
|
392
|
+
}),
|
|
393
|
+
}).catch(() => { })
|
|
394
|
+
} else {
|
|
395
|
+
ghShaRef.current = await gh.pushScene(ghRepo, ghToken, { elements: elements as any, files: files as any })
|
|
396
|
+
setLastSyncTime(new Date())
|
|
397
|
+
setSyncStatus('success')
|
|
398
|
+
setTimeout(() => setSyncStatus('idle'), 2000)
|
|
399
|
+
}
|
|
400
|
+
setGhDirty(false)
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.error('GitHub push failed:', error)
|
|
403
|
+
if (String((error as Error).message).includes('403') && ghRepo && ghToken) {
|
|
404
|
+
try {
|
|
405
|
+
const gh = await import('./utils/ghSync')
|
|
406
|
+
const { access: a, detail } = await gh.checkAccess(ghRepo, ghToken)
|
|
407
|
+
setAccess(a)
|
|
408
|
+
setAccessDetail(detail)
|
|
409
|
+
} catch { setAccess('viewer') }
|
|
410
|
+
}
|
|
411
|
+
if (!isClosing) setSyncStatus('error')
|
|
412
|
+
} finally {
|
|
413
|
+
ghPushInFlightRef.current = false
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
if (excalidrawAPI) {
|
|
419
|
+
loadExistingElements()
|
|
420
|
+
|
|
421
|
+
if (!isConnected) {
|
|
422
|
+
connectWebSocket()
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}, [excalidrawAPI, isConnected])
|
|
426
|
+
|
|
427
|
+
const loadExistingElements = async (): Promise<void> => {
|
|
428
|
+
try {
|
|
429
|
+
const response = await fetch('/api/elements', { headers: authHeaders() }).catch(() => null)
|
|
430
|
+
if (response?.ok) {
|
|
431
|
+
const result: ApiResponse = await response.json()
|
|
432
|
+
if (result.success && result.elements && result.elements.length > 0) {
|
|
433
|
+
const cleanedElements = result.elements.map(cleanElementForExcalidraw)
|
|
434
|
+
const convertedElements = convertElementsPreservingImageProps(cleanedElements)
|
|
435
|
+
if (excalidrawAPI) {
|
|
436
|
+
applySceneUpdateWithoutAutoSync(excalidrawAPI, {
|
|
437
|
+
elements: convertedElements,
|
|
438
|
+
captureUpdate: CaptureUpdateAction.NEVER
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const filesResponse = await fetch('/api/files', { headers: authHeaders() })
|
|
443
|
+
if (filesResponse.ok) {
|
|
444
|
+
const filesResult = await filesResponse.json() as ApiResponse
|
|
445
|
+
if (filesResult.files) {
|
|
446
|
+
excalidrawAPI?.addFiles(Object.values(filesResult.files))
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
const gh = await import('./utils/ghSync')
|
|
452
|
+
const scene = await gh.loadStaticScene()
|
|
453
|
+
if (scene.elements.length > 0 && excalidrawAPI) {
|
|
454
|
+
const converted = convertElementsPreservingImageProps(scene.elements.map(cleanElementForExcalidraw))
|
|
455
|
+
applySceneUpdateWithoutAutoSync(excalidrawAPI, { elements: converted, captureUpdate: CaptureUpdateAction.NEVER })
|
|
456
|
+
}
|
|
457
|
+
if (scene.files) excalidrawAPI?.addFiles(Object.values(scene.files))
|
|
458
|
+
} catch (error) {
|
|
459
|
+
console.error('Error loading existing elements:', error)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const connectWebSocket = (): void => {
|
|
464
|
+
if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) {
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
469
|
+
const wsUrl = `${protocol}//${window.location.host}`
|
|
470
|
+
|
|
471
|
+
websocketRef.current = new WebSocket(wsUrl)
|
|
472
|
+
|
|
473
|
+
websocketRef.current.onopen = () => {
|
|
474
|
+
setIsConnected(true)
|
|
475
|
+
|
|
476
|
+
if (excalidrawAPI) {
|
|
477
|
+
setTimeout(loadExistingElements, 100)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
websocketRef.current.onmessage = (event: MessageEvent) => {
|
|
482
|
+
try {
|
|
483
|
+
const data: WebSocketMessage = JSON.parse(event.data)
|
|
484
|
+
handleWebSocketMessage(data)
|
|
485
|
+
} catch (error) {
|
|
486
|
+
console.error('Error parsing WebSocket message:', error, event.data)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
websocketRef.current.onclose = (event: CloseEvent) => {
|
|
491
|
+
setIsConnected(false)
|
|
492
|
+
|
|
493
|
+
// Reconnect after 3 seconds if not a clean close
|
|
494
|
+
if (event.code !== 1000) {
|
|
495
|
+
setTimeout(connectWebSocket, 3000)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
websocketRef.current.onerror = (error: Event) => {
|
|
500
|
+
console.error('WebSocket error:', error)
|
|
501
|
+
setIsConnected(false)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const handleWebSocketMessage = async (data: WebSocketMessage): Promise<void> => {
|
|
506
|
+
const excalidrawAPI = excalidrawAPIRef.current
|
|
507
|
+
if (!excalidrawAPI) {
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const currentElements = excalidrawAPI.getSceneElements()
|
|
513
|
+
const mergeAndApplySceneElements = (incomingElements: Partial<ExcalidrawElement>[]): void => {
|
|
514
|
+
if (incomingElements.length === 0) return
|
|
515
|
+
|
|
516
|
+
const incomingById = new Map<string, Partial<ExcalidrawElement>>()
|
|
517
|
+
incomingElements.forEach((element) => {
|
|
518
|
+
if (element.id) {
|
|
519
|
+
incomingById.set(element.id, element)
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
const mergedElements: Partial<ExcalidrawElement>[] = currentElements.map((element) => {
|
|
524
|
+
const incoming = incomingById.get(element.id)
|
|
525
|
+
if (!incoming) return element
|
|
526
|
+
incomingById.delete(element.id)
|
|
527
|
+
return { ...element, ...incoming }
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
mergedElements.push(...incomingById.values())
|
|
531
|
+
|
|
532
|
+
const convertedElements = convertElementsPreservingImageProps(mergedElements)
|
|
533
|
+
applySceneUpdateWithoutAutoSync(excalidrawAPI, {
|
|
534
|
+
elements: convertedElements,
|
|
535
|
+
captureUpdate: CaptureUpdateAction.NEVER
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
switch (data.type) {
|
|
540
|
+
case 'initial_elements':
|
|
541
|
+
if (data.elements && data.elements.length > 0) {
|
|
542
|
+
const cleanedElements = data.elements.map(cleanElementForExcalidraw)
|
|
543
|
+
const convertedElements = convertElementsPreservingImageProps(cleanedElements)
|
|
544
|
+
applySceneUpdateWithoutAutoSync(excalidrawAPI, {
|
|
545
|
+
elements: convertedElements,
|
|
546
|
+
captureUpdate: CaptureUpdateAction.NEVER
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
// Load files for image elements
|
|
550
|
+
if ((data as any).files) {
|
|
551
|
+
excalidrawAPI.addFiles(Object.values((data as any).files))
|
|
552
|
+
}
|
|
553
|
+
break
|
|
554
|
+
|
|
555
|
+
case 'files_added':
|
|
556
|
+
if (Array.isArray((data as any).files)) {
|
|
557
|
+
excalidrawAPI.addFiles((data as any).files)
|
|
558
|
+
}
|
|
559
|
+
break
|
|
560
|
+
|
|
561
|
+
case 'element_created':
|
|
562
|
+
if (data.element) {
|
|
563
|
+
const cleanedNewElement = cleanElementForExcalidraw(data.element)
|
|
564
|
+
// Rebuild against full scene so text/container bindings remain intact.
|
|
565
|
+
mergeAndApplySceneElements([cleanedNewElement])
|
|
566
|
+
}
|
|
567
|
+
break
|
|
568
|
+
|
|
569
|
+
case 'element_updated':
|
|
570
|
+
if (data.element) {
|
|
571
|
+
const cleanedUpdatedElement = cleanElementForExcalidraw(data.element)
|
|
572
|
+
// Convert with full scene context so text metrics/container placement can refresh.
|
|
573
|
+
mergeAndApplySceneElements([cleanedUpdatedElement])
|
|
574
|
+
}
|
|
575
|
+
break
|
|
576
|
+
|
|
577
|
+
case 'element_deleted':
|
|
578
|
+
if (data.elementId) {
|
|
579
|
+
const filteredElements = currentElements.filter(el => el.id !== data.elementId)
|
|
580
|
+
applySceneUpdateWithoutAutoSync(excalidrawAPI, {
|
|
581
|
+
elements: filteredElements,
|
|
582
|
+
captureUpdate: CaptureUpdateAction.NEVER
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
break
|
|
586
|
+
|
|
587
|
+
case 'elements_batch_created':
|
|
588
|
+
if (data.elements) {
|
|
589
|
+
const cleanedBatchElements = data.elements.map(cleanElementForExcalidraw)
|
|
590
|
+
mergeAndApplySceneElements(cleanedBatchElements)
|
|
591
|
+
}
|
|
592
|
+
break
|
|
593
|
+
|
|
594
|
+
case 'elements_synced':
|
|
595
|
+
console.log(`Sync confirmed by server: ${data.count} elements`)
|
|
596
|
+
// Sync confirmation already handled by HTTP response
|
|
597
|
+
break
|
|
598
|
+
|
|
599
|
+
case 'sync_status':
|
|
600
|
+
console.log(`Server sync status: ${data.count} elements`)
|
|
601
|
+
break
|
|
602
|
+
|
|
603
|
+
case 'canvas_cleared':
|
|
604
|
+
console.log('Canvas cleared by server')
|
|
605
|
+
applySceneUpdateWithoutAutoSync(excalidrawAPI, {
|
|
606
|
+
elements: [],
|
|
607
|
+
captureUpdate: CaptureUpdateAction.NEVER
|
|
608
|
+
})
|
|
609
|
+
break
|
|
610
|
+
|
|
611
|
+
case 'export_image_request':
|
|
612
|
+
if (data.requestId) {
|
|
613
|
+
try {
|
|
614
|
+
const elements = excalidrawAPI.getSceneElements()
|
|
615
|
+
const appState = excalidrawAPI.getAppState()
|
|
616
|
+
const files = excalidrawAPI.getFiles()
|
|
617
|
+
|
|
618
|
+
if (data.format === 'svg') {
|
|
619
|
+
const svg = await exportToSvg({
|
|
620
|
+
elements,
|
|
621
|
+
appState: {
|
|
622
|
+
...appState,
|
|
623
|
+
exportBackground: data.background !== false
|
|
624
|
+
},
|
|
625
|
+
files
|
|
626
|
+
})
|
|
627
|
+
const svgString = new XMLSerializer().serializeToString(svg)
|
|
628
|
+
await fetch('/api/export/image/result', {
|
|
629
|
+
method: 'POST',
|
|
630
|
+
headers: { 'Content-Type': 'application/json' },
|
|
631
|
+
body: JSON.stringify({
|
|
632
|
+
requestId: data.requestId,
|
|
633
|
+
format: 'svg',
|
|
634
|
+
data: svgString
|
|
635
|
+
})
|
|
636
|
+
})
|
|
637
|
+
} else {
|
|
638
|
+
const blob = await exportToBlob({
|
|
639
|
+
elements,
|
|
640
|
+
appState: {
|
|
641
|
+
...appState,
|
|
642
|
+
exportBackground: data.background !== false
|
|
643
|
+
},
|
|
644
|
+
files,
|
|
645
|
+
mimeType: 'image/png'
|
|
646
|
+
})
|
|
647
|
+
const reader = new FileReader()
|
|
648
|
+
reader.onload = async () => {
|
|
649
|
+
try {
|
|
650
|
+
const resultString = reader.result as string
|
|
651
|
+
const base64 = resultString?.split(',')[1]
|
|
652
|
+
if (!base64) {
|
|
653
|
+
throw new Error('Could not extract base64 data from result')
|
|
654
|
+
}
|
|
655
|
+
await fetch('/api/export/image/result', {
|
|
656
|
+
method: 'POST',
|
|
657
|
+
headers: { 'Content-Type': 'application/json' },
|
|
658
|
+
body: JSON.stringify({
|
|
659
|
+
requestId: data.requestId,
|
|
660
|
+
format: 'png',
|
|
661
|
+
data: base64
|
|
662
|
+
})
|
|
663
|
+
})
|
|
664
|
+
} catch (readerError) {
|
|
665
|
+
console.error('Image export (FileReader) failed:', readerError)
|
|
666
|
+
await fetch('/api/export/image/result', {
|
|
667
|
+
method: 'POST',
|
|
668
|
+
headers: { 'Content-Type': 'application/json' },
|
|
669
|
+
body: JSON.stringify({
|
|
670
|
+
requestId: data.requestId,
|
|
671
|
+
error: (readerError as Error).message
|
|
672
|
+
})
|
|
673
|
+
}).catch(() => { })
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
reader.onerror = async () => {
|
|
677
|
+
console.error('FileReader error:', reader.error)
|
|
678
|
+
await fetch('/api/export/image/result', {
|
|
679
|
+
method: 'POST',
|
|
680
|
+
headers: { 'Content-Type': 'application/json' },
|
|
681
|
+
body: JSON.stringify({
|
|
682
|
+
requestId: data.requestId,
|
|
683
|
+
error: reader.error?.message || 'FileReader failed'
|
|
684
|
+
})
|
|
685
|
+
}).catch(() => { })
|
|
686
|
+
}
|
|
687
|
+
reader.readAsDataURL(blob)
|
|
688
|
+
}
|
|
689
|
+
} catch (exportError) {
|
|
690
|
+
console.error('Image export failed:', exportError)
|
|
691
|
+
await fetch('/api/export/image/result', {
|
|
692
|
+
method: 'POST',
|
|
693
|
+
headers: { 'Content-Type': 'application/json' },
|
|
694
|
+
body: JSON.stringify({
|
|
695
|
+
requestId: data.requestId,
|
|
696
|
+
error: (exportError as Error).message
|
|
697
|
+
})
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
break
|
|
702
|
+
|
|
703
|
+
case 'set_viewport':
|
|
704
|
+
console.log('Received viewport control request', data)
|
|
705
|
+
if (data.requestId) {
|
|
706
|
+
try {
|
|
707
|
+
if (data.scrollToContent) {
|
|
708
|
+
const allElements = excalidrawAPI.getSceneElements()
|
|
709
|
+
if (allElements.length > 0) {
|
|
710
|
+
excalidrawAPI.scrollToContent(allElements, { fitToViewport: true, animate: true })
|
|
711
|
+
}
|
|
712
|
+
} else if (data.scrollToElementId) {
|
|
713
|
+
const allElements = excalidrawAPI.getSceneElements()
|
|
714
|
+
const targetElement = allElements.find(el => el.id === data.scrollToElementId)
|
|
715
|
+
if (targetElement) {
|
|
716
|
+
excalidrawAPI.scrollToContent([targetElement], { fitToViewport: false, animate: true })
|
|
717
|
+
} else {
|
|
718
|
+
throw new Error(`Element ${data.scrollToElementId} not found`)
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
// Direct zoom/scroll control
|
|
722
|
+
const appState: any = {}
|
|
723
|
+
if (data.zoom !== undefined) {
|
|
724
|
+
appState.zoom = { value: data.zoom }
|
|
725
|
+
}
|
|
726
|
+
if (data.offsetX !== undefined) {
|
|
727
|
+
appState.scrollX = data.offsetX
|
|
728
|
+
}
|
|
729
|
+
if (data.offsetY !== undefined) {
|
|
730
|
+
appState.scrollY = data.offsetY
|
|
731
|
+
}
|
|
732
|
+
if (Object.keys(appState).length > 0) {
|
|
733
|
+
applySceneUpdateWithoutAutoSync(excalidrawAPI, { appState })
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
await fetch('/api/viewport/result', {
|
|
738
|
+
method: 'POST',
|
|
739
|
+
headers: { 'Content-Type': 'application/json' },
|
|
740
|
+
body: JSON.stringify({
|
|
741
|
+
requestId: data.requestId,
|
|
742
|
+
success: true,
|
|
743
|
+
message: 'Viewport updated'
|
|
744
|
+
})
|
|
745
|
+
})
|
|
746
|
+
} catch (viewportError) {
|
|
747
|
+
console.error('Viewport control failed:', viewportError)
|
|
748
|
+
await fetch('/api/viewport/result', {
|
|
749
|
+
method: 'POST',
|
|
750
|
+
headers: { 'Content-Type': 'application/json' },
|
|
751
|
+
body: JSON.stringify({
|
|
752
|
+
requestId: data.requestId,
|
|
753
|
+
error: (viewportError as Error).message
|
|
754
|
+
})
|
|
755
|
+
}).catch(() => { })
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
break
|
|
759
|
+
|
|
760
|
+
case 'mermaid_convert':
|
|
761
|
+
console.log('Received Mermaid conversion request from MCP')
|
|
762
|
+
if (data.mermaidDiagram) {
|
|
763
|
+
try {
|
|
764
|
+
const result = await convertMermaidToExcalidraw(data.mermaidDiagram, data.config || DEFAULT_MERMAID_CONFIG)
|
|
765
|
+
|
|
766
|
+
if (result.error) {
|
|
767
|
+
console.error('Mermaid conversion error:', result.error)
|
|
768
|
+
return
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (result.elements && result.elements.length > 0) {
|
|
772
|
+
const convertedElements = convertToExcalidrawElements(result.elements, { regenerateIds: false })
|
|
773
|
+
applySceneUpdateWithoutAutoSync(excalidrawAPI, {
|
|
774
|
+
elements: convertedElements,
|
|
775
|
+
captureUpdate: CaptureUpdateAction.IMMEDIATELY
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
if (result.files) {
|
|
779
|
+
excalidrawAPI.addFiles(Object.values(result.files))
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
console.log('Mermaid diagram converted successfully:', result.elements.length, 'elements')
|
|
783
|
+
|
|
784
|
+
// Sync to backend automatically after creating elements
|
|
785
|
+
await syncToBackend()
|
|
786
|
+
}
|
|
787
|
+
} catch (error) {
|
|
788
|
+
console.error('Error converting Mermaid diagram from WebSocket:', error)
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
break
|
|
792
|
+
|
|
793
|
+
default:
|
|
794
|
+
console.log('Unknown WebSocket message type:', data.type)
|
|
795
|
+
}
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.error('Error processing WebSocket message:', error, data)
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const convertToBackendFormat = (element: ExcalidrawElement): ServerElement => {
|
|
802
|
+
return {
|
|
803
|
+
...element
|
|
804
|
+
} as ServerElement
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const formatSyncTime = (time: Date | null): string => {
|
|
808
|
+
if (!time) return ''
|
|
809
|
+
return time.toLocaleTimeString('zh-CN', {
|
|
810
|
+
hour: '2-digit',
|
|
811
|
+
minute: '2-digit',
|
|
812
|
+
second: '2-digit'
|
|
813
|
+
})
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const syncToBackend = async (options: { silent?: boolean } = {}): Promise<void> => {
|
|
817
|
+
const { silent = false } = options
|
|
818
|
+
|
|
819
|
+
if (!excalidrawAPI) {
|
|
820
|
+
console.warn('Excalidraw API not available')
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (syncInFlightRef.current) {
|
|
825
|
+
return
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (autoSyncTimerRef.current) {
|
|
829
|
+
clearTimeout(autoSyncTimerRef.current)
|
|
830
|
+
autoSyncTimerRef.current = null
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
syncInFlightRef.current = true
|
|
834
|
+
if (!silent) {
|
|
835
|
+
setSyncStatus('syncing')
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
const currentElements = excalidrawAPI.getSceneElements()
|
|
840
|
+
console.log(`Syncing ${currentElements.length} elements to backend`)
|
|
841
|
+
|
|
842
|
+
const activeElements = currentElements.filter(el => !el.isDeleted)
|
|
843
|
+
|
|
844
|
+
const currentFiles = excalidrawAPI.getFiles()
|
|
845
|
+
const filesArray = Object.values(currentFiles)
|
|
846
|
+
|
|
847
|
+
const backendElements = activeElements.map(convertToBackendFormat)
|
|
848
|
+
|
|
849
|
+
const response = await fetch('/api/elements/sync', {
|
|
850
|
+
method: 'POST',
|
|
851
|
+
headers: {
|
|
852
|
+
'Content-Type': 'application/json',
|
|
853
|
+
},
|
|
854
|
+
body: JSON.stringify({
|
|
855
|
+
elements: backendElements,
|
|
856
|
+
timestamp: new Date().toISOString()
|
|
857
|
+
})
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
if (response.ok) {
|
|
861
|
+
const result: ApiResponse = await response.json()
|
|
862
|
+
console.log(`Sync successful: ${result.count} elements synced`)
|
|
863
|
+
|
|
864
|
+
if (filesArray.length > 0) {
|
|
865
|
+
console.log(`Syncing ${filesArray.length} files to backend`)
|
|
866
|
+
const filesResponse = await fetch('/api/files', {
|
|
867
|
+
method: 'POST',
|
|
868
|
+
headers: {
|
|
869
|
+
'Content-Type': 'application/json',
|
|
870
|
+
},
|
|
871
|
+
body: JSON.stringify(filesArray)
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
if (filesResponse.ok) {
|
|
875
|
+
const filesResult = await filesResponse.json()
|
|
876
|
+
console.log(`Files sync successful: ${filesResult.count} files synced`)
|
|
877
|
+
} else {
|
|
878
|
+
console.error('Files sync failed:', await filesResponse.text())
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
setLastSyncTime(new Date())
|
|
883
|
+
|
|
884
|
+
if (!silent) {
|
|
885
|
+
setSyncStatus('success')
|
|
886
|
+
setTimeout(() => setSyncStatus('idle'), 2000)
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
const error: ApiResponse = await response.json()
|
|
890
|
+
console.error('Sync failed:', error.error)
|
|
891
|
+
if (!silent) {
|
|
892
|
+
setSyncStatus('error')
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
} catch (error) {
|
|
896
|
+
console.error('Sync error:', error)
|
|
897
|
+
if (!silent) {
|
|
898
|
+
setSyncStatus('error')
|
|
899
|
+
}
|
|
900
|
+
} finally {
|
|
901
|
+
syncInFlightRef.current = false
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const scheduleAutoSync = (): void => {
|
|
906
|
+
if (!isConnected || !excalidrawAPI) {
|
|
907
|
+
return
|
|
908
|
+
}
|
|
909
|
+
if (!userInteractedRef.current) {
|
|
910
|
+
return
|
|
911
|
+
}
|
|
912
|
+
if (suppressAutoSyncCountRef.current > 0) {
|
|
913
|
+
return
|
|
914
|
+
}
|
|
915
|
+
if (autoSyncTimerRef.current) {
|
|
916
|
+
clearTimeout(autoSyncTimerRef.current)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
autoSyncTimerRef.current = setTimeout(() => {
|
|
920
|
+
autoSyncTimerRef.current = null
|
|
921
|
+
if (suppressAutoSyncCountRef.current > 0 || syncInFlightRef.current) {
|
|
922
|
+
return
|
|
923
|
+
}
|
|
924
|
+
void syncToBackend({ silent: true })
|
|
925
|
+
}, AUTO_SYNC_DEBOUNCE_MS)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const clearCanvas = async (): Promise<void> => {
|
|
929
|
+
if (excalidrawAPI) {
|
|
930
|
+
try {
|
|
931
|
+
const response = await fetch('/api/elements', { headers: authHeaders() })
|
|
932
|
+
const result: ApiResponse = await response.json()
|
|
933
|
+
|
|
934
|
+
if (result.success && result.elements) {
|
|
935
|
+
const deletePromises = result.elements.map(element =>
|
|
936
|
+
fetch(`/api/elements/${element.id}`, { method: 'DELETE' })
|
|
937
|
+
)
|
|
938
|
+
await Promise.all(deletePromises)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
applySceneUpdateWithoutAutoSync(excalidrawAPI, {
|
|
942
|
+
elements: [],
|
|
943
|
+
captureUpdate: CaptureUpdateAction.IMMEDIATELY
|
|
944
|
+
})
|
|
945
|
+
} catch (error) {
|
|
946
|
+
console.error('Error clearing canvas:', error)
|
|
947
|
+
applySceneUpdateWithoutAutoSync(excalidrawAPI, {
|
|
948
|
+
elements: [],
|
|
949
|
+
captureUpdate: CaptureUpdateAction.IMMEDIATELY
|
|
950
|
+
})
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const [showLogin, setShowLogin] = useState<boolean>(false)
|
|
956
|
+
const [loginCode, setLoginCode] = useState<string>('')
|
|
957
|
+
const [deviceInfo, setDeviceInfo] = useState<{ user_code: string; verification_uri: string } | null>(null)
|
|
958
|
+
|
|
959
|
+
const startLogin = async (): Promise<void> => {
|
|
960
|
+
const gh = await import('./utils/ghSync')
|
|
961
|
+
if (loginCode.trim()) {
|
|
962
|
+
gh.setToken(loginCode.trim())
|
|
963
|
+
setGhToken(loginCode.trim())
|
|
964
|
+
setLoginCode('')
|
|
965
|
+
setShowLogin(false)
|
|
966
|
+
return
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const flow = await gh.startDeviceFlow(gh.CLIENT_ID)
|
|
970
|
+
setDeviceInfo({ user_code: flow.user_code, verification_uri: flow.verification_uri })
|
|
971
|
+
window.open(flow.verification_uri, '_blank')
|
|
972
|
+
} catch (err) {
|
|
973
|
+
console.warn('Device flow blocked by CORS, showing agent handoff:', err)
|
|
974
|
+
setDeviceInfo({ user_code: 'ASK-YOUR-AGENT', verification_uri: 'ask the agent to run the github_login tool' })
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return (
|
|
979
|
+
<div className="app">
|
|
980
|
+
{/* Floating status pill (no header — canvas is fullscreen) */}
|
|
981
|
+
<div className="pill">
|
|
982
|
+
<div className={`status-dot ${(serverMode ? isConnected : access === 'editor') ? 'status-connected' : 'status-disconnected'}`}></div>
|
|
983
|
+
<span>
|
|
984
|
+
{serverMode
|
|
985
|
+
? (isConnected ? 'Live' : 'Offline')
|
|
986
|
+
: access === 'unknown'
|
|
987
|
+
? 'Checking access…'
|
|
988
|
+
: access === 'editor'
|
|
989
|
+
? (syncStatus === 'syncing' ? 'Saving…' : ghDirty ? 'Unsaved changes' : lastSyncTime ? `Saved ${formatSyncTime(lastSyncTime)}${ghLogin ? ` · ${ghLogin}` : ''}` : `Can edit${ghLogin ? ` · ${ghLogin}` : ''}`)
|
|
990
|
+
: access === 'viewer'
|
|
991
|
+
? `Read-only${ghLogin ? ` · ${ghLogin}` : ''} — your changes won't save`
|
|
992
|
+
: 'No access — login with a collaborator account'}
|
|
993
|
+
</span>
|
|
994
|
+
{!serverMode && (
|
|
995
|
+
access === 'editor'
|
|
996
|
+
? <button className="pill-btn" onClick={() => { void pushToGitHub(false) }}>Save now</button>
|
|
997
|
+
: <button className="pill-btn" onClick={() => setShowLogin(true)}>Login with GitHub</button>
|
|
998
|
+
)}
|
|
999
|
+
</div>
|
|
1000
|
+
|
|
1001
|
+
{showLogin && !serverMode && (
|
|
1002
|
+
<div className="login-modal">
|
|
1003
|
+
<h3>Login with GitHub</h3>
|
|
1004
|
+
{accessDetail === 'repo-not-covered' && ghLogin && (
|
|
1005
|
+
<p><b>Almost there, {ghLogin}:</b> the app is installed on your account but <b>this repo isn't included</b>. Open your <a href={`https://github.com/settings/installations`} target="_blank" rel="noreferrer">app installations</a>, pick Excalidrop → Configure → add <code>{ghRepo?.owner}/{ghRepo?.repo}</code> (or switch to All repositories), then close and reopen this login.</p>
|
|
1006
|
+
)}
|
|
1007
|
+
{accessDetail === 'app-not-installed' && (
|
|
1008
|
+
<p>First time on this repo? <a href={APP_INSTALL_URL} target="_blank" rel="noreferrer">Install the Excalidrop app</a> on it — repo access is canvas access.</p>
|
|
1009
|
+
)}
|
|
1010
|
+
{accessDetail !== 'repo-not-covered' && accessDetail !== 'app-not-installed' && (
|
|
1011
|
+
<p>First time on this repo? <a href={APP_INSTALL_URL} target="_blank" rel="noreferrer">Install the Excalidrop app</a> on it — repo access is canvas access.</p>
|
|
1012
|
+
)}
|
|
1013
|
+
<p><b>Easiest:</b> ask your agent to run <code>github_login</code> for this site — it shows a code, you approve it (2FA via GitHub), and this page unlocks. No copy-paste.</p>
|
|
1014
|
+
<details>
|
|
1015
|
+
<summary>Or paste a token manually</summary>
|
|
1016
|
+
<input
|
|
1017
|
+
placeholder="Paste token (gh auth token)…"
|
|
1018
|
+
value={loginCode}
|
|
1019
|
+
onChange={(e) => setLoginCode(e.target.value)}
|
|
1020
|
+
/>
|
|
1021
|
+
<div className="login-row">
|
|
1022
|
+
<button className="btn-primary" onClick={() => { void startLogin() }}>Save token</button>
|
|
1023
|
+
</div>
|
|
1024
|
+
</details>
|
|
1025
|
+
<div className="login-row">
|
|
1026
|
+
<button className="btn-secondary" onClick={() => { setShowLogin(false); setDeviceInfo(null) }}>Close</button>
|
|
1027
|
+
</div>
|
|
1028
|
+
{deviceInfo && deviceInfo.user_code !== 'ASK-YOUR-AGENT' && (
|
|
1029
|
+
<div style={{ background: '#f0f0f0', padding: '12px', borderRadius: '8px', marginTop: '8px' }}>
|
|
1030
|
+
<p style={{ margin: '0 0 8px 0' }}><b>Device Flow Started:</b></p>
|
|
1031
|
+
<p style={{ margin: '0 0 8px 0' }}>Open <a href={deviceInfo.verification_uri} target="_blank" rel="noreferrer">{deviceInfo.verification_uri}</a> and enter code:</p>
|
|
1032
|
+
<p style={{ margin: '0', fontSize: '20px', fontWeight: 'bold', fontFamily: 'monospace', letterSpacing: '2px' }}>{deviceInfo.user_code}</p>
|
|
1033
|
+
</div>
|
|
1034
|
+
)}
|
|
1035
|
+
{deviceInfo && deviceInfo.user_code === 'ASK-YOUR-AGENT' && (
|
|
1036
|
+
<p>Ask your agent to run <code>github_login</code> for this site.</p>
|
|
1037
|
+
)}
|
|
1038
|
+
</div>
|
|
1039
|
+
)}
|
|
1040
|
+
|
|
1041
|
+
{/* Canvas Container */}
|
|
1042
|
+
<div className="canvas-container">
|
|
1043
|
+
<div
|
|
1044
|
+
onPointerDownCapture={() => {
|
|
1045
|
+
userInteractedRef.current = true
|
|
1046
|
+
}}
|
|
1047
|
+
onKeyDownCapture={() => {
|
|
1048
|
+
userInteractedRef.current = true
|
|
1049
|
+
}}
|
|
1050
|
+
style={{ width: '100%', height: '100%' }}
|
|
1051
|
+
>
|
|
1052
|
+
<Excalidraw
|
|
1053
|
+
viewModeEnabled={!serverMode && access !== 'editor'}
|
|
1054
|
+
excalidrawAPI={(api: ExcalidrawAPIRefValue) => setExcalidrawAPI(api)}
|
|
1055
|
+
onChange={() => {
|
|
1056
|
+
if (serverMode) scheduleAutoSync()
|
|
1057
|
+
else if (!suppressAutoSyncCountRef.current) setGhDirty(true)
|
|
1058
|
+
}}
|
|
1059
|
+
initialData={{
|
|
1060
|
+
elements: [],
|
|
1061
|
+
appState: {
|
|
1062
|
+
theme: 'light',
|
|
1063
|
+
viewBackgroundColor: '#ffffff'
|
|
1064
|
+
}
|
|
1065
|
+
}}
|
|
1066
|
+
>
|
|
1067
|
+
{/* Custom hamburger menu: functional items only.
|
|
1068
|
+
Rendering <MainMenu> replaces Excalidraw defaults entirely,
|
|
1069
|
+
so Help / Socials / ItemLink entries (excalidraw.com, Discord,
|
|
1070
|
+
GitHub, docs links) are gone. */}
|
|
1071
|
+
<MainMenu>
|
|
1072
|
+
<MainMenu.DefaultItems.LoadScene />
|
|
1073
|
+
<MainMenu.DefaultItems.SaveToActiveFile />
|
|
1074
|
+
<MainMenu.DefaultItems.Export />
|
|
1075
|
+
<MainMenu.DefaultItems.SaveAsImage />
|
|
1076
|
+
<MainMenu.Separator />
|
|
1077
|
+
<MainMenu.DefaultItems.ClearCanvas />
|
|
1078
|
+
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
|
1079
|
+
<MainMenu.DefaultItems.ToggleTheme />
|
|
1080
|
+
<MainMenu.Separator />
|
|
1081
|
+
<MainMenu.DefaultItems.SearchMenu />
|
|
1082
|
+
<MainMenu.DefaultItems.CommandPalette />
|
|
1083
|
+
</MainMenu>
|
|
1084
|
+
</Excalidraw>
|
|
1085
|
+
</div>
|
|
1086
|
+
</div>
|
|
1087
|
+
</div>
|
|
1088
|
+
)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
export default App
|