@commonpub/layer 0.7.17 → 0.7.19
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 +47 -1
- package/components/editors/BlogEditor.vue +47 -1
- package/components/editors/ProjectEditor.vue +47 -1
- package/components/views/ArticleView.vue +14 -3
- package/components/views/BlogView.vue +15 -3
- package/components/views/ProjectView.vue +39 -17
- package/package.json +5 -5
- package/pages/u/[username]/[type]/[slug]/edit.vue +2 -0
|
@@ -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>
|
|
@@ -317,10 +317,14 @@ useJsonLd({
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
/* ── ARTICLE WRAP ── */
|
|
320
|
+
.cpub-article-view {
|
|
321
|
+
overflow-x: clip;
|
|
322
|
+
}
|
|
323
|
+
|
|
320
324
|
.cpub-article-wrap {
|
|
321
325
|
max-width: 720px;
|
|
322
326
|
margin: 0 auto;
|
|
323
|
-
padding: 40px 24px 80px;
|
|
327
|
+
padding: 40px clamp(12px, 4vw, 24px) 80px;
|
|
324
328
|
}
|
|
325
329
|
|
|
326
330
|
/* ── TYPE BADGE ── */
|
|
@@ -771,6 +775,13 @@ useJsonLd({
|
|
|
771
775
|
gap: 6px;
|
|
772
776
|
}
|
|
773
777
|
|
|
778
|
+
/* Global overflow protection for content */
|
|
779
|
+
.cpub-article-wrap :deep(img),
|
|
780
|
+
.cpub-article-wrap :deep(video),
|
|
781
|
+
.cpub-article-wrap :deep(iframe) { max-width: 100%; height: auto; }
|
|
782
|
+
.cpub-article-wrap :deep(pre) { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
783
|
+
.cpub-article-wrap { overflow-wrap: break-word; word-break: break-word; }
|
|
784
|
+
|
|
774
785
|
/* ── COVER PHOTO (in-body) ── */
|
|
775
786
|
.cpub-cover-photo {
|
|
776
787
|
margin-bottom: 24px;
|
|
@@ -784,7 +795,7 @@ useJsonLd({
|
|
|
784
795
|
|
|
785
796
|
/* ── RESPONSIVE ── */
|
|
786
797
|
@media (max-width: 768px) {
|
|
787
|
-
.cpub-article-wrap { padding: 24px
|
|
798
|
+
.cpub-article-wrap { padding-top: 24px; padding-bottom: 48px; }
|
|
788
799
|
.cpub-article-title { font-size: 22px; }
|
|
789
800
|
.cpub-article-lead { font-size: 14px; margin-bottom: 16px; }
|
|
790
801
|
.cpub-related-grid { grid-template-columns: 1fr 1fr; }
|
|
@@ -795,7 +806,7 @@ useJsonLd({
|
|
|
795
806
|
}
|
|
796
807
|
|
|
797
808
|
@media (max-width: 480px) {
|
|
798
|
-
.cpub-article-wrap { padding: 16px
|
|
809
|
+
.cpub-article-wrap { padding-top: 16px; padding-bottom: 40px; }
|
|
799
810
|
.cpub-article-title { font-size: 20px; }
|
|
800
811
|
.cpub-article-lead { font-size: 13px; }
|
|
801
812
|
.cpub-related-grid { grid-template-columns: 1fr; }
|
|
@@ -216,12 +216,24 @@ const hasSeries = computed(() => !!seriesTitle.value && seriesTotalParts.value >
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/* ── BLOG WRAP ── */
|
|
219
|
+
.cpub-blog-view {
|
|
220
|
+
overflow-x: clip;
|
|
221
|
+
}
|
|
222
|
+
|
|
219
223
|
.cpub-blog-wrap {
|
|
220
224
|
max-width: 720px;
|
|
221
225
|
margin: 0 auto;
|
|
222
|
-
padding: 40px 24px 80px;
|
|
226
|
+
padding: 40px clamp(12px, 4vw, 24px) 80px;
|
|
227
|
+
overflow-wrap: break-word;
|
|
228
|
+
word-break: break-word;
|
|
223
229
|
}
|
|
224
230
|
|
|
231
|
+
/* Global overflow protection for content */
|
|
232
|
+
.cpub-blog-wrap :deep(img),
|
|
233
|
+
.cpub-blog-wrap :deep(video),
|
|
234
|
+
.cpub-blog-wrap :deep(iframe) { max-width: 100%; height: auto; }
|
|
235
|
+
.cpub-blog-wrap :deep(pre) { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
236
|
+
|
|
225
237
|
/* ── TYPE BADGE ── */
|
|
226
238
|
.cpub-content-type-badge {
|
|
227
239
|
display: inline-flex;
|
|
@@ -702,7 +714,7 @@ const hasSeries = computed(() => !!seriesTitle.value && seriesTotalParts.value >
|
|
|
702
714
|
|
|
703
715
|
/* ── RESPONSIVE ── */
|
|
704
716
|
@media (max-width: 768px) {
|
|
705
|
-
.cpub-blog-wrap { padding: 24px
|
|
717
|
+
.cpub-blog-wrap { padding-top: 24px; padding-bottom: 48px; }
|
|
706
718
|
.cpub-blog-title { font-size: 22px; }
|
|
707
719
|
.cpub-engagement-row { flex-wrap: wrap; gap: 6px; }
|
|
708
720
|
.cpub-engage-btn { padding: 8px 12px; min-height: 36px; }
|
|
@@ -713,7 +725,7 @@ const hasSeries = computed(() => !!seriesTitle.value && seriesTotalParts.value >
|
|
|
713
725
|
}
|
|
714
726
|
|
|
715
727
|
@media (max-width: 480px) {
|
|
716
|
-
.cpub-blog-wrap { padding: 16px
|
|
728
|
+
.cpub-blog-wrap { padding-top: 16px; padding-bottom: 40px; }
|
|
717
729
|
.cpub-blog-title { font-size: 20px; }
|
|
718
730
|
.cpub-blog-lead { font-size: 13px; }
|
|
719
731
|
.cpub-author-card { flex-direction: column; gap: 12px; }
|
|
@@ -591,8 +591,8 @@ async function handleBuild(): Promise<void> {
|
|
|
591
591
|
<style scoped>
|
|
592
592
|
/* ── HERO COVER ── */
|
|
593
593
|
.cpub-project-view {
|
|
594
|
-
|
|
595
|
-
|
|
594
|
+
/* Prevent any child from creating horizontal scroll */
|
|
595
|
+
overflow-x: clip;
|
|
596
596
|
}
|
|
597
597
|
|
|
598
598
|
.cpub-hero-cover {
|
|
@@ -680,7 +680,7 @@ async function handleBuild(): Promise<void> {
|
|
|
680
680
|
.cpub-page-outer {
|
|
681
681
|
max-width: 1160px;
|
|
682
682
|
margin: 0 auto;
|
|
683
|
-
padding: 0 32px;
|
|
683
|
+
padding: 0 clamp(12px, 3vw, 32px);
|
|
684
684
|
}
|
|
685
685
|
|
|
686
686
|
/* ── BREADCRUMB ── */
|
|
@@ -925,16 +925,19 @@ async function handleBuild(): Promise<void> {
|
|
|
925
925
|
/* ── CONTENT GRID ── */
|
|
926
926
|
.cpub-content-grid {
|
|
927
927
|
display: grid;
|
|
928
|
-
grid-template-columns: 1fr 260px;
|
|
929
|
-
gap: 32px;
|
|
928
|
+
grid-template-columns: minmax(0, 1fr) 260px;
|
|
929
|
+
gap: clamp(16px, 3vw, 32px);
|
|
930
930
|
align-items: start;
|
|
931
931
|
padding-bottom: 64px;
|
|
932
932
|
}
|
|
933
933
|
.cpub-content-grid.cpub-has-toc {
|
|
934
|
-
grid-template-columns: 200px 1fr 260px;
|
|
934
|
+
grid-template-columns: 200px minmax(0, 1fr) 260px;
|
|
935
935
|
}
|
|
936
936
|
|
|
937
|
-
|
|
937
|
+
/* Prevent grid children from overflowing */
|
|
938
|
+
.cpub-content-col,
|
|
939
|
+
.cpub-sidebar,
|
|
940
|
+
.cpub-toc-col {
|
|
938
941
|
min-width: 0;
|
|
939
942
|
overflow-wrap: break-word;
|
|
940
943
|
}
|
|
@@ -960,6 +963,20 @@ async function handleBuild(): Promise<void> {
|
|
|
960
963
|
word-break: break-word;
|
|
961
964
|
}
|
|
962
965
|
|
|
966
|
+
/* Prevent images and media from overflowing prose */
|
|
967
|
+
.cpub-prose :deep(img),
|
|
968
|
+
.cpub-prose :deep(video),
|
|
969
|
+
.cpub-prose :deep(iframe) {
|
|
970
|
+
max-width: 100%;
|
|
971
|
+
height: auto;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/* Code blocks scroll horizontally instead of overflowing */
|
|
975
|
+
.cpub-prose :deep(pre) {
|
|
976
|
+
overflow-x: auto;
|
|
977
|
+
-webkit-overflow-scrolling: touch;
|
|
978
|
+
}
|
|
979
|
+
|
|
963
980
|
.cpub-prose :deep(h2),
|
|
964
981
|
.cpub-prose :deep(.section-title) {
|
|
965
982
|
font-size: 16px;
|
|
@@ -1525,48 +1542,53 @@ async function handleBuild(): Promise<void> {
|
|
|
1525
1542
|
}
|
|
1526
1543
|
|
|
1527
1544
|
/* ── RESPONSIVE ── */
|
|
1545
|
+
|
|
1546
|
+
/* 1200px: Drop left TOC column, keep sidebar */
|
|
1528
1547
|
@media (max-width: 1200px) {
|
|
1529
1548
|
.cpub-content-grid.cpub-has-toc {
|
|
1530
|
-
grid-template-columns: 1fr 260px;
|
|
1549
|
+
grid-template-columns: minmax(0, 1fr) 260px;
|
|
1531
1550
|
}
|
|
1532
1551
|
.cpub-toc-col { display: none; }
|
|
1533
1552
|
}
|
|
1553
|
+
|
|
1554
|
+
/* 1024px: Single column — sidebar stacks below content */
|
|
1534
1555
|
@media (max-width: 1024px) {
|
|
1535
1556
|
.cpub-content-grid,
|
|
1536
1557
|
.cpub-content-grid.cpub-has-toc {
|
|
1537
|
-
grid-template-columns: 1fr;
|
|
1558
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1538
1559
|
}
|
|
1539
1560
|
.cpub-sidebar { position: static; }
|
|
1540
1561
|
.cpub-toc-col { display: none; }
|
|
1541
1562
|
}
|
|
1542
1563
|
|
|
1564
|
+
/* 768px: Tablet — reduce sizes, wrap engagement, scroll tabs */
|
|
1543
1565
|
@media (max-width: 768px) {
|
|
1544
|
-
.cpub-page-outer { padding: 0 16px; }
|
|
1545
1566
|
.cpub-hero-cover { height: 200px; }
|
|
1546
1567
|
.cpub-project-title { font-size: 18px; }
|
|
1547
1568
|
.cpub-project-desc { font-size: 13px; }
|
|
1548
|
-
.cpub-tabs-strip {
|
|
1549
|
-
.cpub-tabs-inner { max-width: none; padding: 0; }
|
|
1569
|
+
.cpub-tabs-strip { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
1570
|
+
.cpub-tabs-inner { max-width: none; padding: 0 clamp(12px, 3vw, 32px); }
|
|
1550
1571
|
.cpub-tab { padding: 10px 12px; font-size: 11px; white-space: nowrap; }
|
|
1551
1572
|
.cpub-engagement-row { flex-wrap: wrap; gap: 6px; }
|
|
1552
1573
|
.cpub-engage-btn { padding: 8px 12px; min-height: 36px; }
|
|
1553
1574
|
.cpub-engage-sep { display: none; }
|
|
1554
|
-
.cpub-content-area { padding: 0 16px; }
|
|
1555
1575
|
.cpub-sidebar { position: static; }
|
|
1556
1576
|
.cpub-author-tags .cpub-author-tag { padding: 2px 8px; font-size: 10px; }
|
|
1577
|
+
.cpub-author-row { flex-wrap: wrap; gap: 8px; }
|
|
1578
|
+
.cpub-content-grid { padding-bottom: 32px; }
|
|
1557
1579
|
}
|
|
1558
1580
|
|
|
1581
|
+
/* 480px: Phone — compact everything */
|
|
1559
1582
|
@media (max-width: 480px) {
|
|
1560
|
-
.cpub-page-outer { padding: 0 12px; }
|
|
1561
1583
|
.cpub-hero-cover { height: 160px; }
|
|
1562
1584
|
.cpub-project-title { font-size: 16px; }
|
|
1563
1585
|
.cpub-project-desc { font-size: 12px; line-height: 1.5; }
|
|
1564
|
-
.cpub-project-meta { padding:
|
|
1565
|
-
.cpub-
|
|
1586
|
+
.cpub-project-meta { padding: 12px 0 0; }
|
|
1587
|
+
.cpub-breadcrumb { font-size: 10px; padding: 10px 0 8px; }
|
|
1566
1588
|
.cpub-tab { padding: 8px 10px; font-size: 10px; }
|
|
1567
1589
|
.cpub-engage-btn { font-size: 11px; padding: 8px 10px; }
|
|
1568
|
-
.cpub-author-row { gap: 8px; }
|
|
1569
1590
|
.cpub-author-detail { font-size: 10px; }
|
|
1570
1591
|
.cpub-toc-item { padding: 6px 0 6px 10px; font-size: 11px; min-height: 36px; display: flex; align-items: center; }
|
|
1592
|
+
.cpub-content-grid { gap: 12px; padding-bottom: 24px; }
|
|
1571
1593
|
}
|
|
1572
1594
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -50,15 +50,15 @@
|
|
|
50
50
|
"vue": "^3.4.0",
|
|
51
51
|
"vue-router": "^4.3.0",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
|
+
"@commonpub/editor": "0.7.5",
|
|
54
|
+
"@commonpub/docs": "0.6.2",
|
|
53
55
|
"@commonpub/auth": "0.5.0",
|
|
54
56
|
"@commonpub/config": "0.9.0",
|
|
55
|
-
"@commonpub/docs": "0.6.2",
|
|
56
|
-
"@commonpub/learning": "0.5.0",
|
|
57
57
|
"@commonpub/explainer": "0.7.6",
|
|
58
|
-
"@commonpub/
|
|
59
|
-
"@commonpub/ui": "0.8.5",
|
|
58
|
+
"@commonpub/learning": "0.5.0",
|
|
60
59
|
"@commonpub/server": "2.27.7",
|
|
61
60
|
"@commonpub/protocol": "0.9.7",
|
|
61
|
+
"@commonpub/ui": "0.8.5",
|
|
62
62
|
"@commonpub/schema": "0.9.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
@@ -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) || '',
|