feat(control-ui): modernize UX with premium Ultrabase design system
Some checks failed
ci / ui (push) Failing after 31s
images / build-and-push (push) Failing after 19s
ci / rust (push) Failing after 2m38s

This commit is contained in:
2026-03-30 21:57:33 +03:00
parent 020ebad570
commit beae85d2d2
4 changed files with 701 additions and 256 deletions

View File

@@ -1,28 +1,61 @@
import { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { Link, Outlet, useLocation } from 'react-router-dom' import { Link, Outlet, useLocation } from 'react-router-dom'
import { getLastRequestIds } from '../api/client' import { getLastRequestIds } from '../api/client'
import { Button, Code, TextInput } from '../components/primitives' import { Button, Code, TextInput } from '../components/primitives'
import { Icons } from '../components/ui/Icons'
type NavItem = { type NavItem = {
label: string label: string
to: string to: string
icon: React.ComponentType<any>
} }
const navItems: NavItem[] = [ type NavCategory = {
{ label: 'Overview', to: '/' }, label: string
{ label: 'Tenants', to: '/tenants' }, items: NavItem[]
{ label: 'Users', to: '/users' }, }
{ label: 'Sessions', to: '/sessions' },
{ label: 'Roles & Permissions', to: '/roles-permissions' }, const navCategories: NavCategory[] = [
{ label: 'Config', to: '/config' }, {
{ label: 'Definitions', to: '/definitions' }, label: 'Platform',
{ label: 'Documents', to: '/documents' }, items: [
{ label: 'Scale & Placement', to: '/scale-placement' }, { label: 'Overview', to: '/', icon: Icons.Activity },
{ label: 'Deployments', to: '/deployments' }, { label: 'Tenants', to: '/tenants', icon: Icons.Tenants },
{ label: 'Observability', to: '/observability' }, { label: 'Fleet Nodes', to: '/fleet', icon: Icons.Fleet },
{ label: 'Platform Drift', to: '/drift' }, { label: 'Observability', to: '/observability', icon: Icons.Observability },
{ label: 'Audit Log', to: '/audit-log' }, ],
{ label: 'Settings', to: '/settings' }, },
{
label: 'Infrastructure',
items: [
{ label: 'Scale & Placement', to: '/scale-placement', icon: Icons.Topology },
{ label: 'Deployments', to: '/deployments', icon: Icons.Deployments },
],
},
{
label: 'Resources',
items: [
{ label: 'Config Registry', to: '/config', icon: Icons.Config },
{ label: 'Type Definitions', to: '/definitions', icon: Icons.Definitions },
{ label: 'Documents', to: '/documents', icon: Icons.Storage },
],
},
{
label: 'Governance',
items: [
{ label: 'Audit Log', to: '/audit-log', icon: Icons.Audit },
{ label: 'Platform Drift', to: '/drift', icon: Icons.Drift },
],
},
{
label: 'Management',
items: [
{ label: 'Users', to: '/users', icon: Icons.Users },
{ label: 'Sessions', to: '/sessions', icon: Icons.Sessions },
{ label: 'Roles & Permissions', to: '/roles-permissions', icon: Icons.ShieldAlert },
{ label: 'Settings', to: '/settings', icon: Icons.Settings },
],
},
] ]
function normalizePath(pathname: string) { function normalizePath(pathname: string) {
@@ -33,7 +66,7 @@ function normalizePath(pathname: string) {
export function Layout() { export function Layout() {
const location = useLocation() const location = useLocation()
const active = normalizePath(location.pathname) const activePath = normalizePath(location.pathname)
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const lastIds = getLastRequestIds() const lastIds = getLastRequestIds()
@@ -74,111 +107,251 @@ export function Layout() {
} }
} }
return ( const NavLink = ({ item }: { item: NavItem }) => {
<div style={{ display: 'flex', minHeight: '100vh', fontFamily: 'system-ui, sans-serif' }}> const active = activePath === normalizePath(item.to)
<aside const Icon = item.icon
return (
<Link
to={item.to}
style={{ style={{
width: 260, display: 'flex',
borderRight: '1px solid #eee', alignItems: 'center',
padding: 16, gap: '12px',
background: '#fafafa', padding: '8px 12px',
borderRadius: 'var(--radius-md)',
color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
background: active ? 'var(--bg-tertiary)' : 'transparent',
fontWeight: active ? 500 : 400,
textDecoration: 'none',
fontSize: '14px',
transition: 'all 0.2s ease',
}}
onMouseOver={(e) => {
if (!active) e.currentTarget.style.background = 'rgba(255,255,255,0.05)'
}}
onMouseOut={(e) => {
if (!active) e.currentTarget.style.background = 'transparent'
}} }}
> >
<div style={{ fontWeight: 700, marginBottom: 16 }}>Cloudlysis Control</div> <span style={{ color: active ? 'var(--accent-primary)' : 'inherit', display: 'flex' }}>
<nav style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <Icon />
{navItems.map((item) => { </span>
const isActive = active === normalizePath(item.to) {item.label}
return ( </Link>
<Link )
key={item.to} }
to={item.to}
style={{
textDecoration: 'none',
color: '#111',
padding: '6px 10px',
borderRadius: 8,
background: isActive ? '#eaeaea' : 'transparent',
}}
>
{item.label}
</Link>
)
})}
</nav>
</aside>
<div style={{ flex: 1 }}> return (
<header <div style={{ display: 'flex', minHeight: '100vh', background: 'var(--bg-primary)' }}>
{/* Sidebar */}
<aside
style={{
width: 240,
borderRight: '1px solid var(--border-primary)',
display: 'flex',
flexDirection: 'column',
background: 'var(--bg-primary)',
position: 'sticky',
top: 0,
height: '100vh',
zIndex: 10,
}}
>
<div
style={{ style={{
borderBottom: '1px solid #eee', padding: '24px',
padding: 16, borderBottom: '1px solid var(--border-primary)',
display: 'flex', display: 'flex',
flexDirection: 'column', alignItems: 'center',
gap: 10, gap: '12px',
}} }}
> >
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}> <div
<div style={{ width: 420, maxWidth: '100%' }}> style={{
width: 32,
height: 32,
background: 'var(--accent-primary)',
borderRadius: 'var(--radius-md)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#000',
}}
>
<Icons.Zap />
</div>
<span style={{ fontWeight: 700, fontSize: '18px', letterSpacing: '-0.02em' }}>
Cloudlysis
</span>
</div>
<nav
style={{
flex: 1,
padding: '20px 12px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
overflowY: 'auto',
}}
>
{navCategories.map((cat) => (
<div key={cat.label} style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div
style={{
padding: '0 12px',
marginBottom: '4px',
fontSize: '11px',
fontWeight: 600,
color: 'var(--text-muted)',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
{cat.label}
</div>
{cat.items.map((item) => (
<NavLink key={item.to} item={item} />
))}
</div>
))}
</nav>
<div
style={{
padding: '16px',
borderTop: '1px solid var(--border-primary)',
background: 'var(--bg-secondary)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div
style={{
width: 32,
height: 32,
borderRadius: '50%',
background: 'var(--bg-tertiary)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: 600,
}}
>
OP
</div>
<div style={{ overflow: 'hidden' }}>
<div
style={{
fontSize: '13px',
fontWeight: 500,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
Operator
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)' }}>Administrator</div>
</div>
</div>
</div>
</aside>
{/* Main Content */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
<header
style={{
borderBottom: '1px solid var(--border-primary)',
padding: '16px 32px',
display: 'flex',
flexDirection: 'column',
gap: '16px',
background: 'var(--bg-primary)',
position: 'sticky',
top: 0,
zIndex: 5,
}}
>
<div style={{ display: 'flex', gap: '16px', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ width: 480, maxWidth: '100%' }}>
<TextInput <TextInput
ariaLabel="Global search" placeholder="Search correlation/trace id..."
placeholder="Search request/correlation/trace id"
value={query} value={query}
onChange={setQuery} onChange={setQuery}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
const id = query.trim()
if (!id) return
openGrafanaLogs(id)
}
}}
/>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
variant="secondary"
size="sm"
onClick={() => {
const id = query.trim() const id = query.trim()
if (!id) return if (!id) return
openGrafanaLogs(id) openGrafanaLogs(id)
} }}
}} disabled={!grafana.base}
/> icon={<Icons.Activity style={{ width: 14, height: 14 }} />}
>
Logs
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => {
const id = query.trim()
if (!id) return
openGrafanaTrace(id)
}}
disabled={!grafana.base}
icon={<Icons.Topology style={{ width: 14, height: 14 }} />}
>
Trace
</Button>
</div> </div>
<Button
onClick={() => {
const id = query.trim()
if (!id) return
openGrafanaLogs(id)
}}
disabled={!grafana.base}
>
Logs
</Button>
<Button
onClick={() => {
const id = query.trim()
if (!id) return
openGrafanaTrace(id)
}}
disabled={!grafana.base}
>
Trace
</Button>
</div> </div>
{lastIds ? ( {lastIds && (
<div style={{ display: 'flex', gap: 16, alignItems: 'center', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '24px', alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}> <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#666' }}>request_id</span> <span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>request_id</span>
<Code>{lastIds.requestId}</Code> <Code>{lastIds.requestId}</Code>
<Button onClick={() => copy(lastIds.requestId)}>Copy</Button> <Button variant="ghost" size="sm" onClick={() => copy(lastIds.requestId)} style={{ padding: '2px 4px' }}>
Copy
</Button>
</div> </div>
{lastIds.correlationId ? ( {lastIds.correlationId && (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}> <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#666' }}>correlation_id</span> <span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>correlation_id</span>
<Code>{lastIds.correlationId}</Code> <Code>{lastIds.correlationId}</Code>
<Button onClick={() => copy(lastIds.correlationId ?? '')}>Copy</Button> <Button variant="ghost" size="sm" onClick={() => copy(lastIds.correlationId || '')} style={{ padding: '2px 4px' }}>
Copy
</Button>
<Button <Button
onClick={() => openGrafanaLogs(lastIds.correlationId ?? '')} variant="primary"
size="sm"
onClick={() => openGrafanaLogs(lastIds.correlationId || '')}
disabled={!grafana.base} disabled={!grafana.base}
style={{ fontSize: '11px', padding: '2px 8px' }}
> >
Investigate Investigate
</Button> </Button>
</div> </div>
) : null} )}
</div> </div>
) : null} )}
</header> </header>
<Outlet />
<main style={{ padding: '32px', flex: 1 }}>
<Outlet />
</main>
</div> </div>
</div> </div>
) )

View File

@@ -1,43 +1,78 @@
import type { KeyboardEvent, ReactNode } from 'react' import React, { type KeyboardEvent, type ReactNode } from 'react'
const colors = { export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
border: '#ddd', variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
borderSubtle: '#eee', size?: 'sm' | 'md' | 'lg'
text: '#111', loading?: boolean
muted: '#666', icon?: ReactNode
danger: '#b00020',
bg: '#fff',
bgSubtle: '#fafafa',
bgActive: '#eaeaea',
} }
export function Button(props: { export const Button: React.FC<ButtonProps> = ({
children: ReactNode children,
onClick?: () => void variant = 'primary',
disabled?: boolean size = 'md',
variant?: 'default' | 'danger' loading,
type?: 'button' | 'submit' icon,
}) { style,
const variant = props.variant ?? 'default' ...props
const borderColor = variant === 'danger' ? colors.danger : colors.border }) => {
const textColor = variant === 'danger' ? colors.danger : colors.text const baseStyle: React.CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
borderRadius: 'var(--radius-md)',
fontWeight: 500,
cursor: props.disabled || loading ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease',
border: '1px solid transparent',
fontSize: size === 'sm' ? '13px' : size === 'lg' ? '16px' : '14px',
padding: size === 'sm' ? '4px 10px' : size === 'lg' ? '10px 20px' : '6px 14px',
opacity: props.disabled || loading ? 0.6 : 1,
...style,
}
const variants = {
primary: {
background: 'var(--accent-primary)',
color: '#000',
},
secondary: {
background: 'var(--bg-tertiary)',
color: 'var(--text-primary)',
borderColor: 'var(--border-primary)',
},
danger: {
background: 'var(--error-primary)',
color: '#fff',
},
ghost: {
background: 'transparent',
color: 'var(--text-secondary)',
},
}
const currentVariant = variants[variant as keyof typeof variants] || variants.primary
return ( return (
<button <button
type={props.type ?? 'button'} style={{ ...baseStyle, ...currentVariant }}
onClick={props.onClick} {...props}
disabled={props.disabled} onMouseOver={(e) => {
style={{ if (props.disabled || loading) return
padding: '8px 10px', if (variant === 'primary') e.currentTarget.style.background = 'var(--accent-secondary)'
borderRadius: 8, if (variant === 'secondary') e.currentTarget.style.borderColor = 'var(--border-secondary)'
border: `1px solid ${borderColor}`, if (variant === 'ghost') e.currentTarget.style.background = 'rgba(255,255,255,0.05)'
background: colors.bg, }}
color: textColor, onMouseOut={(e) => {
cursor: props.disabled ? 'not-allowed' : 'pointer', if (variant === 'primary') e.currentTarget.style.background = 'var(--accent-primary)'
opacity: props.disabled ? 0.6 : 1, if (variant === 'secondary') e.currentTarget.style.borderColor = 'var(--border-primary)'
if (variant === 'ghost') e.currentTarget.style.background = 'transparent'
}} }}
> >
{props.children} {icon}
{children}
{loading && <span style={{ marginLeft: 4 }}>...</span>}
</button> </button>
) )
} }
@@ -48,70 +83,237 @@ export function TextInput(props: {
onChange: (value: string) => void onChange: (value: string) => void
placeholder?: string placeholder?: string
ariaLabel?: string ariaLabel?: string
type?: string
onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void
}) { }) {
return ( return (
<input <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', width: '100%' }}>
id={props.id} {props.ariaLabel && (
aria-label={props.ariaLabel} <label style={{ fontSize: '12px', fontWeight: 500, color: 'var(--text-secondary)' }}>
value={props.value} {props.ariaLabel}
onChange={(e) => props.onChange(e.target.value)} </label>
placeholder={props.placeholder} )}
onKeyDown={props.onKeyDown} <input
style={{ id={props.id}
padding: '8px 10px', type={props.type ?? 'text'}
borderRadius: 8, value={props.value}
border: `1px solid ${colors.border}`, onChange={(e) => props.onChange(e.target.value)}
width: '100%', placeholder={props.placeholder}
}} onKeyDown={props.onKeyDown}
/> style={{
padding: '8px 12px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-primary)',
borderRadius: 'var(--radius-md)',
color: 'var(--text-primary)',
fontSize: '14px',
width: '100%',
outline: 'none',
transition: 'border-color 0.2s ease',
}}
onFocus={(e) => (e.currentTarget.style.borderColor = 'var(--accent-primary)')}
onBlur={(e) => (e.currentTarget.style.borderColor = 'var(--border-primary)')}
/>
</div>
) )
} }
export function Code(props: { children: ReactNode }) { export function Code(props: { children: ReactNode }) {
return <code style={{ fontSize: 12 }}>{props.children}</code> return (
<code
style={{
fontFamily: 'var(--font-mono, monospace)',
fontSize: '13px',
padding: '2px 6px',
background: 'var(--bg-tertiary)',
borderRadius: 'var(--radius-sm)',
color: 'var(--accent-primary)',
border: '1px solid var(--border-primary)',
}}
>
{props.children}
</code>
)
} }
export function ErrorText(props: { children: ReactNode }) { export function ErrorText(props: { children: ReactNode }) {
return <div style={{ color: colors.danger }}>{props.children}</div> return (
<div
style={{
color: 'var(--error-primary)',
fontSize: '13px',
marginTop: '4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
{props.children}
</div>
)
} }
export function MutedText(props: { children: ReactNode }) { export function MutedText(props: { children: ReactNode }) {
return <div style={{ fontSize: 12, color: colors.muted }}>{props.children}</div> return (
<div style={{ fontSize: '13px', color: 'var(--text-muted)', lineHeight: '1.5' }}>
{props.children}
</div>
)
} }
export function Table(props: { columns: ReactNode[]; rows: ReactNode[][] }) { export function Table(props: {
columns?: ReactNode[]
headers?: ReactNode[]
rows: ReactNode[][]
style?: React.CSSProperties
}) {
const headers = props.headers || props.columns || []
return ( return (
<div style={{ overflowX: 'auto' }}> <div
<table style={{ borderCollapse: 'collapse', width: '100%' }}> style={{
width: '100%',
overflowX: 'auto',
border: '1px solid var(--border-primary)',
borderRadius: 'var(--radius-lg)',
background: 'var(--bg-secondary)',
...props.style,
}}
>
<table
style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', fontSize: '14px' }}
>
<thead> <thead>
<tr> <tr style={{ borderBottom: '1px solid var(--border-primary)', background: 'var(--bg-tertiary)' }}>
{props.columns.map((c, idx) => ( {headers.map((h, i) => (
<th <th
key={idx} key={i}
style={{ textAlign: 'left', padding: 8, borderBottom: `1px solid ${colors.borderSubtle}` }} style={{
padding: '12px 16px',
fontWeight: 600,
color: 'var(--text-secondary)',
fontSize: '11px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
> >
{c} {h}
</th> </th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{props.rows.map((r, ridx) => ( {props.rows.length === 0 ? (
<tr key={ridx}> <tr>
{r.map((cell, cidx) => ( <td
<td key={cidx} style={{ padding: 8, borderBottom: `1px solid ${colors.bgActive}` }}> colSpan={headers.length}
{cell} style={{ padding: '48px 16px', textAlign: 'center', color: 'var(--text-muted)' }}
</td> >
))} No data available
</td>
</tr> </tr>
))} ) : (
props.rows.map((row, i) => (
<tr
key={i}
style={{
borderBottom: i === props.rows.length - 1 ? 'none' : '1px solid var(--border-primary)',
transition: 'background 0.2s ease',
}}
onMouseOver={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,0.02)')}
onMouseOut={(e) => (e.currentTarget.style.background = 'transparent')}
>
{row.map((cell, j) => (
<td key={j} style={{ padding: '12px 16px', color: 'var(--text-primary)' }}>
{cell}
</td>
))}
</tr>
))
)}
</tbody> </tbody>
</table> </table>
</div> </div>
) )
} }
export interface CardProps {
children: ReactNode
title?: string
subtitle?: string
footer?: ReactNode
headerAction?: ReactNode
style?: React.CSSProperties
bodyStyle?: React.CSSProperties
}
export const Card: React.FC<CardProps> = ({
children,
title,
subtitle,
footer,
headerAction,
style,
bodyStyle,
}) => {
return (
<div
style={{
background: 'var(--bg-secondary)',
border: '1px solid var(--border-primary)',
borderRadius: 'var(--radius-lg)',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
...style,
}}
>
{(title || subtitle || headerAction) && (
<div
style={{
padding: '16px 20px',
borderBottom: '1px solid var(--border-primary)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div>
{title && <h3 style={{ margin: 0, fontSize: '15px', fontWeight: 600 }}>{title}</h3>}
{subtitle && (
<p
style={{
margin: '4px 0 0 0',
fontSize: '13px',
color: 'var(--text-secondary)',
}}
>
{subtitle}
</p>
)}
</div>
{headerAction && <div>{headerAction}</div>}
</div>
)}
<div style={{ padding: '20px', flex: 1, ...bodyStyle }}>{children}</div>
{footer && (
<div
style={{
padding: '12px 20px',
background: 'var(--bg-tertiary)',
borderTop: '1px solid var(--border-primary)',
display: 'flex',
justifyContent: 'flex-end',
gap: '12px',
}}
>
{footer}
</div>
)}
</div>
)
}
export function Modal(props: { export function Modal(props: {
title: string title: string
open: boolean open: boolean
@@ -128,21 +330,25 @@ export function Modal(props: {
style={{ style={{
position: 'fixed', position: 'fixed',
inset: 0, inset: 0,
background: 'rgba(0,0,0,0.35)', background: 'rgba(0,0,0,0.6)',
backdropFilter: 'blur(4px)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: 24, padding: 24,
zIndex: 1000,
}} }}
onMouseDown={(e) => { onMouseDown={(e) => {
if (e.target === e.currentTarget) props.onClose() if (e.target === e.currentTarget) props.onClose()
}} }}
> >
<div style={{ background: colors.bg, borderRadius: 12, padding: 16, width: 520, maxWidth: '100%' }}> <Card
<div style={{ fontWeight: 700, marginBottom: 8 }}>{props.title}</div> title={props.title}
<div>{props.children}</div> footer={props.footer}
{props.footer ? <div style={{ marginTop: 16 }}>{props.footer}</div> : null} style={{ width: 520, maxWidth: '100%', boxShadow: 'var(--shadow-md)' }}
</div> >
{props.children}
</Card>
</div> </div>
) )
} }

View File

@@ -0,0 +1,99 @@
import React from "react";
type IconProps = React.SVGProps<SVGSVGElement>;
export const Icons = {
Tenants: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="2" y1="20" x2="22" y2="20"/></svg>
),
Topology: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
),
Fleet: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
),
Observability: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
),
Users: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
),
Sessions: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
),
Logout: (props: IconProps) => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
),
ChevronRight: (props: IconProps) => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><polyline points="9 18 15 12 9 6"/></svg>
),
Database: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
),
Mail: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
),
Storage: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
),
Functions: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
),
Realtime: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
),
Refresh: (props: IconProps) => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
),
Plus: (props: IconProps) => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
),
Trash: (props: IconProps) => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
),
AlertTriangle: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
),
Search: (props: IconProps) => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
),
Activity: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
),
ShieldAlert: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
),
ChevronLeft: (props: IconProps) => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><polyline points="15 18 9 12 15 6"/></svg>
),
Zap: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
),
Key: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
),
MoreVertical: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
),
ExternalLink: (props: IconProps) => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
),
Config: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.1a2 2 0 0 1-1-1.72v-.51a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
),
Definitions: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
),
Deployments: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4a2 2 0 0 0 1-1.73z"/><polyline points="7.5 4.21 12 6.81 16.5 4.21"/><polyline points="7.5 19.79 7.5 14.63 3 12"/><polyline points="21 12 16.5 14.63 16.5 19.79"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
),
Drift: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M4.5 16.5c-1.5 1.26-2 2.62-2 3.5s.5 1 1.5 1 2.5-1.5 4.5-3.5 4.5-3.5 6.5-1.5 1.5 1 1.5 1"/><path d="M10 5s.5 0 1.5 1.5.5 2.5-1.5 4.5-4.5 3.5-6.5 1.5-1.5-1-1.5-1"/><path d="M19.5 7.5c1.5-1.26 2-2.62 2-3.5s-.5-1-1.5-1-2.5 1.5-4.5 3.5-4.5 3.5-6.5 1.5-1.5-1-1.5-1"/></svg>
),
Audit: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
),
Settings: (props: IconProps) => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
),
};

View File

@@ -1,111 +1,78 @@
:root { :root {
--text: #6b6375; --bg-primary: #1c1c1c;
--text-h: #08060d; --bg-secondary: #242424;
--bg: #fff; --bg-tertiary: #2e2e2e;
--border: #e5e4e7;
--code-bg: #f4f3ec; --border-primary: #30363d;
--accent: #aa3bff; --border-secondary: #3e444d;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5); --text-primary: #ededed;
--social-bg: rgba(244, 243, 236, 0.5); --text-secondary: #a0a0a0;
--shadow: --text-muted: #6e7681;
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--accent-primary: #3ecf8e;
--accent-secondary: #34b27b;
--accent-muted: rgba(62, 207, 142, 0.1);
--error-primary: #ea4e4e;
--error-muted: rgba(234, 78, 78, 0.1);
--warning-primary: #f5a623;
--warning-muted: rgba(245, 166, 35, 0.1);
--sans: system-ui, 'Segoe UI', Roboto, sans-serif; --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif; --radius-sm: 4px;
--mono: ui-monospace, Consolas, monospace; --radius-md: 6px;
--radius-lg: 8px;
font: 18px/145% var(--sans);
letter-spacing: 0.18px; --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
color-scheme: light dark; --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
} }
@media (prefers-color-scheme: dark) { * {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
#root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
margin: 0; margin: 0;
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
h1, button, input, select, textarea {
h2 { font-family: inherit;
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
} }
h1 { a {
font-size: 56px; color: var(--accent-primary);
letter-spacing: -1.68px; text-decoration: none;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
} }
code, a:hover {
.counter { text-decoration: underline;
font-family: var(--mono); }
display: inline-flex;
/* Custom scrollbar for a more premium feel */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
background: var(--border-primary);
border-radius: 4px; border-radius: 4px;
color: var(--text-h); }
::-webkit-scrollbar-thumb:hover {
background: var(--border-secondary);
} }
code { #root {
font-size: 15px; min-height: 100svh;
line-height: 135%; display: flex;
padding: 4px 8px; flex-direction: column;
background: var(--code-bg);
} }