@commonpub/layer 0.7.1 → 0.7.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.
Files changed (43) hide show
  1. package/components/editors/ArticleEditor.vue +11 -12
  2. package/components/editors/BlogEditor.vue +17 -18
  3. package/components/editors/ExplainerEditor.vue +13 -14
  4. package/components/editors/ProjectEditor.vue +17 -18
  5. package/composables/useMarkdownImport.ts +1 -1
  6. package/package.json +8 -8
  7. package/pages/docs/[siteSlug]/edit.vue +4 -4
  8. package/pages/federated-hubs/[id]/posts/[postId].vue +91 -13
  9. package/pages/hubs/[slug]/index.vue +1 -1
  10. package/pages/hubs/[slug]/posts/[postId].vue +12 -0
  11. package/pages/u/[username]/[type]/[slug]/edit.vue +2 -1
  12. package/server/api/federated-hubs/[id]/posts/[postId]/replies.get.ts +13 -0
  13. package/server/api/federation/hub-post-reply.post.ts +16 -10
  14. package/components/editors/BlockCanvas.vue +0 -487
  15. package/components/editors/BlockInsertZone.vue +0 -84
  16. package/components/editors/BlockPicker.vue +0 -285
  17. package/components/editors/BlockWrapper.vue +0 -192
  18. package/components/editors/EditorBlocks.vue +0 -248
  19. package/components/editors/EditorSection.vue +0 -81
  20. package/components/editors/EditorShell.vue +0 -196
  21. package/components/editors/EditorTagInput.vue +0 -114
  22. package/components/editors/EditorVisibility.vue +0 -110
  23. package/components/editors/blocks/BuildStepBlock.vue +0 -102
  24. package/components/editors/blocks/CalloutBlock.vue +0 -122
  25. package/components/editors/blocks/CheckpointBlock.vue +0 -27
  26. package/components/editors/blocks/CodeBlock.vue +0 -177
  27. package/components/editors/blocks/DividerBlock.vue +0 -22
  28. package/components/editors/blocks/DownloadsBlock.vue +0 -41
  29. package/components/editors/blocks/EmbedBlock.vue +0 -20
  30. package/components/editors/blocks/GalleryBlock.vue +0 -236
  31. package/components/editors/blocks/HeadingBlock.vue +0 -96
  32. package/components/editors/blocks/ImageBlock.vue +0 -271
  33. package/components/editors/blocks/MarkdownBlock.vue +0 -258
  34. package/components/editors/blocks/MathBlock.vue +0 -37
  35. package/components/editors/blocks/PartsListBlock.vue +0 -358
  36. package/components/editors/blocks/QuizBlock.vue +0 -47
  37. package/components/editors/blocks/QuoteBlock.vue +0 -101
  38. package/components/editors/blocks/SectionHeaderBlock.vue +0 -130
  39. package/components/editors/blocks/SliderBlock.vue +0 -318
  40. package/components/editors/blocks/TextBlock.vue +0 -201
  41. package/components/editors/blocks/ToolListBlock.vue +0 -70
  42. package/components/editors/blocks/VideoBlock.vue +0 -22
  43. package/composables/useBlockEditor.ts +0 -187
@@ -1,84 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Insert zone between blocks — shows "+ Insert block" button.
4
- * Appears between every block and at the top/bottom of the canvas.
5
- */
6
- defineEmits<{
7
- insert: [];
8
- }>();
9
-
10
- const isDragOver = ref(false);
11
-
12
- function onDragOver(event: DragEvent): void {
13
- event.preventDefault();
14
- event.dataTransfer!.dropEffect = 'move';
15
- isDragOver.value = true;
16
- }
17
-
18
- function onDragLeave(): void {
19
- isDragOver.value = false;
20
- }
21
- </script>
22
-
23
- <template>
24
- <div
25
- class="cpub-insert-zone"
26
- :class="{ 'cpub-insert-zone--dragover': isDragOver }"
27
- @dragover="onDragOver"
28
- @dragleave="onDragLeave"
29
- @drop="isDragOver = false"
30
- >
31
- <button class="cpub-insert-btn" @click="$emit('insert')">
32
- <i class="fa-solid fa-plus"></i>
33
- <span>Insert block</span>
34
- </button>
35
- </div>
36
- </template>
37
-
38
- <style scoped>
39
- .cpub-insert-zone {
40
- display: flex;
41
- align-items: center;
42
- justify-content: center;
43
- height: 8px;
44
- position: relative;
45
- transition: height 0.15s;
46
- }
47
-
48
- .cpub-insert-zone:hover,
49
- .cpub-insert-zone--dragover {
50
- height: 36px;
51
- }
52
-
53
- .cpub-insert-btn {
54
- display: flex;
55
- align-items: center;
56
- gap: 6px;
57
- font-family: var(--font-mono);
58
- font-size: 10px;
59
- letter-spacing: 0.04em;
60
- color: var(--text-faint);
61
- background: transparent;
62
- border: 2px dashed transparent;
63
- padding: 4px 14px;
64
- cursor: pointer;
65
- opacity: 0;
66
- transition: opacity 0.15s, background 0.1s, border-color 0.1s, color 0.1s;
67
- }
68
-
69
- .cpub-insert-zone:hover .cpub-insert-btn,
70
- .cpub-insert-zone--dragover .cpub-insert-btn {
71
- opacity: 1;
72
- border-color: var(--border2);
73
- }
74
-
75
- .cpub-insert-btn:hover {
76
- border-color: var(--accent);
77
- color: var(--accent);
78
- background: var(--accent-bg);
79
- }
80
-
81
- .cpub-insert-btn i {
82
- font-size: 9px;
83
- }
84
- </style>
@@ -1,285 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Block type picker — appears when clicking an insert zone.
4
- * Shows available block types grouped by category, with search.
5
- */
6
- export interface BlockTypeDef {
7
- type: string;
8
- label: string;
9
- icon: string;
10
- description?: string;
11
- attrs?: Record<string, unknown>;
12
- }
13
-
14
- export interface BlockTypeGroup {
15
- name: string;
16
- blocks: BlockTypeDef[];
17
- }
18
-
19
- const props = defineProps<{
20
- groups: BlockTypeGroup[];
21
- visible: boolean;
22
- }>();
23
-
24
- const emit = defineEmits<{
25
- select: [type: string, attrs?: Record<string, unknown>];
26
- close: [];
27
- }>();
28
-
29
- const search = ref('');
30
- const selectedIndex = ref(0);
31
- const pickerRef = ref<HTMLElement | null>(null);
32
-
33
- const flatBlocks = computed(() => {
34
- return props.groups.flatMap((g) => g.blocks);
35
- });
36
-
37
- const filteredBlocks = computed(() => {
38
- const q = search.value.toLowerCase();
39
- if (!q) return flatBlocks.value;
40
- return flatBlocks.value.filter(
41
- (b) => b.label.toLowerCase().includes(q) || b.type.toLowerCase().includes(q),
42
- );
43
- });
44
-
45
- watch(() => props.visible, (v) => {
46
- if (v) {
47
- search.value = '';
48
- selectedIndex.value = 0;
49
- nextTick(() => {
50
- (pickerRef.value?.querySelector('.cpub-picker-search') as HTMLInputElement)?.focus();
51
- });
52
- }
53
- });
54
-
55
- watch(search, () => {
56
- selectedIndex.value = 0;
57
- });
58
-
59
- function handleKeydown(event: KeyboardEvent): void {
60
- if (event.key === 'Escape') {
61
- event.preventDefault();
62
- emit('close');
63
- return;
64
- }
65
- if (event.key === 'ArrowDown') {
66
- event.preventDefault();
67
- selectedIndex.value = Math.min(selectedIndex.value + 1, filteredBlocks.value.length - 1);
68
- return;
69
- }
70
- if (event.key === 'ArrowUp') {
71
- event.preventDefault();
72
- selectedIndex.value = Math.max(selectedIndex.value - 1, 0);
73
- return;
74
- }
75
- if (event.key === 'Enter') {
76
- event.preventDefault();
77
- const block = filteredBlocks.value[selectedIndex.value];
78
- if (block) {
79
- emit('select', block.type, block.attrs);
80
- }
81
- return;
82
- }
83
- }
84
-
85
- function selectBlock(block: BlockTypeDef): void {
86
- emit('select', block.type, block.attrs);
87
- }
88
-
89
- // Close on click outside
90
- function handleClickOutside(event: MouseEvent): void {
91
- if (pickerRef.value && !pickerRef.value.contains(event.target as Node)) {
92
- emit('close');
93
- }
94
- }
95
-
96
- onMounted(() => {
97
- document.addEventListener('mousedown', handleClickOutside);
98
- });
99
-
100
- onUnmounted(() => {
101
- document.removeEventListener('mousedown', handleClickOutside);
102
- });
103
- </script>
104
-
105
- <template>
106
- <div v-if="visible" ref="pickerRef" class="cpub-picker" @keydown="handleKeydown">
107
- <div class="cpub-picker-header">
108
- <i class="fa-solid fa-magnifying-glass cpub-picker-search-icon"></i>
109
- <input
110
- v-model="search"
111
- type="text"
112
- class="cpub-picker-search"
113
- placeholder="Search blocks..."
114
- aria-label="Search block types"
115
- />
116
- </div>
117
- <div class="cpub-picker-body">
118
- <template v-if="filteredBlocks.length > 0">
119
- <button
120
- v-for="(block, i) in filteredBlocks"
121
- :key="block.type + (block.attrs?.variant ?? '')"
122
- :data-block="block.type"
123
- class="cpub-picker-item"
124
- :class="{ 'cpub-picker-item--active': i === selectedIndex }"
125
- @mouseenter="selectedIndex = i"
126
- @click="selectBlock(block)"
127
- >
128
- <span class="cpub-picker-icon"><i :class="['fa-solid', block.icon]"></i></span>
129
- <span class="cpub-picker-text">
130
- <span class="cpub-picker-label">{{ block.label }}</span>
131
- <span v-if="block.description" class="cpub-picker-desc">{{ block.description }}</span>
132
- </span>
133
- </button>
134
- </template>
135
- <div v-else class="cpub-picker-empty">
136
- No blocks match "{{ search }}"
137
- </div>
138
- </div>
139
- </div>
140
- </template>
141
-
142
- <style scoped>
143
- .cpub-picker {
144
- position: absolute;
145
- left: 50%;
146
- transform: translateX(-50%);
147
- z-index: 100;
148
- background: var(--surface);
149
- border: var(--border-width-default) solid var(--border);
150
- box-shadow: var(--shadow-lg);
151
- min-width: 260px;
152
- max-width: 340px;
153
- max-height: 360px;
154
- display: flex;
155
- flex-direction: column;
156
- }
157
-
158
- .cpub-picker-header {
159
- display: flex;
160
- align-items: center;
161
- padding: 8px 10px;
162
- gap: 8px;
163
- border-bottom: var(--border-width-default) solid var(--border);
164
- flex-shrink: 0;
165
- }
166
-
167
- .cpub-picker-search-icon {
168
- font-size: 10px;
169
- color: var(--text-faint);
170
- flex-shrink: 0;
171
- }
172
-
173
- .cpub-picker-search {
174
- background: transparent;
175
- border: none;
176
- outline: none;
177
- font-size: 12px;
178
- color: var(--text);
179
- width: 100%;
180
- font-family: var(--font-sans);
181
- }
182
-
183
- .cpub-picker-search::placeholder {
184
- color: var(--text-faint);
185
- }
186
-
187
- .cpub-picker-body {
188
- overflow-y: auto;
189
- flex: 1;
190
- padding: 4px;
191
- }
192
-
193
- .cpub-picker-item {
194
- display: flex;
195
- align-items: center;
196
- gap: 10px;
197
- width: 100%;
198
- padding: 7px 10px;
199
- background: transparent;
200
- border: none;
201
- text-align: left;
202
- cursor: pointer;
203
- transition: background 0.08s;
204
- color: var(--text);
205
- font-size: 12px;
206
- }
207
-
208
- .cpub-picker-item:hover,
209
- .cpub-picker-item--active {
210
- background: var(--accent-bg);
211
- }
212
-
213
- .cpub-picker-icon {
214
- width: 26px;
215
- height: 26px;
216
- background: var(--surface2);
217
- border: var(--border-width-default) solid var(--border2);
218
- display: flex;
219
- align-items: center;
220
- justify-content: center;
221
- font-size: 10px;
222
- color: var(--text-dim);
223
- flex-shrink: 0;
224
- }
225
-
226
- .cpub-picker-item--active .cpub-picker-icon,
227
- .cpub-picker-item:hover .cpub-picker-icon {
228
- background: var(--accent-bg);
229
- border-color: var(--accent-border);
230
- color: var(--accent);
231
- }
232
-
233
- .cpub-picker-text {
234
- display: flex;
235
- flex-direction: column;
236
- min-width: 0;
237
- }
238
-
239
- .cpub-picker-label {
240
- font-size: 12px;
241
- font-weight: 500;
242
- }
243
-
244
- .cpub-picker-desc {
245
- font-size: 10px;
246
- color: var(--text-faint);
247
- font-family: var(--font-mono);
248
- }
249
-
250
- .cpub-picker-empty {
251
- padding: 16px;
252
- text-align: center;
253
- font-size: 11px;
254
- color: var(--text-faint);
255
- }
256
-
257
- /* Per-block-type icon colors */
258
- [data-block="heading"] .cpub-picker-icon { color: var(--teal); background: color-mix(in srgb, var(--teal) 10%, transparent); }
259
- [data-block="text"] .cpub-picker-icon,
260
- [data-block="paragraph"] .cpub-picker-icon { color: var(--text-dim); background: var(--surface2); }
261
- [data-block="image"] .cpub-picker-icon { color: #38bdf8; background: rgba(56, 189, 248, 0.08); }
262
- [data-block="code"] .cpub-picker-icon,
263
- [data-block="code_block"] .cpub-picker-icon { color: #c084fc; background: rgba(192, 132, 252, 0.08); }
264
- [data-block="callout"] .cpub-picker-icon { color: #fbbf24; background: rgba(251, 191, 36, 0.08); }
265
- [data-block="quote"] .cpub-picker-icon,
266
- [data-block="blockquote"] .cpub-picker-icon { color: #94a3b8; background: rgba(148, 163, 184, 0.08); }
267
- [data-block="embed"] .cpub-picker-icon { color: #f472b6; background: rgba(244, 114, 182, 0.08); }
268
- [data-block="video"] .cpub-picker-icon { color: #fb923c; background: rgba(251, 146, 60, 0.08); }
269
- [data-block="divider"] .cpub-picker-icon,
270
- [data-block="horizontal_rule"] .cpub-picker-icon,
271
- [data-block="horizontalRule"] .cpub-picker-icon { color: var(--text-faint); background: var(--surface2); }
272
- [data-block="gallery"] .cpub-picker-icon { color: #2dd4bf; background: rgba(45, 212, 191, 0.08); }
273
- [data-block="quiz"] .cpub-picker-icon { color: #4ade80; background: rgba(74, 222, 128, 0.08); }
274
- [data-block="slider"] .cpub-picker-icon,
275
- [data-block="interactiveSlider"] .cpub-picker-icon { color: #818cf8; background: rgba(129, 140, 248, 0.08); }
276
- [data-block="math"] .cpub-picker-icon,
277
- [data-block="mathNotation"] .cpub-picker-icon { color: #e879f9; background: rgba(232, 121, 249, 0.08); }
278
- [data-block="markdown"] .cpub-picker-icon { color: var(--text-dim); background: var(--surface2); }
279
- [data-block="buildStep"] .cpub-picker-icon { color: var(--accent); background: var(--accent-bg); }
280
- [data-block="partsList"] .cpub-picker-icon { color: #fb7185; background: rgba(251, 113, 133, 0.08); }
281
- [data-block="toolList"] .cpub-picker-icon { color: #a78bfa; background: rgba(167, 139, 250, 0.08); }
282
- [data-block="downloads"] .cpub-picker-icon { color: #22d3ee; background: rgba(34, 211, 238, 0.08); }
283
- [data-block="sectionHeader"] .cpub-picker-icon { color: var(--accent); background: var(--accent-bg); }
284
- [data-block="checkpoint"] .cpub-picker-icon { color: #34d399; background: rgba(52, 211, 153, 0.08); }
285
- </style>
@@ -1,192 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * Block wrapper — wraps every content block with:
4
- * - Drag handle (left, appears on hover)
5
- * - Block controls (top-right: move, clone, delete)
6
- * - Selected state (accent outline)
7
- * - Click-to-select
8
- */
9
- import type { EditorBlock } from '../../composables/useBlockEditor';
10
-
11
- const props = defineProps<{
12
- block: EditorBlock;
13
- selected: boolean;
14
- }>();
15
-
16
- const emit = defineEmits<{
17
- select: [];
18
- delete: [];
19
- duplicate: [];
20
- 'move-up': [];
21
- 'move-down': [];
22
- 'drag-start': [event: DragEvent];
23
- 'drag-end': [event: DragEvent];
24
- }>();
25
-
26
- function onDragStart(event: DragEvent): void {
27
- event.dataTransfer?.setData('text/plain', props.block.id);
28
- event.dataTransfer!.effectAllowed = 'move';
29
- emit('drag-start', event);
30
- }
31
-
32
- function onDragEnd(event: DragEvent): void {
33
- emit('drag-end', event);
34
- }
35
- </script>
36
-
37
- <template>
38
- <div
39
- class="cpub-block-wrap"
40
- :class="{ 'cpub-block-wrap--selected': selected }"
41
- @click.stop="emit('select')"
42
- >
43
- <!-- Drag handle (left side) -->
44
- <div class="cpub-block-handle">
45
- <button
46
- class="cpub-handle-btn"
47
- title="Drag to reorder"
48
- draggable="true"
49
- @dragstart="onDragStart"
50
- @dragend="onDragEnd"
51
- >
52
- <i class="fa-solid fa-grip-vertical"></i>
53
- </button>
54
- </div>
55
-
56
- <!-- Block controls (top-right, shown on hover) -->
57
- <div class="cpub-block-controls">
58
- <button class="cpub-block-ctrl" title="Move up" @click.stop="emit('move-up')">
59
- <i class="fa-solid fa-arrow-up"></i>
60
- </button>
61
- <button class="cpub-block-ctrl" title="Move down" @click.stop="emit('move-down')">
62
- <i class="fa-solid fa-arrow-down"></i>
63
- </button>
64
- <button class="cpub-block-ctrl" title="Duplicate" @click.stop="emit('duplicate')">
65
- <i class="fa-solid fa-copy"></i>
66
- </button>
67
- <button class="cpub-block-ctrl cpub-block-ctrl--danger" title="Delete" @click.stop="emit('delete')">
68
- <i class="fa-solid fa-trash"></i>
69
- </button>
70
- </div>
71
-
72
- <!-- Block content -->
73
- <div class="cpub-block-inner">
74
- <slot />
75
- </div>
76
- </div>
77
- </template>
78
-
79
- <style scoped>
80
- .cpub-block-wrap {
81
- position: relative;
82
- border: var(--border-width-default) solid transparent;
83
- transition: border-color 0.12s;
84
- }
85
-
86
- .cpub-block-wrap:hover {
87
- border-color: var(--border2);
88
- }
89
-
90
- .cpub-block-wrap--selected {
91
- border-color: var(--accent);
92
- box-shadow: 0 0 0 2px var(--accent-bg);
93
- }
94
-
95
- /* Drag handle — left side */
96
- .cpub-block-handle {
97
- position: absolute;
98
- left: -36px;
99
- top: 50%;
100
- transform: translateY(-50%);
101
- display: flex;
102
- flex-direction: column;
103
- gap: 2px;
104
- opacity: 0;
105
- transition: opacity 0.12s;
106
- }
107
-
108
- .cpub-block-wrap:hover .cpub-block-handle,
109
- .cpub-block-wrap--selected .cpub-block-handle {
110
- opacity: 1;
111
- }
112
-
113
- .cpub-handle-btn {
114
- width: 28px;
115
- height: 28px;
116
- display: flex;
117
- align-items: center;
118
- justify-content: center;
119
- background: var(--surface);
120
- border: var(--border-width-default) solid var(--border2);
121
- color: var(--text-faint);
122
- cursor: grab;
123
- font-size: 11px;
124
- padding: 0;
125
- }
126
-
127
- .cpub-handle-btn:hover {
128
- border-color: var(--border);
129
- color: var(--text-dim);
130
- background: var(--surface2);
131
- }
132
-
133
- .cpub-handle-btn:active {
134
- cursor: grabbing;
135
- }
136
-
137
- /* Block controls — top-right, offset above the block */
138
- .cpub-block-controls {
139
- --ctrl-surface: rgba(255, 255, 255, 0.15);
140
- position: absolute;
141
- top: -30px;
142
- right: 0;
143
- display: flex;
144
- gap: 0;
145
- background: var(--text);
146
- padding: 2px;
147
- opacity: 0;
148
- transition: opacity 0.12s;
149
- z-index: 10;
150
- }
151
-
152
- .cpub-block-wrap:hover .cpub-block-controls,
153
- .cpub-block-wrap--selected .cpub-block-controls {
154
- opacity: 1;
155
- }
156
-
157
- .cpub-block-ctrl {
158
- width: 26px;
159
- height: 26px;
160
- display: flex;
161
- align-items: center;
162
- justify-content: center;
163
- background: transparent;
164
- border: none;
165
- color: var(--surface3);
166
- cursor: pointer;
167
- font-size: 10px;
168
- padding: 0;
169
- transition: background 0.1s, color 0.1s;
170
- }
171
-
172
- .cpub-block-ctrl:hover {
173
- background: var(--ctrl-surface);
174
- color: var(--surface);
175
- }
176
-
177
- .cpub-block-ctrl--danger:hover {
178
- background: var(--red);
179
- color: var(--surface);
180
- }
181
-
182
- /* Block inner */
183
- .cpub-block-inner {
184
- min-height: 20px;
185
- }
186
-
187
- /* Touch devices: only show controls on selected block since there's no hover */
188
- @media (hover: none) {
189
- .cpub-block-wrap--selected .cpub-block-handle { opacity: 1; }
190
- .cpub-block-wrap--selected .cpub-block-controls { opacity: 1; }
191
- }
192
- </style>