@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.6",
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/editor": "0.7.0",
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.5"
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
- * New-format content AP Article endpoint.
7
- * URI: /u/{username}/{type}/{slug}
6
+ * Middleware: serve ActivityPub Article JSON-LD for content URIs.
8
7
  *
9
- * Serves Article JSON-LD when requested with AP Accept header.
10
- * Browsers see the Nuxt page instead (this handler returns nothing for non-AP requests).
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 = getRouterParam(event, '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
- const article = contentToArticle(
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
  });