@teriusu/rich-editor 0.11.2 → 0.11.3

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.
@@ -24,6 +24,7 @@
24
24
  ListChecks,
25
25
  Quote,
26
26
  ChevronDown,
27
+ Check,
27
28
  Sigma,
28
29
  } from "lucide-svelte";
29
30
  import { cn } from "../utils/cn";
@@ -102,16 +103,6 @@
102
103
  return t("paragraph");
103
104
  }
104
105
 
105
- function isParagraphActive(): boolean {
106
- return (
107
- !isActive("heading") &&
108
- !isActive("bulletList") &&
109
- !isActive("orderedList") &&
110
- !isActive("taskList") &&
111
- !isActive("blockquote")
112
- );
113
- }
114
-
115
106
  async function addLink() {
116
107
  const selection = editor.state.selection.getBookmark();
117
108
  const documentBeforePrompt = editor.state.doc;
@@ -175,7 +166,9 @@
175
166
  const themeTokens = [
176
167
  '--border', '--background', '--foreground', '--popover', '--muted',
177
168
  '--muted-foreground', '--primary', '--primary-foreground', '--accent',
178
- '--accent-foreground', '--ring', '--hce-menu-z', '--hce-menu-surface'
169
+ '--accent-foreground', '--ring', '--hce-menu-z', '--hce-menu-surface',
170
+ '--wh-100', '--wh-500', '--gray-500', '--gray-700', '--gray-900',
171
+ '--primary-500', '--color-line', '--radius-8', '--radius-2xl', '--shadow-popup'
179
172
  ];
180
173
  const syncTheme = () => {
181
174
  const hostStyle = getComputedStyle(editor.view.dom);
@@ -192,7 +185,7 @@
192
185
  const left = viewport?.offsetLeft ?? 0;
193
186
  const top = viewport?.offsetTop ?? 0;
194
187
  anchor.style.left = `${left + width / 2}px`;
195
- anchor.style.top = `${top + height}px`;
188
+ anchor.style.top = `calc(${top + height}px - env(safe-area-inset-bottom, 0px))`;
196
189
  element.style.maxWidth = mobile ? `${Math.max(0, width - 16)}px` : '';
197
190
  element.style.setProperty('--bubble-popup-height', `${Math.max(44, height - element.getBoundingClientRect().height - 24)}px`);
198
191
  };
@@ -329,125 +322,55 @@
329
322
  );
330
323
  </script>
331
324
 
325
+ {#snippet blockItem(labelKey: EditorMessageKey, Icon: typeof Type, select: () => void)}
326
+ {@const selected = getCurrentBlockLabel() === t(labelKey)}
327
+ <button
328
+ type="button"
329
+ class="block-menu-item"
330
+ role="menuitemradio"
331
+ aria-checked={selected}
332
+ onclick={() => { select(); showBlockMenu = false; }}
333
+ >
334
+ <span class="block-menu-icon"><Icon size={16} /></span>
335
+ <span class="block-menu-label">{t(labelKey)}</span>
336
+ {#if selected}<span class="block-menu-check"><Check size={16} /></span>{/if}
337
+ </button>
338
+ {/snippet}
339
+
332
340
  {#snippet blockPopover()}
333
- {#if showBlockMenu}
334
- <div
335
- bind:this={blockPopupEl}
336
- class="bubble-popover absolute bottom-full left-0 mb-1 bg-popover border border-border rounded-lg shadow-xl py-1"
337
- style="min-width: 140px"
338
- onmousedown={(e) => e.preventDefault()}
339
- role="menu"
340
- tabindex="-1"
341
- >
342
- <button
343
- type="button"
344
- class={cn(
345
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
346
- isParagraphActive()
347
- ? "hce-active"
348
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
349
- )}
350
- onclick={() => {
351
- editor.chain().focus().setParagraph().run();
352
- showBlockMenu = false;
353
- }}
354
- >
355
- <Type size={12} /> {t('paragraph')}
356
- </button>
357
- {#each [1, 2, 3] as level (level)}
358
- {#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
359
- {@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
360
- <button
361
- type="button"
362
- class={cn(
363
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
364
- isActive("heading", { level })
365
- ? "hce-active"
366
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
367
- )}
368
- onclick={() => {
369
- editor
370
- .chain()
371
- .focus()
372
- .toggleHeading({ level: level as 1 | 2 | 3 })
373
- .run();
374
- showBlockMenu = false;
375
- }}
376
- >
377
- <Icon size={12} /> {t(level === 1 ? 'heading1' : level === 2 ? 'heading2' : 'heading3')}
378
- </button>
379
- {/if}
380
- {/each}
381
- {#if has('bullet-list')}
382
- <button
383
- type="button"
384
- class={cn(
385
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
386
- isActive("bulletList")
387
- ? "hce-active"
388
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
389
- )}
390
- onclick={() => {
391
- editor.chain().focus().toggleBulletList().run();
392
- showBlockMenu = false;
393
- }}
394
- >
395
- <List size={12} /> {t('bulletList')}
396
- </button>
397
- {/if}
398
- {#if has('ordered-list')}
399
- <button
400
- type="button"
401
- class={cn(
402
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
403
- isActive("orderedList")
404
- ? "hce-active"
405
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
406
- )}
407
- onclick={() => {
408
- editor.chain().focus().toggleOrderedList().run();
409
- showBlockMenu = false;
410
- }}
411
- >
412
- <ListOrdered size={12} /> {t('orderedList')}
413
- </button>
414
- {/if}
415
- {#if has('checklist')}
416
- <button
417
- type="button"
418
- class={cn(
419
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
420
- isActive("taskList")
421
- ? "hce-active"
422
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
423
- )}
424
- onclick={() => {
425
- editor.chain().focus().toggleTaskList().run();
426
- showBlockMenu = false;
427
- }}
428
- >
429
- <ListChecks size={12} /> {t('taskList')}
430
- </button>
431
- {/if}
432
- {#if has('blockquote')}
433
- <button
434
- type="button"
435
- class={cn(
436
- "w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
437
- isActive("blockquote")
438
- ? "hce-active"
439
- : "text-muted-foreground hover:text-foreground hover:bg-muted",
440
- )}
441
- onclick={() => {
442
- editor.chain().focus().toggleBlockquote().run();
443
- showBlockMenu = false;
444
- }}
445
- >
446
- <Quote size={12} /> {t('blockquote')}
447
- </button>
448
- {/if}
449
- </div>
341
+ {#if showBlockMenu}
342
+ <div
343
+ bind:this={blockPopupEl}
344
+ class="bubble-popover bubble-block-menu absolute bottom-full left-0 mb-1"
345
+ onmousedown={(e) => e.preventDefault()}
346
+ role="menu"
347
+ aria-orientation="vertical"
348
+ tabindex="-1"
349
+ >
350
+ {@render blockItem('paragraph', Type, () => { editor.chain().focus().setParagraph().run(); })}
351
+ {#each [1, 2, 3] as level (level)}
352
+ {#if has(level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3')}
353
+ {@const Icon = level === 1 ? Heading1 : level === 2 ? Heading2 : Heading3}
354
+ {@render blockItem(
355
+ level === 1 ? 'heading1' : level === 2 ? 'heading2' : 'heading3', Icon,
356
+ () => { editor.chain().focus().toggleHeading({ level: level as 1 | 2 | 3 }).run(); }
357
+ )}
450
358
  {/if}
359
+ {/each}
360
+ {#if has('bullet-list')}
361
+ {@render blockItem('bulletList', List, () => { editor.chain().focus().toggleBulletList().run(); })}
362
+ {/if}
363
+ {#if has('ordered-list')}
364
+ {@render blockItem('orderedList', ListOrdered, () => { editor.chain().focus().toggleOrderedList().run(); })}
365
+ {/if}
366
+ {#if has('checklist')}
367
+ {@render blockItem('taskList', ListChecks, () => { editor.chain().focus().toggleTaskList().run(); })}
368
+ {/if}
369
+ {#if has('blockquote')}
370
+ {@render blockItem('blockquote', Quote, () => { editor.chain().focus().toggleBlockquote().run(); })}
371
+ {/if}
372
+ </div>
373
+ {/if}
451
374
  {/snippet}
452
375
 
453
376
  {#snippet colorPopover()}
@@ -509,12 +432,13 @@
509
432
  type="button"
510
433
  onclick={() => (showBlockMenu = !showBlockMenu)}
511
434
  aria-haspopup="menu"
435
+ aria-label={getCurrentBlockLabel()}
512
436
  aria-expanded={showBlockMenu}
513
- class="flex items-center gap-1 px-2.5 py-1.5 rounded-full text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
437
+ class="bubble-block-trigger flex items-center gap-1 px-2.5 py-1.5 rounded-full text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
514
438
  >
515
- <Type size={12} />
516
- {getCurrentBlockLabel()}
517
- <ChevronDown size={12} />
439
+ <Type size={mobile ? 14 : 12} />
440
+ {#if !mobile}{getCurrentBlockLabel()}{/if}
441
+ <ChevronDown size={mobile ? 8 : 12} />
518
442
  </button>
519
443
  {#if !mobile}{@render blockPopover()}{/if}
520
444
  </div>
@@ -688,28 +612,30 @@
688
612
 
689
613
  <style>
690
614
  .mobile .hce-floating-panel {
691
- border-radius: 12px;
692
- padding: 4px;
693
- padding-bottom: max(4px, env(safe-area-inset-bottom));
615
+ border-radius: 8px;
616
+ padding: 1px 3px;
694
617
  }
695
618
  .mobile .bubble-toolbar-rail {
696
619
  display: flex;
697
620
  flex-wrap: nowrap;
698
621
  max-width: 100%;
699
- overflow-x: auto;
622
+ gap: 1px;
700
623
  padding: 0;
701
- scrollbar-width: none;
702
- touch-action: pan-x;
703
624
  }
704
- .mobile .bubble-toolbar-rail::-webkit-scrollbar { display: none; }
705
625
  .mobile .bubble-toolbar-rail > * { flex-shrink: 0; }
706
- .mobile button {
707
- min-width: 44px;
708
- min-height: 44px;
626
+ .mobile .bubble-toolbar-rail button {
627
+ width: 28px;
628
+ height: 28px;
629
+ padding: 0;
709
630
  display: inline-flex;
710
631
  align-items: center;
711
632
  justify-content: center;
712
633
  }
634
+ .mobile .bubble-toolbar-rail .bubble-block-trigger {
635
+ width: 32px;
636
+ gap: 1px;
637
+ }
638
+ .mobile .bubble-popover:not(.bubble-block-menu) button { min-width: 44px; min-height: 44px; }
713
639
  .mobile input[type="color"] { min-width: 44px; min-height: 44px; }
714
640
  .mobile .bubble-divider { display: none; }
715
641
  .mobile .bubble-popover-anchor { position: static; }
@@ -719,4 +645,39 @@
719
645
  max-height: min(var(--bubble-popup-height, 50dvh), 300px);
720
646
  overflow-y: auto;
721
647
  }
648
+ .bubble-block-menu {
649
+ width: 200px;
650
+ box-sizing: border-box;
651
+ padding: 4px;
652
+ background: var(--wh-100, var(--popover, var(--background, #fff)));
653
+ border: 1px solid var(--color-line, var(--border));
654
+ border-radius: var(--radius-2xl, 12px);
655
+ box-shadow: var(--shadow-popup, 0 8px 24px rgb(0 0 0 / 12%));
656
+ }
657
+ .mobile .bubble-block-menu { right: auto; max-width: 100%; }
658
+ .block-menu-item {
659
+ display: flex;
660
+ align-items: center;
661
+ width: 100%;
662
+ min-height: 32px;
663
+ gap: 8px;
664
+ padding: 6px 8px;
665
+ border-radius: var(--radius-8, 8px);
666
+ color: var(--gray-700, var(--foreground));
667
+ font: inherit;
668
+ font-size: 13px;
669
+ line-height: 20px;
670
+ text-align: left;
671
+ white-space: nowrap;
672
+ }
673
+ .block-menu-item:hover, .block-menu-item:focus-visible {
674
+ background: var(--wh-500, var(--muted));
675
+ }
676
+ .block-menu-item[aria-checked='true'] {
677
+ color: var(--gray-900, var(--foreground));
678
+ font-weight: 600;
679
+ }
680
+ .block-menu-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
681
+ .block-menu-icon { display: inline-flex; flex: none; color: var(--gray-500, var(--muted-foreground)); }
682
+ .block-menu-check { display: inline-flex; flex: none; color: var(--primary-500, var(--primary)); }
722
683
  </style>
@@ -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;AA0B3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAA4C,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEzF,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,8BAA8B;IAC9B,CAAC,CAAC,EAAE,gBAAgB,CAAC;CACtB,CAAC;AA+iBJ,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;AA2B3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAA4C,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEzF,KAAK,gBAAgB,GAAI;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,8BAA8B;IAC9B,CAAC,CAAC,EAAE,gBAAgB,CAAC;CACtB,CAAC;AAmfJ,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teriusu/rich-editor",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "description": "TipTap 기반 Notion-style 리치 텍스트 에디터 (Svelte)",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",