Add console controls and service carousel
This commit is contained in:
parent
52ef20db0f
commit
79ebbd455c
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './styles.css';
|
||||
|
||||
@ -89,6 +89,8 @@ function App() {
|
||||
const [terminalExpanded, setTerminalExpanded] = useState(false);
|
||||
const [terminalCollapsed, setTerminalCollapsed] = useState(false);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [language, setLanguage] = useState<'en' | 'zh'>('en');
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://127.0.0.1:8788/services')
|
||||
@ -107,6 +109,63 @@ function App() {
|
||||
|
||||
const currentServices = services ?? mockServices;
|
||||
const selected = tabs.find((tab) => tab.id === selectedTab);
|
||||
const labels = language === 'zh'
|
||||
? {
|
||||
product: 'XWorkspace',
|
||||
workspace: '工作空间',
|
||||
collapse: '收起',
|
||||
expand: '展开',
|
||||
connected: '已连接',
|
||||
agentsRunning: '个 Agent 运行中',
|
||||
vaultReady: 'Vault 就绪',
|
||||
homepageTitle: 'AI Workspace 控制面板',
|
||||
homepageSubtitle: '在一个工作空间里统一组织 Runtime、Gateway 和本地 AI 服务。',
|
||||
workspaceReady: '工作空间就绪',
|
||||
activity: '服务活动',
|
||||
coreServices: '核心服务',
|
||||
serviceCards: '服务卡片',
|
||||
today: '今天',
|
||||
newTab: '新标签',
|
||||
terminal: '终端',
|
||||
maximize: '最大化',
|
||||
restore: '还原',
|
||||
themeLight: '浅色',
|
||||
themeDark: '深色',
|
||||
lang: '中/EN',
|
||||
languageLabel: '语言',
|
||||
themeLabel: '主题',
|
||||
}
|
||||
: {
|
||||
product: 'XWorkspace',
|
||||
workspace: 'Workspace',
|
||||
collapse: 'Collapse',
|
||||
expand: 'Expand',
|
||||
connected: 'Connected',
|
||||
agentsRunning: 'Agents Running',
|
||||
vaultReady: 'Vault Ready',
|
||||
homepageTitle: 'AI Workspace Control Plane',
|
||||
homepageSubtitle: 'Runtime, gateway and local AI services are organized in one workspace.',
|
||||
workspaceReady: 'Workspace Ready',
|
||||
activity: 'Service Activity',
|
||||
coreServices: 'Core Services',
|
||||
serviceCards: 'Service Cards',
|
||||
today: 'Today',
|
||||
newTab: 'New Tab',
|
||||
terminal: 'Terminal',
|
||||
maximize: 'Maximize',
|
||||
restore: 'Restore',
|
||||
themeLight: 'Light',
|
||||
themeDark: 'Dark',
|
||||
lang: 'EN/中',
|
||||
languageLabel: 'Language',
|
||||
themeLabel: 'Theme',
|
||||
};
|
||||
|
||||
const breadcrumbItems = [
|
||||
labels.product,
|
||||
labels.workspace,
|
||||
selected && selected.id !== 'workspace' ? selected.label : null,
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
const summary = useMemo(() => {
|
||||
const runningServices = currentServices.filter((service) => service.state === 'Running').length;
|
||||
@ -137,11 +196,11 @@ function App() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={sidebarCollapsed ? 'app-shell sidebar-collapsed' : 'app-shell'}>
|
||||
<div className={[sidebarCollapsed ? 'app-shell sidebar-collapsed' : 'app-shell', theme === 'dark' ? 'theme-dark' : ''].join(' ')}>
|
||||
<aside className="sidebar">
|
||||
<div className="brand">
|
||||
<span className="brand-x">X</span>
|
||||
<strong>XWorkspace</strong>
|
||||
<strong>{labels.product}</strong>
|
||||
</div>
|
||||
|
||||
<nav className="side-nav" aria-label="XWorkspace navigation">
|
||||
@ -165,22 +224,42 @@ function App() {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<button className="collapse-button" type="button" onClick={() => setSidebarCollapsed((value) => !value)}>
|
||||
<Icon name={sidebarCollapsed ? 'menu' : 'arrow-left'} />
|
||||
<span>Collapse</span>
|
||||
<div className="sidebar-tools">
|
||||
<button className="sidebar-tool-button" type="button" onClick={() => setLanguage((value) => (value === 'en' ? 'zh' : 'en'))}>
|
||||
<span>{labels.languageLabel}</span>
|
||||
<strong>{labels.lang}</strong>
|
||||
</button>
|
||||
<button className="sidebar-tool-button" type="button" onClick={() => setTheme((value) => (value === 'light' ? 'dark' : 'light'))}>
|
||||
<span>{labels.themeLabel}</span>
|
||||
<strong>{theme === 'light' ? labels.themeDark : labels.themeLight}</strong>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button className="collapse-button" type="button" aria-label={sidebarCollapsed ? labels.expand : labels.collapse} onClick={() => setSidebarCollapsed((value) => !value)}>
|
||||
<Icon name={sidebarCollapsed ? 'chevrons-right' : 'chevrons-left'} />
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<main className="workspace">
|
||||
<header className="topbar">
|
||||
<button className="menu-button" type="button" aria-label="Toggle sidebar" onClick={() => setSidebarCollapsed((value) => !value)}>
|
||||
<Icon name="menu" />
|
||||
</button>
|
||||
<div className="topbar-left">
|
||||
<button className="menu-button" type="button" aria-label="Toggle sidebar" onClick={() => setSidebarCollapsed((value) => !value)}>
|
||||
<Icon name="menu" />
|
||||
</button>
|
||||
<nav className="breadcrumb" aria-label="Breadcrumb">
|
||||
{breadcrumbItems.map((item, index) => (
|
||||
<React.Fragment key={`${item}-${index}`}>
|
||||
{index > 0 ? <span className="breadcrumb-separator">/</span> : null}
|
||||
<span className={index === breadcrumbItems.length - 1 ? 'breadcrumb-current' : ''}>{item}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="status-strip">
|
||||
<StatusItem label="Asia/Shanghai" icon="globe" />
|
||||
<StatusItem label="Connected" icon="wifi" good />
|
||||
<StatusItem label={`${summary.runningAgents} Agents Running`} icon="bot" good />
|
||||
<StatusItem label="Vault Ready" icon="shield" good />
|
||||
<StatusItem label={labels.connected} icon="wifi" good />
|
||||
<StatusItem label={language === 'zh' ? `${summary.runningAgents} ${labels.agentsRunning}` : `${summary.runningAgents} ${labels.agentsRunning}`} icon="bot" good />
|
||||
<StatusItem label={labels.vaultReady} icon="shield" good />
|
||||
<strong>10:30</strong>
|
||||
<button className="round-button" type="button" aria-label="Notifications">
|
||||
<Icon name="bell" />
|
||||
@ -228,10 +307,11 @@ function App() {
|
||||
{selected?.kind === 'embed' && selected.id !== 'workspace' ? (
|
||||
<EmbedWorkspace tab={selected} />
|
||||
) : (
|
||||
<WorkspaceHome services={currentServices} summary={summary} onOpenHome={() => setSelectedTab('workspace')} />
|
||||
<WorkspaceHome labels={labels} services={currentServices} summary={summary} onOpenHome={() => setSelectedTab('workspace')} />
|
||||
)}
|
||||
|
||||
<TerminalDrawer
|
||||
labels={labels}
|
||||
collapsed={terminalCollapsed}
|
||||
expanded={terminalExpanded}
|
||||
onCollapse={() => setTerminalCollapsed((value) => !value)}
|
||||
@ -243,31 +323,50 @@ function App() {
|
||||
}
|
||||
|
||||
function WorkspaceHome({
|
||||
labels,
|
||||
services,
|
||||
summary,
|
||||
onOpenHome,
|
||||
}: {
|
||||
labels: {
|
||||
homepageTitle: string;
|
||||
homepageSubtitle: string;
|
||||
workspaceReady: string;
|
||||
activity: string;
|
||||
serviceCards: string;
|
||||
coreServices: string;
|
||||
today: string;
|
||||
};
|
||||
services: Service[];
|
||||
summary: { runningServices: number; runningAgents: number; runningTasks: number };
|
||||
onOpenHome: () => void;
|
||||
}) {
|
||||
const cardsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const scrollCards = (direction: 'left' | 'right') => {
|
||||
const viewport = cardsRef.current;
|
||||
if (!viewport) return;
|
||||
const distance = Math.max(280, Math.floor(viewport.clientWidth * 0.72));
|
||||
viewport.scrollBy({ left: direction === 'left' ? -distance : distance, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="workspace-body">
|
||||
<section className="console-board">
|
||||
<div className="command-panel">
|
||||
<div className="board-heading">
|
||||
<div>
|
||||
<h1>AI Workspace Control Plane</h1>
|
||||
<p>Runtime, gateway and local AI services are organized in one workspace.</p>
|
||||
<h1>{labels.homepageTitle}</h1>
|
||||
<p>{labels.homepageSubtitle}</p>
|
||||
</div>
|
||||
<span className="healthy-chip"><span /> Workspace Ready</span>
|
||||
<span className="healthy-chip"><span /> {labels.workspaceReady}</span>
|
||||
</div>
|
||||
|
||||
<div className="activity-card">
|
||||
<div className="activity-head">
|
||||
<h2>Service Activity</h2>
|
||||
<h2>{labels.activity}</h2>
|
||||
<div className="range-tabs" aria-label="Service activity range">
|
||||
{['Today', '7d', '2w', '1m'].map((range, index) => <span className={index === 1 ? 'active' : ''} key={range}>{range}</span>)}
|
||||
{[labels.today, '7d', '2w', '1m'].map((range, index) => <span className={index === 1 ? 'active' : ''} key={range}>{range}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="service-chart" aria-hidden="true">
|
||||
@ -280,21 +379,48 @@ function WorkspaceHome({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a className="service-cards homepage-link" href="#workspace" onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onOpenHome();
|
||||
}}>
|
||||
{services.slice(0, 4).map((service, index) => (
|
||||
<article className={index === 1 ? 'service-card tilted' : 'service-card'} key={service.name}>
|
||||
<Icon name={service.name.toLowerCase().includes('vault') ? 'shield' : service.name.toLowerCase().includes('lite') ? 'chart' : service.name.toLowerCase().includes('bridge') ? 'bridge' : 'claw'} />
|
||||
<span>{service.name}</span>
|
||||
<strong>{service.state}</strong>
|
||||
</article>
|
||||
))}
|
||||
</a>
|
||||
<section className="service-carousel">
|
||||
<div className="carousel-head">
|
||||
<h2>{labels.serviceCards}</h2>
|
||||
<div className="carousel-actions">
|
||||
<button type="button" aria-label="Scroll service cards left" onClick={() => scrollCards('left')}>
|
||||
<Icon name="chevron-left" />
|
||||
</button>
|
||||
<button type="button" aria-label="Scroll service cards right" onClick={() => scrollCards('right')}>
|
||||
<Icon name="chevron-right" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="service-cards-scroll"
|
||||
ref={cardsRef}
|
||||
onWheel={(event) => {
|
||||
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
|
||||
event.currentTarget.scrollBy({ left: event.deltaY, behavior: 'auto' });
|
||||
}}
|
||||
>
|
||||
<a className="service-cards homepage-link" href="#workspace" onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onOpenHome();
|
||||
}}>
|
||||
{services.map((service, index) => (
|
||||
<article className={index === 1 ? 'service-card tilted' : 'service-card'} key={service.name}>
|
||||
<Icon name={service.name.toLowerCase().includes('vault') ? 'shield' : service.name.toLowerCase().includes('lite') ? 'chart' : service.name.toLowerCase().includes('bridge') ? 'bridge' : 'claw'} />
|
||||
<span>{service.name}</span>
|
||||
<strong>{service.state}</strong>
|
||||
</article>
|
||||
))}
|
||||
<article className="service-card ghost-card">
|
||||
<Icon name="cube" />
|
||||
<span>Future Probe</span>
|
||||
<strong>Reserved</strong>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section className="service-summary panel">
|
||||
<div className="panel-head">
|
||||
<h2>Core Services</h2>
|
||||
<h2>{labels.coreServices}</h2>
|
||||
</div>
|
||||
<div className="service-list compact">
|
||||
{services.map((service) => (
|
||||
@ -331,11 +457,20 @@ function EmbedWorkspace({ tab }: { tab: Tab }) {
|
||||
}
|
||||
|
||||
function TerminalDrawer({
|
||||
labels,
|
||||
collapsed,
|
||||
expanded,
|
||||
onCollapse,
|
||||
onToggle,
|
||||
}: {
|
||||
labels: {
|
||||
terminal: string;
|
||||
newTab: string;
|
||||
maximize: string;
|
||||
restore: string;
|
||||
collapse: string;
|
||||
expand: string;
|
||||
};
|
||||
collapsed: boolean;
|
||||
expanded: boolean;
|
||||
onCollapse: () => void;
|
||||
@ -346,12 +481,12 @@ function TerminalDrawer({
|
||||
<div className="terminal-head">
|
||||
<div>
|
||||
<Icon name="terminal" />
|
||||
<strong>Terminal</strong>
|
||||
<strong>{labels.terminal}</strong>
|
||||
</div>
|
||||
<div className="terminal-actions">
|
||||
<a href="http://127.0.0.1:7681" target="_blank" rel="noreferrer">New Tab</a>
|
||||
<button type="button" onClick={onCollapse}>{collapsed ? 'Expand' : 'Collapse'}</button>
|
||||
<button type="button" onClick={onToggle}>{expanded ? 'Restore' : 'Maximize'}</button>
|
||||
<a href="http://127.0.0.1:7681" target="_blank" rel="noreferrer">{labels.newTab}</a>
|
||||
<button type="button" onClick={onCollapse}>{collapsed ? labels.expand : labels.collapse}</button>
|
||||
<button type="button" onClick={onToggle}>{expanded ? labels.restore : labels.maximize}</button>
|
||||
<button type="button" aria-label="Terminal menu">⋮</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -417,7 +552,14 @@ function Icon({ name }: { name: string }) {
|
||||
wifi: <path d="M4 9a12 12 0 0 1 16 0M7 12a7.5 7.5 0 0 1 10 0M10 15a3 3 0 0 1 4 0M12 19h.01" />,
|
||||
bell: <path d="M18 16H6l1.4-2V10a4.6 4.6 0 0 1 9.2 0v4zM10 19h4" />,
|
||||
user: <path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM4 21a8 8 0 0 1 16 0" />,
|
||||
languages: <path d="M4 6h9M8.5 4v2c0 4-2.2 7.2-5.5 9M6 10c1.5 2.2 3.8 4.1 6.6 5.5M14 18h7M17.5 6l4.5 12M20.3 13h-5.6" />,
|
||||
moon: <path d="M20 14.5A7.5 7.5 0 1 1 9.5 4 6 6 0 0 0 20 14.5Z" />,
|
||||
sun: <path d="M12 3v2.2M12 18.8V21M4.9 4.9l1.6 1.6M17.5 17.5l1.6 1.6M3 12h2.2M18.8 12H21M4.9 19.1l1.6-1.6M17.5 6.5l1.6-1.6M12 7.2a4.8 4.8 0 1 0 0 9.6 4.8 4.8 0 0 0 0-9.6Z" />,
|
||||
'arrow-left': <path d="M19 12H5M12 5l-7 7 7 7" />,
|
||||
'chevron-left': <path d="m15 18-6-6 6-6" />,
|
||||
'chevron-right': <path d="m9 18 6-6-6-6" />,
|
||||
'chevrons-left': <path d="m13.5 17-5-5 5-5M19 17l-5-5 5-5" />,
|
||||
'chevrons-right': <path d="m10.5 17 5-5-5-5M5 17l5-5-5-5" />,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -50,6 +50,19 @@ button {
|
||||
grid-template-columns: 76px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.app-shell.theme-dark {
|
||||
--text: #e8edf8;
|
||||
--muted: #97a3b8;
|
||||
--paper: rgba(21, 29, 43, 0.88);
|
||||
--line: rgba(255, 255, 255, 0.08);
|
||||
--line-strong: rgba(255, 255, 255, 0.14);
|
||||
--shadow: 0 18px 42px rgba(5, 10, 18, 0.32);
|
||||
}
|
||||
|
||||
.app-shell.theme-dark body {
|
||||
background: #0e1420;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@ -92,7 +105,8 @@ button {
|
||||
|
||||
.sidebar-collapsed .brand strong,
|
||||
.sidebar-collapsed .side-nav span,
|
||||
.sidebar-collapsed .collapse-button span {
|
||||
.sidebar-collapsed .sidebar-tool-button span,
|
||||
.sidebar-collapsed .sidebar-tool-button strong {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -144,24 +158,71 @@ button {
|
||||
}
|
||||
|
||||
.collapse-button {
|
||||
margin-top: auto;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 18px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-top: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: #7f8aa0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-tools {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sidebar-tool-button {
|
||||
min-height: 64px;
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sidebar-collapsed .collapse-button {
|
||||
justify-content: center;
|
||||
.sidebar-tool-button .icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.sidebar-tool-button span {
|
||||
color: #a9b3c4;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sidebar-tool-button strong {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.sidebar-collapsed .sidebar-tool-button {
|
||||
min-height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-collapsed .collapse-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
@ -182,6 +243,13 @@ button {
|
||||
backdrop-filter: blur(22px);
|
||||
}
|
||||
|
||||
.topbar-left {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.menu-button,
|
||||
.round-button {
|
||||
width: 28px;
|
||||
@ -203,6 +271,32 @@ button {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.breadcrumb span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
flex: 0 0 auto;
|
||||
color: rgba(99, 112, 138, 0.75);
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -319,6 +413,56 @@ button {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.app-shell.theme-dark .topbar {
|
||||
background: rgba(12, 18, 29, 0.84);
|
||||
}
|
||||
|
||||
.app-shell.theme-dark .menu-button,
|
||||
.app-shell.theme-dark .round-button,
|
||||
.app-shell.theme-dark .carousel-actions button,
|
||||
.app-shell.theme-dark .terminal-actions a,
|
||||
.app-shell.theme-dark .terminal-actions button {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.app-shell.theme-dark .sidebar-tool-button,
|
||||
.app-shell.theme-dark .collapse-button {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: #c7d0de;
|
||||
}
|
||||
|
||||
.app-shell.theme-dark .tab {
|
||||
color: #a9b5ca;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.app-shell.theme-dark .tab.active,
|
||||
.app-shell.theme-dark .activity-card,
|
||||
.app-shell.theme-dark .service-card,
|
||||
.app-shell.theme-dark .service-summary,
|
||||
.app-shell.theme-dark .embed-panel,
|
||||
.app-shell.theme-dark .terminal-drawer {
|
||||
background: rgba(21, 29, 43, 0.88);
|
||||
}
|
||||
|
||||
.app-shell.theme-dark .range-tabs .active {
|
||||
background: rgba(132, 201, 244, 0.22);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.app-shell.theme-dark .service-card.ghost-card {
|
||||
background: rgba(29, 40, 58, 0.78);
|
||||
}
|
||||
|
||||
.app-shell.theme-dark .service-card span,
|
||||
.app-shell.theme-dark .board-heading p,
|
||||
.app-shell.theme-dark .breadcrumb,
|
||||
.app-shell.theme-dark .status-strip,
|
||||
.app-shell.theme-dark .service-row span:last-child {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
min-height: 280px;
|
||||
padding: 24px;
|
||||
@ -400,7 +544,8 @@ button {
|
||||
|
||||
.service-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(250px, 280px);
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
@ -409,6 +554,57 @@ button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.service-carousel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.carousel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.carousel-head h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.carousel-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.carousel-actions button {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.service-cards-scroll {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 4px 2px 14px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(76, 105, 167, 0.45) transparent;
|
||||
}
|
||||
|
||||
.service-cards-scroll::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.service-cards-scroll::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: rgba(76, 105, 167, 0.45);
|
||||
}
|
||||
|
||||
.service-card {
|
||||
min-height: 144px;
|
||||
display: grid;
|
||||
@ -427,6 +623,11 @@ button {
|
||||
transform: rotate(3deg);
|
||||
}
|
||||
|
||||
.service-card.ghost-card {
|
||||
border-style: dashed;
|
||||
background: rgba(236, 243, 255, 0.74);
|
||||
}
|
||||
|
||||
.service-card .icon {
|
||||
color: var(--blue);
|
||||
}
|
||||
@ -816,7 +1017,7 @@ button {
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.service-cards {
|
||||
grid-template-columns: repeat(2, minmax(160px, 1fr));
|
||||
grid-auto-columns: minmax(240px, 260px);
|
||||
}
|
||||
|
||||
.core-grid,
|
||||
@ -853,6 +1054,14 @@ button {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.topbar-left {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
max-width: calc(100vw - 96px);
|
||||
}
|
||||
|
||||
.workspace-tabs {
|
||||
overflow-x: auto;
|
||||
}
|
||||
@ -862,11 +1071,15 @@ button {
|
||||
}
|
||||
|
||||
.service-cards {
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-columns: minmax(220px, 86vw);
|
||||
}
|
||||
|
||||
.board-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-tools {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user