Resolve ui stuff
This commit is contained in:
+18
-5
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||||
import { NavMenu } from './components/NavMenu'
|
import { NavMenu } from './components/NavMenu'
|
||||||
import { FilterBar } from './components/FilterBar'
|
import { FilterBar } from './components/FilterBar'
|
||||||
import { LogTimeline, computePresetRange, type TimeRange, type TimeRangePresetId } from './components/LogTimeline'
|
import { LogTimeline, computeBucketMs, computePresetRange, type TimeRange, type TimeRangePresetId } from './components/LogTimeline'
|
||||||
import { LogPreview } from './components/LogPreview'
|
import { LogPreview } from './components/LogPreview'
|
||||||
import { Params } from './components/Params'
|
import { Params } from './components/Params'
|
||||||
import { Metrics } from './components/Metrics'
|
import { Metrics } from './components/Metrics'
|
||||||
@@ -122,7 +122,13 @@ function StandaloneLog() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const [nextBuckets, nextLogs] = await Promise.all([
|
const [nextBuckets, nextLogs] = await Promise.all([
|
||||||
fetchTimeline({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, filter: filter || undefined }),
|
fetchTimeline({
|
||||||
|
startMs: effectiveRange?.startMs,
|
||||||
|
endMs: effectiveRange?.endMs,
|
||||||
|
// Only meaningful for a fixed window — 'all' has no span to derive a resolution from, so it keeps the server's default bucketing.
|
||||||
|
bucketMs: effectiveRange ? computeBucketMs(effectiveRange.endMs - effectiveRange.startMs) : undefined,
|
||||||
|
filter: filter || undefined,
|
||||||
|
}),
|
||||||
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
|
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
|
||||||
])
|
])
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
@@ -235,7 +241,13 @@ function FullApp() {
|
|||||||
try {
|
try {
|
||||||
const [nextStats, nextBuckets, nextLogs] = await Promise.all([
|
const [nextStats, nextBuckets, nextLogs] = await Promise.all([
|
||||||
fetchStats(),
|
fetchStats(),
|
||||||
fetchTimeline({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, filter: filter || undefined }),
|
fetchTimeline({
|
||||||
|
startMs: effectiveRange?.startMs,
|
||||||
|
endMs: effectiveRange?.endMs,
|
||||||
|
// Only meaningful for a fixed window — 'all' has no span to derive a resolution from, so it keeps the server's default bucketing.
|
||||||
|
bucketMs: effectiveRange ? computeBucketMs(effectiveRange.endMs - effectiveRange.startMs) : undefined,
|
||||||
|
filter: filter || undefined,
|
||||||
|
}),
|
||||||
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
|
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
|
||||||
])
|
])
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
@@ -265,7 +277,7 @@ function FullApp() {
|
|||||||
<DocsNavProvider>
|
<DocsNavProvider>
|
||||||
<div className="flex h-screen w-screen overflow-hidden">
|
<div className="flex h-screen w-screen overflow-hidden">
|
||||||
<NavMenu stats={stats} />
|
<NavMenu stats={stats} />
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col overflow-y-auto">
|
||||||
<FilterBar
|
<FilterBar
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={setFilter}
|
onChange={setFilter}
|
||||||
@@ -277,7 +289,8 @@ function FullApp() {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex min-h-0 flex-1 flex-col" hidden={logViewerCollapsed}>
|
{/* min-h-[...] keeps this from being squeezed to 0 by the sections below it in the shared scroll container — flex-1 alone lets a flex item shrink past its content, which let this section's absolutely-unclipped content spill over Params et al instead of the column just scrolling. */}
|
||||||
|
<div className="flex min-h-[22rem] flex-1 flex-col" hidden={logViewerCollapsed}>
|
||||||
<LogTimeline
|
<LogTimeline
|
||||||
buckets={buckets}
|
buckets={buckets}
|
||||||
range={range}
|
range={range}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type AlertRule,
|
type AlertRule,
|
||||||
type CompareOp,
|
type CompareOp,
|
||||||
} from '../lib/api'
|
} from '../lib/api'
|
||||||
|
import { sectionHeaderClassName, sectionHeaderLeftClassName } from './CollapsibleSection'
|
||||||
import { HelpIcon } from './HelpIcon'
|
import { HelpIcon } from './HelpIcon'
|
||||||
import { queryInputClassName } from './FilterBar'
|
import { queryInputClassName } from './FilterBar'
|
||||||
|
|
||||||
@@ -264,12 +265,14 @@ export function Alerts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border p-3 text-sm">
|
<div className="border-b border-border text-sm">
|
||||||
<div className="mb-2 flex items-center gap-1.5 text-xs text-text-subtle">
|
<div className={sectionHeaderClassName}>
|
||||||
|
<div className={sectionHeaderLeftClassName}>
|
||||||
Alerts
|
Alerts
|
||||||
<HelpIcon sectionId="alerts" label="Help: Alerts" />
|
<HelpIcon sectionId="alerts" label="Help: Alerts" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
</div>
|
||||||
|
<div className="flex gap-4 p-3">
|
||||||
<form onSubmit={handleSubmit} className="w-96 shrink-0 space-y-2">
|
<form onSubmit={handleSubmit} className="w-96 shrink-0 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-text-subtle">Name</label>
|
<label className="mb-1 block text-xs text-text-subtle">Name</label>
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import type { TimeRange } from './LogTimeline'
|
import type { TimeRange } from './LogTimeline'
|
||||||
|
|
||||||
|
/// Shared look for a primary section's title bar: shaded so it reads as a
|
||||||
|
/// distinct band rather than blending into the section body below it.
|
||||||
|
export const sectionHeaderClassName =
|
||||||
|
'flex items-center justify-between gap-2 border-b border-border-soft bg-surface-1 px-3 py-1.5'
|
||||||
|
|
||||||
|
/// Left-hand cluster of a section header: title text, help icon, collapse/pop-out controls.
|
||||||
|
export const sectionHeaderLeftClassName = 'flex items-center gap-1.5 text-sm font-medium text-text-secondary'
|
||||||
|
|
||||||
/// localStorage key holding a record of section id -> collapsed, mirroring LogPreview's VISIBLE_PARAM_COLUMNS_STORAGE_KEY pattern.
|
/// localStorage key holding a record of section id -> collapsed, mirroring LogPreview's VISIBLE_PARAM_COLUMNS_STORAGE_KEY pattern.
|
||||||
const COLLAPSED_SECTIONS_STORAGE_KEY = 'ottyl.collapsedSections'
|
const COLLAPSED_SECTIONS_STORAGE_KEY = 'ottyl.collapsedSections'
|
||||||
|
|
||||||
@@ -46,7 +54,7 @@ export function CollapseToggle({ collapsed, onToggle }: { collapsed: boolean; on
|
|||||||
title={collapsed ? 'Expand section' : 'Collapse section'}
|
title={collapsed ? 'Expand section' : 'Collapse section'}
|
||||||
aria-label={collapsed ? 'Expand section' : 'Collapse section'}
|
aria-label={collapsed ? 'Expand section' : 'Collapse section'}
|
||||||
aria-expanded={!collapsed}
|
aria-expanded={!collapsed}
|
||||||
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border-soft text-[10px] leading-none text-text-faint hover:border-border-strong hover:text-text-secondary"
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
|
||||||
>
|
>
|
||||||
{collapsed ? '▸' : '▾'}
|
{collapsed ? '▸' : '▾'}
|
||||||
</button>
|
</button>
|
||||||
@@ -73,7 +81,7 @@ export function PopOutButton({ section, range }: { section: PopOutSectionId; ran
|
|||||||
onClick={() => window.open(popOutUrl(section, range), '_blank')}
|
onClick={() => window.open(popOutUrl(section, range), '_blank')}
|
||||||
title="Open in new tab"
|
title="Open in new tab"
|
||||||
aria-label="Open in new tab"
|
aria-label="Open in new tab"
|
||||||
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border-soft text-[10px] leading-none text-text-faint hover:border-border-strong hover:text-text-secondary"
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
|
||||||
>
|
>
|
||||||
↗
|
↗
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ import {
|
|||||||
type Panel,
|
type Panel,
|
||||||
type PromqlSeries,
|
type PromqlSeries,
|
||||||
} from '../lib/api'
|
} from '../lib/api'
|
||||||
import { CollapseToggle, PopOutButton, useSectionCollapsed } from './CollapsibleSection'
|
import {
|
||||||
|
CollapseToggle,
|
||||||
|
PopOutButton,
|
||||||
|
sectionHeaderClassName,
|
||||||
|
sectionHeaderLeftClassName,
|
||||||
|
useSectionCollapsed,
|
||||||
|
} from './CollapsibleSection'
|
||||||
import { HelpIcon } from './HelpIcon'
|
import { HelpIcon } from './HelpIcon'
|
||||||
|
|
||||||
const CHART_TYPES: ChartType[] = ['line', 'bar', 'pie']
|
const CHART_TYPES: ChartType[] = ['line', 'bar', 'pie']
|
||||||
@@ -210,27 +216,40 @@ export function Dashboards({ range }: { range: TimeRange | null }) {
|
|||||||
|
|
||||||
if (!activeProfile) {
|
if (!activeProfile) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5 border-b border-border p-3 text-xs text-text-faint">
|
<div className="border-b border-border text-sm">
|
||||||
|
<div className={sectionHeaderClassName}>
|
||||||
|
<div className={sectionHeaderLeftClassName}>
|
||||||
|
Dashboards
|
||||||
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||||
<PopOutButton section="dashboards" range={range} />
|
<PopOutButton section="dashboards" range={range} />
|
||||||
Dashboards — activate a profile (see the settings menu) to create and save dashboards.
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 text-xs text-text-faint">
|
||||||
|
Activate a profile (see the settings menu) to create and save dashboards.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border p-3 text-sm">
|
<div className="border-b border-border text-sm">
|
||||||
<div className="mb-2 flex items-center gap-1.5 text-xs text-text-subtle">
|
<div className={sectionHeaderClassName}>
|
||||||
|
<div className={sectionHeaderLeftClassName}>
|
||||||
|
Dashboards — profile "{activeProfile}"
|
||||||
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||||
<PopOutButton section="dashboards" range={range} />
|
<PopOutButton section="dashboards" range={range} />
|
||||||
Dashboards — profile "{activeProfile}"
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && !collapsed && (
|
{error && !collapsed && (
|
||||||
<div className="mb-2 rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{error}</div>
|
<div className="mx-3 mt-3 rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{error}</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-4" hidden={collapsed}>
|
<div className="flex gap-4 p-3" hidden={collapsed}>
|
||||||
<div className="w-56 shrink-0 space-y-3">
|
<div className="w-56 shrink-0 space-y-3">
|
||||||
<form onSubmit={handleCreateDashboard} className="flex gap-1">
|
<form onSubmit={handleCreateDashboard} className="flex gap-1">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function HelpIcon({ sectionId, label }: { sectionId: string; label?: stri
|
|||||||
onClick={() => openSection(sectionId)}
|
onClick={() => openSection(sectionId)}
|
||||||
title="Open docs"
|
title="Open docs"
|
||||||
aria-label={label ?? 'Open docs for this section'}
|
aria-label={label ?? 'Open docs for this section'}
|
||||||
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border-soft text-[10px] leading-none text-text-faint hover:border-border-strong hover:text-text-secondary"
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
|
||||||
>
|
>
|
||||||
?
|
?
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { metricPrimitiveToDisplayString, type LogEntry, type ParamDeclaration } from '../lib/api'
|
import { metricPrimitiveToDisplayString, type LogEntry, type ParamDeclaration } from '../lib/api'
|
||||||
|
import { sectionHeaderClassName, sectionHeaderLeftClassName } from './CollapsibleSection'
|
||||||
import { HelpIcon } from './HelpIcon'
|
import { HelpIcon } from './HelpIcon'
|
||||||
|
|
||||||
// Namespaced under `ottyl.` so future per-viewer preferences elsewhere in the app won't collide with this key.
|
// Namespaced under `ottyl.` so future per-viewer preferences elsewhere in the app won't collide with this key.
|
||||||
@@ -102,8 +103,11 @@ export function LogPreview({ logs, params }: { logs: LogEntry[]; params: ParamDe
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
<div className="flex items-center justify-between border-b border-border-faint px-3 py-1">
|
<div className={sectionHeaderClassName}>
|
||||||
|
<div className={sectionHeaderLeftClassName}>
|
||||||
|
Log preview
|
||||||
<HelpIcon sectionId="log-preview" label="Help: Log preview" />
|
<HelpIcon sectionId="log-preview" label="Help: Log preview" />
|
||||||
|
</div>
|
||||||
<ColumnPicker paramNames={paramNames} visible={visible} onToggle={toggleColumn} />
|
<ColumnPicker paramNames={paramNames} visible={visible} onToggle={toggleColumn} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import type { TimelineBucket } from '../lib/api'
|
import type { TimelineBucket } from '../lib/api'
|
||||||
|
import { sectionHeaderClassName, sectionHeaderLeftClassName } from './CollapsibleSection'
|
||||||
import { HelpIcon } from './HelpIcon'
|
import { HelpIcon } from './HelpIcon'
|
||||||
|
|
||||||
export interface TimeRange {
|
export interface TimeRange {
|
||||||
@@ -26,6 +27,14 @@ export function computePresetRange(id: TimeRangePresetId): TimeRange | null {
|
|||||||
return { startMs: now - preset.ms, endMs: now }
|
return { startMs: now - preset.ms, endMs: now }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Buckets a fixed range into this many slots, so e.g. a rolling `5m` preset always requests a resolution fine enough to visibly slide instead of a handful of coarse, jumpy bars.
|
||||||
|
const TIMELINE_TARGET_BUCKETS = 60
|
||||||
|
|
||||||
|
/// Callers with an explicit (non-`all`) range pass this as `bucketMs` to `fetchTimeline` — `LogTimeline` derives the same value from its own `domainSpan` when laying out bars, so requested and rendered resolution always agree.
|
||||||
|
export function computeBucketMs(domainSpanMs: number): number {
|
||||||
|
return Math.max(1000, Math.round(domainSpanMs / TIMELINE_TARGET_BUCKETS))
|
||||||
|
}
|
||||||
|
|
||||||
function formatTimestamp(ms: number): string {
|
function formatTimestamp(ms: number): string {
|
||||||
return new Date(ms).toLocaleTimeString()
|
return new Date(ms).toLocaleTimeString()
|
||||||
}
|
}
|
||||||
@@ -52,6 +61,8 @@ export function LogTimeline({
|
|||||||
const domainStart = range?.startMs ?? buckets[0]?.start_ms ?? 0
|
const domainStart = range?.startMs ?? buckets[0]?.start_ms ?? 0
|
||||||
const domainEnd = range?.endMs ?? Date.now()
|
const domainEnd = range?.endMs ?? Date.now()
|
||||||
const domainSpan = Math.max(1, domainEnd - domainStart)
|
const domainSpan = Math.max(1, domainEnd - domainStart)
|
||||||
|
// Matches what the caller requested via `computeBucketMs` for a fixed range; with no range ('all') there's no such request, so fall back to the actual spacing between returned buckets.
|
||||||
|
const bucketMs = range ? computeBucketMs(domainSpan) : buckets.length >= 2 ? buckets[1].start_ms - buckets[0].start_ms : domainSpan
|
||||||
const firstBucket = buckets[0] ?? null
|
const firstBucket = buckets[0] ?? null
|
||||||
const lastBucket = buckets[buckets.length - 1] ?? null
|
const lastBucket = buckets[buckets.length - 1] ?? null
|
||||||
|
|
||||||
@@ -103,13 +114,13 @@ export function LogTimeline({
|
|||||||
const containerLeft = containerRef.current?.getBoundingClientRect().left ?? 0
|
const containerLeft = containerRef.current?.getBoundingClientRect().left ?? 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border p-3">
|
<div className="border-b border-border">
|
||||||
<div className="mb-1 flex items-center justify-between text-xs text-text-subtle">
|
<div className={sectionHeaderClassName}>
|
||||||
<span className="flex items-center gap-1.5">
|
<span className={sectionHeaderLeftClassName}>
|
||||||
Timeline
|
Timeline
|
||||||
<HelpIcon sectionId="timeline" label="Help: Timeline" />
|
<HelpIcon sectionId="timeline" label="Help: Timeline" />
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5 text-xs text-text-subtle">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{TIME_RANGE_PRESETS.map((preset) => (
|
{TIME_RANGE_PRESETS.map((preset) => (
|
||||||
<button
|
<button
|
||||||
@@ -134,9 +145,10 @@ export function LogTimeline({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-3">
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="relative h-24 w-full cursor-crosshair select-none rounded bg-surface-1"
|
className="relative h-24 w-full cursor-crosshair select-none overflow-hidden rounded bg-surface-1"
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
@@ -146,12 +158,17 @@ export function LogTimeline({
|
|||||||
setHoverX(null)
|
setHoverX(null)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full items-end gap-px overflow-hidden px-1">
|
{/* Positioned by actual elapsed time within the domain (not evenly spread via flex), so a rolling preset like `5m` visibly slides — a bucket's left/width tracks its real place in the window instead of jumping to a fixed slot as buckets enter/leave it. */}
|
||||||
|
<div className="absolute inset-x-1 bottom-0 top-0">
|
||||||
{buckets.map((b) => (
|
{buckets.map((b) => (
|
||||||
<div
|
<div
|
||||||
key={b.start_ms}
|
key={b.start_ms}
|
||||||
className="min-w-[2px] flex-1 rounded-t bg-accent/70"
|
className="absolute bottom-0 min-w-[2px] rounded-t bg-accent/70"
|
||||||
style={{ height: `${(b.count / maxCount) * 100}%` }}
|
style={{
|
||||||
|
left: `${((b.start_ms - domainStart) / domainSpan) * 100}%`,
|
||||||
|
width: `${(bucketMs / domainSpan) * 100}%`,
|
||||||
|
height: `${(b.count / maxCount) * 100}%`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -189,5 +206,6 @@ export function LogTimeline({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
type MetricDeclaration,
|
type MetricDeclaration,
|
||||||
type MetricParamMatcher,
|
type MetricParamMatcher,
|
||||||
} from '../lib/api'
|
} from '../lib/api'
|
||||||
import { CollapseToggle, PopOutButton, useSectionCollapsed } from './CollapsibleSection'
|
import { CollapseToggle, PopOutButton, sectionHeaderClassName, sectionHeaderLeftClassName, useSectionCollapsed } from './CollapsibleSection'
|
||||||
import { HelpIcon } from './HelpIcon'
|
import { HelpIcon } from './HelpIcon'
|
||||||
|
|
||||||
const EMPTY_MATCHER: MetricParamMatcher = { param_name: '', capture_group: '', kind: 'string' }
|
const EMPTY_MATCHER: MetricParamMatcher = { param_name: '', capture_group: '', kind: 'string' }
|
||||||
@@ -150,14 +150,18 @@ export function Metrics({ range }: { range: TimeRange | null }) {
|
|||||||
const groups = groupByLabels(bucketValues)
|
const groups = groupByLabels(bucketValues)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border p-3 text-sm">
|
<div className="border-b border-border text-sm">
|
||||||
<div className="mb-2 flex items-center gap-1.5 text-xs text-text-subtle">
|
<div className={sectionHeaderClassName}>
|
||||||
|
<div className={sectionHeaderLeftClassName}>
|
||||||
Metrics
|
Metrics
|
||||||
<HelpIcon sectionId="metrics" label="Help: Metrics" />
|
<HelpIcon sectionId="metrics" label="Help: Metrics" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||||
<PopOutButton section="metrics" range={range} />
|
<PopOutButton section="metrics" range={range} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4" hidden={collapsed}>
|
</div>
|
||||||
|
<div className="flex gap-4 p-3" hidden={collapsed}>
|
||||||
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
|
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-text-subtle">Name</label>
|
<label className="mb-1 block text-xs text-text-subtle">Name</label>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ApiError, declareParam, type ParamDeclaration } from '../lib/api'
|
import { ApiError, declareParam, type ParamDeclaration } from '../lib/api'
|
||||||
import { CollapseToggle, PopOutButton, useSectionCollapsed } from './CollapsibleSection'
|
import { CollapseToggle, PopOutButton, sectionHeaderClassName, sectionHeaderLeftClassName, useSectionCollapsed } from './CollapsibleSection'
|
||||||
import { HelpIcon } from './HelpIcon'
|
import { HelpIcon } from './HelpIcon'
|
||||||
|
|
||||||
const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
|
const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
|
||||||
@@ -33,14 +33,18 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border p-3 text-sm">
|
<div className="border-b border-border text-sm">
|
||||||
<div className="mb-2 flex items-center gap-1.5 text-xs text-text-subtle">
|
<div className={sectionHeaderClassName}>
|
||||||
|
<div className={sectionHeaderLeftClassName}>
|
||||||
Params
|
Params
|
||||||
<HelpIcon sectionId="parameters" label="Help: Params" />
|
<HelpIcon sectionId="parameters" label="Help: Params" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
||||||
<PopOutButton section="params" />
|
<PopOutButton section="params" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4" hidden={collapsed}>
|
</div>
|
||||||
|
<div className="flex gap-4 p-3" hidden={collapsed}>
|
||||||
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
|
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-text-subtle">Name</label>
|
<label className="mb-1 block text-xs text-text-subtle">Name</label>
|
||||||
|
|||||||
+128
-112
@@ -1,5 +1,12 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Author-origin display utilities (flex, grid, ...) beat the UA [hidden]
|
||||||
|
* rule regardless of specificity, so collapsible sections that pair
|
||||||
|
* `hidden` with a display utility never actually hide. Force it back. */
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Theme system: every visual role the app uses (surfaces, borders, text,
|
* Theme system: every visual role the app uses (surfaces, borders, text,
|
||||||
* danger banners, accent highlights) is a plain CSS custom property here,
|
* danger banners, accent highlights) is a plain CSS custom property here,
|
||||||
@@ -9,16 +16,19 @@
|
|||||||
* so re-declaring the `--c-*` variables under a `html.<theme>` selector
|
* so re-declaring the `--c-*` variables under a `html.<theme>` selector
|
||||||
* re-skins the whole app with no per-component changes.
|
* re-skins the whole app with no per-component changes.
|
||||||
*
|
*
|
||||||
* Derivation: surfaces/borders/text are always neutral (S=0) greys, never
|
* Derivation: `dark`/`grey` share one true black-based neutral (S=0) scale,
|
||||||
* hue-tinted. `dark`/`grey` share one true black-based grey scale, `light`
|
* `light` is its exact lightness mirror — these three stay grey throughout.
|
||||||
* is its exact lightness mirror. Each named theme (red/green/blue/yellow/
|
* Each named theme (red/green/blue/yellow/pink/orange) instead tints its
|
||||||
* pink/orange) takes its hue's real Tailwind swatch at each scale step,
|
* whole surface/border/text scale in its own hue at low saturation (so the
|
||||||
* converts it to HSL, zeroes the saturation, and keeps that lightness as
|
* chrome itself reads as "a shade of red", not grey-with-a-red-light), and
|
||||||
* its grey — so e.g. red's surface-0 is grey at red-950's lightness, not
|
* pulls its accent trio from two *other* hues on the wheel: --c-accent and
|
||||||
* neutral-950. Colour survives only on --c-accent*. To add a new themed
|
* --c-accent-text share the base hue's complement (base+180°), while
|
||||||
* variant: pick its hue's Tailwind swatches at 950/900/800/700/600/500/
|
* --c-accent-soft sits at a split-complement (~base+150°/-150°) — a
|
||||||
* 400/300/100, desaturate each (HSL S->0), and slot the greys into the
|
* "composite" accent distinct from the straight complement. To add a new
|
||||||
* same --c-surface-*/--c-border-*/--c-text-* roles used below.
|
* themed variant: pick a base hue H, a complement C = H+180, and a
|
||||||
|
* composite K = H+150 (mod 360), then fill --c-surface-, --c-border-,
|
||||||
|
* --c-text- as hsl(H, ...) at the saturation/lightness steps below, accent
|
||||||
|
* + accent-text as hsl(C, ...), and accent-soft as hsl(K, ...).
|
||||||
*/
|
*/
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
@@ -102,142 +112,148 @@ html.grey {
|
|||||||
--c-accent-text: #7dd3fc;
|
--c-accent-text: #7dd3fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* red-950..300/100 desaturated (S->0), lightness kept */
|
/* base hue 0 (red): surfaces/borders/text tinted red; accent complement 180
|
||||||
|
(cyan), accent-soft composite 150 (spring green) */
|
||||||
html.red {
|
html.red {
|
||||||
--c-surface-0: #282828;
|
--c-surface-0: hsl(0 32% 10%);
|
||||||
--c-surface-1: #4e4e4e;
|
--c-surface-1: hsl(0 28% 16%);
|
||||||
--c-surface-2: #5a5a5a;
|
--c-surface-2: hsl(0 26% 22%);
|
||||||
|
|
||||||
--c-border: #5a5a5a;
|
--c-border: hsl(0 24% 24%);
|
||||||
--c-border-soft: #6a6a6a;
|
--c-border-soft: hsl(0 22% 33%);
|
||||||
--c-border-faint: #4e4e4e;
|
--c-border-faint: hsl(0 28% 16%);
|
||||||
--c-border-strong: #b4b4b4;
|
--c-border-strong: hsl(0 15% 62%);
|
||||||
|
|
||||||
--c-text: #f0f0f0;
|
--c-text: hsl(0 15% 94%);
|
||||||
--c-text-secondary: #d0d0d0;
|
--c-text-secondary: hsl(0 12% 82%);
|
||||||
--c-text-muted: #b4b4b4;
|
--c-text-muted: hsl(0 10% 65%);
|
||||||
--c-text-subtle: #9a9a9a;
|
--c-text-subtle: hsl(0 8% 50%);
|
||||||
--c-text-faint: #818181;
|
--c-text-faint: hsl(0 8% 38%);
|
||||||
--c-text-faintest: #6a6a6a;
|
--c-text-faintest: hsl(0 10% 28%);
|
||||||
|
|
||||||
--c-accent: #f87171;
|
--c-accent: hsl(180 70% 62%);
|
||||||
--c-accent-soft: #b91c1c;
|
--c-accent-soft: hsl(150 55% 34%);
|
||||||
--c-accent-text: #fca5a5;
|
--c-accent-text: hsl(180 75% 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* green-950..300/100 desaturated (S->0), lightness kept */
|
/* base hue 140 (green): surfaces/borders/text tinted green; accent complement
|
||||||
|
320 (magenta), accent-soft composite 290 (purple) */
|
||||||
html.green {
|
html.green {
|
||||||
--c-surface-0: #1a1a1a;
|
--c-surface-0: hsl(140 32% 10%);
|
||||||
--c-surface-1: #343434;
|
--c-surface-1: hsl(140 28% 16%);
|
||||||
--c-surface-2: #3d3d3d;
|
--c-surface-2: hsl(140 26% 22%);
|
||||||
|
|
||||||
--c-border: #3d3d3d;
|
--c-border: hsl(140 24% 24%);
|
||||||
--c-border-soft: #4a4a4a;
|
--c-border-soft: hsl(140 22% 33%);
|
||||||
--c-border-faint: #343434;
|
--c-border-faint: hsl(140 28% 16%);
|
||||||
--c-border-strong: #949494;
|
--c-border-strong: hsl(140 15% 62%);
|
||||||
|
|
||||||
--c-text: #ececec;
|
--c-text: hsl(140 15% 94%);
|
||||||
--c-text-secondary: #bbbbbb;
|
--c-text-secondary: hsl(140 12% 82%);
|
||||||
--c-text-muted: #949494;
|
--c-text-muted: hsl(140 10% 65%);
|
||||||
--c-text-subtle: #747474;
|
--c-text-subtle: hsl(140 8% 50%);
|
||||||
--c-text-faint: #5c5c5c;
|
--c-text-faint: hsl(140 8% 38%);
|
||||||
--c-text-faintest: #4a4a4a;
|
--c-text-faintest: hsl(140 10% 28%);
|
||||||
|
|
||||||
--c-accent: #4ade80;
|
--c-accent: hsl(320 70% 62%);
|
||||||
--c-accent-soft: #15803d;
|
--c-accent-soft: hsl(290 55% 34%);
|
||||||
--c-accent-text: #86efac;
|
--c-accent-text: hsl(320 75% 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* blue-950..300/100 desaturated (S->0), lightness kept */
|
/* base hue 210 (blue): surfaces/borders/text tinted blue; accent complement
|
||||||
|
30 (orange), accent-soft composite 0 (red) */
|
||||||
html.blue {
|
html.blue {
|
||||||
--c-surface-0: #363636;
|
--c-surface-0: hsl(210 32% 10%);
|
||||||
--c-surface-1: #545454;
|
--c-surface-1: hsl(210 28% 16%);
|
||||||
--c-surface-2: #666666;
|
--c-surface-2: hsl(210 26% 22%);
|
||||||
|
|
||||||
--c-border: #666666;
|
--c-border: hsl(210 24% 24%);
|
||||||
--c-border-soft: #7a7a7a;
|
--c-border-soft: hsl(210 22% 33%);
|
||||||
--c-border-faint: #545454;
|
--c-border-faint: hsl(210 28% 16%);
|
||||||
--c-border-strong: #adadad;
|
--c-border-strong: hsl(210 15% 62%);
|
||||||
|
|
||||||
--c-text: #ececec;
|
--c-text: hsl(210 15% 94%);
|
||||||
--c-text-secondary: #c8c8c8;
|
--c-text-secondary: hsl(210 12% 82%);
|
||||||
--c-text-muted: #adadad;
|
--c-text-muted: hsl(210 10% 65%);
|
||||||
--c-text-subtle: #989898;
|
--c-text-subtle: hsl(210 8% 50%);
|
||||||
--c-text-faint: #888888;
|
--c-text-faint: hsl(210 8% 38%);
|
||||||
--c-text-faintest: #7a7a7a;
|
--c-text-faintest: hsl(210 10% 28%);
|
||||||
|
|
||||||
--c-accent: #60a5fa;
|
--c-accent: hsl(30 70% 62%);
|
||||||
--c-accent-soft: #1d4ed8;
|
--c-accent-soft: hsl(0 55% 34%);
|
||||||
--c-accent-text: #93c5fd;
|
--c-accent-text: hsl(30 75% 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* yellow-950..300/100 desaturated (S->0), lightness kept */
|
/* base hue 50 (yellow): surfaces/borders/text tinted yellow; accent
|
||||||
|
complement 230 (blue), accent-soft composite 200 (azure) */
|
||||||
html.yellow {
|
html.yellow {
|
||||||
--c-surface-0: #242424;
|
--c-surface-0: hsl(50 32% 10%);
|
||||||
--c-surface-1: #424242;
|
--c-surface-1: hsl(50 28% 16%);
|
||||||
--c-surface-2: #4a4a4a;
|
--c-surface-2: hsl(50 26% 22%);
|
||||||
|
|
||||||
--c-border: #4a4a4a;
|
--c-border: hsl(50 24% 24%);
|
||||||
--c-border-soft: #545454;
|
--c-border-soft: hsl(50 22% 33%);
|
||||||
--c-border-faint: #424242;
|
--c-border-faint: hsl(50 28% 16%);
|
||||||
--c-border-strong: #888888;
|
--c-border-strong: hsl(50 15% 62%);
|
||||||
|
|
||||||
--c-text: #e0e0e0;
|
--c-text: hsl(50 15% 94%);
|
||||||
--c-text-secondary: #a2a2a2;
|
--c-text-secondary: hsl(50 12% 82%);
|
||||||
--c-text-muted: #888888;
|
--c-text-muted: hsl(50 10% 65%);
|
||||||
--c-text-subtle: #797979;
|
--c-text-subtle: hsl(50 8% 50%);
|
||||||
--c-text-faint: #676767;
|
--c-text-faint: hsl(50 8% 38%);
|
||||||
--c-text-faintest: #545454;
|
--c-text-faintest: hsl(50 10% 28%);
|
||||||
|
|
||||||
--c-accent: #facc15;
|
--c-accent: hsl(230 70% 62%);
|
||||||
--c-accent-soft: #a16207;
|
--c-accent-soft: hsl(200 55% 34%);
|
||||||
--c-accent-text: #fde047;
|
--c-accent-text: hsl(230 75% 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* pink-950..300/100 desaturated (S->0), lightness kept */
|
/* base hue 330 (pink): surfaces/borders/text tinted pink; accent complement
|
||||||
|
150 (spring green), accent-soft composite 120 (green) */
|
||||||
html.pink {
|
html.pink {
|
||||||
--c-surface-0: #2c2c2c;
|
--c-surface-0: hsl(330 32% 10%);
|
||||||
--c-surface-1: #4e4e4e;
|
--c-surface-1: hsl(330 28% 16%);
|
||||||
--c-surface-2: #5a5a5a;
|
--c-surface-2: hsl(330 26% 22%);
|
||||||
|
|
||||||
--c-border: #5a5a5a;
|
--c-border: hsl(330 24% 24%);
|
||||||
--c-border-soft: #6b6b6b;
|
--c-border-soft: hsl(330 22% 33%);
|
||||||
--c-border-faint: #4e4e4e;
|
--c-border-faint: hsl(330 28% 16%);
|
||||||
--c-border-strong: #b3b3b3;
|
--c-border-strong: hsl(330 15% 62%);
|
||||||
|
|
||||||
--c-text: #f2f2f2;
|
--c-text: hsl(330 15% 94%);
|
||||||
--c-text-secondary: #d0d0d0;
|
--c-text-secondary: hsl(330 12% 82%);
|
||||||
--c-text-muted: #b3b3b3;
|
--c-text-muted: hsl(330 10% 65%);
|
||||||
--c-text-subtle: #9a9a9a;
|
--c-text-subtle: hsl(330 8% 50%);
|
||||||
--c-text-faint: #818181;
|
--c-text-faint: hsl(330 8% 38%);
|
||||||
--c-text-faintest: #6b6b6b;
|
--c-text-faintest: hsl(330 10% 28%);
|
||||||
|
|
||||||
--c-accent: #f472b6;
|
--c-accent: hsl(150 70% 62%);
|
||||||
--c-accent-soft: #be185d;
|
--c-accent-soft: hsl(120 55% 34%);
|
||||||
--c-accent-text: #f9a8d4;
|
--c-accent-text: hsl(150 75% 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* orange-950..300/100 desaturated (S->0), lightness kept */
|
/* base hue 30 (orange): surfaces/borders/text tinted orange; accent
|
||||||
|
complement 210 (blue), accent-soft composite 180 (cyan) */
|
||||||
html.orange {
|
html.orange {
|
||||||
--c-surface-0: #252525;
|
--c-surface-0: hsl(30 32% 10%);
|
||||||
--c-surface-1: #474747;
|
--c-surface-1: hsl(30 28% 16%);
|
||||||
--c-surface-2: #565656;
|
--c-surface-2: hsl(30 26% 22%);
|
||||||
|
|
||||||
--c-border: #565656;
|
--c-border: hsl(30 24% 24%);
|
||||||
--c-border-soft: #676767;
|
--c-border-soft: hsl(30 22% 33%);
|
||||||
--c-border-faint: #474747;
|
--c-border-faint: hsl(30 28% 16%);
|
||||||
--c-border-strong: #9b9b9b;
|
--c-border-strong: hsl(30 15% 62%);
|
||||||
|
|
||||||
--c-text: #eaeaea;
|
--c-text: hsl(30 15% 94%);
|
||||||
--c-text-secondary: #b8b8b8;
|
--c-text-secondary: hsl(30 12% 82%);
|
||||||
--c-text-muted: #9b9b9b;
|
--c-text-muted: hsl(30 10% 65%);
|
||||||
--c-text-subtle: #888888;
|
--c-text-subtle: hsl(30 8% 50%);
|
||||||
--c-text-faint: #7b7b7b;
|
--c-text-faint: hsl(30 8% 38%);
|
||||||
--c-text-faintest: #676767;
|
--c-text-faintest: hsl(30 10% 28%);
|
||||||
|
|
||||||
--c-accent: #fb923c;
|
--c-accent: hsl(210 70% 62%);
|
||||||
--c-accent-soft: #c2410c;
|
--c-accent-soft: hsl(180 55% 34%);
|
||||||
--c-accent-text: #fdba74;
|
--c-accent-text: hsl(210 75% 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|||||||
+96
-96
@@ -95,136 +95,136 @@ export const BUILTIN_THEMES: Record<string, ThemeVars> = {
|
|||||||
'--c-accent-text': '#7dd3fc',
|
'--c-accent-text': '#7dd3fc',
|
||||||
},
|
},
|
||||||
red: {
|
red: {
|
||||||
'--c-surface-0': '#282828',
|
'--c-surface-0': 'hsl(0 32% 10%)',
|
||||||
'--c-surface-1': '#4e4e4e',
|
'--c-surface-1': 'hsl(0 28% 16%)',
|
||||||
'--c-surface-2': '#5a5a5a',
|
'--c-surface-2': 'hsl(0 26% 22%)',
|
||||||
'--c-border': '#5a5a5a',
|
'--c-border': 'hsl(0 24% 24%)',
|
||||||
'--c-border-soft': '#6a6a6a',
|
'--c-border-soft': 'hsl(0 22% 33%)',
|
||||||
'--c-border-faint': '#4e4e4e',
|
'--c-border-faint': 'hsl(0 28% 16%)',
|
||||||
'--c-border-strong': '#b4b4b4',
|
'--c-border-strong': 'hsl(0 15% 62%)',
|
||||||
'--c-text': '#f0f0f0',
|
'--c-text': 'hsl(0 15% 94%)',
|
||||||
'--c-text-secondary': '#d0d0d0',
|
'--c-text-secondary': 'hsl(0 12% 82%)',
|
||||||
'--c-text-muted': '#b4b4b4',
|
'--c-text-muted': 'hsl(0 10% 65%)',
|
||||||
'--c-text-subtle': '#9a9a9a',
|
'--c-text-subtle': 'hsl(0 8% 50%)',
|
||||||
'--c-text-faint': '#818181',
|
'--c-text-faint': 'hsl(0 8% 38%)',
|
||||||
'--c-text-faintest': '#6a6a6a',
|
'--c-text-faintest': 'hsl(0 10% 28%)',
|
||||||
'--c-danger-bg': '#450a0a',
|
'--c-danger-bg': '#450a0a',
|
||||||
'--c-danger-border': '#7f1d1d',
|
'--c-danger-border': '#7f1d1d',
|
||||||
'--c-danger-text': '#f87171',
|
'--c-danger-text': '#f87171',
|
||||||
'--c-danger-text-strong': '#fca5a5',
|
'--c-danger-text-strong': '#fca5a5',
|
||||||
'--c-accent': '#f87171',
|
'--c-accent': 'hsl(180 70% 62%)',
|
||||||
'--c-accent-soft': '#b91c1c',
|
'--c-accent-soft': 'hsl(150 55% 34%)',
|
||||||
'--c-accent-text': '#fca5a5',
|
'--c-accent-text': 'hsl(180 75% 80%)',
|
||||||
},
|
},
|
||||||
green: {
|
green: {
|
||||||
'--c-surface-0': '#1a1a1a',
|
'--c-surface-0': 'hsl(140 32% 10%)',
|
||||||
'--c-surface-1': '#343434',
|
'--c-surface-1': 'hsl(140 28% 16%)',
|
||||||
'--c-surface-2': '#3d3d3d',
|
'--c-surface-2': 'hsl(140 26% 22%)',
|
||||||
'--c-border': '#3d3d3d',
|
'--c-border': 'hsl(140 24% 24%)',
|
||||||
'--c-border-soft': '#4a4a4a',
|
'--c-border-soft': 'hsl(140 22% 33%)',
|
||||||
'--c-border-faint': '#343434',
|
'--c-border-faint': 'hsl(140 28% 16%)',
|
||||||
'--c-border-strong': '#949494',
|
'--c-border-strong': 'hsl(140 15% 62%)',
|
||||||
'--c-text': '#ececec',
|
'--c-text': 'hsl(140 15% 94%)',
|
||||||
'--c-text-secondary': '#bbbbbb',
|
'--c-text-secondary': 'hsl(140 12% 82%)',
|
||||||
'--c-text-muted': '#949494',
|
'--c-text-muted': 'hsl(140 10% 65%)',
|
||||||
'--c-text-subtle': '#747474',
|
'--c-text-subtle': 'hsl(140 8% 50%)',
|
||||||
'--c-text-faint': '#5c5c5c',
|
'--c-text-faint': 'hsl(140 8% 38%)',
|
||||||
'--c-text-faintest': '#4a4a4a',
|
'--c-text-faintest': 'hsl(140 10% 28%)',
|
||||||
'--c-danger-bg': '#450a0a',
|
'--c-danger-bg': '#450a0a',
|
||||||
'--c-danger-border': '#7f1d1d',
|
'--c-danger-border': '#7f1d1d',
|
||||||
'--c-danger-text': '#f87171',
|
'--c-danger-text': '#f87171',
|
||||||
'--c-danger-text-strong': '#fca5a5',
|
'--c-danger-text-strong': '#fca5a5',
|
||||||
'--c-accent': '#4ade80',
|
'--c-accent': 'hsl(320 70% 62%)',
|
||||||
'--c-accent-soft': '#15803d',
|
'--c-accent-soft': 'hsl(290 55% 34%)',
|
||||||
'--c-accent-text': '#86efac',
|
'--c-accent-text': 'hsl(320 75% 80%)',
|
||||||
},
|
},
|
||||||
blue: {
|
blue: {
|
||||||
'--c-surface-0': '#363636',
|
'--c-surface-0': 'hsl(210 32% 10%)',
|
||||||
'--c-surface-1': '#545454',
|
'--c-surface-1': 'hsl(210 28% 16%)',
|
||||||
'--c-surface-2': '#666666',
|
'--c-surface-2': 'hsl(210 26% 22%)',
|
||||||
'--c-border': '#666666',
|
'--c-border': 'hsl(210 24% 24%)',
|
||||||
'--c-border-soft': '#7a7a7a',
|
'--c-border-soft': 'hsl(210 22% 33%)',
|
||||||
'--c-border-faint': '#545454',
|
'--c-border-faint': 'hsl(210 28% 16%)',
|
||||||
'--c-border-strong': '#adadad',
|
'--c-border-strong': 'hsl(210 15% 62%)',
|
||||||
'--c-text': '#ececec',
|
'--c-text': 'hsl(210 15% 94%)',
|
||||||
'--c-text-secondary': '#c8c8c8',
|
'--c-text-secondary': 'hsl(210 12% 82%)',
|
||||||
'--c-text-muted': '#adadad',
|
'--c-text-muted': 'hsl(210 10% 65%)',
|
||||||
'--c-text-subtle': '#989898',
|
'--c-text-subtle': 'hsl(210 8% 50%)',
|
||||||
'--c-text-faint': '#888888',
|
'--c-text-faint': 'hsl(210 8% 38%)',
|
||||||
'--c-text-faintest': '#7a7a7a',
|
'--c-text-faintest': 'hsl(210 10% 28%)',
|
||||||
'--c-danger-bg': '#450a0a',
|
'--c-danger-bg': '#450a0a',
|
||||||
'--c-danger-border': '#7f1d1d',
|
'--c-danger-border': '#7f1d1d',
|
||||||
'--c-danger-text': '#f87171',
|
'--c-danger-text': '#f87171',
|
||||||
'--c-danger-text-strong': '#fca5a5',
|
'--c-danger-text-strong': '#fca5a5',
|
||||||
'--c-accent': '#60a5fa',
|
'--c-accent': 'hsl(30 70% 62%)',
|
||||||
'--c-accent-soft': '#1d4ed8',
|
'--c-accent-soft': 'hsl(0 55% 34%)',
|
||||||
'--c-accent-text': '#93c5fd',
|
'--c-accent-text': 'hsl(30 75% 80%)',
|
||||||
},
|
},
|
||||||
yellow: {
|
yellow: {
|
||||||
'--c-surface-0': '#242424',
|
'--c-surface-0': 'hsl(50 32% 10%)',
|
||||||
'--c-surface-1': '#424242',
|
'--c-surface-1': 'hsl(50 28% 16%)',
|
||||||
'--c-surface-2': '#4a4a4a',
|
'--c-surface-2': 'hsl(50 26% 22%)',
|
||||||
'--c-border': '#4a4a4a',
|
'--c-border': 'hsl(50 24% 24%)',
|
||||||
'--c-border-soft': '#545454',
|
'--c-border-soft': 'hsl(50 22% 33%)',
|
||||||
'--c-border-faint': '#424242',
|
'--c-border-faint': 'hsl(50 28% 16%)',
|
||||||
'--c-border-strong': '#888888',
|
'--c-border-strong': 'hsl(50 15% 62%)',
|
||||||
'--c-text': '#e0e0e0',
|
'--c-text': 'hsl(50 15% 94%)',
|
||||||
'--c-text-secondary': '#a2a2a2',
|
'--c-text-secondary': 'hsl(50 12% 82%)',
|
||||||
'--c-text-muted': '#888888',
|
'--c-text-muted': 'hsl(50 10% 65%)',
|
||||||
'--c-text-subtle': '#797979',
|
'--c-text-subtle': 'hsl(50 8% 50%)',
|
||||||
'--c-text-faint': '#676767',
|
'--c-text-faint': 'hsl(50 8% 38%)',
|
||||||
'--c-text-faintest': '#545454',
|
'--c-text-faintest': 'hsl(50 10% 28%)',
|
||||||
'--c-danger-bg': '#450a0a',
|
'--c-danger-bg': '#450a0a',
|
||||||
'--c-danger-border': '#7f1d1d',
|
'--c-danger-border': '#7f1d1d',
|
||||||
'--c-danger-text': '#f87171',
|
'--c-danger-text': '#f87171',
|
||||||
'--c-danger-text-strong': '#fca5a5',
|
'--c-danger-text-strong': '#fca5a5',
|
||||||
'--c-accent': '#facc15',
|
'--c-accent': 'hsl(230 70% 62%)',
|
||||||
'--c-accent-soft': '#a16207',
|
'--c-accent-soft': 'hsl(200 55% 34%)',
|
||||||
'--c-accent-text': '#fde047',
|
'--c-accent-text': 'hsl(230 75% 80%)',
|
||||||
},
|
},
|
||||||
pink: {
|
pink: {
|
||||||
'--c-surface-0': '#2c2c2c',
|
'--c-surface-0': 'hsl(330 32% 10%)',
|
||||||
'--c-surface-1': '#4e4e4e',
|
'--c-surface-1': 'hsl(330 28% 16%)',
|
||||||
'--c-surface-2': '#5a5a5a',
|
'--c-surface-2': 'hsl(330 26% 22%)',
|
||||||
'--c-border': '#5a5a5a',
|
'--c-border': 'hsl(330 24% 24%)',
|
||||||
'--c-border-soft': '#6b6b6b',
|
'--c-border-soft': 'hsl(330 22% 33%)',
|
||||||
'--c-border-faint': '#4e4e4e',
|
'--c-border-faint': 'hsl(330 28% 16%)',
|
||||||
'--c-border-strong': '#b3b3b3',
|
'--c-border-strong': 'hsl(330 15% 62%)',
|
||||||
'--c-text': '#f2f2f2',
|
'--c-text': 'hsl(330 15% 94%)',
|
||||||
'--c-text-secondary': '#d0d0d0',
|
'--c-text-secondary': 'hsl(330 12% 82%)',
|
||||||
'--c-text-muted': '#b3b3b3',
|
'--c-text-muted': 'hsl(330 10% 65%)',
|
||||||
'--c-text-subtle': '#9a9a9a',
|
'--c-text-subtle': 'hsl(330 8% 50%)',
|
||||||
'--c-text-faint': '#818181',
|
'--c-text-faint': 'hsl(330 8% 38%)',
|
||||||
'--c-text-faintest': '#6b6b6b',
|
'--c-text-faintest': 'hsl(330 10% 28%)',
|
||||||
'--c-danger-bg': '#450a0a',
|
'--c-danger-bg': '#450a0a',
|
||||||
'--c-danger-border': '#7f1d1d',
|
'--c-danger-border': '#7f1d1d',
|
||||||
'--c-danger-text': '#f87171',
|
'--c-danger-text': '#f87171',
|
||||||
'--c-danger-text-strong': '#fca5a5',
|
'--c-danger-text-strong': '#fca5a5',
|
||||||
'--c-accent': '#f472b6',
|
'--c-accent': 'hsl(150 70% 62%)',
|
||||||
'--c-accent-soft': '#be185d',
|
'--c-accent-soft': 'hsl(120 55% 34%)',
|
||||||
'--c-accent-text': '#f9a8d4',
|
'--c-accent-text': 'hsl(150 75% 80%)',
|
||||||
},
|
},
|
||||||
orange: {
|
orange: {
|
||||||
'--c-surface-0': '#252525',
|
'--c-surface-0': 'hsl(30 32% 10%)',
|
||||||
'--c-surface-1': '#474747',
|
'--c-surface-1': 'hsl(30 28% 16%)',
|
||||||
'--c-surface-2': '#565656',
|
'--c-surface-2': 'hsl(30 26% 22%)',
|
||||||
'--c-border': '#565656',
|
'--c-border': 'hsl(30 24% 24%)',
|
||||||
'--c-border-soft': '#676767',
|
'--c-border-soft': 'hsl(30 22% 33%)',
|
||||||
'--c-border-faint': '#474747',
|
'--c-border-faint': 'hsl(30 28% 16%)',
|
||||||
'--c-border-strong': '#9b9b9b',
|
'--c-border-strong': 'hsl(30 15% 62%)',
|
||||||
'--c-text': '#eaeaea',
|
'--c-text': 'hsl(30 15% 94%)',
|
||||||
'--c-text-secondary': '#b8b8b8',
|
'--c-text-secondary': 'hsl(30 12% 82%)',
|
||||||
'--c-text-muted': '#9b9b9b',
|
'--c-text-muted': 'hsl(30 10% 65%)',
|
||||||
'--c-text-subtle': '#888888',
|
'--c-text-subtle': 'hsl(30 8% 50%)',
|
||||||
'--c-text-faint': '#7b7b7b',
|
'--c-text-faint': 'hsl(30 8% 38%)',
|
||||||
'--c-text-faintest': '#676767',
|
'--c-text-faintest': 'hsl(30 10% 28%)',
|
||||||
'--c-danger-bg': '#450a0a',
|
'--c-danger-bg': '#450a0a',
|
||||||
'--c-danger-border': '#7f1d1d',
|
'--c-danger-border': '#7f1d1d',
|
||||||
'--c-danger-text': '#f87171',
|
'--c-danger-text': '#f87171',
|
||||||
'--c-danger-text-strong': '#fca5a5',
|
'--c-danger-text-strong': '#fca5a5',
|
||||||
'--c-accent': '#fb923c',
|
'--c-accent': 'hsl(210 70% 62%)',
|
||||||
'--c-accent-soft': '#c2410c',
|
'--c-accent-soft': 'hsl(180 55% 34%)',
|
||||||
'--c-accent-text': '#fdba74',
|
'--c-accent-text': 'hsl(210 75% 80%)',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user