@commonpub/layer 0.82.0 → 0.83.0
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/AppToast.vue +1 -1
- package/components/ContentAvatar.vue +98 -0
- package/components/CpubCriteriaBar.vue +88 -0
- package/components/CpubDateTimeField.vue +73 -0
- package/components/CpubMarkdown.vue +3 -1
- package/components/FormatToggle.vue +2 -2
- package/components/ImageUpload.vue +5 -8
- package/components/MirrorDetailModal.vue +3 -1
- package/components/MirrorRequestApproveModal.vue +3 -1
- package/components/ProductEditModal.vue +184 -0
- package/components/RemoteFollowDialog.vue +2 -2
- package/components/SearchSidebar.vue +14 -21
- package/components/ShareToHubModal.vue +3 -1
- package/components/admin/layouts/AdminLayoutsPalette.vue +5 -1
- package/components/admin/layouts/AdminLayoutsPaletteTile.vue +7 -1
- package/components/admin/layouts/AdminLayoutsToolbar.vue +1 -1
- package/components/blocks/BlockCompareColumnsView.vue +92 -0
- package/components/blocks/BlockContentRenderer.vue +17 -0
- package/components/blocks/BlockCriteriaBarView.vue +25 -0
- package/components/blocks/BlockGalleryView.vue +5 -0
- package/components/blocks/BlockHtmlView.vue +26 -0
- package/components/blocks/BlockImageView.vue +4 -0
- package/components/blocks/BlockJudgesShowcaseView.vue +52 -0
- package/components/blocks/BlockRoadmapView.vue +84 -0
- package/components/blocks/BlockSponsorsView.vue +89 -0
- package/components/blocks/BlockTableView.vue +49 -0
- package/components/blocks/BlockTabsView.vue +121 -0
- package/components/contest/ContestBodyCanvas.vue +155 -0
- package/components/contest/ContestCriteriaEditor.vue +79 -0
- package/components/contest/ContestEditor.vue +948 -0
- package/components/contest/ContestEntries.vue +1 -1
- package/components/contest/ContestEntryPrivateData.vue +126 -0
- package/components/contest/ContestHero.vue +114 -186
- package/components/contest/ContestJudgeManager.vue +6 -4
- package/components/contest/ContestJudgingCriteria.vue +5 -21
- package/components/contest/ContestPrizes.vue +8 -1
- package/components/contest/ContestProposalForm.vue +88 -0
- package/components/contest/ContestRules.vue +8 -1
- package/components/contest/ContestSidebar.vue +8 -2
- package/components/contest/ContestStageSubmission.vue +10 -36
- package/components/contest/ContestStagesEditor.vue +141 -65
- package/components/contest/ContestStakeholderManager.vue +3 -2
- package/components/contest/ContestSubmissionField.vue +141 -0
- package/components/contest/blocks/CompareColumnsBlock.vue +127 -0
- package/components/contest/blocks/ContestTabPanel.vue +27 -0
- package/components/contest/blocks/CriteriaBarBlock.vue +118 -0
- package/components/contest/blocks/HtmlBlock.vue +61 -0
- package/components/contest/blocks/JudgesShowcaseBlock.vue +96 -0
- package/components/contest/blocks/RoadmapBlock.vue +127 -0
- package/components/contest/blocks/SponsorsBlock.vue +127 -0
- package/components/contest/blocks/TableBlock.vue +101 -0
- package/components/contest/blocks/TabsBlock.vue +168 -0
- package/components/editors/ArticleEditor.vue +9 -16
- package/components/editors/ExplainerEditor.vue +8 -5
- package/components/editors/ProjectEditor.vue +13 -10
- package/components/homepage/CustomHtmlSection.vue +11 -2
- package/components/hub/HubProducts.vue +4 -2
- package/components/nav/NavDropdown.vue +1 -5
- package/components/nav/NavLink.vue +2 -0
- package/components/views/ArticleView.vue +3 -56
- package/components/views/ExplainerView.vue +4 -0
- package/components/views/ProjectView.vue +83 -245
- package/composables/useContestEditor.ts +388 -0
- package/composables/useDocsPageTree.ts +154 -0
- package/composables/useDocsSiteSettings.ts +107 -0
- package/composables/useEditorAutosave.ts +131 -0
- package/composables/useEngagement.ts +13 -6
- package/composables/useFeatures.ts +9 -1
- package/composables/useFileUpload.ts +60 -0
- package/composables/useProfileContent.ts +84 -0
- package/composables/useSanitize.ts +38 -4
- package/composables/useScrollSpy.ts +87 -0
- package/layouts/admin.vue +41 -19
- package/layouts/default.vue +18 -9
- package/nuxt.config.ts +13 -0
- package/package.json +9 -9
- package/pages/[type]/index.vue +6 -1
- package/pages/admin/api-keys.vue +13 -3
- package/pages/admin/features.vue +2 -0
- package/pages/admin/federation.vue +1 -1
- package/pages/admin/layouts/[id].vue +30 -2
- package/pages/admin/settings.vue +2 -1
- package/pages/admin/users.vue +1 -1
- package/pages/admin/video-categories.vue +203 -0
- package/pages/cert/[code].vue +6 -2
- package/pages/contests/[slug]/edit.vue +4 -769
- package/pages/contests/[slug]/entries/[entryId].vue +34 -1
- package/pages/contests/[slug]/index.vue +93 -7
- package/pages/contests/[slug]/judge.vue +49 -26
- package/pages/contests/create.vue +5 -466
- package/pages/contests/index.vue +7 -2
- package/pages/cookies.vue +1 -1
- package/pages/docs/[siteSlug]/[...pagePath].vue +13 -26
- package/pages/docs/[siteSlug]/edit.vue +93 -231
- package/pages/events/[slug]/edit.vue +20 -20
- package/pages/events/create.vue +18 -18
- package/pages/events/index.vue +7 -2
- package/pages/hubs/[slug]/index.vue +34 -9
- package/pages/hubs/[slug]/invites.vue +312 -0
- package/pages/hubs/[slug]/members.vue +128 -0
- package/pages/hubs/[slug]/posts/[postId].vue +2 -2
- package/pages/hubs/index.vue +6 -1
- package/pages/learn/[slug]/[lessonSlug]/index.vue +12 -3
- package/pages/learn/index.vue +8 -1
- package/pages/messages/index.vue +1 -1
- package/pages/mirror/[id].vue +1 -1
- package/pages/products/[slug].vue +55 -2
- package/pages/products/index.vue +6 -1
- package/pages/settings/account.vue +8 -8
- package/pages/settings/profile.vue +23 -14
- package/pages/u/[username]/[type]/[slug]/edit.vue +12 -5
- package/pages/u/[username]/followers.vue +11 -3
- package/pages/u/[username]/following.vue +10 -8
- package/pages/u/[username]/index.vue +73 -7
- package/pages/videos/index.vue +13 -10
- package/server/api/admin/api-keys/[id]/usage.get.ts +2 -2
- package/server/api/admin/api-keys/[id].delete.ts +2 -2
- package/server/api/admin/api-keys/index.get.ts +1 -0
- package/server/api/admin/api-keys/index.post.ts +1 -0
- package/server/api/admin/federation/refederate.post.ts +18 -1
- package/server/api/admin/layouts/[id]/publish.post.ts +1 -4
- package/server/api/admin/layouts/[id]/versions/[versionId]/revert.post.ts +1 -5
- package/server/api/admin/layouts/[id]/versions/index.get.ts +1 -4
- package/server/api/admin/layouts/[id].delete.ts +1 -4
- package/server/api/admin/layouts/[id].get.ts +1 -4
- package/server/api/admin/layouts/[id].put.ts +1 -4
- package/server/api/auth/federated/login.post.ts +12 -5
- package/server/api/content/[id]/__tests__/versions.get.test.ts +127 -0
- package/server/api/content/[id]/build.get.ts +11 -0
- package/server/api/content/[id]/report.post.ts +2 -0
- package/server/api/content/[id]/versions.get.ts +15 -0
- package/server/api/contests/[slug]/entries/[entryId]/private.get.ts +48 -0
- package/server/api/contests/[slug]/entries/[entryId]/submission.put.ts +1 -1
- package/server/api/contests/[slug]/entries/[entryId]/vote.delete.ts +1 -2
- package/server/api/contests/[slug]/entries/[entryId]/vote.post.ts +1 -2
- package/server/api/contests/[slug]/export.get.ts +43 -0
- package/server/api/contests/[slug]/judge.post.ts +8 -2
- package/server/api/contests/[slug]/proposal.post.ts +36 -0
- package/server/api/contests/[slug]/user-search.get.ts +30 -0
- package/server/api/contests/index.post.ts +1 -1
- package/server/api/docs/[siteSlug]/nav.get.ts +6 -1
- package/server/api/docs/[siteSlug]/pages/[pageId].get.ts +5 -1
- package/server/api/docs/[siteSlug]/pages/index.get.ts +6 -1
- package/server/api/docs/[siteSlug]/search.get.ts +7 -1
- package/server/api/events/[slug]/attendees.get.ts +10 -0
- package/server/api/events/[slug].get.ts +9 -0
- package/server/api/events/index.get.ts +8 -1
- package/server/api/federated-hubs/[id]/posts/[postId]/replies.get.ts +1 -1
- package/server/api/federation/content/[id]/build.get.ts +10 -0
- package/server/api/hubs/[slug]/invites/[id].delete.ts +17 -0
- package/server/api/hubs/[slug]/invites.get.ts +5 -3
- package/server/api/hubs/[slug]/posts/[postId]/poll-options.get.ts +1 -2
- package/server/api/hubs/[slug]/posts/[postId]/poll-vote.post.ts +1 -2
- package/server/api/hubs/[slug]/posts/[postId]/vote.post.ts +1 -2
- package/server/api/hubs/[slug]/requests/[userId]/approve.post.ts +15 -0
- package/server/api/hubs/[slug]/requests/[userId]/deny.post.ts +15 -0
- package/server/api/hubs/[slug]/requests.get.ts +20 -0
- package/server/api/hubs/[slug]/resources/[id].delete.ts +1 -2
- package/server/api/hubs/[slug]/resources/[id].put.ts +1 -2
- package/server/api/products/[id].delete.ts +22 -2
- package/server/api/registry/ping.post.ts +17 -3
- package/server/api/search/index.get.ts +5 -3
- package/server/api/social/bookmark.get.ts +1 -0
- package/server/api/social/bookmark.post.ts +1 -0
- package/server/api/social/bookmarks.get.ts +1 -0
- package/server/api/social/comments/[id].delete.ts +1 -0
- package/server/api/social/comments.get.ts +1 -0
- package/server/api/social/comments.post.ts +1 -0
- package/server/api/social/like.get.ts +1 -0
- package/server/api/social/like.post.ts +1 -0
- package/server/api/users/[username]/content.get.ts +15 -3
- package/server/api/users/[username]/follow.delete.ts +1 -0
- package/server/api/users/[username]/follow.post.ts +1 -0
- package/server/api/users/[username]/followers.get.ts +2 -1
- package/server/api/users/[username]/following.get.ts +2 -1
- package/server/middleware/content-ap.ts +8 -3
- package/server/middleware/csrf.ts +93 -0
- package/server/plugins/federation-hub-sync.ts +48 -17
- package/server/plugins/notification-email.ts +22 -3
- package/server/routes/hubs/[slug]/inbox.ts +13 -1
- package/server/routes/inbox.ts +14 -1
- package/server/routes/users/[username]/inbox.ts +13 -1
- package/server/utils/inbox.ts +7 -2
- package/server/utils/validate.ts +22 -0
- package/theme/base.css +5 -0
- package/theme/prose.css +20 -0
- package/theme/stoa-dark.css +4 -0
- package/types/contestBlocks.ts +122 -0
- package/utils/contestBlocks.ts +107 -0
- package/utils/contestBody.ts +25 -0
- package/utils/contestStages.ts +62 -0
- package/utils/contestSubmission.ts +97 -0
- package/utils/datetime.ts +45 -0
- package/utils/projectBlocks.ts +162 -0
- package/components/editors/BlogEditor.vue +0 -648
|
@@ -1,648 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { BlockCanvas, EditorBlocks, EditorSection, EditorTagInput, EditorVisibility, type BlockEditor, type BlockTypeGroup } from '@commonpub/editor/vue';
|
|
3
|
-
|
|
4
|
-
const props = defineProps<{
|
|
5
|
-
blockEditor: BlockEditor;
|
|
6
|
-
metadata: Record<string, unknown>;
|
|
7
|
-
}>();
|
|
8
|
-
|
|
9
|
-
const emit = defineEmits<{
|
|
10
|
-
'update:metadata': [metadata: Record<string, unknown>];
|
|
11
|
-
}>();
|
|
12
|
-
|
|
13
|
-
const { user } = useAuth();
|
|
14
|
-
|
|
15
|
-
function updateMeta(key: string, value: unknown): void {
|
|
16
|
-
emit('update:metadata', { ...props.metadata, [key]: value });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const blockTypes: BlockTypeGroup[] = [
|
|
20
|
-
{
|
|
21
|
-
name: 'Text',
|
|
22
|
-
blocks: [
|
|
23
|
-
{ type: 'paragraph', label: 'Paragraph', icon: 'fa-align-left', description: 'Body text' },
|
|
24
|
-
{ type: 'heading', label: 'Heading', icon: 'fa-heading', description: 'Section header' },
|
|
25
|
-
{ type: 'blockquote', label: 'Quote', icon: 'fa-quote-left', description: 'Blockquote' },
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
name: 'Media',
|
|
30
|
-
blocks: [
|
|
31
|
-
{ type: 'image', label: 'Image', icon: 'fa-image', description: 'Upload or embed' },
|
|
32
|
-
{ type: 'code_block', label: 'Code Block', icon: 'fa-code', description: 'Syntax highlighted code' },
|
|
33
|
-
{ type: 'video', label: 'Video', icon: 'fa-film', description: 'YouTube, Vimeo embed' },
|
|
34
|
-
{ type: 'horizontal_rule', label: 'Divider', icon: 'fa-minus', description: 'Visual separator' },
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: 'Rich',
|
|
39
|
-
blocks: [
|
|
40
|
-
{ type: 'callout', label: 'Callout', icon: 'fa-circle-info', description: 'Tip, warning, or note', attrs: { variant: 'info' } },
|
|
41
|
-
{ type: 'embed', label: 'Embed', icon: 'fa-globe', description: 'External embed' },
|
|
42
|
-
{ type: 'markdown', label: 'Markdown', icon: 'fa-brands fa-markdown', description: 'Raw markdown block' },
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const openSections = ref<Record<string, boolean>>({
|
|
48
|
-
meta: true, excerpt: true, banner: false, seo: true, publishing: true, author: true, social: false,
|
|
49
|
-
});
|
|
50
|
-
function toggleSection(key: string): void {
|
|
51
|
-
openSections.value[key] = !openSections.value[key];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const tags = computed(() => (props.metadata.tags as string[]) || []);
|
|
55
|
-
function onTagsUpdate(newTags: string[]): void { updateMeta('tags', newTags); }
|
|
56
|
-
|
|
57
|
-
// --- Cover image ---
|
|
58
|
-
const coverImageUrl = computed(() => (props.metadata.coverImageUrl as string) || '');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
function onCoverUpload(event: Event): void {
|
|
62
|
-
const input = event.target as HTMLInputElement;
|
|
63
|
-
if (!input.files?.length) return;
|
|
64
|
-
const file = input.files[0];
|
|
65
|
-
if (!file) return;
|
|
66
|
-
const formData = new FormData();
|
|
67
|
-
formData.append('file', file);
|
|
68
|
-
formData.append('purpose', 'cover');
|
|
69
|
-
$fetch<{ url: string }>('/api/files/upload', { method: 'POST', body: formData })
|
|
70
|
-
.then((res) => { updateMeta('coverImageUrl', res.url); })
|
|
71
|
-
.catch(() => { /* silent fallback */ });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function onCoverUrl(): void {
|
|
75
|
-
const url = window.prompt('Enter image URL:');
|
|
76
|
-
if (url) updateMeta('coverImageUrl', url);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function removeCover(): void {
|
|
80
|
-
updateMeta('coverImageUrl', '');
|
|
81
|
-
}
|
|
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
|
-
|
|
103
|
-
// --- Word count ---
|
|
104
|
-
const wordCount = computed(() => {
|
|
105
|
-
let count = 0;
|
|
106
|
-
for (const block of props.blockEditor.blocks.value) {
|
|
107
|
-
const html = (block.content.html as string) || '';
|
|
108
|
-
const text = (block.content.text as string) || '';
|
|
109
|
-
const code = (block.content.code as string) || '';
|
|
110
|
-
const instructions = (block.content.instructions as string) || '';
|
|
111
|
-
let combined = html.replace(/<[^>]*>/g, ' ') + ' ' + text + ' ' + code + ' ' + instructions;
|
|
112
|
-
const children = block.content.children;
|
|
113
|
-
if (Array.isArray(children)) {
|
|
114
|
-
for (const child of children) {
|
|
115
|
-
const [, childData] = child as [string, Record<string, unknown>];
|
|
116
|
-
const childHtml = (childData?.html as string) || '';
|
|
117
|
-
const childCode = (childData?.code as string) || '';
|
|
118
|
-
combined += ' ' + childHtml.replace(/<[^>]*>/g, ' ') + ' ' + childCode;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
count += combined.split(/\s+/).filter((w) => w.length > 0).length;
|
|
122
|
-
}
|
|
123
|
-
return count;
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const readTime = computed(() => Math.max(1, Math.round(wordCount.value / 200)));
|
|
127
|
-
const blockCount = computed(() => props.blockEditor.blocks.value.length);
|
|
128
|
-
|
|
129
|
-
// --- Author info ---
|
|
130
|
-
const authorName = computed(() => user.value?.name || 'Author');
|
|
131
|
-
const authorInitials = computed(() => {
|
|
132
|
-
const name = authorName.value;
|
|
133
|
-
const parts = name.split(/\s+/);
|
|
134
|
-
return parts.map((p) => p[0]?.toUpperCase() || '').join('').slice(0, 2);
|
|
135
|
-
});
|
|
136
|
-
const authorUsername = computed(() => user.value?.username || '');
|
|
137
|
-
|
|
138
|
-
// --- SEO preview ---
|
|
139
|
-
const siteDomain = computed(() => {
|
|
140
|
-
try { return new URL(useRequestURL().origin).hostname; } catch { return 'example.com'; }
|
|
141
|
-
});
|
|
142
|
-
const seoDesc = computed(() => (props.metadata.seoDescription as string) || (props.metadata.description as string) || '');
|
|
143
|
-
|
|
144
|
-
// --- Schedule ---
|
|
145
|
-
// Open the schedule control automatically when editing an already-scheduled post
|
|
146
|
-
// (metadata loads asynchronously, hence the immediate watch rather than a one-shot init).
|
|
147
|
-
const scheduleEnabled = ref(false);
|
|
148
|
-
watch(() => props.metadata.scheduledAt, (v) => {
|
|
149
|
-
if (v) scheduleEnabled.value = true;
|
|
150
|
-
}, { immediate: true });
|
|
151
|
-
// Turning the toggle off discards any pending schedule time so the Schedule
|
|
152
|
-
// button (gated on metadata.scheduledAt) and saved drafts don't keep a stale value.
|
|
153
|
-
watch(scheduleEnabled, (on) => {
|
|
154
|
-
if (!on && props.metadata.scheduledAt) updateMeta('scheduledAt', '');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// --- Mobile sidebar toggles ---
|
|
158
|
-
const mobileLeftOpen = ref(false);
|
|
159
|
-
const mobileRightOpen = ref(false);
|
|
160
|
-
function toggleMobileLeft(): void {
|
|
161
|
-
mobileLeftOpen.value = !mobileLeftOpen.value;
|
|
162
|
-
if (mobileLeftOpen.value) mobileRightOpen.value = false;
|
|
163
|
-
}
|
|
164
|
-
function toggleMobileRight(): void {
|
|
165
|
-
mobileRightOpen.value = !mobileRightOpen.value;
|
|
166
|
-
if (mobileRightOpen.value) mobileLeftOpen.value = false;
|
|
167
|
-
}
|
|
168
|
-
function closeMobileSidebars(): void {
|
|
169
|
-
mobileLeftOpen.value = false;
|
|
170
|
-
mobileRightOpen.value = false;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// --- Canvas toolbar ---
|
|
174
|
-
const canvasRef = ref<{ toggleMark: (mark: string) => void; toggleLink: () => void; getActiveMarks: () => Record<string, boolean> } | null>(null);
|
|
175
|
-
const viewportMode = ref<'desktop' | 'tablet' | 'mobile'>('desktop');
|
|
176
|
-
const canvasMaxWidth = computed(() => {
|
|
177
|
-
if (viewportMode.value === 'mobile') return '375px';
|
|
178
|
-
if (viewportMode.value === 'tablet') return '768px';
|
|
179
|
-
return '720px';
|
|
180
|
-
});
|
|
181
|
-
</script>
|
|
182
|
-
|
|
183
|
-
<template>
|
|
184
|
-
<div class="cpub-be-shell">
|
|
185
|
-
<!-- Mobile sidebar toggles -->
|
|
186
|
-
<div class="cpub-be-mobile-toggles">
|
|
187
|
-
<button class="cpub-be-mobile-btn" aria-label="Toggle blocks panel" @click="toggleMobileLeft"><i class="fa-solid fa-layer-group"></i></button>
|
|
188
|
-
<button class="cpub-be-mobile-btn" aria-label="Toggle properties panel" @click="toggleMobileRight"><i class="fa-solid fa-sliders"></i></button>
|
|
189
|
-
</div>
|
|
190
|
-
<div v-if="mobileLeftOpen || mobileRightOpen" class="cpub-be-mobile-overlay" @click="closeMobileSidebars" />
|
|
191
|
-
|
|
192
|
-
<!-- LEFT: Block Library -->
|
|
193
|
-
<aside class="cpub-be-library" :class="{ 'cpub-be-sidebar-open': mobileLeftOpen }" aria-label="Block library">
|
|
194
|
-
<EditorBlocks :groups="blockTypes" :block-editor="blockEditor" />
|
|
195
|
-
</aside>
|
|
196
|
-
|
|
197
|
-
<!-- CENTER: Canvas with toolbar, cover, title, subtitle, byline, blocks -->
|
|
198
|
-
<div class="cpub-be-center">
|
|
199
|
-
<!-- Canvas toolbar -->
|
|
200
|
-
<div class="cpub-be-canvas-toolbar">
|
|
201
|
-
<!-- Text formatting -->
|
|
202
|
-
<button class="cpub-be-tool-btn" title="Bold (Ctrl+B)" @click="canvasRef?.toggleMark('bold')"><i class="fa-solid fa-bold"></i></button>
|
|
203
|
-
<button class="cpub-be-tool-btn" title="Italic (Ctrl+I)" @click="canvasRef?.toggleMark('italic')"><i class="fa-solid fa-italic"></i></button>
|
|
204
|
-
<button class="cpub-be-tool-btn" title="Strikethrough" @click="canvasRef?.toggleMark('strike')"><i class="fa-solid fa-strikethrough"></i></button>
|
|
205
|
-
<button class="cpub-be-tool-btn" title="Inline code" @click="canvasRef?.toggleMark('code')"><i class="fa-solid fa-code"></i></button>
|
|
206
|
-
<button class="cpub-be-tool-btn" title="Link" @click="canvasRef?.toggleLink()"><i class="fa-solid fa-link"></i></button>
|
|
207
|
-
<div class="cpub-be-toolbar-divider" />
|
|
208
|
-
<div class="cpub-be-viewport-tabs">
|
|
209
|
-
<button class="cpub-be-viewport-tab" :class="{ active: viewportMode === 'desktop' }" title="Desktop" @click="viewportMode = 'desktop'"><i class="fa-solid fa-desktop"></i></button>
|
|
210
|
-
<button class="cpub-be-viewport-tab" :class="{ active: viewportMode === 'tablet' }" title="Tablet" @click="viewportMode = 'tablet'"><i class="fa-solid fa-tablet-screen-button"></i></button>
|
|
211
|
-
<button class="cpub-be-viewport-tab" :class="{ active: viewportMode === 'mobile' }" title="Mobile" @click="viewportMode = 'mobile'"><i class="fa-solid fa-mobile-screen"></i></button>
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
<div class="cpub-be-canvas" :style="{ maxWidth: canvasMaxWidth }">
|
|
216
|
-
<!-- Cover image area -->
|
|
217
|
-
<div class="cpub-be-cover" :class="{ 'has-image': !!coverImageUrl }">
|
|
218
|
-
<template v-if="coverImageUrl">
|
|
219
|
-
<img :src="coverImageUrl" alt="Cover image" class="cpub-be-cover-img" />
|
|
220
|
-
<div class="cpub-be-cover-actions">
|
|
221
|
-
<button class="cpub-be-cover-btn" @click="removeCover">
|
|
222
|
-
<i class="fa-solid fa-trash"></i> Remove
|
|
223
|
-
</button>
|
|
224
|
-
<label class="cpub-be-cover-btn">
|
|
225
|
-
<i class="fa-solid fa-arrow-up-from-bracket"></i> Replace
|
|
226
|
-
<input type="file" accept="image/*" class="cpub-sr-only" @change="onCoverUpload">
|
|
227
|
-
</label>
|
|
228
|
-
</div>
|
|
229
|
-
</template>
|
|
230
|
-
<template v-else>
|
|
231
|
-
<div class="cpub-be-cover-placeholder">
|
|
232
|
-
<div class="cpub-be-cover-icon"><i class="fa-regular fa-image"></i></div>
|
|
233
|
-
<span class="cpub-be-cover-text">Cover image</span>
|
|
234
|
-
</div>
|
|
235
|
-
<div class="cpub-be-cover-overlay">
|
|
236
|
-
<label class="cpub-be-cover-btn primary">
|
|
237
|
-
<i class="fa-solid fa-arrow-up-from-bracket"></i> Upload
|
|
238
|
-
<input type="file" accept="image/*" class="cpub-sr-only" @change="onCoverUpload">
|
|
239
|
-
</label>
|
|
240
|
-
<button class="cpub-be-cover-btn" @click="onCoverUrl">
|
|
241
|
-
<i class="fa-solid fa-link"></i> From URL
|
|
242
|
-
</button>
|
|
243
|
-
</div>
|
|
244
|
-
</template>
|
|
245
|
-
</div>
|
|
246
|
-
|
|
247
|
-
<!-- Title -->
|
|
248
|
-
<textarea
|
|
249
|
-
class="cpub-be-title"
|
|
250
|
-
rows="2"
|
|
251
|
-
placeholder="Your post title..."
|
|
252
|
-
:value="(metadata.title as string) || ''"
|
|
253
|
-
@input="updateMeta('title', ($event.target as HTMLTextAreaElement).value)"
|
|
254
|
-
/>
|
|
255
|
-
|
|
256
|
-
<!-- Subtitle -->
|
|
257
|
-
<textarea
|
|
258
|
-
class="cpub-be-subtitle"
|
|
259
|
-
rows="1"
|
|
260
|
-
placeholder="Add a subtitle (optional)..."
|
|
261
|
-
:value="(metadata.subtitle as string) || ''"
|
|
262
|
-
@input="updateMeta('subtitle', ($event.target as HTMLTextAreaElement).value)"
|
|
263
|
-
/>
|
|
264
|
-
|
|
265
|
-
<!-- Byline -->
|
|
266
|
-
<div class="cpub-be-byline">
|
|
267
|
-
<div class="cpub-be-byline-av">{{ authorInitials }}</div>
|
|
268
|
-
<div class="cpub-be-byline-info">
|
|
269
|
-
<div class="cpub-be-byline-name">{{ authorName }}</div>
|
|
270
|
-
<div class="cpub-be-byline-meta">
|
|
271
|
-
<span>{{ new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) }}</span>
|
|
272
|
-
<span class="cpub-be-sep">·</span>
|
|
273
|
-
<span>{{ readTime }} min read</span>
|
|
274
|
-
<template v-if="metadata.category">
|
|
275
|
-
<span class="cpub-be-sep">·</span>
|
|
276
|
-
<span class="cpub-be-category-tag">{{ metadata.category }}</span>
|
|
277
|
-
</template>
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
|
|
282
|
-
<!-- Block editor canvas -->
|
|
283
|
-
<BlockCanvas ref="canvasRef" :block-editor="blockEditor" :block-types="blockTypes" />
|
|
284
|
-
</div>
|
|
285
|
-
|
|
286
|
-
<!-- Word count bar -->
|
|
287
|
-
<div class="cpub-be-wc-bar">
|
|
288
|
-
<div class="cpub-be-wc-stat"><i class="fa-solid fa-font"></i> <span>{{ wordCount }}</span> words</div>
|
|
289
|
-
<div class="cpub-be-wc-stat"><i class="fa-regular fa-clock"></i> <span>{{ readTime }}</span> min read</div>
|
|
290
|
-
<div class="cpub-be-wc-stat"><i class="fa-solid fa-paragraph"></i> <span>{{ blockCount }}</span> blocks</div>
|
|
291
|
-
<div class="cpub-be-wc-spacer" />
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
|
|
295
|
-
<!-- RIGHT: Properties -->
|
|
296
|
-
<aside class="cpub-be-right" :class="{ 'cpub-be-sidebar-open': mobileRightOpen }" aria-label="Blog properties">
|
|
297
|
-
<div class="cpub-be-right-body">
|
|
298
|
-
<!-- Meta -->
|
|
299
|
-
<EditorSection title="Meta" icon="fa-tag" :open="openSections.meta" @toggle="toggleSection('meta')">
|
|
300
|
-
<div class="cpub-ep-field">
|
|
301
|
-
<label class="cpub-ep-flabel">Slug</label>
|
|
302
|
-
<input class="cpub-ep-input cpub-ep-input-mono" type="text" :value="metadata.slug" placeholder="auto-generated" @input="updateMeta('slug', ($event.target as HTMLInputElement).value)">
|
|
303
|
-
</div>
|
|
304
|
-
<div class="cpub-ep-field">
|
|
305
|
-
<label class="cpub-ep-flabel">Category</label>
|
|
306
|
-
<select class="cpub-ep-select" :value="metadata.category || ''" @change="updateMeta('category', ($event.target as HTMLSelectElement).value)">
|
|
307
|
-
<option value="">Select category</option>
|
|
308
|
-
<option value="hardware">Hardware & Makers</option>
|
|
309
|
-
<option value="software">Software</option>
|
|
310
|
-
<option value="ai-ml">AI & ML</option>
|
|
311
|
-
<option value="homelab">Home Lab</option>
|
|
312
|
-
<option value="tutorial">Tutorial</option>
|
|
313
|
-
<option value="opinion">Opinion</option>
|
|
314
|
-
</select>
|
|
315
|
-
</div>
|
|
316
|
-
<div class="cpub-ep-field">
|
|
317
|
-
<label class="cpub-ep-flabel">Tags</label>
|
|
318
|
-
<EditorTagInput :tags="tags" @update:tags="onTagsUpdate" />
|
|
319
|
-
</div>
|
|
320
|
-
</EditorSection>
|
|
321
|
-
|
|
322
|
-
<!-- Excerpt / Description -->
|
|
323
|
-
<EditorSection title="Excerpt" icon="fa-align-left" :open="openSections.excerpt" @toggle="toggleSection('excerpt')">
|
|
324
|
-
<div class="cpub-ep-field">
|
|
325
|
-
<label class="cpub-ep-flabel">Custom Excerpt</label>
|
|
326
|
-
<textarea class="cpub-ep-textarea" rows="3" :value="(metadata.description as string) || ''" placeholder="Short description shown in feed previews..." @input="updateMeta('description', ($event.target as HTMLTextAreaElement).value)" />
|
|
327
|
-
<span class="cpub-ep-hint cpub-ep-hint-right">{{ ((metadata.description as string) || '').length }} / 300</span>
|
|
328
|
-
</div>
|
|
329
|
-
</EditorSection>
|
|
330
|
-
|
|
331
|
-
<!-- Banner Image -->
|
|
332
|
-
<EditorSection title="Banner Image" icon="fa-panorama" :open="openSections.banner" @toggle="toggleSection('banner')">
|
|
333
|
-
<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>
|
|
334
|
-
<div v-if="bannerUrl" class="cpub-be-banner-preview">
|
|
335
|
-
<img :src="bannerUrl" alt="Banner" class="cpub-be-banner-img" />
|
|
336
|
-
<div class="cpub-be-banner-actions">
|
|
337
|
-
<button class="cpub-be-cover-btn" @click="removeBanner"><i class="fa-solid fa-trash"></i> Remove</button>
|
|
338
|
-
<label class="cpub-be-cover-btn">
|
|
339
|
-
<i class="fa-solid fa-arrow-up-from-bracket"></i> Replace
|
|
340
|
-
<input type="file" accept="image/*" class="cpub-sr-only" @change="onBannerUpload">
|
|
341
|
-
</label>
|
|
342
|
-
</div>
|
|
343
|
-
</div>
|
|
344
|
-
<div v-else>
|
|
345
|
-
<label class="cpub-be-cover-btn primary" style="display: inline-flex;">
|
|
346
|
-
<i class="fa-solid fa-arrow-up-from-bracket"></i> Upload Banner
|
|
347
|
-
<input type="file" accept="image/*" class="cpub-sr-only" @change="onBannerUpload">
|
|
348
|
-
</label>
|
|
349
|
-
</div>
|
|
350
|
-
</EditorSection>
|
|
351
|
-
|
|
352
|
-
<!-- SEO Preview -->
|
|
353
|
-
<EditorSection title="SEO Preview" icon="fa-brands fa-google" :open="openSections.seo" @toggle="toggleSection('seo')">
|
|
354
|
-
<div class="cpub-be-seo-card">
|
|
355
|
-
<div class="cpub-be-seo-url">
|
|
356
|
-
<span class="cpub-be-seo-favicon">C</span>
|
|
357
|
-
{{ siteDomain }} › {{ authorUsername ? `@${authorUsername}` : 'blog' }}
|
|
358
|
-
</div>
|
|
359
|
-
<div class="cpub-be-seo-title">{{ (metadata.title as string) || 'Post title' }}</div>
|
|
360
|
-
<div class="cpub-be-seo-desc">{{ seoDesc || 'Post description will appear here...' }}</div>
|
|
361
|
-
</div>
|
|
362
|
-
<div class="cpub-ep-field" style="margin-top: 10px;">
|
|
363
|
-
<label class="cpub-ep-flabel">SEO Description</label>
|
|
364
|
-
<textarea class="cpub-ep-textarea" rows="3" :value="(metadata.seoDescription as string) || ''" placeholder="Search engine description..." @input="updateMeta('seoDescription', ($event.target as HTMLTextAreaElement).value)" />
|
|
365
|
-
<span class="cpub-ep-hint cpub-ep-hint-right">{{ ((metadata.seoDescription as string) || '').length }}/160</span>
|
|
366
|
-
</div>
|
|
367
|
-
</EditorSection>
|
|
368
|
-
|
|
369
|
-
<!-- Publishing -->
|
|
370
|
-
<EditorSection title="Publishing" icon="fa-globe" :open="openSections.publishing" @toggle="toggleSection('publishing')">
|
|
371
|
-
<div class="cpub-ep-field">
|
|
372
|
-
<label class="cpub-ep-flabel">Visibility</label>
|
|
373
|
-
<EditorVisibility :model-value="(metadata.visibility as string) || 'public'" @update:model-value="(v: string) => updateMeta('visibility', v)" />
|
|
374
|
-
</div>
|
|
375
|
-
<div class="cpub-ep-field">
|
|
376
|
-
<label class="cpub-be-schedule-row">
|
|
377
|
-
<span class="cpub-be-toggle-switch">
|
|
378
|
-
<input v-model="scheduleEnabled" type="checkbox" />
|
|
379
|
-
<span class="cpub-be-toggle-track" />
|
|
380
|
-
</span>
|
|
381
|
-
<span class="cpub-be-toggle-label">Schedule for later</span>
|
|
382
|
-
</label>
|
|
383
|
-
</div>
|
|
384
|
-
<div v-if="scheduleEnabled" class="cpub-ep-field">
|
|
385
|
-
<label class="cpub-ep-flabel">Publish Date</label>
|
|
386
|
-
<input class="cpub-ep-input cpub-ep-input-mono" type="datetime-local" :value="metadata.scheduledAt" @input="updateMeta('scheduledAt', ($event.target as HTMLInputElement).value)">
|
|
387
|
-
</div>
|
|
388
|
-
<div class="cpub-ep-field">
|
|
389
|
-
<label class="cpub-ep-flabel">Series <span class="cpub-ep-optional">(optional)</span></label>
|
|
390
|
-
<input class="cpub-ep-input" type="text" :value="metadata.series" placeholder="e.g. Home Lab Chronicles" @input="updateMeta('series', ($event.target as HTMLInputElement).value)">
|
|
391
|
-
</div>
|
|
392
|
-
</EditorSection>
|
|
393
|
-
|
|
394
|
-
<!-- Author -->
|
|
395
|
-
<EditorSection title="Author" icon="fa-user" :open="openSections.author" @toggle="toggleSection('author')">
|
|
396
|
-
<div class="cpub-be-author-row">
|
|
397
|
-
<div class="cpub-be-author-av">{{ authorInitials }}</div>
|
|
398
|
-
<div class="cpub-be-author-info">
|
|
399
|
-
<div class="cpub-be-author-name">{{ authorName }}</div>
|
|
400
|
-
<div class="cpub-be-author-role">{{ authorUsername ? `@${authorUsername}` : '' }}</div>
|
|
401
|
-
</div>
|
|
402
|
-
<span class="cpub-be-author-badge">You</span>
|
|
403
|
-
</div>
|
|
404
|
-
</EditorSection>
|
|
405
|
-
|
|
406
|
-
<!-- Social -->
|
|
407
|
-
<EditorSection title="Social" icon="fa-share-nodes" :open="openSections.social" @toggle="toggleSection('social')">
|
|
408
|
-
<div class="cpub-ep-field">
|
|
409
|
-
<label class="cpub-ep-flabel">Open Graph Image</label>
|
|
410
|
-
<div class="cpub-be-og-thumb">
|
|
411
|
-
<div class="cpub-be-og-overlay">
|
|
412
|
-
<i class="fa-solid fa-arrow-up-from-bracket"></i>
|
|
413
|
-
<span>Upload OG image</span>
|
|
414
|
-
</div>
|
|
415
|
-
</div>
|
|
416
|
-
</div>
|
|
417
|
-
</EditorSection>
|
|
418
|
-
</div>
|
|
419
|
-
</aside>
|
|
420
|
-
</div>
|
|
421
|
-
</template>
|
|
422
|
-
|
|
423
|
-
<style scoped>
|
|
424
|
-
.cpub-be-shell { display: flex; flex: 1; overflow: hidden; }
|
|
425
|
-
.cpub-be-library { width: 220px; flex-shrink: 0; background: var(--surface); border-right: var(--border-width-default) solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
|
|
426
|
-
.cpub-be-center { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
427
|
-
.cpub-be-canvas { flex: 1; overflow-y: auto; background: var(--bg); margin: 0 auto; width: 100%; transition: max-width 0.2s; }
|
|
428
|
-
|
|
429
|
-
/* Canvas toolbar */
|
|
430
|
-
.cpub-be-canvas-toolbar {
|
|
431
|
-
display: flex; align-items: center; gap: 2px; padding: 4px 12px;
|
|
432
|
-
background: var(--surface); border-bottom: var(--border-width-default) solid var(--border); flex-shrink: 0; min-height: 32px;
|
|
433
|
-
}
|
|
434
|
-
.cpub-be-tool-btn {
|
|
435
|
-
width: 28px; height: 26px; display: flex; align-items: center; justify-content: center;
|
|
436
|
-
background: none; border: var(--border-width-default) solid transparent; color: var(--text-dim);
|
|
437
|
-
font-size: 11px; cursor: pointer;
|
|
438
|
-
}
|
|
439
|
-
.cpub-be-tool-btn:hover { background: var(--surface2); border-color: var(--border2); color: var(--text); }
|
|
440
|
-
.cpub-be-toolbar-divider { width: 2px; height: 18px; background: var(--border2); margin: 0 6px; }
|
|
441
|
-
.cpub-be-viewport-tabs { display: flex; gap: 0; margin-left: auto; }
|
|
442
|
-
.cpub-be-viewport-tab {
|
|
443
|
-
width: 28px; height: 24px; display: flex; align-items: center; justify-content: center;
|
|
444
|
-
background: none; border: var(--border-width-default) solid var(--border); border-left-width: 0; color: var(--text-faint);
|
|
445
|
-
font-size: 10px; cursor: pointer;
|
|
446
|
-
}
|
|
447
|
-
.cpub-be-viewport-tab:first-child { border-left-width: 2px; }
|
|
448
|
-
.cpub-be-viewport-tab.active { background: var(--border); color: var(--color-text-inverse); }
|
|
449
|
-
.cpub-be-viewport-tab:hover:not(.active) { background: var(--surface2); color: var(--text-dim); }
|
|
450
|
-
.cpub-be-right { width: 320px; flex-shrink: 0; background: var(--surface); border-left: var(--border-width-default) solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
|
|
451
|
-
.cpub-be-right-body { flex: 1; overflow-y: auto; }
|
|
452
|
-
|
|
453
|
-
/* Cover image */
|
|
454
|
-
.cpub-be-cover {
|
|
455
|
-
position: relative;
|
|
456
|
-
width: 100%;
|
|
457
|
-
aspect-ratio: 16/7;
|
|
458
|
-
background: var(--surface2);
|
|
459
|
-
border-bottom: var(--border-width-default) solid var(--border);
|
|
460
|
-
display: flex;
|
|
461
|
-
align-items: center;
|
|
462
|
-
justify-content: center;
|
|
463
|
-
overflow: hidden;
|
|
464
|
-
}
|
|
465
|
-
.cpub-be-cover-img { width: 100%; height: 100%; object-fit: cover; }
|
|
466
|
-
.cpub-be-cover-placeholder { display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
|
467
|
-
.cpub-be-cover-icon { font-size: 28px; color: var(--text-faint); }
|
|
468
|
-
.cpub-be-cover-text { font-size: 11px; color: var(--text-faint); font-family: var(--font-mono); }
|
|
469
|
-
.cpub-be-cover-overlay, .cpub-be-cover-actions {
|
|
470
|
-
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
471
|
-
background: var(--color-surface-scrim); opacity: 0; transition: opacity 0.15s;
|
|
472
|
-
}
|
|
473
|
-
.cpub-be-cover:hover .cpub-be-cover-overlay,
|
|
474
|
-
.cpub-be-cover:hover .cpub-be-cover-actions,
|
|
475
|
-
.cpub-be-cover:focus-within .cpub-be-cover-overlay,
|
|
476
|
-
.cpub-be-cover:focus-within .cpub-be-cover-actions { opacity: 1; }
|
|
477
|
-
.cpub-be-cover-btn {
|
|
478
|
-
font-size: 11px; padding: 6px 12px; background: var(--surface); border: var(--border-width-default) solid var(--border);
|
|
479
|
-
color: var(--text-dim); cursor: pointer; display: inline-flex; align-items: center; gap: 5px;
|
|
480
|
-
font-family: var(--font-mono); box-shadow: var(--shadow-sm);
|
|
481
|
-
}
|
|
482
|
-
.cpub-be-cover-btn.primary { background: var(--accent); color: var(--color-text-inverse); border-color: var(--accent); box-shadow: var(--shadow-sm); }
|
|
483
|
-
.cpub-be-cover-btn:hover { background: var(--surface2); }
|
|
484
|
-
.cpub-be-cover-btn.primary:hover { opacity: 0.9; background: var(--accent); }
|
|
485
|
-
|
|
486
|
-
/* Title & Subtitle in canvas */
|
|
487
|
-
.cpub-be-title {
|
|
488
|
-
width: 100%; border: none; outline: none; resize: none; background: transparent;
|
|
489
|
-
font-size: 28px; font-weight: 700; line-height: 1.25; color: var(--text);
|
|
490
|
-
padding: 24px 48px 0; font-family: var(--font-sans, system-ui);
|
|
491
|
-
}
|
|
492
|
-
.cpub-be-title::placeholder { color: var(--text-faint); }
|
|
493
|
-
.cpub-be-subtitle {
|
|
494
|
-
width: 100%; border: none; outline: none; resize: none; background: transparent;
|
|
495
|
-
font-size: 16px; font-weight: 400; line-height: 1.5; color: var(--text-dim);
|
|
496
|
-
padding: 8px 48px 0; font-family: var(--font-sans, system-ui);
|
|
497
|
-
}
|
|
498
|
-
.cpub-be-subtitle::placeholder { color: var(--text-faint); }
|
|
499
|
-
|
|
500
|
-
/* Byline */
|
|
501
|
-
.cpub-be-byline {
|
|
502
|
-
display: flex; align-items: center; gap: 10px; padding: 16px 48px 8px;
|
|
503
|
-
}
|
|
504
|
-
.cpub-be-byline-av {
|
|
505
|
-
width: 32px; height: 32px; border-radius: 50%; background: var(--accent);
|
|
506
|
-
display: flex; align-items: center; justify-content: center;
|
|
507
|
-
font-size: 11px; font-weight: 700; color: var(--color-text-inverse); flex-shrink: 0; border: var(--border-width-default) solid var(--border);
|
|
508
|
-
}
|
|
509
|
-
.cpub-be-byline-info { flex: 1; }
|
|
510
|
-
.cpub-be-byline-name { font-size: 13px; font-weight: 600; color: var(--text); }
|
|
511
|
-
.cpub-be-byline-meta { font-size: 11px; color: var(--text-faint); display: flex; align-items: center; gap: 4px; }
|
|
512
|
-
.cpub-be-sep { color: var(--text-faint); }
|
|
513
|
-
.cpub-be-category-tag { color: var(--accent); }
|
|
514
|
-
|
|
515
|
-
/* Word count bar */
|
|
516
|
-
.cpub-be-wc-bar {
|
|
517
|
-
display: flex; align-items: center; gap: 18px; padding: 10px 40px;
|
|
518
|
-
background: var(--surface); border-top: var(--border-width-default) solid var(--border); flex-shrink: 0;
|
|
519
|
-
}
|
|
520
|
-
.cpub-be-wc-stat {
|
|
521
|
-
font-family: var(--font-mono); font-size: 10px; color: var(--text-faint);
|
|
522
|
-
display: flex; align-items: center; gap: 5px;
|
|
523
|
-
}
|
|
524
|
-
.cpub-be-wc-stat i { font-size: 9px; }
|
|
525
|
-
.cpub-be-wc-stat span { color: var(--text-dim); }
|
|
526
|
-
.cpub-be-wc-spacer { flex: 1; }
|
|
527
|
-
|
|
528
|
-
/* Right panel extras */
|
|
529
|
-
.cpub-ep-input-mono { font-family: var(--font-mono); font-size: 11px; }
|
|
530
|
-
.cpub-ep-hint-right { text-align: right; display: block; }
|
|
531
|
-
.cpub-ep-optional { font-size: 9px; font-weight: 400; color: var(--text-faint); }
|
|
532
|
-
|
|
533
|
-
/* SEO card */
|
|
534
|
-
.cpub-be-seo-card {
|
|
535
|
-
background: var(--surface); border: var(--border-width-default) solid var(--border); padding: 14px;
|
|
536
|
-
font-family: Arial, sans-serif; box-shadow: var(--shadow-sm);
|
|
537
|
-
}
|
|
538
|
-
.cpub-be-seo-url { font-size: 11px; color: var(--green); margin-bottom: 4px; display: flex; align-items: center; gap: 4px; }
|
|
539
|
-
.cpub-be-seo-favicon {
|
|
540
|
-
width: 14px; height: 14px; background: var(--accent); display: inline-flex;
|
|
541
|
-
align-items: center; justify-content: center; font-size: 8px; color: var(--color-text-inverse);
|
|
542
|
-
font-weight: 700; flex-shrink: 0; border: var(--border-width-default) solid var(--border);
|
|
543
|
-
}
|
|
544
|
-
.cpub-be-seo-title { font-size: 15px; color: var(--accent); margin-bottom: 4px; line-height: 1.3; }
|
|
545
|
-
.cpub-be-seo-desc { font-size: 12px; color: var(--text-dim); line-height: 1.45; }
|
|
546
|
-
|
|
547
|
-
/* Schedule toggle */
|
|
548
|
-
.cpub-be-schedule-row { display: flex; align-items: center; gap: 8px; cursor: pointer; }
|
|
549
|
-
.cpub-be-toggle-switch {
|
|
550
|
-
position: relative; width: 30px; height: 16px; flex-shrink: 0;
|
|
551
|
-
}
|
|
552
|
-
.cpub-be-toggle-switch input { display: none; }
|
|
553
|
-
.cpub-be-toggle-track {
|
|
554
|
-
display: block; width: 100%; height: 100%; background: var(--surface3);
|
|
555
|
-
border: var(--border-width-default) solid var(--border); cursor: pointer; transition: background 0.15s; position: relative;
|
|
556
|
-
}
|
|
557
|
-
.cpub-be-toggle-track::after {
|
|
558
|
-
content: ''; position: absolute; width: 8px; height: 8px;
|
|
559
|
-
background: var(--text-faint); top: 2px; left: 2px; transition: all 0.15s;
|
|
560
|
-
}
|
|
561
|
-
.cpub-be-toggle-switch input:checked + .cpub-be-toggle-track { background: var(--accent); border-color: var(--border); }
|
|
562
|
-
.cpub-be-toggle-switch input:checked + .cpub-be-toggle-track::after { left: 16px; background: var(--color-text-inverse); }
|
|
563
|
-
.cpub-be-toggle-label { font-size: 12px; color: var(--text-dim); }
|
|
564
|
-
|
|
565
|
-
/* Author row */
|
|
566
|
-
.cpub-be-author-row {
|
|
567
|
-
display: flex; align-items: center; gap: 10px; padding: 10px;
|
|
568
|
-
background: var(--surface); border: var(--border-width-default) solid var(--border); box-shadow: var(--shadow-sm);
|
|
569
|
-
}
|
|
570
|
-
.cpub-be-author-av {
|
|
571
|
-
width: 32px; height: 32px; border-radius: 50%; background: var(--accent);
|
|
572
|
-
display: flex; align-items: center; justify-content: center;
|
|
573
|
-
font-size: 11px; font-weight: 700; color: var(--color-text-inverse); flex-shrink: 0; border: var(--border-width-default) solid var(--border);
|
|
574
|
-
}
|
|
575
|
-
.cpub-be-author-info { flex: 1; }
|
|
576
|
-
.cpub-be-author-name { font-size: 12px; font-weight: 600; color: var(--text); }
|
|
577
|
-
.cpub-be-author-role { font-size: 10px; color: var(--text-faint); font-family: var(--font-mono); }
|
|
578
|
-
.cpub-be-author-badge {
|
|
579
|
-
font-family: var(--font-mono); font-size: 9px; padding: 2px 6px;
|
|
580
|
-
background: var(--accent-bg); border: var(--border-width-default) solid var(--accent); color: var(--accent);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/* OG thumb */
|
|
584
|
-
.cpub-be-og-thumb {
|
|
585
|
-
width: 100%; aspect-ratio: 1200/630; background: var(--surface2);
|
|
586
|
-
border: var(--border-width-default) solid var(--border); overflow: hidden; position: relative; cursor: pointer;
|
|
587
|
-
background-image: linear-gradient(var(--border2) 1px, transparent 1px), linear-gradient(90deg, var(--border2) 1px, transparent 1px);
|
|
588
|
-
background-size: 20px 20px;
|
|
589
|
-
}
|
|
590
|
-
.cpub-be-og-overlay {
|
|
591
|
-
position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center;
|
|
592
|
-
justify-content: center; gap: 5px; background: var(--color-surface-scrim);
|
|
593
|
-
opacity: 0; transition: opacity 0.15s;
|
|
594
|
-
}
|
|
595
|
-
.cpub-be-og-thumb:hover .cpub-be-og-overlay { opacity: 1; }
|
|
596
|
-
.cpub-be-og-overlay i { font-size: 18px; color: var(--text-dim); }
|
|
597
|
-
.cpub-be-og-overlay span { font-size: 11px; color: var(--text-dim); font-family: var(--font-mono); }
|
|
598
|
-
|
|
599
|
-
.cpub-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
|
|
600
|
-
|
|
601
|
-
/* Mobile sidebar toggles */
|
|
602
|
-
.cpub-be-mobile-toggles { display: none; }
|
|
603
|
-
.cpub-be-mobile-overlay { display: none; }
|
|
604
|
-
|
|
605
|
-
@media (max-width: 1200px) {
|
|
606
|
-
.cpub-be-library {
|
|
607
|
-
position: fixed; top: 0; bottom: 0; left: 0; z-index: 200;
|
|
608
|
-
transform: translateX(-100%); transition: transform 0.2s ease;
|
|
609
|
-
}
|
|
610
|
-
.cpub-be-library.cpub-be-sidebar-open { transform: translateX(0); }
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
@media (max-width: 1024px) {
|
|
614
|
-
.cpub-be-right {
|
|
615
|
-
position: fixed; top: 0; bottom: 0; right: 0; z-index: 200;
|
|
616
|
-
transform: translateX(100%); transition: transform 0.2s ease;
|
|
617
|
-
}
|
|
618
|
-
.cpub-be-right.cpub-be-sidebar-open { transform: translateX(0); }
|
|
619
|
-
|
|
620
|
-
.cpub-be-mobile-toggles {
|
|
621
|
-
display: flex; position: fixed; bottom: 16px; right: 16px;
|
|
622
|
-
gap: 8px; z-index: 100;
|
|
623
|
-
}
|
|
624
|
-
.cpub-be-mobile-btn {
|
|
625
|
-
width: 44px; height: 44px; border: var(--border-width-default) solid var(--border); background: var(--surface);
|
|
626
|
-
color: var(--text-dim); font-size: 16px; cursor: pointer;
|
|
627
|
-
display: flex; align-items: center; justify-content: center;
|
|
628
|
-
box-shadow: var(--shadow-md);
|
|
629
|
-
}
|
|
630
|
-
.cpub-be-mobile-btn:hover { background: var(--surface2); color: var(--text); }
|
|
631
|
-
.cpub-be-mobile-overlay {
|
|
632
|
-
display: block; position: fixed; inset: 0;
|
|
633
|
-
background: var(--color-surface-overlay-light); z-index: 199;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/* Touch devices: always show cover overlays and OG overlay */
|
|
638
|
-
@media (hover: none) {
|
|
639
|
-
.cpub-be-cover-overlay,
|
|
640
|
-
.cpub-be-cover-actions { opacity: 1; }
|
|
641
|
-
.cpub-be-og-overlay { opacity: 1; }
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/* Banner preview */
|
|
645
|
-
.cpub-be-banner-preview { position: relative; margin-bottom: 8px; }
|
|
646
|
-
.cpub-be-banner-img { width: 100%; height: 80px; object-fit: cover; display: block; border: var(--border-width-default) solid var(--border); }
|
|
647
|
-
.cpub-be-banner-actions { display: flex; gap: 6px; margin-top: 6px; }
|
|
648
|
-
</style>
|