@fastkit/vui-wysiwyg 7.1.0 → 7.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/README.md +1011 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1 +1,1012 @@
|
|
|
1
|
+
|
|
1
2
|
# @fastkit/vui-wysiwyg
|
|
3
|
+
|
|
4
|
+
🌐 English | [日本語](https://github.com/dadajam4/fastkit/blob/main/packages/vui-wysiwyg/README-ja.md)
|
|
5
|
+
|
|
6
|
+
A high-featured WYSIWYG editor component for Vue.js 3. Built on Tiptap and ProseMirror, it provides rich editing capabilities and customizability, fully integrated with the @fastkit/vui design system.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Rich Text Editing**: Comprehensive editing features including text formatting, lists, links, coloring, etc.
|
|
11
|
+
- **@fastkit/vui Integration**: Complete integration with the vui design system and component compatibility
|
|
12
|
+
- **Customizable Toolbar**: Support for both fixed toolbar and floating menu
|
|
13
|
+
- **Extension System**: Powerful extension architecture based on Tiptap/ProseMirror
|
|
14
|
+
- **Form Controls**: Standard form validation and input control functionality
|
|
15
|
+
- **Complete TypeScript Support**: Type safety through strict type definitions
|
|
16
|
+
- **Accessibility**: Keyboard navigation and screen reader support
|
|
17
|
+
- **SSR Support**: Full server-side rendering support
|
|
18
|
+
- **High Performance**: Efficient virtual DOM manipulation and optimized rendering
|
|
19
|
+
- **Custom Styling**: Detailed style customization through Sass variables
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @fastkit/vui-wysiwyg
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Basic Usage
|
|
28
|
+
|
|
29
|
+
### Basic Editor
|
|
30
|
+
|
|
31
|
+
```vue
|
|
32
|
+
<template>
|
|
33
|
+
<div class="editor-container">
|
|
34
|
+
<VWysiwygEditor
|
|
35
|
+
v-model="content"
|
|
36
|
+
label="Article Content"
|
|
37
|
+
placeholder="Enter content here..."
|
|
38
|
+
:tools="defaultTools"
|
|
39
|
+
:extensions="extensions"
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<div class="preview">
|
|
43
|
+
<h3>Preview</h3>
|
|
44
|
+
<div v-html="content"></div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup lang="ts">
|
|
50
|
+
import { ref } from 'vue'
|
|
51
|
+
import { VWysiwygEditor } from '@fastkit/vui-wysiwyg'
|
|
52
|
+
import {
|
|
53
|
+
WysiwygFormatBoldTool,
|
|
54
|
+
WysiwygFormatItalicTool,
|
|
55
|
+
WysiwygFormatUnderlineTool,
|
|
56
|
+
WysiwygBulletListTool,
|
|
57
|
+
WysiwygOrderedListTool,
|
|
58
|
+
WysiwygLinkTool,
|
|
59
|
+
WysiwygHistoryTool,
|
|
60
|
+
} from '@fastkit/vui-wysiwyg'
|
|
61
|
+
|
|
62
|
+
const content = ref('<p>Initial content</p>')
|
|
63
|
+
|
|
64
|
+
// Basic tool set
|
|
65
|
+
const defaultTools = [
|
|
66
|
+
WysiwygFormatBoldTool,
|
|
67
|
+
WysiwygFormatItalicTool,
|
|
68
|
+
WysiwygFormatUnderlineTool,
|
|
69
|
+
WysiwygBulletListTool,
|
|
70
|
+
WysiwygOrderedListTool,
|
|
71
|
+
WysiwygLinkTool,
|
|
72
|
+
WysiwygHistoryTool,
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
const extensions = []
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<style scoped>
|
|
79
|
+
.editor-container {
|
|
80
|
+
max-width: 800px;
|
|
81
|
+
margin: 0 auto;
|
|
82
|
+
padding: 20px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.preview {
|
|
86
|
+
margin-top: 30px;
|
|
87
|
+
padding: 20px;
|
|
88
|
+
border: 1px solid #e1e5e9;
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
background-color: #f8f9fa;
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Customized Editor Configuration
|
|
96
|
+
|
|
97
|
+
```vue
|
|
98
|
+
<template>
|
|
99
|
+
<div class="advanced-editor">
|
|
100
|
+
<VWysiwygEditor
|
|
101
|
+
v-model="articleContent"
|
|
102
|
+
label="Article Editor"
|
|
103
|
+
hint="Markdown can also be used"
|
|
104
|
+
:tools="advancedTools"
|
|
105
|
+
:extensions="customExtensions"
|
|
106
|
+
:floating-toolbar="false"
|
|
107
|
+
:disabled-min-height="false"
|
|
108
|
+
:disabled-max-height="false"
|
|
109
|
+
:remove-default-wrapper="true"
|
|
110
|
+
:max-length="5000"
|
|
111
|
+
counter
|
|
112
|
+
required
|
|
113
|
+
size="large"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<script setup lang="ts">
|
|
119
|
+
import { ref } from 'vue'
|
|
120
|
+
import {
|
|
121
|
+
VWysiwygEditor,
|
|
122
|
+
WysiwygFormatBoldTool,
|
|
123
|
+
WysiwygFormatItalicTool,
|
|
124
|
+
WysiwygFormatUnderlineTool,
|
|
125
|
+
WysiwygBulletListTool,
|
|
126
|
+
WysiwygOrderedListTool,
|
|
127
|
+
WysiwygLinkTool,
|
|
128
|
+
WysiwygHistoryTool,
|
|
129
|
+
WysiwygColorTool,
|
|
130
|
+
WysiwygTextAlignTool,
|
|
131
|
+
WysiwygCustomTagTool,
|
|
132
|
+
createWysiwygExtension,
|
|
133
|
+
} from '@fastkit/vui-wysiwyg'
|
|
134
|
+
import { TextAlign } from '@tiptap/extension-text-align'
|
|
135
|
+
import { Color } from '@tiptap/extension-color'
|
|
136
|
+
|
|
137
|
+
const articleContent = ref('')
|
|
138
|
+
|
|
139
|
+
// Advanced tool set
|
|
140
|
+
const advancedTools = [
|
|
141
|
+
WysiwygFormatBoldTool,
|
|
142
|
+
WysiwygFormatItalicTool,
|
|
143
|
+
WysiwygFormatUnderlineTool,
|
|
144
|
+
WysiwygBulletListTool,
|
|
145
|
+
WysiwygOrderedListTool,
|
|
146
|
+
WysiwygLinkTool,
|
|
147
|
+
WysiwygColorTool,
|
|
148
|
+
WysiwygTextAlignTool,
|
|
149
|
+
WysiwygCustomTagTool,
|
|
150
|
+
WysiwygHistoryTool,
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
// Custom extensions
|
|
154
|
+
const customExtensions = [
|
|
155
|
+
createWysiwygExtension(TextAlign.configure({
|
|
156
|
+
types: ['heading', 'paragraph'],
|
|
157
|
+
alignments: ['left', 'center', 'right', 'justify'],
|
|
158
|
+
})),
|
|
159
|
+
createWysiwygExtension(Color.configure({
|
|
160
|
+
types: ['textStyle'],
|
|
161
|
+
})),
|
|
162
|
+
]
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<style scoped>
|
|
166
|
+
.advanced-editor {
|
|
167
|
+
max-width: 1000px;
|
|
168
|
+
margin: 0 auto;
|
|
169
|
+
padding: 30px;
|
|
170
|
+
}
|
|
171
|
+
</style>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Component API and Event Handling
|
|
175
|
+
|
|
176
|
+
```vue
|
|
177
|
+
<template>
|
|
178
|
+
<div class="api-example">
|
|
179
|
+
<VWysiwygEditor
|
|
180
|
+
ref="editorRef"
|
|
181
|
+
v-model="content"
|
|
182
|
+
label="API-Integrated Editor"
|
|
183
|
+
:tools="tools"
|
|
184
|
+
@input="handleInput"
|
|
185
|
+
@focus="handleFocus"
|
|
186
|
+
@blur="handleBlur"
|
|
187
|
+
@update:modelValue="handleUpdate"
|
|
188
|
+
/>
|
|
189
|
+
|
|
190
|
+
<div class="editor-controls">
|
|
191
|
+
<VButton @click="insertText">Insert Text</VButton>
|
|
192
|
+
<VButton @click="focusEditor">Focus</VButton>
|
|
193
|
+
<VButton @click="getContent">Get Content</VButton>
|
|
194
|
+
<VButton @click="clearContent">Clear</VButton>
|
|
195
|
+
<VButton @click="formatSelection">Bold Selection</VButton>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="editor-stats">
|
|
199
|
+
<div>Character count: {{ textLength }}</div>
|
|
200
|
+
<div>HTML length: {{ htmlLength }}</div>
|
|
201
|
+
<div>Focus state: {{ isFocused ? 'Focused' : 'Unfocused' }}</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</template>
|
|
205
|
+
|
|
206
|
+
<script setup lang="ts">
|
|
207
|
+
import { ref, computed } from 'vue'
|
|
208
|
+
import {
|
|
209
|
+
VWysiwygEditor,
|
|
210
|
+
VWysiwygEditorAPI,
|
|
211
|
+
WysiwygFormatBoldTool,
|
|
212
|
+
WysiwygFormatItalicTool,
|
|
213
|
+
WysiwygHistoryTool,
|
|
214
|
+
} from '@fastkit/vui-wysiwyg'
|
|
215
|
+
import { VButton } from '@fastkit/vui'
|
|
216
|
+
|
|
217
|
+
const editorRef = ref<VWysiwygEditorAPI>()
|
|
218
|
+
const content = ref('<p>Sample text</p>')
|
|
219
|
+
const isFocused = ref(false)
|
|
220
|
+
|
|
221
|
+
const tools = [
|
|
222
|
+
WysiwygFormatBoldTool,
|
|
223
|
+
WysiwygFormatItalicTool,
|
|
224
|
+
WysiwygHistoryTool,
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
const textLength = computed(() => {
|
|
228
|
+
const editor = editorRef.value?.editor
|
|
229
|
+
return editor ? editor.getText().length : 0
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const htmlLength = computed(() => content.value.length)
|
|
233
|
+
|
|
234
|
+
// Event handlers
|
|
235
|
+
const handleInput = (value: string) => {
|
|
236
|
+
console.log('Input changed:', value)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const handleFocus = (event: FocusEvent) => {
|
|
240
|
+
isFocused.value = true
|
|
241
|
+
console.log('Editor focused:', event)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const handleBlur = (event: FocusEvent) => {
|
|
245
|
+
isFocused.value = false
|
|
246
|
+
console.log('Editor blurred:', event)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const handleUpdate = (value: string) => {
|
|
250
|
+
console.log('Content updated:', value)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Editor operation methods
|
|
254
|
+
const insertText = () => {
|
|
255
|
+
const editor = editorRef.value?.editor
|
|
256
|
+
if (editor) {
|
|
257
|
+
editor.chain().focus().insertContent(' inserted text ').run()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const focusEditor = () => {
|
|
262
|
+
const editor = editorRef.value?.editor
|
|
263
|
+
if (editor) {
|
|
264
|
+
editor.chain().focus('end').run()
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const getContent = () => {
|
|
269
|
+
const editor = editorRef.value?.editor
|
|
270
|
+
if (editor) {
|
|
271
|
+
const html = editor.getHTML()
|
|
272
|
+
const text = editor.getText()
|
|
273
|
+
console.log('HTML:', html)
|
|
274
|
+
console.log('Text:', text)
|
|
275
|
+
alert(`HTML: ${html.slice(0, 100)}...\nText: ${text.slice(0, 100)}...`)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const clearContent = () => {
|
|
280
|
+
const editor = editorRef.value?.editor
|
|
281
|
+
if (editor) {
|
|
282
|
+
editor.chain().focus().clearContent().run()
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const formatSelection = () => {
|
|
287
|
+
const editor = editorRef.value?.editor
|
|
288
|
+
if (editor) {
|
|
289
|
+
editor.chain().focus().toggleBold().run()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
</script>
|
|
293
|
+
|
|
294
|
+
<style scoped>
|
|
295
|
+
.api-example {
|
|
296
|
+
max-width: 800px;
|
|
297
|
+
margin: 0 auto;
|
|
298
|
+
padding: 20px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.editor-controls {
|
|
302
|
+
margin: 20px 0;
|
|
303
|
+
display: flex;
|
|
304
|
+
gap: 10px;
|
|
305
|
+
flex-wrap: wrap;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.editor-stats {
|
|
309
|
+
margin-top: 20px;
|
|
310
|
+
padding: 15px;
|
|
311
|
+
background-color: #f8f9fa;
|
|
312
|
+
border-radius: 8px;
|
|
313
|
+
font-family: monospace;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.editor-stats > div {
|
|
317
|
+
margin: 5px 0;
|
|
318
|
+
}
|
|
319
|
+
</style>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Creating Custom Extensions
|
|
323
|
+
|
|
324
|
+
### Creating Custom Marks
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// custom-extensions/highlight.ts
|
|
328
|
+
import { Mark, mergeAttributes } from '@tiptap/core'
|
|
329
|
+
import { WysiwygExtensionFactory } from '@fastkit/vui-wysiwyg'
|
|
330
|
+
|
|
331
|
+
export interface HighlightOptions {
|
|
332
|
+
multicolor: boolean
|
|
333
|
+
HTMLAttributes: Record<string, any>
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
declare module '@tiptap/core' {
|
|
337
|
+
interface Commands<ReturnType> {
|
|
338
|
+
highlight: {
|
|
339
|
+
setHighlight: (attributes?: { color?: string }) => ReturnType
|
|
340
|
+
toggleHighlight: (attributes?: { color?: string }) => ReturnType
|
|
341
|
+
unsetHighlight: () => ReturnType
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export const Highlight = Mark.create<HighlightOptions>({
|
|
347
|
+
name: 'highlight',
|
|
348
|
+
|
|
349
|
+
addOptions() {
|
|
350
|
+
return {
|
|
351
|
+
multicolor: false,
|
|
352
|
+
HTMLAttributes: {},
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
addAttributes() {
|
|
357
|
+
if (!this.options.multicolor) {
|
|
358
|
+
return {}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
color: {
|
|
363
|
+
default: null,
|
|
364
|
+
parseHTML: element => element.getAttribute('data-color') || element.style.backgroundColor,
|
|
365
|
+
renderHTML: attributes => {
|
|
366
|
+
if (!attributes.color) return {}
|
|
367
|
+
return {
|
|
368
|
+
'data-color': attributes.color,
|
|
369
|
+
style: `background-color: ${attributes.color}; color: inherit`,
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
parseHTML() {
|
|
377
|
+
return [
|
|
378
|
+
{
|
|
379
|
+
tag: 'mark',
|
|
380
|
+
},
|
|
381
|
+
]
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
renderHTML({ HTMLAttributes }) {
|
|
385
|
+
return ['mark', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
addCommands() {
|
|
389
|
+
return {
|
|
390
|
+
setHighlight:
|
|
391
|
+
attributes =>
|
|
392
|
+
({ commands }) => {
|
|
393
|
+
return commands.setMark(this.name, attributes)
|
|
394
|
+
},
|
|
395
|
+
toggleHighlight:
|
|
396
|
+
attributes =>
|
|
397
|
+
({ commands }) => {
|
|
398
|
+
return commands.toggleMark(this.name, attributes)
|
|
399
|
+
},
|
|
400
|
+
unsetHighlight:
|
|
401
|
+
() =>
|
|
402
|
+
({ commands }) => {
|
|
403
|
+
return commands.unsetMark(this.name)
|
|
404
|
+
},
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
// Custom extensions factory
|
|
410
|
+
export const CustomHighlightExtension: WysiwygExtensionFactory<HighlightOptions> = (ctx) => {
|
|
411
|
+
return Highlight.configure({
|
|
412
|
+
multicolor: true,
|
|
413
|
+
HTMLAttributes: {
|
|
414
|
+
class: 'custom-highlight',
|
|
415
|
+
},
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Creating Custom Tools
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// custom-tools/highlight-tool.ts
|
|
424
|
+
import { WysiwygEditorToolFactory, WysiwygEditorTool } from '@fastkit/vui-wysiwyg'
|
|
425
|
+
import { CustomHighlightExtension } from '../custom-extensions/highlight'
|
|
426
|
+
|
|
427
|
+
export interface HighlightToolOptions {
|
|
428
|
+
colors?: string[]
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export const CustomHighlightTool: WysiwygEditorToolFactory<HighlightToolOptions> = (
|
|
432
|
+
vui,
|
|
433
|
+
options = {}
|
|
434
|
+
) => {
|
|
435
|
+
const colors = options.colors || ['#ffeb3b', '#4caf50', '#2196f3', '#f44336']
|
|
436
|
+
|
|
437
|
+
const tools: WysiwygEditorTool[] = colors.map((color, index) => ({
|
|
438
|
+
key: `highlight-${index}`,
|
|
439
|
+
icon: ({ vui }) => vui.icon('palette'),
|
|
440
|
+
active: ({ editor }) => editor.isActive('highlight', { color }),
|
|
441
|
+
onClick: ({ editor }) => {
|
|
442
|
+
editor.chain().focus().toggleHighlight({ color }).run()
|
|
443
|
+
},
|
|
444
|
+
floating: true,
|
|
445
|
+
extensions: [CustomHighlightExtension],
|
|
446
|
+
}))
|
|
447
|
+
|
|
448
|
+
return tools
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Editor with Custom Elements
|
|
453
|
+
|
|
454
|
+
```vue
|
|
455
|
+
<template>
|
|
456
|
+
<div class="custom-editor">
|
|
457
|
+
<VWysiwygEditor
|
|
458
|
+
v-model="content"
|
|
459
|
+
label="Editor with Custom Features"
|
|
460
|
+
:tools="customTools"
|
|
461
|
+
:extensions="customExtensions"
|
|
462
|
+
/>
|
|
463
|
+
</div>
|
|
464
|
+
</template>
|
|
465
|
+
|
|
466
|
+
<script setup lang="ts">
|
|
467
|
+
import { ref } from 'vue'
|
|
468
|
+
import {
|
|
469
|
+
VWysiwygEditor,
|
|
470
|
+
WysiwygFormatBoldTool,
|
|
471
|
+
WysiwygFormatItalicTool,
|
|
472
|
+
WysiwygHistoryTool,
|
|
473
|
+
createWysiwygExtension,
|
|
474
|
+
} from '@fastkit/vui-wysiwyg'
|
|
475
|
+
import { CustomHighlightTool, CustomHighlightExtension } from './custom-tools/highlight-tool'
|
|
476
|
+
|
|
477
|
+
const content = ref('')
|
|
478
|
+
|
|
479
|
+
const customTools = [
|
|
480
|
+
WysiwygFormatBoldTool,
|
|
481
|
+
WysiwygFormatItalicTool,
|
|
482
|
+
// Custom highlight tool
|
|
483
|
+
CustomHighlightTool,
|
|
484
|
+
WysiwygHistoryTool,
|
|
485
|
+
]
|
|
486
|
+
|
|
487
|
+
const customExtensions = [
|
|
488
|
+
createWysiwygExtension(CustomHighlightExtension),
|
|
489
|
+
]
|
|
490
|
+
</script>
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Form Integration and Validation
|
|
494
|
+
|
|
495
|
+
### Form with Validation
|
|
496
|
+
|
|
497
|
+
```vue
|
|
498
|
+
<template>
|
|
499
|
+
<div class="form-example">
|
|
500
|
+
<VForm @submit="handleSubmit" @invalid="handleInvalid">
|
|
501
|
+
<VWysiwygEditor
|
|
502
|
+
v-model="formData.content"
|
|
503
|
+
label="Article Content"
|
|
504
|
+
hint="Please enter at least 100 characters"
|
|
505
|
+
:tools="tools"
|
|
506
|
+
:min-length="100"
|
|
507
|
+
:max-length="5000"
|
|
508
|
+
counter
|
|
509
|
+
required
|
|
510
|
+
:error-messages="errors.content"
|
|
511
|
+
/>
|
|
512
|
+
|
|
513
|
+
<VWysiwygEditor
|
|
514
|
+
v-model="formData.summary"
|
|
515
|
+
label="Article Summary"
|
|
516
|
+
hint="Enter article summary within 200 characters"
|
|
517
|
+
:tools="basicTools"
|
|
518
|
+
:max-length="200"
|
|
519
|
+
counter
|
|
520
|
+
required
|
|
521
|
+
size="small"
|
|
522
|
+
:error-messages="errors.summary"
|
|
523
|
+
/>
|
|
524
|
+
|
|
525
|
+
<div class="form-actions">
|
|
526
|
+
<VButton type="submit" variant="contained" color="primary">
|
|
527
|
+
Save Article
|
|
528
|
+
</VButton>
|
|
529
|
+
<VButton type="button" @click="previewArticle">
|
|
530
|
+
Preview
|
|
531
|
+
</VButton>
|
|
532
|
+
</div>
|
|
533
|
+
</VForm>
|
|
534
|
+
|
|
535
|
+
<!-- Preview modal -->
|
|
536
|
+
<VDialog v-model="showPreview" max-width="800px">
|
|
537
|
+
<VCard>
|
|
538
|
+
<VCardTitle>Article Preview</VCardTitle>
|
|
539
|
+
<VCardContent>
|
|
540
|
+
<div class="preview-content">
|
|
541
|
+
<h3>{{ formData.title }}</h3>
|
|
542
|
+
<div class="summary" v-html="formData.summary"></div>
|
|
543
|
+
<div class="content" v-html="formData.content"></div>
|
|
544
|
+
</div>
|
|
545
|
+
</VCardContent>
|
|
546
|
+
<VCardActions>
|
|
547
|
+
<VButton @click="showPreview = false">Close</VButton>
|
|
548
|
+
</VCardActions>
|
|
549
|
+
</VCard>
|
|
550
|
+
</VDialog>
|
|
551
|
+
</div>
|
|
552
|
+
</template>
|
|
553
|
+
|
|
554
|
+
<script setup lang="ts">
|
|
555
|
+
import { ref, reactive } from 'vue'
|
|
556
|
+
import {
|
|
557
|
+
VWysiwygEditor,
|
|
558
|
+
WysiwygFormatBoldTool,
|
|
559
|
+
WysiwygFormatItalicTool,
|
|
560
|
+
WysiwygFormatUnderlineTool,
|
|
561
|
+
WysiwygBulletListTool,
|
|
562
|
+
WysiwygOrderedListTool,
|
|
563
|
+
WysiwygLinkTool,
|
|
564
|
+
WysiwygHistoryTool,
|
|
565
|
+
} from '@fastkit/vui-wysiwyg'
|
|
566
|
+
import {
|
|
567
|
+
VForm,
|
|
568
|
+
VButton,
|
|
569
|
+
VDialog,
|
|
570
|
+
VCard,
|
|
571
|
+
VCardTitle,
|
|
572
|
+
VCardContent,
|
|
573
|
+
VCardActions,
|
|
574
|
+
} from '@fastkit/vui'
|
|
575
|
+
|
|
576
|
+
const showPreview = ref(false)
|
|
577
|
+
|
|
578
|
+
const formData = reactive({
|
|
579
|
+
title: '',
|
|
580
|
+
content: '',
|
|
581
|
+
summary: '',
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
const errors = reactive({
|
|
585
|
+
content: [] as string[],
|
|
586
|
+
summary: [] as string[],
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
const tools = [
|
|
590
|
+
WysiwygFormatBoldTool,
|
|
591
|
+
WysiwygFormatItalicTool,
|
|
592
|
+
WysiwygFormatUnderlineTool,
|
|
593
|
+
WysiwygBulletListTool,
|
|
594
|
+
WysiwygOrderedListTool,
|
|
595
|
+
WysiwygLinkTool,
|
|
596
|
+
WysiwygHistoryTool,
|
|
597
|
+
]
|
|
598
|
+
|
|
599
|
+
const basicTools = [
|
|
600
|
+
WysiwygFormatBoldTool,
|
|
601
|
+
WysiwygFormatItalicTool,
|
|
602
|
+
WysiwygHistoryTool,
|
|
603
|
+
]
|
|
604
|
+
|
|
605
|
+
const handleSubmit = async () => {
|
|
606
|
+
// Form validation
|
|
607
|
+
errors.content = []
|
|
608
|
+
errors.summary = []
|
|
609
|
+
|
|
610
|
+
if (!formData.content || formData.content.length < 100) {
|
|
611
|
+
errors.content.push('Article content must be at least 100 characters')
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (!formData.summary) {
|
|
615
|
+
errors.summary.push('Summary is required')
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (errors.content.length === 0 && errors.summary.length === 0) {
|
|
619
|
+
try {
|
|
620
|
+
// API投稿処理
|
|
621
|
+
await saveArticle(formData)
|
|
622
|
+
alert('記事が保存されました')
|
|
623
|
+
} catch (error) {
|
|
624
|
+
console.error('保存エラー:', error)
|
|
625
|
+
alert('保存に失敗しました')
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const handleInvalid = (event: Event) => {
|
|
631
|
+
console.log('フォーム検証エラー:', event)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const previewArticle = () => {
|
|
635
|
+
showPreview.value = true
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const saveArticle = async (data: typeof formData) => {
|
|
639
|
+
// API呼び出し実装
|
|
640
|
+
const response = await fetch('/api/articles', {
|
|
641
|
+
method: 'POST',
|
|
642
|
+
headers: {
|
|
643
|
+
'Content-Type': 'application/json',
|
|
644
|
+
},
|
|
645
|
+
body: JSON.stringify(data),
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
if (!response.ok) {
|
|
649
|
+
throw new Error('保存に失敗しました')
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return response.json()
|
|
653
|
+
}
|
|
654
|
+
</script>
|
|
655
|
+
|
|
656
|
+
<style scoped>
|
|
657
|
+
.form-example {
|
|
658
|
+
max-width: 900px;
|
|
659
|
+
margin: 0 auto;
|
|
660
|
+
padding: 30px;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.form-actions {
|
|
664
|
+
margin-top: 30px;
|
|
665
|
+
display: flex;
|
|
666
|
+
gap: 15px;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.preview-content {
|
|
670
|
+
max-height: 600px;
|
|
671
|
+
overflow-y: auto;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.preview-content h3 {
|
|
675
|
+
margin-bottom: 15px;
|
|
676
|
+
padding-bottom: 10px;
|
|
677
|
+
border-bottom: 1px solid #e1e5e9;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.summary {
|
|
681
|
+
margin-bottom: 20px;
|
|
682
|
+
padding: 15px;
|
|
683
|
+
background-color: #f8f9fa;
|
|
684
|
+
border-radius: 8px;
|
|
685
|
+
font-style: italic;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.content {
|
|
689
|
+
line-height: 1.6;
|
|
690
|
+
}
|
|
691
|
+
</style>
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
## スタイリングとテーマ
|
|
695
|
+
|
|
696
|
+
### カスタムテーマの作成
|
|
697
|
+
|
|
698
|
+
```scss
|
|
699
|
+
// custom-wysiwyg-theme.scss
|
|
700
|
+
@import '@fastkit/vui-wysiwyg/vui-wysiwyg.css';
|
|
701
|
+
|
|
702
|
+
// カスタムエディタ変数
|
|
703
|
+
:root {
|
|
704
|
+
--wysiwyg-editor-bg: #ffffff;
|
|
705
|
+
--wysiwyg-editor-border: #e1e5e9;
|
|
706
|
+
--wysiwyg-editor-border-focus: #007acc;
|
|
707
|
+
--wysiwyg-editor-text: #333333;
|
|
708
|
+
--wysiwyg-editor-placeholder: #999999;
|
|
709
|
+
--wysiwyg-toolbar-bg: #f8f9fa;
|
|
710
|
+
--wysiwyg-toolbar-border: #e1e5e9;
|
|
711
|
+
--wysiwyg-button-hover: #e2e6ea;
|
|
712
|
+
--wysiwyg-button-active: #007acc;
|
|
713
|
+
--wysiwyg-button-active-bg: #e3f2fd;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ダークテーマ
|
|
717
|
+
[data-theme="dark"] {
|
|
718
|
+
--wysiwyg-editor-bg: #1a1a1a;
|
|
719
|
+
--wysiwyg-editor-border: #444444;
|
|
720
|
+
--wysiwyg-editor-border-focus: #4fc3f7;
|
|
721
|
+
--wysiwyg-editor-text: #ffffff;
|
|
722
|
+
--wysiwyg-editor-placeholder: #bbbbbb;
|
|
723
|
+
--wysiwyg-toolbar-bg: #2d2d2d;
|
|
724
|
+
--wysiwyg-toolbar-border: #444444;
|
|
725
|
+
--wysiwyg-button-hover: #404040;
|
|
726
|
+
--wysiwyg-button-active: #4fc3f7;
|
|
727
|
+
--wysiwyg-button-active-bg: #0d47a1;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// カスタムエディタスタイル
|
|
731
|
+
.v-wysiwyg-editor {
|
|
732
|
+
background-color: var(--wysiwyg-editor-bg);
|
|
733
|
+
border: 1px solid var(--wysiwyg-editor-border);
|
|
734
|
+
border-radius: 8px;
|
|
735
|
+
transition: border-color 0.2s ease;
|
|
736
|
+
|
|
737
|
+
&:focus-within {
|
|
738
|
+
border-color: var(--wysiwyg-editor-border-focus);
|
|
739
|
+
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ツールバーのスタイル
|
|
743
|
+
.v-wysiwyg-editor__toolbar {
|
|
744
|
+
background-color: var(--wysiwyg-toolbar-bg);
|
|
745
|
+
border-bottom: 1px solid var(--wysiwyg-toolbar-border);
|
|
746
|
+
padding: 8px 12px;
|
|
747
|
+
|
|
748
|
+
.v-button {
|
|
749
|
+
margin: 0 2px;
|
|
750
|
+
transition: all 0.2s ease;
|
|
751
|
+
|
|
752
|
+
&:hover {
|
|
753
|
+
background-color: var(--wysiwyg-button-hover);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
&.v-button--active {
|
|
757
|
+
color: var(--wysiwyg-button-active);
|
|
758
|
+
background-color: var(--wysiwyg-button-active-bg);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// エディタコンテンツのスタイル
|
|
764
|
+
.v-wysiwyg-editor__input__prose {
|
|
765
|
+
color: var(--wysiwyg-editor-text);
|
|
766
|
+
padding: 16px;
|
|
767
|
+
min-height: 200px;
|
|
768
|
+
line-height: 1.6;
|
|
769
|
+
|
|
770
|
+
&::placeholder {
|
|
771
|
+
color: var(--wysiwyg-editor-placeholder);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// プロージングスタイル
|
|
775
|
+
h1, h2, h3, h4, h5, h6 {
|
|
776
|
+
font-weight: bold;
|
|
777
|
+
margin: 1em 0 0.5em 0;
|
|
778
|
+
|
|
779
|
+
&:first-child {
|
|
780
|
+
margin-top: 0;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
h1 { font-size: 2em; }
|
|
785
|
+
h2 { font-size: 1.5em; }
|
|
786
|
+
h3 { font-size: 1.25em; }
|
|
787
|
+
|
|
788
|
+
p {
|
|
789
|
+
margin: 0.5em 0;
|
|
790
|
+
|
|
791
|
+
&:first-child {
|
|
792
|
+
margin-top: 0;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
&:last-child {
|
|
796
|
+
margin-bottom: 0;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
ul, ol {
|
|
801
|
+
padding-left: 1.5em;
|
|
802
|
+
margin: 0.5em 0;
|
|
803
|
+
|
|
804
|
+
li {
|
|
805
|
+
margin: 0.25em 0;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
blockquote {
|
|
810
|
+
border-left: 4px solid var(--wysiwyg-editor-border-focus);
|
|
811
|
+
padding-left: 16px;
|
|
812
|
+
margin: 1em 0;
|
|
813
|
+
font-style: italic;
|
|
814
|
+
opacity: 0.8;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
code {
|
|
818
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
819
|
+
padding: 2px 4px;
|
|
820
|
+
border-radius: 4px;
|
|
821
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
a {
|
|
825
|
+
color: var(--wysiwyg-button-active);
|
|
826
|
+
text-decoration: underline;
|
|
827
|
+
|
|
828
|
+
&:hover {
|
|
829
|
+
text-decoration: none;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// フローティングメニューのスタイル
|
|
835
|
+
.v-wysiwyg-editor__bubble-menu {
|
|
836
|
+
background-color: var(--wysiwyg-toolbar-bg);
|
|
837
|
+
border: 1px solid var(--wysiwyg-toolbar-border);
|
|
838
|
+
border-radius: 8px;
|
|
839
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
840
|
+
padding: 4px;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// サイズバリエーション
|
|
845
|
+
.v-wysiwyg-editor--small {
|
|
846
|
+
.v-wysiwyg-editor__input__prose {
|
|
847
|
+
padding: 12px;
|
|
848
|
+
min-height: 120px;
|
|
849
|
+
font-size: 0.875em;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.v-wysiwyg-editor__toolbar {
|
|
853
|
+
padding: 6px 8px;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.v-wysiwyg-editor--large {
|
|
858
|
+
.v-wysiwyg-editor__input__prose {
|
|
859
|
+
padding: 20px;
|
|
860
|
+
min-height: 300px;
|
|
861
|
+
font-size: 1.125em;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.v-wysiwyg-editor__toolbar {
|
|
865
|
+
padding: 10px 16px;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// 読み取り専用モード
|
|
870
|
+
.v-wysiwyg-editor--readonly {
|
|
871
|
+
.v-wysiwyg-editor__input__prose {
|
|
872
|
+
background-color: #f8f9fa;
|
|
873
|
+
cursor: not-allowed;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// エラー状態
|
|
878
|
+
.v-wysiwyg-editor--error {
|
|
879
|
+
border-color: #f44336;
|
|
880
|
+
|
|
881
|
+
&:focus-within {
|
|
882
|
+
border-color: #f44336;
|
|
883
|
+
box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.2);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
## API Specification
|
|
889
|
+
|
|
890
|
+
### `VWysiwygEditor`コンポーネント
|
|
891
|
+
|
|
892
|
+
```typescript
|
|
893
|
+
interface VWysiwygEditorProps {
|
|
894
|
+
// 基本プロパティ
|
|
895
|
+
modelValue?: string // エディタコンテンツ (HTML)
|
|
896
|
+
label?: string // ラベルテキスト
|
|
897
|
+
hint?: string // ヒントテキスト
|
|
898
|
+
placeholder?: string // プレースホルダー
|
|
899
|
+
|
|
900
|
+
// バリデーション
|
|
901
|
+
required?: boolean // 必須入力
|
|
902
|
+
disabled?: boolean // 無効状態
|
|
903
|
+
readonly?: boolean // 読み取り専用
|
|
904
|
+
minLength?: number // 最小文字数
|
|
905
|
+
maxLength?: number // 最大文字数
|
|
906
|
+
counter?: boolean // 文字数カウンター表示
|
|
907
|
+
|
|
908
|
+
// エディタ設定
|
|
909
|
+
tools?: RawWysiwygEditorTool[] // ツールバー設定
|
|
910
|
+
extensions?: RawWysiwygExtension[] // 拡張機能
|
|
911
|
+
floatingToolbar?: boolean // フローティングツールバー
|
|
912
|
+
disabledMinHeight?: boolean // 最小高さ無効化
|
|
913
|
+
disabledMaxHeight?: boolean // 最大高さ無効化
|
|
914
|
+
removeDefaultWrapper?: boolean // デフォルトラッパー除去
|
|
915
|
+
|
|
916
|
+
// フォーム統合
|
|
917
|
+
size?: 'small' | 'medium' | 'large' // サイズ
|
|
918
|
+
errorMessages?: string[] // エラーメッセージ
|
|
919
|
+
|
|
920
|
+
// アドornment
|
|
921
|
+
startAdornment?: VNodeChild // 開始装飾
|
|
922
|
+
endAdornment?: VNodeChild // 終了装飾
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
interface VWysiwygEditorEvents {
|
|
926
|
+
'update:modelValue': (value: string) => void
|
|
927
|
+
input: (value: string) => void
|
|
928
|
+
focus: (event: FocusEvent) => void
|
|
929
|
+
blur: (event: FocusEvent) => void
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
interface VWysiwygEditorAPI {
|
|
933
|
+
readonly editor: Editor | undefined
|
|
934
|
+
readonly control: TextableControl
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
### ツール定義
|
|
939
|
+
|
|
940
|
+
```typescript
|
|
941
|
+
interface WysiwygEditorTool {
|
|
942
|
+
key: string // ツール識別子
|
|
943
|
+
icon: WysiwygEditorToolIcon // アイコン
|
|
944
|
+
active?: boolean | ((ctx: WysiwygEditorContext) => boolean) // アクティブ状態
|
|
945
|
+
disabled?: boolean | ((ctx: WysiwygEditorContext) => boolean) // 無効状態
|
|
946
|
+
onClick: (ctx: WysiwygEditorContext, ev: MouseEvent) => any // クリックハンドラ
|
|
947
|
+
floating?: boolean // フローティングメニュー表示
|
|
948
|
+
extensions?: Extensions // 依存拡張機能
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
type WysiwygEditorToolFactory<Options = void> = (
|
|
952
|
+
vui: VuiService,
|
|
953
|
+
options?: Options
|
|
954
|
+
) => WysiwygEditorTool | WysiwygEditorTool[]
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
### 拡張機能
|
|
958
|
+
|
|
959
|
+
```typescript
|
|
960
|
+
type WysiwygExtensionFactory<Options = any, Storage = any> = (
|
|
961
|
+
ctx: WysiwygEditorInitializeContext
|
|
962
|
+
) => Extension<Options, Storage> | Node<Options, Storage> | Mark<Options, Storage>
|
|
963
|
+
|
|
964
|
+
function createWysiwygExtension<Options = any, Storage = any>(
|
|
965
|
+
extension: WysiwygExtensionSource<Options, Storage>
|
|
966
|
+
): CreatedWysiwygExtension<Options, Storage>
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
## 組み込みツール
|
|
970
|
+
|
|
971
|
+
### テキストフォーマット
|
|
972
|
+
- `WysiwygFormatBoldTool` - 太字
|
|
973
|
+
- `WysiwygFormatItalicTool` - 斜体
|
|
974
|
+
- `WysiwygFormatUnderlineTool` - 下線
|
|
975
|
+
|
|
976
|
+
### リスト
|
|
977
|
+
- `WysiwygBulletListTool` - 箇条書きリスト
|
|
978
|
+
- `WysiwygOrderedListTool` - 番号付きリスト
|
|
979
|
+
|
|
980
|
+
### その他
|
|
981
|
+
- `WysiwygLinkTool` - リンク
|
|
982
|
+
- `WysiwygHistoryTool` - 元に戻す/やり直し
|
|
983
|
+
- `WysiwygColorTool` - テキスト色
|
|
984
|
+
- `WysiwygTextAlignTool` - テキスト整列
|
|
985
|
+
- `WysiwygCustomTagTool` - カスタムタグ
|
|
986
|
+
|
|
987
|
+
## Considerations
|
|
988
|
+
|
|
989
|
+
### ブラウザ対応
|
|
990
|
+
- モダンブラウザのみサポート
|
|
991
|
+
- Internet Explorer非対応
|
|
992
|
+
- ProseMirrorの制約に準拠
|
|
993
|
+
|
|
994
|
+
### パフォーマンス
|
|
995
|
+
- 大量のコンテンツ編集時は仮想スクロール推奨
|
|
996
|
+
- 複雑な拡張機能は性能に影響する可能性
|
|
997
|
+
- SSRでは初期化に注意が必要
|
|
998
|
+
|
|
999
|
+
### セキュリティ
|
|
1000
|
+
- HTMLコンテンツのサニタイゼーションは実装者責任
|
|
1001
|
+
- XSS攻撃への対策が必要
|
|
1002
|
+
- 信頼できないコンテンツの表示には注意
|
|
1003
|
+
|
|
1004
|
+
## License
|
|
1005
|
+
|
|
1006
|
+
MIT
|
|
1007
|
+
|
|
1008
|
+
## Related Packages
|
|
1009
|
+
|
|
1010
|
+
- [@fastkit/vui](../vui/README.md): 基盤UIコンポーネントライブラリ
|
|
1011
|
+
- [@fastkit/vue-form-control](../vue-form-control/README.md): フォーム制御機能
|
|
1012
|
+
- [@tiptap/vue-3](https://tiptap.dev/installation/vue3): 基盤エディタライブラリ
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastkit/vui-wysiwyg",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.1",
|
|
4
4
|
"description": "vui-wysiwyg",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"repository": {
|
|
@@ -301,11 +301,11 @@
|
|
|
301
301
|
"@types/markdown-it": "^14.1.1"
|
|
302
302
|
},
|
|
303
303
|
"devDependencies": {
|
|
304
|
-
"@fastkit/vui": "^0.20.
|
|
304
|
+
"@fastkit/vui": "^0.20.1"
|
|
305
305
|
},
|
|
306
306
|
"peerDependencies": {
|
|
307
307
|
"vue": "^3.4.0",
|
|
308
|
-
"@fastkit/vui": "^0.20.
|
|
308
|
+
"@fastkit/vui": "^0.20.1"
|
|
309
309
|
},
|
|
310
310
|
"scripts": {
|
|
311
311
|
"build": "plugboy build",
|