@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.
@@ -1,480 +1,1118 @@
1
1
  <script setup lang="ts">
2
- definePageMeta({ middleware: 'auth' });
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
- const { data: site, refresh: refreshSite } = useLazyFetch(() => `/api/docs/${siteSlug.value}`);
8
- const { data: pages, refresh: refreshPages } = useLazyFetch(() => `/api/docs/${siteSlug.value}/pages`);
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
- const { show: toast } = useToast();
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
- // ═══ SITE SETTINGS ═══
15
- const editSiteName = ref('');
16
- const editSiteDesc = ref('');
17
- const savingSite = ref(false);
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
- watch(site, (s) => {
20
- if (!s) return;
21
- editSiteName.value = s.name ?? '';
22
- editSiteDesc.value = s.description ?? '';
23
- }, { immediate: true });
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: { name: editSiteName.value, description: editSiteDesc.value },
130
+ body: {
131
+ title: selectedPage.value?.title,
132
+ slug: pageSlug.value,
133
+ content: blockEditor.toBlockTuples(),
134
+ },
31
135
  });
32
- toast('Site settings updated', 'success');
33
- await refreshSite();
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
- toast(err instanceof Error ? err.message : 'Failed to update', 'error');
141
+ autoSaveStatus.value = 'error';
142
+ toast(err instanceof Error ? err.message : 'Failed to save page', 'error');
36
143
  } finally {
37
- savingSite.value = false;
144
+ savingPage.value = false;
38
145
  }
39
146
  }
40
147
 
41
- // ═══ PAGE CREATION ═══
42
- const showNewPage = ref(false);
43
- const newPageTitle = ref('');
44
- const newPageSlug = ref('');
45
- const newPageContent = ref('');
46
- const newPageParentId = ref<string | null>(null);
47
- const savingPage = ref(false);
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
- function autoSlug(title: string): string {
50
- return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
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
- watch(newPageTitle, (t) => {
54
- if (!newPageSlug.value || newPageSlug.value === autoSlug(newPageTitle.value.slice(0, -1))) {
55
- newPageSlug.value = autoSlug(t);
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
- async function createPage(): Promise<void> {
60
- if (!newPageTitle.value.trim()) return;
61
- savingPage.value = true;
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
- await $fetch(`/api/docs/${siteSlug.value}/pages`, {
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: newPageTitle.value,
67
- slug: newPageSlug.value || autoSlug(newPageTitle.value),
68
- content: newPageContent.value,
69
- parentId: newPageParentId.value || undefined,
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
- // ═══ DELETE SITE ═══
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}`, { method: 'DELETE' });
92
- toast('Docs site deleted', 'success');
93
- await navigateTo('/docs');
94
- } catch {
95
- toast('Failed to delete docs site', 'error');
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
- // ═══ VERSION CREATION ═══
100
- const showNewVersion = ref(false);
101
- const newVersion = ref('');
102
- const newVersionDefault = ref(false);
103
- const savingVersion = ref(false);
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 createVersion(): Promise<void> {
106
- if (!newVersion.value.trim()) return;
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}/versions`, {
266
+ await $fetch(`/api/docs/${siteSlug.value}/pages/reorder`, {
110
267
  method: 'POST',
111
- body: { version: newVersion.value, isDefault: newVersionDefault.value },
268
+ body: { pageIds },
112
269
  });
113
- toast('Version created', 'success');
114
- newVersion.value = '';
115
- newVersionDefault.value = false;
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
- // ═══ PAGE EDITING ═══
126
- interface DocsPage {
127
- id: string;
128
- title: string;
129
- slug: string;
130
- content: string;
131
- sortOrder: number;
132
- parentId: string | null;
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
- function startEditPage(page: DocsPage): void {
143
- editingPageId.value = page.id;
144
- editPageTitle.value = page.title;
145
- editPageContent.value = page.content ?? '';
146
- editPageParentId.value = page.parentId;
147
- showPreview.value = false;
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 savePageEdit(): Promise<void> {
151
- if (!editingPageId.value) return;
152
- savingEdit.value = true;
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}/pages/${editingPageId.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('Page updated', 'success');
163
- editingPageId.value = null;
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 page', 'error');
373
+ toast(err instanceof Error ? err.message : 'Failed to update settings', 'error');
167
374
  } finally {
168
- savingEdit.value = false;
375
+ savingSettings.value = false;
169
376
  }
170
377
  }
171
378
 
172
- // ═══ PAGE DELETION ═══
173
- async function deletePage(pageId: string): Promise<void> {
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}/pages/${pageId}`, { method: 'DELETE' });
177
- toast('Page deleted', 'success');
178
- if (editingPageId.value === pageId) editingPageId.value = null;
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 page', 'error');
386
+ toast('Failed to delete docs site', 'error');
182
387
  }
183
388
  }
184
389
 
185
- // ═══ PAGE REORDERING ═══
186
- async function movePage(pageId: string, direction: 'up' | 'down'): Promise<void> {
187
- if (!pages.value) return;
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}/pages/reorder`, {
394
+ await $fetch(`/api/docs/${siteSlug.value}/versions`, {
199
395
  method: 'POST',
200
- body: { pageIds: allPages.map(p => p.id) },
396
+ body: { version: newVersion.value, isDefault: newVersionDefault.value },
201
397
  });
202
- await refreshPages();
203
- } catch {
204
- toast('Failed to reorder', 'error');
205
- }
206
- }
207
-
208
- // ═══ MARKDOWN TOOLBAR ═══
209
- function insertMarkdown(textarea: HTMLTextAreaElement | null, before: string, after: string = ''): void {
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-edit" v-if="site">
238
- <div class="docs-edit-header">
239
- <div>
240
- <h1 class="page-title">Edit: {{ site.name }}</h1>
241
- <NuxtLink :to="`/docs/${siteSlug}`" class="cpub-back-link">&larr; Back to docs</NuxtLink>
242
- </div>
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
- <!-- ═══ SITE SETTINGS ═══ -->
246
- <section class="edit-section">
247
- <div class="section-header">
248
- <h2 class="section-heading"><i class="fa-solid fa-gear"></i> Site Settings</h2>
249
- </div>
250
- <div class="settings-form">
251
- <div class="form-field">
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
- <div style="display: flex; gap: 8px; align-items: center;">
260
- <button class="cpub-btn cpub-btn-sm" :disabled="savingSite" @click="saveSiteSettings">
261
- {{ savingSite ? 'Saving...' : 'Save Settings' }}
262
- </button>
263
- <button class="cpub-btn cpub-btn-sm" style="color: var(--red); border-color: var(--red-border); margin-left: auto;" @click="deleteSite">
264
- <i class="fa-solid fa-trash"></i> Delete Site
265
- </button>
266
- </div>
267
- </div>
268
- </section>
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
- <!-- New page form -->
280
- <div v-if="showNewPage" class="new-form">
281
- <div class="form-row">
282
- <div class="form-field" style="flex:1">
283
- <label class="form-label">Title</label>
284
- <input v-model="newPageTitle" class="edit-input" placeholder="Page title" />
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
- <div class="form-field" style="flex:1">
287
- <label class="form-label">Slug</label>
288
- <input v-model="newPageSlug" class="edit-input" placeholder="auto-generated" />
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
- <div class="form-field">
292
- <label class="form-label">Parent Page</label>
293
- <select v-model="newPageParentId" class="edit-select">
294
- <option :value="null">— None (top level) —</option>
295
- <option v-for="p in sortedPages" :key="p.id" :value="p.id">{{ p.title }}</option>
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>&lt;/&gt;</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
- <textarea ref="newContent" v-model="newPageContent" class="edit-textarea edit-textarea-md" placeholder="Markdown content..." rows="10" />
308
- <div class="form-actions">
309
- <button class="cpub-btn cpub-btn-sm cpub-btn-primary" :disabled="savingPage || !newPageTitle.trim()" @click="createPage">
310
- {{ savingPage ? 'Creating...' : 'Create Page' }}
311
- </button>
312
- <button class="cpub-btn cpub-btn-sm" @click="showNewPage = false">Cancel</button>
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
- </div>
520
+ </template>
315
521
 
316
- <!-- Page list -->
317
- <div v-if="sortedPages.length" class="page-list">
318
- <div v-for="(page, idx) in sortedPages" :key="page.id" class="page-item">
319
- <template v-if="editingPageId === page.id">
320
- <div class="form-row">
321
- <div class="form-field" style="flex:1">
322
- <label class="form-label">Title</label>
323
- <input v-model="editPageTitle" class="edit-input" />
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="form-field" style="flex:1">
326
- <label class="form-label">Parent</label>
327
- <select v-model="editPageParentId" class="edit-select">
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
- </div>
333
- <!-- Toolbar + Preview toggle -->
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>&lt;/&gt;</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
- </div>
346
- <div class="editor-pane" :class="{ 'with-preview': showPreview }">
347
- <textarea ref="editContent" v-model="editPageContent" class="edit-textarea edit-textarea-md" rows="14" />
348
- <div v-if="showPreview" class="preview-pane"><pre class="preview-md">{{ editPageContent }}</pre></div>
349
- </div>
350
- <div class="form-actions">
351
- <button class="cpub-btn cpub-btn-sm cpub-btn-primary" :disabled="savingEdit" @click="savePageEdit">
352
- {{ savingEdit ? 'Saving...' : 'Save' }}
353
- </button>
354
- <button class="cpub-btn cpub-btn-sm" @click="editingPageId = null">Cancel</button>
355
- </div>
356
- </template>
357
- <template v-else>
358
- <div class="page-item-row">
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="page-item-actions">
365
- <button class="page-action-btn" title="Move up" :disabled="idx === 0" @click="movePage(page.id, 'up')">
366
- <i class="fa-solid fa-chevron-up"></i>
367
- </button>
368
- <button class="page-action-btn" title="Move down" :disabled="idx === sortedPages.length - 1" @click="movePage(page.id, 'down')">
369
- <i class="fa-solid fa-chevron-down"></i>
370
- </button>
371
- <button class="cpub-btn cpub-btn-sm" @click="startEditPage(page)">
372
- <i class="fa-solid fa-pen"></i> Edit
373
- </button>
374
- <button class="page-action-btn page-action-delete" title="Delete page" @click="deletePage(page.id)">
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
- </div>
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
- <div v-if="showNewVersion" class="new-form">
395
- <input v-model="newVersion" class="edit-input" placeholder="Version (e.g. 1.0.0)" />
396
- <label class="cpub-checkbox">
397
- <input type="checkbox" v-model="newVersionDefault" /> Set as default version
398
- </label>
399
- <div class="form-actions">
400
- <button class="cpub-btn cpub-btn-sm cpub-btn-primary" :disabled="savingVersion || !newVersion.trim()" @click="createVersion">
401
- {{ savingVersion ? 'Creating...' : 'Create Version' }}
402
- </button>
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
- <p v-else class="edit-empty">No versions yet.</p>
414
- </section>
626
+ </Teleport>
415
627
  </div>
416
628
  </template>
417
629
 
418
630
  <style scoped>
419
- .docs-edit { max-width: 800px; margin: 0 auto; padding: 32px; }
420
- .docs-edit-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 24px; }
421
- .page-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
422
- .cpub-back-link { color: var(--accent); text-decoration: none; font-size: 12px; }
423
- .cpub-back-link:hover { text-decoration: underline; }
424
-
425
- .edit-section { border: var(--border-width-default) solid var(--border); background: var(--surface); padding: 20px; margin-bottom: 16px; box-shadow: var(--shadow-md); }
426
- .section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
427
- .section-heading { font-size: 14px; font-weight: 700; display: flex; align-items: center; gap: 8px; }
428
- .section-heading i { font-size: 12px; color: var(--accent); }
429
-
430
- .settings-form { display: flex; flex-direction: column; gap: 10px; }
431
- .form-field { display: flex; flex-direction: column; gap: 3px; }
432
- .form-label { font-size: 10px; font-weight: 600; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
433
- .form-row { display: flex; gap: 10px; }
434
-
435
- .new-form { display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 2px dashed var(--border); margin-bottom: 16px; background: var(--surface2); }
436
- .edit-input { padding: 6px 10px; border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: 13px; }
437
- .edit-input:focus { border-color: var(--accent); outline: none; }
438
- .edit-select { padding: 6px 10px; border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: 12px; }
439
- .edit-select:focus { border-color: var(--accent); outline: none; }
440
- .edit-textarea { padding: 8px 10px; border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: 13px; resize: vertical; font-family: inherit; }
441
- .edit-textarea-md { font-family: var(--font-mono); font-size: 13px; line-height: 1.6; min-height: 120px; }
442
- .edit-textarea:focus { border-color: var(--accent); outline: none; }
443
- .form-actions { display: flex; gap: 8px; margin-top: 4px; }
444
-
445
- /* Markdown toolbar */
446
- .md-toolbar { display: flex; gap: 2px; padding: 4px; background: var(--surface2); border: var(--border-width-default) solid var(--border); border-bottom: none; }
447
- .md-btn { padding: 4px 8px; background: none; border: var(--border-width-default) solid transparent; color: var(--text-dim); cursor: pointer; font-size: 12px; font-family: var(--font-mono); display: inline-flex; align-items: center; gap: 4px; }
448
- .md-btn:hover { background: var(--surface); border-color: var(--border); }
449
- .md-btn.active { background: var(--accent-bg); color: var(--accent); border-color: var(--accent-border); }
450
- .md-spacer { flex: 1; }
451
-
452
- /* Editor + Preview pane */
453
- .editor-pane { display: flex; gap: 0; }
454
- .editor-pane .edit-textarea-md { flex: 1; border-top: none; }
455
- .editor-pane.with-preview .edit-textarea-md { width: 50%; }
456
- .preview-pane { flex: 1; padding: 0; border: var(--border-width-default) solid var(--border); border-left: none; border-top: none; background: var(--surface); overflow-y: auto; max-height: 400px; }
457
- .preview-md { margin: 0; padding: 12px 16px; font-size: 12px; font-family: var(--font-mono); line-height: 1.6; color: var(--text-dim); white-space: pre-wrap; word-wrap: break-word; }
458
-
459
- /* Page list */
460
- .page-list { display: flex; flex-direction: column; }
461
- .page-item { padding: 12px 0; border-bottom: var(--border-width-default) solid var(--border2); }
462
- .page-item:last-child { border-bottom: none; }
463
- .page-item-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
464
- .page-item-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
465
- .page-item-title { font-size: 13px; font-weight: 600; }
466
- .page-item-slug { font-size: 11px; font-family: var(--font-mono); color: var(--text-faint); }
467
- .page-item-child-badge { font-size: 9px; font-family: var(--font-mono); color: var(--accent); background: var(--accent-bg); padding: 1px 6px; border: var(--border-width-default) solid var(--accent-border); }
468
- .page-item-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
469
- .page-action-btn { padding: 4px 6px; background: none; border: var(--border-width-default) solid var(--border2); color: var(--text-faint); cursor: pointer; font-size: 10px; }
470
- .page-action-btn:hover { color: var(--text); border-color: var(--border); }
471
- .page-action-btn:disabled { opacity: 0.3; cursor: not-allowed; }
472
- .page-action-delete:hover { color: var(--red); border-color: var(--red); }
473
-
474
- .version-list { display: flex; flex-wrap: wrap; gap: 8px; }
475
- .version-item { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border: var(--border-width-default) solid var(--border); background: var(--surface2); font-size: 12px; font-family: var(--font-mono); }
476
- .version-label { font-weight: 600; }
477
- .version-default-badge { font-size: 10px; padding: 1px 6px; background: var(--accent-bg); color: var(--accent); border: var(--border-width-default) solid var(--accent-border); }
478
-
479
- .edit-empty { color: var(--text-faint); font-size: 12px; padding: 16px 0; }
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>