@commonpub/layer 0.4.6 → 0.4.7
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/SiteLogo.vue +19 -12
- package/components/editors/BlockWrapper.vue +3 -3
- package/components/editors/DocsPageTree.vue +559 -0
- package/components/editors/EditorShell.vue +62 -34
- package/components/views/ExplainerView.vue +14 -1
- package/package.json +5 -5
- package/pages/docs/[siteSlug]/[...pagePath].vue +20 -6
- package/pages/docs/[siteSlug]/edit.vue +1021 -383
- package/pages/index.vue +30 -22
- package/server/api/docs/[siteSlug]/pages/[pageId].get.ts +35 -2
- package/server/api/docs/[siteSlug]/pages/index.get.ts +14 -1
- package/server/api/docs/migrate-content.post.ts +101 -0
|
@@ -1,480 +1,1118 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
import type { BlockTuple } from '@commonpub/editor';
|
|
3
|
+
import type { BlockTypeGroup } from '../../../components/editors/BlockPicker.vue';
|
|
4
|
+
import type { PageTreeItem } from '../../../components/editors/DocsPageTree.vue';
|
|
5
|
+
|
|
6
|
+
definePageMeta({ layout: false, middleware: 'auth' });
|
|
3
7
|
|
|
4
8
|
const route = useRoute();
|
|
5
9
|
const siteSlug = computed(() => route.params.siteSlug as string);
|
|
10
|
+
const { show: toast } = useToast();
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
const { data:
|
|
12
|
+
// ═══ DATA FETCHING ═══
|
|
13
|
+
const { data: site, refresh: refreshSite } = await useFetch(() => `/api/docs/${siteSlug.value}`);
|
|
14
|
+
const { data: rawPages, refresh: refreshPages } = await useFetch(() => `/api/docs/${siteSlug.value}/pages`);
|
|
9
15
|
|
|
10
16
|
useSeoMeta({ title: () => `Edit ${site.value?.name ?? 'Docs'} — ${useSiteName()}` });
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
interface DocsPage {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
content: string | BlockTuple[];
|
|
23
|
+
sortOrder: number;
|
|
24
|
+
parentId: string | null;
|
|
25
|
+
}
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
const pages = computed<DocsPage[]>(() => (rawPages.value as DocsPage[]) ?? []);
|
|
28
|
+
const treePages = computed<PageTreeItem[]>(() =>
|
|
29
|
+
pages.value.map(p => ({
|
|
30
|
+
id: p.id,
|
|
31
|
+
title: p.title,
|
|
32
|
+
slug: p.slug,
|
|
33
|
+
parentId: p.parentId,
|
|
34
|
+
sortOrder: p.sortOrder,
|
|
35
|
+
})),
|
|
36
|
+
);
|
|
18
37
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
// ═══ BLOCK EDITOR ═══
|
|
39
|
+
const blockEditor = useBlockEditor();
|
|
40
|
+
|
|
41
|
+
// Docs-specific block palette — no explainer/project blocks
|
|
42
|
+
const blockTypes: BlockTypeGroup[] = [
|
|
43
|
+
{
|
|
44
|
+
name: 'Text',
|
|
45
|
+
blocks: [
|
|
46
|
+
{ type: 'paragraph', label: 'Paragraph', icon: 'fa-align-left', description: 'Body text' },
|
|
47
|
+
{ type: 'heading', label: 'Heading', icon: 'fa-heading', description: 'Section heading (H2-H4)' },
|
|
48
|
+
{ type: 'blockquote', label: 'Quote', icon: 'fa-quote-left', description: 'Blockquote with attribution' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Code & Media',
|
|
53
|
+
blocks: [
|
|
54
|
+
{ type: 'code_block', label: 'Code Block', icon: 'fa-code', description: 'Syntax highlighted code' },
|
|
55
|
+
{ type: 'image', label: 'Image', icon: 'fa-image', description: 'Upload or embed image' },
|
|
56
|
+
{ type: 'embed', label: 'Embed', icon: 'fa-globe', description: 'External embed' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Layout',
|
|
61
|
+
blocks: [
|
|
62
|
+
{ type: 'callout', label: 'Callout', icon: 'fa-circle-info', description: 'Info, tip, warning, or danger', attrs: { variant: 'info' } },
|
|
63
|
+
{ type: 'horizontal_rule', label: 'Divider', icon: 'fa-minus', description: 'Visual separator' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// ═══ PAGE SELECTION ═══
|
|
69
|
+
const selectedPageId = ref<string | null>(null);
|
|
70
|
+
const selectedPage = computed<DocsPage | null>(() =>
|
|
71
|
+
pages.value.find(p => p.id === selectedPageId.value) ?? null,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Page properties (right panel)
|
|
75
|
+
const pageSlug = ref('');
|
|
76
|
+
const savingPage = ref(false);
|
|
77
|
+
const autoSaveTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
|
78
|
+
const autoSaveStatus = ref<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
|
79
|
+
const isDirty = ref(false);
|
|
80
|
+
const isLoadingPage = ref(false); // Guard: suppresses dirty-marking during page load
|
|
81
|
+
const markdownNotice = ref<string | null>(null); // Shows notice when markdown page is converted
|
|
82
|
+
|
|
83
|
+
// Load page content when selecting
|
|
84
|
+
async function selectPage(pageId: string): Promise<void> {
|
|
85
|
+
// Save current page first if dirty
|
|
86
|
+
if (isDirty.value && selectedPageId.value) {
|
|
87
|
+
await saveCurrentPage();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
selectedPageId.value = pageId;
|
|
91
|
+
const page = pages.value.find(p => p.id === pageId);
|
|
92
|
+
if (!page) return;
|
|
93
|
+
|
|
94
|
+
// Guard: suppress dirty-marking during content load
|
|
95
|
+
isLoadingPage.value = true;
|
|
96
|
+
|
|
97
|
+
// Load content into block editor
|
|
98
|
+
if (Array.isArray(page.content)) {
|
|
99
|
+
blockEditor.fromBlockTuples(page.content as BlockTuple[]);
|
|
100
|
+
} else if (typeof page.content === 'string' && page.content.trim()) {
|
|
101
|
+
// Legacy markdown content — convert to blocks
|
|
102
|
+
markdownNotice.value = page.title;
|
|
103
|
+
blockEditor.clearBlocks();
|
|
104
|
+
const { importMarkdown } = useMarkdownImport(blockEditor);
|
|
105
|
+
await importMarkdown(page.content, 'replace');
|
|
106
|
+
} else {
|
|
107
|
+
blockEditor.clearBlocks();
|
|
108
|
+
blockEditor.addBlock('paragraph');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Load properties
|
|
112
|
+
pageSlug.value = page.slug ?? '';
|
|
113
|
+
isDirty.value = false;
|
|
114
|
+
autoSaveStatus.value = 'idle';
|
|
115
|
+
|
|
116
|
+
// Release guard after watchers have flushed
|
|
117
|
+
await nextTick();
|
|
118
|
+
isLoadingPage.value = false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ══�� SAVING ═══
|
|
122
|
+
async function saveCurrentPage(): Promise<void> {
|
|
123
|
+
if (!selectedPageId.value) return;
|
|
124
|
+
savingPage.value = true;
|
|
125
|
+
autoSaveStatus.value = 'saving';
|
|
24
126
|
|
|
25
|
-
async function saveSiteSettings(): Promise<void> {
|
|
26
|
-
savingSite.value = true;
|
|
27
127
|
try {
|
|
28
|
-
await $fetch(`/api/docs/${siteSlug.value}`, {
|
|
128
|
+
await $fetch(`/api/docs/${siteSlug.value}/pages/${selectedPageId.value}`, {
|
|
29
129
|
method: 'PUT',
|
|
30
|
-
body: {
|
|
130
|
+
body: {
|
|
131
|
+
title: selectedPage.value?.title,
|
|
132
|
+
slug: pageSlug.value,
|
|
133
|
+
content: blockEditor.toBlockTuples(),
|
|
134
|
+
},
|
|
31
135
|
});
|
|
32
|
-
|
|
33
|
-
|
|
136
|
+
isDirty.value = false;
|
|
137
|
+
autoSaveStatus.value = 'saved';
|
|
138
|
+
// Refresh pages list to keep tree in sync
|
|
139
|
+
await refreshPages();
|
|
34
140
|
} catch (err: unknown) {
|
|
35
|
-
|
|
141
|
+
autoSaveStatus.value = 'error';
|
|
142
|
+
toast(err instanceof Error ? err.message : 'Failed to save page', 'error');
|
|
36
143
|
} finally {
|
|
37
|
-
|
|
144
|
+
savingPage.value = false;
|
|
38
145
|
}
|
|
39
146
|
}
|
|
40
147
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
148
|
+
// Autosave: debounce 5 seconds for docs (shorter than article 30s)
|
|
149
|
+
function scheduleAutoSave(): void {
|
|
150
|
+
if (autoSaveTimer.value) clearTimeout(autoSaveTimer.value);
|
|
151
|
+
autoSaveTimer.value = setTimeout(() => {
|
|
152
|
+
if (isDirty.value && selectedPageId.value) {
|
|
153
|
+
saveCurrentPage();
|
|
154
|
+
}
|
|
155
|
+
}, 5000);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Watch for changes — skip during page load to avoid false dirty
|
|
159
|
+
watch(() => blockEditor.blocks.value, () => {
|
|
160
|
+
if (isLoadingPage.value) return;
|
|
161
|
+
isDirty.value = true;
|
|
162
|
+
scheduleAutoSave();
|
|
163
|
+
}, { deep: true });
|
|
164
|
+
|
|
165
|
+
watch(pageSlug, () => {
|
|
166
|
+
if (isLoadingPage.value) return;
|
|
167
|
+
isDirty.value = true;
|
|
168
|
+
scheduleAutoSave();
|
|
169
|
+
});
|
|
48
170
|
|
|
49
|
-
|
|
50
|
-
|
|
171
|
+
// Keyboard shortcut: Cmd+S to save
|
|
172
|
+
function handleKeydown(e: KeyboardEvent): void {
|
|
173
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
saveCurrentPage();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Warn about unsaved changes on navigation
|
|
180
|
+
function handleBeforeUnload(e: BeforeUnloadEvent): void {
|
|
181
|
+
if (isDirty.value) {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
}
|
|
51
184
|
}
|
|
52
185
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
186
|
+
onMounted(() => {
|
|
187
|
+
document.addEventListener('keydown', handleKeydown);
|
|
188
|
+
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
189
|
+
|
|
190
|
+
// Auto-select page from ?page= query or first page
|
|
191
|
+
const requestedPage = route.query.page as string | undefined;
|
|
192
|
+
if (requestedPage && pages.value.length > 0) {
|
|
193
|
+
const match = pages.value.find(p => p.slug === requestedPage || p.id === requestedPage);
|
|
194
|
+
if (match) {
|
|
195
|
+
selectPage(match.id);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (pages.value.length > 0 && !selectedPageId.value) {
|
|
200
|
+
const first = [...pages.value].sort((a, b) => a.sortOrder - b.sortOrder)[0];
|
|
201
|
+
if (first) selectPage(first.id);
|
|
56
202
|
}
|
|
57
203
|
});
|
|
58
204
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
205
|
+
onUnmounted(() => {
|
|
206
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
207
|
+
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
208
|
+
if (autoSaveTimer.value) clearTimeout(autoSaveTimer.value);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ═══ PAGE TREE ACTIONS ═══
|
|
212
|
+
const pendingReparent = ref(false);
|
|
213
|
+
async function handleCreatePage(parentId: string | null, title: string): Promise<void> {
|
|
62
214
|
try {
|
|
63
|
-
|
|
215
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
216
|
+
const result = await $fetch(`/api/docs/${siteSlug.value}/pages`, {
|
|
64
217
|
method: 'POST',
|
|
65
218
|
body: {
|
|
66
|
-
title
|
|
67
|
-
slug
|
|
68
|
-
content:
|
|
69
|
-
parentId:
|
|
219
|
+
title,
|
|
220
|
+
slug,
|
|
221
|
+
content: [['paragraph', { html: '' }]],
|
|
222
|
+
parentId: parentId ?? undefined,
|
|
70
223
|
sortOrder: (pages.value?.length ?? 0) + 1,
|
|
71
224
|
},
|
|
72
225
|
});
|
|
73
|
-
toast('Page created', 'success');
|
|
74
|
-
newPageTitle.value = '';
|
|
75
|
-
newPageSlug.value = '';
|
|
76
|
-
newPageContent.value = '';
|
|
77
|
-
newPageParentId.value = null;
|
|
78
|
-
showNewPage.value = false;
|
|
79
226
|
await refreshPages();
|
|
227
|
+
if (result && typeof result === 'object' && 'id' in result) {
|
|
228
|
+
selectPage((result as { id: string }).id);
|
|
229
|
+
}
|
|
230
|
+
toast('Page created', 'success');
|
|
80
231
|
} catch (err: unknown) {
|
|
81
232
|
toast(err instanceof Error ? err.message : 'Failed to create page', 'error');
|
|
82
|
-
} finally {
|
|
83
|
-
savingPage.value = false;
|
|
84
233
|
}
|
|
85
234
|
}
|
|
86
235
|
|
|
87
|
-
|
|
88
|
-
async function deleteSite(): Promise<void> {
|
|
89
|
-
if (!confirm('Delete this entire docs site? All pages and versions will be permanently deleted.')) return;
|
|
236
|
+
async function handleRenamePage(pageId: string, newTitle: string): Promise<void> {
|
|
90
237
|
try {
|
|
91
|
-
await $fetch(`/api/docs/${siteSlug.value}`, {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
238
|
+
await $fetch(`/api/docs/${siteSlug.value}/pages/${pageId}`, {
|
|
239
|
+
method: 'PUT',
|
|
240
|
+
body: { title: newTitle },
|
|
241
|
+
});
|
|
242
|
+
await refreshPages();
|
|
243
|
+
toast('Page renamed', 'success');
|
|
244
|
+
} catch (err: unknown) {
|
|
245
|
+
toast(err instanceof Error ? err.message : 'Failed to rename', 'error');
|
|
96
246
|
}
|
|
97
247
|
}
|
|
98
248
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
249
|
+
async function handleDeletePage(pageId: string): Promise<void> {
|
|
250
|
+
try {
|
|
251
|
+
await $fetch(`/api/docs/${siteSlug.value}/pages/${pageId}`, { method: 'DELETE' });
|
|
252
|
+
if (selectedPageId.value === pageId) {
|
|
253
|
+
selectedPageId.value = null;
|
|
254
|
+
blockEditor.clearBlocks();
|
|
255
|
+
}
|
|
256
|
+
await refreshPages();
|
|
257
|
+
toast('Page deleted', 'success');
|
|
258
|
+
} catch (err: unknown) {
|
|
259
|
+
toast(err instanceof Error ? err.message : 'Failed to delete', 'error');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
104
262
|
|
|
105
|
-
async function
|
|
106
|
-
|
|
107
|
-
savingVersion.value = true;
|
|
263
|
+
async function handleReorder(pageIds: string[]): Promise<void> {
|
|
264
|
+
pendingReparent.value = false; // Cancel reparent's deferred refresh
|
|
108
265
|
try {
|
|
109
|
-
await $fetch(`/api/docs/${siteSlug.value}/
|
|
266
|
+
await $fetch(`/api/docs/${siteSlug.value}/pages/reorder`, {
|
|
110
267
|
method: 'POST',
|
|
111
|
-
body: {
|
|
268
|
+
body: { pageIds },
|
|
112
269
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
showNewVersion.value = false;
|
|
117
|
-
await refreshSite();
|
|
118
|
-
} catch (err: unknown) {
|
|
119
|
-
toast(err instanceof Error ? err.message : 'Failed to create version', 'error');
|
|
120
|
-
} finally {
|
|
121
|
-
savingVersion.value = false;
|
|
270
|
+
await refreshPages();
|
|
271
|
+
} catch {
|
|
272
|
+
toast('Failed to reorder', 'error');
|
|
122
273
|
}
|
|
123
274
|
}
|
|
124
275
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
276
|
+
async function handleReparent(pageId: string, newParentId: string | null): Promise<void> {
|
|
277
|
+
try {
|
|
278
|
+
await $fetch(`/api/docs/${siteSlug.value}/pages/${pageId}`, {
|
|
279
|
+
method: 'PUT',
|
|
280
|
+
body: { parentId: newParentId ?? null },
|
|
281
|
+
});
|
|
282
|
+
// Don't refresh here — if reorder follows immediately, let reorder refresh
|
|
283
|
+
// If reparent is standalone (drag inside), refresh
|
|
284
|
+
pendingReparent.value = true;
|
|
285
|
+
setTimeout(async () => {
|
|
286
|
+
if (pendingReparent.value) {
|
|
287
|
+
pendingReparent.value = false;
|
|
288
|
+
await refreshPages();
|
|
289
|
+
}
|
|
290
|
+
}, 100);
|
|
291
|
+
} catch {
|
|
292
|
+
toast('Failed to move page', 'error');
|
|
293
|
+
}
|
|
133
294
|
}
|
|
134
295
|
|
|
135
|
-
const editingPageId = ref<string | null>(null);
|
|
136
|
-
const editPageContent = ref('');
|
|
137
|
-
const editPageTitle = ref('');
|
|
138
|
-
const editPageParentId = ref<string | null>(null);
|
|
139
|
-
const savingEdit = ref(false);
|
|
140
|
-
const showPreview = ref(false);
|
|
141
296
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
297
|
+
// ═══ PAGE TITLE EDITING ═══
|
|
298
|
+
const editingTitle = ref(false);
|
|
299
|
+
const editTitleValue = ref('');
|
|
300
|
+
|
|
301
|
+
function startEditTitle(): void {
|
|
302
|
+
if (!selectedPage.value) return;
|
|
303
|
+
editTitleValue.value = selectedPage.value.title;
|
|
304
|
+
editingTitle.value = true;
|
|
305
|
+
nextTick(() => {
|
|
306
|
+
const input = document.querySelector('.cpub-docs-title-input') as HTMLInputElement | null;
|
|
307
|
+
input?.focus();
|
|
308
|
+
input?.select();
|
|
309
|
+
});
|
|
148
310
|
}
|
|
149
311
|
|
|
150
|
-
async function
|
|
151
|
-
if (!
|
|
152
|
-
|
|
312
|
+
async function confirmEditTitle(): Promise<void> {
|
|
313
|
+
if (!selectedPageId.value || !editTitleValue.value.trim()) {
|
|
314
|
+
editingTitle.value = false;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
await handleRenamePage(selectedPageId.value, editTitleValue.value.trim());
|
|
318
|
+
editingTitle.value = false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ═══ WORD COUNT ═══
|
|
322
|
+
const wordCount = computed(() => {
|
|
323
|
+
let count = 0;
|
|
324
|
+
for (const block of blockEditor.blocks.value) {
|
|
325
|
+
const html = (block.content.html as string) || (block.content.text as string) || (block.content.code as string) || '';
|
|
326
|
+
count += html.replace(/<[^>]*>/g, '').split(/\s+/).filter(Boolean).length;
|
|
327
|
+
}
|
|
328
|
+
return count;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ═��═ MARKDOWN IMPORT ═══
|
|
332
|
+
const showImportDialog = ref(false);
|
|
333
|
+
|
|
334
|
+
function handleMarkdownImport(md: string, mode: 'append' | 'replace'): void {
|
|
335
|
+
const { importMarkdown } = useMarkdownImport(blockEditor);
|
|
336
|
+
importMarkdown(md, mode);
|
|
337
|
+
showImportDialog.value = false;
|
|
338
|
+
isDirty.value = true;
|
|
339
|
+
scheduleAutoSave();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ═══ SITE SETTINGS ═══
|
|
343
|
+
const showSettings = ref(false);
|
|
344
|
+
const settingsName = ref('');
|
|
345
|
+
const settingsDesc = ref('');
|
|
346
|
+
const savingSettings = ref(false);
|
|
347
|
+
const newVersion = ref('');
|
|
348
|
+
const newVersionDefault = ref(false);
|
|
349
|
+
const savingVersion = ref(false);
|
|
350
|
+
|
|
351
|
+
interface DocsSiteVersion {
|
|
352
|
+
id: string;
|
|
353
|
+
version: string;
|
|
354
|
+
isDefault: boolean;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
watch(site, (s) => {
|
|
358
|
+
if (!s) return;
|
|
359
|
+
settingsName.value = (s as Record<string, unknown>).name as string ?? '';
|
|
360
|
+
settingsDesc.value = (s as Record<string, unknown>).description as string ?? '';
|
|
361
|
+
}, { immediate: true });
|
|
362
|
+
|
|
363
|
+
async function saveSiteSettings(): Promise<void> {
|
|
364
|
+
savingSettings.value = true;
|
|
153
365
|
try {
|
|
154
|
-
await $fetch(`/api/docs/${siteSlug.value}
|
|
366
|
+
await $fetch(`/api/docs/${siteSlug.value}`, {
|
|
155
367
|
method: 'PUT',
|
|
156
|
-
body: {
|
|
157
|
-
title: editPageTitle.value,
|
|
158
|
-
content: editPageContent.value,
|
|
159
|
-
parentId: editPageParentId.value || undefined,
|
|
160
|
-
},
|
|
368
|
+
body: { name: settingsName.value, description: settingsDesc.value },
|
|
161
369
|
});
|
|
162
|
-
toast('
|
|
163
|
-
|
|
164
|
-
await refreshPages();
|
|
370
|
+
toast('Site settings updated', 'success');
|
|
371
|
+
await refreshSite();
|
|
165
372
|
} catch (err: unknown) {
|
|
166
|
-
toast(err instanceof Error ? err.message : 'Failed to update
|
|
373
|
+
toast(err instanceof Error ? err.message : 'Failed to update settings', 'error');
|
|
167
374
|
} finally {
|
|
168
|
-
|
|
375
|
+
savingSettings.value = false;
|
|
169
376
|
}
|
|
170
377
|
}
|
|
171
378
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (!confirm('Delete this page? This cannot be undone.')) return;
|
|
379
|
+
async function deleteSite(): Promise<void> {
|
|
380
|
+
if (!confirm('Delete this entire docs site? All pages and versions will be permanently deleted.')) return;
|
|
175
381
|
try {
|
|
176
|
-
await $fetch(`/api/docs/${siteSlug.value}
|
|
177
|
-
toast('
|
|
178
|
-
|
|
179
|
-
await refreshPages();
|
|
382
|
+
await $fetch(`/api/docs/${siteSlug.value}`, { method: 'DELETE' });
|
|
383
|
+
toast('Docs site deleted', 'success');
|
|
384
|
+
await navigateTo('/docs');
|
|
180
385
|
} catch {
|
|
181
|
-
toast('Failed to delete
|
|
386
|
+
toast('Failed to delete docs site', 'error');
|
|
182
387
|
}
|
|
183
388
|
}
|
|
184
389
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const allPages = [...(pages.value as DocsPage[])].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
189
|
-
const idx = allPages.findIndex(p => p.id === pageId);
|
|
190
|
-
if (idx === -1) return;
|
|
191
|
-
if (direction === 'up' && idx === 0) return;
|
|
192
|
-
if (direction === 'down' && idx === allPages.length - 1) return;
|
|
193
|
-
|
|
194
|
-
const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
|
|
195
|
-
[allPages[idx], allPages[swapIdx]] = [allPages[swapIdx]!, allPages[idx]!];
|
|
196
|
-
|
|
390
|
+
async function createVersion(): Promise<void> {
|
|
391
|
+
if (!newVersion.value.trim()) return;
|
|
392
|
+
savingVersion.value = true;
|
|
197
393
|
try {
|
|
198
|
-
await $fetch(`/api/docs/${siteSlug.value}/
|
|
394
|
+
await $fetch(`/api/docs/${siteSlug.value}/versions`, {
|
|
199
395
|
method: 'POST',
|
|
200
|
-
body: {
|
|
396
|
+
body: { version: newVersion.value, isDefault: newVersionDefault.value },
|
|
201
397
|
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (!textarea) return;
|
|
211
|
-
const start = textarea.selectionStart;
|
|
212
|
-
const end = textarea.selectionEnd;
|
|
213
|
-
const selected = textarea.value.substring(start, end);
|
|
214
|
-
const replacement = before + (selected || 'text') + after;
|
|
215
|
-
textarea.setRangeText(replacement, start, end, 'select');
|
|
216
|
-
textarea.focus();
|
|
217
|
-
|
|
218
|
-
// Update the reactive value
|
|
219
|
-
if (editingPageId.value) {
|
|
220
|
-
editPageContent.value = textarea.value;
|
|
221
|
-
} else {
|
|
222
|
-
newPageContent.value = textarea.value;
|
|
398
|
+
toast('Version created', 'success');
|
|
399
|
+
newVersion.value = '';
|
|
400
|
+
newVersionDefault.value = false;
|
|
401
|
+
await refreshSite();
|
|
402
|
+
} catch (err: unknown) {
|
|
403
|
+
toast(err instanceof Error ? err.message : 'Failed to create version', 'error');
|
|
404
|
+
} finally {
|
|
405
|
+
savingVersion.value = false;
|
|
223
406
|
}
|
|
224
407
|
}
|
|
225
|
-
|
|
226
|
-
// Template refs for markdown toolbar
|
|
227
|
-
const newContentRef = useTemplateRef<HTMLTextAreaElement>('newContent');
|
|
228
|
-
const editContentRef = useTemplateRef<HTMLTextAreaElement>('editContent');
|
|
229
|
-
|
|
230
|
-
const sortedPages = computed(() => {
|
|
231
|
-
if (!pages.value) return [];
|
|
232
|
-
return [...(pages.value as DocsPage[])].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
233
|
-
});
|
|
234
408
|
</script>
|
|
235
409
|
|
|
236
410
|
<template>
|
|
237
|
-
<div class="docs-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<
|
|
242
|
-
</
|
|
411
|
+
<div class="cpub-docs-editor">
|
|
412
|
+
<!-- Top bar -->
|
|
413
|
+
<div class="cpub-docs-topbar">
|
|
414
|
+
<NuxtLink :to="`/docs/${siteSlug}`" class="cpub-docs-back" aria-label="Back to docs">
|
|
415
|
+
<i class="fa-solid fa-arrow-left" />
|
|
416
|
+
</NuxtLink>
|
|
417
|
+
<span class="cpub-docs-topbar-title">{{ site?.name ?? 'Docs' }}</span>
|
|
418
|
+
<div class="cpub-docs-topbar-spacer" />
|
|
419
|
+
<button
|
|
420
|
+
v-if="selectedPageId"
|
|
421
|
+
class="cpub-docs-toolbar-btn"
|
|
422
|
+
title="Import Markdown"
|
|
423
|
+
@click="showImportDialog = true"
|
|
424
|
+
>
|
|
425
|
+
<i class="fa-brands fa-markdown" />
|
|
426
|
+
</button>
|
|
427
|
+
<button class="cpub-docs-toolbar-btn" title="Site Settings" @click="showSettings = true">
|
|
428
|
+
<i class="fa-solid fa-gear" />
|
|
429
|
+
</button>
|
|
430
|
+
<button
|
|
431
|
+
class="cpub-docs-toolbar-btn"
|
|
432
|
+
:class="{ 'cpub-docs-toolbar-btn-saving': savingPage }"
|
|
433
|
+
:disabled="!isDirty || savingPage"
|
|
434
|
+
@click="saveCurrentPage"
|
|
435
|
+
>
|
|
436
|
+
<i class="fa-solid" :class="savingPage ? 'fa-spinner fa-spin' : 'fa-floppy-disk'" />
|
|
437
|
+
<span>{{ savingPage ? 'Saving' : isDirty ? 'Save' : 'Saved' }}</span>
|
|
438
|
+
</button>
|
|
243
439
|
</div>
|
|
244
440
|
|
|
245
|
-
<!--
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
<label class="form-label">Name</label>
|
|
253
|
-
<input v-model="editSiteName" class="edit-input" />
|
|
254
|
-
</div>
|
|
255
|
-
<div class="form-field">
|
|
256
|
-
<label class="form-label">Description</label>
|
|
257
|
-
<textarea v-model="editSiteDesc" class="edit-textarea" rows="2" />
|
|
441
|
+
<!-- 3-panel editor -->
|
|
442
|
+
<EditorsEditorShell :show-left-sidebar="true" :show-right-sidebar="!!selectedPageId">
|
|
443
|
+
<!-- LEFT: Page tree -->
|
|
444
|
+
<template #left>
|
|
445
|
+
<div class="cpub-docs-left-header">
|
|
446
|
+
<span class="cpub-docs-left-label">Pages</span>
|
|
447
|
+
<span class="cpub-docs-page-count">{{ pages.length }}</span>
|
|
258
448
|
</div>
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
<!-- ═══ PAGES ═══ -->
|
|
271
|
-
<section class="edit-section">
|
|
272
|
-
<div class="section-header">
|
|
273
|
-
<h2 class="section-heading"><i class="fa-solid fa-file-lines"></i> Pages</h2>
|
|
274
|
-
<button class="cpub-btn cpub-btn-sm" @click="showNewPage = !showNewPage">
|
|
275
|
-
<i class="fa-solid fa-plus"></i> Add Page
|
|
276
|
-
</button>
|
|
277
|
-
</div>
|
|
449
|
+
<EditorsDocsPageTree
|
|
450
|
+
:pages="treePages"
|
|
451
|
+
:selected-page-id="selectedPageId"
|
|
452
|
+
@select="selectPage"
|
|
453
|
+
@create="handleCreatePage"
|
|
454
|
+
@rename="handleRenamePage"
|
|
455
|
+
@delete="handleDeletePage"
|
|
456
|
+
@reorder="handleReorder"
|
|
457
|
+
@reparent="handleReparent"
|
|
458
|
+
/>
|
|
459
|
+
</template>
|
|
278
460
|
|
|
279
|
-
<!--
|
|
280
|
-
<
|
|
281
|
-
<div class="
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
<input
|
|
461
|
+
<!-- CENTER: Block editor -->
|
|
462
|
+
<template #default>
|
|
463
|
+
<div v-if="selectedPage" class="cpub-docs-center">
|
|
464
|
+
<!-- Page title -->
|
|
465
|
+
<div class="cpub-docs-title-area">
|
|
466
|
+
<input
|
|
467
|
+
v-if="editingTitle"
|
|
468
|
+
v-model="editTitleValue"
|
|
469
|
+
class="cpub-docs-title-input"
|
|
470
|
+
@keydown.enter="confirmEditTitle"
|
|
471
|
+
@keydown.escape="editingTitle = false"
|
|
472
|
+
@blur="confirmEditTitle"
|
|
473
|
+
/>
|
|
474
|
+
<h1 v-else class="cpub-docs-title" @click="startEditTitle">
|
|
475
|
+
{{ selectedPage.title || 'Untitled Page' }}
|
|
476
|
+
</h1>
|
|
285
477
|
</div>
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
478
|
+
|
|
479
|
+
<!-- Markdown conversion notice -->
|
|
480
|
+
<div v-if="markdownNotice" class="cpub-docs-notice">
|
|
481
|
+
<i class="fa-solid fa-circle-info" />
|
|
482
|
+
<span>"{{ markdownNotice }}" was converted from markdown to blocks. Saving will store the block format.</span>
|
|
483
|
+
<button class="cpub-docs-notice-dismiss" @click="markdownNotice = null" aria-label="Dismiss">
|
|
484
|
+
<i class="fa-solid fa-xmark" />
|
|
485
|
+
</button>
|
|
289
486
|
</div>
|
|
487
|
+
|
|
488
|
+
<!-- Block canvas -->
|
|
489
|
+
<EditorsBlockCanvas
|
|
490
|
+
:block-editor="blockEditor"
|
|
491
|
+
:block-types="blockTypes"
|
|
492
|
+
/>
|
|
290
493
|
</div>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
</select>
|
|
297
|
-
</div>
|
|
298
|
-
<!-- Markdown toolbar -->
|
|
299
|
-
<div class="md-toolbar">
|
|
300
|
-
<button class="md-btn" title="Bold" @click="insertMarkdown(newContentRef!, '**', '**')"><b>B</b></button>
|
|
301
|
-
<button class="md-btn" title="Italic" @click="insertMarkdown(newContentRef!, '_', '_')"><i>I</i></button>
|
|
302
|
-
<button class="md-btn" title="Code" @click="insertMarkdown(newContentRef!, '`', '`')"><code></></code></button>
|
|
303
|
-
<button class="md-btn" title="Heading" @click="insertMarkdown(newContentRef!, '## ')">H</button>
|
|
304
|
-
<button class="md-btn" title="Link" @click="insertMarkdown(newContentRef!, '[', '](url)')">🔗</button>
|
|
305
|
-
<button class="md-btn" title="List" @click="insertMarkdown(newContentRef!, '- ')">≡</button>
|
|
494
|
+
|
|
495
|
+
<!-- Empty state -->
|
|
496
|
+
<div v-else class="cpub-docs-empty">
|
|
497
|
+
<i class="fa-solid fa-file-lines cpub-docs-empty-icon" />
|
|
498
|
+
<p class="cpub-docs-empty-text">Select a page from the sidebar or create a new one.</p>
|
|
306
499
|
</div>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
<
|
|
500
|
+
</template>
|
|
501
|
+
|
|
502
|
+
<!-- RIGHT: Page properties -->
|
|
503
|
+
<template #right>
|
|
504
|
+
<div v-if="selectedPage" class="cpub-docs-props">
|
|
505
|
+
<h3 class="cpub-docs-props-heading">Page Properties</h3>
|
|
506
|
+
|
|
507
|
+
<div class="cpub-docs-field">
|
|
508
|
+
<label class="cpub-docs-field-label">Slug</label>
|
|
509
|
+
<input v-model="pageSlug" class="cpub-docs-field-input" placeholder="page-slug" />
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<div class="cpub-docs-field">
|
|
513
|
+
<label class="cpub-docs-field-label">Parent</label>
|
|
514
|
+
<div class="cpub-docs-field-value">
|
|
515
|
+
{{ selectedPage.parentId ? pages.find(p => p.id === selectedPage.parentId)?.title ?? 'Unknown' : 'Top level' }}
|
|
516
|
+
</div>
|
|
517
|
+
<span class="cpub-docs-field-hint">Drag pages in the tree to change hierarchy</span>
|
|
518
|
+
</div>
|
|
313
519
|
</div>
|
|
314
|
-
</
|
|
520
|
+
</template>
|
|
315
521
|
|
|
316
|
-
<!--
|
|
317
|
-
<
|
|
318
|
-
<
|
|
319
|
-
<
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
522
|
+
<!-- STATUS BAR -->
|
|
523
|
+
<template #status>
|
|
524
|
+
<span class="cpub-docs-stat">
|
|
525
|
+
<span class="cpub-docs-stat-label">Pages:</span>
|
|
526
|
+
<span class="cpub-docs-stat-value">{{ pages.length }}</span>
|
|
527
|
+
</span>
|
|
528
|
+
<span v-if="selectedPageId" class="cpub-docs-stat">
|
|
529
|
+
<span class="cpub-docs-stat-label">Words:</span>
|
|
530
|
+
<span class="cpub-docs-stat-value">{{ wordCount }}</span>
|
|
531
|
+
</span>
|
|
532
|
+
<span v-if="selectedPageId" class="cpub-docs-stat">
|
|
533
|
+
<span class="cpub-docs-stat-label">Blocks:</span>
|
|
534
|
+
<span class="cpub-docs-stat-value">{{ blockEditor.blocks.value.length }}</span>
|
|
535
|
+
</span>
|
|
536
|
+
<div style="flex: 1" />
|
|
537
|
+
<span class="cpub-docs-stat">
|
|
538
|
+
<span
|
|
539
|
+
class="cpub-docs-save-dot"
|
|
540
|
+
:class="{
|
|
541
|
+
'cpub-docs-save-dot-clean': !isDirty && autoSaveStatus !== 'error',
|
|
542
|
+
'cpub-docs-save-dot-dirty': isDirty,
|
|
543
|
+
'cpub-docs-save-dot-error': autoSaveStatus === 'error',
|
|
544
|
+
}"
|
|
545
|
+
/>
|
|
546
|
+
<span class="cpub-docs-stat-label">
|
|
547
|
+
{{ autoSaveStatus === 'saving' ? 'Saving...' : autoSaveStatus === 'error' ? 'Save failed' : isDirty ? 'Unsaved changes' : 'All saved' }}
|
|
548
|
+
</span>
|
|
549
|
+
</span>
|
|
550
|
+
</template>
|
|
551
|
+
</EditorsEditorShell>
|
|
552
|
+
|
|
553
|
+
<!-- Markdown import dialog -->
|
|
554
|
+
<EditorsMarkdownImportDialog
|
|
555
|
+
:show="showImportDialog"
|
|
556
|
+
@close="showImportDialog = false"
|
|
557
|
+
@import="handleMarkdownImport"
|
|
558
|
+
/>
|
|
559
|
+
|
|
560
|
+
<!-- Site settings panel -->
|
|
561
|
+
<Teleport to="body">
|
|
562
|
+
<div v-if="showSettings" class="cpub-settings-overlay" @click.self="showSettings = false">
|
|
563
|
+
<div class="cpub-settings-panel">
|
|
564
|
+
<div class="cpub-settings-header">
|
|
565
|
+
<h2 class="cpub-settings-title"><i class="fa-solid fa-gear" /> Site Settings</h2>
|
|
566
|
+
<button class="cpub-settings-close" @click="showSettings = false" aria-label="Close settings">
|
|
567
|
+
<i class="fa-solid fa-xmark" />
|
|
568
|
+
</button>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<div class="cpub-settings-body">
|
|
572
|
+
<!-- Site info -->
|
|
573
|
+
<section class="cpub-settings-section">
|
|
574
|
+
<h3 class="cpub-settings-section-title">General</h3>
|
|
575
|
+
<div class="cpub-settings-field">
|
|
576
|
+
<label class="cpub-settings-label">Site Name</label>
|
|
577
|
+
<input v-model="settingsName" class="cpub-settings-input" />
|
|
324
578
|
</div>
|
|
325
|
-
<div class="
|
|
326
|
-
<label class="
|
|
327
|
-
<
|
|
328
|
-
<option :value="null">— None —</option>
|
|
329
|
-
<option v-for="p in sortedPages.filter(p => p.id !== page.id)" :key="p.id" :value="p.id">{{ p.title }}</option>
|
|
330
|
-
</select>
|
|
579
|
+
<div class="cpub-settings-field">
|
|
580
|
+
<label class="cpub-settings-label">Description</label>
|
|
581
|
+
<textarea v-model="settingsDesc" class="cpub-settings-textarea" rows="3" />
|
|
331
582
|
</div>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
<div class="md-toolbar">
|
|
335
|
-
<button class="md-btn" title="Bold" @click="insertMarkdown(editContentRef!, '**', '**')"><b>B</b></button>
|
|
336
|
-
<button class="md-btn" title="Italic" @click="insertMarkdown(editContentRef!, '_', '_')"><i>I</i></button>
|
|
337
|
-
<button class="md-btn" title="Code" @click="insertMarkdown(editContentRef!, '`', '`')"><code></></code></button>
|
|
338
|
-
<button class="md-btn" title="Heading" @click="insertMarkdown(editContentRef!, '## ')">H</button>
|
|
339
|
-
<button class="md-btn" title="Link" @click="insertMarkdown(editContentRef!, '[', '](url)')">🔗</button>
|
|
340
|
-
<button class="md-btn" title="List" @click="insertMarkdown(editContentRef!, '- ')">≡</button>
|
|
341
|
-
<div class="md-spacer"></div>
|
|
342
|
-
<button class="md-btn" :class="{ active: showPreview }" @click="showPreview = !showPreview">
|
|
343
|
-
<i class="fa-solid fa-eye"></i> Preview
|
|
583
|
+
<button class="cpub-btn cpub-btn-sm" :disabled="savingSettings" @click="saveSiteSettings">
|
|
584
|
+
{{ savingSettings ? 'Saving...' : 'Save Settings' }}
|
|
344
585
|
</button>
|
|
345
|
-
</
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
<div class="page-item-info">
|
|
360
|
-
<span class="page-item-title">{{ page.title }}</span>
|
|
361
|
-
<span class="page-item-slug">/{{ page.slug }}</span>
|
|
362
|
-
<span v-if="page.parentId" class="page-item-child-badge">child</span>
|
|
586
|
+
</section>
|
|
587
|
+
|
|
588
|
+
<!-- Versions -->
|
|
589
|
+
<section class="cpub-settings-section">
|
|
590
|
+
<h3 class="cpub-settings-section-title">Versions</h3>
|
|
591
|
+
<div v-if="(site as any)?.versions?.length" class="cpub-settings-versions">
|
|
592
|
+
<div
|
|
593
|
+
v-for="v in ((site as any).versions as DocsSiteVersion[])"
|
|
594
|
+
:key="v.id"
|
|
595
|
+
class="cpub-settings-version-item"
|
|
596
|
+
>
|
|
597
|
+
<span class="cpub-settings-version-label">{{ v.version }}</span>
|
|
598
|
+
<span v-if="v.isDefault" class="cpub-settings-version-badge">default</span>
|
|
599
|
+
</div>
|
|
363
600
|
</div>
|
|
364
|
-
<div class="
|
|
365
|
-
<
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
<i class="fa-solid fa-trash"></i>
|
|
376
|
-
</button>
|
|
601
|
+
<div class="cpub-settings-field" style="margin-top: 10px;">
|
|
602
|
+
<label class="cpub-settings-label">New Version</label>
|
|
603
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
604
|
+
<input v-model="newVersion" class="cpub-settings-input" placeholder="e.g. 2.0" style="flex: 1;" />
|
|
605
|
+
<label class="cpub-settings-checkbox">
|
|
606
|
+
<input type="checkbox" v-model="newVersionDefault" /> Default
|
|
607
|
+
</label>
|
|
608
|
+
<button class="cpub-btn cpub-btn-sm" :disabled="savingVersion || !newVersion.trim()" @click="createVersion">
|
|
609
|
+
{{ savingVersion ? 'Creating...' : 'Create' }}
|
|
610
|
+
</button>
|
|
611
|
+
</div>
|
|
377
612
|
</div>
|
|
378
|
-
</
|
|
379
|
-
</template>
|
|
380
|
-
</div>
|
|
381
|
-
</div>
|
|
382
|
-
<p v-else class="edit-empty">No pages yet. Create one above.</p>
|
|
383
|
-
</section>
|
|
384
|
-
|
|
385
|
-
<!-- ═══ VERSIONS ═══ -->
|
|
386
|
-
<section class="edit-section">
|
|
387
|
-
<div class="section-header">
|
|
388
|
-
<h2 class="section-heading"><i class="fa-solid fa-code-branch"></i> Versions</h2>
|
|
389
|
-
<button class="cpub-btn cpub-btn-sm" @click="showNewVersion = !showNewVersion">
|
|
390
|
-
<i class="fa-solid fa-plus"></i> Add Version
|
|
391
|
-
</button>
|
|
392
|
-
</div>
|
|
613
|
+
</section>
|
|
393
614
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
</
|
|
403
|
-
<button class="cpub-btn cpub-btn-sm" @click="showNewVersion = false">Cancel</button>
|
|
404
|
-
</div>
|
|
405
|
-
</div>
|
|
406
|
-
|
|
407
|
-
<div v-if="site.versions?.length" class="version-list">
|
|
408
|
-
<div v-for="v in site.versions" :key="v.id" class="version-item">
|
|
409
|
-
<span class="version-label">{{ v.version }}</span>
|
|
410
|
-
<span v-if="v.isDefault" class="version-default-badge">default</span>
|
|
615
|
+
<!-- Danger zone -->
|
|
616
|
+
<section class="cpub-settings-section cpub-settings-danger">
|
|
617
|
+
<h3 class="cpub-settings-section-title">Danger Zone</h3>
|
|
618
|
+
<p class="cpub-settings-danger-text">Permanently delete this docs site and all its pages.</p>
|
|
619
|
+
<button class="cpub-btn cpub-btn-sm cpub-btn-danger" @click="deleteSite">
|
|
620
|
+
<i class="fa-solid fa-trash" /> Delete Site
|
|
621
|
+
</button>
|
|
622
|
+
</section>
|
|
623
|
+
</div>
|
|
411
624
|
</div>
|
|
412
625
|
</div>
|
|
413
|
-
|
|
414
|
-
</section>
|
|
626
|
+
</Teleport>
|
|
415
627
|
</div>
|
|
416
628
|
</template>
|
|
417
629
|
|
|
418
630
|
<style scoped>
|
|
419
|
-
.docs-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
.
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
.
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
.
|
|
631
|
+
.cpub-docs-editor {
|
|
632
|
+
display: flex;
|
|
633
|
+
flex-direction: column;
|
|
634
|
+
height: 100vh;
|
|
635
|
+
background: var(--bg);
|
|
636
|
+
color: var(--text);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/* Top bar */
|
|
640
|
+
.cpub-docs-topbar {
|
|
641
|
+
height: 44px;
|
|
642
|
+
flex-shrink: 0;
|
|
643
|
+
background: var(--surface);
|
|
644
|
+
border-bottom: var(--border-width-default) solid var(--border);
|
|
645
|
+
display: flex;
|
|
646
|
+
align-items: center;
|
|
647
|
+
padding: 0 12px;
|
|
648
|
+
gap: 10px;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.cpub-docs-back {
|
|
652
|
+
width: 28px;
|
|
653
|
+
height: 28px;
|
|
654
|
+
display: flex;
|
|
655
|
+
align-items: center;
|
|
656
|
+
justify-content: center;
|
|
657
|
+
color: var(--text-dim);
|
|
658
|
+
text-decoration: none;
|
|
659
|
+
font-size: 12px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.cpub-docs-back:hover {
|
|
663
|
+
color: var(--text);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.cpub-docs-topbar-title {
|
|
667
|
+
font-family: var(--font-mono);
|
|
668
|
+
font-size: 12px;
|
|
669
|
+
font-weight: 700;
|
|
670
|
+
color: var(--accent);
|
|
671
|
+
letter-spacing: 0.04em;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.cpub-docs-topbar-spacer {
|
|
675
|
+
flex: 1;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.cpub-docs-toolbar-btn {
|
|
679
|
+
display: flex;
|
|
680
|
+
align-items: center;
|
|
681
|
+
gap: 6px;
|
|
682
|
+
padding: 5px 12px;
|
|
683
|
+
background: var(--surface2);
|
|
684
|
+
border: var(--border-width-default) solid var(--border);
|
|
685
|
+
color: var(--text-dim);
|
|
686
|
+
font-family: var(--font-mono);
|
|
687
|
+
font-size: 10px;
|
|
688
|
+
cursor: pointer;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.cpub-docs-toolbar-btn:hover {
|
|
692
|
+
background: var(--surface);
|
|
693
|
+
color: var(--text);
|
|
694
|
+
border-color: var(--accent);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.cpub-docs-toolbar-btn:disabled {
|
|
698
|
+
opacity: 0.5;
|
|
699
|
+
cursor: default;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/* Left panel header */
|
|
703
|
+
.cpub-docs-left-header {
|
|
704
|
+
display: flex;
|
|
705
|
+
align-items: center;
|
|
706
|
+
justify-content: space-between;
|
|
707
|
+
padding: 0 0 8px;
|
|
708
|
+
border-bottom: var(--border-width-default) solid var(--border2);
|
|
709
|
+
margin-bottom: 4px;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.cpub-docs-left-label {
|
|
713
|
+
font-family: var(--font-mono);
|
|
714
|
+
font-size: 9px;
|
|
715
|
+
font-weight: 700;
|
|
716
|
+
text-transform: uppercase;
|
|
717
|
+
letter-spacing: 0.1em;
|
|
718
|
+
color: var(--text-faint);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.cpub-docs-page-count {
|
|
722
|
+
font-family: var(--font-mono);
|
|
723
|
+
font-size: 9px;
|
|
724
|
+
color: var(--text-faint);
|
|
725
|
+
background: var(--surface2);
|
|
726
|
+
padding: 1px 6px;
|
|
727
|
+
border: var(--border-width-default) solid var(--border2);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/* Center: editor */
|
|
731
|
+
.cpub-docs-center {
|
|
732
|
+
max-width: 740px;
|
|
733
|
+
margin: 0 auto;
|
|
734
|
+
width: 100%;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.cpub-docs-title-area {
|
|
738
|
+
margin-bottom: 8px;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.cpub-docs-title {
|
|
742
|
+
font-size: 24px;
|
|
743
|
+
font-weight: 700;
|
|
744
|
+
color: var(--text);
|
|
745
|
+
cursor: text;
|
|
746
|
+
padding: 4px 0;
|
|
747
|
+
border-bottom: 2px solid transparent;
|
|
748
|
+
transition: border-color 0.15s;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.cpub-docs-title:hover {
|
|
752
|
+
border-bottom-color: var(--border2);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.cpub-docs-title-input {
|
|
756
|
+
width: 100%;
|
|
757
|
+
font-size: 24px;
|
|
758
|
+
font-weight: 700;
|
|
759
|
+
color: var(--text);
|
|
760
|
+
background: none;
|
|
761
|
+
border: none;
|
|
762
|
+
border-bottom: 2px solid var(--accent);
|
|
763
|
+
padding: 4px 0;
|
|
764
|
+
outline: none;
|
|
765
|
+
font-family: inherit;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/* Empty state */
|
|
769
|
+
.cpub-docs-empty {
|
|
770
|
+
display: flex;
|
|
771
|
+
flex-direction: column;
|
|
772
|
+
align-items: center;
|
|
773
|
+
justify-content: center;
|
|
774
|
+
height: 100%;
|
|
775
|
+
gap: 12px;
|
|
776
|
+
color: var(--text-faint);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.cpub-docs-empty-icon {
|
|
780
|
+
font-size: 32px;
|
|
781
|
+
opacity: 0.3;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.cpub-docs-empty-text {
|
|
785
|
+
font-size: 13px;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/* Right panel: properties */
|
|
789
|
+
.cpub-docs-props {
|
|
790
|
+
display: flex;
|
|
791
|
+
flex-direction: column;
|
|
792
|
+
gap: 14px;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.cpub-docs-props-heading {
|
|
796
|
+
font-family: var(--font-mono);
|
|
797
|
+
font-size: 9px;
|
|
798
|
+
font-weight: 700;
|
|
799
|
+
text-transform: uppercase;
|
|
800
|
+
letter-spacing: 0.1em;
|
|
801
|
+
color: var(--text-faint);
|
|
802
|
+
padding-bottom: 8px;
|
|
803
|
+
border-bottom: var(--border-width-default) solid var(--border2);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.cpub-docs-field {
|
|
807
|
+
display: flex;
|
|
808
|
+
flex-direction: column;
|
|
809
|
+
gap: 4px;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.cpub-docs-field-label {
|
|
813
|
+
font-family: var(--font-mono);
|
|
814
|
+
font-size: 9px;
|
|
815
|
+
font-weight: 600;
|
|
816
|
+
text-transform: uppercase;
|
|
817
|
+
letter-spacing: 0.06em;
|
|
818
|
+
color: var(--text-faint);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.cpub-docs-field-input {
|
|
822
|
+
padding: 6px 8px;
|
|
823
|
+
background: var(--surface2);
|
|
824
|
+
border: var(--border-width-default) solid var(--border);
|
|
825
|
+
color: var(--text);
|
|
826
|
+
font-size: 12px;
|
|
827
|
+
font-family: var(--font-mono);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.cpub-docs-field-input:focus {
|
|
831
|
+
border-color: var(--accent);
|
|
832
|
+
outline: none;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.cpub-docs-field-hint {
|
|
836
|
+
font-size: 10px;
|
|
837
|
+
color: var(--text-faint);
|
|
838
|
+
line-height: 1.3;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.cpub-docs-field-value {
|
|
842
|
+
font-size: 12px;
|
|
843
|
+
color: var(--text-dim);
|
|
844
|
+
padding: 6px 0;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/* Markdown conversion notice */
|
|
848
|
+
.cpub-docs-notice {
|
|
849
|
+
display: flex;
|
|
850
|
+
align-items: center;
|
|
851
|
+
gap: 8px;
|
|
852
|
+
padding: 8px 12px;
|
|
853
|
+
margin-bottom: 12px;
|
|
854
|
+
background: var(--accent-bg);
|
|
855
|
+
border: var(--border-width-default) solid var(--accent-border);
|
|
856
|
+
font-size: 12px;
|
|
857
|
+
color: var(--text-dim);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.cpub-docs-notice i:first-child {
|
|
861
|
+
color: var(--accent);
|
|
862
|
+
font-size: 13px;
|
|
863
|
+
flex-shrink: 0;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
.cpub-docs-notice-dismiss {
|
|
867
|
+
margin-left: auto;
|
|
868
|
+
width: 20px;
|
|
869
|
+
height: 20px;
|
|
870
|
+
display: flex;
|
|
871
|
+
align-items: center;
|
|
872
|
+
justify-content: center;
|
|
873
|
+
background: none;
|
|
874
|
+
border: none;
|
|
875
|
+
color: var(--text-faint);
|
|
876
|
+
cursor: pointer;
|
|
877
|
+
flex-shrink: 0;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.cpub-docs-notice-dismiss:hover {
|
|
881
|
+
color: var(--text);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/* Status bar stats */
|
|
885
|
+
.cpub-docs-stat {
|
|
886
|
+
display: flex;
|
|
887
|
+
align-items: center;
|
|
888
|
+
gap: 4px;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.cpub-docs-stat-label {
|
|
892
|
+
text-transform: uppercase;
|
|
893
|
+
color: var(--text-faint);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.cpub-docs-stat-value {
|
|
897
|
+
color: var(--text-dim);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.cpub-docs-save-dot {
|
|
901
|
+
width: 6px;
|
|
902
|
+
height: 6px;
|
|
903
|
+
border-radius: 50%;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.cpub-docs-save-dot-clean {
|
|
907
|
+
background: var(--green, #2a9d5c);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.cpub-docs-save-dot-dirty {
|
|
911
|
+
background: var(--yellow, #d4a017);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.cpub-docs-save-dot-error {
|
|
915
|
+
background: var(--red, #e04030);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/* Settings panel */
|
|
919
|
+
.cpub-settings-overlay {
|
|
920
|
+
position: fixed;
|
|
921
|
+
inset: 0;
|
|
922
|
+
z-index: 10000;
|
|
923
|
+
background: rgba(0, 0, 0, 0.5);
|
|
924
|
+
display: flex;
|
|
925
|
+
align-items: flex-start;
|
|
926
|
+
justify-content: center;
|
|
927
|
+
padding-top: 60px;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.cpub-settings-panel {
|
|
931
|
+
width: 480px;
|
|
932
|
+
max-height: 80vh;
|
|
933
|
+
background: var(--surface);
|
|
934
|
+
border: var(--border-width-default) solid var(--border);
|
|
935
|
+
box-shadow: var(--shadow-xl, 8px 8px 0 var(--border));
|
|
936
|
+
display: flex;
|
|
937
|
+
flex-direction: column;
|
|
938
|
+
overflow: hidden;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.cpub-settings-header {
|
|
942
|
+
display: flex;
|
|
943
|
+
align-items: center;
|
|
944
|
+
justify-content: space-between;
|
|
945
|
+
padding: 14px 20px;
|
|
946
|
+
border-bottom: var(--border-width-default) solid var(--border);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.cpub-settings-title {
|
|
950
|
+
font-family: var(--font-mono);
|
|
951
|
+
font-size: 12px;
|
|
952
|
+
font-weight: 700;
|
|
953
|
+
text-transform: uppercase;
|
|
954
|
+
letter-spacing: 0.06em;
|
|
955
|
+
display: flex;
|
|
956
|
+
align-items: center;
|
|
957
|
+
gap: 8px;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.cpub-settings-title i {
|
|
961
|
+
color: var(--accent);
|
|
962
|
+
font-size: 11px;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.cpub-settings-close {
|
|
966
|
+
width: 28px;
|
|
967
|
+
height: 28px;
|
|
968
|
+
display: flex;
|
|
969
|
+
align-items: center;
|
|
970
|
+
justify-content: center;
|
|
971
|
+
background: none;
|
|
972
|
+
border: var(--border-width-default) solid transparent;
|
|
973
|
+
color: var(--text-dim);
|
|
974
|
+
cursor: pointer;
|
|
975
|
+
font-size: 13px;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.cpub-settings-close:hover {
|
|
979
|
+
background: var(--surface2);
|
|
980
|
+
border-color: var(--border);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.cpub-settings-body {
|
|
984
|
+
flex: 1;
|
|
985
|
+
overflow-y: auto;
|
|
986
|
+
padding: 0;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.cpub-settings-section {
|
|
990
|
+
padding: 16px 20px;
|
|
991
|
+
border-bottom: var(--border-width-default) solid var(--border);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.cpub-settings-section:last-child {
|
|
995
|
+
border-bottom: none;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.cpub-settings-section-title {
|
|
999
|
+
font-family: var(--font-mono);
|
|
1000
|
+
font-size: 9px;
|
|
1001
|
+
font-weight: 700;
|
|
1002
|
+
text-transform: uppercase;
|
|
1003
|
+
letter-spacing: 0.1em;
|
|
1004
|
+
color: var(--text-faint);
|
|
1005
|
+
margin-bottom: 12px;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.cpub-settings-field {
|
|
1009
|
+
display: flex;
|
|
1010
|
+
flex-direction: column;
|
|
1011
|
+
gap: 4px;
|
|
1012
|
+
margin-bottom: 10px;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
.cpub-settings-label {
|
|
1016
|
+
font-family: var(--font-mono);
|
|
1017
|
+
font-size: 9px;
|
|
1018
|
+
font-weight: 600;
|
|
1019
|
+
text-transform: uppercase;
|
|
1020
|
+
letter-spacing: 0.06em;
|
|
1021
|
+
color: var(--text-faint);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
.cpub-settings-input {
|
|
1025
|
+
padding: 6px 10px;
|
|
1026
|
+
background: var(--surface2);
|
|
1027
|
+
border: var(--border-width-default) solid var(--border);
|
|
1028
|
+
color: var(--text);
|
|
1029
|
+
font-size: 13px;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.cpub-settings-input:focus {
|
|
1033
|
+
border-color: var(--accent);
|
|
1034
|
+
outline: none;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.cpub-settings-textarea {
|
|
1038
|
+
padding: 6px 10px;
|
|
1039
|
+
background: var(--surface2);
|
|
1040
|
+
border: var(--border-width-default) solid var(--border);
|
|
1041
|
+
color: var(--text);
|
|
1042
|
+
font-size: 13px;
|
|
1043
|
+
resize: vertical;
|
|
1044
|
+
font-family: inherit;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.cpub-settings-textarea:focus {
|
|
1048
|
+
border-color: var(--accent);
|
|
1049
|
+
outline: none;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
.cpub-settings-versions {
|
|
1053
|
+
display: flex;
|
|
1054
|
+
flex-wrap: wrap;
|
|
1055
|
+
gap: 6px;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.cpub-settings-version-item {
|
|
1059
|
+
display: flex;
|
|
1060
|
+
align-items: center;
|
|
1061
|
+
gap: 6px;
|
|
1062
|
+
padding: 4px 10px;
|
|
1063
|
+
background: var(--surface2);
|
|
1064
|
+
border: var(--border-width-default) solid var(--border);
|
|
1065
|
+
font-family: var(--font-mono);
|
|
1066
|
+
font-size: 11px;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.cpub-settings-version-label {
|
|
1070
|
+
font-weight: 600;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
.cpub-settings-version-badge {
|
|
1074
|
+
font-size: 9px;
|
|
1075
|
+
padding: 1px 5px;
|
|
1076
|
+
background: var(--accent-bg);
|
|
1077
|
+
color: var(--accent);
|
|
1078
|
+
border: var(--border-width-default) solid var(--accent-border);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.cpub-settings-checkbox {
|
|
1082
|
+
display: flex;
|
|
1083
|
+
align-items: center;
|
|
1084
|
+
gap: 4px;
|
|
1085
|
+
font-size: 11px;
|
|
1086
|
+
color: var(--text-dim);
|
|
1087
|
+
cursor: pointer;
|
|
1088
|
+
white-space: nowrap;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
.cpub-settings-checkbox input {
|
|
1092
|
+
accent-color: var(--accent);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.cpub-settings-danger {
|
|
1096
|
+
background: rgba(224, 64, 48, 0.03);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
.cpub-settings-danger .cpub-settings-section-title {
|
|
1100
|
+
color: var(--red, #e04030);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.cpub-settings-danger-text {
|
|
1104
|
+
font-size: 12px;
|
|
1105
|
+
color: var(--text-dim);
|
|
1106
|
+
margin-bottom: 10px;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.cpub-btn-danger {
|
|
1110
|
+
color: var(--red, #e04030);
|
|
1111
|
+
border-color: var(--red, #e04030);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.cpub-btn-danger:hover {
|
|
1115
|
+
background: var(--red, #e04030);
|
|
1116
|
+
color: var(--color-text-inverse, #fff);
|
|
1117
|
+
}
|
|
480
1118
|
</style>
|