slidev-excalidraw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted, watch, onBeforeUnmount } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ src: string
6
+ darkMode?: boolean
7
+ editable?: boolean
8
+ persist?: boolean
9
+ }>()
10
+
11
+ const container = ref<HTMLDivElement>()
12
+ const editorContainer = ref<HTMLDivElement>()
13
+ const editing = ref(false)
14
+ let fileData: any = null
15
+ let reactRoot: any = null
16
+
17
+ async function loadData() {
18
+ const res = await fetch(props.src)
19
+ fileData = await res.json()
20
+ }
21
+
22
+ async function renderSvg() {
23
+ if (!container.value || !fileData) return
24
+
25
+ const { exportToSvg } = await import('@excalidraw/utils')
26
+
27
+ const svg = await exportToSvg({
28
+ elements: fileData.elements,
29
+ appState: {
30
+ ...fileData.appState,
31
+ exportWithDarkMode: props.darkMode ?? false,
32
+ exportBackground: false,
33
+ },
34
+ files: fileData.files || null,
35
+ })
36
+
37
+ svg.style.width = '100%'
38
+ svg.style.height = '100%'
39
+ container.value.innerHTML = ''
40
+ container.value.appendChild(svg)
41
+ }
42
+
43
+ async function startEditing() {
44
+ if (!editorContainer.value || !fileData) return
45
+ editing.value = true
46
+
47
+ const React = await import('react')
48
+ const ReactDOM = await import('react-dom/client')
49
+ const { Excalidraw } = await import('@excalidraw/excalidraw')
50
+ await import('@excalidraw/excalidraw/index.css')
51
+
52
+ if (reactRoot) {
53
+ reactRoot.unmount()
54
+ }
55
+ reactRoot = ReactDOM.createRoot(editorContainer.value)
56
+
57
+ const onChange = (elements: any, appState: any, files: any) => {
58
+ fileData = { elements, appState, files }
59
+ }
60
+
61
+ reactRoot.render(
62
+ React.createElement(Excalidraw, {
63
+ initialData: fileData,
64
+ onChange,
65
+ })
66
+ )
67
+ }
68
+
69
+ async function stopEditing() {
70
+ if (reactRoot) {
71
+ reactRoot.unmount()
72
+ reactRoot = null
73
+ }
74
+ editing.value = false
75
+
76
+ if (props.persist !== false) {
77
+ const filename = props.src.replace(/^\//, '')
78
+ await fetch('/api/save-excalidraw', {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ file: filename, data: fileData }),
82
+ })
83
+ }
84
+
85
+ renderSvg()
86
+ }
87
+
88
+ onMounted(async () => {
89
+ await loadData()
90
+ await renderSvg()
91
+ })
92
+
93
+ onBeforeUnmount(() => {
94
+ if (reactRoot) {
95
+ reactRoot.unmount()
96
+ reactRoot = null
97
+ }
98
+ })
99
+
100
+ watch(() => [props.src, props.darkMode], async () => {
101
+ await loadData()
102
+ await renderSvg()
103
+ })
104
+ </script>
105
+
106
+ <template>
107
+ <div class="excalidraw-wrapper">
108
+ <div v-show="!editing" ref="container" class="excalidraw-container" @dblclick="editable !== false && startEditing()" />
109
+ <div v-show="editing" ref="editorContainer" class="excalidraw-editor" />
110
+ <div class="btn-group">
111
+ <button v-if="!editing && editable !== false" class="edit-btn" @click="startEditing" title="Edit">
112
+ ✏️
113
+ </button>
114
+ <button v-if="editing" class="edit-btn done-btn" @click="stopEditing" title="Done">
115
+
116
+ </button>
117
+ </div>
118
+ </div>
119
+ </template>
120
+
121
+ <style scoped>
122
+ .excalidraw-wrapper {
123
+ width: 100%;
124
+ height: 100%;
125
+ position: relative;
126
+ }
127
+ .excalidraw-container {
128
+ width: 100%;
129
+ height: 100%;
130
+ display: flex;
131
+ justify-content: center;
132
+ align-items: center;
133
+ cursor: pointer;
134
+ }
135
+ .excalidraw-editor {
136
+ width: 100%;
137
+ height: 80vh;
138
+ }
139
+ .btn-group {
140
+ position: absolute;
141
+ top: 4px;
142
+ right: 4px;
143
+ z-index: 10;
144
+ display: flex;
145
+ gap: 4px;
146
+ }
147
+ .edit-btn {
148
+ background: rgba(255,255,255,0.8);
149
+ border: 1px solid #ddd;
150
+ border-radius: 4px;
151
+ padding: 2px 6px;
152
+ cursor: pointer;
153
+ font-size: 14px;
154
+ opacity: 0.5;
155
+ transition: opacity 0.2s;
156
+ }
157
+ .edit-btn:hover {
158
+ opacity: 1;
159
+ }
160
+ .done-btn {
161
+ opacity: 0.8;
162
+ background: rgba(34,197,94,0.8);
163
+ color: white;
164
+ }
165
+ </style>
package/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { Plugin } from 'vite'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ export function excalidrawSavePlugin(publicDir: string): Plugin {
6
+ return {
7
+ name: 'slidev-excalidraw-save',
8
+ configureServer(server) {
9
+ server.middlewares.use('/api/save-excalidraw', (req, res) => {
10
+ if (req.method !== 'POST') {
11
+ res.statusCode = 405
12
+ res.end()
13
+ return
14
+ }
15
+ let body = ''
16
+ req.on('data', (chunk: string) => { body += chunk })
17
+ req.on('end', () => {
18
+ const { file, data } = JSON.parse(body)
19
+ const filePath = path.join(publicDir, file)
20
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2))
21
+ res.statusCode = 200
22
+ res.end('ok')
23
+ })
24
+ })
25
+ },
26
+ }
27
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "slidev-excalidraw",
3
+ "version": "0.1.0",
4
+ "description": "Excalidraw integration for Slidev with inline editing and auto-save",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "keywords": [
8
+ "slidev",
9
+ "slidev-addon",
10
+ "excalidraw",
11
+ "slides",
12
+ "presentation",
13
+ "diagram"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Seonglae Cho",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/seonglae/slidev-excalidraw"
20
+ },
21
+ "files": [
22
+ "components",
23
+ "index.ts",
24
+ "README.md"
25
+ ],
26
+ "peerDependencies": {
27
+ "@excalidraw/excalidraw": ">=0.18",
28
+ "@excalidraw/utils": ">=0.1",
29
+ "react": "^18",
30
+ "react-dom": "^18",
31
+ "@slidev/cli": ">=52"
32
+ },
33
+ "slidev": {
34
+ "components": "./components"
35
+ }
36
+ }