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 { getLastRequestIds } from '../api/client'
import { Button, Code, TextInput } from '../components/primitives'
import { Icons } from '../components/ui/Icons'
type NavItem = {
label: string
to: string
icon: React.ComponentType<any>
}
const navItems: NavItem[] = [
{ label: 'Overview', to: '/' },
{ label: 'Tenants', to: '/tenants' },
{ label: 'Users', to: '/users' },
{ label: 'Sessions', to: '/sessions' },
{ label: 'Roles & Permissions', to: '/roles-permissions' },
{ label: 'Config', to: '/config' },
{ label: 'Definitions', to: '/definitions' },
{ label: 'Documents', to: '/documents' },
{ label: 'Scale & Placement', to: '/scale-placement' },
{ label: 'Deployments', to: '/deployments' },
{ label: 'Observability', to: '/observability' },
{ label: 'Platform Drift', to: '/drift' },
{ label: 'Audit Log', to: '/audit-log' },
{ label: 'Settings', to: '/settings' },
type NavCategory = {
label: string
items: NavItem[]
}
const navCategories: NavCategory[] = [
{
label: 'Platform',
items: [
{ label: 'Overview', to: '/', icon: Icons.Activity },
{ label: 'Tenants', to: '/tenants', icon: Icons.Tenants },
{ label: 'Fleet Nodes', to: '/fleet', icon: Icons.Fleet },
{ label: 'Observability', to: '/observability', icon: Icons.Observability },
],
},
{
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) {
@@ -33,7 +66,7 @@ function normalizePath(pathname: string) {
export function Layout() {
const location = useLocation()
const active = normalizePath(location.pathname)
const activePath = normalizePath(location.pathname)
const [query, setQuery] = useState('')
const lastIds = getLastRequestIds()
@@ -74,54 +107,176 @@ export function Layout() {
}
}
return (
<div style={{ display: 'flex', minHeight: '100vh', fontFamily: 'system-ui, sans-serif' }}>
<aside
style={{
width: 260,
borderRight: '1px solid #eee',
padding: 16,
background: '#fafafa',
}}
>
<div style={{ fontWeight: 700, marginBottom: 16 }}>Cloudlysis Control</div>
<nav style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{navItems.map((item) => {
const isActive = active === normalizePath(item.to)
const NavLink = ({ item }: { item: NavItem }) => {
const active = activePath === normalizePath(item.to)
const Icon = item.icon
return (
<Link
key={item.to}
to={item.to}
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
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',
color: '#111',
padding: '6px 10px',
borderRadius: 8,
background: isActive ? '#eaeaea' : 'transparent',
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'
}}
>
<span style={{ color: active ? 'var(--accent-primary)' : 'inherit', display: 'flex' }}>
<Icon />
</span>
{item.label}
</Link>
)
})}
</nav>
</aside>
}
<div style={{ flex: 1 }}>
<header
return (
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--bg-primary)' }}>
{/* Sidebar */}
<aside
style={{
borderBottom: '1px solid #eee',
padding: 16,
width: 240,
borderRight: '1px solid var(--border-primary)',
display: 'flex',
flexDirection: 'column',
gap: 10,
background: 'var(--bg-primary)',
position: 'sticky',
top: 0,
height: '100vh',
zIndex: 10,
}}
>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<div style={{ width: 420, maxWidth: '100%' }}>
<div
style={{
padding: '24px',
borderBottom: '1px solid var(--border-primary)',
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<div
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
ariaLabel="Global search"
placeholder="Search request/correlation/trace id"
placeholder="Search correlation/trace id..."
value={query}
onChange={setQuery}
onKeyDown={(e) => {
@@ -133,52 +288,70 @@ export function Layout() {
}}
/>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<Button
variant="secondary"
size="sm"
onClick={() => {
const id = query.trim()
if (!id) return
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>
{lastIds ? (
<div style={{ display: 'flex', gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#666' }}>request_id</span>
<Code>{lastIds.requestId}</Code>
<Button onClick={() => copy(lastIds.requestId)}>Copy</Button>
</div>
{lastIds.correlationId ? (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ fontSize: 12, color: '#666' }}>correlation_id</span>
{lastIds && (
<div style={{ display: 'flex', gap: '24px', alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>request_id</span>
<Code>{lastIds.requestId}</Code>
<Button variant="ghost" size="sm" onClick={() => copy(lastIds.requestId)} style={{ padding: '2px 4px' }}>
Copy
</Button>
</div>
{lastIds.correlationId && (
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>correlation_id</span>
<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
onClick={() => openGrafanaLogs(lastIds.correlationId ?? '')}
variant="primary"
size="sm"
onClick={() => openGrafanaLogs(lastIds.correlationId || '')}
disabled={!grafana.base}
style={{ fontSize: '11px', padding: '2px 8px' }}
>
Investigate
</Button>
</div>
) : null}
)}
</div>
) : null}
)}
</header>
<main style={{ padding: '32px', flex: 1 }}>
<Outlet />
</main>
</div>
</div>
)

View File

@@ -1,43 +1,78 @@
import type { KeyboardEvent, ReactNode } from 'react'
import React, { type KeyboardEvent, type ReactNode } from 'react'
const colors = {
border: '#ddd',
borderSubtle: '#eee',
text: '#111',
muted: '#666',
danger: '#b00020',
bg: '#fff',
bgSubtle: '#fafafa',
bgActive: '#eaeaea',
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
size?: 'sm' | 'md' | 'lg'
loading?: boolean
icon?: ReactNode
}
export function Button(props: {
children: ReactNode
onClick?: () => void
disabled?: boolean
variant?: 'default' | 'danger'
type?: 'button' | 'submit'
}) {
const variant = props.variant ?? 'default'
const borderColor = variant === 'danger' ? colors.danger : colors.border
const textColor = variant === 'danger' ? colors.danger : colors.text
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
loading,
icon,
style,
...props
}) => {
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 (
<button
type={props.type ?? 'button'}
onClick={props.onClick}
disabled={props.disabled}
style={{
padding: '8px 10px',
borderRadius: 8,
border: `1px solid ${borderColor}`,
background: colors.bg,
color: textColor,
cursor: props.disabled ? 'not-allowed' : 'pointer',
opacity: props.disabled ? 0.6 : 1,
style={{ ...baseStyle, ...currentVariant }}
{...props}
onMouseOver={(e) => {
if (props.disabled || loading) return
if (variant === 'primary') e.currentTarget.style.background = 'var(--accent-secondary)'
if (variant === 'secondary') e.currentTarget.style.borderColor = 'var(--border-secondary)'
if (variant === 'ghost') e.currentTarget.style.background = 'rgba(255,255,255,0.05)'
}}
onMouseOut={(e) => {
if (variant === 'primary') e.currentTarget.style.background = 'var(--accent-primary)'
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>
)
}
@@ -48,70 +83,237 @@ export function TextInput(props: {
onChange: (value: string) => void
placeholder?: string
ariaLabel?: string
type?: string
onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void
}) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', width: '100%' }}>
{props.ariaLabel && (
<label style={{ fontSize: '12px', fontWeight: 500, color: 'var(--text-secondary)' }}>
{props.ariaLabel}
</label>
)}
<input
id={props.id}
aria-label={props.ariaLabel}
type={props.type ?? 'text'}
value={props.value}
onChange={(e) => props.onChange(e.target.value)}
placeholder={props.placeholder}
onKeyDown={props.onKeyDown}
style={{
padding: '8px 10px',
borderRadius: 8,
border: `1px solid ${colors.border}`,
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 }) {
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 }) {
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 }) {
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 (
<div style={{ overflowX: 'auto' }}>
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
<thead>
<tr>
{props.columns.map((c, idx) => (
<th
key={idx}
style={{ textAlign: 'left', padding: 8, borderBottom: `1px solid ${colors.borderSubtle}` }}
<div
style={{
width: '100%',
overflowX: 'auto',
border: '1px solid var(--border-primary)',
borderRadius: 'var(--radius-lg)',
background: 'var(--bg-secondary)',
...props.style,
}}
>
{c}
<table
style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', fontSize: '14px' }}
>
<thead>
<tr style={{ borderBottom: '1px solid var(--border-primary)', background: 'var(--bg-tertiary)' }}>
{headers.map((h, i) => (
<th
key={i}
style={{
padding: '12px 16px',
fontWeight: 600,
color: 'var(--text-secondary)',
fontSize: '11px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}
>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{props.rows.map((r, ridx) => (
<tr key={ridx}>
{r.map((cell, cidx) => (
<td key={cidx} style={{ padding: 8, borderBottom: `1px solid ${colors.bgActive}` }}>
{props.rows.length === 0 ? (
<tr>
<td
colSpan={headers.length}
style={{ padding: '48px 16px', textAlign: 'center', color: 'var(--text-muted)' }}
>
No data available
</td>
</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>
</table>
</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: {
title: string
open: boolean
@@ -128,21 +330,25 @@ export function Modal(props: {
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.35)',
background: 'rgba(0,0,0,0.6)',
backdropFilter: 'blur(4px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 24,
zIndex: 1000,
}}
onMouseDown={(e) => {
if (e.target === e.currentTarget) props.onClose()
}}
>
<div style={{ background: colors.bg, borderRadius: 12, padding: 16, width: 520, maxWidth: '100%' }}>
<div style={{ fontWeight: 700, marginBottom: 8 }}>{props.title}</div>
<div>{props.children}</div>
{props.footer ? <div style={{ marginTop: 16 }}>{props.footer}</div> : null}
</div>
<Card
title={props.title}
footer={props.footer}
style={{ width: 520, maxWidth: '100%', boxShadow: 'var(--shadow-md)' }}
>
{props.children}
</Card>
</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 {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--bg-primary: #1c1c1c;
--bg-secondary: #242424;
--bg-tertiary: #2e2e2e;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
--border-primary: #30363d;
--border-secondary: #3e444d;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--text-primary: #ededed;
--text-secondary: #a0a0a0;
--text-muted: #6e7681;
@media (max-width: 1024px) {
font-size: 16px;
}
--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);
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
@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;
}
body {
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,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
button, input, select, textarea {
font-family: inherit;
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
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;
a {
color: var(--accent-primary);
text-decoration: none;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
a:hover {
text-decoration: underline;
}
/* 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;
color: var(--text-h);
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-secondary);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
#root {
min-height: 100svh;
display: flex;
flex-direction: column;
}