Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,903 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user