@commonpub/editor 0.7.3 → 0.7.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/editor",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "type": "module",
5
5
  "description": "TipTap block editor with 18+ maker-focused extensions for CommonPub",
6
6
  "license": "AGPL-3.0-or-later",
@@ -51,7 +51,7 @@
51
51
  "unified": "^11.0.5",
52
52
  "zod": "^4.3.6",
53
53
  "@commonpub/config": "0.9.0",
54
- "@commonpub/schema": "0.9.4"
54
+ "@commonpub/schema": "0.9.5"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vue": "^3.4.0",
@@ -394,6 +394,13 @@ function onKeydown(event: KeyboardEvent): void {
394
394
 
395
395
  onMounted(() => { document.addEventListener('keydown', onKeydown); });
396
396
  onUnmounted(() => { document.removeEventListener('keydown', onKeydown); });
397
+
398
+ /** Expose formatting commands for external toolbar integration */
399
+ defineExpose({
400
+ toggleMark,
401
+ toggleLink,
402
+ getActiveMarks: () => floatingToolbar.value.activeMarks,
403
+ });
397
404
  </script>
398
405
 
399
406
  <template>
@@ -24,6 +24,8 @@ const flatBlocks = computed(() => {
24
24
  return props.groups.flatMap((g) => g.blocks);
25
25
  });
26
26
 
27
+ const isSearching = computed(() => search.value.trim().length > 0);
28
+
27
29
  const filteredBlocks = computed(() => {
28
30
  const q = search.value.toLowerCase();
29
31
  if (!q) return flatBlocks.value;
@@ -32,6 +34,13 @@ const filteredBlocks = computed(() => {
32
34
  );
33
35
  });
34
36
 
37
+ /** Flat index of a block across all groups (for keyboard nav) */
38
+ function globalIndex(groupIdx: number, blockIdx: number): number {
39
+ let idx = 0;
40
+ for (let g = 0; g < groupIdx; g++) idx += props.groups[g]!.blocks.length;
41
+ return idx + blockIdx;
42
+ }
43
+
35
44
  watch(() => props.visible, (v) => {
36
45
  if (v) {
37
46
  search.value = '';
@@ -104,7 +113,29 @@ onUnmounted(() => {
104
113
  />
105
114
  </div>
106
115
  <div class="cpub-picker-body">
107
- <template v-if="filteredBlocks.length > 0">
116
+ <!-- Grouped view (no search) -->
117
+ <template v-if="!isSearching && groups.length > 0">
118
+ <template v-for="(group, gi) in groups" :key="group.name">
119
+ <div class="cpub-picker-group-header">{{ group.name }}</div>
120
+ <button
121
+ v-for="(block, bi) in group.blocks"
122
+ :key="block.type + (block.attrs?.variant ?? '')"
123
+ :data-block="block.type"
124
+ class="cpub-picker-item"
125
+ :class="{ 'cpub-picker-item--active': globalIndex(gi, bi) === selectedIndex }"
126
+ @mouseenter="selectedIndex = globalIndex(gi, bi)"
127
+ @click="selectBlock(block)"
128
+ >
129
+ <span class="cpub-picker-icon"><i :class="['fa-solid', block.icon]"></i></span>
130
+ <span class="cpub-picker-text">
131
+ <span class="cpub-picker-label">{{ block.label }}</span>
132
+ <span v-if="block.description" class="cpub-picker-desc">{{ block.description }}</span>
133
+ </span>
134
+ </button>
135
+ </template>
136
+ </template>
137
+ <!-- Flat filtered view (searching) -->
138
+ <template v-else-if="filteredBlocks.length > 0">
108
139
  <button
109
140
  v-for="(block, i) in filteredBlocks"
110
141
  :key="block.type + (block.attrs?.variant ?? '')"
@@ -179,6 +210,21 @@ onUnmounted(() => {
179
210
  padding: 4px;
180
211
  }
181
212
 
213
+ .cpub-picker-group-header {
214
+ font-family: var(--font-mono);
215
+ font-size: 9px;
216
+ font-weight: 700;
217
+ text-transform: uppercase;
218
+ letter-spacing: 0.08em;
219
+ color: var(--text-faint);
220
+ padding: 8px 10px 4px;
221
+ margin-top: 2px;
222
+ }
223
+
224
+ .cpub-picker-group-header:first-child {
225
+ margin-top: 0;
226
+ }
227
+
182
228
  .cpub-picker-item {
183
229
  display: flex;
184
230
  align-items: center;
@@ -50,6 +50,15 @@ watch(() => props.selected, (sel) => {
50
50
  function onDragStart(event: DragEvent): void {
51
51
  event.dataTransfer?.setData('text/plain', props.block.id);
52
52
  event.dataTransfer!.effectAllowed = 'move';
53
+
54
+ // Custom drag preview showing block type
55
+ const preview = document.createElement('div');
56
+ preview.textContent = props.block.type.replace(/_/g, ' ');
57
+ preview.style.cssText = 'position:fixed;top:-999px;left:-999px;padding:6px 12px;background:#1a1a1a;color:#eee;font-family:monospace;font-size:11px;font-weight:600;text-transform:capitalize;border:2px solid #333;pointer-events:none;white-space:nowrap;';
58
+ document.body.appendChild(preview);
59
+ event.dataTransfer!.setDragImage(preview, 0, 0);
60
+ requestAnimationFrame(() => document.body.removeChild(preview));
61
+
53
62
  emit('drag-start', event);
54
63
  }
55
64