@commonpub/layer 0.4.13 → 0.5.1

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.4.13",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -50,16 +50,16 @@
50
50
  "vue": "^3.4.0",
51
51
  "vue-router": "^4.3.0",
52
52
  "zod": "^4.3.6",
53
+ "@commonpub/config": "0.8.0",
54
+ "@commonpub/editor": "0.5.0",
55
+ "@commonpub/explainer": "0.6.1",
56
+ "@commonpub/schema": "0.8.15",
57
+ "@commonpub/protocol": "0.9.6",
58
+ "@commonpub/docs": "0.6.0",
53
59
  "@commonpub/auth": "0.5.0",
54
60
  "@commonpub/learning": "0.5.0",
55
- "@commonpub/schema": "0.8.15",
56
- "@commonpub/editor": "0.5.0",
57
- "@commonpub/config": "0.8.0",
58
- "@commonpub/docs": "0.5.2",
59
- "@commonpub/explainer": "0.5.3",
60
61
  "@commonpub/server": "2.23.1",
61
- "@commonpub/ui": "0.8.4",
62
- "@commonpub/protocol": "0.9.5"
62
+ "@commonpub/ui": "0.8.4"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@testing-library/jest-dom": "^6.9.1",
@@ -1,6 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import type { Component } from 'vue';
3
3
  import type { BlockTuple } from '@commonpub/editor';
4
+ import { isExplainerDocument, createEmptyDocument } from '@commonpub/explainer';
5
+ import type { ExplainerDocument } from '@commonpub/explainer';
6
+ import { ExplainerSectionEditor } from '@commonpub/explainer/vue';
4
7
  definePageMeta({ layout: false, middleware: 'auth' });
5
8
 
6
9
  const route = useRoute();
@@ -29,9 +32,23 @@ const { extract: extractError } = useApiError();
29
32
  const mode = ref<'write' | 'preview' | 'code'>('write');
30
33
  const contentId = ref<string | null>(null);
31
34
 
32
- // --- Block editor ---
35
+ // --- Block editor (articles, blogs, projects) ---
33
36
  const blockEditor = useBlockEditor();
34
37
 
38
+ // --- Explainer document (explainers only) ---
39
+ const isExplainer = computed(() => contentType.value === 'explainer');
40
+ // The prop passed to ExplainerSectionEditor — set once on load, not updated on edits
41
+ const explainerDocInit = ref<ExplainerDocument | null>(null);
42
+ // The latest version from the editor — used for saving
43
+ const explainerDocLatest = ref<ExplainerDocument | null>(null);
44
+
45
+ function getContentForSave(): unknown {
46
+ if (isExplainer.value) {
47
+ return explainerDocLatest.value ?? explainerDocInit.value;
48
+ }
49
+ return blockEditor.toBlockTuples();
50
+ }
51
+
35
52
  // --- Content save composable ---
36
53
  const {
37
54
  saving,
@@ -50,7 +67,7 @@ const {
50
67
  isNew,
51
68
  contentId,
52
69
  isDirty,
53
- getBlockTuples: () => blockEditor.toBlockTuples(),
70
+ getBlockTuples: getContentForSave as () => BlockTuple[],
54
71
  extractError,
55
72
  onAfterSave: syncBOM,
56
73
  });
@@ -59,7 +76,7 @@ const {
59
76
  const { errors: publishErrors, showErrors: showPublishErrors, validate, dismiss: dismissPublishErrors } = usePublishValidation({
60
77
  title,
61
78
  metadata,
62
- getBlockTuples: () => blockEditor.toBlockTuples(),
79
+ getBlockTuples: getContentForSave as () => BlockTuple[],
63
80
  });
64
81
 
65
82
  // --- Specialized editor component map ---
@@ -80,7 +97,18 @@ if (!isNew.value) {
80
97
  const d = data.value as Record<string, unknown>;
81
98
  contentId.value = d.id as string;
82
99
  title.value = d.title as string;
83
- if (Array.isArray(d.content)) {
100
+ if (isExplainer.value && isExplainerDocument(d.content)) {
101
+ // Load ExplainerDocument for explainers
102
+ const doc = d.content as unknown as ExplainerDocument;
103
+ // Sync: prefer row-level title if hero title is empty
104
+ if (!doc.hero.title && title.value) {
105
+ doc.hero.title = title.value;
106
+ } else if (doc.hero.title) {
107
+ title.value = doc.hero.title;
108
+ }
109
+ explainerDocInit.value = doc;
110
+ explainerDocLatest.value = JSON.parse(JSON.stringify(doc));
111
+ } else if (Array.isArray(d.content)) {
84
112
  blockEditor.fromBlockTuples(d.content as [string, Record<string, unknown>][]);
85
113
  }
86
114
  metadata.value = {
@@ -115,7 +143,42 @@ watch(title, (newTitle) => {
115
143
 
116
144
  // --- Dirty tracking + autosave ---
117
145
  watch(() => blockEditor.blocks.value, () => { isDirty.value = true; }, { deep: true });
118
- initAutoSave([() => blockEditor.blocks.value, title, metadata]);
146
+ watch(explainerDocLatest, () => { isDirty.value = true; }, { deep: true });
147
+ initAutoSave([() => blockEditor.blocks.value, () => explainerDocLatest.value, title, metadata]);
148
+
149
+ // --- Explainer document events ---
150
+ function handleExplainerUpdate(doc: ExplainerDocument): void {
151
+ // Store latest for saving — DON'T update explainerDocInit (would cause editor re-render loop)
152
+ explainerDocLatest.value = doc;
153
+ // Sync title from hero
154
+ if (doc.hero.title) title.value = doc.hero.title;
155
+ // Sync metadata
156
+ if (doc.meta.description) metadata.value = { ...metadata.value, description: doc.meta.description };
157
+ if (doc.meta.difficulty) metadata.value = { ...metadata.value, difficulty: doc.meta.difficulty };
158
+ if (doc.meta.estimatedMinutes) metadata.value = { ...metadata.value, estimatedMinutes: doc.meta.estimatedMinutes };
159
+ if (doc.meta.tags?.length) metadata.value = { ...metadata.value, tags: doc.meta.tags };
160
+ if (doc.hero.coverImageUrl) metadata.value = { ...metadata.value, coverImageUrl: doc.hero.coverImageUrl };
161
+ isDirty.value = true;
162
+ }
163
+
164
+ function handleExplainerSave(doc: ExplainerDocument): void {
165
+ handleExplainerUpdate(doc);
166
+ silentSave();
167
+ }
168
+
169
+ // --- Init new explainer ---
170
+ if (isNew.value && isExplainer.value) {
171
+ const emptyDoc = createEmptyDocument();
172
+ explainerDocInit.value = emptyDoc;
173
+ explainerDocLatest.value = JSON.parse(JSON.stringify(emptyDoc));
174
+ }
175
+
176
+ // Sync starter form title to explainer doc hero
177
+ watch(title, (newTitle) => {
178
+ if (isExplainer.value && explainerDocInit.value && !explainerDocLatest.value?.hero.title) {
179
+ explainerDocInit.value.hero.title = newTitle;
180
+ }
181
+ });
119
182
 
120
183
  function handleMetadataUpdate(newMetadata: Record<string, unknown>): void {
121
184
  if (newMetadata.title !== undefined && typeof newMetadata.title === 'string') {
@@ -344,8 +407,17 @@ async function handleUrlImport(result: ImportedContent): Promise<void> {
344
407
 
345
408
  <div v-if="error" class="cpub-editor-error" role="alert">{{ error }}</div>
346
409
 
347
- <!-- Write mode with specialized editor -->
348
- <template v-if="mode === 'write' && hasSpecializedEditor">
410
+ <!-- Explainer: section-oriented editor -->
411
+ <template v-if="mode === 'write' && isExplainer && explainerDocInit">
412
+ <ExplainerSectionEditor
413
+ :document="explainerDocInit"
414
+ @update:document="handleExplainerUpdate"
415
+ @save="handleExplainerSave"
416
+ />
417
+ </template>
418
+
419
+ <!-- Write mode with specialized editor (articles, blogs, projects) -->
420
+ <template v-else-if="mode === 'write' && hasSpecializedEditor && !isExplainer">
349
421
  <component
350
422
  :is="editorComponent"
351
423
  :block-editor="blockEditor"