@teriusu/rich-editor 0.4.1 → 0.5.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.
package/README.md CHANGED
@@ -1,25 +1,25 @@
1
- # @teriusu/rich-editor
1
+ # @hancomac/rich-editor
2
2
 
3
3
  TipTap v3 기반 리치 텍스트 에디터. Svelte 5 컴포넌트 라이브러리.
4
4
 
5
5
  ## 설치
6
6
 
7
7
  ```bash
8
- npm install @teriusu/rich-editor
8
+ npm install @hancomac/rich-editor
9
9
  ```
10
10
 
11
11
  Tailwind CSS v4 사용 시 `@source` 추가:
12
12
  ```css
13
- @import "@teriusu/rich-editor/styles";
14
- @source "../node_modules/@teriusu/rich-editor/dist";
13
+ @import "@hancomac/rich-editor/styles";
14
+ @source "../node_modules/@hancomac/rich-editor/dist";
15
15
  ```
16
16
 
17
17
  ## 기본 사용법
18
18
 
19
19
  ```svelte
20
20
  <script>
21
- import { TipTapEditor } from '@teriusu/rich-editor';
22
- import '@teriusu/rich-editor/styles';
21
+ import { TipTapEditor } from '@hancomac/rich-editor';
22
+ import '@hancomac/rich-editor/styles';
23
23
 
24
24
  let html = $state('');
25
25
  </script>
@@ -121,7 +121,7 @@ TipTap v2 커스텀 태그를 자동 변환합니다. 에디터 로드 시 `tran
121
121
  게시물 조회 페이지(읽기 전용)에서도 적용:
122
122
 
123
123
  ```js
124
- import { transformLegacyHtml } from '@teriusu/rich-editor';
124
+ import { transformLegacyHtml } from '@hancomac/rich-editor';
125
125
 
126
126
  const html = transformLegacyHtml(post.content);
127
127
  ```
@@ -132,7 +132,7 @@ const html = transformLegacyHtml(post.content);
132
132
 
133
133
  ```svelte
134
134
  <script>
135
- import { TipTapEditor } from '@teriusu/rich-editor';
135
+ import { TipTapEditor } from '@hancomac/rich-editor';
136
136
  import { Node } from '@tiptap/core';
137
137
 
138
138
  const MyExtension = Node.create({ /* ... */ });
@@ -22,8 +22,11 @@
22
22
  Code,
23
23
  } from "lucide-svelte";
24
24
  import { cn } from "../utils/cn";
25
+ import type { ToolbarFeature } from "../types";
25
26
 
26
- let { editor, minimal = false }: { editor: Editor; minimal?: boolean } = $props();
27
+ let { editor, features }: { editor: Editor; features: Set<ToolbarFeature> } = $props();
28
+
29
+ const has = (f: ToolbarFeature) => features.has(f);
27
30
 
28
31
  let showHeadings = $state(false);
29
32
  let menuEl: HTMLDivElement | undefined = $state();
@@ -81,7 +84,7 @@
81
84
 
82
85
  <div bind:this={menuEl} class="bubble-toolbar-container" style="visibility: hidden">
83
86
  <div class="flex items-center gap-0.5 px-1.5 py-1 bg-foreground rounded-lg shadow-xl">
84
- {#if !minimal}
87
+ {#if has('h1') || has('h2') || has('h3')}
85
88
  <!-- Block type selector -->
86
89
  <div class="relative">
87
90
  <button
@@ -113,6 +116,7 @@
113
116
  <Type size={12} /> 본문
114
117
  </button>
115
118
  {#each [1, 2, 3] as level}
119
+ {#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
116
120
  {@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
117
121
  <button
118
122
  type="button"
@@ -133,6 +137,7 @@
133
137
  >
134
138
  <Icon size={12} /> 제목 {level}
135
139
  </button>
140
+ {/if}
136
141
  {/each}
137
142
  </div>
138
143
  {/if}
@@ -142,6 +147,7 @@
142
147
  {/if}
143
148
 
144
149
  <!-- Format buttons -->
150
+ {#if has('bold')}
145
151
  <button
146
152
  type="button"
147
153
  onclick={() => editor.chain().focus().toggleBold().run()}
@@ -156,6 +162,8 @@
156
162
  >
157
163
  <Bold size={iconSize} />
158
164
  </button>
165
+ {/if}
166
+ {#if has('italic')}
159
167
  <button
160
168
  type="button"
161
169
  onclick={() => editor.chain().focus().toggleItalic().run()}
@@ -170,7 +178,8 @@
170
178
  >
171
179
  <Italic size={iconSize} />
172
180
  </button>
173
- {#if !minimal}
181
+ {/if}
182
+ {#if has('underline')}
174
183
  <button
175
184
  type="button"
176
185
  onclick={() => editor.chain().focus().toggleUnderline().run()}
@@ -186,6 +195,7 @@
186
195
  <UnderlineIcon size={iconSize} />
187
196
  </button>
188
197
  {/if}
198
+ {#if has('strike')}
189
199
  <button
190
200
  type="button"
191
201
  onclick={() => editor.chain().focus().toggleStrike().run()}
@@ -200,7 +210,8 @@
200
210
  >
201
211
  <Strikethrough size={iconSize} />
202
212
  </button>
203
- {#if !minimal}
213
+ {/if}
214
+ {#if has('highlight')}
204
215
  <button
205
216
  type="button"
206
217
  onclick={() => editor.chain().focus().toggleHighlight().run()}
@@ -216,6 +227,7 @@
216
227
  <Highlighter size={iconSize} />
217
228
  </button>
218
229
  {/if}
230
+ {#if has('code')}
219
231
  <button
220
232
  type="button"
221
233
  onclick={() => editor.chain().focus().toggleCode().run()}
@@ -230,7 +242,9 @@
230
242
  >
231
243
  <Code size={iconSize} />
232
244
  </button>
245
+ {/if}
233
246
 
247
+ {#if has('link')}
234
248
  <div class="w-px h-5 bg-white/20 mx-0.5"></div>
235
249
 
236
250
  <button
@@ -247,11 +261,13 @@
247
261
  >
248
262
  <LinkIcon size={iconSize} />
249
263
  </button>
264
+ {/if}
250
265
 
251
- {#if !minimal}
266
+ {#if has('align-left') || has('align-center') || has('align-right')}
252
267
  <div class="w-px h-5 bg-white/20 mx-0.5"></div>
253
268
 
254
269
  <!-- Alignment -->
270
+ {#if has('align-left')}
255
271
  <button
256
272
  type="button"
257
273
  onclick={() => editor.chain().focus().setTextAlign("left").run()}
@@ -266,6 +282,8 @@
266
282
  >
267
283
  <AlignLeft size={iconSize} />
268
284
  </button>
285
+ {/if}
286
+ {#if has('align-center')}
269
287
  <button
270
288
  type="button"
271
289
  onclick={() => editor.chain().focus().setTextAlign("center").run()}
@@ -280,6 +298,8 @@
280
298
  >
281
299
  <AlignCenter size={iconSize} />
282
300
  </button>
301
+ {/if}
302
+ {#if has('align-right')}
283
303
  <button
284
304
  type="button"
285
305
  onclick={() => editor.chain().focus().setTextAlign("right").run()}
@@ -294,6 +314,7 @@
294
314
  >
295
315
  <AlignRight size={iconSize} />
296
316
  </button>
317
+ {/if}
297
318
  {/if}
298
319
  </div>
299
320
  </div>
@@ -1,7 +1,8 @@
1
1
  import type { Editor } from "@tiptap/core";
2
+ import type { ToolbarFeature } from "../types";
2
3
  type $$ComponentProps = {
3
4
  editor: Editor;
4
- minimal?: boolean;
5
+ features: Set<ToolbarFeature>;
5
6
  };
6
7
  declare const BubbleToolbar: import("svelte").Component<$$ComponentProps, {}, "">;
7
8
  type BubbleToolbar = ReturnType<typeof BubbleToolbar>;
@@ -1 +1 @@
1
- {"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAsB1C,KAAK,gBAAgB,GAAI;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAqNhE,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"BubbleToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BubbleToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAqB3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE9C,KAAK,gBAAgB,GAAI;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAA;CAAE,CAAC;AA0O5E,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -43,19 +43,24 @@
43
43
  } from "lucide-svelte";
44
44
  import { cn } from "../utils/cn";
45
45
  import InputModal from "./InputModal.svelte";
46
+ import type { ToolbarFeature } from "../types";
46
47
 
47
48
  let {
48
49
  editor,
50
+ features,
49
51
  onPdfClick,
50
52
  onFileClick,
51
53
  onVideoClick,
52
54
  }: {
53
55
  editor: Editor;
56
+ features: Set<ToolbarFeature>;
54
57
  onPdfClick: () => void;
55
58
  onFileClick?: () => void;
56
59
  onVideoClick?: () => void;
57
60
  } = $props();
58
61
 
62
+ const has = (f: ToolbarFeature) => features.has(f);
63
+
59
64
  const iconSize = 16;
60
65
 
61
66
  let codeMenuOpen = $state(false);
@@ -119,6 +124,7 @@
119
124
  style="top: var(--header-height, 74px)"
120
125
  >
121
126
  <!-- Format -->
127
+ {#if has('bold')}
122
128
  <button
123
129
  type="button"
124
130
  onclick={() => editor.chain().focus().toggleBold().run()}
@@ -133,6 +139,8 @@
133
139
  >
134
140
  <Bold size={iconSize} />
135
141
  </button>
142
+ {/if}
143
+ {#if has('italic')}
136
144
  <button
137
145
  type="button"
138
146
  onclick={() => editor.chain().focus().toggleItalic().run()}
@@ -147,6 +155,8 @@
147
155
  >
148
156
  <Italic size={iconSize} />
149
157
  </button>
158
+ {/if}
159
+ {#if has('underline')}
150
160
  <button
151
161
  type="button"
152
162
  onclick={() => editor.chain().focus().toggleUnderline().run()}
@@ -161,6 +171,8 @@
161
171
  >
162
172
  <UnderlineIcon size={iconSize} />
163
173
  </button>
174
+ {/if}
175
+ {#if has('strike')}
164
176
  <button
165
177
  type="button"
166
178
  onclick={() => editor.chain().focus().toggleStrike().run()}
@@ -175,6 +187,8 @@
175
187
  >
176
188
  <Strikethrough size={iconSize} />
177
189
  </button>
190
+ {/if}
191
+ {#if has('highlight')}
178
192
  <button
179
193
  type="button"
180
194
  onclick={() => editor.chain().focus().toggleHighlight().run()}
@@ -189,6 +203,8 @@
189
203
  >
190
204
  <Highlighter size={iconSize} />
191
205
  </button>
206
+ {/if}
207
+ {#if has('superscript')}
192
208
  <button
193
209
  type="button"
194
210
  onclick={() => editor.chain().focus().toggleSuperscript().run()}
@@ -203,6 +219,8 @@
203
219
  >
204
220
  <Superscript size={iconSize} />
205
221
  </button>
222
+ {/if}
223
+ {#if has('subscript')}
206
224
  <button
207
225
  type="button"
208
226
  onclick={() => editor.chain().focus().toggleSubscript().run()}
@@ -217,10 +235,14 @@
217
235
  >
218
236
  <Subscript size={iconSize} />
219
237
  </button>
238
+ {/if}
220
239
 
240
+ {#if has('align-left') || has('align-center') || has('align-right')}
221
241
  <div class="w-px h-6 bg-border mx-0.5"></div>
242
+ {/if}
222
243
 
223
244
  <!-- Alignment -->
245
+ {#if has('align-left')}
224
246
  <button
225
247
  type="button"
226
248
  onclick={() => editor.chain().focus().setTextAlign("left").run()}
@@ -235,6 +257,8 @@
235
257
  >
236
258
  <AlignLeft size={iconSize} />
237
259
  </button>
260
+ {/if}
261
+ {#if has('align-center')}
238
262
  <button
239
263
  type="button"
240
264
  onclick={() => editor.chain().focus().setTextAlign("center").run()}
@@ -249,6 +273,8 @@
249
273
  >
250
274
  <AlignCenter size={iconSize} />
251
275
  </button>
276
+ {/if}
277
+ {#if has('align-right')}
252
278
  <button
253
279
  type="button"
254
280
  onclick={() => editor.chain().focus().setTextAlign("right").run()}
@@ -263,10 +289,14 @@
263
289
  >
264
290
  <AlignRight size={iconSize} />
265
291
  </button>
292
+ {/if}
266
293
 
294
+ {#if has('h1') || has('h2') || has('h3')}
267
295
  <div class="w-px h-6 bg-border mx-0.5"></div>
296
+ {/if}
268
297
 
269
298
  <!-- Headings -->
299
+ {#if has('h1')}
270
300
  <button
271
301
  type="button"
272
302
  onclick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
@@ -281,6 +311,8 @@
281
311
  >
282
312
  <Heading1 size={iconSize} />
283
313
  </button>
314
+ {/if}
315
+ {#if has('h2')}
284
316
  <button
285
317
  type="button"
286
318
  onclick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
@@ -295,6 +327,8 @@
295
327
  >
296
328
  <Heading2 size={iconSize} />
297
329
  </button>
330
+ {/if}
331
+ {#if has('h3')}
298
332
  <button
299
333
  type="button"
300
334
  onclick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
@@ -309,10 +343,14 @@
309
343
  >
310
344
  <Heading3 size={iconSize} />
311
345
  </button>
346
+ {/if}
312
347
 
348
+ {#if has('bullet-list') || has('ordered-list') || has('checklist') || has('blockquote') || has('horizontal-rule') || has('toggle')}
313
349
  <div class="w-px h-6 bg-border mx-0.5"></div>
350
+ {/if}
314
351
 
315
- <!-- Lists -->
352
+ <!-- Lists & Blocks -->
353
+ {#if has('bullet-list')}
316
354
  <button
317
355
  type="button"
318
356
  onclick={() => editor.chain().focus().toggleBulletList().run()}
@@ -327,6 +365,8 @@
327
365
  >
328
366
  <List size={iconSize} />
329
367
  </button>
368
+ {/if}
369
+ {#if has('ordered-list')}
330
370
  <button
331
371
  type="button"
332
372
  onclick={() => editor.chain().focus().toggleOrderedList().run()}
@@ -341,6 +381,8 @@
341
381
  >
342
382
  <ListOrdered size={iconSize} />
343
383
  </button>
384
+ {/if}
385
+ {#if has('checklist')}
344
386
  <button
345
387
  type="button"
346
388
  onclick={() => editor.chain().focus().toggleTaskList().run()}
@@ -355,6 +397,8 @@
355
397
  >
356
398
  <CheckSquare size={iconSize} />
357
399
  </button>
400
+ {/if}
401
+ {#if has('blockquote')}
358
402
  <button
359
403
  type="button"
360
404
  onclick={() => editor.chain().focus().toggleBlockquote().run()}
@@ -369,6 +413,8 @@
369
413
  >
370
414
  <Quote size={iconSize} />
371
415
  </button>
416
+ {/if}
417
+ {#if has('horizontal-rule')}
372
418
  <button
373
419
  type="button"
374
420
  onclick={() => editor.chain().focus().setHorizontalRule().run()}
@@ -378,6 +424,8 @@
378
424
  >
379
425
  <Minus size={iconSize} />
380
426
  </button>
427
+ {/if}
428
+ {#if has('toggle')}
381
429
  <button
382
430
  type="button"
383
431
  onclick={() => editor.chain().focus().setDetails().run()}
@@ -392,10 +440,14 @@
392
440
  >
393
441
  <ChevronRight size={iconSize} />
394
442
  </button>
443
+ {/if}
395
444
 
445
+ {#if has('link') || has('image') || has('pdf') || has('youtube') || has('video') || has('file')}
396
446
  <div class="w-px h-6 bg-border mx-0.5"></div>
447
+ {/if}
397
448
 
398
449
  <!-- Media -->
450
+ {#if has('link')}
399
451
  <button
400
452
  type="button"
401
453
  onclick={addLink}
@@ -410,6 +462,8 @@
410
462
  >
411
463
  <LinkIcon size={iconSize} />
412
464
  </button>
465
+ {/if}
466
+ {#if has('image')}
413
467
  <button
414
468
  type="button"
415
469
  onclick={addImage}
@@ -419,6 +473,8 @@
419
473
  >
420
474
  <ImageIcon size={iconSize} />
421
475
  </button>
476
+ {/if}
477
+ {#if has('pdf')}
422
478
  <button
423
479
  type="button"
424
480
  onclick={onPdfClick}
@@ -428,6 +484,8 @@
428
484
  >
429
485
  <FileText size={iconSize} />
430
486
  </button>
487
+ {/if}
488
+ {#if has('youtube')}
431
489
  <button
432
490
  type="button"
433
491
  onclick={() => {
@@ -440,7 +498,8 @@
440
498
  >
441
499
  <Video size={iconSize} />
442
500
  </button>
443
- {#if onVideoClick}
501
+ {/if}
502
+ {#if has('video') && onVideoClick}
444
503
  <button
445
504
  type="button"
446
505
  onclick={onVideoClick}
@@ -451,7 +510,7 @@
451
510
  <Film size={iconSize} />
452
511
  </button>
453
512
  {/if}
454
- {#if onFileClick}
513
+ {#if has('file') && onFileClick}
455
514
  <button
456
515
  type="button"
457
516
  onclick={onFileClick}
@@ -464,6 +523,7 @@
464
523
  {/if}
465
524
 
466
525
  <!-- Columns -->
526
+ {#if has('columns-2')}
467
527
  <button
468
528
  type="button"
469
529
  onclick={() => editor.chain().focus().setColumns(2).run()}
@@ -478,6 +538,8 @@
478
538
  >
479
539
  <Columns2 size={iconSize} />
480
540
  </button>
541
+ {/if}
542
+ {#if has('columns-3')}
481
543
  <button
482
544
  type="button"
483
545
  onclick={() => editor.chain().focus().setColumns(3).run()}
@@ -487,8 +549,10 @@
487
549
  >
488
550
  <Columns3 size={iconSize} />
489
551
  </button>
552
+ {/if}
490
553
 
491
554
  <!-- Table menu -->
555
+ {#if has('table')}
492
556
  <div bind:this={tableMenuEl} class="relative">
493
557
  <button
494
558
  type="button"
@@ -731,8 +795,10 @@
731
795
  </div>
732
796
  {/if}
733
797
  </div>
798
+ {/if}
734
799
 
735
800
  <!-- Code block menu -->
801
+ {#if has('code-block')}
736
802
  <div bind:this={codeMenuEl} class="relative">
737
803
  <button
738
804
  type="button"
@@ -788,10 +854,14 @@
788
854
  </div>
789
855
  {/if}
790
856
  </div>
857
+ {/if}
791
858
 
859
+ {#if has('undo') || has('redo')}
792
860
  <div class="w-px h-6 bg-border mx-0.5"></div>
861
+ {/if}
793
862
 
794
863
  <!-- Undo / Redo -->
864
+ {#if has('undo')}
795
865
  <button
796
866
  type="button"
797
867
  onclick={() => editor.chain().focus().undo().run()}
@@ -805,6 +875,8 @@
805
875
  >
806
876
  <Undo size={iconSize} />
807
877
  </button>
878
+ {/if}
879
+ {#if has('redo')}
808
880
  <button
809
881
  type="button"
810
882
  onclick={() => editor.chain().focus().redo().run()}
@@ -818,6 +890,7 @@
818
890
  >
819
891
  <Redo size={iconSize} />
820
892
  </button>
893
+ {/if}
821
894
 
822
895
  <!-- Modals -->
823
896
  {#if modalState?.type === "link"}
@@ -1,6 +1,8 @@
1
1
  import type { Editor } from "@tiptap/core";
2
+ import type { ToolbarFeature } from "../types";
2
3
  type $$ComponentProps = {
3
4
  editor: Editor;
5
+ features: Set<ToolbarFeature>;
4
6
  onPdfClick: () => void;
5
7
  onFileClick?: () => void;
6
8
  onVideoClick?: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"FixedToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/FixedToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA6C1C,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAiiBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"FixedToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/FixedToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA4C3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE9C,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAymBJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}