@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.
Files changed (2) hide show
  1. package/README.md +1011 -0
  2. 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.0",
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.0"
304
+ "@fastkit/vui": "^0.20.1"
305
305
  },
306
306
  "peerDependencies": {
307
307
  "vue": "^3.4.0",
308
- "@fastkit/vui": "^0.20.0"
308
+ "@fastkit/vui": "^0.20.1"
309
309
  },
310
310
  "scripts": {
311
311
  "build": "plugboy build",