Docs, ticks, working app
This commit is contained in:
@@ -2,10 +2,31 @@ import { useState } from 'react'
|
||||
import type { Stats } from '../lib/api'
|
||||
import type { ColorScheme } from '../lib/api'
|
||||
import { useSettingsPanel } from '../lib/useSettingsPanel'
|
||||
import { DocsPanel } from './Docs'
|
||||
|
||||
type PanelName = 'settings' | 'presets' | null
|
||||
type PanelName = 'settings' | 'presets' | 'docs' | null
|
||||
|
||||
const COLOR_SCHEMES: ColorScheme[] = ['light', 'dark', 'system']
|
||||
const PANEL_TITLES: Record<Exclude<PanelName, null>, string> = {
|
||||
settings: 'Settings',
|
||||
presets: 'Presets',
|
||||
docs: 'Docs',
|
||||
}
|
||||
|
||||
/** Mirrors the backend's `profile::DEFAULT_PROFILE_NAME` — always exists, can't be deleted. */
|
||||
const DEFAULT_PROFILE_NAME = 'Default'
|
||||
|
||||
const COLOR_SCHEMES: ColorScheme[] = [
|
||||
'system',
|
||||
'light',
|
||||
'dark',
|
||||
'grey',
|
||||
'red',
|
||||
'green',
|
||||
'blue',
|
||||
'yellow',
|
||||
'pink',
|
||||
'orange',
|
||||
]
|
||||
|
||||
export function NavMenu({ stats }: { stats: Stats | null }) {
|
||||
const [openPanel, setOpenPanel] = useState<PanelName>(null)
|
||||
@@ -17,16 +38,16 @@ export function NavMenu({ stats }: { stats: Stats | null }) {
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<div className="flex h-full w-48 shrink-0 flex-col justify-between border-r border-neutral-800 p-3">
|
||||
<div className="flex h-full w-48 shrink-0 flex-col justify-between border-r border-border p-3">
|
||||
<div>
|
||||
<div className="mb-4 text-lg font-semibold text-neutral-100">ottyl</div>
|
||||
<nav className="flex flex-col gap-1 text-sm text-neutral-400">
|
||||
<span className="rounded bg-neutral-800 px-2 py-1 text-neutral-100">Dashboard</span>
|
||||
<div className="mb-4 text-lg font-semibold text-text">ottyl</div>
|
||||
<nav className="flex flex-col gap-1 text-sm text-text-muted">
|
||||
<span className="rounded bg-surface-2 px-2 py-1 text-text">Dashboard</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePanel('presets')}
|
||||
className={`rounded px-2 py-1 text-left hover:bg-neutral-800 ${
|
||||
openPanel === 'presets' ? 'bg-neutral-800 text-neutral-100' : ''
|
||||
className={`rounded px-2 py-1 text-left hover:bg-surface-2 ${
|
||||
openPanel === 'presets' ? 'bg-surface-2 text-text' : ''
|
||||
}`}
|
||||
>
|
||||
Presets
|
||||
@@ -34,15 +55,24 @@ export function NavMenu({ stats }: { stats: Stats | null }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePanel('settings')}
|
||||
className={`rounded px-2 py-1 text-left hover:bg-neutral-800 ${
|
||||
openPanel === 'settings' ? 'bg-neutral-800 text-neutral-100' : ''
|
||||
className={`rounded px-2 py-1 text-left hover:bg-surface-2 ${
|
||||
openPanel === 'settings' ? 'bg-surface-2 text-text' : ''
|
||||
}`}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => togglePanel('docs')}
|
||||
className={`rounded px-2 py-1 text-left hover:bg-surface-2 ${
|
||||
openPanel === 'docs' ? 'bg-surface-2 text-text' : ''
|
||||
}`}
|
||||
>
|
||||
Docs
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="text-xs text-neutral-500">
|
||||
<div className="text-xs text-text-subtle">
|
||||
{stats ? (
|
||||
<>
|
||||
<div>{stats.log_count} logs</div>
|
||||
@@ -57,26 +87,30 @@ export function NavMenu({ stats }: { stats: Stats | null }) {
|
||||
</div>
|
||||
|
||||
{openPanel && (
|
||||
<div className="fixed inset-y-0 left-48 z-20 flex w-80 flex-col overflow-y-auto border-r border-neutral-800 bg-neutral-900 p-4 shadow-xl">
|
||||
<div
|
||||
className={`fixed inset-y-0 left-48 z-20 flex flex-col overflow-y-auto border-r border-border bg-surface-1 p-4 shadow-xl ${
|
||||
openPanel === 'docs' ? 'w-[32rem]' : 'w-80'
|
||||
}`}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-neutral-100">
|
||||
{openPanel === 'settings' ? 'Settings' : 'Presets'}
|
||||
</h2>
|
||||
<h2 className="text-sm font-semibold text-text">{PANEL_TITLES[openPanel]}</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenPanel(null)}
|
||||
className="rounded px-1.5 text-neutral-500 hover:bg-neutral-800 hover:text-neutral-200"
|
||||
className="rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-text"
|
||||
aria-label="Close panel"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
{panel.error && (
|
||||
<div className="mb-3 rounded border border-red-900 bg-red-950/50 px-2 py-1 text-xs text-red-300">
|
||||
{panel.error && openPanel !== 'docs' && (
|
||||
<div className="mb-3 rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text-strong">
|
||||
{panel.error}
|
||||
</div>
|
||||
)}
|
||||
{openPanel === 'settings' ? <SettingsPanel panel={panel} /> : <PresetsPanel panel={panel} />}
|
||||
{openPanel === 'settings' && <SettingsPanel panel={panel} />}
|
||||
{openPanel === 'presets' && <PresetsPanel panel={panel} />}
|
||||
{openPanel === 'docs' && <DocsPanel />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -103,56 +137,53 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 text-sm text-neutral-300">
|
||||
<div className="flex flex-col gap-5 text-sm text-text-secondary">
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-500">Colour scheme</h3>
|
||||
<div className="flex gap-1">
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-text-subtle">Colour scheme</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{COLOR_SCHEMES.map((scheme) => (
|
||||
<button
|
||||
key={scheme}
|
||||
type="button"
|
||||
onClick={() => panel.setColorScheme(scheme)}
|
||||
className={`flex-1 rounded border px-2 py-1 text-xs capitalize ${
|
||||
className={`rounded border px-2 py-1 text-xs capitalize ${
|
||||
panel.settings?.color_scheme === scheme
|
||||
? 'border-neutral-400 bg-neutral-800 text-neutral-100'
|
||||
: 'border-neutral-700 text-neutral-400 hover:bg-neutral-800'
|
||||
? 'border-border-strong bg-surface-2 text-text'
|
||||
: 'border-border-soft text-text-muted hover:bg-surface-2'
|
||||
}`}
|
||||
>
|
||||
{scheme}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-neutral-600">
|
||||
"Light" only recolours the base page for now — most components still use fixed dark colours.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-500">Import CSS</h3>
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-text-subtle">Import CSS</h3>
|
||||
<form onSubmit={handleImport} className="flex flex-col gap-2">
|
||||
<input
|
||||
type="file"
|
||||
accept=".css,text/css"
|
||||
onChange={handleFileChange}
|
||||
className="text-xs text-neutral-400 file:mr-2 file:rounded file:border file:border-neutral-700 file:bg-neutral-800 file:px-2 file:py-1 file:text-neutral-200"
|
||||
className="text-xs text-text-muted file:mr-2 file:rounded file:border file:border-border-soft file:bg-surface-2 file:px-2 file:py-1 file:text-text"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="filename.css"
|
||||
value={filename}
|
||||
onChange={(e) => setFilename(e.target.value)}
|
||||
className="rounded border border-neutral-700 bg-neutral-900 px-2 py-1 text-xs text-neutral-100 placeholder:text-neutral-500 focus:border-neutral-500 focus:outline-none"
|
||||
className="rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Paste CSS content here"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
rows={4}
|
||||
className="resize-y rounded border border-neutral-700 bg-neutral-900 px-2 py-1 font-mono text-xs text-neutral-100 placeholder:text-neutral-500 focus:border-neutral-500 focus:outline-none"
|
||||
className="resize-y rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded border border-neutral-700 px-2 py-1 text-xs text-neutral-200 hover:bg-neutral-800"
|
||||
className="rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
@@ -160,31 +191,31 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-500">Configured CSS</h3>
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-text-subtle">Configured CSS</h3>
|
||||
{panel.cssEntries.length === 0 ? (
|
||||
<div className="text-xs text-neutral-600">No CSS configured.</div>
|
||||
<div className="text-xs text-text-faint">No CSS configured.</div>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-1">
|
||||
{panel.cssEntries.map((entry) => (
|
||||
<li
|
||||
key={entry.id}
|
||||
className="flex items-center justify-between rounded border border-neutral-800 px-2 py-1 text-xs"
|
||||
className="flex items-center justify-between rounded border border-border px-2 py-1 text-xs"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-neutral-200">{entry.filename ?? entry.path_or_url}</div>
|
||||
<div className="text-[10px] uppercase text-neutral-600">{entry.kind}</div>
|
||||
<div className="truncate text-text">{entry.filename ?? entry.path_or_url}</div>
|
||||
<div className="text-[10px] uppercase text-text-faint">{entry.kind}</div>
|
||||
</div>
|
||||
{entry.kind === 'imported' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.removeCssEntry(entry.id)}
|
||||
className="ml-2 shrink-0 rounded px-1.5 text-neutral-500 hover:bg-neutral-800 hover:text-red-400"
|
||||
className="ml-2 shrink-0 rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-danger-text"
|
||||
aria-label={`Delete ${entry.filename}`}
|
||||
>
|
||||
delete
|
||||
</button>
|
||||
) : (
|
||||
<span className="ml-2 shrink-0 text-neutral-700" title="Linked CSS isn't deletable here">
|
||||
<span className="ml-2 shrink-0 text-text-faintest" title="Linked CSS isn't deletable here">
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
@@ -208,25 +239,25 @@ function PresetsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 text-sm text-neutral-300">
|
||||
<div className="flex flex-col gap-4 text-sm text-text-secondary">
|
||||
<form onSubmit={handleCreate} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="new profile name"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
className="flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 py-1 text-xs text-neutral-100 placeholder:text-neutral-500 focus:border-neutral-500 focus:outline-none"
|
||||
className="flex-1 rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded border border-neutral-700 px-2 py-1 text-xs text-neutral-200 hover:bg-neutral-800"
|
||||
className="rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{panel.profiles.length === 0 ? (
|
||||
<div className="text-xs text-neutral-600">No profiles yet.</div>
|
||||
<div className="text-xs text-text-faint">No profiles yet.</div>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-1">
|
||||
{panel.profiles.map((profile) => {
|
||||
@@ -235,7 +266,7 @@ function PresetsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> })
|
||||
<li
|
||||
key={profile.name}
|
||||
className={`rounded border px-2 py-1.5 text-xs ${
|
||||
isActive ? 'border-neutral-500 bg-neutral-800' : 'border-neutral-800'
|
||||
isActive ? 'border-border-strong bg-surface-2' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -244,23 +275,29 @@ function PresetsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> })
|
||||
onClick={() => !isActive && panel.switchProfile(profile.name)}
|
||||
disabled={isActive}
|
||||
className={`truncate text-left font-medium ${
|
||||
isActive ? 'cursor-default text-neutral-100' : 'text-neutral-200 hover:underline'
|
||||
isActive ? 'cursor-default text-text' : 'text-text hover:underline'
|
||||
}`}
|
||||
title={isActive ? 'Active profile' : `Switch to ${profile.name}`}
|
||||
>
|
||||
{profile.name}
|
||||
{isActive && <span className="ml-1 text-[10px] uppercase text-neutral-400">active</span>}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.removeProfile(profile.name)}
|
||||
className="ml-2 shrink-0 rounded px-1.5 text-neutral-500 hover:bg-neutral-800 hover:text-red-400"
|
||||
aria-label={`Delete ${profile.name}`}
|
||||
>
|
||||
delete
|
||||
{isActive && <span className="ml-1 text-[10px] uppercase text-text-muted">active</span>}
|
||||
</button>
|
||||
{profile.name === DEFAULT_PROFILE_NAME ? (
|
||||
<span className="ml-2 shrink-0 text-text-faintest" title="The Default profile can't be deleted">
|
||||
—
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.removeProfile(profile.name)}
|
||||
className="ml-2 shrink-0 rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-danger-text"
|
||||
aria-label={`Delete ${profile.name}`}
|
||||
>
|
||||
delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] text-neutral-500">
|
||||
<div className="mt-1 text-[10px] text-text-subtle">
|
||||
{profile.param_count} params · {profile.metric_count} metrics · {profile.dashboard_count} dashboards
|
||||
</div>
|
||||
</li>
|
||||
@@ -268,7 +305,10 @@ function PresetsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> })
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
<p className="text-[11px] text-neutral-600">
|
||||
<p className="text-[11px] text-text-faint">
|
||||
A new profile starts as a copy of the currently active one (or empty, if none is active).
|
||||
</p>
|
||||
<p className="text-[11px] text-text-faint">
|
||||
Switching profiles hot-swaps the running engine's declared params/metrics immediately — no restart needed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user