@zzalai/leafer-multi-roi 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 otaku
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # LeaferJS Multi ROI 组件
2
+
3
+ [English](README_EN.md) | 中文
4
+
5
+ 一个基于 Vue3 和 LeaferJS 的多区域选择组件,用于在图片上进行区域标注和编辑。
6
+
7
+ ## 功能特点
8
+
9
+ - 🖼️ 支持图片加载和显示
10
+ - 📐 支持矩形区域的绘制、编辑和删除
11
+ - 🔍 支持画布缩放和拖拽
12
+ - ⌨️ 支持键盘热键操作
13
+ - 🔄 支持撤销/重做功能
14
+ - 📤 支持画布信息 JSON 导出和导入
15
+ - 🎨 支持 CSS 变量自定义样式
16
+
17
+ ## 安装
18
+
19
+ ### 使用 npm
20
+
21
+ ```bash
22
+ npm install @zzalai/leafer-multi-roi
23
+ ```
24
+
25
+ ### 使用 yarn
26
+
27
+ ```bash
28
+ yarn add @zzalai/leafer-multi-roi
29
+ ```
30
+
31
+ ### 使用 pnpm
32
+
33
+ ```bash
34
+ pnpm add @zzalai/leafer-multi-roi
35
+ ```
36
+
37
+ ## 使用方法
38
+
39
+ ### 全局注册
40
+
41
+ ```javascript
42
+ // main.ts
43
+ import { createApp } from 'vue'
44
+ import App from './App.vue'
45
+ import RoiEditor from '@zzalai/leafer-multi-roi'
46
+
47
+ const app = createApp(App)
48
+ app.use(RoiEditor)
49
+ app.mount('#app')
50
+ ```
51
+
52
+ ### 局部使用
53
+
54
+ ```vue
55
+ <template>
56
+ <div class="app">
57
+ <RoiEditor
58
+ :imageSrc="imageSrc"
59
+ :options="editorOptions"
60
+ @roiChange="handleRoiChange"
61
+ @loadStart="handleLoadStart"
62
+ @loadSuccess="handleLoadSuccess"
63
+ @loadError="handleLoadError"
64
+ />
65
+ </div>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { ref, computed } from 'vue'
70
+ import { RoiEditor } from '@zzalai/leafer-multi-roi'
71
+
72
+ // 图片源
73
+ const imageUrl = ref('https://picsum.photos/1280/1080')
74
+ const imageSrc = computed(() => ({
75
+ id: 'test-image',
76
+ url: imageUrl.value
77
+ }))
78
+
79
+ // 编辑器选项
80
+ const editorOptions = ref({
81
+ regionStyle: {
82
+ fill: 'rgba(100, 149, 237, 0.3)',
83
+ stroke: 'rgba(100, 149, 237, 0.8)',
84
+ strokeWidth: 2
85
+ },
86
+ selectedRegionStyle: {
87
+ fill: 'rgba(100, 149, 237, 0.5)',
88
+ stroke: 'rgba(100, 149, 237, 1)',
89
+ strokeWidth: 2
90
+ },
91
+ maxRegions: 20, // 最大区域数量限制,默认值为20
92
+ maxUndoSteps: 100 // 最大撤销/重做步数限制,默认值为100
93
+ })
94
+
95
+ // 处理ROI变化
96
+ const handleRoiChange = (data: any) => {
97
+ console.log('ROI changed:', data)
98
+ }
99
+
100
+ // 处理图片加载开始
101
+ const handleLoadStart = () => {
102
+ console.log('Image load started')
103
+ }
104
+
105
+ // 处理图片加载成功
106
+ const handleLoadSuccess = () => {
107
+ console.log('Image load success')
108
+ }
109
+
110
+ // 处理图片加载失败
111
+ const handleLoadError = (error: any) => {
112
+ console.error('Image load error:', error)
113
+ }
114
+ </script>
115
+ ```
116
+
117
+ ### 手动调用方法
118
+
119
+ ```vue
120
+ <template>
121
+ <div class="app">
122
+ <RoiEditor
123
+ ref="roiEditor"
124
+ :imageSrc="imageSrc"
125
+ />
126
+ <button @click="reloadImage">重新加载图片</button>
127
+ <button @click="exportCanvas">导出画布</button>
128
+ <input type="file" @change="importCanvas" accept=".json" />
129
+ </div>
130
+ </template>
131
+
132
+ <script setup lang="ts">
133
+ import { ref, computed } from 'vue'
134
+ import { RoiEditor } from '@zzalai/leafer-multi-roi'
135
+
136
+ const roiEditor = ref<InstanceType<typeof RoiEditor> | null>(null)
137
+ const imageUrl = ref('https://picsum.photos/1280/1080')
138
+ const imageSrc = computed(() => ({
139
+ id: 'test-image',
140
+ url: imageUrl.value
141
+ }))
142
+
143
+ // 重新加载图片
144
+ const reloadImage = () => {
145
+ roiEditor.value?.loadImage()
146
+ }
147
+
148
+ // 导出画布
149
+ const exportCanvas = () => {
150
+ const json = roiEditor.value?.exportCanvasJSON()
151
+ if (json) {
152
+ const blob = new Blob([json], { type: 'application/json' })
153
+ const url = URL.createObjectURL(blob)
154
+ const a = document.createElement('a')
155
+ a.href = url
156
+ a.download = 'canvas.json'
157
+ a.click()
158
+ URL.revokeObjectURL(url)
159
+ }
160
+ }
161
+
162
+ // 导入画布
163
+ const importCanvas = async (event: Event) => {
164
+ const target = event.target as HTMLInputElement
165
+ const file = target.files?.[0]
166
+ if (file) {
167
+ const reader = new FileReader()
168
+ reader.onload = async (e) => {
169
+ const jsonString = e.target?.result as string
170
+ if (roiEditor.value) {
171
+ const success = await roiEditor.value.importCanvasJSON(jsonString, { resetZoom: true })
172
+ if (success) {
173
+ alert('画布导入成功!')
174
+ } else {
175
+ alert('画布导入失败。')
176
+ }
177
+ }
178
+ }
179
+ reader.readAsText(file)
180
+ }
181
+ target.value = ''
182
+ }
183
+ </script>
184
+ ```
185
+
186
+ ## 适用场景
187
+
188
+ - **图片标注**:在图片上标注感兴趣的区域
189
+ - **目标检测**:为目标检测模型生成训练数据
190
+ - **图像分析**:标记图像中的特定区域进行分析
191
+ - **医疗影像**:在医疗影像上标记病变区域
192
+ - **电商产品**:在产品图片上标记不同的部件
193
+
194
+ ## 热键操作
195
+
196
+ | 热键 | 功能 |
197
+ |------|------|
198
+ | V | 选择工具 |
199
+ | M | 框选工具 |
200
+ | Ctrl+Z | 撤销 |
201
+ | Ctrl+Y | 重做 |
202
+ | Delete | 删除选中区域 |
203
+ | Ctrl++ | 放大 |
204
+ | Ctrl+- | 缩小 |
205
+ | Ctrl+0 | 重置缩放 |
206
+ | Alt | 显示/隐藏热键提示 |
207
+
208
+ ## 暴露的方法
209
+
210
+ - `getROIAnnotations()`:获取所有 ROI 标注数据
211
+ - `getImageInfo()`:获取图片信息
212
+ - `exportCanvasJSON()`:导出画布信息为 JSON 字符串
213
+ - `importCanvasJSON(jsonString, options)`:从 JSON 字符串导入画布信息
214
+ - `loadImage()`:手动加载图片
215
+
216
+ ## CSS 可重置变量
217
+
218
+ ```css
219
+ :root {
220
+ /* 颜色变量 */
221
+ --leafer-roi-color-primary: #007aff; /* 主题色 */
222
+ --leafer-roi-color-background: #f5f5f5; /* 背景色 */
223
+ --leafer-roi-color-background-light: #f0f0f0; /* 浅背景色 */
224
+ --leafer-roi-color-white: #fff; /* 白色 */
225
+ --leafer-roi-color-text: #333; /* 文本色 */
226
+ --leafer-roi-color-text-secondary: #666; /* 次要文本色 */
227
+ --leafer-roi-color-text-tertiary: #999999; /* 三级文本色 */
228
+ --leafer-roi-color-border: #ddd; /* 边框色 */
229
+ --leafer-roi-color-border-light: #e0e0e0; /* 浅边框色 */
230
+ --leafer-roi-color-error: #e74c3c; /* 错误色 */
231
+ --leafer-roi-color-button: #3498db; /* 按钮色 */
232
+ --leafer-roi-color-button-hover: #2980b9; /* 按钮悬停色 */
233
+
234
+ /* 尺寸变量 */
235
+ --leafer-roi-padding-toolbar: 10px; /* 工具栏内边距 */
236
+ --leafer-roi-padding-tool-button: 8px; /* 工具按钮内边距 */
237
+ --leafer-roi-size-tool-icon: 18px; /* 工具图标尺寸 */
238
+ --leafer-roi-size-zoom-button: 36px; /* 缩放按钮尺寸 */
239
+ --leafer-roi-size-zoom-value: 60px; /* 缩放值显示宽度 */
240
+ --leafer-roi-font-size-hotkey: 10px; /* 热键提示字体大小 */
241
+ --leafer-roi-padding-hotkey: 1px 3px; /* 热键提示内边距 */
242
+ --leafer-roi-padding-error: 20px; /* 错误提示内边距 */
243
+ --leafer-roi-padding-error-button: 8px 16px; /* 错误提示按钮内边距 */
244
+
245
+ /* 边框圆角 */
246
+ --leafer-roi-border-radius-tool-button: 4px; /* 工具按钮圆角 */
247
+ --leafer-roi-border-radius-hotkey: 2px; /* 热键提示圆角 */
248
+ --leafer-roi-border-radius-overlay: 8px; /* 遮罩圆角 */
249
+ --leafer-roi-border-radius-zoom: 8px; /* 缩放控制器圆角 */
250
+
251
+ /* 阴影 */
252
+ --leafer-roi-shadow-tool-button: 0 2px 4px rgba(0, 0, 0, 0.1); /* 工具按钮阴影 */
253
+ --leafer-roi-shadow-tool-button-active: 0 2px 4px rgba(0, 122, 255, 0.3); /* 工具按钮激活阴影 */
254
+ --leafer-roi-shadow-tool-button-hover: 0 4px 6px rgba(0, 0, 0, 0.1); /* 工具按钮悬停阴影 */
255
+ --leafer-roi-shadow-overlay: 0 4px 12px rgba(0, 0, 0, 0.1); /* 遮罩阴影 */
256
+ --leafer-roi-shadow-zoom: 0 2px 8px rgba(0, 0, 0, 0.15); /* 缩放控制器阴影 */
257
+
258
+ /* 动画 */
259
+ --leafer-roi-transition-time: 0.2s; /* 过渡动画时长 */
260
+ --leafer-roi-animation-gradient: 2s; /* 渐变动画时长 */
261
+ }
262
+ ```
263
+
264
+ ## 事件
265
+
266
+ - `roiChange`:当 ROI 发生变化时触发
267
+ - `loadStart`:当图片开始加载时触发
268
+ - `loadSuccess`:当图片加载成功时触发
269
+ - `loadError`:当图片加载失败时触发
270
+ - `undoStateChange`:当撤销状态变化时触发
271
+ - `redoStateChange`:当重做状态变化时触发
272
+
273
+ ## 浏览器兼容性
274
+
275
+ - Chrome 60+
276
+ - Firefox 55+
277
+ - Safari 12+
278
+ - Edge 79+
279
+
280
+ ## 依赖
281
+
282
+ - Vue 3.3.0+
283
+ - LeaferUI 2.0.8+
284
+ - Tinykeys 3.0.0+
285
+ - @zzalai/leafer-undo-redo 1.0.3+
286
+
287
+ ## 许可证
288
+
289
+ MIT License
290
+
291
+ ## 贡献
292
+
293
+ 欢迎提交 Issue 和 Pull Request!
package/README_EN.md ADDED
@@ -0,0 +1,293 @@
1
+ # LeaferJS Multi ROI Component
2
+
3
+ [中文](README.md) | English
4
+
5
+ A Vue3 component based on LeaferJS for multi-region selection on images, used for area annotation and editing on images.
6
+
7
+ ## Features
8
+
9
+ - 🖼️ Support image loading and display
10
+ - 📐 Support drawing, editing and deleting rectangular regions
11
+ - 🔍 Support canvas zooming and dragging
12
+ - ⌨️ Support keyboard hotkey operations
13
+ - 🔄 Support undo/redo functionality
14
+ - 📤 Support canvas information JSON export and import
15
+ - 🎨 Support CSS variables for custom styling
16
+
17
+ ## Installation
18
+
19
+ ### Using npm
20
+
21
+ ```bash
22
+ npm install @zzalai/leafer-multi-roi
23
+ ```
24
+
25
+ ### Using yarn
26
+
27
+ ```bash
28
+ yarn add @zzalai/leafer-multi-roi
29
+ ```
30
+
31
+ ### Using pnpm
32
+
33
+ ```bash
34
+ pnpm add @zzalai/leafer-multi-roi
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Global Registration
40
+
41
+ ```javascript
42
+ // main.ts
43
+ import { createApp } from 'vue'
44
+ import App from './App.vue'
45
+ import RoiEditor from '@zzalai/leafer-multi-roi'
46
+
47
+ const app = createApp(App)
48
+ app.use(RoiEditor)
49
+ app.mount('#app')
50
+ ```
51
+
52
+ ### Local Usage
53
+
54
+ ```vue
55
+ <template>
56
+ <div class="app">
57
+ <RoiEditor
58
+ :imageSrc="imageSrc"
59
+ :options="editorOptions"
60
+ @roiChange="handleRoiChange"
61
+ @loadStart="handleLoadStart"
62
+ @loadSuccess="handleLoadSuccess"
63
+ @loadError="handleLoadError"
64
+ />
65
+ </div>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { ref, computed } from 'vue'
70
+ import { RoiEditor } from '@zzalai/leafer-multi-roi'
71
+
72
+ // Image source
73
+ const imageUrl = ref('https://picsum.photos/1280/1080')
74
+ const imageSrc = computed(() => ({
75
+ id: 'test-image',
76
+ url: imageUrl.value
77
+ }))
78
+
79
+ // Editor options
80
+ const editorOptions = ref({
81
+ regionStyle: {
82
+ fill: 'rgba(100, 149, 237, 0.3)',
83
+ stroke: 'rgba(100, 149, 237, 0.8)',
84
+ strokeWidth: 2
85
+ },
86
+ selectedRegionStyle: {
87
+ fill: 'rgba(100, 149, 237, 0.5)',
88
+ stroke: 'rgba(100, 149, 237, 1)',
89
+ strokeWidth: 2
90
+ },
91
+ maxRegions: 20, // Maximum number of regions, default is 20
92
+ maxUndoSteps: 100 // Maximum undo/redo steps, default is 100
93
+ })
94
+
95
+ // Handle ROI change
96
+ const handleRoiChange = (data: any) => {
97
+ console.log('ROI changed:', data)
98
+ }
99
+
100
+ // Handle image load start
101
+ const handleLoadStart = () => {
102
+ console.log('Image load started')
103
+ }
104
+
105
+ // Handle image load success
106
+ const handleLoadSuccess = () => {
107
+ console.log('Image load success')
108
+ }
109
+
110
+ // Handle image load error
111
+ const handleLoadError = (error: any) => {
112
+ console.error('Image load error:', error)
113
+ }
114
+ </script>
115
+ ```
116
+
117
+ ### Manual Method Calls
118
+
119
+ ```vue
120
+ <template>
121
+ <div class="app">
122
+ <RoiEditor
123
+ ref="roiEditor"
124
+ :imageSrc="imageSrc"
125
+ />
126
+ <button @click="reloadImage">Reload Image</button>
127
+ <button @click="exportCanvas">Export Canvas</button>
128
+ <input type="file" @change="importCanvas" accept=".json" />
129
+ </div>
130
+ </template>
131
+
132
+ <script setup lang="ts">
133
+ import { ref, computed } from 'vue'
134
+ import { RoiEditor } from '@zzalai/leafer-multi-roi'
135
+
136
+ const roiEditor = ref<InstanceType<typeof RoiEditor> | null>(null)
137
+ const imageUrl = ref('https://picsum.photos/1280/1080')
138
+ const imageSrc = computed(() => ({
139
+ id: 'test-image',
140
+ url: imageUrl.value
141
+ }))
142
+
143
+ // Reload image
144
+ const reloadImage = () => {
145
+ roiEditor.value?.loadImage()
146
+ }
147
+
148
+ // Export canvas
149
+ const exportCanvas = () => {
150
+ const json = roiEditor.value?.exportCanvasJSON()
151
+ if (json) {
152
+ const blob = new Blob([json], { type: 'application/json' })
153
+ const url = URL.createObjectURL(blob)
154
+ const a = document.createElement('a')
155
+ a.href = url
156
+ a.download = 'canvas.json'
157
+ a.click()
158
+ URL.revokeObjectURL(url)
159
+ }
160
+ }
161
+
162
+ // Import canvas
163
+ const importCanvas = async (event: Event) => {
164
+ const target = event.target as HTMLInputElement
165
+ const file = target.files?.[0]
166
+ if (file) {
167
+ const reader = new FileReader()
168
+ reader.onload = async (e) => {
169
+ const jsonString = e.target?.result as string
170
+ if (roiEditor.value) {
171
+ const success = await roiEditor.value.importCanvasJSON(jsonString, { resetZoom: true })
172
+ if (success) {
173
+ alert('Canvas imported successfully!')
174
+ } else {
175
+ alert('Failed to import canvas.')
176
+ }
177
+ }
178
+ }
179
+ reader.readAsText(file)
180
+ }
181
+ target.value = ''
182
+ }
183
+ </script>
184
+ ```
185
+
186
+ ## Application Scenarios
187
+
188
+ - **Image Annotation**:Annotate regions of interest on images
189
+ - **Object Detection**:Generate training data for object detection models
190
+ - **Image Analysis**:Mark specific regions in images for analysis
191
+ - **Medical Imaging**:Mark lesion areas on medical images
192
+ - **E-commerce Products**:Mark different parts on product images
193
+
194
+ ## Hotkey Operations
195
+
196
+ | Hotkey | Function |
197
+ |--------|----------|
198
+ | V | Select tool |
199
+ | M | Rectangle tool |
200
+ | Ctrl+Z | Undo |
201
+ | Ctrl+Y | Redo |
202
+ | Delete | Delete selected region |
203
+ | Ctrl++ | Zoom in |
204
+ | Ctrl+- | Zoom out |
205
+ | Ctrl+0 | Reset zoom |
206
+ | Alt | Show/hide hotkey hints |
207
+
208
+ ## Exposed Methods
209
+
210
+ - `getROIAnnotations()`:Get all ROI annotation data
211
+ - `getImageInfo()`:Get image information
212
+ - `exportCanvasJSON()`:Export canvas information as JSON string
213
+ - `importCanvasJSON(jsonString, options)`:Import canvas information from JSON string
214
+ - `loadImage()`:Manually load image
215
+
216
+ ## CSS Customizable Variables
217
+
218
+ ```css
219
+ :root {
220
+ /* Color variables */
221
+ --leafer-roi-color-primary: #007aff; /* Primary color */
222
+ --leafer-roi-color-background: #f5f5f5; /* Background color */
223
+ --leafer-roi-color-background-light: #f0f0f0; /* Light background color */
224
+ --leafer-roi-color-white: #fff; /* White color */
225
+ --leafer-roi-color-text: #333; /* Text color */
226
+ --leafer-roi-color-text-secondary: #666; /* Secondary text color */
227
+ --leafer-roi-color-text-tertiary: #999999; /* Tertiary text color */
228
+ --leafer-roi-color-border: #ddd; /* Border color */
229
+ --leafer-roi-color-border-light: #e0e0e0; /* Light border color */
230
+ --leafer-roi-color-error: #e74c3c; /* Error color */
231
+ --leafer-roi-color-button: #3498db; /* Button color */
232
+ --leafer-roi-color-button-hover: #2980b9; /* Button hover color */
233
+
234
+ /* Size variables */
235
+ --leafer-roi-padding-toolbar: 10px; /* Toolbar padding */
236
+ --leafer-roi-padding-tool-button: 8px; /* Tool button padding */
237
+ --leafer-roi-size-tool-icon: 18px; /* Tool icon size */
238
+ --leafer-roi-size-zoom-button: 36px; /* Zoom button size */
239
+ --leafer-roi-size-zoom-value: 60px; /* Zoom value display width */
240
+ --leafer-roi-font-size-hotkey: 10px; /* Hotkey hint font size */
241
+ --leafer-roi-padding-hotkey: 1px 3px; /* Hotkey hint padding */
242
+ --leafer-roi-padding-error: 20px; /* Error hint padding */
243
+ --leafer-roi-padding-error-button: 8px 16px; /* Error hint button padding */
244
+
245
+ /* Border radius */
246
+ --leafer-roi-border-radius-tool-button: 4px; /* Tool button border radius */
247
+ --leafer-roi-border-radius-hotkey: 2px; /* Hotkey hint border radius */
248
+ --leafer-roi-border-radius-overlay: 8px; /* Overlay border radius */
249
+ --leafer-roi-border-radius-zoom: 8px; /* Zoom controller border radius */
250
+
251
+ /* Shadows */
252
+ --leafer-roi-shadow-tool-button: 0 2px 4px rgba(0, 0, 0, 0.1); /* Tool button shadow */
253
+ --leafer-roi-shadow-tool-button-active: 0 2px 4px rgba(0, 122, 255, 0.3); /* Tool button active shadow */
254
+ --leafer-roi-shadow-tool-button-hover: 0 4px 6px rgba(0, 0, 0, 0.1); /* Tool button hover shadow */
255
+ --leafer-roi-shadow-overlay: 0 4px 12px rgba(0, 0, 0, 0.1); /* Overlay shadow */
256
+ --leafer-roi-shadow-zoom: 0 2px 8px rgba(0, 0, 0, 0.15); /* Zoom controller shadow */
257
+
258
+ /* Animations */
259
+ --leafer-roi-transition-time: 0.2s; /* Transition animation duration */
260
+ --leafer-roi-animation-gradient: 2s; /* Gradient animation duration */
261
+ }
262
+ ```
263
+
264
+ ## Events
265
+
266
+ - `roiChange`:Triggered when ROI changes
267
+ - `loadStart`:Triggered when image starts loading
268
+ - `loadSuccess`:Triggered when image loads successfully
269
+ - `loadError`:Triggered when image fails to load
270
+ - `undoStateChange`:Triggered when undo state changes
271
+ - `redoStateChange`:Triggered when redo state changes
272
+
273
+ ## Browser Compatibility
274
+
275
+ - Chrome 60+
276
+ - Firefox 55+
277
+ - Safari 12+
278
+ - Edge 79+
279
+
280
+ ## Dependencies
281
+
282
+ - Vue 3.3.0+
283
+ - LeaferUI 2.0.8+
284
+ - Tinykeys 3.0.0+
285
+ - @zzalai/leafer-undo-redo 1.0.3+
286
+
287
+ ## License
288
+
289
+ MIT License
290
+
291
+ ## Contribution
292
+
293
+ Welcome to submit Issues and Pull Requests!
@@ -0,0 +1 @@
1
+ :root{--leafer-roi-color-primary:#007aff;--leafer-roi-color-background:#f5f5f5;--leafer-roi-color-background-light:#f0f0f0;--leafer-roi-color-white:#fff;--leafer-roi-color-text:#333;--leafer-roi-color-text-secondary:#666;--leafer-roi-color-text-tertiary:#999;--leafer-roi-color-border:#ddd;--leafer-roi-color-border-light:#e0e0e0;--leafer-roi-color-error:#e74c3c;--leafer-roi-color-button:#3498db;--leafer-roi-color-button-hover:#2980b9;--leafer-roi-padding-toolbar:10px;--leafer-roi-padding-tool-button:8px;--leafer-roi-size-tool-icon:18px;--leafer-roi-size-zoom-button:36px;--leafer-roi-size-zoom-value:60px;--leafer-roi-font-size-hotkey:10px;--leafer-roi-padding-hotkey:1px 3px;--leafer-roi-padding-error:20px;--leafer-roi-padding-error-button:8px 16px;--leafer-roi-border-radius-tool-button:4px;--leafer-roi-border-radius-hotkey:2px;--leafer-roi-border-radius-overlay:8px;--leafer-roi-border-radius-zoom:8px;--leafer-roi-shadow-tool-button:0 2px 4px #0000001a;--leafer-roi-shadow-tool-button-active:0 2px 4px #007aff4d;--leafer-roi-shadow-tool-button-hover:0 4px 6px #0000001a;--leafer-roi-shadow-overlay:0 4px 12px #0000001a;--leafer-roi-shadow-zoom:0 2px 8px #00000026;--leafer-roi-transition-time:.2s;--leafer-roi-animation-gradient:2s}.roi-editor[data-v-9c4af5f9]{width:100%;height:100%}.canvas-container[data-v-9c4af5f9]{outline:none;width:100%;height:calc(100% - 55px);position:relative;overflow:hidden}.canvas-container[data-v-9c4af5f9]:focus{outline:2px solid var(--leafer-roi-color-primary);outline-offset:-2px}.loading-overlay[data-v-9c4af5f9]{background-color:var(--leafer-roi-color-background-light);border-radius:var(--leafer-roi-border-radius-overlay);box-shadow:var(--leafer-roi-shadow-overlay);z-index:1000;justify-content:center;align-items:center;min-width:100%;min-height:100%;display:flex;position:absolute;top:50%;left:50%;overflow:hidden;transform:translate(-50%,-50%)}.gradient-animation[data-v-9c4af5f9]{animation:gradientShift-9c4af5f9 var(--leafer-roi-animation-gradient) ease-in-out infinite;opacity:.7;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%) 0 0/200% 200%;position:absolute;inset:0}@keyframes gradientShift-9c4af5f9{0%{background-position:0%}50%{background-position:100%}to{background-position:0%}}.loading-text[data-v-9c4af5f9]{z-index:1;color:#fff;text-shadow:0 2px 4px #0003;font-size:16px;font-weight:500;position:relative}.error-overlay[data-v-9c4af5f9]{background-color:var(--leafer-roi-color-white);border-radius:var(--leafer-roi-border-radius-overlay);box-shadow:var(--leafer-roi-shadow-overlay);padding:var(--leafer-roi-padding-error);z-index:1000;flex-direction:column;justify-content:center;align-items:center;min-width:200px;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.error-overlay p[data-v-9c4af5f9]{color:var(--leafer-roi-color-error);margin-bottom:20px;font-size:16px}.error-overlay button[data-v-9c4af5f9]{padding:var(--leafer-roi-padding-error-button);background-color:var(--leafer-roi-color-button);color:#fff;border-radius:var(--leafer-roi-border-radius-tool-button);cursor:pointer;border:none;font-size:14px}.error-overlay button[data-v-9c4af5f9]:hover{background-color:var(--leafer-roi-color-button-hover)}.zoom-controller[data-v-9c4af5f9]{background-color:var(--leafer-roi-color-white);border-radius:var(--leafer-roi-border-radius-zoom);box-shadow:var(--leafer-roi-shadow-zoom);z-index:100;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;overflow:hidden}.zoom-button[data-v-9c4af5f9]{width:var(--leafer-roi-size-zoom-button);height:var(--leafer-roi-size-zoom-button);background-color:var(--leafer-roi-color-white);color:var(--leafer-roi-color-text);cursor:pointer;transition:all var(--leafer-roi-transition-time) ease;border:none;justify-content:center;align-items:center;display:flex;position:relative}.zoom-button[data-v-9c4af5f9]:hover{background-color:var(--leafer-roi-color-background-light);color:var(--leafer-roi-color-primary)}.zoom-button[data-v-9c4af5f9]:active{background-color:#e0e0e0}.zoom-value[data-v-9c4af5f9]{min-width:var(--leafer-roi-size-zoom-value);height:var(--leafer-roi-size-zoom-button);line-height:var(--leafer-roi-size-zoom-button);text-align:center;color:var(--leafer-roi-color-text);cursor:pointer;border-left:1px solid var(--leafer-roi-color-border-light);border-right:1px solid var(--leafer-roi-color-border-light);transition:all var(--leafer-roi-transition-time) ease;font-size:14px;font-weight:500;position:relative}.zoom-value .hotkey-hint[data-v-9c4af5f9]{line-height:1}.zoom-value[data-v-9c4af5f9]:hover{background-color:var(--leafer-roi-color-background-light);color:var(--leafer-roi-color-primary)}.toolbar[data-v-9c4af5f9]{padding:var(--leafer-roi-padding-toolbar);background-color:var(--leafer-roi-color-background);border-top:1px solid var(--leafer-roi-color-border);justify-content:center;align-items:center;gap:10px;display:flex}.tool-button[data-v-9c4af5f9]{padding:var(--leafer-roi-padding-tool-button);border-radius:var(--leafer-roi-border-radius-tool-button);background-color:var(--leafer-roi-color-white);color:var(--leafer-roi-color-text);cursor:pointer;box-shadow:var(--leafer-roi-shadow-tool-button);transition:all var(--leafer-roi-transition-time) ease;border:none;justify-content:center;align-items:center;display:flex;position:relative}.tool-button[data-v-9c4af5f9]:hover{box-shadow:var(--leafer-roi-shadow-tool-button-hover);background-color:#e3e3e3;transform:translateY(-1px)}.tool-button[data-v-9c4af5f9]:active{box-shadow:var(--leafer-roi-shadow-tool-button);transform:translateY(0)}.tool-button.active[data-v-9c4af5f9]{background-color:var(--leafer-roi-color-primary);color:#fff;box-shadow:var(--leafer-roi-shadow-tool-button-active)}.tool-button svg[data-v-9c4af5f9]{width:var(--leafer-roi-size-tool-icon);height:var(--leafer-roi-size-tool-icon)}.hotkey-hint[data-v-9c4af5f9]{font-size:var(--leafer-roi-font-size-hotkey);color:var(--leafer-roi-color-text-tertiary);background-color:var(--leafer-roi-color-background);padding:var(--leafer-roi-padding-hotkey);border-radius:var(--leafer-roi-border-radius-hotkey);pointer-events:none;font-weight:700;position:absolute;bottom:2px;right:2px}.tool-button.active .hotkey-hint[data-v-9c4af5f9]{color:#fffc;background-color:#0003}.app[data-v-4b675dba]{max-width:1200px;margin:0 auto;padding:20px;font-family:Arial,sans-serif}h1[data-v-4b675dba]{text-align:center;margin-bottom:30px}.editor-container[data-v-4b675dba]{border:1px solid #ddd;border-radius:8px;width:100%;height:600px;margin-bottom:30px;overflow:hidden}.controls[data-v-4b675dba]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}.control-group[data-v-4b675dba]{margin-bottom:15px}label[data-v-4b675dba]{margin-bottom:5px;font-weight:700;display:block}input[data-v-4b675dba]{border:1px solid #ddd;border-radius:4px;width:100%;margin-bottom:10px;padding:8px}button[data-v-4b675dba]{color:#fff;cursor:pointer;background-color:#007bff;border:none;border-radius:4px;margin-right:10px;padding:8px 16px}button[data-v-4b675dba]:hover{background-color:#0069d9}.output[data-v-4b675dba]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}pre[data-v-4b675dba]{white-space:pre-wrap;word-wrap:break-word;background-color:#fff;border-radius:4px;max-height:300px;padding:15px;overflow-y:auto}.status[data-v-4b675dba]{background-color:#f5f5f5;border-radius:8px;padding:20px}