@commonpub/layer 0.7.18 → 0.7.20
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.
|
@@ -128,6 +128,26 @@ function removeCover(): void {
|
|
|
128
128
|
updateMeta('coverImageUrl', '');
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// --- Banner image ---
|
|
132
|
+
const bannerUrl = computed(() => (props.metadata.bannerUrl as string) || '');
|
|
133
|
+
|
|
134
|
+
function onBannerUpload(event: Event): void {
|
|
135
|
+
const input = event.target as HTMLInputElement;
|
|
136
|
+
if (!input.files?.length) return;
|
|
137
|
+
const file = input.files[0];
|
|
138
|
+
if (!file) return;
|
|
139
|
+
const formData = new FormData();
|
|
140
|
+
formData.append('file', file);
|
|
141
|
+
formData.append('purpose', 'cover');
|
|
142
|
+
$fetch<{ url: string }>('/api/files/upload', { method: 'POST', body: formData })
|
|
143
|
+
.then((res) => { updateMeta('bannerUrl', res.url); })
|
|
144
|
+
.catch(() => {});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function removeBanner(): void {
|
|
148
|
+
updateMeta('bannerUrl', '');
|
|
149
|
+
}
|
|
150
|
+
|
|
131
151
|
// --- SEO preview ---
|
|
132
152
|
const seoDomain = computed(() => {
|
|
133
153
|
try { return new URL(useRequestURL().origin).hostname; } catch { return 'example.com'; }
|
|
@@ -136,7 +156,7 @@ const seoPreviewDesc = computed(() => (props.metadata.seoDescription as string)
|
|
|
136
156
|
|
|
137
157
|
// --- Right panel ---
|
|
138
158
|
const openSections = ref<Record<string, boolean>>({
|
|
139
|
-
content: true, seo: false, publishing: true, cover: false,
|
|
159
|
+
content: true, seo: false, publishing: true, cover: false, banner: false,
|
|
140
160
|
});
|
|
141
161
|
function toggleSection(key: string): void {
|
|
142
162
|
openSections.value[key] = !openSections.value[key];
|
|
@@ -347,6 +367,27 @@ const canvasMaxWidth = computed(() => {
|
|
|
347
367
|
</div>
|
|
348
368
|
</EditorSection>
|
|
349
369
|
|
|
370
|
+
<!-- Banner Image -->
|
|
371
|
+
<EditorSection title="Banner Image" icon="fa-panorama" :open="openSections.banner" @toggle="toggleSection('banner')">
|
|
372
|
+
<p class="cpub-ep-hint" style="margin-bottom: 8px;">Hero background at the top of the page. Falls back to your profile banner if not set.</p>
|
|
373
|
+
<div v-if="bannerUrl" class="cpub-ae-banner-preview">
|
|
374
|
+
<img :src="bannerUrl" alt="Banner" class="cpub-ae-banner-img" />
|
|
375
|
+
<div class="cpub-ae-banner-actions">
|
|
376
|
+
<button class="cpub-ae-cover-btn" @click="removeBanner"><i class="fa-solid fa-trash"></i> Remove</button>
|
|
377
|
+
<label class="cpub-ae-cover-btn">
|
|
378
|
+
<i class="fa-solid fa-arrow-up-from-bracket"></i> Replace
|
|
379
|
+
<input type="file" accept="image/*" class="cpub-sr-only" @change="onBannerUpload">
|
|
380
|
+
</label>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
<div v-else>
|
|
384
|
+
<label class="cpub-ae-cover-btn primary" style="display: inline-flex;">
|
|
385
|
+
<i class="fa-solid fa-arrow-up-from-bracket"></i> Upload Banner
|
|
386
|
+
<input type="file" accept="image/*" class="cpub-sr-only" @change="onBannerUpload">
|
|
387
|
+
</label>
|
|
388
|
+
</div>
|
|
389
|
+
</EditorSection>
|
|
390
|
+
|
|
350
391
|
<!-- SEO -->
|
|
351
392
|
<EditorSection title="SEO Preview" icon="fa-brands fa-google" :open="openSections.seo" @toggle="toggleSection('seo')">
|
|
352
393
|
<div class="cpub-seo-card">
|
|
@@ -583,4 +624,9 @@ const canvasMaxWidth = computed(() => {
|
|
|
583
624
|
}
|
|
584
625
|
.cpub-seo-title { font-size: 14px; color: var(--accent); font-weight: 500; margin-bottom: 2px; }
|
|
585
626
|
.cpub-seo-desc { font-size: 11px; color: var(--text-dim); line-height: 1.5; }
|
|
627
|
+
|
|
628
|
+
/* Banner preview */
|
|
629
|
+
.cpub-ae-banner-preview { position: relative; margin-bottom: 8px; }
|
|
630
|
+
.cpub-ae-banner-img { width: 100%; height: 80px; object-fit: cover; display: block; border: var(--border-width-default) solid var(--border); }
|
|
631
|
+
.cpub-ae-banner-actions { display: flex; gap: 6px; margin-top: 6px; }
|
|
586
632
|
</style>
|
|
@@ -45,7 +45,7 @@ const blockTypes: BlockTypeGroup[] = [
|
|
|
45
45
|
];
|
|
46
46
|
|
|
47
47
|
const openSections = ref<Record<string, boolean>>({
|
|
48
|
-
meta: true, excerpt: true, seo: true, publishing: true, author: true, social: false,
|
|
48
|
+
meta: true, excerpt: true, banner: false, seo: true, publishing: true, author: true, social: false,
|
|
49
49
|
});
|
|
50
50
|
function toggleSection(key: string): void {
|
|
51
51
|
openSections.value[key] = !openSections.value[key];
|
|
@@ -80,6 +80,26 @@ function removeCover(): void {
|
|
|
80
80
|
updateMeta('coverImageUrl', '');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// --- Banner image ---
|
|
84
|
+
const bannerUrl = computed(() => (props.metadata.bannerUrl as string) || '');
|
|
85
|
+
|
|
86
|
+
function onBannerUpload(event: Event): void {
|
|
87
|
+
const input = event.target as HTMLInputElement;
|
|
88
|
+
if (!input.files?.length) return;
|
|
89
|
+
const file = input.files[0];
|
|
90
|
+
if (!file) return;
|
|
91
|
+
const formData = new FormData();
|
|
92
|
+
formData.append('file', file);
|
|
93
|
+
formData.append('purpose', 'cover');
|
|
94
|
+
$fetch<{ url: string }>('/api/files/upload', { method: 'POST', body: formData })
|
|
95
|
+
.then((res) => { updateMeta('bannerUrl', res.url); })
|
|
96
|
+
.catch(() => {});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function removeBanner(): void {
|
|
100
|
+
updateMeta('bannerUrl', '');
|
|
101
|
+
}
|
|
102
|
+
|
|
83
103
|
// --- Word count ---
|
|
84
104
|
const wordCount = computed(() => {
|
|
85
105
|
let count = 0;
|
|
@@ -289,6 +309,27 @@ const canvasMaxWidth = computed(() => {
|
|
|
289
309
|
</div>
|
|
290
310
|
</EditorSection>
|
|
291
311
|
|
|
312
|
+
<!-- Banner Image -->
|
|
313
|
+
<EditorSection title="Banner Image" icon="fa-panorama" :open="openSections.banner" @toggle="toggleSection('banner')">
|
|
314
|
+
<p class="cpub-ep-hint" style="margin-bottom: 8px;">Hero background at the top of the page. Falls back to your profile banner if not set.</p>
|
|
315
|
+
<div v-if="bannerUrl" class="cpub-be-banner-preview">
|
|
316
|
+
<img :src="bannerUrl" alt="Banner" class="cpub-be-banner-img" />
|
|
317
|
+
<div class="cpub-be-banner-actions">
|
|
318
|
+
<button class="cpub-be-cover-btn" @click="removeBanner"><i class="fa-solid fa-trash"></i> Remove</button>
|
|
319
|
+
<label class="cpub-be-cover-btn">
|
|
320
|
+
<i class="fa-solid fa-arrow-up-from-bracket"></i> Replace
|
|
321
|
+
<input type="file" accept="image/*" class="cpub-sr-only" @change="onBannerUpload">
|
|
322
|
+
</label>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
<div v-else>
|
|
326
|
+
<label class="cpub-be-cover-btn primary" style="display: inline-flex;">
|
|
327
|
+
<i class="fa-solid fa-arrow-up-from-bracket"></i> Upload Banner
|
|
328
|
+
<input type="file" accept="image/*" class="cpub-sr-only" @change="onBannerUpload">
|
|
329
|
+
</label>
|
|
330
|
+
</div>
|
|
331
|
+
</EditorSection>
|
|
332
|
+
|
|
292
333
|
<!-- SEO Preview -->
|
|
293
334
|
<EditorSection title="SEO Preview" icon="fa-brands fa-google" :open="openSections.seo" @toggle="toggleSection('seo')">
|
|
294
335
|
<div class="cpub-be-seo-card">
|
|
@@ -580,4 +621,9 @@ const canvasMaxWidth = computed(() => {
|
|
|
580
621
|
.cpub-be-cover-actions { opacity: 1; }
|
|
581
622
|
.cpub-be-og-overlay { opacity: 1; }
|
|
582
623
|
}
|
|
624
|
+
|
|
625
|
+
/* Banner preview */
|
|
626
|
+
.cpub-be-banner-preview { position: relative; margin-bottom: 8px; }
|
|
627
|
+
.cpub-be-banner-img { width: 100%; height: 80px; object-fit: cover; display: block; border: var(--border-width-default) solid var(--border); }
|
|
628
|
+
.cpub-be-banner-actions { display: flex; gap: 6px; margin-top: 6px; }
|
|
583
629
|
</style>
|
|
@@ -52,7 +52,7 @@ const seoDomain = computed(() => {
|
|
|
52
52
|
const seoPreviewDesc = computed(() => (props.metadata.seoDescription as string) || (props.metadata.description as string) || '');
|
|
53
53
|
|
|
54
54
|
const openSections = ref<Record<string, boolean>>({
|
|
55
|
-
meta: true, tags: true, visibility: true, cover: false, seo: false, checklist: true,
|
|
55
|
+
meta: true, tags: true, visibility: true, cover: false, banner: false, seo: false, checklist: true,
|
|
56
56
|
});
|
|
57
57
|
function toggleSection(key: string): void {
|
|
58
58
|
openSections.value[key] = !openSections.value[key];
|
|
@@ -85,6 +85,26 @@ function removeCover(): void {
|
|
|
85
85
|
updateMeta('coverImageUrl', '');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// --- Banner image ---
|
|
89
|
+
const bannerUrl = computed(() => (props.metadata.bannerUrl as string) || '');
|
|
90
|
+
|
|
91
|
+
function onBannerUpload(event: Event): void {
|
|
92
|
+
const input = event.target as HTMLInputElement;
|
|
93
|
+
if (!input.files?.length) return;
|
|
94
|
+
const file = input.files[0];
|
|
95
|
+
if (!file) return;
|
|
96
|
+
const formData = new FormData();
|
|
97
|
+
formData.append('file', file);
|
|
98
|
+
formData.append('purpose', 'cover');
|
|
99
|
+
$fetch<{ url: string }>('/api/files/upload', { method: 'POST', body: formData })
|
|
100
|
+
.then((res) => { updateMeta('bannerUrl', res.url); })
|
|
101
|
+
.catch(() => {});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function removeBanner(): void {
|
|
105
|
+
updateMeta('bannerUrl', '');
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
const tags = computed(() => (props.metadata.tags as string[]) || []);
|
|
89
109
|
function onTagsUpdate(newTags: string[]): void { updateMeta('tags', newTags); }
|
|
90
110
|
const visibility = computed(() => (props.metadata.visibility as string) || 'public');
|
|
@@ -297,6 +317,27 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
297
317
|
</div>
|
|
298
318
|
</EditorSection>
|
|
299
319
|
|
|
320
|
+
<!-- Banner Image -->
|
|
321
|
+
<EditorSection title="Banner Image" icon="fa-panorama" :open="openSections.banner" @toggle="toggleSection('banner')">
|
|
322
|
+
<p class="cpub-pe-hint" style="margin-bottom: 8px;">Hero background at the top of the page. Falls back to your profile banner if not set.</p>
|
|
323
|
+
<div v-if="bannerUrl" class="cpub-pe-banner-preview">
|
|
324
|
+
<img :src="bannerUrl" alt="Banner" class="cpub-pe-banner-img" />
|
|
325
|
+
<div class="cpub-pe-banner-actions">
|
|
326
|
+
<button class="cpub-pe-cover-btn" @click="removeBanner"><i class="fa-solid fa-trash"></i> Remove</button>
|
|
327
|
+
<label class="cpub-pe-cover-btn">
|
|
328
|
+
<i class="fa-solid fa-arrow-up-from-bracket"></i> Replace
|
|
329
|
+
<input type="file" accept="image/*" class="cpub-sr-only" @change="onBannerUpload">
|
|
330
|
+
</label>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
<div v-else>
|
|
334
|
+
<label class="cpub-pe-cover-btn primary" style="display: inline-flex;">
|
|
335
|
+
<i class="fa-solid fa-arrow-up-from-bracket"></i> Upload Banner
|
|
336
|
+
<input type="file" accept="image/*" class="cpub-sr-only" @change="onBannerUpload">
|
|
337
|
+
</label>
|
|
338
|
+
</div>
|
|
339
|
+
</EditorSection>
|
|
340
|
+
|
|
300
341
|
<EditorSection title="SEO Preview" icon="fa-brands fa-google" :open="openSections.seo" @toggle="toggleSection('seo')">
|
|
301
342
|
<div class="cpub-seo-card">
|
|
302
343
|
<div class="cpub-seo-url">
|
|
@@ -497,4 +538,9 @@ const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
|
497
538
|
}
|
|
498
539
|
.cpub-seo-title { font-size: 14px; color: var(--accent); font-weight: 500; margin-bottom: 2px; }
|
|
499
540
|
.cpub-seo-desc { font-size: 11px; color: var(--text-dim); line-height: 1.5; }
|
|
541
|
+
|
|
542
|
+
/* Banner preview */
|
|
543
|
+
.cpub-pe-banner-preview { position: relative; margin-bottom: 8px; }
|
|
544
|
+
.cpub-pe-banner-img { width: 100%; height: 80px; object-fit: cover; display: block; border: var(--border-width-default) solid var(--border); }
|
|
545
|
+
.cpub-pe-banner-actions { display: flex; gap: 6px; margin-top: 6px; }
|
|
500
546
|
</style>
|
|
@@ -603,10 +603,18 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeydown); });
|
|
|
603
603
|
.cpub-content-wrap {
|
|
604
604
|
max-width: 720px;
|
|
605
605
|
margin: 0 auto;
|
|
606
|
-
padding: 44px 36px 80px;
|
|
606
|
+
padding: 44px clamp(12px, 4vw, 36px) 80px;
|
|
607
607
|
min-height: calc(100vh - 51px - 80px);
|
|
608
|
+
overflow-wrap: break-word;
|
|
609
|
+
word-break: break-word;
|
|
608
610
|
}
|
|
609
611
|
|
|
612
|
+
/* Prevent content overflow */
|
|
613
|
+
.cpub-content-wrap :deep(img),
|
|
614
|
+
.cpub-content-wrap :deep(video),
|
|
615
|
+
.cpub-content-wrap :deep(iframe) { max-width: 100%; height: auto; }
|
|
616
|
+
.cpub-content-wrap :deep(pre) { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
617
|
+
|
|
610
618
|
/* ── COVER IMAGE ── */
|
|
611
619
|
.cpub-explainer-cover {
|
|
612
620
|
width: 100%;
|
|
@@ -773,7 +781,7 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeydown); });
|
|
|
773
781
|
@media (max-width: 768px) {
|
|
774
782
|
.cpub-explainer-sidebar { display: none; }
|
|
775
783
|
.cpub-mobile-author { display: flex; }
|
|
776
|
-
.cpub-content-wrap { padding: 24px
|
|
784
|
+
.cpub-content-wrap { padding-top: 24px; padding-bottom: 48px; }
|
|
777
785
|
.cpub-section-nav { flex-direction: column; gap: 16px; }
|
|
778
786
|
.cpub-section-title { font-size: 22px; }
|
|
779
787
|
.cpub-explainer-topbar { gap: 6px; padding: 0 10px; }
|
|
@@ -786,7 +794,7 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeydown); });
|
|
|
786
794
|
}
|
|
787
795
|
|
|
788
796
|
@media (max-width: 480px) {
|
|
789
|
-
.cpub-content-wrap { padding: 16px
|
|
797
|
+
.cpub-content-wrap { padding-top: 16px; padding-bottom: 40px; }
|
|
790
798
|
.cpub-section-title { font-size: 18px; }
|
|
791
799
|
.cpub-section-body { font-size: 14px; }
|
|
792
800
|
.cpub-section-num-badge { width: 28px; height: 28px; font-size: 11px; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.20",
|
|
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/editor": "0.7.5",
|
|
54
|
-
"@commonpub/config": "0.9.0",
|
|
55
54
|
"@commonpub/auth": "0.5.0",
|
|
56
55
|
"@commonpub/docs": "0.6.2",
|
|
57
|
-
"@commonpub/
|
|
58
|
-
"@commonpub/schema": "0.9.5",
|
|
59
|
-
"@commonpub/protocol": "0.9.7",
|
|
56
|
+
"@commonpub/config": "0.9.0",
|
|
60
57
|
"@commonpub/learning": "0.5.0",
|
|
58
|
+
"@commonpub/server": "2.27.7",
|
|
59
|
+
"@commonpub/explainer": "0.7.6",
|
|
61
60
|
"@commonpub/ui": "0.8.5",
|
|
62
|
-
"@commonpub/
|
|
61
|
+
"@commonpub/protocol": "0.9.7",
|
|
62
|
+
"@commonpub/schema": "0.9.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -41,6 +41,7 @@ const metadata = ref<Record<string, unknown>>({
|
|
|
41
41
|
tags: [],
|
|
42
42
|
visibility: 'public',
|
|
43
43
|
coverImageUrl: '',
|
|
44
|
+
bannerUrl: '',
|
|
44
45
|
...(hubFromQuery ? { hubSlug: hubFromQuery } : {}),
|
|
45
46
|
});
|
|
46
47
|
const isDirty = ref(false);
|
|
@@ -147,6 +148,7 @@ if (!isNew.value) {
|
|
|
147
148
|
tags: d.tags ? (d.tags as { name: string }[]).map((t) => t.name) : [],
|
|
148
149
|
visibility: (d.visibility as string) || 'public',
|
|
149
150
|
coverImageUrl: (d.coverImageUrl as string) || '',
|
|
151
|
+
bannerUrl: (d.bannerUrl as string) || '',
|
|
150
152
|
seoDescription: (d.seoDescription as string) || '',
|
|
151
153
|
difficulty: (d.difficulty as string) || '',
|
|
152
154
|
buildTime: (d.buildTime as string) || '',
|