优化
This commit is contained in:
@@ -29,10 +29,12 @@
|
||||
.admin-density .el-empty__description { margin-top: 0; }
|
||||
.admin-density .el-empty__description p { font-size: var(--admin-font-body, 13px); }
|
||||
.admin-density-dialog .el-form-item__label { line-height: 22px; }
|
||||
.admin-density-dialog .el-form--inline .el-form-item__label { align-items: center; }
|
||||
.admin-density-dialog .el-descriptions__label.el-descriptions__cell { font-size: var(--admin-font-caption, 12px); background: var(--admin-label-bg); }
|
||||
.admin-density-dialog .el-descriptions__content.el-descriptions__cell { font-size: var(--admin-font-body, 13px); overflow-wrap: anywhere; }
|
||||
.admin-density-dialog.el-dialog.is-fullscreen { width: 100%; max-width: 100%; height: 100vh; max-height: 100vh; margin: 0; border-radius: 0; }
|
||||
.admin-density-dialog.el-dialog.admin-density-workspace:not(.is-fullscreen) { height: 92vh; max-height: 92vh; margin-top: 4vh; }
|
||||
.admin-density-dialog.el-dialog.admin-density-workspace:not(.is-fullscreen) { height: 92vh; max-height: 92vh; margin: 4vh auto; }
|
||||
.admin-density-dialog.el-dialog.admin-density-workspace > .el-dialog__body { flex: 1 1 auto; }
|
||||
.admin-density-dialog .el-dialog__headerbtn { top: 0; }
|
||||
.admin-density-dialog .app-dialog__native-fullscreen-control { top: 6px; }
|
||||
.admin-density-confirm.el-message-box { --el-font-size-base: var(--admin-font-body); border-radius: var(--admin-radius, 4px); padding: var(--admin-space-5, 12px) var(--admin-space-6, 16px); }
|
||||
|
||||
+142
-32
@@ -97,7 +97,7 @@ const notificationConsumers = [
|
||||
'notifications/components/TemplateEditorDialog.vue',
|
||||
]
|
||||
const systemConsumers = ['iam/pages/InternalUsersView.vue', 'iam/pages/RolesView.vue', 'iam/pages/PermissionsView.vue']
|
||||
const selfOperatedPages = ['self-operated/components/AnalyticsView.vue']
|
||||
const selfOperatedPages = ['self-operated/components/AnalyticsView.vue', 'self-operated/components/InventoryTurnoverView.vue']
|
||||
const selfOperatedConsumers = [...selfOperatedPages, 'self-operated/components/SkuTrend.vue', 'self-operated/pages/OverviewView.vue']
|
||||
const consumers = [
|
||||
...selfOperatedConsumers,
|
||||
@@ -156,14 +156,53 @@ const densityPages = [...new Set([
|
||||
])]
|
||||
type TemplateNode = NonNullable<NonNullable<ReturnType<typeof parse>['descriptor']['template']>['ast']>['children'][number]
|
||||
type ElementNode = Extract<TemplateNode, { type: 1 }>
|
||||
const classesOf = (node: ElementNode, preload = true, dense = preload, workbench = 'DOMESTIC'): string[] => node.props.flatMap(prop => {
|
||||
const classesOf = (node: ElementNode, preload = true, dense = preload, region = 'DOMESTIC', workbench = false, embedded = false): string[] => node.props.flatMap(prop => {
|
||||
if (prop.type === 6 && prop.name === 'class') return prop.value?.content.split(/\s+/) ?? []
|
||||
if (prop.type !== 7 || prop.name !== 'bind' || prop.arg?.type !== 4 || prop.arg.content !== 'class' || prop.exp?.type !== 4 || !prop.exp.content.includes('density')) return []
|
||||
// Evaluate only the authored density class binding with the shared component's two modes.
|
||||
const value = new Function('mode', 'isPreload', 'dense', 'isDomestic', 'isOverseas', `return (${prop.exp.content})`)(preload ? 'preload' : 'confirmed', preload, dense, workbench === 'DOMESTIC', workbench === 'OVERSEAS') as Record<string, boolean>
|
||||
// Keep visual opt-in and embedding independent from the shared component's density/region modes.
|
||||
const value = new Function('mode', 'isPreload', 'dense', 'isDomestic', 'isOverseas', 'workbench', 'embedded', `return (${prop.exp.content})`)(preload ? 'preload' : 'confirmed', preload, dense, region === 'DOMESTIC', region === 'OVERSEAS', workbench, embedded) as Record<string, boolean>
|
||||
const flatten = (value: unknown): string[] => typeof value === 'string' ? value.split(/\s+/) : Array.isArray(value) ? value.flatMap(flatten) : value && typeof value === 'object' ? Object.entries(value).filter(([, enabled]) => enabled).flatMap(([names]) => names.split(/\s+/)) : []
|
||||
return flatten(value)
|
||||
})
|
||||
const elementsOf = (source: string, tag?: string): ElementNode[] => {
|
||||
const parsed = parse(source)
|
||||
expect(parsed.errors).toEqual([])
|
||||
const result: ElementNode[] = []
|
||||
const visit = (nodes: TemplateNode[]) => {
|
||||
for (const node of nodes) {
|
||||
if (node.type !== 1) continue
|
||||
if (!tag || node.tag === tag) result.push(node)
|
||||
visit(node.children)
|
||||
}
|
||||
}
|
||||
visit(parsed.descriptor.template!.ast!.children)
|
||||
return result
|
||||
}
|
||||
const expectWorkbenchDialogs = (source: string) => {
|
||||
const dialogs = elementsOf(source, 'AppDialog')
|
||||
expect(dialogs.length).toBeGreaterThan(0)
|
||||
for (const dialog of dialogs) {
|
||||
expect(classesOf(dialog), dialog.loc.source.split('>')[0]).toEqual(expect.arrayContaining(['admin-density-dialog', 'admin-workbench', 'is-dialog']))
|
||||
}
|
||||
return dialogs
|
||||
}
|
||||
const confirmationClasses = (source: string): string[][] => {
|
||||
const script = source.includes('<script') ? parse(source).descriptor.scriptSetup!.content : source
|
||||
const ast = ts.createSourceFile('confirmation.ts', script, ts.ScriptTarget.Latest, true)
|
||||
const result: string[][] = []
|
||||
const visit = (node: ts.Node) => {
|
||||
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.expression.getText(ast) === 'ElMessageBox') {
|
||||
const options = node.arguments[2]
|
||||
expect(options && ts.isObjectLiteralExpression(options)).toBe(true)
|
||||
const customClass = options && ts.isObjectLiteralExpression(options) ? options.properties.find(prop => ts.isPropertyAssignment(prop) && prop.name.getText(ast) === 'customClass') : undefined
|
||||
expect(customClass && ts.isPropertyAssignment(customClass) && ts.isStringLiteral(customClass.initializer)).toBe(true)
|
||||
if (customClass && ts.isPropertyAssignment(customClass) && ts.isStringLiteral(customClass.initializer)) result.push(customClass.initializer.text.split(/\s+/))
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(ast)
|
||||
return result
|
||||
}
|
||||
const styles = [
|
||||
...parse(read('shared/components/BatchImportDialog.vue')).descriptor.styles.map(style => ({ file: 'BatchImportDialog.vue', css: style.content })),
|
||||
...consumers.flatMap(file => parse(read(`modules/${file}`)).descriptor.styles.map(style => ({ file, css: style.content }))),
|
||||
@@ -178,6 +217,24 @@ const cssRoot = (css: string) => {
|
||||
}
|
||||
|
||||
describe('approved Admin density ownership', () => {
|
||||
it('keeps sailing cutoff controls compact and delegates editor borders to the shared opt-in', () => {
|
||||
const view = parse(read('modules/sailing-calendar/pages/SailingCalendarView.vue')).descriptor
|
||||
const rules = new Map<string, Record<string, string>>()
|
||||
for (const style of view.styles) cssRoot(style.content).walkRules(rule => {
|
||||
const values: Record<string, string> = {}
|
||||
rule.walkDecls(decl => { values[decl.prop] = decl.value })
|
||||
rules.set(rule.selector, values)
|
||||
})
|
||||
expect(rules.get('.sailing-entry-label')).toEqual({ width: '116px' })
|
||||
expect(rules.get('.cutoff-presets')).toEqual({ flex: '0 0 auto' })
|
||||
expect(rules.get('.sailing-date-editor')).toMatchObject({ width: '280px', 'max-width': '100%' })
|
||||
expect(rules.get('.cutoff-rule-row, .cutoff-time-row')).toMatchObject({ 'flex-wrap': 'wrap' })
|
||||
expect(view.template!.content).toContain('density-table-editor density-table-editor--outlined cutoff-days-editor')
|
||||
expect(view.template!.content).toContain('density-table-editor--outlined cutoff-time-editor')
|
||||
expect(view.template!.content).toContain('role="status" aria-live="polite"')
|
||||
expect(view.styles.map(style => style.content).join('\n')).not.toContain('--el-input-border-color')
|
||||
})
|
||||
|
||||
it('outlines only the three opted-in customer billing editors without overriding native focus or disabled styles', () => {
|
||||
const rules = new Map<string, Record<string, string>>()
|
||||
cssRoot(shared).walkRules(rule => {
|
||||
@@ -362,7 +419,8 @@ describe('approved Admin density ownership', () => {
|
||||
it('opts transfer approval into the shared centered confirmation appearance', () => {
|
||||
const dialog = read('modules/overseas-warehouse/components/WarehouseTransferReviewDialog.vue')
|
||||
const css = read('styles/adminDensity.css')
|
||||
expect(dialog).toContain('class="admin-density-dialog density-confirm-dialog"')
|
||||
const [frame] = expectWorkbenchDialogs(dialog)
|
||||
expect(classesOf(frame!)).toContain('density-confirm-dialog')
|
||||
expect(dialog).toContain('align-center')
|
||||
expect(css).toContain('.admin-density-dialog.density-confirm-dialog.el-dialog:not(.is-fullscreen) { margin: auto; }')
|
||||
expect(css).toContain('.admin-density-dialog.density-confirm-dialog.el-dialog .el-dialog__header { border-bottom: 0; }')
|
||||
@@ -399,7 +457,8 @@ describe('approved Admin density ownership', () => {
|
||||
expect(dialogs).toHaveLength(3)
|
||||
for (const dialog of dialogs) { expect(dialog).toContain('admin-density-dialog'); expect(dialog).toContain('fullscreenable') }
|
||||
expect(view.template!.content).toContain('density-table density-table--editable')
|
||||
expect(view.template!.content).toContain('label-position="left" label-width="110px"')
|
||||
expect(view.template!.content).toContain('label-position="left" label-width="150px"')
|
||||
expect(view.template!.content).toContain('<el-date-picker v-model="plannedAt" type="datetime"')
|
||||
expect(view.styles.map(style => style.content).join('\n')).not.toContain('arrival-field')
|
||||
expect(create.template!.content).toContain('aria-label="预约保存回执"')
|
||||
expect(create.template!.content).not.toContain('<FbaAppointmentFields')
|
||||
@@ -421,7 +480,8 @@ describe('approved Admin density ownership', () => {
|
||||
expect(page.template!.content).toMatch(/class="[^"]*density-status-tabs[^"]*" role="tablist"/)
|
||||
expect(page.styles.map((style) => style.content).join('\n')).not.toMatch(/\.(status-tabs?|supplier-tabs|container-scope-tabs)\s*[{:.]/)
|
||||
}
|
||||
expect(read('modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue')).toContain('class="density-element-tabs density-status-tabs"')
|
||||
const returnTabs = elementsOf(read('modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue'), 'el-tabs')
|
||||
expect(returnTabs.some(node => ['density-element-tabs', 'density-status-tabs'].every(name => classesOf(node).includes(name)))).toBe(true)
|
||||
const selectors: string[] = []
|
||||
cssRoot(shared).walkRules((rule) => { selectors.push(...rule.selectors) })
|
||||
expect(selectors).toContain('.admin-density .density-status-tabs > button.is-active::after')
|
||||
@@ -436,15 +496,21 @@ describe('approved Admin density ownership', () => {
|
||||
|
||||
it('keeps dashboard visualizations outside detail-field table styling', () => {
|
||||
const dashboard = read('modules/dashboard/pages/DashboardView.vue')
|
||||
expect(dashboard).toContain('class="dashboard-metrics"')
|
||||
expect(dashboard).toContain('class="dashboard-metric-mark"')
|
||||
const metrics = elementsOf(dashboard, 'div').filter(node => classesOf(node).includes('dashboard-metrics'))
|
||||
expect(metrics).toHaveLength(1)
|
||||
const cards = elementsOf(dashboard, 'article').filter(node => classesOf(node).includes('dashboard-metric'))
|
||||
expect(cards).toHaveLength(1)
|
||||
expect(classesOf(cards[0]!)).toEqual(expect.arrayContaining(['workbench-metric', 'workbench-tone']))
|
||||
const icon = elementsOf(dashboard, 'span').find(node => classesOf(node).includes('workbench-icon'))
|
||||
expect(icon).toBeDefined()
|
||||
expect(icon!.children.some(node => node.type === 1 && node.tag === 'component')).toBe(true)
|
||||
expect(dashboard).not.toContain('density-fields')
|
||||
})
|
||||
|
||||
it('registers the self-operated dashboard with an explicit density root and chart layout', () => {
|
||||
const source = read('modules/self-operated/pages/OverviewView.vue')
|
||||
expect(source).toContain("@/styles/adminDensity.css")
|
||||
expect(source).toContain('<AdminPage class="admin-density dashboard-page">')
|
||||
expect(source).toContain('<AdminPage class="admin-density admin-workbench dashboard-page">')
|
||||
expect(source).toContain('class="dashboard-kpis"')
|
||||
expect(source).toContain('class="dashboard-main-charts"')
|
||||
expect(source).not.toContain('density-fields')
|
||||
@@ -521,7 +587,8 @@ describe('approved Admin density ownership', () => {
|
||||
const source = read('modules/warehouses/pages/WarehousesView.vue')
|
||||
expect(source).toContain('class="density-form density-dialog-sections"')
|
||||
expect(source).toContain('density-table density-table--editable warehouse-editor-table')
|
||||
expect(source).toContain('class="warehouse-import-dialog admin-density-dialog"')
|
||||
const importer = expectWorkbenchDialogs(source).find(node => classesOf(node).includes('warehouse-import-dialog'))
|
||||
expect(importer).toBeDefined()
|
||||
expect(source).toContain('class="density-table warehouse-import-summary"')
|
||||
expect(source).toContain('class="density-actions density-actions--footer"')
|
||||
expect(source).not.toContain('<FormSection')
|
||||
@@ -591,7 +658,7 @@ describe('approved Admin density ownership', () => {
|
||||
it('keeps download task detail on shared facts, error and footer primitives', () => {
|
||||
const source = read('modules/downloads/pages/DownloadCenterView.vue')
|
||||
const dialog = source.slice(source.indexOf('<AppDialog'), source.indexOf('</AppDialog>'))
|
||||
expect(dialog).toContain('class="admin-density-dialog"')
|
||||
expectWorkbenchDialogs(source)
|
||||
expect(dialog).toContain('fullscreenable')
|
||||
expect(dialog).toContain('data-tone="summary"')
|
||||
expect(dialog).toContain('class="task-detail-grid density-fields"')
|
||||
@@ -606,7 +673,7 @@ describe('approved Admin density ownership', () => {
|
||||
const list = read('modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue')
|
||||
const detail = read('modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue')
|
||||
const dialog = list.slice(list.indexOf('<AppDialog'), list.indexOf('</AppDialog>'))
|
||||
expect(dialog).toContain('class="admin-density-dialog"')
|
||||
expectWorkbenchDialogs(list)
|
||||
expect(dialog).toContain('width="min(560px, calc(100vw - 28px))"')
|
||||
expect(dialog).toContain(':confirm-loading="uploading"')
|
||||
expect(dialog).toContain('class="density-table density-table--editable"')
|
||||
@@ -621,7 +688,9 @@ describe('approved Admin density ownership', () => {
|
||||
const editor = read('modules/overseas-master-data/components/FleetQuoteEditor.vue')
|
||||
const importer = read('modules/overseas-master-data/components/FleetQuoteImport.vue')
|
||||
for (const source of [editor, importer]) {
|
||||
expect(source).toContain('class="admin-density-dialog" fullscreenable :confirm-loading="saving"')
|
||||
const [dialog] = expectWorkbenchDialogs(source)
|
||||
expect(dialog!.props.some(prop => prop.type === 6 && prop.name === 'fullscreenable')).toBe(true)
|
||||
expect(dialog!.props.some(prop => prop.type === 7 && prop.name === 'bind' && prop.arg?.type === 4 && prop.arg.content === 'confirm-loading' && prop.exp?.type === 4 && prop.exp.content === 'saving')).toBe(true)
|
||||
expect(source).toContain('density-table--editable')
|
||||
expect(source).toContain('data-tone="work"')
|
||||
expect(source).toContain('data-tone="source"')
|
||||
@@ -632,19 +701,20 @@ describe('approved Admin density ownership', () => {
|
||||
}
|
||||
expect(editor).toContain('v-if="readonly"')
|
||||
expect(editor).toContain('class="density-fields fleet-quote-facts"')
|
||||
expect(read('modules/overseas-master-data/pages/FleetQuotesView.vue')).toContain("customClass: 'admin-density-confirm'")
|
||||
const confirmations = confirmationClasses(read('modules/overseas-master-data/pages/FleetQuotesView.vue'))
|
||||
expect(confirmations.length).toBeGreaterThan(0)
|
||||
for (const classes of confirmations) expect(classes).toContain('admin-density-confirm')
|
||||
})
|
||||
|
||||
it('keeps pricing dialogs on shared tables and sections without local form paint', () => {
|
||||
const rate = read('modules/overseas-master-data/pages/ExchangeRatesView.vue')
|
||||
const quote = read('modules/overseas-master-data/pages/OverseasQuotesView.vue')
|
||||
for (const source of [rate, quote]) {
|
||||
const dialogs = parse(source).descriptor.template!.content.match(/<AppDialog[\s\S]*?<\/AppDialog>/g)!
|
||||
const dialogs = expectWorkbenchDialogs(source)
|
||||
expect(dialogs.length).toBeGreaterThan(0)
|
||||
for (const dialog of dialogs) {
|
||||
expect(dialog).toContain('class="admin-density-dialog"')
|
||||
expect(dialog).not.toContain('label-position="top"')
|
||||
expect(dialog).not.toContain('#header')
|
||||
expect(dialog.loc.source).not.toContain('label-position="top"')
|
||||
expect(dialog.loc.source).not.toContain('#header')
|
||||
}
|
||||
expect(source).toContain('density-table--editable')
|
||||
expect(source).not.toContain('line-grid')
|
||||
@@ -660,10 +730,13 @@ describe('approved Admin density ownership', () => {
|
||||
|
||||
it('keeps the supplier license dialog styled at its own teleported root', () => {
|
||||
const source = read('modules/suppliers/components/SupplierLicenseDialog.vue')
|
||||
expect(source).toContain('<AppDialog class="admin-density-dialog"')
|
||||
expectWorkbenchDialogs(source)
|
||||
expect(source).toContain('width="min(560px, calc(100vw - 24px))"')
|
||||
expect(source).toContain('aria-label="营业执照信息"')
|
||||
expect(source).toContain('class="density-dialog-section" data-tone="source"')
|
||||
const sections = elementsOf(source, 'section')
|
||||
expect(sections).toHaveLength(1)
|
||||
expect(classesOf(sections[0]!)).toEqual(expect.arrayContaining(['density-dialog-section', 'workbench-evidence']))
|
||||
expect(sections[0]!.props.some(prop => prop.type === 6 && prop.name === 'data-tone' && prop.value?.content === 'source')).toBe(true)
|
||||
expect(source).toContain('class="density-table license-info-table"')
|
||||
expect(source).not.toContain('license-card')
|
||||
expect(source).not.toContain('box-shadow')
|
||||
@@ -731,10 +804,16 @@ describe('approved Admin density ownership', () => {
|
||||
expect(source).toContain('dense?: boolean')
|
||||
const ast = parse(source).descriptor.template!.ast!
|
||||
const root = ast.children.find(node => node.type === 1) as ElementNode
|
||||
expect(classesOf(root, false, true).some(name => name.startsWith('admin-density')), file).toBe(true)
|
||||
expect(classesOf(root, false, false).some(name => name.startsWith('admin-density')), file).toBe(false)
|
||||
for (const workbench of [false, true]) {
|
||||
expect(classesOf(root, false, true, 'DOMESTIC', workbench).some(name => name.startsWith('admin-density')), file).toBe(true)
|
||||
expect(classesOf(root, false, false, 'DOMESTIC', workbench).some(name => name.startsWith('admin-density')), file).toBe(false)
|
||||
if (file.endsWith('OverseasDetailShell.vue')) {
|
||||
for (const dense of [false, true]) expect(classesOf(root, false, dense, 'DOMESTIC', workbench).includes('admin-workbench')).toBe(workbench)
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(read('modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue')).toContain(':dense="dense"')
|
||||
expect(read('modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue')).toContain(':workbench="workbench"')
|
||||
expect(read('modules/overseas-warehouse/pages/InventoryTransactionsView.vue')).toContain('<InboundCodeRelationshipDialog dense')
|
||||
expect(read('modules/inbounds/pages/InboundOrdersView.vue')).toContain('<InboundCodeRelationshipDialog dense')
|
||||
for (const file of ['CartonDetailView', 'ProductDetailView']) expect(read('modules/overseas-warehouse/pages/' + file + '.vue')).not.toContain(' dense')
|
||||
@@ -761,8 +840,14 @@ describe('approved Admin density ownership', () => {
|
||||
for (const file of ['loading-lists/components/LoadingListDetailDrawer.vue', 'port-schedules/components/CarrierScheduleQueryPanel.vue']) {
|
||||
const ast = parse(read(`modules/${file}`)).descriptor.template!.ast!
|
||||
const root = ast.children.find(node => node.type === 1) as ElementNode
|
||||
expect(classesOf(root, false, true)).toContain('admin-density')
|
||||
expect(classesOf(root, false, false)).not.toContain('admin-density')
|
||||
for (const embedded of [false, true]) {
|
||||
expect(classesOf(root, false, true, 'DOMESTIC', false, embedded)).toContain('admin-density')
|
||||
expect(classesOf(root, false, false, 'DOMESTIC', false, embedded)).not.toContain('admin-density')
|
||||
if (file.includes('LoadingListDetail')) {
|
||||
expect(classesOf(root, false, true, 'DOMESTIC', false, embedded).includes('workbench-panel')).toBe(!embedded)
|
||||
expect(classesOf(root, false, true, 'DOMESTIC', false, embedded).includes('is-compact')).toBe(!embedded)
|
||||
}
|
||||
}
|
||||
if (file.includes('CarrierSchedule')) {
|
||||
const dialog = root.children.find(node => node.type === 1 && node.tag === 'AppDialog') as ElementNode
|
||||
expect(classesOf(dialog, false, true)).toContain('admin-density-dialog')
|
||||
@@ -883,11 +968,12 @@ describe('approved Admin density ownership', () => {
|
||||
it('keeps the collection monitor in the Admin theme without legacy list controls', () => {
|
||||
const page = read('modules/self-operated/pages/CollectionView.vue')
|
||||
const monitor = read('modules/self-operated/components/CollectionAccountMonitor.vue')
|
||||
expect(page).toContain('class="admin-density"')
|
||||
expect(page).toContain('class="admin-density admin-workbench"')
|
||||
expect(page).toContain('@/styles/adminDensity.css')
|
||||
expect(page).toContain('@/styles/adminWorkbench.css')
|
||||
expect(page).toContain('<CollectionAccountMonitor')
|
||||
expect(page).not.toContain('<DataTable')
|
||||
expect(monitor).toContain('background: var(--el-bg-color)')
|
||||
expect(monitor).toContain('workbench-panel')
|
||||
expect(monitor).not.toMatch(/--el-[\w-]+\s*:/)
|
||||
})
|
||||
it('opts the confirmed self-operated pages into integrated text actions without changing older outlined consumers', () => {
|
||||
@@ -940,6 +1026,8 @@ describe('approved Admin density ownership', () => {
|
||||
})
|
||||
expect(rules.get('.admin-density .el-table__body tr.el-table__row--striped td.el-table__cell')?.background).toBe('var(--admin-row-stripe)')
|
||||
expect(rules.get('.admin-density-dialog .el-descriptions__label.el-descriptions__cell')?.background).toBe('var(--admin-label-bg)')
|
||||
expect(rules.get('.admin-density-dialog .el-form--inline .el-form-item__label')?.['align-items']).toBe('center')
|
||||
expect(rules.get('.admin-density-dialog.el-dialog.admin-density-workspace > .el-dialog__body')?.flex).toBe('1 1 auto')
|
||||
for (const tone of ['warning', 'purple', 'primary', 'success', 'danger']) {
|
||||
expect(rules.get(`:is(.admin-density, .admin-density-dialog) .density-action[data-tone="${tone}"]`)?.['--action-hover-bg']).toBe(`var(--admin-action-${tone}-hover)`)
|
||||
}
|
||||
@@ -1055,6 +1143,7 @@ describe('approved Admin density ownership', () => {
|
||||
expect(read('modules/loading-lists/components/LoadingListsView.vue')).toContain('class="business-page loading-lists-page admin-density"')
|
||||
expect(read('modules/loading-lists/components/PalletLabelPrintDialog.vue')).toContain("'admin-density-dialog': dense")
|
||||
expect(read('modules/container-loading/components/ContainerSimulationCreate.vue')).toContain("'admin-density-dialog': dense")
|
||||
expect(read('modules/container-loading/components/ContainerSimulationCreate.vue')).toContain("dense ? 'min(1000px, calc(100vw - 28px))' : 'min(560px, calc(100vw - 28px))'")
|
||||
for (const file of ['loading-lists/components/LoadingListDetailDrawer.vue', 'loading-lists/components/PalletLabelPrintDialog.vue', 'container-loading/components/ContainerSimulationCreate.vue']) {
|
||||
const template = parse(read(`modules/${file}`)).descriptor.template!.ast!
|
||||
const visit = (nodes: typeof template.children) => {
|
||||
@@ -1123,7 +1212,12 @@ describe('approved Admin density ownership', () => {
|
||||
expect(source).toContain('density-table density-table--editable dispatch-create-table')
|
||||
expect(source).toContain('<DispatchResourceFields continuous')
|
||||
expect(source).toContain('label-position="left" label-width="130px"')
|
||||
for (const tone of ['summary', 'source', 'work']) expect(source).toContain(`data-tone="${tone}"`)
|
||||
expect(source).toContain('class="density-dialog-section dispatch-setup"')
|
||||
expect(source).toContain('class="density-dialog-section dispatch-draft-toolbar"')
|
||||
expect(source).toContain('class="density-form dispatch-setup-fields"')
|
||||
expect(source).not.toContain('data-tone="work"')
|
||||
expect(source).not.toContain('data-tone="summary"')
|
||||
expect(source).not.toContain('data-tone="source"')
|
||||
expect(source).toContain('class="cargo-workspace"')
|
||||
expect(source).toContain('<LineClampText')
|
||||
expect(source).toContain('class="dispatch-source-group"')
|
||||
@@ -1233,7 +1327,7 @@ describe('approved Admin density ownership', () => {
|
||||
it('keeps discard readonly sections and confirmation compact while retaining photo geometry', () => {
|
||||
const content = read('modules/overseas-warehouse/pages/CargoDiscardsView.vue')
|
||||
const dialog = content.slice(content.indexOf('<AppDialog'))
|
||||
expect(dialog).toContain('class="admin-density-dialog"')
|
||||
expectWorkbenchDialogs(content)
|
||||
expect(dialog).toContain('fullscreenable')
|
||||
expect(dialog).toContain('920px')
|
||||
expect(dialog).toContain('data-tone="summary"')
|
||||
@@ -1242,7 +1336,9 @@ describe('approved Admin density ownership', () => {
|
||||
expect(dialog).toContain('preview-teleported')
|
||||
expect(dialog).toContain('<LineClampText')
|
||||
expect(dialog).not.toContain('<el-timeline')
|
||||
expect(content).toContain("customClass: 'admin-density-confirm'")
|
||||
const confirmations = confirmationClasses(content)
|
||||
expect(confirmations).toHaveLength(1)
|
||||
expect(confirmations[0]).toEqual(expect.arrayContaining(['admin-density-confirm', 'admin-workbench', 'is-dialog']))
|
||||
expect(content).toContain('height: 150px')
|
||||
const styles = parse(content).descriptor.styles.map(style => style.content).join('\n')
|
||||
expect(styles).not.toMatch(/background:|border-left:|font-size:|border-radius:/)
|
||||
@@ -1250,14 +1346,16 @@ describe('approved Admin density ownership', () => {
|
||||
it('keeps overseas location editing and native confirmations on the shared dialog contract', () => {
|
||||
const content = read('modules/overseas-warehouse/pages/LocationsView.vue')
|
||||
const dialog = content.slice(content.indexOf('<AppDialog'))
|
||||
expect(dialog).toContain('class="admin-density-dialog"')
|
||||
expectWorkbenchDialogs(content)
|
||||
expect(dialog).toContain('fullscreenable')
|
||||
expect(dialog).toContain('density-table density-table--editable')
|
||||
expect(dialog).toContain('label-position="left" label-width="110px" :disabled="formLocked"')
|
||||
expect(dialog).toContain('data-tone="work"')
|
||||
expect(dialog).not.toContain('label-position="top"')
|
||||
expect(content).not.toContain('editor-grid')
|
||||
expect(content.match(/customClass: 'admin-density-confirm'/g)).toHaveLength(2)
|
||||
const confirmations = confirmationClasses(content)
|
||||
expect(confirmations).toHaveLength(2)
|
||||
for (const classes of confirmations) expect(classes).toEqual(expect.arrayContaining(['admin-density-confirm', 'admin-workbench', 'is-dialog']))
|
||||
expect(parse(content).descriptor.styles).toHaveLength(0)
|
||||
})
|
||||
it('keeps overseas pallet editing on one shared continuous table with native fullscreen', () => {
|
||||
@@ -1426,4 +1524,16 @@ describe('approved Admin density ownership', () => {
|
||||
expect(customs).toContain('class="confirmation-history-note"')
|
||||
})
|
||||
|
||||
it('keeps container detail spacing compact and uses shared table styling for dense fulfillment records', () => {
|
||||
const detail = read('modules/container-loading/pages/ContainerDetailView.vue')
|
||||
const carrier = read('modules/port-schedules/components/CarrierScheduleQueryPanel.vue')
|
||||
expect(detail).toContain('<AdminPage spacing="compact" class="container-detail-page admin-density admin-workbench">')
|
||||
expect(detail).toContain('class="density-element-tabs" @tab-change="rememberWorkbenchTab"')
|
||||
expect(detail).toContain('<header class="detail-section__heading"><h2 id="container-voyage-heading">船名航次信息</h2><ContainerPortSchedulePanel')
|
||||
expect(carrier).toContain('class="fulfillment-table density-table"')
|
||||
expect(carrier).toContain('v-if="dense && fulfillmentEvents.length"')
|
||||
expect(carrier).toContain('v-else-if="fulfillmentEvents.length" class="fulfillment-timeline"')
|
||||
expect(carrier).not.toMatch(/\.fulfillment-table\s+[^{}]*\{[^{}]*(?:font-size|background|padding|border):/)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
@import './adminWorkbenchTokens.css';
|
||||
|
||||
.admin-workbench {
|
||||
--el-color-primary: var(--workbench-primary);
|
||||
--el-color-primary-light-3: var(--workbench-primary-hover);
|
||||
--el-color-primary-light-5: var(--workbench-primary-disabled);
|
||||
--el-color-primary-light-7: var(--workbench-accent-border);
|
||||
--el-color-primary-light-8: var(--workbench-accent-border);
|
||||
--el-color-primary-light-9: var(--workbench-accent-soft);
|
||||
--el-color-primary-dark-2: var(--workbench-primary-active);
|
||||
}
|
||||
|
||||
/* Keep business grids in the page; these primitives only provide appearance. */
|
||||
.admin-workbench.admin-page {
|
||||
min-width: 0;
|
||||
padding: var(--workbench-page-padding);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: var(--workbench-surface);
|
||||
color: var(--workbench-text);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.admin-workbench.admin-page.is-embedded {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Override AdminPage's scoped normal spacing only for this explicit opt-in. */
|
||||
.admin-workbench.admin-page.admin-density.is-spacing-normal {
|
||||
gap: var(--workbench-page-space);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-heading {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-title {
|
||||
margin: 0;
|
||||
color: var(--workbench-text);
|
||||
font-size: var(--workbench-title-size);
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-meta {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--workbench-muted);
|
||||
font-size: var(--workbench-note-size);
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-meta > i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: var(--workbench-accent);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-section {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: var(--workbench-section-gap);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-section-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-section-title {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin: 0;
|
||||
color: var(--workbench-text);
|
||||
font-size: var(--workbench-section-title-size);
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-section-title::before {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: var(--workbench-accent);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-badge {
|
||||
max-width: 100%;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--workbench-surface-soft);
|
||||
color: var(--workbench-muted);
|
||||
font-size: var(--workbench-note-size);
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-badge.is-toned {
|
||||
border: 1px solid var(--workbench-accent-border);
|
||||
background: var(--workbench-accent-soft);
|
||||
color: var(--workbench-accent);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric,
|
||||
.admin-workbench .workbench-panel {
|
||||
min-width: 0;
|
||||
padding: var(--workbench-card-space);
|
||||
border: 1px solid var(--workbench-border);
|
||||
border-radius: var(--workbench-radius);
|
||||
background: var(--workbench-surface);
|
||||
box-shadow: var(--workbench-shadow);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric {
|
||||
min-height: var(--workbench-metric-min-height);
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(145deg, var(--workbench-surface) 58%, var(--workbench-surface-tint));
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-panel.is-flush {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-panel.is-compact {
|
||||
padding: var(--workbench-compact-card-space);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric.is-compact {
|
||||
min-height: 0;
|
||||
padding: var(--workbench-compact-card-space);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric.is-compact .workbench-metric-heading {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric.is-compact .workbench-metric-label {
|
||||
font-size: var(--workbench-note-size);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric.is-compact .workbench-metric-value {
|
||||
margin: 10px 0 6px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric.is-compact .workbench-metric-value > strong {
|
||||
font-size: var(--workbench-compact-number-size);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-icon.is-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--workbench-tab-radius);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-icon.is-compact > svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-icon {
|
||||
display: inline-flex;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--workbench-icon-radius);
|
||||
background: var(--workbench-accent-soft);
|
||||
color: var(--workbench-accent);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-icon > svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-brand-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--workbench-icon-radius);
|
||||
background: var(--workbench-primary);
|
||||
color: var(--workbench-surface);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric-label {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--workbench-text);
|
||||
font-size: var(--workbench-label-size);
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric-value {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 4px 6px;
|
||||
margin: 20px 0 12px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric-value > strong {
|
||||
min-width: 0;
|
||||
color: var(--workbench-number);
|
||||
font-size: var(--workbench-number-size);
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-metric-value > span {
|
||||
color: var(--workbench-muted);
|
||||
font-size: var(--workbench-note-size);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-note,
|
||||
.admin-workbench .workbench-status {
|
||||
display: block;
|
||||
margin: 4px 0;
|
||||
color: var(--workbench-muted);
|
||||
font-size: var(--workbench-note-size);
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-panel.workbench-tone {
|
||||
border-color: var(--workbench-accent-border);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-title {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
color: var(--workbench-text);
|
||||
font-size: var(--workbench-label-size);
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-title::before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: var(--workbench-accent);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-actions {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-actions .el-button {
|
||||
--el-button-text-color: var(--workbench-primary);
|
||||
--el-button-hover-text-color: var(--workbench-primary-active);
|
||||
--el-button-disabled-text-color: var(--workbench-primary-disabled);
|
||||
font-size: var(--workbench-note-size);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-actions .el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-canvas {
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-empty {
|
||||
display: flex;
|
||||
min-height: 240px;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: var(--workbench-card-space);
|
||||
border-radius: var(--workbench-icon-radius);
|
||||
background: var(--workbench-surface-soft);
|
||||
color: var(--workbench-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-empty.is-compact {
|
||||
min-height: 0;
|
||||
padding: var(--workbench-compact-card-space);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-empty-icon {
|
||||
display: grid;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border: 1px solid var(--workbench-border);
|
||||
border-radius: var(--workbench-icon-radius);
|
||||
background: var(--workbench-surface);
|
||||
color: var(--workbench-muted);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-chart-empty p {
|
||||
max-width: 28em;
|
||||
margin: 0;
|
||||
color: var(--workbench-muted);
|
||||
font-size: var(--workbench-note-size);
|
||||
line-height: 1.7;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* DataTable retains ownership of toolbar size, controls and query layout. */
|
||||
.admin-workbench .query-toolbar.workbench-query {
|
||||
border-color: var(--workbench-border);
|
||||
border-radius: var(--workbench-radius);
|
||||
background: var(--workbench-surface);
|
||||
box-shadow: var(--workbench-shadow);
|
||||
}
|
||||
|
||||
/* Integrated filters share the table surface; never nest another query card. */
|
||||
.admin-workbench .data-table--integrated > .data-table__filters .query-toolbar.workbench-query {
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-toolbar,
|
||||
.admin-workbench .workbench-table {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* DataTable forwards attrs to its inner table, so opt in through this wrapper. */
|
||||
.admin-workbench .workbench-toolbar > .data-table,
|
||||
.admin-workbench .workbench-table > .data-table {
|
||||
--list-card-accent: var(--workbench-primary);
|
||||
--list-card-border: var(--workbench-border);
|
||||
--list-card-muted-bg: var(--workbench-surface-soft);
|
||||
--list-card-radius: var(--workbench-radius);
|
||||
min-width: 0;
|
||||
border-color: var(--workbench-border);
|
||||
border-radius: var(--workbench-radius);
|
||||
background: var(--workbench-surface);
|
||||
box-shadow: var(--workbench-shadow);
|
||||
}
|
||||
|
||||
/* Integrated DataTable already owns its border; default tables need a frame. */
|
||||
.admin-workbench .workbench-toolbar > .data-table:not(.data-table--integrated),
|
||||
.admin-workbench .workbench-table > .data-table:not(.data-table--integrated) {
|
||||
border: 1px solid var(--workbench-border);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-toolbar > .data-table > .data-table__toolbar,
|
||||
.admin-workbench .workbench-table > .data-table > .data-table__toolbar {
|
||||
border-color: var(--workbench-border);
|
||||
background: var(--workbench-surface);
|
||||
}
|
||||
|
||||
/* The main page is one continuous surface; only inner cards retain their frame. */
|
||||
.admin-workbench.workbench-panel.is-page-surface,
|
||||
.admin-workbench .workbench-panel.is-page-surface,
|
||||
.admin-workbench .workbench-table.is-page-surface > .data-table,
|
||||
.admin-workbench .workbench-toolbar.is-page-surface > .data-table {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Main lists share one compact header. Nested tables and dialog density stay local. */
|
||||
@media (min-width: 721px) {
|
||||
.admin-workbench .workbench-table.is-page-surface > .data-table > .data-table__toolbar,
|
||||
.admin-workbench .workbench-toolbar.is-page-surface > .data-table > .data-table__toolbar {
|
||||
--el-component-size: var(--admin-control-height, 32px);
|
||||
min-height: var(--workbench-list-toolbar-height);
|
||||
box-sizing: border-box;
|
||||
padding: var(--workbench-list-toolbar-padding) var(--workbench-list-inset);
|
||||
gap: 8px 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-table.is-page-surface > .data-table > .data-table__toolbar > .data-table__toolbar-start {
|
||||
flex: 1 1 280px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-table.is-page-surface > .data-table > .data-table__toolbar > .data-table__toolbar-actions,
|
||||
.admin-workbench .workbench-toolbar.is-page-surface > .data-table > .data-table__toolbar > .data-table__toolbar-actions {
|
||||
max-width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-table.is-page-surface > .data-table > .data-table__toolbar .el-button,
|
||||
.admin-workbench .workbench-toolbar.is-page-surface > .data-table > .data-table__toolbar .el-button {
|
||||
min-height: var(--admin-control-height, 32px);
|
||||
height: var(--admin-control-height, 32px);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-table.is-page-surface > .data-table > .data-table__filters .query-toolbar,
|
||||
.admin-workbench .workbench-toolbar.is-page-surface > .data-table > .data-table__filters .query-toolbar {
|
||||
--el-component-size: var(--admin-control-height, 32px);
|
||||
padding: var(--workbench-list-query-padding) var(--workbench-list-inset);
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-table.is-page-surface > .data-table > .data-table__filters .query-toolbar__filters,
|
||||
.admin-workbench .workbench-toolbar.is-page-surface > .data-table > .data-table__filters .query-toolbar__filters {
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
/* The toolbar supplies the inset; moved tabs must not add a second frame. */
|
||||
.admin-workbench .data-table__toolbar-start .workbench-list-tabs {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.admin-workbench .data-table__toolbar-start .workbench-list-tabs.density-status-tabs > button,
|
||||
.admin-workbench .data-table__toolbar-start .workbench-list-tabs.workbench-tabs > button,
|
||||
.admin-workbench .data-table__toolbar-start .workbench-list-tabs.density-element-tabs > .el-tabs__header .el-tabs__nav .el-tabs__item {
|
||||
min-height: var(--admin-control-height, 32px);
|
||||
height: var(--admin-control-height, 32px);
|
||||
}
|
||||
|
||||
.admin-workbench .data-table__toolbar-start .workbench-list-tabs.density-element-tabs > .el-tabs__header,
|
||||
.admin-workbench .data-table__toolbar-start .workbench-list-tabs.density-element-tabs > .el-tabs__header .el-tabs__nav-wrap {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-workbench .data-table__toolbar-start .workbench-list-tabs.density-status-tabs > button:is(:hover, .is-active),
|
||||
.admin-workbench .data-table__toolbar-start .workbench-list-tabs.density-element-tabs > .el-tabs__header .el-tabs__nav .el-tabs__item:is(:hover, .is-active) {
|
||||
color: var(--workbench-primary);
|
||||
}
|
||||
|
||||
.admin-workbench .data-table__toolbar-start .workbench-list-tabs.density-status-tabs > button.is-active::after,
|
||||
.admin-workbench .data-table__toolbar-start .workbench-list-tabs.density-element-tabs > .el-tabs__header .el-tabs__nav .el-tabs__item.is-active::after {
|
||||
background: var(--workbench-primary);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-table > .data-table > .el-table,
|
||||
.admin-workbench .workbench-table > .el-table {
|
||||
--el-table-border-color: var(--workbench-border);
|
||||
--el-table-header-bg-color: var(--workbench-surface-soft);
|
||||
--el-table-header-text-color: var(--workbench-muted);
|
||||
--el-table-row-hover-bg-color: var(--workbench-accent-soft);
|
||||
--el-table-current-row-bg-color: var(--workbench-accent-soft);
|
||||
--el-table-text-color: var(--workbench-text);
|
||||
color: var(--workbench-text);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-table .el-table th.el-table__cell {
|
||||
color: var(--workbench-muted);
|
||||
}
|
||||
|
||||
.admin-workbench .el-button.density-action:not([data-tone]),
|
||||
.admin-workbench .el-button.density-action[data-tone='primary'] {
|
||||
--action-color: var(--workbench-primary);
|
||||
--action-border: var(--workbench-accent-border);
|
||||
--action-hover-bg: var(--workbench-accent-soft);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-table .data-table .data-table__page-button[aria-current='page']:not(:disabled),
|
||||
.admin-workbench .workbench-table .data-table__business-actions .el-button--primary:not(:disabled) {
|
||||
box-shadow: var(--workbench-shadow);
|
||||
}
|
||||
|
||||
/* Keep selection, focus and disabled states consistent for existing native tabs. */
|
||||
.admin-workbench .workbench-tabs {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tabs > button {
|
||||
display: inline-flex;
|
||||
min-height: var(--admin-tab-height);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--workbench-tab-radius);
|
||||
background: transparent;
|
||||
color: var(--workbench-muted);
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tabs > button:hover:not(:disabled):not([aria-pressed='true']):not(.is-active) {
|
||||
background: var(--workbench-surface-soft);
|
||||
color: var(--workbench-primary);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tabs > button[aria-pressed='true'],
|
||||
.admin-workbench .workbench-tabs > button.is-active {
|
||||
border-color: var(--workbench-accent-border);
|
||||
background: var(--workbench-accent-soft);
|
||||
color: var(--workbench-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tabs > button:focus-visible,
|
||||
.admin-workbench .workbench-input:focus-visible {
|
||||
outline: 2px solid var(--workbench-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tabs > button:disabled {
|
||||
color: var(--workbench-primary-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-input {
|
||||
min-height: var(--admin-control-height);
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--workbench-border);
|
||||
border-radius: var(--workbench-tab-radius);
|
||||
background: var(--workbench-surface);
|
||||
color: var(--workbench-text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-input:disabled {
|
||||
background: var(--workbench-surface-soft);
|
||||
color: var(--workbench-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Teleported dialogs opt in on their own root; retain the density frame geometry. */
|
||||
.admin-workbench.is-dialog.el-dialog:not(.is-fullscreen) {
|
||||
border-radius: var(--workbench-radius);
|
||||
}
|
||||
|
||||
.admin-workbench.is-dialog.el-dialog .el-dialog__header,
|
||||
.admin-workbench.is-dialog.el-dialog .el-dialog__footer {
|
||||
border-color: var(--workbench-border);
|
||||
background: var(--workbench-surface);
|
||||
}
|
||||
|
||||
.admin-workbench.is-dialog.el-dialog .el-dialog__title {
|
||||
color: var(--workbench-text);
|
||||
}
|
||||
|
||||
/* A teleported drawer carries its own opt-in. Density owns its frame geometry. */
|
||||
.admin-workbench.is-drawer.el-drawer {
|
||||
background: var(--workbench-surface);
|
||||
color: var(--workbench-text);
|
||||
}
|
||||
|
||||
.admin-workbench.is-drawer.el-drawer .el-drawer__header,
|
||||
.admin-workbench.is-drawer.el-drawer .el-drawer__footer {
|
||||
border-color: var(--workbench-border);
|
||||
background: var(--workbench-surface);
|
||||
}
|
||||
|
||||
.admin-workbench.is-drawer.el-drawer .el-drawer__title {
|
||||
color: var(--workbench-text);
|
||||
}
|
||||
|
||||
/* Existing density classes retain layout and typography; adapt neutral paint only. */
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-detail-layout,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-detail-header,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-detail-overview,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-fields,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-fields > div,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-dialog-section:not([data-tone]) {
|
||||
background: var(--workbench-surface);
|
||||
}
|
||||
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-detail-layout > .detail-page-layout__header,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-detail-overview,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-fields,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-fields > div,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-descriptions .el-descriptions__cell,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .form-section.density-form-section,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-dialog-section:not([data-tone]) {
|
||||
border-color: var(--workbench-border);
|
||||
}
|
||||
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-detail-title,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-detail-overview dd,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-fields dd,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-descriptions .el-descriptions__content.el-descriptions__cell,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-form-section .form-section__heading h2 {
|
||||
color: var(--workbench-text);
|
||||
}
|
||||
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-detail-overview dt,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-detail-overview small,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-form-section .form-section__heading p,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-form .el-form-item__label {
|
||||
color: var(--workbench-muted);
|
||||
}
|
||||
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-fields dt,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-descriptions .el-descriptions__label.el-descriptions__cell {
|
||||
background: var(--workbench-surface-soft);
|
||||
color: var(--workbench-muted);
|
||||
}
|
||||
|
||||
/* Neutral form tokens leave required marks, validation, disabled and semantic colors intact. */
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer).density-form,
|
||||
.admin-workbench:is(.admin-density, .admin-density-dialog, .admin-density-drawer) .density-form {
|
||||
--el-border-color: var(--workbench-border);
|
||||
--el-fill-color-blank: var(--workbench-surface);
|
||||
--el-text-color-regular: var(--workbench-text);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-evidence table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--admin-font-body);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-evidence th,
|
||||
.admin-workbench .workbench-evidence td {
|
||||
border: 1px solid var(--workbench-border);
|
||||
text-align: left;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-evidence table:not(.density-table) :is(th, td) {
|
||||
padding: var(--admin-space-1) var(--admin-space-3);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-evidence th {
|
||||
background: var(--workbench-surface-soft);
|
||||
color: var(--workbench-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-notice {
|
||||
margin: 0;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--workbench-accent-border);
|
||||
border-radius: var(--workbench-icon-radius);
|
||||
background: var(--workbench-accent-soft);
|
||||
color: var(--workbench-text);
|
||||
font-size: var(--workbench-note-size);
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* State labels remain the source of meaning; color only reinforces them. */
|
||||
.admin-workbench .workbench-step {
|
||||
min-width: 0;
|
||||
padding: var(--workbench-compact-card-space);
|
||||
border: 1px solid var(--workbench-border);
|
||||
border-radius: var(--workbench-icon-radius);
|
||||
background: var(--workbench-surface-soft);
|
||||
color: var(--workbench-muted);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-step-index {
|
||||
display: grid;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border: 1px solid var(--workbench-border);
|
||||
border-radius: 50%;
|
||||
background: var(--workbench-surface);
|
||||
color: var(--workbench-muted);
|
||||
font-size: var(--workbench-note-size);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-step[data-state='active'],
|
||||
.admin-workbench .workbench-step[data-state='done'] {
|
||||
border-color: var(--workbench-accent-border);
|
||||
background: var(--workbench-accent-soft);
|
||||
color: var(--workbench-accent);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-step[data-state='active'] .workbench-step-index,
|
||||
.admin-workbench .workbench-step[data-state='done'] .workbench-step-index {
|
||||
border-color: var(--workbench-accent);
|
||||
background: var(--workbench-accent);
|
||||
color: var(--workbench-surface);
|
||||
}
|
||||
|
||||
.admin-workbench .el-button--primary:not(.is-plain):not(.is-link):not(.is-text) {
|
||||
--el-button-bg-color: var(--workbench-primary);
|
||||
--el-button-border-color: var(--workbench-primary);
|
||||
--el-button-hover-bg-color: var(--workbench-primary-hover);
|
||||
--el-button-hover-border-color: var(--workbench-primary-hover);
|
||||
--el-button-active-bg-color: var(--workbench-primary-active);
|
||||
--el-button-active-border-color: var(--workbench-primary-active);
|
||||
--el-button-disabled-bg-color: var(--workbench-primary-disabled);
|
||||
--el-button-disabled-border-color: var(--workbench-primary-disabled);
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-skeleton {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
height: var(--workbench-metric-min-height);
|
||||
border: 1px solid var(--workbench-border);
|
||||
border-radius: var(--workbench-radius);
|
||||
background: var(--workbench-surface-soft);
|
||||
animation: admin-workbench-loading 1.6s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes admin-workbench-loading {
|
||||
from { opacity: 0.55; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.admin-workbench .workbench-skeleton {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { compileStyle, parse } from 'vue/compiler-sfc'
|
||||
import { adminWorkbenchChartTheme } from './adminWorkbenchChartTheme'
|
||||
|
||||
const read = (path: string) => readFileSync(resolve(process.cwd(), 'src', path), 'utf8')
|
||||
const tokens = read('styles/adminWorkbenchTokens.css')
|
||||
const styles = read('styles/adminWorkbench.css')
|
||||
const page = read('modules/self-operated/pages/OverviewView.vue')
|
||||
const listPages = [
|
||||
'modules/self-operated/components/AnalyticsView.vue',
|
||||
'modules/self-operated/components/InventoryTurnoverView.vue',
|
||||
]
|
||||
const consumers = [
|
||||
...listPages,
|
||||
'modules/self-operated/pages/InventoryView.vue',
|
||||
'modules/self-operated/components/SkuTrend.vue',
|
||||
'modules/self-operated/components/ChartDetailsDialog.vue',
|
||||
'modules/self-operated/pages/CollectionView.vue',
|
||||
'modules/self-operated/components/CollectionAccountMonitor.vue',
|
||||
'modules/self-operated/components/CollectionProgressGauge.vue',
|
||||
'modules/self-operated/components/CollectionTaskProgress.vue',
|
||||
]
|
||||
const root = (source: string) => {
|
||||
const result = compileStyle({ source, filename: 'workbench.css', id: 'workbench-contract' })
|
||||
expect(result.errors).toEqual([])
|
||||
return result.rawResult!.root
|
||||
}
|
||||
const declarationsFor = (element: Element) => {
|
||||
const declarations = new Map<string, string>()
|
||||
root(styles).walkRules(rule => {
|
||||
if (rule.selectors.some(selector => element.matches(selector))) {
|
||||
rule.walkDecls(declaration => { declarations.set(declaration.prop, declaration.value) })
|
||||
}
|
||||
})
|
||||
return declarations
|
||||
}
|
||||
const fixture = (markup: string) => {
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = markup
|
||||
return (selector: string) => {
|
||||
const element = container.querySelector(selector)
|
||||
expect(element).not.toBeNull()
|
||||
return element!
|
||||
}
|
||||
}
|
||||
|
||||
describe('Admin workbench opt-in styles', () => {
|
||||
it('keeps canvas chart colors aligned with the public CSS palette', () => {
|
||||
const declarations = new Map<string, string>()
|
||||
root(tokens).walkRules(rule => {
|
||||
if (rule.selector !== '.admin-workbench') return
|
||||
rule.walkDecls(declaration => { declarations.set(declaration.prop, declaration.value) })
|
||||
})
|
||||
for (const name of ['primary', 'text', 'muted', 'border', 'surface'] as const) {
|
||||
expect(adminWorkbenchChartTheme.colors[name]).toBe(declarations.get(`--workbench-${name}`))
|
||||
}
|
||||
expect(adminWorkbenchChartTheme.colors.primarySoft).toBe(declarations.get('--workbench-accent-soft'))
|
||||
})
|
||||
|
||||
it('keeps lazy-loaded styles scoped to opted-in pages and teleported roots', () => {
|
||||
for (const source of [tokens, styles]) {
|
||||
root(source).walkRules(rule => {
|
||||
if (rule.parent?.type === 'atrule' && rule.parent.name.endsWith('keyframes')) return
|
||||
for (const selector of rule.selectors) {
|
||||
expect(selector).toMatch(/^\.admin-workbench(?:\s|\.|:|\[|$)/)
|
||||
expect(selector).not.toMatch(/:global|:root/)
|
||||
if (selector.includes('.el-dialog')) expect(selector).toMatch(/^\.admin-workbench\.is-dialog\.el-dialog/)
|
||||
if (selector.includes('.el-drawer')) expect(selector).toMatch(/^\.admin-workbench\.is-drawer\.el-drawer/)
|
||||
}
|
||||
})
|
||||
root(source).walkDecls(declaration => {
|
||||
expect(declaration.prop).not.toMatch(/^--admin-/)
|
||||
expect(declaration.important).toBeFalsy()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves every workbench variable from the public tokens instead of page defaults', () => {
|
||||
const declared = new Set<string>()
|
||||
root(tokens).walkDecls(declaration => {
|
||||
if (declaration.prop.startsWith('--workbench-')) declared.add(declaration.prop)
|
||||
})
|
||||
const pageStyles = [page, ...consumers.map(read)].flatMap(source => parse(source).descriptor.styles.map(style => style.content)).join('\n')
|
||||
for (const source of [styles, pageStyles]) {
|
||||
root(source).walkDecls(declaration => {
|
||||
expect(declaration.prop).not.toMatch(/^--workbench-/)
|
||||
})
|
||||
for (const match of source.matchAll(/var\((--workbench-[\w-]+)/g)) {
|
||||
expect(declared.has(match[1]!)).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('opts list cards and teleported dialogs in on their actual rendering containers', () => {
|
||||
for (const file of listPages) {
|
||||
const source = read(file)
|
||||
expect(source).toContain("import '@/styles/adminWorkbench.css'")
|
||||
const template = parse(source).descriptor.template!.ast!
|
||||
type TemplateElement = Extract<(typeof template.children)[number], { type: 1 }>
|
||||
const classesOf = (node?: TemplateElement) => node?.props.flatMap(prop => prop.type === 6 && prop.name === 'class' ? prop.value?.content.split(/\s+/) ?? [] : []) ?? []
|
||||
let tables = 0
|
||||
const visit = (nodes: typeof template.children, ancestors: TemplateElement[]) => {
|
||||
for (const node of nodes) {
|
||||
if (node.type !== 1) continue
|
||||
if (node.tag === 'AdminPage') expect(classesOf(node)).toEqual(expect.arrayContaining(['admin-density', 'admin-workbench']))
|
||||
if (node.tag === 'DataTable') {
|
||||
expect(classesOf(ancestors.at(-1))).toContain('workbench-table')
|
||||
expect(classesOf(node)).not.toContain('workbench-table')
|
||||
tables++
|
||||
}
|
||||
if (node.tag === 'AppDialog') expect(classesOf(node)).toEqual(expect.arrayContaining(['admin-density-dialog', 'admin-workbench', 'is-dialog']))
|
||||
visit(node.children, [...ancestors, node])
|
||||
}
|
||||
}
|
||||
visit(template.children, [])
|
||||
expect(tables).toBe(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('paints standalone query cards without adding a nested surface inside integrated filters', () => {
|
||||
const get = fixture(`
|
||||
<main class="admin-density admin-workbench">
|
||||
<section id="standalone" class="query-toolbar workbench-query"></section>
|
||||
<div class="data-table data-table--integrated">
|
||||
<div class="data-table__filters"><section id="integrated" class="query-toolbar workbench-query"></section></div>
|
||||
</div>
|
||||
</main>
|
||||
<section id="outside" class="query-toolbar workbench-query"></section>
|
||||
`)
|
||||
const standalone = declarationsFor(get('#standalone'))
|
||||
const integrated = declarationsFor(get('#integrated'))
|
||||
expect(standalone.get('border-color')).toBe('var(--workbench-border)')
|
||||
expect(standalone.get('border-radius')).toBe('var(--workbench-radius)')
|
||||
expect(standalone.get('box-shadow')).toBe('var(--workbench-shadow)')
|
||||
expect(integrated.get('background')).toBe('transparent')
|
||||
expect(integrated.get('border-radius')).toBe('0')
|
||||
expect(integrated.get('box-shadow')).toBe('none')
|
||||
expect(declarationsFor(get('#outside')).size).toBe(0)
|
||||
})
|
||||
|
||||
it('adds a default table frame only on the real outer container', () => {
|
||||
const get = fixture(`
|
||||
<main class="admin-workbench">
|
||||
<div class="workbench-table"><div id="default" class="data-table"><div class="el-table"></div></div></div>
|
||||
<div class="workbench-table"><div id="integrated" class="data-table data-table--integrated"></div></div>
|
||||
<div class="data-table"><div id="inner" class="el-table workbench-table"></div></div>
|
||||
</main>
|
||||
`)
|
||||
expect(declarationsFor(get('#default')).get('border')).toBe('1px solid var(--workbench-border)')
|
||||
expect(declarationsFor(get('#integrated')).get('border')).toBeUndefined()
|
||||
expect(declarationsFor(get('#inner')).get('border')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('requires an explicit drawer root while leaving its size and scrolling to the component', () => {
|
||||
const get = fixture(`
|
||||
<section id="drawer" class="el-drawer app-drawer admin-density-drawer admin-workbench is-drawer">
|
||||
<header class="el-drawer__header"><span class="el-drawer__title">详情</span></header>
|
||||
<div class="el-drawer__body"></div><footer class="el-drawer__footer"></footer>
|
||||
</section>
|
||||
<section id="outside" class="el-drawer app-drawer admin-density-drawer"><header class="el-drawer__header"></header></section>
|
||||
`)
|
||||
expect(declarationsFor(get('#drawer')).get('background')).toBe('var(--workbench-surface)')
|
||||
expect(declarationsFor(get('#drawer .el-drawer__header')).get('border-color')).toBe('var(--workbench-border)')
|
||||
expect(declarationsFor(get('#drawer .el-drawer__title')).get('color')).toBe('var(--workbench-text)')
|
||||
expect(declarationsFor(get('#outside .el-drawer__header')).size).toBe(0)
|
||||
})
|
||||
|
||||
it('removes only the page frame while retaining the inner cards and table grid', () => {
|
||||
const get = fixture(`
|
||||
<main class="admin-page admin-workbench">
|
||||
<section id="page" class="workbench-panel is-page-surface">
|
||||
<article id="card" class="workbench-panel"></article>
|
||||
<div class="workbench-table is-page-surface"><div id="table" class="data-table data-table--integrated"><div id="grid" class="el-table"></div></div></div>
|
||||
</section>
|
||||
</main>
|
||||
`)
|
||||
for (const selector of ['#page', '#table']) {
|
||||
const appearance = declarationsFor(get(selector))
|
||||
expect(appearance.get('border')).toBe('0')
|
||||
expect(appearance.get('border-radius')).toBe('0')
|
||||
expect(appearance.get('box-shadow')).toBe('none')
|
||||
}
|
||||
expect(declarationsFor(get('#card')).get('border')).toBe('1px solid var(--workbench-border)')
|
||||
expect(declarationsFor(get('#grid')).get('--el-table-border-color')).toBe('var(--workbench-border)')
|
||||
})
|
||||
|
||||
it('uses one page inset when a routed workbench is explicitly embedded', () => {
|
||||
const get = fixture(`
|
||||
<main id="page" class="admin-page admin-workbench">
|
||||
<section id="embedded" class="admin-page admin-workbench is-embedded"></section>
|
||||
<section id="card" class="workbench-panel"></section>
|
||||
</main>
|
||||
<main id="standalone" class="admin-page admin-workbench"></main>
|
||||
`)
|
||||
expect(declarationsFor(get('#embedded')).get('padding')).toBe('0')
|
||||
for (const selector of ['#page', '#standalone']) {
|
||||
expect(declarationsFor(get(selector)).get('padding')).toBe('var(--workbench-page-padding)')
|
||||
}
|
||||
expect(declarationsFor(get('#card')).get('padding')).toBe('var(--workbench-card-space)')
|
||||
})
|
||||
|
||||
it('compacts only main list headers and aligns their filters without fixing content height', () => {
|
||||
const table = (id: string) => `
|
||||
<div class="data-table data-table--integrated">
|
||||
<div id="${id}-header" class="data-table__toolbar">
|
||||
<div class="data-table__toolbar-start"><nav id="${id}-tabs" class="density-status-tabs workbench-list-tabs"><button>状态</button></nav></div>
|
||||
<div id="${id}-actions" class="data-table__toolbar-actions"><button class="el-button">新增</button></div>
|
||||
</div>
|
||||
<div class="data-table__filters"><div id="${id}-query" class="query-toolbar"><div class="query-toolbar__filters"></div></div></div>
|
||||
<div id="${id}-body" class="el-table"></div>
|
||||
</div>`
|
||||
const get = fixture(`
|
||||
<main class="admin-page admin-density admin-workbench">
|
||||
<div class="workbench-table is-page-surface">${table('main')}</div>
|
||||
<div class="workbench-table">${table('nested')}</div>
|
||||
</main>
|
||||
<div class="workbench-table is-page-surface">${table('outside')}</div>
|
||||
`)
|
||||
const header = declarationsFor(get('#main-header'))
|
||||
const query = declarationsFor(get('#main-query'))
|
||||
expect(header.get('min-height')).toBe('var(--workbench-list-toolbar-height)')
|
||||
expect(header.get('padding')).toBe('var(--workbench-list-toolbar-padding) var(--workbench-list-inset)')
|
||||
expect(query.get('padding')).toBe('var(--workbench-list-query-padding) var(--workbench-list-inset)')
|
||||
expect(header.get('flex-wrap')).toBe('wrap')
|
||||
expect(header.has('height')).toBe(false)
|
||||
expect(declarationsFor(get('#main-actions')).get('max-width')).toBe('100%')
|
||||
expect(declarationsFor(get('#main-tabs')).get('padding')).toBe('0')
|
||||
expect(declarationsFor(get('#main-tabs')).get('background')).toBe('transparent')
|
||||
for (const selector of ['#nested-header', '#outside-header', '#nested-query', '#outside-query', '#main-body']) {
|
||||
expect(declarationsFor(get(selector)).has('padding')).toBe(false)
|
||||
expect(declarationsFor(get(selector)).has('height')).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('aligns wrapped native tables and dialog actions without repainting semantic actions', () => {
|
||||
const get = fixture(`
|
||||
<section class="admin-workbench is-dialog admin-density-dialog">
|
||||
<div class="workbench-table"><div id="native" class="el-table"></div></div>
|
||||
<div class="workbench-table"><div class="data-table"><div id="shared" class="el-table"></div></div></div>
|
||||
<button id="primary" class="el-button density-action" data-tone="primary"></button>
|
||||
<button id="danger" class="el-button density-action" data-tone="danger"></button>
|
||||
</section>
|
||||
<div class="workbench-table"><div id="outside" class="el-table"></div></div>
|
||||
`)
|
||||
expect(declarationsFor(get('#native'))).toEqual(declarationsFor(get('#shared')))
|
||||
expect(declarationsFor(get('#native')).get('--el-table-header-bg-color')).toBe('var(--workbench-surface-soft)')
|
||||
expect(declarationsFor(get('#primary')).get('--action-color')).toBe('var(--workbench-primary)')
|
||||
expect(declarationsFor(get('#danger')).get('--action-color')).toBeUndefined()
|
||||
expect(declarationsFor(get('#outside')).size).toBe(0)
|
||||
})
|
||||
|
||||
it('adapts neutral density content without repainting semantic sections or validation states', () => {
|
||||
const get = fixture(`
|
||||
<main class="admin-density admin-workbench">
|
||||
<dl class="density-fields"><div><dt id="label">名称</dt><dd id="value">内容</dd></div></dl>
|
||||
<section id="neutral" class="density-dialog-section"></section>
|
||||
<section id="business" class="density-dialog-section" data-tone="source"></section>
|
||||
<form id="form" class="density-form">
|
||||
<div class="el-form-item is-error"><label class="el-form-item__label">必填</label><div id="error" class="el-form-item__error">错误</div></div>
|
||||
</form>
|
||||
</main>
|
||||
<main class="admin-density"><dl class="density-fields"><div><dt id="outside">名称</dt></div></dl></main>
|
||||
`)
|
||||
expect(declarationsFor(get('#label')).get('background')).toBe('var(--workbench-surface-soft)')
|
||||
expect(declarationsFor(get('#value')).get('color')).toBe('var(--workbench-text)')
|
||||
expect(declarationsFor(get('#neutral')).get('border-color')).toBe('var(--workbench-border)')
|
||||
expect(declarationsFor(get('#business')).size).toBe(0)
|
||||
expect(declarationsFor(get('#error')).size).toBe(0)
|
||||
expect(declarationsFor(get('#outside')).size).toBe(0)
|
||||
expect([...declarationsFor(get('#form')).keys()]).toEqual(['--el-border-color', '--el-fill-color-blank', '--el-text-color-regular'])
|
||||
})
|
||||
|
||||
it('keeps new surfaces optical and leaves business dimensions and density untouched', () => {
|
||||
const neutralPaint = new Set(['color', 'background', 'border-color', 'border-radius', 'box-shadow', '--el-border-color', '--el-fill-color-blank', '--el-text-color-regular'])
|
||||
root(styles).walkRules(rule => {
|
||||
if (rule.selector.includes('.workbench-query') || rule.selector.includes('.is-drawer')
|
||||
|| /density-(?:detail|fields|descriptions|form|dialog-section)/.test(rule.selector)) {
|
||||
rule.walkDecls(declaration => { expect(neutralPaint.has(declaration.prop)).toBe(true) })
|
||||
}
|
||||
if (rule.selector.includes('.workbench-brand-mark')) {
|
||||
rule.walkDecls(declaration => { expect(['display', 'align-items', 'justify-content', 'border-radius', 'background', 'color']).toContain(declaration.prop) })
|
||||
}
|
||||
})
|
||||
const get = fixture('<main class="admin-workbench"><span id="brand" class="workbench-brand-mark"></span></main>')
|
||||
const brand = declarationsFor(get('#brand'))
|
||||
expect(brand.get('display')).toBe('inline-flex')
|
||||
expect(brand.get('background')).toBe('var(--workbench-primary)')
|
||||
expect(brand.get('color')).toBe('var(--workbench-surface)')
|
||||
})
|
||||
|
||||
it('preserves metric values and confines header density changes to explicit list surfaces', () => {
|
||||
root(styles).walkRules(rule => {
|
||||
if (rule.selector.includes('.workbench-metric')) {
|
||||
rule.walkDecls(declaration => {
|
||||
expect(['text-overflow', 'line-clamp', '-webkit-line-clamp']).not.toContain(declaration.prop)
|
||||
if (declaration.prop === 'overflow') expect(declaration.value).not.toBe('hidden')
|
||||
expect(declaration.prop).not.toBe('height')
|
||||
})
|
||||
}
|
||||
if (rule.selector.includes('.data-table__toolbar')) {
|
||||
rule.walkDecls(declaration => {
|
||||
if (rule.selector.includes('.is-page-surface')) {
|
||||
expect(rule.parent?.type === 'atrule' && rule.parent.params).toBe('(min-width: 721px)')
|
||||
} else if (!rule.selector.includes('.workbench-list-tabs')) {
|
||||
expect(declaration.prop).not.toMatch(/^(?:padding|height|min-height|display|gap)/)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (rule.selector.includes('.is-dialog') || rule.selector.includes('.is-drawer')) {
|
||||
rule.walkDecls(declaration => {
|
||||
expect(declaration.prop).not.toMatch(/^(?:width|height|min-height|max-height|padding|margin|overflow)/)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
const fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif'
|
||||
|
||||
const colors = {
|
||||
primary: '#3784fa',
|
||||
primarySoft: '#eff6ff',
|
||||
primaryFaint: 'rgba(55,132,250,0.03)',
|
||||
primaryLight: '#5b9bfb',
|
||||
teal: '#169b9b',
|
||||
tealLight: '#46b5bc',
|
||||
green: '#18a46f',
|
||||
purple: '#8b5cf6',
|
||||
amber: '#bf850e',
|
||||
rose: '#e85e70',
|
||||
muted: '#64748b',
|
||||
text: '#1f2937',
|
||||
border: '#e5eaf2',
|
||||
axis: '#d8e2ef',
|
||||
unknown: '#a2adbf',
|
||||
surface: '#fff',
|
||||
}
|
||||
|
||||
/** Explicitly consumed by workbench charts; never registers a global ECharts theme. */
|
||||
export const adminWorkbenchChartTheme = {
|
||||
fontFamily,
|
||||
colors,
|
||||
tooltip: {
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
extraCssText: 'box-shadow: 0 8px 24px rgba(31, 55, 90, 0.1);',
|
||||
textStyle: { color: colors.text, fontSize: 12, fontFamily },
|
||||
},
|
||||
axisLabel: { color: colors.muted, fontSize: 12 },
|
||||
axisName: { color: colors.muted, fontSize: 12 },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { type: 'dashed' as const, color: colors.border, opacity: 0.8 },
|
||||
},
|
||||
}
|
||||
|
||||
export type AdminWorkbenchChartTheme = typeof adminWorkbenchChartTheme
|
||||
@@ -0,0 +1,74 @@
|
||||
/* Blue-and-white workbench preview. Pages opt in without changing admin density. */
|
||||
.admin-workbench {
|
||||
--workbench-primary: #3784fa;
|
||||
--workbench-primary-hover: #5b9bfb;
|
||||
--workbench-primary-active: #276dd5;
|
||||
--workbench-primary-disabled: #a9c9fc;
|
||||
--workbench-number: #1890ff;
|
||||
--workbench-text: #1f2937;
|
||||
--workbench-muted: #64748b;
|
||||
--workbench-surface: #fff;
|
||||
--workbench-surface-soft: #f5f7fb;
|
||||
--workbench-surface-tint: #f8faff;
|
||||
--workbench-border: #e5eaf2;
|
||||
--workbench-shadow: 0 4px 18px rgb(31 55 90 / 4%);
|
||||
--workbench-radius: 16px;
|
||||
--workbench-icon-radius: 12px;
|
||||
--workbench-page-padding: 12px;
|
||||
--workbench-list-inset: 12px;
|
||||
--workbench-list-toolbar-height: 48px;
|
||||
--workbench-list-toolbar-padding: 6px;
|
||||
--workbench-list-query-padding: 8px;
|
||||
--workbench-page-space: 24px;
|
||||
--workbench-section-gap: 16px;
|
||||
--workbench-grid-gap: 18px;
|
||||
--workbench-card-space: 20px;
|
||||
--workbench-metric-min-height: 186px;
|
||||
--workbench-title-size: 24px;
|
||||
--workbench-section-title-size: 18px;
|
||||
--workbench-label-size: 15px;
|
||||
--workbench-number-size: 34px;
|
||||
--workbench-compact-number-size: 26px;
|
||||
--workbench-compact-card-space: 14px;
|
||||
--workbench-tab-radius: 8px;
|
||||
--workbench-note-size: 13px;
|
||||
--workbench-accent: #3784fa;
|
||||
--workbench-accent-soft: #eff6ff;
|
||||
--workbench-accent-border: #dbeafe;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tone[data-tone='blue'] {
|
||||
--workbench-accent: #3784fa;
|
||||
--workbench-accent-soft: #eff6ff;
|
||||
--workbench-accent-border: #dbeafe;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tone[data-tone='teal'] {
|
||||
--workbench-accent: #169b9b;
|
||||
--workbench-accent-soft: #effcfa;
|
||||
--workbench-accent-border: #c7eeea;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tone[data-tone='green'] {
|
||||
--workbench-accent: #18a46f;
|
||||
--workbench-accent-soft: #ecfdf5;
|
||||
--workbench-accent-border: #c7efdd;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tone[data-tone='purple'] {
|
||||
--workbench-accent: #8b5cf6;
|
||||
--workbench-accent-soft: #f5f3ff;
|
||||
--workbench-accent-border: #e6ddfc;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tone[data-tone='amber'] {
|
||||
--workbench-accent: #bf850e;
|
||||
--workbench-accent-soft: #fffbeb;
|
||||
--workbench-accent-border: #f5e2ad;
|
||||
}
|
||||
|
||||
.admin-workbench .workbench-tone[data-tone='rose'] {
|
||||
--workbench-accent: #e85e70;
|
||||
--workbench-accent-soft: #fff1f3;
|
||||
--workbench-accent-border: #f8d7df;
|
||||
}
|
||||
+1
-20
@@ -1,7 +1,7 @@
|
||||
@import './foundation.css';
|
||||
@import './shell.css';
|
||||
|
||||
.forbidden-page { display: flex; min-height: min(560px, calc(100vh - 136px)); align-items: center; justify-content: center; flex-direction: column; padding: clamp(28px, 7vw, 64px) clamp(18px, 6vw, 48px); border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; text-align: center; }
|
||||
.forbidden-page { display: flex; min-height: min(560px, calc(100vh - 136px)); align-items: center; justify-content: center; flex-direction: column; padding: clamp(28px, 7vw, 64px) clamp(18px, 6vw, 48px); border: 1px solid var(--workbench-border); border-radius: var(--workbench-radius); background: var(--workbench-surface); color: var(--workbench-text); box-shadow: var(--workbench-shadow); text-align: center; }
|
||||
.forbidden-icon { display: grid; width: 56px; height: 56px; place-items: center; margin-bottom: 18px; border-radius: 8px; background: #fef3f2; color: #b42318; }
|
||||
.forbidden-page .section-kicker { margin: 0 0 8px; }
|
||||
.forbidden-page h2 { max-width: 560px; margin: 0 0 10px; font-size: 20px; line-height: 1.35; }
|
||||
@@ -9,23 +9,7 @@
|
||||
.forbidden-page .forbidden-source { margin-bottom: 20px; color: #475467; font-size: 12px; }
|
||||
.forbidden-page .el-button { min-height: 44px; }
|
||||
|
||||
.login-page { display: grid; min-height: 100vh; grid-template-columns: minmax(420px, 1.05fr) minmax(420px, .95fr); background: #fff; }
|
||||
.login-intro { display: flex; flex-direction: column; justify-content: space-between; padding: 46px clamp(42px, 7vw, 100px); background: var(--cc-navy); color: #fff; }
|
||||
.login-brand { display: flex; align-items: center; min-height: 64px; }
|
||||
.login-brand-logo { display: block; width: 205px; height: 64px; object-fit: contain; object-position: left center; }
|
||||
.eyebrow, .section-kicker { color: #3975e9; font-size: 11px; font-weight: 750; letter-spacing: 0; }
|
||||
.login-intro h1 { max-width: 680px; margin: 16px 0 22px; font-size: 52px; line-height: 1.16; letter-spacing: 0; }
|
||||
.intro-copy { max-width: 560px; color: #b9c6d8; font-size: 16px; line-height: 1.8; }
|
||||
.security-note { display: flex; align-items: center; gap: 10px; color: #cbd5e1; font-size: 13px; }
|
||||
.login-panel { display: grid; place-items: center; padding: 48px; }
|
||||
.login-card { width: min(410px, 100%); }
|
||||
.login-card-icon { display: grid; width: 48px; height: 48px; place-items: center; border-radius: 12px; background: #eaf1ff; color: var(--cc-primary); }
|
||||
.login-card h2 { margin: 22px 0 8px; font-size: 28px; }
|
||||
.login-card > p { margin: 0 0 30px; color: var(--cc-muted); }
|
||||
.login-submit { width: 100%; }
|
||||
.login-submit span { display: flex; align-items: center; gap: 8px; }
|
||||
.login-error { margin: 14px 0 0; color: #b42318; font-size: 13px; line-height: 1.5; }
|
||||
.login-error > p { margin: 0; }
|
||||
.api-error-alert-title { min-width: 0; }
|
||||
|
||||
.dashboard-stack { display: flex; flex-direction: column; gap: 20px; }
|
||||
@@ -205,8 +189,6 @@
|
||||
.filter-panel { align-items: stretch; flex-direction: column; }
|
||||
.filter-actions { justify-content: flex-end; }
|
||||
.form-grid.three-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.login-page { grid-template-columns: 1fr; }
|
||||
.login-intro { display: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -255,7 +237,6 @@
|
||||
.detail-identity .status-badge { grid-column: 2; justify-self: start; }
|
||||
.section-heading-row { flex-direction: column; }
|
||||
.legend-list { display: none; }
|
||||
.login-panel { padding: 30px 20px; }
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
|
||||
+47
-22
@@ -1,7 +1,7 @@
|
||||
.app-shell { min-width: 0; min-height: 100vh; }
|
||||
.app-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 20; display: flex; width: 240px; flex-direction: column; border-right: 1px solid rgba(255,255,255,.3); background: rgba(255,255,255,.92); color: var(--cc-text-secondary); box-shadow: 4px 0 32px rgba(0,0,0,.08); backdrop-filter: blur(20px); }
|
||||
.app-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 20; display: flex; width: 240px; flex-direction: column; border-right: 1px solid var(--workbench-border); background: var(--workbench-surface); color: var(--workbench-text); }
|
||||
.is-collapsed .app-sidebar { width: 76px; }
|
||||
.brand-block { display: flex; height: 64px; flex: 0 0 64px; align-items: center; padding: 0 18px; background: var(--cc-topbar); }
|
||||
.brand-block { display: flex; height: 64px; flex: 0 0 64px; align-items: center; padding: 0 18px; background: var(--workbench-primary); }
|
||||
.brand-logo { display: block; object-fit: contain; object-position: left center; }
|
||||
.brand-logo-full { width: 184px; height: 50px; }
|
||||
.brand-logo-mark { display: none; width: 40px; height: 40px; }
|
||||
@@ -9,7 +9,9 @@
|
||||
.is-collapsed .brand-logo-mark { display: block; }
|
||||
.side-nav {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 12px 8px;
|
||||
scrollbar-color: rgba(21, 94, 239, .22) transparent;
|
||||
@@ -29,39 +31,62 @@
|
||||
border-right: 0 !important;
|
||||
background: transparent !important;
|
||||
--el-menu-bg-color: transparent;
|
||||
--el-menu-text-color: var(--cc-text-secondary);
|
||||
--el-menu-active-color: var(--cc-menu-active);
|
||||
--el-menu-hover-bg-color: var(--cc-primary-soft);
|
||||
--el-menu-text-color: var(--workbench-text);
|
||||
--el-menu-active-color: var(--workbench-primary);
|
||||
--el-menu-hover-bg-color: var(--workbench-accent-soft);
|
||||
}
|
||||
.side-nav-menu.el-menu--collapse { width: 56px; }
|
||||
.side-nav-menu.el-menu--collapse { width: 100%; }
|
||||
.side-nav-menu .el-menu-item,
|
||||
.side-nav-menu .el-sub-menu__title { height: 44px; margin: 2px 0; border-radius: 0 var(--cc-radius-lg) var(--cc-radius-lg) 0; line-height: 44px; transition: color .16s ease, background .16s ease; }
|
||||
.side-nav-menu .el-menu-item:hover,
|
||||
.side-nav-menu .el-sub-menu__title:hover { color: var(--cc-menu-active); background: linear-gradient(135deg, rgba(102,126,234,.10), rgba(240,147,251,.08)); }
|
||||
.side-nav-menu .el-menu-item.is-active { background: linear-gradient(135deg, rgba(102,126,234,.18), rgba(240,147,251,.12)); color: var(--cc-menu-active); font-weight: 700; box-shadow: inset 3px 0 var(--cc-menu-active); }
|
||||
.side-nav-menu .el-sub-menu .el-menu { padding: 4px 0; background: var(--cc-surface-muted) !important; }
|
||||
.side-nav-menu .el-sub-menu .el-menu-item { min-width: 0; height: 40px; margin: 0 12px; padding-left: 54px !important; border-radius: var(--cc-radius-md); color: var(--cc-muted); line-height: 40px; }
|
||||
.side-nav-menu .el-sub-menu .el-menu-item:hover { background: rgba(102,126,234,.08); color: var(--cc-menu-active); }
|
||||
.side-nav-menu .el-sub-menu .el-menu-item.is-active { background: rgba(102,126,234,.12); box-shadow: none; color: var(--cc-menu-active); font-weight: 700; }
|
||||
.side-nav-menu .el-sub-menu__title:hover { color: var(--workbench-primary); background: var(--workbench-accent-soft); }
|
||||
.side-nav-menu .el-menu-item.is-active { background: var(--workbench-accent-soft); color: var(--workbench-primary); font-weight: 700; box-shadow: inset 3px 0 var(--workbench-primary); }
|
||||
.side-nav-menu .el-sub-menu .el-menu { padding: 4px 0; background: var(--workbench-surface-soft) !important; }
|
||||
.side-nav-menu .el-sub-menu .el-menu-item { min-width: 0; height: 40px; margin: 0 12px; padding-left: 54px !important; border-radius: var(--cc-radius-md); color: var(--workbench-muted); line-height: 40px; }
|
||||
.side-nav-menu .el-sub-menu .el-menu-item:hover { background: var(--workbench-accent-soft); color: var(--workbench-primary); }
|
||||
.side-nav-menu .el-sub-menu .el-menu-item.is-active { background: var(--workbench-accent-soft); box-shadow: none; color: var(--workbench-primary); font-weight: 700; }
|
||||
.side-nav-menu .el-menu-item:focus-visible,
|
||||
.side-nav-menu .el-sub-menu__title:focus-visible { outline: 2px solid var(--workbench-primary); outline-offset: -2px; }
|
||||
.side-nav-menu .el-menu-item svg,
|
||||
.side-nav-menu .el-sub-menu__title svg { flex: 0 0 auto; margin-right: 12px; }
|
||||
/* Use the width left after the vertical scrollbar, including Element's leaf tooltip trigger. */
|
||||
.side-nav-menu.el-menu--collapse > .el-menu-item,
|
||||
.side-nav-menu.el-menu--collapse > .el-sub-menu > .el-sub-menu__title,
|
||||
.side-nav-menu.el-menu--collapse > .el-menu-item > .el-menu-tooltip__trigger { justify-content: center; padding: 0; }
|
||||
.side-nav-menu.el-menu--collapse > .el-menu-item .menu-icon,
|
||||
.side-nav-menu.el-menu--collapse > .el-sub-menu > .el-sub-menu__title .menu-icon { margin-right: 0; }
|
||||
.app-main { min-width: 0; min-height: 100vh; margin-left: 240px; }
|
||||
.is-collapsed .app-main { margin-left: 76px; }
|
||||
.app-header { position: sticky; top: 0; z-index: 15; display: flex; min-width: 0; min-height: 64px; align-items: center; gap: 16px; padding: 8px 20px; background: var(--cc-topbar); color: #fff; box-shadow: 0 8px 32px rgba(0,0,0,.15), 0 2px 8px rgba(0,0,0,.1); backdrop-filter: blur(20px); }
|
||||
.shell-menu-button { display: grid; width: 44px; height: 44px; flex: 0 0 44px; place-items: center; padding: 0; border: 0; border-radius: var(--cc-radius-md); background: transparent; color: #fff; cursor: pointer; transition: background .16s ease; }
|
||||
.shell-menu-button:hover { background: rgba(255,255,255,.12); }
|
||||
.shell-menu-button:focus-visible { outline-color: rgba(255,255,255,.8); }
|
||||
.app-header { position: sticky; top: 0; z-index: 15; display: flex; min-width: 0; min-height: 64px; align-items: center; gap: 16px; padding: 8px 20px; border-bottom: 1px solid var(--workbench-border); background: var(--workbench-surface); color: var(--workbench-text); }
|
||||
.shell-menu-button { display: grid; width: 44px; height: 44px; flex: 0 0 44px; place-items: center; padding: 0; border: 0; border-radius: var(--cc-radius-md); background: transparent; color: var(--workbench-muted); cursor: pointer; transition: background .16s ease; }
|
||||
.shell-menu-button:hover { background: var(--workbench-accent-soft); color: var(--workbench-primary); }
|
||||
.shell-menu-button:focus-visible { outline: 2px solid var(--workbench-primary); outline-offset: 2px; }
|
||||
.page-heading { min-width: 0; flex: 1; }
|
||||
.page-heading h1 { margin: 0; overflow: hidden; font-size: var(--cc-list-page-title-font-size); letter-spacing: 0; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.page-heading p { margin: 4px 0 0; overflow: hidden; color: rgba(255,255,255,.72); font-size: var(--cc-list-page-description-font-size); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.page-heading p { margin: 4px 0 0; overflow: hidden; color: var(--workbench-muted); font-size: var(--cc-list-page-description-font-size); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.header-user { display: flex; min-width: 0; flex: 0 0 auto; align-items: center; gap: 10px; }
|
||||
.user-avatar { display: grid; width: 36px; height: 36px; place-items: center; border-radius: 50%; background: #fff; color: var(--cc-primary); font-weight: 700; box-shadow: var(--cc-shadow-sm); }
|
||||
.user-avatar { display: grid; width: 36px; height: 36px; place-items: center; border-radius: 50%; background: var(--workbench-accent-soft); color: var(--workbench-primary); font-weight: 700; }
|
||||
.user-copy { display: flex; flex-direction: column; }
|
||||
.user-copy strong { font-size: 13px; }
|
||||
.user-copy span { color: var(--cc-muted); font-size: 11px; }
|
||||
.header-user .el-button { width: 44px; height: 44px; color: #fff; }
|
||||
.header-user .el-button:hover { background: rgba(255,255,255,.15); color: #fff; }
|
||||
.page-container { padding: var(--cc-space-5) var(--cc-space-5) var(--cc-space-7); }
|
||||
.user-copy span { color: var(--workbench-muted); font-size: 11px; }
|
||||
.header-user .el-button { width: 44px; height: 44px; color: var(--workbench-muted); }
|
||||
.header-user .el-button:hover { background: var(--workbench-accent-soft); color: var(--workbench-primary); }
|
||||
.page-container { padding: var(--cc-space-3) var(--cc-space-3) var(--cc-space-4); }
|
||||
|
||||
/* Collapsed menus and header dropdowns are teleported outside the themed shell regions. */
|
||||
.admin-workbench.shell-nav-popper {
|
||||
--el-menu-bg-color: var(--workbench-surface);
|
||||
--el-menu-text-color: var(--workbench-text);
|
||||
--el-menu-active-color: var(--workbench-primary);
|
||||
--el-menu-hover-bg-color: var(--workbench-accent-soft);
|
||||
border-color: var(--workbench-border);
|
||||
box-shadow: var(--workbench-shadow);
|
||||
}
|
||||
.admin-workbench.shell-nav-popper .el-menu--popup { background: var(--workbench-surface); box-shadow: none; }
|
||||
.admin-workbench.shell-nav-popper .el-menu-item.is-active { background: var(--workbench-accent-soft); color: var(--workbench-primary); }
|
||||
.admin-workbench.shell-nav-popper .el-menu-item:focus-visible,
|
||||
.admin-workbench.shell-nav-popper .el-sub-menu__title:focus-visible { outline: 2px solid var(--workbench-primary); outline-offset: -2px; }
|
||||
.admin-workbench.shell-header-popper { border-color: var(--workbench-border); box-shadow: var(--workbench-shadow); }
|
||||
|
||||
/* Static permission preview */
|
||||
.app-header > .el-dropdown { min-width: 0; max-width: 320px; flex: 0 1 320px; }
|
||||
|
||||
Reference in New Issue
Block a user