@commonpub/layer 0.4.5 → 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 +7 -7
- package/pages/docs/[siteSlug]/[...pagePath].vue +20 -6
- package/pages/docs/[siteSlug]/edit.vue +1021 -383
- package/pages/index.vue +42 -0
- 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
- package/theme/agora-dark.css +3 -0
- package/theme/agora.css +3 -0
package/components/SiteLogo.vue
CHANGED
|
@@ -6,18 +6,26 @@
|
|
|
6
6
|
<span class="cpub-logo-name">CommonPub</span>
|
|
7
7
|
</span>
|
|
8
8
|
|
|
9
|
-
<!-- Agora logo: Town Square mark + Fraunces wordmark -->
|
|
9
|
+
<!-- Agora logo: Textured Town Square mark + Fraunces wordmark -->
|
|
10
10
|
<span class="cpub-logo-agora">
|
|
11
|
-
<svg class="cpub-logo-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
<svg class="cpub-logo-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="24" height="24" aria-hidden="true">
|
|
12
|
+
<defs>
|
|
13
|
+
<filter id="cpub-tex" x="-5%" y="-5%" width="110%" height="110%">
|
|
14
|
+
<feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="4" result="noise"/>
|
|
15
|
+
<feDisplacementMap in="SourceGraphic" in2="noise" scale="2"/>
|
|
16
|
+
</filter>
|
|
17
|
+
</defs>
|
|
18
|
+
<g filter="url(#cpub-tex)">
|
|
19
|
+
<rect x="18" y="18" width="72" height="72" fill="none" stroke="currentColor" stroke-width="7" stroke-linejoin="round"/>
|
|
20
|
+
<rect x="110" y="18" width="72" height="72" fill="none" stroke="currentColor" stroke-width="7" stroke-linejoin="round"/>
|
|
21
|
+
<rect x="18" y="110" width="72" height="72" fill="none" stroke="currentColor" stroke-width="7" stroke-linejoin="round"/>
|
|
22
|
+
<rect x="110" y="110" width="72" height="72" fill="none" stroke="currentColor" stroke-width="7" stroke-linejoin="round"/>
|
|
23
|
+
<rect x="88" y="88" width="24" height="24" fill="var(--accent, #3d8b5e)"/>
|
|
24
|
+
<line x1="34" y1="54" x2="72" y2="54" stroke="var(--accent, #3d8b5e)" stroke-width="3.5" stroke-linecap="round" opacity="0.7"/>
|
|
25
|
+
<line x1="128" y1="46" x2="166" y2="46" stroke="var(--accent, #3d8b5e)" stroke-width="3.5" stroke-linecap="round" opacity="0.7"/>
|
|
26
|
+
<line x1="34" y1="148" x2="68" y2="148" stroke="var(--accent, #3d8b5e)" stroke-width="3.5" stroke-linecap="round" opacity="0.7"/>
|
|
27
|
+
<line x1="128" y1="142" x2="162" y2="142" stroke="var(--accent, #3d8b5e)" stroke-width="3.5" stroke-linecap="round" opacity="0.7"/>
|
|
28
|
+
</g>
|
|
21
29
|
</svg>
|
|
22
30
|
<span class="cpub-logo-wordmark">Common<span class="cpub-logo-pub">Pub</span></span>
|
|
23
31
|
</span>
|
|
@@ -43,7 +51,6 @@
|
|
|
43
51
|
.cpub-logo-bracket { color: var(--accent); font-size: 15px; }
|
|
44
52
|
.cpub-logo-name { margin-left: 2px; }
|
|
45
53
|
|
|
46
|
-
/* Agora logo hidden by default — shown via agora.css */
|
|
47
54
|
.cpub-logo-agora {
|
|
48
55
|
display: none;
|
|
49
56
|
align-items: center;
|
|
@@ -184,9 +184,9 @@ function onDragEnd(event: DragEvent): void {
|
|
|
184
184
|
min-height: 20px;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
/* Touch devices:
|
|
187
|
+
/* Touch devices: only show controls on selected block since there's no hover */
|
|
188
188
|
@media (hover: none) {
|
|
189
|
-
.cpub-block-handle { opacity: 1; }
|
|
190
|
-
.cpub-block-controls { opacity: 1; }
|
|
189
|
+
.cpub-block-wrap--selected .cpub-block-handle { opacity: 1; }
|
|
190
|
+
.cpub-block-wrap--selected .cpub-block-controls { opacity: 1; }
|
|
191
191
|
}
|
|
192
192
|
</style>
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
export interface PageTreeItem {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
parentId: string | null;
|
|
7
|
+
sortOrder: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PageTreeNode extends PageTreeItem {
|
|
11
|
+
children: PageTreeNode[];
|
|
12
|
+
depth: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
pages: PageTreeItem[];
|
|
17
|
+
selectedPageId: string | null;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{
|
|
21
|
+
select: [pageId: string];
|
|
22
|
+
create: [parentId: string | null, title: string];
|
|
23
|
+
rename: [pageId: string, title: string];
|
|
24
|
+
delete: [pageId: string];
|
|
25
|
+
reorder: [pageIds: string[]];
|
|
26
|
+
reparent: [pageId: string, newParentId: string | null];
|
|
27
|
+
}>();
|
|
28
|
+
|
|
29
|
+
// Build nested tree from flat pages
|
|
30
|
+
const tree = computed<PageTreeNode[]>(() => {
|
|
31
|
+
return buildTree(props.pages, null, 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function buildTree(pages: PageTreeItem[], parentId: string | null, depth: number): PageTreeNode[] {
|
|
35
|
+
return pages
|
|
36
|
+
.filter(p => p.parentId === parentId)
|
|
37
|
+
.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
38
|
+
.map(p => ({
|
|
39
|
+
...p,
|
|
40
|
+
depth,
|
|
41
|
+
children: buildTree(pages, p.id, depth + 1),
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Expand/collapse state
|
|
46
|
+
const expandedIds = ref<Set<string>>(new Set());
|
|
47
|
+
|
|
48
|
+
// Auto-expand parents of selected page
|
|
49
|
+
watch(() => props.selectedPageId, (selectedId) => {
|
|
50
|
+
if (!selectedId) return;
|
|
51
|
+
const ancestors = getAncestors(selectedId);
|
|
52
|
+
for (const id of ancestors) {
|
|
53
|
+
expandedIds.value.add(id);
|
|
54
|
+
}
|
|
55
|
+
}, { immediate: true });
|
|
56
|
+
|
|
57
|
+
function getAncestors(pageId: string): string[] {
|
|
58
|
+
const result: string[] = [];
|
|
59
|
+
let current = props.pages.find(p => p.id === pageId);
|
|
60
|
+
while (current?.parentId) {
|
|
61
|
+
result.push(current.parentId);
|
|
62
|
+
current = props.pages.find(p => p.id === current!.parentId);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toggleExpanded(id: string): void {
|
|
68
|
+
const s = new Set(expandedIds.value);
|
|
69
|
+
if (s.has(id)) s.delete(id);
|
|
70
|
+
else s.add(id);
|
|
71
|
+
expandedIds.value = s;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isExpanded(id: string): boolean {
|
|
75
|
+
return expandedIds.value.has(id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Inline add page
|
|
79
|
+
const addingUnder = ref<string | null | 'root'>(null);
|
|
80
|
+
const newPageTitle = ref('');
|
|
81
|
+
|
|
82
|
+
function startAdd(parentId: string | null): void {
|
|
83
|
+
addingUnder.value = parentId ?? 'root';
|
|
84
|
+
newPageTitle.value = '';
|
|
85
|
+
nextTick(() => {
|
|
86
|
+
const input = document.querySelector('.cpub-tree-add-input') as HTMLInputElement | null;
|
|
87
|
+
input?.focus();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function confirmAdd(): void {
|
|
92
|
+
if (!newPageTitle.value.trim()) {
|
|
93
|
+
addingUnder.value = null;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const parentId = addingUnder.value === 'root' ? null : addingUnder.value;
|
|
97
|
+
emit('create', parentId, newPageTitle.value.trim());
|
|
98
|
+
addingUnder.value = null;
|
|
99
|
+
newPageTitle.value = '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cancelAdd(): void {
|
|
103
|
+
addingUnder.value = null;
|
|
104
|
+
newPageTitle.value = '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Compute depth for the inline add row (child depth = parent depth + 1)
|
|
108
|
+
const addingDepth = computed(() => {
|
|
109
|
+
if (addingUnder.value === null || addingUnder.value === 'root') return 0;
|
|
110
|
+
// Find the parent node's depth in the flat list
|
|
111
|
+
const parentNode = flatNodes.value.find(n => n.id === addingUnder.value);
|
|
112
|
+
return parentNode ? parentNode.depth + 1 : 1;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Context menu
|
|
116
|
+
const contextMenu = ref<{ pageId: string; x: number; y: number } | null>(null);
|
|
117
|
+
|
|
118
|
+
function showContext(pageId: string, event: MouseEvent): void {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
contextMenu.value = { pageId, x: event.clientX, y: event.clientY };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function closeContext(): void {
|
|
124
|
+
contextMenu.value = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function contextRename(): void {
|
|
128
|
+
if (!contextMenu.value) return;
|
|
129
|
+
const page = props.pages.find(p => p.id === contextMenu.value!.pageId);
|
|
130
|
+
if (!page) return;
|
|
131
|
+
const newTitle = prompt('Rename page:', page.title);
|
|
132
|
+
if (newTitle && newTitle.trim()) {
|
|
133
|
+
emit('rename', page.id, newTitle.trim());
|
|
134
|
+
}
|
|
135
|
+
closeContext();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function contextDelete(): void {
|
|
139
|
+
if (!contextMenu.value) return;
|
|
140
|
+
const page = props.pages.find(p => p.id === contextMenu.value!.pageId);
|
|
141
|
+
if (!page) return;
|
|
142
|
+
if (confirm(`Delete "${page.title}"? This cannot be undone.`)) {
|
|
143
|
+
emit('delete', page.id);
|
|
144
|
+
}
|
|
145
|
+
closeContext();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function contextAddChild(): void {
|
|
149
|
+
if (!contextMenu.value) return;
|
|
150
|
+
const parentId = contextMenu.value.pageId;
|
|
151
|
+
expandedIds.value.add(parentId);
|
|
152
|
+
closeContext();
|
|
153
|
+
startAdd(parentId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Drag and drop
|
|
157
|
+
const draggedId = ref<string | null>(null);
|
|
158
|
+
const dropTargetId = ref<string | null>(null);
|
|
159
|
+
const dropPosition = ref<'before' | 'inside' | 'after' | null>(null);
|
|
160
|
+
|
|
161
|
+
function onDragStart(pageId: string, event: DragEvent): void {
|
|
162
|
+
draggedId.value = pageId;
|
|
163
|
+
if (event.dataTransfer) {
|
|
164
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
165
|
+
event.dataTransfer.setData('text/plain', pageId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function onDragOver(targetId: string, event: DragEvent): void {
|
|
170
|
+
if (draggedId.value === targetId) return;
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
|
|
173
|
+
|
|
174
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
175
|
+
const y = event.clientY - rect.top;
|
|
176
|
+
const h = rect.height;
|
|
177
|
+
|
|
178
|
+
dropTargetId.value = targetId;
|
|
179
|
+
if (y < h * 0.25) dropPosition.value = 'before';
|
|
180
|
+
else if (y > h * 0.75) dropPosition.value = 'after';
|
|
181
|
+
else dropPosition.value = 'inside';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function onDragLeave(): void {
|
|
185
|
+
dropTargetId.value = null;
|
|
186
|
+
dropPosition.value = null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function onDrop(targetId: string): void {
|
|
190
|
+
if (!draggedId.value || draggedId.value === targetId) return;
|
|
191
|
+
|
|
192
|
+
const target = props.pages.find(p => p.id === targetId);
|
|
193
|
+
if (!target) return;
|
|
194
|
+
|
|
195
|
+
if (dropPosition.value === 'inside') {
|
|
196
|
+
// Reparent: make dragged page a child of target
|
|
197
|
+
emit('reparent', draggedId.value, targetId);
|
|
198
|
+
expandedIds.value.add(targetId);
|
|
199
|
+
} else {
|
|
200
|
+
// Before/after: move dragged page to target's parent and set sibling order
|
|
201
|
+
const newParentId = target.parentId;
|
|
202
|
+
const dragId = draggedId.value;
|
|
203
|
+
|
|
204
|
+
// Compute the new sibling order INCLUDING the dragged page
|
|
205
|
+
const siblings = props.pages
|
|
206
|
+
.filter(p => p.parentId === newParentId && p.id !== dragId)
|
|
207
|
+
.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
208
|
+
|
|
209
|
+
const targetIdx = siblings.findIndex(p => p.id === targetId);
|
|
210
|
+
const insertIdx = dropPosition.value === 'before' ? targetIdx : targetIdx + 1;
|
|
211
|
+
const ordered = [...siblings];
|
|
212
|
+
const draggedPage = props.pages.find(p => p.id === dragId);
|
|
213
|
+
if (draggedPage) {
|
|
214
|
+
ordered.splice(insertIdx, 0, draggedPage);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Emit a single 'move' event so parent can handle atomically
|
|
218
|
+
emit('reparent', dragId, newParentId);
|
|
219
|
+
emit('reorder', ordered.map(p => p.id));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
draggedId.value = null;
|
|
223
|
+
dropTargetId.value = null;
|
|
224
|
+
dropPosition.value = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function onDragEnd(): void {
|
|
228
|
+
draggedId.value = null;
|
|
229
|
+
dropTargetId.value = null;
|
|
230
|
+
dropPosition.value = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Flatten tree for rendering
|
|
234
|
+
function flattenForRender(nodes: PageTreeNode[]): PageTreeNode[] {
|
|
235
|
+
const result: PageTreeNode[] = [];
|
|
236
|
+
for (const node of nodes) {
|
|
237
|
+
result.push(node);
|
|
238
|
+
if (node.children.length > 0 && isExpanded(node.id)) {
|
|
239
|
+
result.push(...flattenForRender(node.children));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const flatNodes = computed(() => flattenForRender(tree.value));
|
|
246
|
+
|
|
247
|
+
// Close context menu on click outside
|
|
248
|
+
onMounted(() => {
|
|
249
|
+
document.addEventListener('click', closeContext);
|
|
250
|
+
});
|
|
251
|
+
onUnmounted(() => {
|
|
252
|
+
document.removeEventListener('click', closeContext);
|
|
253
|
+
});
|
|
254
|
+
</script>
|
|
255
|
+
|
|
256
|
+
<template>
|
|
257
|
+
<div class="cpub-page-tree" role="tree" aria-label="Pages">
|
|
258
|
+
<div
|
|
259
|
+
v-for="node in flatNodes"
|
|
260
|
+
:key="node.id"
|
|
261
|
+
class="cpub-tree-item"
|
|
262
|
+
:class="{
|
|
263
|
+
'cpub-tree-item-selected': selectedPageId === node.id,
|
|
264
|
+
'cpub-tree-item-dragging': draggedId === node.id,
|
|
265
|
+
'cpub-tree-item-drop-before': dropTargetId === node.id && dropPosition === 'before',
|
|
266
|
+
'cpub-tree-item-drop-inside': dropTargetId === node.id && dropPosition === 'inside',
|
|
267
|
+
'cpub-tree-item-drop-after': dropTargetId === node.id && dropPosition === 'after',
|
|
268
|
+
}"
|
|
269
|
+
:style="{ paddingLeft: `${12 + node.depth * 16}px` }"
|
|
270
|
+
role="treeitem"
|
|
271
|
+
:aria-selected="selectedPageId === node.id"
|
|
272
|
+
:aria-expanded="node.children.length > 0 ? isExpanded(node.id) : undefined"
|
|
273
|
+
draggable="true"
|
|
274
|
+
@click="emit('select', node.id)"
|
|
275
|
+
@contextmenu="showContext(node.id, $event)"
|
|
276
|
+
@dragstart="onDragStart(node.id, $event)"
|
|
277
|
+
@dragover="onDragOver(node.id, $event)"
|
|
278
|
+
@dragleave="onDragLeave"
|
|
279
|
+
@drop.prevent="onDrop(node.id)"
|
|
280
|
+
@dragend="onDragEnd"
|
|
281
|
+
>
|
|
282
|
+
<!-- Expand/collapse toggle -->
|
|
283
|
+
<button
|
|
284
|
+
v-if="node.children.length > 0"
|
|
285
|
+
class="cpub-tree-toggle"
|
|
286
|
+
:aria-label="isExpanded(node.id) ? 'Collapse' : 'Expand'"
|
|
287
|
+
@click.stop="toggleExpanded(node.id)"
|
|
288
|
+
>
|
|
289
|
+
<i class="fa-solid" :class="isExpanded(node.id) ? 'fa-chevron-down' : 'fa-chevron-right'" />
|
|
290
|
+
</button>
|
|
291
|
+
<span v-else class="cpub-tree-toggle-spacer" />
|
|
292
|
+
|
|
293
|
+
<i class="fa-solid fa-file-lines cpub-tree-icon" />
|
|
294
|
+
<span class="cpub-tree-title">{{ node.title || 'Untitled' }}</span>
|
|
295
|
+
|
|
296
|
+
<!-- Kebab menu -->
|
|
297
|
+
<button class="cpub-tree-kebab" aria-label="Page actions" @click.stop="showContext(node.id, $event)">
|
|
298
|
+
<i class="fa-solid fa-ellipsis-vertical" />
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<!-- Inline add at root or under parent -->
|
|
303
|
+
<div
|
|
304
|
+
v-if="addingUnder !== null"
|
|
305
|
+
class="cpub-tree-add-row"
|
|
306
|
+
:style="{ paddingLeft: `${12 + addingDepth * 16}px` }"
|
|
307
|
+
>
|
|
308
|
+
<i class="fa-solid fa-file-circle-plus cpub-tree-icon cpub-tree-icon-add" />
|
|
309
|
+
<input
|
|
310
|
+
v-model="newPageTitle"
|
|
311
|
+
class="cpub-tree-add-input"
|
|
312
|
+
placeholder="Page title..."
|
|
313
|
+
@keydown.enter="confirmAdd"
|
|
314
|
+
@keydown.escape="cancelAdd"
|
|
315
|
+
@blur="confirmAdd"
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<!-- Add page button -->
|
|
320
|
+
<button class="cpub-tree-add-btn" @click="startAdd(null)">
|
|
321
|
+
<i class="fa-solid fa-plus" /> Add Page
|
|
322
|
+
</button>
|
|
323
|
+
|
|
324
|
+
<!-- Context menu -->
|
|
325
|
+
<Teleport to="body">
|
|
326
|
+
<div
|
|
327
|
+
v-if="contextMenu"
|
|
328
|
+
class="cpub-tree-context"
|
|
329
|
+
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
|
|
330
|
+
@click.stop
|
|
331
|
+
>
|
|
332
|
+
<button class="cpub-tree-context-item" @click="contextRename">
|
|
333
|
+
<i class="fa-solid fa-pen" /> Rename
|
|
334
|
+
</button>
|
|
335
|
+
<button class="cpub-tree-context-item" @click="contextAddChild">
|
|
336
|
+
<i class="fa-solid fa-folder-plus" /> Add Child
|
|
337
|
+
</button>
|
|
338
|
+
<button class="cpub-tree-context-item cpub-tree-context-danger" @click="contextDelete">
|
|
339
|
+
<i class="fa-solid fa-trash" /> Delete
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
</Teleport>
|
|
343
|
+
</div>
|
|
344
|
+
</template>
|
|
345
|
+
|
|
346
|
+
<style scoped>
|
|
347
|
+
.cpub-page-tree {
|
|
348
|
+
display: flex;
|
|
349
|
+
flex-direction: column;
|
|
350
|
+
padding: 4px 0;
|
|
351
|
+
user-select: none;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.cpub-tree-item {
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
gap: 6px;
|
|
358
|
+
padding: 6px 8px 6px 12px;
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
transition: background 0.1s;
|
|
361
|
+
position: relative;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.cpub-tree-item:hover {
|
|
365
|
+
background: var(--surface2);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.cpub-tree-item-selected {
|
|
369
|
+
background: var(--accent-bg);
|
|
370
|
+
border-left: 2px solid var(--accent);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.cpub-tree-item-dragging {
|
|
374
|
+
opacity: 0.4;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.cpub-tree-item-drop-before::before {
|
|
378
|
+
content: '';
|
|
379
|
+
position: absolute;
|
|
380
|
+
top: 0;
|
|
381
|
+
left: 8px;
|
|
382
|
+
right: 8px;
|
|
383
|
+
height: 2px;
|
|
384
|
+
background: var(--accent);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.cpub-tree-item-drop-inside {
|
|
388
|
+
background: var(--accent-bg);
|
|
389
|
+
outline: 2px solid var(--accent);
|
|
390
|
+
outline-offset: -2px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.cpub-tree-item-drop-after::after {
|
|
394
|
+
content: '';
|
|
395
|
+
position: absolute;
|
|
396
|
+
bottom: 0;
|
|
397
|
+
left: 8px;
|
|
398
|
+
right: 8px;
|
|
399
|
+
height: 2px;
|
|
400
|
+
background: var(--accent);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.cpub-tree-toggle {
|
|
404
|
+
width: 16px;
|
|
405
|
+
height: 16px;
|
|
406
|
+
display: flex;
|
|
407
|
+
align-items: center;
|
|
408
|
+
justify-content: center;
|
|
409
|
+
background: none;
|
|
410
|
+
border: none;
|
|
411
|
+
color: var(--text-faint);
|
|
412
|
+
cursor: pointer;
|
|
413
|
+
font-size: 8px;
|
|
414
|
+
padding: 0;
|
|
415
|
+
flex-shrink: 0;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.cpub-tree-toggle:hover {
|
|
419
|
+
color: var(--text);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.cpub-tree-toggle-spacer {
|
|
423
|
+
width: 16px;
|
|
424
|
+
flex-shrink: 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.cpub-tree-icon {
|
|
428
|
+
font-size: 11px;
|
|
429
|
+
color: var(--text-faint);
|
|
430
|
+
flex-shrink: 0;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.cpub-tree-icon-add {
|
|
434
|
+
color: var(--accent);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.cpub-tree-title {
|
|
438
|
+
font-size: 12px;
|
|
439
|
+
font-weight: 500;
|
|
440
|
+
color: var(--text);
|
|
441
|
+
white-space: nowrap;
|
|
442
|
+
overflow: hidden;
|
|
443
|
+
text-overflow: ellipsis;
|
|
444
|
+
flex: 1;
|
|
445
|
+
min-width: 0;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.cpub-tree-item-selected .cpub-tree-title {
|
|
449
|
+
color: var(--accent);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.cpub-tree-kebab {
|
|
453
|
+
width: 20px;
|
|
454
|
+
height: 20px;
|
|
455
|
+
display: flex;
|
|
456
|
+
align-items: center;
|
|
457
|
+
justify-content: center;
|
|
458
|
+
background: none;
|
|
459
|
+
border: none;
|
|
460
|
+
color: var(--text-faint);
|
|
461
|
+
cursor: pointer;
|
|
462
|
+
font-size: 10px;
|
|
463
|
+
padding: 0;
|
|
464
|
+
flex-shrink: 0;
|
|
465
|
+
opacity: 0;
|
|
466
|
+
transition: opacity 0.1s;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.cpub-tree-item:hover .cpub-tree-kebab {
|
|
470
|
+
opacity: 1;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.cpub-tree-kebab:hover {
|
|
474
|
+
color: var(--text);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.cpub-tree-add-row {
|
|
478
|
+
display: flex;
|
|
479
|
+
align-items: center;
|
|
480
|
+
gap: 6px;
|
|
481
|
+
padding: 4px 8px;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.cpub-tree-add-input {
|
|
485
|
+
flex: 1;
|
|
486
|
+
padding: 3px 6px;
|
|
487
|
+
background: var(--surface);
|
|
488
|
+
border: var(--border-width-default) solid var(--accent);
|
|
489
|
+
color: var(--text);
|
|
490
|
+
font-size: 12px;
|
|
491
|
+
outline: none;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.cpub-tree-add-btn {
|
|
495
|
+
display: flex;
|
|
496
|
+
align-items: center;
|
|
497
|
+
justify-content: center;
|
|
498
|
+
gap: 6px;
|
|
499
|
+
width: 100%;
|
|
500
|
+
padding: 8px;
|
|
501
|
+
margin-top: 4px;
|
|
502
|
+
background: none;
|
|
503
|
+
border: 2px dashed var(--border2);
|
|
504
|
+
color: var(--text-faint);
|
|
505
|
+
font-family: var(--font-mono);
|
|
506
|
+
font-size: 10px;
|
|
507
|
+
text-transform: uppercase;
|
|
508
|
+
letter-spacing: 0.06em;
|
|
509
|
+
cursor: pointer;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.cpub-tree-add-btn:hover {
|
|
513
|
+
border-color: var(--accent);
|
|
514
|
+
color: var(--accent);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* Context menu */
|
|
518
|
+
.cpub-tree-context {
|
|
519
|
+
position: fixed;
|
|
520
|
+
z-index: 1000;
|
|
521
|
+
background: var(--surface);
|
|
522
|
+
border: var(--border-width-default) solid var(--border);
|
|
523
|
+
box-shadow: var(--shadow-md);
|
|
524
|
+
min-width: 140px;
|
|
525
|
+
padding: 4px 0;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.cpub-tree-context-item {
|
|
529
|
+
display: flex;
|
|
530
|
+
align-items: center;
|
|
531
|
+
gap: 8px;
|
|
532
|
+
width: 100%;
|
|
533
|
+
padding: 6px 12px;
|
|
534
|
+
background: none;
|
|
535
|
+
border: none;
|
|
536
|
+
color: var(--text);
|
|
537
|
+
font-size: 12px;
|
|
538
|
+
cursor: pointer;
|
|
539
|
+
text-align: left;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.cpub-tree-context-item:hover {
|
|
543
|
+
background: var(--surface2);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.cpub-tree-context-item i {
|
|
547
|
+
width: 12px;
|
|
548
|
+
font-size: 10px;
|
|
549
|
+
color: var(--text-faint);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.cpub-tree-context-danger {
|
|
553
|
+
color: var(--red);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.cpub-tree-context-danger i {
|
|
557
|
+
color: var(--red);
|
|
558
|
+
}
|
|
559
|
+
</style>
|