feat(control-ui): modernize UX with premium Ultrabase design system
This commit is contained in:
@@ -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,54 +107,176 @@ 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
|
||||||
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)
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.to}
|
|
||||||
to={item.to}
|
to={item.to}
|
||||||
style={{
|
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',
|
textDecoration: 'none',
|
||||||
color: '#111',
|
fontSize: '14px',
|
||||||
padding: '6px 10px',
|
transition: 'all 0.2s ease',
|
||||||
borderRadius: 8,
|
}}
|
||||||
background: isActive ? '#eaeaea' : 'transparent',
|
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}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})}
|
}
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
return (
|
||||||
<header
|
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--bg-primary)' }}>
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside
|
||||||
style={{
|
style={{
|
||||||
borderBottom: '1px solid #eee',
|
width: 240,
|
||||||
padding: 16,
|
borderRight: '1px solid var(--border-primary)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
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
|
||||||
<div style={{ width: 420, maxWidth: '100%' }}>
|
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
|
<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) => {
|
||||||
@@ -133,52 +288,70 @@ export function Layout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
<Button
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const id = query.trim()
|
const id = query.trim()
|
||||||
if (!id) return
|
if (!id) return
|
||||||
openGrafanaLogs(id)
|
openGrafanaLogs(id)
|
||||||
}}
|
}}
|
||||||
disabled={!grafana.base}
|
disabled={!grafana.base}
|
||||||
|
icon={<Icons.Activity style={{ width: 14, height: 14 }} />}
|
||||||
>
|
>
|
||||||
Logs
|
Logs
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const id = query.trim()
|
const id = query.trim()
|
||||||
if (!id) return
|
if (!id) return
|
||||||
openGrafanaTrace(id)
|
openGrafanaTrace(id)
|
||||||
}}
|
}}
|
||||||
disabled={!grafana.base}
|
disabled={!grafana.base}
|
||||||
|
icon={<Icons.Topology style={{ width: 14, height: 14 }} />}
|
||||||
>
|
>
|
||||||
Trace
|
Trace
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{lastIds.correlationId ? (
|
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
{lastIds && (
|
||||||
<span style={{ fontSize: 12, color: '#666' }}>correlation_id</span>
|
<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>
|
<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>
|
||||||
|
|
||||||
|
<main style={{ padding: '32px', flex: 1 }}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<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
|
<input
|
||||||
id={props.id}
|
id={props.id}
|
||||||
aria-label={props.ariaLabel}
|
type={props.type ?? 'text'}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
onChange={(e) => props.onChange(e.target.value)}
|
onChange={(e) => props.onChange(e.target.value)}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
onKeyDown={props.onKeyDown}
|
onKeyDown={props.onKeyDown}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 10px',
|
padding: '8px 12px',
|
||||||
borderRadius: 8,
|
background: 'var(--bg-tertiary)',
|
||||||
border: `1px solid ${colors.border}`,
|
border: '1px solid var(--border-primary)',
|
||||||
|
borderRadius: 'var(--radius-md)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: '14px',
|
||||||
width: '100%',
|
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={{
|
||||||
<thead>
|
width: '100%',
|
||||||
<tr>
|
overflowX: 'auto',
|
||||||
{props.columns.map((c, idx) => (
|
border: '1px solid var(--border-primary)',
|
||||||
<th
|
borderRadius: 'var(--radius-lg)',
|
||||||
key={idx}
|
background: 'var(--bg-secondary)',
|
||||||
style={{ textAlign: 'left', padding: 8, borderBottom: `1px solid ${colors.borderSubtle}` }}
|
...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>
|
</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}
|
||||||
|
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}
|
{cell}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
99
control/ui/src/components/ui/Icons.tsx
Normal file
99
control/ui/src/components/ui/Icons.tsx
Normal 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>
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
--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;
|
|
||||||
|
|
||||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
--border-primary: #30363d;
|
||||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
--border-secondary: #3e444d;
|
||||||
--mono: ui-monospace, Consolas, monospace;
|
|
||||||
|
|
||||||
font: 18px/145% var(--sans);
|
--text-primary: #ededed;
|
||||||
letter-spacing: 0.18px;
|
--text-secondary: #a0a0a0;
|
||||||
color-scheme: light dark;
|
--text-muted: #6e7681;
|
||||||
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) {
|
--accent-primary: #3ecf8e;
|
||||||
font-size: 16px;
|
--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;
|
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user