@commonpub/layer 0.7.6 → 0.7.8
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/layer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
"vue-router": "^4.3.0",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
53
|
"@commonpub/auth": "0.5.0",
|
|
54
|
-
"@commonpub/config": "0.9.0",
|
|
55
54
|
"@commonpub/docs": "0.6.2",
|
|
56
|
-
"@commonpub/
|
|
55
|
+
"@commonpub/config": "0.9.0",
|
|
56
|
+
"@commonpub/editor": "0.7.1",
|
|
57
57
|
"@commonpub/explainer": "0.7.4",
|
|
58
58
|
"@commonpub/learning": "0.5.0",
|
|
59
59
|
"@commonpub/schema": "0.9.4",
|
|
60
60
|
"@commonpub/protocol": "0.9.7",
|
|
61
61
|
"@commonpub/ui": "0.8.5",
|
|
62
|
-
"@commonpub/server": "2.27.
|
|
62
|
+
"@commonpub/server": "2.27.6"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { provide } from 'vue';
|
|
2
3
|
import type { BlockTuple } from '@commonpub/editor';
|
|
3
|
-
import { BlockCanvas, EditorShell, useBlockEditor, type BlockTypeGroup } from '@commonpub/editor/vue';
|
|
4
|
+
import { BlockCanvas, EditorShell, useBlockEditor, UPLOAD_HANDLER_KEY, type BlockTypeGroup } from '@commonpub/editor/vue';
|
|
4
5
|
import type { PageTreeItem } from '../../../components/editors/DocsPageTree.vue';
|
|
5
6
|
|
|
6
7
|
definePageMeta({ layout: false, middleware: 'auth' });
|
|
@@ -9,6 +10,15 @@ const route = useRoute();
|
|
|
9
10
|
const siteSlug = computed(() => route.params.siteSlug as string);
|
|
10
11
|
const { show: toast } = useToast();
|
|
11
12
|
|
|
13
|
+
// Provide upload handler to block components (ImageBlock, GalleryBlock)
|
|
14
|
+
provide(UPLOAD_HANDLER_KEY, async (file: File) => {
|
|
15
|
+
const formData = new FormData();
|
|
16
|
+
formData.append('file', file);
|
|
17
|
+
formData.append('purpose', 'content');
|
|
18
|
+
const res = await $fetch<{ url: string; width?: number | null; height?: number | null }>('/api/files/upload', { method: 'POST', body: formData });
|
|
19
|
+
return { url: res.url, width: res.width ?? null, height: res.height ?? null };
|
|
20
|
+
});
|
|
21
|
+
|
|
12
22
|
// ═══ DATA FETCHING ═══
|
|
13
23
|
const { data: site, refresh: refreshSite } = await useFetch<{ id: string; name: string; slug: string; description: string; ownerId: string; versions?: Array<{ id: string; version: string; isDefault: boolean }> }>(() => `/api/docs/${siteSlug.value}`);
|
|
14
24
|
|
|
@@ -466,6 +476,22 @@ async function createVersion(): Promise<void> {
|
|
|
466
476
|
</NuxtLink>
|
|
467
477
|
<span class="cpub-docs-topbar-title">{{ site?.name ?? 'Docs' }}</span>
|
|
468
478
|
<div class="cpub-docs-topbar-spacer" />
|
|
479
|
+
<button
|
|
480
|
+
class="cpub-docs-toolbar-btn"
|
|
481
|
+
title="Undo (Ctrl+Z)"
|
|
482
|
+
:disabled="!blockEditor.canUndo.value"
|
|
483
|
+
@click="blockEditor.undo()"
|
|
484
|
+
>
|
|
485
|
+
<i class="fa-solid fa-rotate-left" />
|
|
486
|
+
</button>
|
|
487
|
+
<button
|
|
488
|
+
class="cpub-docs-toolbar-btn"
|
|
489
|
+
title="Redo (Ctrl+Shift+Z)"
|
|
490
|
+
:disabled="!blockEditor.canRedo.value"
|
|
491
|
+
@click="blockEditor.redo()"
|
|
492
|
+
>
|
|
493
|
+
<i class="fa-solid fa-rotate-right" />
|
|
494
|
+
</button>
|
|
469
495
|
<button
|
|
470
496
|
v-if="selectedPageId"
|
|
471
497
|
class="cpub-docs-toolbar-btn"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Component } from 'vue';
|
|
3
|
+
import { provide } from 'vue';
|
|
3
4
|
import type { BlockTuple } from '@commonpub/editor';
|
|
4
|
-
import { BlockCanvas, useBlockEditor } from '@commonpub/editor/vue';
|
|
5
|
+
import { BlockCanvas, useBlockEditor, UPLOAD_HANDLER_KEY, SEARCH_PRODUCTS_KEY } from '@commonpub/editor/vue';
|
|
5
6
|
import { isExplainerDocument, createEmptyDocument } from '@commonpub/explainer';
|
|
6
7
|
import type { ExplainerDocument } from '@commonpub/explainer';
|
|
7
8
|
import { ExplainerSectionEditor } from '@commonpub/explainer/vue';
|
|
@@ -93,6 +94,20 @@ const { errors: publishErrors, showErrors: showPublishErrors, validate, dismiss:
|
|
|
93
94
|
getBlockTuples: getContentForSave as () => BlockTuple[],
|
|
94
95
|
});
|
|
95
96
|
|
|
97
|
+
// --- Provide upload + search handlers to block components via inject ---
|
|
98
|
+
provide(UPLOAD_HANDLER_KEY, async (file: File) => {
|
|
99
|
+
const formData = new FormData();
|
|
100
|
+
formData.append('file', file);
|
|
101
|
+
formData.append('purpose', 'content');
|
|
102
|
+
const res = await $fetch<{ url: string; width?: number | null; height?: number | null }>('/api/files/upload', { method: 'POST', body: formData });
|
|
103
|
+
return { url: res.url, width: res.width ?? null, height: res.height ?? null };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
provide(SEARCH_PRODUCTS_KEY, async (query: string) => {
|
|
107
|
+
const res = await $fetch<{ items: Array<{ id: string; name: string; slug: string; description: string | null; category: string | null; imageUrl: string | null; purchaseUrl: string | null }> }>(`/api/products?q=${encodeURIComponent(query)}&limit=10`);
|
|
108
|
+
return res.items ?? [];
|
|
109
|
+
});
|
|
110
|
+
|
|
96
111
|
// --- Specialized editor component map ---
|
|
97
112
|
const editorMap: Record<string, Component> = {
|
|
98
113
|
article: resolveComponent('EditorsArticleEditor') as Component,
|
|
@@ -389,6 +404,24 @@ async function handleUrlImport(result: ImportedContent): Promise<void> {
|
|
|
389
404
|
<i class="fa-solid fa-exclamation-triangle"></i> Save failed
|
|
390
405
|
</span>
|
|
391
406
|
</div>
|
|
407
|
+
<div v-if="!isExplainer" class="cpub-topbar-undo-redo">
|
|
408
|
+
<button
|
|
409
|
+
class="cpub-topbar-icon-btn"
|
|
410
|
+
title="Undo (Ctrl+Z)"
|
|
411
|
+
:disabled="!blockEditor.canUndo.value"
|
|
412
|
+
@click="blockEditor.undo()"
|
|
413
|
+
>
|
|
414
|
+
<i class="fa-solid fa-rotate-left"></i>
|
|
415
|
+
</button>
|
|
416
|
+
<button
|
|
417
|
+
class="cpub-topbar-icon-btn"
|
|
418
|
+
title="Redo (Ctrl+Shift+Z)"
|
|
419
|
+
:disabled="!blockEditor.canRedo.value"
|
|
420
|
+
@click="blockEditor.redo()"
|
|
421
|
+
>
|
|
422
|
+
<i class="fa-solid fa-rotate-right"></i>
|
|
423
|
+
</button>
|
|
424
|
+
</div>
|
|
392
425
|
<div class="cpub-mode-tabs">
|
|
393
426
|
<button :class="['cpub-mode-tab', { active: mode === 'write' }]" @click="mode = 'write'">Write</button>
|
|
394
427
|
<button :class="['cpub-mode-tab', { active: mode === 'preview' }]" @click="enterPreview">Preview</button>
|
|
@@ -604,6 +637,37 @@ async function handleUrlImport(result: ImportedContent): Promise<void> {
|
|
|
604
637
|
.cpub-autosave-status--saved { color: var(--green); }
|
|
605
638
|
.cpub-autosave-status--error { color: var(--red); }
|
|
606
639
|
|
|
640
|
+
.cpub-topbar-undo-redo {
|
|
641
|
+
display: flex;
|
|
642
|
+
align-items: center;
|
|
643
|
+
gap: 2px;
|
|
644
|
+
margin-right: 4px;
|
|
645
|
+
flex-shrink: 0;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.cpub-topbar-icon-btn {
|
|
649
|
+
width: 30px;
|
|
650
|
+
height: 30px;
|
|
651
|
+
display: flex;
|
|
652
|
+
align-items: center;
|
|
653
|
+
justify-content: center;
|
|
654
|
+
background: none;
|
|
655
|
+
border: var(--border-width-default) solid transparent;
|
|
656
|
+
color: var(--text-dim);
|
|
657
|
+
cursor: pointer;
|
|
658
|
+
font-size: 12px;
|
|
659
|
+
padding: 0;
|
|
660
|
+
}
|
|
661
|
+
.cpub-topbar-icon-btn:hover:not(:disabled) {
|
|
662
|
+
background: var(--surface2);
|
|
663
|
+
border-color: var(--border2);
|
|
664
|
+
color: var(--text);
|
|
665
|
+
}
|
|
666
|
+
.cpub-topbar-icon-btn:disabled {
|
|
667
|
+
opacity: 0.3;
|
|
668
|
+
cursor: not-allowed;
|
|
669
|
+
}
|
|
670
|
+
|
|
607
671
|
.cpub-mode-tabs {
|
|
608
672
|
display: flex;
|
|
609
673
|
background: var(--surface2);
|
|
@@ -721,6 +785,7 @@ async function handleUrlImport(result: ImportedContent): Promise<void> {
|
|
|
721
785
|
.cpub-editor-back { margin-left: 0; }
|
|
722
786
|
.cpub-topbar-title-input { max-width: none; font-size: 12px; padding: 3px 6px; }
|
|
723
787
|
.cpub-autosave-status { display: none; }
|
|
788
|
+
.cpub-topbar-undo-redo { display: none; }
|
|
724
789
|
.cpub-mode-tabs { margin: 0 6px; padding: 1px; }
|
|
725
790
|
.cpub-mode-tab { padding: 4px 10px; font-size: 10px; }
|
|
726
791
|
.cpub-topbar-spacer { display: none; }
|
|
@@ -3,11 +3,13 @@ import { contentItems, users } from '@commonpub/schema';
|
|
|
3
3
|
import { eq, and, isNull } from 'drizzle-orm';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* URI: /u/{username}/{type}/{slug}
|
|
6
|
+
* Middleware: serve ActivityPub Article JSON-LD for content URIs.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Matches /u/{username}/{type}/{slug} with AP Accept headers.
|
|
9
|
+
* Non-AP requests pass through to the Nuxt page renderer.
|
|
10
|
+
*
|
|
11
|
+
* This MUST be a middleware (not a server route) because a server route
|
|
12
|
+
* returning undefined sends HTTP 204, which prevents the Nuxt page from rendering.
|
|
11
13
|
*/
|
|
12
14
|
export default defineEventHandler(async (event) => {
|
|
13
15
|
const accept = getRequestHeader(event, 'accept') ?? '';
|
|
@@ -17,12 +19,14 @@ export default defineEventHandler(async (event) => {
|
|
|
17
19
|
|
|
18
20
|
if (!isAPRequest) return;
|
|
19
21
|
|
|
22
|
+
const path = getRequestURL(event).pathname;
|
|
23
|
+
const match = path.match(/^\/u\/([a-zA-Z0-9_-]+)\/([a-z]+)\/([a-z0-9][a-z0-9_-]*)$/);
|
|
24
|
+
if (!match) return;
|
|
25
|
+
|
|
20
26
|
const config = useConfig();
|
|
21
27
|
if (!config.features.federation) return;
|
|
22
28
|
|
|
23
|
-
const username =
|
|
24
|
-
const type = getRouterParam(event, 'type');
|
|
25
|
-
const slug = getRouterParam(event, 'slug');
|
|
29
|
+
const [, username, type, slug] = match;
|
|
26
30
|
if (!username || !type || !slug) return;
|
|
27
31
|
|
|
28
32
|
const db = useDB();
|
|
@@ -51,7 +55,7 @@ export default defineEventHandler(async (event) => {
|
|
|
51
55
|
|
|
52
56
|
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
return contentToArticle(
|
|
55
59
|
{
|
|
56
60
|
id: row.content.id,
|
|
57
61
|
type: row.content.type,
|
|
@@ -68,6 +72,4 @@ export default defineEventHandler(async (event) => {
|
|
|
68
72
|
{ username: row.author.username, displayName: row.author.displayName ?? row.author.username },
|
|
69
73
|
domain,
|
|
70
74
|
);
|
|
71
|
-
|
|
72
|
-
return article;
|
|
73
75
|
});
|