904 lines
22 KiB
Vue
904 lines
22 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, nextTick, watch } from 'vue'
|
|
import { useTableStore } from '#/store/zq-smart-table'
|
|
import { SmartItemType, type Table } from '#/types/zq-smart-table/table'
|
|
import { useI18n } from '@vben/locales'
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
import {
|
|
Grid,
|
|
FileText,
|
|
Search,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Ellipsis,
|
|
Pencil,
|
|
Trash2,
|
|
FilePlus2,
|
|
BookOpen,
|
|
} from '@vben/icons'
|
|
import { ElMessageBox } from 'element-plus'
|
|
|
|
type TreeNode = Table & { children: TreeNode[] }
|
|
interface FlatNode {
|
|
id: string
|
|
name: string
|
|
type: SmartItemType
|
|
depth: number
|
|
hasChildren: boolean
|
|
parentId: string | null | undefined
|
|
}
|
|
|
|
const emit = defineEmits<{
|
|
selectSpace: [spaceId: string]
|
|
selectDoc: [tableId: string]
|
|
showSpaces: []
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const tableStore = useTableStore()
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
|
|
const SIDEBAR_DEFAULT = 256
|
|
const SIDEBAR_MIN = 200
|
|
const SIDEBAR_MAX = 480
|
|
|
|
const sidebarWidth = ref(SIDEBAR_DEFAULT)
|
|
const isResizing = ref(false)
|
|
const searchText = ref('')
|
|
|
|
function onResizeStart(e: MouseEvent) {
|
|
e.preventDefault()
|
|
isResizing.value = true
|
|
const startX = e.clientX
|
|
const startW = sidebarWidth.value
|
|
|
|
function onMove(ev: MouseEvent) {
|
|
const newW = startW + (ev.clientX - startX)
|
|
sidebarWidth.value = Math.max(SIDEBAR_MIN, Math.min(SIDEBAR_MAX, newW))
|
|
}
|
|
|
|
function onUp() {
|
|
isResizing.value = false
|
|
document.removeEventListener('mousemove', onMove)
|
|
document.removeEventListener('mouseup', onUp)
|
|
}
|
|
|
|
document.addEventListener('mousemove', onMove)
|
|
document.addEventListener('mouseup', onUp)
|
|
}
|
|
|
|
function onResizeDblClick() {
|
|
sidebarWidth.value = SIDEBAR_DEFAULT
|
|
}
|
|
|
|
// Active state
|
|
const activeView = computed(() => {
|
|
const tableId = route.params.tableId as string | undefined
|
|
if (tableId) return { type: 'doc' as const, id: tableId }
|
|
return { type: 'spaces' as const, id: null }
|
|
})
|
|
|
|
// ==================== My Documents Tree ====================
|
|
const expandedIds = ref<Set<string>>(new Set())
|
|
|
|
function toggleExpand(id: string) {
|
|
const s = new Set(expandedIds.value)
|
|
if (s.has(id)) s.delete(id)
|
|
else s.add(id)
|
|
expandedIds.value = s
|
|
}
|
|
|
|
function buildTree(items: Table[]): TreeNode[] {
|
|
const map = new Map<string, TreeNode>()
|
|
const roots: TreeNode[] = []
|
|
for (const item of items) {
|
|
map.set(item.id, { ...item, children: [] })
|
|
}
|
|
for (const node of map.values()) {
|
|
const pid = node.parentId
|
|
if (pid && map.has(pid)) {
|
|
map.get(pid)!.children.push(node)
|
|
} else {
|
|
roots.push(node)
|
|
}
|
|
}
|
|
return roots
|
|
}
|
|
|
|
function filterTree(nodes: TreeNode[], q: string): TreeNode[] {
|
|
if (!q) return nodes
|
|
const result: TreeNode[] = []
|
|
for (const node of nodes) {
|
|
const childMatch = filterTree(node.children, q)
|
|
if (node.name.toLowerCase().includes(q) || childMatch.length > 0) {
|
|
result.push({ ...node, children: childMatch })
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
function flattenTree(nodes: TreeNode[], depth = 0): FlatNode[] {
|
|
const result: FlatNode[] = []
|
|
for (const node of nodes) {
|
|
result.push({
|
|
id: node.id,
|
|
name: node.name,
|
|
type: node.type,
|
|
depth,
|
|
hasChildren: node.children.length > 0,
|
|
parentId: node.parentId,
|
|
})
|
|
if (node.children.length > 0 && expandedIds.value.has(node.id)) {
|
|
result.push(...flattenTree(node.children, depth + 1))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
const tree = computed(() => buildTree(tableStore.tables))
|
|
const filteredTree = computed(() => {
|
|
const q = searchText.value.trim().toLowerCase()
|
|
return filterTree(tree.value, q)
|
|
})
|
|
const flatList = computed(() => flattenTree(filteredTree.value))
|
|
const hasSearchResults = computed(() => flatList.value.length > 0)
|
|
|
|
watch(() => tableStore.tables, () => {
|
|
if (expandedIds.value.size === 0 && tableStore.tables.length > 0) {
|
|
const ids = new Set<string>()
|
|
function walk(nodes: TreeNode[]) {
|
|
for (const n of nodes) {
|
|
if (n.children.length > 0) {
|
|
ids.add(n.id)
|
|
walk(n.children)
|
|
}
|
|
}
|
|
}
|
|
walk(tree.value as TreeNode[])
|
|
expandedIds.value = ids
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// Drag state
|
|
const dragId = ref<string | null>(null)
|
|
const dropTargetId = ref<string | null>(null)
|
|
const dropPosition = ref<'inside' | 'before' | 'after' | null>(null)
|
|
|
|
function onDragStart(e: DragEvent, id: string) {
|
|
dragId.value = id
|
|
if (e.dataTransfer) {
|
|
e.dataTransfer.effectAllowed = 'move'
|
|
e.dataTransfer.setData('text/plain', id)
|
|
}
|
|
}
|
|
|
|
function onDragOver(e: DragEvent, id: string) {
|
|
e.preventDefault()
|
|
if (dragId.value === id) return
|
|
dropTargetId.value = id
|
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
const y = e.clientY - rect.top
|
|
const h = rect.height
|
|
if (y < h * 0.25) dropPosition.value = 'before'
|
|
else if (y > h * 0.75) dropPosition.value = 'after'
|
|
else dropPosition.value = 'inside'
|
|
}
|
|
|
|
function onDragLeave() {
|
|
dropTargetId.value = null
|
|
dropPosition.value = null
|
|
}
|
|
|
|
function onDrop(e: DragEvent) {
|
|
e.preventDefault()
|
|
if (!dragId.value || !dropTargetId.value || dragId.value === dropTargetId.value) {
|
|
resetDrag()
|
|
return
|
|
}
|
|
const targetItem = tableStore.tables.find(t => t.id === dropTargetId.value)
|
|
if (!targetItem) { resetDrag(); return }
|
|
const draggedId = dragId.value
|
|
if (dropPosition.value === 'inside') {
|
|
const children = tableStore.tables.filter(
|
|
(t) => (t.parentId ?? null) === dropTargetId.value && t.id !== draggedId,
|
|
)
|
|
const lastChild = children.length > 0 ? children[children.length - 1] : null
|
|
tableStore.moveTable(draggedId, dropTargetId.value, lastChild?.id ?? null)
|
|
expandedIds.value = new Set([...expandedIds.value, dropTargetId.value])
|
|
} else {
|
|
const newParentId = targetItem.parentId ?? null
|
|
const siblings = tableStore.tables.filter(
|
|
(t) => (t.parentId ?? null) === newParentId && t.id !== draggedId,
|
|
)
|
|
const targetIdx = siblings.findIndex((s) => s.id === dropTargetId.value)
|
|
let afterId: string | null = null
|
|
if (dropPosition.value === 'before') {
|
|
afterId = targetIdx > 0 ? (siblings[targetIdx - 1]?.id ?? null) : null
|
|
} else {
|
|
afterId = dropTargetId.value
|
|
}
|
|
tableStore.moveTable(draggedId, newParentId, afterId)
|
|
}
|
|
resetDrag()
|
|
}
|
|
|
|
function resetDrag() {
|
|
dragId.value = null
|
|
dropTargetId.value = null
|
|
dropPosition.value = null
|
|
}
|
|
|
|
function getItemDropClass(nodeId: string) {
|
|
if (dropTargetId.value !== nodeId) return ''
|
|
if (dropPosition.value === 'inside') return 'drop-inside'
|
|
if (dropPosition.value === 'before') return 'drop-before'
|
|
if (dropPosition.value === 'after') return 'drop-after'
|
|
return ''
|
|
}
|
|
|
|
// Rename
|
|
const renamingId = ref<string | null>(null)
|
|
const renameValue = ref('')
|
|
const renameInputEl = ref<HTMLInputElement | null>(null)
|
|
function setRenameRef(el: any) { renameInputEl.value = el }
|
|
|
|
function startRename(tableId: string, currentName: string) {
|
|
renamingId.value = tableId
|
|
renameValue.value = currentName
|
|
nextTick(() => {
|
|
renameInputEl.value?.focus()
|
|
renameInputEl.value?.select()
|
|
})
|
|
}
|
|
|
|
async function confirmRename(tableId: string) {
|
|
if (renameValue.value.trim()) {
|
|
const table = tableStore.tables.find((item) => item.id === tableId)
|
|
if (table) {
|
|
table.name = renameValue.value.trim()
|
|
const { updateTableApi } = await import('#/api/smart-table')
|
|
updateTableApi(tableId, { name: table.name }).catch(() => {})
|
|
}
|
|
}
|
|
renamingId.value = null
|
|
}
|
|
|
|
function handleDeleteTable(tableId: string, tableName: string) {
|
|
ElMessageBox.confirm(
|
|
t('sidebar.deleteTableConfirm', { name: tableName }),
|
|
t('sidebar.deleteTable'),
|
|
{
|
|
confirmButtonText: t('common.confirm'),
|
|
cancelButtonText: t('common.cancel'),
|
|
type: 'warning',
|
|
},
|
|
).then(async () => {
|
|
await tableStore.deleteTable(tableId)
|
|
if (activeView.value.type === 'doc' && activeView.value.id === tableId) {
|
|
router.push('/wiki')
|
|
}
|
|
}).catch(() => {})
|
|
}
|
|
|
|
function handleContextCommand(command: string, tableId: string, tableName: string) {
|
|
if (command === 'rename') startRename(tableId, tableName)
|
|
else if (command === 'delete') handleDeleteTable(tableId, tableName)
|
|
else if (command === 'addSubPage') handleAddSubPage(tableId)
|
|
}
|
|
|
|
// Actions
|
|
async function handleAddDocument() {
|
|
const name = t('document.untitled')
|
|
const newId = await tableStore.addDocument(name)
|
|
if (newId) {
|
|
await tableStore.loadTableFull(newId)
|
|
router.push(`/wiki/doc/${newId}`)
|
|
emit('selectDoc', newId)
|
|
}
|
|
}
|
|
|
|
async function handleAddSubPage(parentId: string) {
|
|
const name = t('document.untitled')
|
|
const newId = await tableStore.addSubPage(parentId, name)
|
|
if (newId) {
|
|
expandedIds.value = new Set([...expandedIds.value, parentId])
|
|
await tableStore.loadTableFull(newId)
|
|
router.push(`/wiki/doc/${newId}`)
|
|
emit('selectDoc', newId)
|
|
}
|
|
}
|
|
|
|
function handleSelectDoc(tableId: string) {
|
|
if (renamingId.value === tableId) return
|
|
tableStore.activeTableId = tableId
|
|
router.push(`/wiki/doc/${tableId}`)
|
|
emit('selectDoc', tableId)
|
|
}
|
|
|
|
function handleShowSpaces() {
|
|
router.push('/wiki')
|
|
emit('showSpaces')
|
|
}
|
|
|
|
function getNodeIcon(type: SmartItemType) {
|
|
return type === SmartItemType.Document ? FileText : Grid
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="zq-sidebar"
|
|
:class="{ 'is-resizing': isResizing }"
|
|
:style="{ width: `${sidebarWidth}px` }"
|
|
>
|
|
<div class="zq-sidebar__inner" :style="{ width: `${sidebarWidth}px` }">
|
|
<!-- Header -->
|
|
<div class="zq-sidebar__header">
|
|
<div class="zq-sidebar__brand">
|
|
<div class="zq-sidebar__logo">
|
|
<BookOpen class="w-4 h-4" />
|
|
</div>
|
|
<span class="zq-sidebar__brand-title">{{ t('wiki.title') }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<div class="zq-sidebar__search">
|
|
<Search class="zq-sidebar__search-icon" />
|
|
<input
|
|
v-model="searchText"
|
|
class="zq-sidebar__search-input"
|
|
:placeholder="t('wiki.searchDocPlaceholder')"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Scrollable content -->
|
|
<div class="zq-sidebar__content">
|
|
<!-- Wiki Spaces entry -->
|
|
<div class="zq-sidebar__section">
|
|
<div
|
|
class="zq-sidebar__section-header"
|
|
:class="{ 'is-active': activeView.type === 'spaces' }"
|
|
@click="handleShowSpaces"
|
|
>
|
|
<BookOpen class="zq-sidebar__section-icon" />
|
|
<span class="zq-sidebar__section-title">{{ t('wiki.wikiSpaces') }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Divider -->
|
|
<div class="zq-sidebar__divider" />
|
|
|
|
<!-- My Documents Section -->
|
|
<div class="zq-sidebar__section">
|
|
<div class="zq-sidebar__section-header zq-sidebar__section-header--docs">
|
|
<FileText class="zq-sidebar__section-icon" />
|
|
<span class="zq-sidebar__section-title">{{ t('wiki.myDocuments') }}</span>
|
|
</div>
|
|
|
|
<!-- Document tree -->
|
|
<template v-if="flatList.length > 0">
|
|
<div
|
|
v-for="node in flatList"
|
|
:key="node.id"
|
|
class="zq-sidebar__item"
|
|
:style="{ '--depth': node.depth }"
|
|
:class="[
|
|
{
|
|
'is-active': activeView.type === 'doc' && activeView.id === node.id,
|
|
'is-dragging': dragId === node.id,
|
|
},
|
|
getItemDropClass(node.id),
|
|
]"
|
|
draggable="true"
|
|
@dragstart="onDragStart($event, node.id)"
|
|
@dragover="onDragOver($event, node.id)"
|
|
@dragleave="onDragLeave"
|
|
@drop="onDrop($event)"
|
|
@dragend="resetDrag"
|
|
@click="handleSelectDoc(node.id)"
|
|
>
|
|
<button
|
|
v-if="node.hasChildren"
|
|
class="zq-sidebar__expand-toggle"
|
|
@click.stop="toggleExpand(node.id)"
|
|
>
|
|
<component
|
|
:is="expandedIds.has(node.id) ? ChevronDown : ChevronRight"
|
|
class="w-3.5 h-3.5"
|
|
/>
|
|
</button>
|
|
<span v-else class="zq-sidebar__expand-spacer" />
|
|
|
|
<component :is="getNodeIcon(node.type)" class="zq-sidebar__item-icon" />
|
|
|
|
<input
|
|
v-if="renamingId === node.id"
|
|
:ref="setRenameRef"
|
|
v-model="renameValue"
|
|
class="zq-sidebar__rename-input"
|
|
@blur="confirmRename(node.id)"
|
|
@keyup.enter="confirmRename(node.id)"
|
|
@keyup.escape="renamingId = null"
|
|
@click.stop
|
|
/>
|
|
<span v-else class="zq-sidebar__item-name">{{ node.name }}</span>
|
|
|
|
<el-dropdown
|
|
v-if="renamingId !== node.id"
|
|
trigger="click"
|
|
class="zq-sidebar__item-actions"
|
|
@command="(cmd: string) => handleContextCommand(cmd, node.id, node.name)"
|
|
>
|
|
<button class="zq-sidebar__more-btn" @click.stop>
|
|
<Ellipsis class="w-3.5 h-3.5" />
|
|
</button>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item command="addSubPage">
|
|
<div class="flex items-center gap-2">
|
|
<FilePlus2 class="w-3.5 h-3.5" />
|
|
<span>{{ t('sidebar.newSubPage') }}</span>
|
|
</div>
|
|
</el-dropdown-item>
|
|
<el-dropdown-item command="rename">
|
|
<div class="flex items-center gap-2">
|
|
<Pencil class="w-3.5 h-3.5" />
|
|
<span>{{ t('common.rename') }}</span>
|
|
</div>
|
|
</el-dropdown-item>
|
|
<el-dropdown-item command="delete" divided>
|
|
<div class="flex items-center gap-2" style="color: var(--zq-danger-color)">
|
|
<Trash2 class="w-3.5 h-3.5" />
|
|
<span>{{ t('common.delete') }}</span>
|
|
</div>
|
|
</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Empty my docs -->
|
|
<div v-if="!searchText && flatList.length === 0 && !tableStore.loading" class="zq-sidebar__empty">
|
|
{{ t('wiki.emptyMyDocs') }}
|
|
</div>
|
|
|
|
<!-- Search no results -->
|
|
<div v-if="searchText && !hasSearchResults" class="zq-sidebar__no-results">
|
|
<Search class="w-6 h-6" style="color: var(--zq-text-placeholder)" />
|
|
<span>{{ t('wiki.noResults') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bottom actions -->
|
|
<div class="zq-sidebar__footer">
|
|
<button class="zq-sidebar__footer-btn" @click="handleAddDocument">
|
|
<FilePlus2 class="w-4 h-4" />
|
|
<span>{{ t('sidebar.newDocument') }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Resize handle -->
|
|
<div
|
|
class="zq-sidebar__resize-handle"
|
|
@mousedown="onResizeStart"
|
|
@dblclick="onResizeDblClick"
|
|
/>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.zq-sidebar {
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
overflow: visible;
|
|
z-index: 20;
|
|
}
|
|
|
|
.zq-sidebar.is-resizing {
|
|
transition: none;
|
|
}
|
|
|
|
.zq-sidebar__inner {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
background: var(--zq-bg-sidebar);
|
|
border-right: 1px solid var(--zq-border-color-light);
|
|
}
|
|
|
|
/* Resize handle */
|
|
.zq-sidebar__resize-handle {
|
|
position: absolute;
|
|
top: 0;
|
|
right: -3px;
|
|
width: 3px;
|
|
height: 100%;
|
|
cursor: col-resize;
|
|
z-index: 30;
|
|
transition: background-color 0.15s;
|
|
}
|
|
|
|
.zq-sidebar__resize-handle:hover,
|
|
.zq-sidebar.is-resizing .zq-sidebar__resize-handle {
|
|
background-color: var(--zq-brand-color);
|
|
}
|
|
|
|
/* Header */
|
|
.zq-sidebar__header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 12px 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.zq-sidebar__brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.zq-sidebar__logo {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
background: var(--zq-brand-color);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.zq-sidebar__brand-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--zq-text-primary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* Search */
|
|
.zq-sidebar__search {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin: 4px 12px 8px;
|
|
padding: 7px 10px;
|
|
border-radius: 8px;
|
|
background: var(--zq-bg-primary);
|
|
border: 1px solid transparent;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.zq-sidebar__search:focus-within {
|
|
border-color: var(--zq-brand-color);
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--zq-brand-color) 15%, transparent);
|
|
}
|
|
|
|
.zq-sidebar__search-icon {
|
|
width: 14px;
|
|
height: 14px;
|
|
color: var(--zq-text-placeholder);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.zq-sidebar__search-input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
border: none;
|
|
outline: none;
|
|
background: transparent;
|
|
font-size: 13px;
|
|
color: var(--zq-text-primary);
|
|
line-height: 1;
|
|
}
|
|
|
|
.zq-sidebar__search-input::placeholder {
|
|
color: var(--zq-text-placeholder);
|
|
}
|
|
|
|
/* Scrollable content */
|
|
.zq-sidebar__content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
padding: 4px 0 16px;
|
|
}
|
|
|
|
.zq-sidebar__content::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.zq-sidebar__content::-webkit-scrollbar-thumb {
|
|
background: var(--zq-border-color);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.zq-sidebar__content::-webkit-scrollbar-thumb:hover {
|
|
background: var(--zq-text-placeholder);
|
|
}
|
|
|
|
/* Section */
|
|
.zq-sidebar__section {
|
|
padding: 0 8px;
|
|
}
|
|
|
|
.zq-sidebar__section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 8px;
|
|
margin: 2px 0;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--zq-text-secondary);
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.zq-sidebar__section-header:hover {
|
|
background: var(--zq-bg-cell-hover);
|
|
color: var(--zq-text-primary);
|
|
}
|
|
|
|
.zq-sidebar__section-header.is-active {
|
|
background: var(--zq-brand-color-light);
|
|
color: var(--zq-brand-color);
|
|
}
|
|
|
|
.zq-sidebar__section-header--docs {
|
|
cursor: default;
|
|
}
|
|
|
|
.zq-sidebar__section-header--docs:hover {
|
|
background: transparent;
|
|
color: var(--zq-text-secondary);
|
|
}
|
|
|
|
.zq-sidebar__section-icon {
|
|
width: 15px;
|
|
height: 15px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.zq-sidebar__section-title {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.zq-sidebar__section-add {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 22px;
|
|
height: 22px;
|
|
border-radius: 4px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--zq-text-placeholder);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.zq-sidebar__section-add:hover {
|
|
background: var(--zq-bg-cell-hover);
|
|
color: var(--zq-brand-color);
|
|
}
|
|
|
|
|
|
/* Divider */
|
|
.zq-sidebar__divider {
|
|
height: 1px;
|
|
margin: 6px 12px;
|
|
background: var(--zq-border-color-light);
|
|
}
|
|
|
|
/* Tree items (same as ZqTableSidebar) */
|
|
.zq-sidebar__item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 5px 8px 5px calc(8px + var(--depth, 0) * 16px);
|
|
margin: 1px 0;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
color: var(--zq-text-primary);
|
|
transition: background-color 0.15s, color 0.15s;
|
|
position: relative;
|
|
user-select: none;
|
|
}
|
|
|
|
.zq-sidebar__item:hover {
|
|
background: var(--zq-bg-cell-hover);
|
|
}
|
|
|
|
.zq-sidebar__item.is-active {
|
|
background: var(--zq-brand-color-light);
|
|
color: var(--zq-brand-color);
|
|
}
|
|
|
|
.zq-sidebar__item.is-active::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 6px;
|
|
bottom: 6px;
|
|
width: 3px;
|
|
border-radius: 0 2px 2px 0;
|
|
background: var(--zq-brand-color);
|
|
}
|
|
|
|
.zq-sidebar__item.is-dragging {
|
|
opacity: 0.4;
|
|
}
|
|
|
|
.zq-sidebar__item.drop-inside {
|
|
background: color-mix(in srgb, var(--zq-brand-color) 12%, transparent);
|
|
outline: 2px solid var(--zq-brand-color);
|
|
outline-offset: -2px;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.zq-sidebar__item.drop-before {
|
|
box-shadow: inset 0 2px 0 0 var(--zq-brand-color);
|
|
}
|
|
|
|
.zq-sidebar__item.drop-after {
|
|
box-shadow: inset 0 -2px 0 0 var(--zq-brand-color);
|
|
}
|
|
|
|
.zq-sidebar__expand-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 4px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--zq-text-placeholder);
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.zq-sidebar__expand-toggle:hover {
|
|
background: var(--zq-bg-cell-hover);
|
|
color: var(--zq-text-secondary);
|
|
}
|
|
|
|
.zq-sidebar__expand-spacer {
|
|
width: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.zq-sidebar__item-icon {
|
|
width: 15px;
|
|
height: 15px;
|
|
flex-shrink: 0;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.zq-sidebar__item.is-active .zq-sidebar__item-icon {
|
|
opacity: 1;
|
|
}
|
|
|
|
.zq-sidebar__item-name {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.zq-sidebar__rename-input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
padding: 1px 6px;
|
|
border-radius: 4px;
|
|
border: 1px solid var(--zq-brand-color);
|
|
outline: none;
|
|
background: var(--zq-bg-primary);
|
|
color: var(--zq-text-primary);
|
|
}
|
|
|
|
.zq-sidebar__item-actions {
|
|
flex-shrink: 0;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.zq-sidebar__more-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 4px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--zq-text-placeholder);
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition: all 0.15s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.zq-sidebar__item:hover .zq-sidebar__more-btn {
|
|
opacity: 1;
|
|
}
|
|
|
|
.zq-sidebar__more-btn:hover {
|
|
background: var(--zq-bg-secondary);
|
|
color: var(--zq-text-secondary);
|
|
}
|
|
|
|
/* Empty & No Results */
|
|
.zq-sidebar__empty {
|
|
padding: 16px;
|
|
font-size: 12px;
|
|
color: var(--zq-text-placeholder);
|
|
text-align: center;
|
|
}
|
|
|
|
.zq-sidebar__no-results {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 24px 16px;
|
|
font-size: 13px;
|
|
color: var(--zq-text-placeholder);
|
|
}
|
|
|
|
/* Footer */
|
|
.zq-sidebar__footer {
|
|
flex-shrink: 0;
|
|
border-top: 1px solid var(--zq-border-color-light);
|
|
padding: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.zq-sidebar__footer-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
padding: 7px 12px;
|
|
border-radius: 6px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--zq-text-secondary);
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.zq-sidebar__footer-btn:hover {
|
|
background: var(--zq-bg-cell-hover);
|
|
color: var(--zq-text-primary);
|
|
}
|
|
|
|
</style>
|