@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.
- package/components/editors/ArticleEditor.vue +11 -12
- package/components/editors/BlogEditor.vue +17 -18
- package/components/editors/ExplainerEditor.vue +13 -14
- package/components/editors/ProjectEditor.vue +17 -18
- package/composables/useMarkdownImport.ts +1 -1
- package/package.json +8 -8
- package/pages/docs/[siteSlug]/edit.vue +4 -4
- package/pages/federated-hubs/[id]/posts/[postId].vue +91 -13
- package/pages/hubs/[slug]/index.vue +1 -1
- package/pages/hubs/[slug]/posts/[postId].vue +12 -0
- package/pages/u/[username]/[type]/[slug]/edit.vue +2 -1
- package/server/api/federated-hubs/[id]/posts/[postId]/replies.get.ts +13 -0
- package/server/api/federation/hub-post-reply.post.ts +16 -10
- package/components/editors/BlockCanvas.vue +0 -487
- package/components/editors/BlockInsertZone.vue +0 -84
- package/components/editors/BlockPicker.vue +0 -285
- package/components/editors/BlockWrapper.vue +0 -192
- package/components/editors/EditorBlocks.vue +0 -248
- package/components/editors/EditorSection.vue +0 -81
- package/components/editors/EditorShell.vue +0 -196
- package/components/editors/EditorTagInput.vue +0 -114
- package/components/editors/EditorVisibility.vue +0 -110
- package/components/editors/blocks/BuildStepBlock.vue +0 -102
- package/components/editors/blocks/CalloutBlock.vue +0 -122
- package/components/editors/blocks/CheckpointBlock.vue +0 -27
- package/components/editors/blocks/CodeBlock.vue +0 -177
- package/components/editors/blocks/DividerBlock.vue +0 -22
- package/components/editors/blocks/DownloadsBlock.vue +0 -41
- package/components/editors/blocks/EmbedBlock.vue +0 -20
- package/components/editors/blocks/GalleryBlock.vue +0 -236
- package/components/editors/blocks/HeadingBlock.vue +0 -96
- package/components/editors/blocks/ImageBlock.vue +0 -271
- package/components/editors/blocks/MarkdownBlock.vue +0 -258
- package/components/editors/blocks/MathBlock.vue +0 -37
- package/components/editors/blocks/PartsListBlock.vue +0 -358
- package/components/editors/blocks/QuizBlock.vue +0 -47
- package/components/editors/blocks/QuoteBlock.vue +0 -101
- package/components/editors/blocks/SectionHeaderBlock.vue +0 -130
- package/components/editors/blocks/SliderBlock.vue +0 -318
- package/components/editors/blocks/TextBlock.vue +0 -201
- package/components/editors/blocks/ToolListBlock.vue +0 -70
- package/components/editors/blocks/VideoBlock.vue +0 -22
- package/composables/useBlockEditor.ts +0 -187
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type
|
|
3
|
-
import type { BlockTypeGroup } from './BlockPicker.vue';
|
|
2
|
+
import { BlockCanvas, EditorBlocks, EditorSection, EditorTagInput, EditorVisibility, type BlockEditor, type BlockTypeGroup } from '@commonpub/editor/vue';
|
|
4
3
|
|
|
5
4
|
const props = defineProps<{
|
|
6
5
|
blockEditor: BlockEditor;
|
|
@@ -201,7 +200,7 @@ const canvasMaxWidth = computed(() => {
|
|
|
201
200
|
|
|
202
201
|
<!-- Modules tab -->
|
|
203
202
|
<div v-if="activeLeftTab === 'modules'" class="cpub-ae-left-body">
|
|
204
|
-
<
|
|
203
|
+
<EditorBlocks :groups="blockTypes" :block-editor="blockEditor" />
|
|
205
204
|
</div>
|
|
206
205
|
|
|
207
206
|
<!-- Structure tab -->
|
|
@@ -267,7 +266,7 @@ const canvasMaxWidth = computed(() => {
|
|
|
267
266
|
<!-- Scrollable canvas -->
|
|
268
267
|
<div class="cpub-ae-canvas">
|
|
269
268
|
<div class="cpub-ae-canvas-inner" :style="{ maxWidth: canvasMaxWidth }">
|
|
270
|
-
<
|
|
269
|
+
<BlockCanvas :block-editor="blockEditor" :block-types="blockTypes" />
|
|
271
270
|
</div>
|
|
272
271
|
</div>
|
|
273
272
|
|
|
@@ -297,7 +296,7 @@ const canvasMaxWidth = computed(() => {
|
|
|
297
296
|
</div>
|
|
298
297
|
<div class="cpub-ae-right-body">
|
|
299
298
|
<!-- Content / Metadata -->
|
|
300
|
-
<
|
|
299
|
+
<EditorSection title="Content" icon="fa-pen-nib" :open="openSections.content" @toggle="toggleSection('content')">
|
|
301
300
|
<div class="cpub-ep-field">
|
|
302
301
|
<label class="cpub-ep-flabel">Slug</label>
|
|
303
302
|
<input class="cpub-ep-input" type="text" :value="metadata.slug" placeholder="auto-generated" @input="updateMeta('slug', ($event.target as HTMLInputElement).value)">
|
|
@@ -331,22 +330,22 @@ const canvasMaxWidth = computed(() => {
|
|
|
331
330
|
</div>
|
|
332
331
|
</template>
|
|
333
332
|
</div>
|
|
334
|
-
</
|
|
333
|
+
</EditorSection>
|
|
335
334
|
|
|
336
335
|
<!-- SEO -->
|
|
337
|
-
<
|
|
336
|
+
<EditorSection title="SEO" icon="fa-magnifying-glass" :open="openSections.seo" @toggle="toggleSection('seo')">
|
|
338
337
|
<div class="cpub-ep-field">
|
|
339
338
|
<label class="cpub-ep-flabel">Meta Description</label>
|
|
340
339
|
<textarea class="cpub-ep-textarea" rows="3" :value="metadata.seoDescription as string" placeholder="Search engine description..." @input="updateMeta('seoDescription', ($event.target as HTMLTextAreaElement).value)" />
|
|
341
340
|
<span class="cpub-ep-hint">{{ ((metadata.seoDescription as string) || '').length }}/160</span>
|
|
342
341
|
</div>
|
|
343
|
-
</
|
|
342
|
+
</EditorSection>
|
|
344
343
|
|
|
345
344
|
<!-- Publishing -->
|
|
346
|
-
<
|
|
345
|
+
<EditorSection title="Publishing" icon="fa-rocket" :open="openSections.publishing" @toggle="toggleSection('publishing')">
|
|
347
346
|
<div class="cpub-ep-field">
|
|
348
347
|
<label class="cpub-ep-flabel">Visibility</label>
|
|
349
|
-
<
|
|
348
|
+
<EditorVisibility :model-value="visibility" @update:model-value="onVisibilityUpdate" />
|
|
350
349
|
</div>
|
|
351
350
|
<div class="cpub-ep-field">
|
|
352
351
|
<label class="cpub-ep-flabel">Category</label>
|
|
@@ -362,9 +361,9 @@ const canvasMaxWidth = computed(() => {
|
|
|
362
361
|
</div>
|
|
363
362
|
<div class="cpub-ep-field">
|
|
364
363
|
<label class="cpub-ep-flabel">Tags</label>
|
|
365
|
-
<
|
|
364
|
+
<EditorTagInput :tags="tags" @update:tags="onTagsUpdate" />
|
|
366
365
|
</div>
|
|
367
|
-
</
|
|
366
|
+
</EditorSection>
|
|
368
367
|
</div>
|
|
369
368
|
</aside>
|
|
370
369
|
</div>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type
|
|
3
|
-
import type { BlockTypeGroup } from './BlockPicker.vue';
|
|
2
|
+
import { BlockCanvas, EditorBlocks, EditorSection, EditorTagInput, EditorVisibility, type BlockEditor, type BlockTypeGroup } from '@commonpub/editor/vue';
|
|
4
3
|
|
|
5
4
|
const props = defineProps<{
|
|
6
5
|
blockEditor: BlockEditor;
|
|
@@ -149,7 +148,7 @@ const canvasMaxWidth = computed(() => {
|
|
|
149
148
|
|
|
150
149
|
<!-- LEFT: Block Library -->
|
|
151
150
|
<aside class="cpub-be-library" :class="{ 'cpub-be-sidebar-open': mobileLeftOpen }" aria-label="Block library">
|
|
152
|
-
<
|
|
151
|
+
<EditorBlocks :groups="blockTypes" :block-editor="blockEditor" />
|
|
153
152
|
</aside>
|
|
154
153
|
|
|
155
154
|
<!-- CENTER: Canvas with toolbar, cover, title, subtitle, byline, blocks -->
|
|
@@ -231,7 +230,7 @@ const canvasMaxWidth = computed(() => {
|
|
|
231
230
|
</div>
|
|
232
231
|
|
|
233
232
|
<!-- Block editor canvas -->
|
|
234
|
-
<
|
|
233
|
+
<BlockCanvas :block-editor="blockEditor" :block-types="blockTypes" />
|
|
235
234
|
</div>
|
|
236
235
|
|
|
237
236
|
<!-- Word count bar -->
|
|
@@ -247,7 +246,7 @@ const canvasMaxWidth = computed(() => {
|
|
|
247
246
|
<aside class="cpub-be-right" :class="{ 'cpub-be-sidebar-open': mobileRightOpen }" aria-label="Blog properties">
|
|
248
247
|
<div class="cpub-be-right-body">
|
|
249
248
|
<!-- Meta -->
|
|
250
|
-
<
|
|
249
|
+
<EditorSection title="Meta" icon="fa-tag" :open="openSections.meta" @toggle="toggleSection('meta')">
|
|
251
250
|
<div class="cpub-ep-field">
|
|
252
251
|
<label class="cpub-ep-flabel">Slug</label>
|
|
253
252
|
<input class="cpub-ep-input cpub-ep-input-mono" type="text" :value="metadata.slug" placeholder="auto-generated" @input="updateMeta('slug', ($event.target as HTMLInputElement).value)">
|
|
@@ -266,21 +265,21 @@ const canvasMaxWidth = computed(() => {
|
|
|
266
265
|
</div>
|
|
267
266
|
<div class="cpub-ep-field">
|
|
268
267
|
<label class="cpub-ep-flabel">Tags</label>
|
|
269
|
-
<
|
|
268
|
+
<EditorTagInput :tags="tags" @update:tags="onTagsUpdate" />
|
|
270
269
|
</div>
|
|
271
|
-
</
|
|
270
|
+
</EditorSection>
|
|
272
271
|
|
|
273
272
|
<!-- Excerpt / Description -->
|
|
274
|
-
<
|
|
273
|
+
<EditorSection title="Excerpt" icon="fa-align-left" :open="openSections.excerpt" @toggle="toggleSection('excerpt')">
|
|
275
274
|
<div class="cpub-ep-field">
|
|
276
275
|
<label class="cpub-ep-flabel">Custom Excerpt</label>
|
|
277
276
|
<textarea class="cpub-ep-textarea" rows="3" :value="(metadata.description as string) || ''" placeholder="Short description shown in feed previews..." @input="updateMeta('description', ($event.target as HTMLTextAreaElement).value)" />
|
|
278
277
|
<span class="cpub-ep-hint cpub-ep-hint-right">{{ ((metadata.description as string) || '').length }} / 300</span>
|
|
279
278
|
</div>
|
|
280
|
-
</
|
|
279
|
+
</EditorSection>
|
|
281
280
|
|
|
282
281
|
<!-- SEO Preview -->
|
|
283
|
-
<
|
|
282
|
+
<EditorSection title="SEO Preview" icon="fa-brands fa-google" :open="openSections.seo" @toggle="toggleSection('seo')">
|
|
284
283
|
<div class="cpub-be-seo-card">
|
|
285
284
|
<div class="cpub-be-seo-url">
|
|
286
285
|
<span class="cpub-be-seo-favicon">C</span>
|
|
@@ -294,13 +293,13 @@ const canvasMaxWidth = computed(() => {
|
|
|
294
293
|
<textarea class="cpub-ep-textarea" rows="3" :value="(metadata.seoDescription as string) || ''" placeholder="Search engine description..." @input="updateMeta('seoDescription', ($event.target as HTMLTextAreaElement).value)" />
|
|
295
294
|
<span class="cpub-ep-hint cpub-ep-hint-right">{{ ((metadata.seoDescription as string) || '').length }}/160</span>
|
|
296
295
|
</div>
|
|
297
|
-
</
|
|
296
|
+
</EditorSection>
|
|
298
297
|
|
|
299
298
|
<!-- Publishing -->
|
|
300
|
-
<
|
|
299
|
+
<EditorSection title="Publishing" icon="fa-globe" :open="openSections.publishing" @toggle="toggleSection('publishing')">
|
|
301
300
|
<div class="cpub-ep-field">
|
|
302
301
|
<label class="cpub-ep-flabel">Visibility</label>
|
|
303
|
-
<
|
|
302
|
+
<EditorVisibility :model-value="(metadata.visibility as string) || 'public'" @update:model-value="(v: string) => updateMeta('visibility', v)" />
|
|
304
303
|
</div>
|
|
305
304
|
<div class="cpub-ep-field">
|
|
306
305
|
<label class="cpub-be-schedule-row">
|
|
@@ -319,10 +318,10 @@ const canvasMaxWidth = computed(() => {
|
|
|
319
318
|
<label class="cpub-ep-flabel">Series <span class="cpub-ep-optional">(optional)</span></label>
|
|
320
319
|
<input class="cpub-ep-input" type="text" :value="metadata.series" placeholder="e.g. Home Lab Chronicles" @input="updateMeta('series', ($event.target as HTMLInputElement).value)">
|
|
321
320
|
</div>
|
|
322
|
-
</
|
|
321
|
+
</EditorSection>
|
|
323
322
|
|
|
324
323
|
<!-- Author -->
|
|
325
|
-
<
|
|
324
|
+
<EditorSection title="Author" icon="fa-user" :open="openSections.author" @toggle="toggleSection('author')">
|
|
326
325
|
<div class="cpub-be-author-row">
|
|
327
326
|
<div class="cpub-be-author-av">{{ authorInitials }}</div>
|
|
328
327
|
<div class="cpub-be-author-info">
|
|
@@ -331,10 +330,10 @@ const canvasMaxWidth = computed(() => {
|
|
|
331
330
|
</div>
|
|
332
331
|
<span class="cpub-be-author-badge">You</span>
|
|
333
332
|
</div>
|
|
334
|
-
</
|
|
333
|
+
</EditorSection>
|
|
335
334
|
|
|
336
335
|
<!-- Social -->
|
|
337
|
-
<
|
|
336
|
+
<EditorSection title="Social" icon="fa-share-nodes" :open="openSections.social" @toggle="toggleSection('social')">
|
|
338
337
|
<div class="cpub-ep-field">
|
|
339
338
|
<label class="cpub-ep-flabel">Open Graph Image</label>
|
|
340
339
|
<div class="cpub-be-og-thumb">
|
|
@@ -344,7 +343,7 @@ const canvasMaxWidth = computed(() => {
|
|
|
344
343
|
</div>
|
|
345
344
|
</div>
|
|
346
345
|
</div>
|
|
347
|
-
</
|
|
346
|
+
</EditorSection>
|
|
348
347
|
</div>
|
|
349
348
|
</aside>
|
|
350
349
|
</div>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type
|
|
3
|
-
import type { BlockTypeGroup } from './BlockPicker.vue';
|
|
2
|
+
import { BlockCanvas, EditorBlocks, EditorSection, EditorTagInput, EditorVisibility, type BlockEditor, type BlockTypeGroup } from '@commonpub/editor/vue';
|
|
4
3
|
|
|
5
4
|
const props = defineProps<{
|
|
6
5
|
blockEditor: BlockEditor;
|
|
@@ -203,7 +202,7 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
203
202
|
</div>
|
|
204
203
|
|
|
205
204
|
<div v-if="activeLeftTab === 'modules'" class="cpub-ee-left-body">
|
|
206
|
-
<
|
|
205
|
+
<EditorBlocks :groups="blockTypes" :block-editor="blockEditor" />
|
|
207
206
|
</div>
|
|
208
207
|
|
|
209
208
|
<div v-else-if="activeLeftTab === 'structure'" class="cpub-ee-left-body" style="padding: 10px;">
|
|
@@ -299,7 +298,7 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
299
298
|
|
|
300
299
|
<div class="cpub-ee-canvas">
|
|
301
300
|
<div class="cpub-ee-canvas-inner" :style="{ maxWidth: canvasMaxWidth }">
|
|
302
|
-
<
|
|
301
|
+
<BlockCanvas :block-editor="blockEditor" :block-types="blockTypes" />
|
|
303
302
|
</div>
|
|
304
303
|
</div>
|
|
305
304
|
|
|
@@ -318,7 +317,7 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
318
317
|
<!-- RIGHT: Properties -->
|
|
319
318
|
<aside class="cpub-ee-right" :class="{ 'cpub-ee-sidebar-open': mobileRightOpen }" aria-label="Explainer properties">
|
|
320
319
|
<div class="cpub-ee-right-body">
|
|
321
|
-
<
|
|
320
|
+
<EditorSection title="Content" icon="fa-sliders" :open="openSections.section" @toggle="toggleSection('section')">
|
|
322
321
|
<div class="cpub-ep-field">
|
|
323
322
|
<label class="cpub-ep-flabel">Slug</label>
|
|
324
323
|
<input class="cpub-ep-input" type="text" :value="metadata.slug" placeholder="auto-generated" @input="updateMeta('slug', ($event.target as HTMLInputElement).value)">
|
|
@@ -333,27 +332,27 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
333
332
|
</div>
|
|
334
333
|
<div class="cpub-ep-field">
|
|
335
334
|
<label class="cpub-ep-flabel">Tags</label>
|
|
336
|
-
<
|
|
335
|
+
<EditorTagInput :tags="tags" @update:tags="onTagsUpdate" />
|
|
337
336
|
</div>
|
|
338
|
-
</
|
|
337
|
+
</EditorSection>
|
|
339
338
|
|
|
340
|
-
<
|
|
339
|
+
<EditorSection title="Difficulty" icon="fa-gauge-high" :open="openSections.difficulty" @toggle="toggleSection('difficulty')">
|
|
341
340
|
<select class="cpub-ep-select" :value="metadata.difficulty || 'beginner'" @change="updateMeta('difficulty', ($event.target as HTMLSelectElement).value)">
|
|
342
341
|
<option value="beginner">Beginner</option>
|
|
343
342
|
<option value="intermediate">Intermediate</option>
|
|
344
343
|
<option value="advanced">Advanced</option>
|
|
345
344
|
</select>
|
|
346
|
-
</
|
|
345
|
+
</EditorSection>
|
|
347
346
|
|
|
348
|
-
<
|
|
349
|
-
<
|
|
350
|
-
</
|
|
347
|
+
<EditorSection title="Visibility" icon="fa-eye" :open="openSections.visibility" @toggle="toggleSection('visibility')">
|
|
348
|
+
<EditorVisibility :model-value="visibility" @update:model-value="onVisibilityUpdate" />
|
|
349
|
+
</EditorSection>
|
|
351
350
|
|
|
352
|
-
<
|
|
351
|
+
<EditorSection title="Cover Image" icon="fa-image" :open="openSections.cover" @toggle="toggleSection('cover')">
|
|
353
352
|
<div class="cpub-ep-field">
|
|
354
353
|
<input class="cpub-ep-input" type="url" :value="metadata.coverImageUrl" placeholder="https://..." @input="updateMeta('coverImageUrl', ($event.target as HTMLInputElement).value)">
|
|
355
354
|
</div>
|
|
356
|
-
</
|
|
355
|
+
</EditorSection>
|
|
357
356
|
</div>
|
|
358
357
|
</aside>
|
|
359
358
|
</div>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type
|
|
3
|
-
import type { BlockTypeGroup } from './BlockPicker.vue';
|
|
2
|
+
import { BlockCanvas, EditorBlocks, EditorSection, EditorTagInput, EditorVisibility, type BlockEditor, type BlockTypeGroup } from '@commonpub/editor/vue';
|
|
4
3
|
|
|
5
4
|
const props = defineProps<{
|
|
6
5
|
blockEditor: BlockEditor;
|
|
@@ -151,7 +150,7 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
151
150
|
|
|
152
151
|
<!-- LEFT: Block Library -->
|
|
153
152
|
<aside class="cpub-pe-library" :class="{ 'cpub-pe-sidebar-open': mobileLeftOpen }" aria-label="Block library">
|
|
154
|
-
<
|
|
153
|
+
<EditorBlocks :groups="blockTypes" :block-editor="blockEditor" />
|
|
155
154
|
</aside>
|
|
156
155
|
|
|
157
156
|
<!-- CENTER: Canvas with toolbar -->
|
|
@@ -202,7 +201,7 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
202
201
|
placeholder="Project title..."
|
|
203
202
|
/>
|
|
204
203
|
|
|
205
|
-
<
|
|
204
|
+
<BlockCanvas :block-editor="blockEditor" :block-types="blockTypes" />
|
|
206
205
|
</div>
|
|
207
206
|
</div>
|
|
208
207
|
|
|
@@ -217,7 +216,7 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
217
216
|
<!-- RIGHT: Settings Panel -->
|
|
218
217
|
<aside class="cpub-pe-settings" :class="{ 'cpub-pe-sidebar-open': mobileRightOpen }" aria-label="Project settings">
|
|
219
218
|
<div class="cpub-pe-settings-body">
|
|
220
|
-
<
|
|
219
|
+
<EditorSection title="Project Meta" icon="fa-sliders" :open="openSections.meta" @toggle="toggleSection('meta')">
|
|
221
220
|
<div class="cpub-ep-field">
|
|
222
221
|
<label class="cpub-ep-flabel">Slug</label>
|
|
223
222
|
<input class="cpub-ep-input" type="text" :value="metadata.slug" placeholder="project-url-slug" @input="updateMeta('slug', ($event.target as HTMLInputElement).value)">
|
|
@@ -246,17 +245,17 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
246
245
|
<label class="cpub-ep-flabel">Description</label>
|
|
247
246
|
<textarea class="cpub-ep-textarea" rows="3" :value="metadata.description as string" placeholder="Brief project description..." @input="updateMeta('description', ($event.target as HTMLTextAreaElement).value)" />
|
|
248
247
|
</div>
|
|
249
|
-
</
|
|
248
|
+
</EditorSection>
|
|
250
249
|
|
|
251
|
-
<
|
|
252
|
-
<
|
|
253
|
-
</
|
|
250
|
+
<EditorSection title="Tags" icon="fa-tag" :open="openSections.tags" @toggle="toggleSection('tags')">
|
|
251
|
+
<EditorTagInput :tags="tags" @update:tags="onTagsUpdate" />
|
|
252
|
+
</EditorSection>
|
|
254
253
|
|
|
255
|
-
<
|
|
256
|
-
<
|
|
257
|
-
</
|
|
254
|
+
<EditorSection title="Visibility" icon="fa-eye" :open="openSections.visibility" @toggle="toggleSection('visibility')">
|
|
255
|
+
<EditorVisibility :model-value="visibility" @update:model-value="onVisibilityUpdate" />
|
|
256
|
+
</EditorSection>
|
|
258
257
|
|
|
259
|
-
<
|
|
258
|
+
<EditorSection title="Cover Image" icon="fa-image" :open="openSections.cover" @toggle="toggleSection('cover')">
|
|
260
259
|
<div class="cpub-pe-cover" :class="{ 'has-image': !!coverImageUrl }">
|
|
261
260
|
<template v-if="coverImageUrl">
|
|
262
261
|
<img :src="coverImageUrl" alt="Cover image" class="cpub-pe-cover-img" />
|
|
@@ -282,17 +281,17 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
282
281
|
</div>
|
|
283
282
|
</template>
|
|
284
283
|
</div>
|
|
285
|
-
</
|
|
284
|
+
</EditorSection>
|
|
286
285
|
|
|
287
|
-
<
|
|
286
|
+
<EditorSection title="SEO" icon="fa-magnifying-glass" :open="openSections.seo" @toggle="toggleSection('seo')">
|
|
288
287
|
<div class="cpub-pe-field">
|
|
289
288
|
<label class="cpub-pe-flabel">Meta Description</label>
|
|
290
289
|
<textarea class="cpub-pe-textarea" rows="3" :value="metadata.seoDescription as string" placeholder="Search engine description (recommended 50-160 chars)" @input="updateMeta('seoDescription', ($event.target as HTMLTextAreaElement).value)" />
|
|
291
290
|
<span class="cpub-pe-hint">{{ ((metadata.seoDescription as string) || '').length }}/160</span>
|
|
292
291
|
</div>
|
|
293
|
-
</
|
|
292
|
+
</EditorSection>
|
|
294
293
|
|
|
295
|
-
<
|
|
294
|
+
<EditorSection title="Checklist" icon="fa-circle-check" :open="openSections.checklist" @toggle="toggleSection('checklist')">
|
|
296
295
|
<div class="cpub-pe-checklist">
|
|
297
296
|
<div v-for="item in checklist" :key="item.label" class="cpub-pe-check-item" :class="{ pass: item.pass }">
|
|
298
297
|
<i :class="item.pass ? 'fa-regular fa-square-check' : 'fa-regular fa-square'" :style="{ color: item.pass ? 'var(--green)' : 'var(--text-faint)' }"></i>
|
|
@@ -302,7 +301,7 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
302
301
|
<div class="cpub-pe-checklist-summary">
|
|
303
302
|
{{ checklistDone }}/{{ checklist.length }} complete
|
|
304
303
|
</div>
|
|
305
|
-
</
|
|
304
|
+
</EditorSection>
|
|
306
305
|
</div>
|
|
307
306
|
</aside>
|
|
308
307
|
</div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { markdownToBlockTuples } from '@commonpub/editor';
|
|
5
5
|
import type { BlockTuple } from '@commonpub/editor';
|
|
6
|
-
import type { BlockEditor } from '
|
|
6
|
+
import type { BlockEditor } from '@commonpub/editor/vue';
|
|
7
7
|
|
|
8
8
|
export function useMarkdownImport(blockEditor: BlockEditor) {
|
|
9
9
|
const importing = ref(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -52,14 +52,14 @@
|
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
53
|
"@commonpub/auth": "0.5.0",
|
|
54
54
|
"@commonpub/config": "0.8.0",
|
|
55
|
-
"@commonpub/editor": "0.5.0",
|
|
56
|
-
"@commonpub/learning": "0.5.0",
|
|
57
55
|
"@commonpub/docs": "0.6.1",
|
|
58
|
-
"@commonpub/
|
|
59
|
-
"@commonpub/
|
|
60
|
-
"@commonpub/
|
|
61
|
-
"@commonpub/
|
|
62
|
-
"@commonpub/
|
|
56
|
+
"@commonpub/explainer": "0.7.3",
|
|
57
|
+
"@commonpub/editor": "0.6.0",
|
|
58
|
+
"@commonpub/learning": "0.5.0",
|
|
59
|
+
"@commonpub/protocol": "0.9.7",
|
|
60
|
+
"@commonpub/schema": "0.9.2",
|
|
61
|
+
"@commonpub/server": "2.27.2",
|
|
62
|
+
"@commonpub/ui": "0.8.4"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { BlockTuple } from '@commonpub/editor';
|
|
3
|
-
import type
|
|
3
|
+
import { BlockCanvas, EditorShell, useBlockEditor, type BlockTypeGroup } from '@commonpub/editor/vue';
|
|
4
4
|
import type { PageTreeItem } from '../../../components/editors/DocsPageTree.vue';
|
|
5
5
|
|
|
6
6
|
definePageMeta({ layout: false, middleware: 'auth' });
|
|
@@ -489,7 +489,7 @@ async function createVersion(): Promise<void> {
|
|
|
489
489
|
</div>
|
|
490
490
|
|
|
491
491
|
<!-- 3-panel editor -->
|
|
492
|
-
<
|
|
492
|
+
<EditorShell :show-left-sidebar="true" :show-right-sidebar="!!selectedPageId">
|
|
493
493
|
<!-- LEFT: Page tree -->
|
|
494
494
|
<template #left>
|
|
495
495
|
<div class="cpub-docs-left-header">
|
|
@@ -543,7 +543,7 @@ async function createVersion(): Promise<void> {
|
|
|
543
543
|
</div>
|
|
544
544
|
|
|
545
545
|
<!-- Block canvas -->
|
|
546
|
-
<
|
|
546
|
+
<BlockCanvas
|
|
547
547
|
:block-editor="blockEditor"
|
|
548
548
|
:block-types="blockTypes"
|
|
549
549
|
/>
|
|
@@ -649,7 +649,7 @@ async function createVersion(): Promise<void> {
|
|
|
649
649
|
</span>
|
|
650
650
|
</span>
|
|
651
651
|
</template>
|
|
652
|
-
</
|
|
652
|
+
</EditorShell>
|
|
653
653
|
|
|
654
654
|
<!-- Markdown import dialog -->
|
|
655
655
|
<EditorsMarkdownImportDialog
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { FederatedHubListItem, FederatedHubPostItem } from '@commonpub/server';
|
|
2
|
+
import type { FederatedHubListItem, FederatedHubPostItem, FederatedHubPostReplyItem } from '@commonpub/server';
|
|
3
3
|
|
|
4
4
|
const route = useRoute();
|
|
5
5
|
const id = route.params.id as string;
|
|
@@ -10,6 +10,9 @@ const toast = useToast();
|
|
|
10
10
|
|
|
11
11
|
const { data: hub } = useLazyFetch<FederatedHubListItem>(`/api/federated-hubs/${id}`);
|
|
12
12
|
const { data: post, refresh: refreshPost } = useLazyFetch<FederatedHubPostItem>(`/api/federated-hubs/${id}/posts/${postId}`);
|
|
13
|
+
const { data: repliesData, refresh: refreshReplies } = useLazyFetch<{ items: FederatedHubPostReplyItem[]; total: number }>(`/api/federated-hubs/${id}/posts/${postId}/replies`, { default: () => ({ items: [], total: 0 }) });
|
|
14
|
+
|
|
15
|
+
const replies = computed(() => repliesData.value?.items ?? []);
|
|
13
16
|
|
|
14
17
|
function formatDate(d: string | Date | null): string {
|
|
15
18
|
if (!d) return '';
|
|
@@ -68,6 +71,7 @@ async function handleLike(): Promise<void> {
|
|
|
68
71
|
// Reply
|
|
69
72
|
const replyContent = ref('');
|
|
70
73
|
const replying = ref(false);
|
|
74
|
+
const replyingTo = ref<string | null>(null);
|
|
71
75
|
|
|
72
76
|
async function handleReply(): Promise<void> {
|
|
73
77
|
if (!replyContent.value.trim()) return;
|
|
@@ -75,11 +79,12 @@ async function handleReply(): Promise<void> {
|
|
|
75
79
|
try {
|
|
76
80
|
await $fetch('/api/federation/hub-post-reply' as string, {
|
|
77
81
|
method: 'POST',
|
|
78
|
-
body: { federatedHubPostId: postId, content: replyContent.value },
|
|
82
|
+
body: { federatedHubPostId: postId, content: replyContent.value, parentId: replyingTo.value || undefined },
|
|
79
83
|
});
|
|
80
84
|
replyContent.value = '';
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
replyingTo.value = null;
|
|
86
|
+
toast.success('Reply posted');
|
|
87
|
+
await Promise.all([refreshReplies(), refreshPost()]);
|
|
83
88
|
} catch {
|
|
84
89
|
toast.error('Failed to send reply');
|
|
85
90
|
} finally {
|
|
@@ -162,12 +167,15 @@ useHead({
|
|
|
162
167
|
|
|
163
168
|
<!-- Reply form -->
|
|
164
169
|
<div v-if="isAuthenticated" class="cpub-reply-form">
|
|
170
|
+
<div v-if="replyingTo" class="cpub-replying-to">
|
|
171
|
+
Replying to a comment <button class="cpub-cancel-reply" @click="replyingTo = null"><i class="fa-solid fa-xmark"></i></button>
|
|
172
|
+
</div>
|
|
165
173
|
<div class="cpub-reply-row">
|
|
166
174
|
<input
|
|
167
175
|
v-model="replyContent"
|
|
168
176
|
class="cpub-reply-input"
|
|
169
177
|
type="text"
|
|
170
|
-
placeholder="Write a reply
|
|
178
|
+
placeholder="Write a reply..."
|
|
171
179
|
aria-label="Write a reply"
|
|
172
180
|
@keydown.enter="handleReply"
|
|
173
181
|
/>
|
|
@@ -176,18 +184,53 @@ useHead({
|
|
|
176
184
|
</button>
|
|
177
185
|
</div>
|
|
178
186
|
<p class="cpub-fed-reply-hint">
|
|
179
|
-
<i class="fa-solid fa-globe"></i> Your reply will be sent to {{ hub?.originDomain }} via ActivityPub
|
|
187
|
+
<i class="fa-solid fa-globe"></i> Your reply will also be sent to {{ hub?.originDomain }} via ActivityPub
|
|
180
188
|
</p>
|
|
181
189
|
</div>
|
|
182
190
|
|
|
183
|
-
<!--
|
|
191
|
+
<!-- Replies -->
|
|
184
192
|
<div class="cpub-replies-section">
|
|
185
|
-
<
|
|
186
|
-
|
|
193
|
+
<h3 v-if="replies.length" class="cpub-replies-title">{{ repliesData?.total ?? 0 }} Local Replies</h3>
|
|
194
|
+
<div v-for="reply in replies" :key="reply.id" class="cpub-reply">
|
|
195
|
+
<div class="cpub-reply-author">
|
|
196
|
+
<div class="cpub-reply-avatar">
|
|
197
|
+
<img v-if="reply.author?.avatarUrl" :src="reply.author.avatarUrl" :alt="reply.author?.displayName || reply.author?.username" class="cpub-reply-avatar-img" />
|
|
198
|
+
<span v-else>{{ (reply.author?.displayName || reply.author?.username || 'U').charAt(0).toUpperCase() }}</span>
|
|
199
|
+
</div>
|
|
200
|
+
<NuxtLink v-if="reply.author" :to="`/u/${reply.author.username}`" class="cpub-reply-author-name">{{ reply.author.displayName || reply.author.username }}</NuxtLink>
|
|
201
|
+
<span class="cpub-post-sep">·</span>
|
|
202
|
+
<time class="cpub-post-time">{{ formatDate(reply.createdAt) }}</time>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="cpub-reply-content"><MentionText :text="reply.content" /></div>
|
|
205
|
+
<div class="cpub-reply-actions">
|
|
206
|
+
<button v-if="isAuthenticated" class="cpub-reply-btn" @click="replyingTo = reply.id; replyContent = `@${reply.author?.username ?? ''} `">
|
|
207
|
+
<i class="fa-solid fa-reply"></i> Reply
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<!-- Nested replies -->
|
|
212
|
+
<div v-if="reply.replies?.length" class="cpub-nested-replies">
|
|
213
|
+
<div v-for="child in reply.replies" :key="child.id" class="cpub-reply cpub-reply-nested">
|
|
214
|
+
<div class="cpub-reply-author">
|
|
215
|
+
<div class="cpub-reply-avatar">
|
|
216
|
+
<img v-if="child.author?.avatarUrl" :src="child.author.avatarUrl" :alt="child.author?.displayName || child.author?.username" class="cpub-reply-avatar-img" />
|
|
217
|
+
<span v-else>{{ (child.author?.displayName || child.author?.username || 'U').charAt(0).toUpperCase() }}</span>
|
|
218
|
+
</div>
|
|
219
|
+
<NuxtLink v-if="child.author" :to="`/u/${child.author.username}`" class="cpub-reply-author-name">{{ child.author.displayName || child.author.username }}</NuxtLink>
|
|
220
|
+
<span class="cpub-post-sep">·</span>
|
|
221
|
+
<time class="cpub-post-time">{{ formatDate(child.createdAt) }}</time>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="cpub-reply-content"><MentionText :text="child.content" /></div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<!-- Federation thread info -->
|
|
229
|
+
<div class="cpub-fed-thread-info" style="padding: 16px">
|
|
187
230
|
<p class="cpub-empty-state-desc">
|
|
188
|
-
|
|
231
|
+
<i class="fa-solid fa-globe"></i>
|
|
189
232
|
<a v-if="post.objectUri" :href="post.objectUri" target="_blank" rel="noopener noreferrer" class="cpub-inline-link">
|
|
190
|
-
View full thread on
|
|
233
|
+
View full thread on {{ hub?.originDomain }} <i class="fa-solid fa-arrow-up-right-from-square"></i>
|
|
191
234
|
</a>
|
|
192
235
|
</p>
|
|
193
236
|
</div>
|
|
@@ -278,6 +321,11 @@ useHead({
|
|
|
278
321
|
|
|
279
322
|
/* Reply form */
|
|
280
323
|
.cpub-reply-form { margin-bottom: 16px; }
|
|
324
|
+
.cpub-replying-to {
|
|
325
|
+
font-size: 11px; color: var(--text-dim); margin-bottom: 6px;
|
|
326
|
+
display: flex; align-items: center; gap: 6px;
|
|
327
|
+
}
|
|
328
|
+
.cpub-cancel-reply { background: none; border: none; cursor: pointer; color: var(--text-faint); font-size: 12px; }
|
|
281
329
|
.cpub-reply-row { display: flex; gap: 8px; }
|
|
282
330
|
.cpub-reply-input {
|
|
283
331
|
flex: 1; padding: 8px 12px; background: var(--surface); border: var(--border-width-default) solid var(--border);
|
|
@@ -293,8 +341,38 @@ useHead({
|
|
|
293
341
|
|
|
294
342
|
/* Replies section */
|
|
295
343
|
.cpub-replies-section {}
|
|
296
|
-
.cpub-
|
|
297
|
-
|
|
344
|
+
.cpub-replies-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
|
|
345
|
+
|
|
346
|
+
.cpub-reply {
|
|
347
|
+
padding: 12px 16px; background: var(--surface); border: var(--border-width-default) solid var(--border);
|
|
348
|
+
margin-bottom: 8px;
|
|
349
|
+
}
|
|
350
|
+
.cpub-reply-nested { margin-left: 24px; border-color: var(--border2); }
|
|
351
|
+
.cpub-nested-replies { margin-top: 8px; }
|
|
352
|
+
|
|
353
|
+
.cpub-reply-author { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-faint); margin-bottom: 6px; }
|
|
354
|
+
|
|
355
|
+
.cpub-reply-avatar {
|
|
356
|
+
width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
|
|
357
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
358
|
+
font-family: var(--font-mono); font-size: 9px; font-weight: 700; color: var(--text-dim);
|
|
359
|
+
overflow: hidden;
|
|
360
|
+
}
|
|
361
|
+
.cpub-reply-avatar-img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
|
|
362
|
+
|
|
363
|
+
.cpub-reply-author-name { font-weight: 500; color: var(--text-dim); text-decoration: none; }
|
|
364
|
+
.cpub-reply-author-name:hover { color: var(--accent); }
|
|
365
|
+
|
|
366
|
+
.cpub-reply-content { font-size: 13px; line-height: 1.6; color: var(--text); }
|
|
367
|
+
|
|
368
|
+
.cpub-reply-actions { margin-top: 6px; }
|
|
369
|
+
.cpub-reply-btn {
|
|
370
|
+
background: none; border: none; cursor: pointer; font-size: 11px;
|
|
371
|
+
color: var(--text-faint); padding: 2px 0;
|
|
372
|
+
}
|
|
373
|
+
.cpub-reply-btn:hover { color: var(--accent); }
|
|
374
|
+
|
|
375
|
+
.cpub-fed-thread-info { text-align: center; margin-top: 8px; }
|
|
298
376
|
.cpub-empty-state-desc { font-size: 12px; color: var(--text-faint); line-height: 1.5; }
|
|
299
377
|
.cpub-inline-link { color: var(--accent); text-decoration: none; white-space: nowrap; }
|
|
300
378
|
.cpub-inline-link:hover { text-decoration: underline; }
|
|
@@ -63,7 +63,7 @@ const postsVM = computed<HubPostViewModel[]>(() => {
|
|
|
63
63
|
author: {
|
|
64
64
|
name: p.author?.displayName || p.author?.username || p.remoteActorName || 'Unknown',
|
|
65
65
|
handle: p.author ? null : remoteDomain(p.remoteActorUri ?? undefined),
|
|
66
|
-
avatarUrl: p.author?.avatarUrl ?? null,
|
|
66
|
+
avatarUrl: p.author?.avatarUrl ?? p.remoteActorAvatarUrl ?? null,
|
|
67
67
|
},
|
|
68
68
|
createdAt: p.createdAt,
|
|
69
69
|
likeCount: p.likeCount ?? 0,
|