Compare commits
2 commits
171cb05305
...
f1dc95b51c
| Author | SHA1 | Date | |
|---|---|---|---|
| f1dc95b51c | |||
|
|
fb266cf17a |
23 changed files with 1449 additions and 84 deletions
5
bun.lock
5
bun.lock
|
|
@ -5,6 +5,7 @@
|
|||
"": {
|
||||
"name": "dimensionlab-website",
|
||||
"dependencies": {
|
||||
"@iconify/svelte": "^5.2.2",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"ajv": "^8.20.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
|
|
@ -90,6 +91,10 @@
|
|||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@iconify/svelte": ["@iconify/svelte@5.2.2", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "svelte": ">5.0.0" } }, "sha512-XMXxD3nzH7yB68C3K4sNwzrJ1TBscODR0ZqCeNf0KGuEMCTSuCo0jHNDZ0o0iRXTylO41NSz/DWam/4atLG1yw=="],
|
||||
|
||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"db:check": "drizzle-kit check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/svelte": "^5.2.2",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"ajv": "^8.20.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
|
|
|
|||
31
src/app.css
31
src/app.css
|
|
@ -1,30 +1 @@
|
|||
:root {
|
||||
color-scheme: dark;
|
||||
font-family:
|
||||
"IBM Plex Mono", "Roboto Mono", "SFMono-Regular", Consolas, monospace;
|
||||
background: #050505;
|
||||
color: #f4f4f4;
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background:
|
||||
linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||
#050505;
|
||||
background-size: 48px 48px;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
@import "./lib/ui/tokens.css";
|
||||
|
|
|
|||
116
src/lib/ui/components/DashboardFrame.svelte
Normal file
116
src/lib/ui/components/DashboardFrame.svelte
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
<script lang="ts">
|
||||
import type { UiDashboardPreview } from "../types";
|
||||
import ModuleCard from "./ModuleCard.svelte";
|
||||
import ServicePanel from "./ServicePanel.svelte";
|
||||
import StatusStrip from "./StatusStrip.svelte";
|
||||
import TelemetryGrid from "./TelemetryGrid.svelte";
|
||||
|
||||
const generatedTitleId = $props.id();
|
||||
|
||||
let {
|
||||
dashboard,
|
||||
titleId = `${generatedTitleId}-title`,
|
||||
}: {
|
||||
dashboard: UiDashboardPreview;
|
||||
titleId?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<main class="dashboard-frame" aria-labelledby={titleId}>
|
||||
<header class="dashboard-frame__header">
|
||||
<div>
|
||||
{#if dashboard.eyebrow}
|
||||
<p>{dashboard.eyebrow}</p>
|
||||
{/if}
|
||||
<h1 id={titleId}>{dashboard.title}</h1>
|
||||
{#if dashboard.subtitle}
|
||||
<span>{dashboard.subtitle}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if dashboard.modules.length}
|
||||
<div class="dashboard-frame__modules">
|
||||
{#each dashboard.modules as module (module.id)}
|
||||
<ModuleCard {module} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<TelemetryGrid cards={dashboard.telemetry} />
|
||||
|
||||
<section class="dashboard-frame__panels" aria-label="Service groups">
|
||||
{#each dashboard.serviceGroups as group (group.id)}
|
||||
<ServicePanel {group} />
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<StatusStrip items={dashboard.statusItems} />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.dashboard-frame {
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
gap: var(--ui-space-3);
|
||||
padding: clamp(0.75rem, 1.3vw, 1.25rem);
|
||||
background:
|
||||
linear-gradient(90deg, transparent 0 49%, rgba(255, 255, 255, 0.08) 50%, transparent 51%),
|
||||
rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.dashboard-frame__header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--ui-space-4);
|
||||
align-items: start;
|
||||
border-bottom: var(--ui-border);
|
||||
padding-bottom: var(--ui-space-3);
|
||||
}
|
||||
|
||||
.dashboard-frame__header p,
|
||||
.dashboard-frame__header span,
|
||||
h1 {
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dashboard-frame__header p,
|
||||
.dashboard-frame__header span {
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: clamp(2.45rem, 5vw, 4.2rem);
|
||||
font-weight: 850;
|
||||
letter-spacing: 0;
|
||||
line-height: 0.85;
|
||||
}
|
||||
|
||||
.dashboard-frame__modules {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: end;
|
||||
gap: var(--ui-space-3);
|
||||
}
|
||||
|
||||
.dashboard-frame__panels {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
|
||||
gap: var(--ui-space-3);
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.dashboard-frame__header {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-frame__modules {
|
||||
justify-content: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
src/lib/ui/components/FooterCell.svelte
Normal file
89
src/lib/ui/components/FooterCell.svelte
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<script lang="ts">
|
||||
import type { UiStatusItem } from "../types";
|
||||
|
||||
let { item }: { item: UiStatusItem } = $props();
|
||||
|
||||
let target = $derived(item.link?.external ? "_blank" : undefined);
|
||||
let rel = $derived(item.link?.external ? "noreferrer" : undefined);
|
||||
</script>
|
||||
|
||||
{#snippet cellContent()}
|
||||
<span>{item.label}</span>
|
||||
<strong>{item.value}</strong>
|
||||
{/snippet}
|
||||
|
||||
{#if item.link}
|
||||
<a
|
||||
class="footer-cell"
|
||||
data-severity={item.severity || "neutral"}
|
||||
href={item.link.href}
|
||||
target={target}
|
||||
rel={rel}
|
||||
aria-label={item.link.label}
|
||||
>
|
||||
{@render cellContent()}
|
||||
</a>
|
||||
{:else}
|
||||
<div class="footer-cell" data-severity={item.severity || "neutral"}>
|
||||
{@render cellContent()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.footer-cell {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
min-height: 2.65rem;
|
||||
align-items: center;
|
||||
background: rgba(2, 3, 2, 0.92);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
span,
|
||||
strong {
|
||||
min-width: 0;
|
||||
padding: var(--ui-space-2) var(--ui-space-3);
|
||||
overflow-wrap: anywhere;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
span {
|
||||
height: 100%;
|
||||
border-right: var(--ui-border);
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--ui-color-text);
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.footer-cell[data-severity="ok"] strong {
|
||||
background: var(--ui-color-accent);
|
||||
color: #060706;
|
||||
}
|
||||
|
||||
.footer-cell[data-severity="warning"] strong {
|
||||
color: var(--ui-color-warning);
|
||||
}
|
||||
|
||||
.footer-cell[data-severity="danger"] strong {
|
||||
color: var(--ui-color-danger);
|
||||
}
|
||||
|
||||
.footer-cell[data-severity="stale"] strong,
|
||||
.footer-cell[data-severity="unavailable"] strong {
|
||||
color: var(--ui-color-stale);
|
||||
}
|
||||
|
||||
.footer-cell[data-severity="loading"] strong {
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.footer-cell:where(a):hover strong {
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
</style>
|
||||
53
src/lib/ui/components/IconGlyph.svelte
Normal file
53
src/lib/ui/components/IconGlyph.svelte
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<script lang="ts">
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
let {
|
||||
name,
|
||||
label,
|
||||
size = "md",
|
||||
}: {
|
||||
name?: string;
|
||||
label?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="icon-glyph"
|
||||
data-size={size}
|
||||
data-icon-name={name}
|
||||
aria-label={label}
|
||||
aria-hidden={label ? undefined : "true"}
|
||||
>
|
||||
{#if name}
|
||||
<Icon icon={name} />
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.icon-glyph {
|
||||
display: inline-grid;
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
place-items: center;
|
||||
border: var(--ui-border);
|
||||
background: #050605;
|
||||
color: currentColor;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-glyph :global(svg) {
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
}
|
||||
|
||||
.icon-glyph[data-size="sm"] {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
.icon-glyph[data-size="lg"] {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
</style>
|
||||
86
src/lib/ui/components/ModuleCard.svelte
Normal file
86
src/lib/ui/components/ModuleCard.svelte
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts">
|
||||
import type { UiModuleBlock } from "../types";
|
||||
import IconGlyph from "./IconGlyph.svelte";
|
||||
|
||||
let { module }: { module: UiModuleBlock } = $props();
|
||||
</script>
|
||||
|
||||
<aside class="module-card" data-severity={module.severity || "neutral"}>
|
||||
<div>
|
||||
{#if module.title}
|
||||
<h2>{module.title}</h2>
|
||||
{/if}
|
||||
{#if module.value}
|
||||
<strong>{module.value}</strong>
|
||||
{/if}
|
||||
{#if module.detail || module.label}
|
||||
<p>{module.detail || module.label}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if module.icon}
|
||||
<IconGlyph name={module.icon} size="lg" />
|
||||
{/if}
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.module-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--ui-space-4);
|
||||
align-items: start;
|
||||
min-width: min(100%, 18rem);
|
||||
border: var(--ui-border);
|
||||
background: rgba(6, 8, 7, 0.82);
|
||||
padding: var(--ui-space-3);
|
||||
}
|
||||
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.82rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin-top: var(--ui-space-1);
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: 2.25rem;
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: var(--ui-space-1);
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.module-card[data-severity="ok"] :global(.icon-glyph) {
|
||||
background: var(--ui-color-accent);
|
||||
color: #050605;
|
||||
}
|
||||
|
||||
.module-card[data-severity="warning"] {
|
||||
border-color: color-mix(in srgb, var(--ui-color-warning), transparent 42%);
|
||||
color: var(--ui-color-warning);
|
||||
}
|
||||
|
||||
.module-card[data-severity="danger"] {
|
||||
border-color: color-mix(in srgb, var(--ui-color-danger), transparent 32%);
|
||||
color: var(--ui-color-danger);
|
||||
}
|
||||
|
||||
.module-card[data-severity="stale"],
|
||||
.module-card[data-severity="unavailable"] {
|
||||
color: var(--ui-color-stale);
|
||||
}
|
||||
|
||||
.module-card[data-severity="loading"] {
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
</style>
|
||||
81
src/lib/ui/components/Panel.svelte
Normal file
81
src/lib/ui/components/Panel.svelte
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
title,
|
||||
density = "dense",
|
||||
children,
|
||||
}: {
|
||||
title?: string;
|
||||
density?: "compact" | "dense";
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section class="panel" data-density={density}>
|
||||
{#if title}
|
||||
<header class="panel__header">
|
||||
<h2>{title}</h2>
|
||||
</header>
|
||||
{/if}
|
||||
<div class="panel__body">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
border: var(--ui-border);
|
||||
background: rgba(3, 4, 3, 0.86);
|
||||
box-shadow: var(--ui-shadow-hard);
|
||||
}
|
||||
|
||||
.panel::before,
|
||||
.panel::after {
|
||||
position: absolute;
|
||||
width: 0.55rem;
|
||||
height: 0.55rem;
|
||||
border-color: var(--ui-color-line-strong);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.panel::before {
|
||||
top: 0.2rem;
|
||||
right: 0.2rem;
|
||||
border-top: 1px solid;
|
||||
border-right: 1px solid;
|
||||
}
|
||||
|
||||
.panel::after {
|
||||
right: 0.2rem;
|
||||
bottom: 0.2rem;
|
||||
border-right: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.panel__header {
|
||||
padding: var(--ui-space-3) var(--ui-space-3) 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: clamp(1.75rem, 3vw, 2.5rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
line-height: 0.9;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.panel__body {
|
||||
display: grid;
|
||||
gap: var(--ui-space-2);
|
||||
padding: var(--ui-space-3);
|
||||
}
|
||||
|
||||
.panel[data-density="compact"] .panel__body {
|
||||
padding: var(--ui-space-2);
|
||||
}
|
||||
</style>
|
||||
17
src/lib/ui/components/ServicePanel.svelte
Normal file
17
src/lib/ui/components/ServicePanel.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import type { UiServiceGroup } from "../types";
|
||||
import Panel from "./Panel.svelte";
|
||||
import ServiceRow from "./ServiceRow.svelte";
|
||||
import StatusStrip from "./StatusStrip.svelte";
|
||||
|
||||
let { group }: { group: UiServiceGroup } = $props();
|
||||
</script>
|
||||
|
||||
<Panel title={group.title}>
|
||||
{#if group.summary?.length}
|
||||
<StatusStrip items={group.summary} compact />
|
||||
{/if}
|
||||
{#each group.services as service (service.id)}
|
||||
<ServiceRow {service} />
|
||||
{/each}
|
||||
</Panel>
|
||||
106
src/lib/ui/components/ServiceRow.svelte
Normal file
106
src/lib/ui/components/ServiceRow.svelte
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<script lang="ts">
|
||||
import type { UiServiceRow } from "../types";
|
||||
import IconGlyph from "./IconGlyph.svelte";
|
||||
import StatusBadge from "./StatusBadge.svelte";
|
||||
|
||||
let { service }: { service: UiServiceRow } = $props();
|
||||
|
||||
let target = $derived(service.link?.external ? "_blank" : undefined);
|
||||
let rel = $derived(service.link?.external ? "noreferrer" : undefined);
|
||||
</script>
|
||||
|
||||
{#snippet rowContent()}
|
||||
<IconGlyph name={service.icon} />
|
||||
<div class="service-row__main">
|
||||
<h3>{service.label}</h3>
|
||||
<p>{service.description}</p>
|
||||
</div>
|
||||
{#if service.detail}
|
||||
<StatusBadge label={service.detail} severity={service.severity} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if service.link}
|
||||
<a
|
||||
class="service-row"
|
||||
data-severity={service.severity}
|
||||
href={service.link.href}
|
||||
target={target}
|
||||
rel={rel}
|
||||
aria-label={service.link.label}
|
||||
>
|
||||
{@render rowContent()}
|
||||
</a>
|
||||
{:else}
|
||||
<article class="service-row" data-severity={service.severity}>
|
||||
{@render rowContent()}
|
||||
</article>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.service-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: var(--ui-space-3);
|
||||
align-items: center;
|
||||
min-height: 3.75rem;
|
||||
border: var(--ui-border);
|
||||
background: rgba(12, 13, 12, 0.72);
|
||||
color: inherit;
|
||||
padding: var(--ui-space-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.service-row[data-severity="warning"] {
|
||||
border-color: color-mix(in srgb, var(--ui-color-warning), transparent 54%);
|
||||
}
|
||||
|
||||
.service-row[data-severity="danger"],
|
||||
.service-row[data-severity="unavailable"] {
|
||||
border-color: color-mix(in srgb, var(--ui-color-danger), transparent 50%);
|
||||
}
|
||||
|
||||
.service-row[data-severity="loading"] {
|
||||
border-color: color-mix(in srgb, var(--ui-color-accent), transparent 56%);
|
||||
}
|
||||
|
||||
.service-row:where(a):hover {
|
||||
border-color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.service-row__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.22rem;
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.25;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 580px) {
|
||||
.service-row {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.service-row :global(.status-badge) {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
52
src/lib/ui/components/StatusBadge.svelte
Normal file
52
src/lib/ui/components/StatusBadge.svelte
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script lang="ts">
|
||||
import type { UiSeverity } from "../types";
|
||||
|
||||
let {
|
||||
label,
|
||||
severity = "neutral",
|
||||
}: {
|
||||
label: string;
|
||||
severity?: UiSeverity;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<span class="status-badge" data-severity={severity}>{label}</span>
|
||||
|
||||
<style>
|
||||
.status-badge {
|
||||
display: inline-grid;
|
||||
min-height: 1.6rem;
|
||||
align-items: center;
|
||||
border: var(--ui-border);
|
||||
padding: 0 var(--ui-space-2);
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge[data-severity="ok"] {
|
||||
border-color: color-mix(in srgb, var(--ui-color-ok), transparent 42%);
|
||||
color: var(--ui-color-ok);
|
||||
}
|
||||
|
||||
.status-badge[data-severity="warning"] {
|
||||
border-color: color-mix(in srgb, var(--ui-color-warning), transparent 35%);
|
||||
color: var(--ui-color-warning);
|
||||
}
|
||||
|
||||
.status-badge[data-severity="danger"] {
|
||||
border-color: color-mix(in srgb, var(--ui-color-danger), transparent 30%);
|
||||
color: var(--ui-color-danger);
|
||||
}
|
||||
|
||||
.status-badge[data-severity="stale"],
|
||||
.status-badge[data-severity="unavailable"] {
|
||||
color: var(--ui-color-stale);
|
||||
}
|
||||
|
||||
.status-badge[data-severity="loading"] {
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
</style>
|
||||
32
src/lib/ui/components/StatusStrip.svelte
Normal file
32
src/lib/ui/components/StatusStrip.svelte
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import type { UiStatusItem } from "../types";
|
||||
import FooterCell from "./FooterCell.svelte";
|
||||
|
||||
let {
|
||||
items,
|
||||
compact = false,
|
||||
}: {
|
||||
items: UiStatusItem[];
|
||||
compact?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section class="status-strip" data-compact={compact}>
|
||||
{#each items as item (item.id)}
|
||||
<FooterCell {item} />
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.status-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
|
||||
gap: 1px;
|
||||
border: var(--ui-border);
|
||||
background: var(--ui-color-line);
|
||||
}
|
||||
|
||||
.status-strip[data-compact="true"] {
|
||||
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
158
src/lib/ui/components/TelemetryCard.svelte
Normal file
158
src/lib/ui/components/TelemetryCard.svelte
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
<script lang="ts">
|
||||
import { formatMetricValue } from "../format";
|
||||
import type { UiTelemetryCard } from "../types";
|
||||
import IconGlyph from "./IconGlyph.svelte";
|
||||
|
||||
let { card }: { card: UiTelemetryCard } = $props();
|
||||
|
||||
let progress = $derived(resolveProgress(card));
|
||||
let value = $derived(formatMetricValue(card.value));
|
||||
</script>
|
||||
|
||||
<article class="telemetry-card" data-severity={card.severity}>
|
||||
<header>
|
||||
{#if card.icon}
|
||||
<IconGlyph name={card.icon} size="sm" />
|
||||
{/if}
|
||||
<h3>{card.label}</h3>
|
||||
</header>
|
||||
<strong>{value}</strong>
|
||||
{#if progress !== null}
|
||||
<div class="telemetry-card__bar" aria-hidden="true">
|
||||
<span style={`--metric-progress: ${progress}%`}></span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if card.sparkline?.length}
|
||||
<svg class="telemetry-card__sparkline" viewBox="0 0 100 24" aria-hidden="true">
|
||||
<polyline points={sparklinePoints(card.sparkline)} />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if card.detail || card.description}
|
||||
<p>{card.detail || card.description}</p>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.telemetry-card {
|
||||
display: grid;
|
||||
min-height: 7.15rem;
|
||||
border: var(--ui-border);
|
||||
background: linear-gradient(180deg, rgba(13, 15, 14, 0.82), rgba(2, 3, 2, 0.92));
|
||||
padding: var(--ui-space-3);
|
||||
color: var(--ui-color-text);
|
||||
gap: var(--ui-space-2);
|
||||
}
|
||||
|
||||
.telemetry-card[data-severity="warning"] {
|
||||
border-color: color-mix(in srgb, var(--ui-color-warning), transparent 42%);
|
||||
}
|
||||
|
||||
.telemetry-card[data-severity="danger"] {
|
||||
border-color: color-mix(in srgb, var(--ui-color-danger), transparent 22%);
|
||||
color: var(--ui-color-danger);
|
||||
}
|
||||
|
||||
.telemetry-card[data-severity="stale"],
|
||||
.telemetry-card[data-severity="unavailable"] {
|
||||
color: var(--ui-color-stale);
|
||||
}
|
||||
|
||||
.telemetry-card[data-severity="loading"] {
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.telemetry-card[data-severity="loading"] .telemetry-card__bar span {
|
||||
min-width: 1.25rem;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--ui-space-2);
|
||||
}
|
||||
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
overflow-wrap: anywhere;
|
||||
color: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: clamp(2rem, 4vw, 3rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
line-height: 0.82;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.67rem;
|
||||
line-height: 1.2;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.telemetry-card__bar {
|
||||
height: 0.35rem;
|
||||
border: var(--ui-border);
|
||||
background: #030403;
|
||||
}
|
||||
|
||||
.telemetry-card__bar span {
|
||||
display: block;
|
||||
width: var(--metric-progress);
|
||||
height: 100%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.telemetry-card__sparkline {
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.telemetry-card__sparkline polyline {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: square;
|
||||
stroke-linejoin: miter;
|
||||
stroke-width: 2;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" module>
|
||||
import { clampPercent } from "../format";
|
||||
|
||||
function resolveProgress(card: {
|
||||
progress?: number;
|
||||
value: { kind: string; value: number | string };
|
||||
}): number | null {
|
||||
if (typeof card.progress === "number") return clampPercent(card.progress);
|
||||
if (card.value.kind !== "percent") return null;
|
||||
|
||||
const progress = Number(card.value.value);
|
||||
return Number.isFinite(progress) ? clampPercent(progress) : null;
|
||||
}
|
||||
|
||||
function sparklinePoints(values: number[]): string {
|
||||
const max = Math.max(...values, 1);
|
||||
const step = values.length > 1 ? 100 / (values.length - 1) : 100;
|
||||
|
||||
return values
|
||||
.map((value, index) => {
|
||||
const x = index * step;
|
||||
const y = 24 - (Math.max(0, value) / max) * 22;
|
||||
return `${x.toFixed(2)},${y.toFixed(2)}`;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
</script>
|
||||
22
src/lib/ui/components/TelemetryGrid.svelte
Normal file
22
src/lib/ui/components/TelemetryGrid.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import type { UiTelemetryCard } from "../types";
|
||||
import TelemetryCard from "./TelemetryCard.svelte";
|
||||
|
||||
let { cards }: { cards: UiTelemetryCard[] } = $props();
|
||||
</script>
|
||||
|
||||
<section class="telemetry-grid" aria-label="Telemetry">
|
||||
{#each cards as card (card.id)}
|
||||
<TelemetryCard {card} />
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.telemetry-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(var(--ui-density-card-min), 1fr));
|
||||
gap: 1px;
|
||||
border: var(--ui-border);
|
||||
background: var(--ui-color-line);
|
||||
}
|
||||
</style>
|
||||
97
src/lib/ui/components/render.test.ts
Normal file
97
src/lib/ui/components/render.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { render } from "svelte/server";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import DashboardFrame from "./DashboardFrame.svelte";
|
||||
import FooterCell from "./FooterCell.svelte";
|
||||
import ServiceRow from "./ServiceRow.svelte";
|
||||
import TelemetryCard from "./TelemetryCard.svelte";
|
||||
import { dashboardPreviewFixtures } from "../fixtures";
|
||||
|
||||
describe("dashboard UI components", () => {
|
||||
test("renders the primary generic dashboard fixture", () => {
|
||||
const { body } = render(DashboardFrame, {
|
||||
props: {
|
||||
dashboard: dashboardPreviewFixtures.primary,
|
||||
},
|
||||
});
|
||||
|
||||
expect(body).toContain("Operations Console");
|
||||
expect(body).toContain("Core Throughput");
|
||||
expect(body).toContain("Queue Workers");
|
||||
expect(body).toContain("data-icon-name=\"mdi:server-network\"");
|
||||
expect(body).toContain("data-severity=\"warning\"");
|
||||
expect(body).not.toContain("dashboard-title");
|
||||
});
|
||||
|
||||
test("renders another generic dashboard fixture through the same component", () => {
|
||||
const { body } = render(DashboardFrame, {
|
||||
props: {
|
||||
dashboard: dashboardPreviewFixtures.secondary,
|
||||
},
|
||||
});
|
||||
|
||||
expect(body).toContain("Support Desk");
|
||||
expect(body).toContain("Response Window");
|
||||
expect(body).toContain("Regional Nodes");
|
||||
expect(body).toContain("data-icon-name=\"mdi:headset\"");
|
||||
expect(body).toContain("data-severity=\"loading\"");
|
||||
});
|
||||
|
||||
test("renders optional service and status links as focusable anchors", () => {
|
||||
const service = render(ServiceRow, {
|
||||
props: {
|
||||
service: {
|
||||
id: "linked-service",
|
||||
label: "Linked Service",
|
||||
description: "Generic linked destination",
|
||||
severity: "ok",
|
||||
detail: "ready",
|
||||
link: { href: "https://example.test/service", external: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
const footer = render(FooterCell, {
|
||||
props: {
|
||||
item: {
|
||||
id: "linked-status",
|
||||
label: "Linked Status",
|
||||
value: "open",
|
||||
severity: "neutral",
|
||||
link: { href: "https://example.test/status" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(service.body).toContain("<a ");
|
||||
expect(service.body).toContain("href=\"https://example.test/service\"");
|
||||
expect(service.body).toContain("target=\"_blank\"");
|
||||
expect(footer.body).toContain("<a ");
|
||||
expect(footer.body).toContain("href=\"https://example.test/status\"");
|
||||
});
|
||||
|
||||
test("does not render progress bars for non-percent metrics without explicit progress", () => {
|
||||
const withoutProgress = render(TelemetryCard, {
|
||||
props: {
|
||||
card: {
|
||||
id: "bytes",
|
||||
label: "Bytes Metric",
|
||||
value: { kind: "bytes", value: 2048 },
|
||||
severity: "neutral",
|
||||
},
|
||||
},
|
||||
});
|
||||
const withProgress = render(TelemetryCard, {
|
||||
props: {
|
||||
card: {
|
||||
id: "bytes-with-progress",
|
||||
label: "Bytes With Progress",
|
||||
value: { kind: "bytes", value: 2048 },
|
||||
progress: 42,
|
||||
severity: "neutral",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(withoutProgress.body).not.toContain("telemetry-card__bar");
|
||||
expect(withProgress.body).toContain("--metric-progress: 42%");
|
||||
});
|
||||
});
|
||||
49
src/lib/ui/content-boundary.test.ts
Normal file
49
src/lib/ui/content-boundary.test.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
const forbiddenTerms = [
|
||||
"dimensionlab",
|
||||
"dimension lab",
|
||||
"vaultwarden",
|
||||
"forgejo",
|
||||
"grafana",
|
||||
"uptime kuma",
|
||||
"prometheus",
|
||||
"backrest",
|
||||
"open webui",
|
||||
"comfyui",
|
||||
"adminer",
|
||||
"cockpit",
|
||||
"ollama",
|
||||
];
|
||||
|
||||
describe("UI content boundary", () => {
|
||||
test("keeps environment-specific content out of reusable UI source", () => {
|
||||
const source = readUiSource(join(process.cwd(), "src", "lib", "ui"));
|
||||
const normalized = source.toLowerCase();
|
||||
|
||||
expect(
|
||||
forbiddenTerms.filter((term) => normalized.includes(term)),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test("keeps icon rendering driven by icon identifiers", () => {
|
||||
const source = readUiSource(join(process.cwd(), "src", "lib", "ui"));
|
||||
|
||||
expect(source).not.toContain("@iconify-json/");
|
||||
expect(source).not.toContain("/icons/");
|
||||
});
|
||||
});
|
||||
|
||||
function readUiSource(path: string): string {
|
||||
const stats = statSync(path);
|
||||
if (stats.isFile()) {
|
||||
if (path.endsWith(".test.ts")) return "";
|
||||
return readFileSync(path, "utf8");
|
||||
}
|
||||
|
||||
return readdirSync(path)
|
||||
.map((entry) => readUiSource(join(path, entry)))
|
||||
.join("\n");
|
||||
}
|
||||
130
src/lib/ui/fixtures.ts
Normal file
130
src/lib/ui/fixtures.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import type { UiDashboardPreview } from "./types";
|
||||
|
||||
export const dashboardPreviewFixtures: Record<string, UiDashboardPreview> = {
|
||||
primary: {
|
||||
eyebrow: "Command Surface",
|
||||
title: "Operations Console",
|
||||
subtitle: "Capacity, latency, and queue health",
|
||||
telemetry: [
|
||||
metric("core-throughput", "Core Throughput", "mdi:server-network", 82, "ok"),
|
||||
metric("edge-cache", "Edge Cache", "mdi:database-clock", 67, "neutral"),
|
||||
metric("queue-depth", "Queue Depth", "mdi:tray-full", 74, "warning"),
|
||||
metric("error-budget", "Error Budget", "mdi:chart-timeline-variant", 18, "danger"),
|
||||
metric("build-latency", "Build Latency", "mdi:timer-sand", 23, "stale"),
|
||||
metric("worker-load", "Worker Load", "mdi:cpu-64-bit", 55, "ok"),
|
||||
],
|
||||
modules: [
|
||||
{
|
||||
id: "ambient-conditions",
|
||||
title: "Local Node",
|
||||
value: "21.4 C",
|
||||
detail: "clear window",
|
||||
icon: "mdi:weather-partly-cloudy",
|
||||
severity: "ok",
|
||||
},
|
||||
],
|
||||
serviceGroups: [
|
||||
{
|
||||
id: "queue-workers",
|
||||
title: "Queue Workers",
|
||||
services: [
|
||||
service("ingest", "Ingest", "Event intake pipeline", "mdi:arrow-collapse-down", "ok", "1.252 ms"),
|
||||
service("scheduler", "Scheduler", "Timed job coordinator", "mdi:calendar-clock", "warning", "1.487 ms"),
|
||||
service("archive", "Archive", "Cold storage transfer", "mdi:archive-arrow-down", "ok", "1.301 ms"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "regional-nodes",
|
||||
title: "Regional Nodes",
|
||||
services: [
|
||||
service("north", "North", "Primary traffic cell", "mdi:access-point", "ok", "899 ms"),
|
||||
service("west", "West", "Replica traffic cell", "mdi:access-point-network", "neutral", "1.118 ms"),
|
||||
service("south", "South", "Maintenance window", "mdi:wrench-clock", "stale", "paused"),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "control-plane",
|
||||
title: "Control Plane",
|
||||
services: [
|
||||
service("identity", "Identity", "Access token exchange", "mdi:account-key", "ok", "721 ms"),
|
||||
service("policy", "Policy", "Rules evaluation", "mdi:shield-check", "ok", "812 ms"),
|
||||
service("audit", "Audit", "Event ledger writer", "mdi:text-box-search", "warning", "1.442 ms"),
|
||||
],
|
||||
},
|
||||
],
|
||||
statusItems: [
|
||||
{ id: "system", label: "System", value: "Nominal", severity: "ok" },
|
||||
{ id: "sync", label: "Last Sync", value: "2 minutes ago", severity: "stale" },
|
||||
{ id: "uptime", label: "Uptime", value: "14d 08h", severity: "neutral" },
|
||||
{ id: "refresh", label: "Refresh", value: "15s", severity: "neutral" },
|
||||
],
|
||||
},
|
||||
secondary: {
|
||||
eyebrow: "Support Surface",
|
||||
title: "Support Desk",
|
||||
subtitle: "Response, routing, and regional health",
|
||||
telemetry: [
|
||||
metric("response-window", "Response Window", "mdi:headset", 41, "ok"),
|
||||
metric("ticket-load", "Ticket Load", "mdi:ticket-confirmation", 68, "warning"),
|
||||
metric("handoff-drift", "Handoff Drift", "mdi:swap-horizontal", 11, "neutral"),
|
||||
metric("coverage-gap", "Coverage Gap", "mdi:map-marker-alert", 7, "danger"),
|
||||
metric("routing-warmup", "Routing Warmup", "mdi:progress-clock", 0, "loading"),
|
||||
],
|
||||
modules: [
|
||||
{
|
||||
id: "shift-state",
|
||||
title: "Shift State",
|
||||
value: "covered",
|
||||
detail: "static fixture",
|
||||
icon: "mdi:clipboard-check-outline",
|
||||
severity: "ok",
|
||||
},
|
||||
],
|
||||
serviceGroups: [
|
||||
{
|
||||
id: "regional-nodes",
|
||||
title: "Regional Nodes",
|
||||
services: [
|
||||
service("desk-a", "Desk A", "Frontline queue", "mdi:monitor-dashboard", "ok", "843 ms"),
|
||||
service("desk-b", "Desk B", "Escalation queue", "mdi:monitor-star", "warning", "1.204 ms"),
|
||||
service("desk-c", "Desk C", "Overflow queue", "mdi:monitor-off", "unavailable", "offline"),
|
||||
],
|
||||
},
|
||||
],
|
||||
statusItems: [
|
||||
{ id: "desk", label: "Desk", value: "Covered", severity: "ok" },
|
||||
{ id: "handoff", label: "Handoff", value: "Pending", severity: "warning" },
|
||||
{ id: "routing", label: "Routing", value: "Manual", severity: "neutral" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function metric(
|
||||
id: string,
|
||||
label: string,
|
||||
icon: string,
|
||||
progress: number,
|
||||
severity: UiDashboardPreview["telemetry"][number]["severity"],
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
icon,
|
||||
progress,
|
||||
severity,
|
||||
value: { kind: "percent" as const, value: progress },
|
||||
detail: "static fixture",
|
||||
sparkline: [12, 18, 15, 28, 24, progress],
|
||||
};
|
||||
}
|
||||
|
||||
function service(
|
||||
id: string,
|
||||
label: string,
|
||||
description: string,
|
||||
icon: string,
|
||||
severity: UiDashboardPreview["serviceGroups"][number]["services"][number]["severity"],
|
||||
detail: string,
|
||||
) {
|
||||
return { id, label, description, icon, severity, detail };
|
||||
}
|
||||
41
src/lib/ui/format.ts
Normal file
41
src/lib/ui/format.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type { UiMetricValue } from "./types";
|
||||
|
||||
const byteUnits = ["B", "KB", "MB", "GB", "TB"];
|
||||
|
||||
export function formatMetricValue(metric: UiMetricValue): string {
|
||||
if (metric.kind === "text") return String(metric.value);
|
||||
|
||||
const value = typeof metric.value === "number" ? metric.value : Number(metric.value);
|
||||
const precision = metric.precision ?? inferPrecision(value);
|
||||
const unit = metric.unit ?? defaultUnit(metric.kind);
|
||||
|
||||
if (metric.kind === "bytes") return formatBytes(value, precision);
|
||||
return `${value.toFixed(precision)}${unit ? ` ${unit}` : ""}`;
|
||||
}
|
||||
|
||||
export function clampPercent(value = 0): number {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(0, Math.min(100, value));
|
||||
}
|
||||
|
||||
function defaultUnit(kind: UiMetricValue["kind"]): string {
|
||||
if (kind === "percent") return "%";
|
||||
if (kind === "temperature") return "C";
|
||||
if (kind === "latency") return "ms";
|
||||
return "";
|
||||
}
|
||||
|
||||
function inferPrecision(value: number): number {
|
||||
return Number.isInteger(value) ? 0 : 1;
|
||||
}
|
||||
|
||||
function formatBytes(value: number, precision: number): string {
|
||||
let size = value;
|
||||
let index = 0;
|
||||
while (size >= 1024 && index < byteUnits.length - 1) {
|
||||
size /= 1024;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return `${size.toFixed(precision)} ${byteUnits[index]}`;
|
||||
}
|
||||
23
src/lib/ui/index.ts
Normal file
23
src/lib/ui/index.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
export { default as DashboardFrame } from "./components/DashboardFrame.svelte";
|
||||
export { default as FooterCell } from "./components/FooterCell.svelte";
|
||||
export { default as IconGlyph } from "./components/IconGlyph.svelte";
|
||||
export { default as ModuleCard } from "./components/ModuleCard.svelte";
|
||||
export { default as Panel } from "./components/Panel.svelte";
|
||||
export { default as ServicePanel } from "./components/ServicePanel.svelte";
|
||||
export { default as ServiceRow } from "./components/ServiceRow.svelte";
|
||||
export { default as StatusBadge } from "./components/StatusBadge.svelte";
|
||||
export { default as StatusStrip } from "./components/StatusStrip.svelte";
|
||||
export { default as TelemetryCard } from "./components/TelemetryCard.svelte";
|
||||
export { default as TelemetryGrid } from "./components/TelemetryGrid.svelte";
|
||||
export { dashboardPreviewFixtures } from "./fixtures";
|
||||
export type {
|
||||
UiDashboardPreview,
|
||||
UiLink,
|
||||
UiMetricValue,
|
||||
UiModuleBlock,
|
||||
UiServiceGroup,
|
||||
UiServiceRow,
|
||||
UiSeverity,
|
||||
UiStatusItem,
|
||||
UiTelemetryCard,
|
||||
} from "./types";
|
||||
81
src/lib/ui/tokens.css
Normal file
81
src/lib/ui/tokens.css
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
:root {
|
||||
color-scheme: dark;
|
||||
--ui-color-canvas: #020302;
|
||||
--ui-color-surface: #060706;
|
||||
--ui-color-surface-raised: #0b0d0c;
|
||||
--ui-color-line: rgba(244, 244, 244, 0.16);
|
||||
--ui-color-line-strong: rgba(244, 244, 244, 0.32);
|
||||
--ui-color-text: #f3f4ed;
|
||||
--ui-color-muted: #8d948c;
|
||||
--ui-color-dim: #555b55;
|
||||
--ui-color-accent: #d7ff00;
|
||||
--ui-color-ok: #bfff00;
|
||||
--ui-color-warning: #ffb020;
|
||||
--ui-color-danger: #ff1744;
|
||||
--ui-color-stale: #82909a;
|
||||
--ui-color-unavailable: #65707a;
|
||||
--ui-font-mono: "IBM Plex Mono", "Roboto Mono", "SFMono-Regular", Consolas, monospace;
|
||||
--ui-font-display: "Teko", "IBM Plex Mono", "Roboto Mono", monospace;
|
||||
--ui-space-1: 0.25rem;
|
||||
--ui-space-2: 0.5rem;
|
||||
--ui-space-3: 0.75rem;
|
||||
--ui-space-4: 1rem;
|
||||
--ui-space-5: 1.25rem;
|
||||
--ui-space-6: 1.5rem;
|
||||
--ui-radius-none: 0;
|
||||
--ui-border: 1px solid var(--ui-color-line);
|
||||
--ui-border-strong: 1px solid var(--ui-color-line-strong);
|
||||
--ui-shadow-hard: 0 0 0 1px rgba(215, 255, 0, 0.12) inset;
|
||||
--ui-z-base: 0;
|
||||
--ui-z-panel: 1;
|
||||
--ui-z-overlay: 10;
|
||||
--ui-density-card-min: 9.75rem;
|
||||
--ui-focus-ring: 0 0 0 2px var(--ui-color-canvas), 0 0 0 4px var(--ui-color-accent);
|
||||
}
|
||||
|
||||
html {
|
||||
background: var(--ui-color-canvas);
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background:
|
||||
linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
|
||||
var(--ui-color-canvas);
|
||||
background-size: 48px 48px, 48px 48px, auto;
|
||||
color: var(--ui-color-text);
|
||||
font-family: var(--ui-font-mono);
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
[tabindex]:focus-visible {
|
||||
outline: 0;
|
||||
box-shadow: var(--ui-focus-ring);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
86
src/lib/ui/types.ts
Normal file
86
src/lib/ui/types.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
export type UiSeverity =
|
||||
| "neutral"
|
||||
| "ok"
|
||||
| "warning"
|
||||
| "danger"
|
||||
| "stale"
|
||||
| "unavailable"
|
||||
| "loading";
|
||||
|
||||
export type UiMetricKind =
|
||||
| "bytes"
|
||||
| "latency"
|
||||
| "number"
|
||||
| "percent"
|
||||
| "temperature"
|
||||
| "text";
|
||||
|
||||
export interface UiMetricValue {
|
||||
kind: UiMetricKind;
|
||||
value: number | string;
|
||||
unit?: string;
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
export interface UiLink {
|
||||
href: string;
|
||||
label?: string;
|
||||
external?: boolean;
|
||||
}
|
||||
|
||||
export interface UiTelemetryCard {
|
||||
id: string;
|
||||
label: string;
|
||||
value: UiMetricValue;
|
||||
detail?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
severity: UiSeverity;
|
||||
progress?: number;
|
||||
sparkline?: number[];
|
||||
}
|
||||
|
||||
export interface UiServiceRow {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
severity: UiSeverity;
|
||||
detail?: string;
|
||||
link?: UiLink;
|
||||
}
|
||||
|
||||
export interface UiServiceGroup {
|
||||
id: string;
|
||||
title: string;
|
||||
services: UiServiceRow[];
|
||||
summary?: UiStatusItem[];
|
||||
}
|
||||
|
||||
export interface UiStatusItem {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
severity?: UiSeverity;
|
||||
link?: UiLink;
|
||||
}
|
||||
|
||||
export interface UiModuleBlock {
|
||||
id: string;
|
||||
title?: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
detail?: string;
|
||||
icon?: string;
|
||||
severity?: UiSeverity;
|
||||
}
|
||||
|
||||
export interface UiDashboardPreview {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
eyebrow?: string;
|
||||
telemetry: UiTelemetryCard[];
|
||||
serviceGroups: UiServiceGroup[];
|
||||
modules: UiModuleBlock[];
|
||||
statusItems: UiStatusItem[];
|
||||
}
|
||||
|
|
@ -1,83 +1,124 @@
|
|||
<script lang="ts">
|
||||
import { DashboardFrame, dashboardPreviewFixtures } from "$lib/ui";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const fixtureKeys = ["runtime", "primary", "secondary"] as const;
|
||||
let selectedFixture: (typeof fixtureKeys)[number] = $state("runtime");
|
||||
let runtimeDashboard = $derived({
|
||||
...dashboardPreviewFixtures.primary,
|
||||
eyebrow: data.dashboard.schemaVersion || "Runtime",
|
||||
title: data.dashboard.title,
|
||||
subtitle: data.dashboard.subtitle,
|
||||
modules: [
|
||||
{
|
||||
id: "runtime-state",
|
||||
title: "Runtime",
|
||||
value: data.dashboard.status,
|
||||
detail: data.dashboard.message,
|
||||
icon: "mdi:database-check-outline",
|
||||
severity: "loading" as const,
|
||||
},
|
||||
],
|
||||
statusItems: [
|
||||
{
|
||||
id: "runtime-status",
|
||||
label: "Runtime",
|
||||
value: data.dashboard.status,
|
||||
severity: "loading" as const,
|
||||
},
|
||||
{
|
||||
id: "schema-version",
|
||||
label: "Schema",
|
||||
value: data.dashboard.schemaVersion || "pending",
|
||||
severity: "neutral" as const,
|
||||
},
|
||||
{
|
||||
id: "revision",
|
||||
label: "Revision",
|
||||
value: data.dashboard.currentRevisionId?.slice(0, 12) || "pending",
|
||||
severity: "stale" as const,
|
||||
},
|
||||
],
|
||||
});
|
||||
let dashboard = $derived(
|
||||
selectedFixture === "runtime"
|
||||
? runtimeDashboard
|
||||
: dashboardPreviewFixtures[selectedFixture],
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.dashboard.title} | Dimension Lab</title>
|
||||
<title>{dashboard.title}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Standalone SvelteKit runtime for a model-driven system overview dashboard."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<main class="shell" aria-labelledby="page-title">
|
||||
<section class="hero">
|
||||
<p class="eyebrow">{data.dashboard.subtitle}</p>
|
||||
<h1 id="page-title">{data.dashboard.title}</h1>
|
||||
<p class="message">{data.dashboard.message}</p>
|
||||
<div class="status" data-state={data.dashboard.status}>
|
||||
<span class="status__label">Runtime</span>
|
||||
<strong>{data.dashboard.status}</strong>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<div class="preview-shell">
|
||||
<div class="fixture-switcher" aria-label="Preview fixture">
|
||||
{#each fixtureKeys as key}
|
||||
<button
|
||||
type="button"
|
||||
class:active={selectedFixture === key}
|
||||
aria-pressed={selectedFixture === key}
|
||||
onclick={() => {
|
||||
selectedFixture = key;
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<DashboardFrame {dashboard} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shell {
|
||||
display: grid;
|
||||
.preview-shell {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding: clamp(1rem, 2vw, 2rem);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hero {
|
||||
align-self: center;
|
||||
max-width: 64rem;
|
||||
border: 1px solid rgba(244, 244, 244, 0.24);
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
padding: clamp(1.25rem, 4vw, 3rem);
|
||||
box-shadow: 0 0 0 1px rgba(207, 255, 0, 0.18) inset;
|
||||
.fixture-switcher {
|
||||
position: fixed;
|
||||
z-index: var(--ui-z-overlay);
|
||||
top: var(--ui-space-3);
|
||||
right: var(--ui-space-3);
|
||||
display: inline-grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 1px;
|
||||
border: var(--ui-border);
|
||||
background: var(--ui-color-line);
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
.status__label {
|
||||
margin: 0;
|
||||
color: #cfff00;
|
||||
font-size: 0.78rem;
|
||||
button {
|
||||
min-width: 5.5rem;
|
||||
min-height: 2rem;
|
||||
border: 0;
|
||||
background: rgba(2, 3, 2, 0.92);
|
||||
color: var(--ui-color-muted);
|
||||
cursor: pointer;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: clamp(2.6rem, 9vw, 7rem);
|
||||
line-height: 0.9;
|
||||
text-transform: uppercase;
|
||||
button.active {
|
||||
background: var(--ui-color-accent);
|
||||
color: var(--ui-color-canvas);
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 42rem;
|
||||
margin: 1.25rem 0 0;
|
||||
color: rgba(244, 244, 244, 0.72);
|
||||
font-size: clamp(0.9rem, 2vw, 1.08rem);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
margin-top: 2rem;
|
||||
border: 1px solid rgba(207, 255, 0, 0.6);
|
||||
padding: 0.7rem 0.85rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status strong {
|
||||
color: #f4f4f4;
|
||||
@media (max-width: 780px) {
|
||||
.fixture-switcher {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
right: auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
28
src/routes/page.test.ts
Normal file
28
src/routes/page.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { render } from "svelte/server";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import Page from "./+page.svelte";
|
||||
|
||||
describe("home page design-system preview", () => {
|
||||
test("renders the runtime dashboard data from the server load", () => {
|
||||
const { body } = render(Page, {
|
||||
props: {
|
||||
data: {
|
||||
dashboard: {
|
||||
title: "Runtime Surface",
|
||||
subtitle: "Loaded from persistence",
|
||||
status: "building",
|
||||
message: "Runtime model is available.",
|
||||
schemaVersion: "dashboard.v1",
|
||||
currentRevisionId: "revision-1234567890",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(body).toContain("Runtime Surface");
|
||||
expect(body).toContain("Loaded from persistence");
|
||||
expect(body).toContain("revision");
|
||||
expect(body).toContain("primary");
|
||||
expect(body).toContain("secondary");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue