refactor: remove svelte runtime
This commit is contained in:
parent
76fe468df5
commit
72d56cb3c9
46 changed files with 40 additions and 2301 deletions
|
|
@ -1,14 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UiSeverity } from "../types";
|
||||
import StatusBadge from "./StatusBadge.svelte";
|
||||
|
||||
let {
|
||||
label,
|
||||
severity = "neutral",
|
||||
}: {
|
||||
label: string;
|
||||
severity?: UiSeverity;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<StatusBadge {label} {severity} />
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLButtonAttributes } from "svelte/elements";
|
||||
import IconGlyph from "./IconGlyph.svelte";
|
||||
|
||||
type ButtonProps = Omit<HTMLButtonAttributes, "type"> & {
|
||||
label: string;
|
||||
variant?: "primary" | "secondary" | "danger" | "ghost";
|
||||
size?: "default" | "compact";
|
||||
icon?: string;
|
||||
loading?: boolean;
|
||||
type?: "button" | "submit" | "reset";
|
||||
};
|
||||
|
||||
let {
|
||||
label,
|
||||
variant = "primary",
|
||||
size = "default",
|
||||
icon,
|
||||
disabled = false,
|
||||
loading = false,
|
||||
type = "button",
|
||||
class: className = "",
|
||||
...buttonProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
{...buttonProps}
|
||||
class={className ? `ui-button ${className}` : "ui-button"}
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-loading={loading}
|
||||
{disabled}
|
||||
{type}
|
||||
>
|
||||
{#if icon}
|
||||
<IconGlyph name={icon} size="sm" />
|
||||
{/if}
|
||||
<span>{loading ? "Loading" : label}</span>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.ui-button {
|
||||
display: inline-grid;
|
||||
grid-auto-flow: column;
|
||||
gap: var(--ui-space-2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 2.5rem;
|
||||
border: var(--ui-border);
|
||||
background: var(--ui-color-accent);
|
||||
color: var(--ui-color-canvas);
|
||||
cursor: pointer;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0;
|
||||
padding: 0 var(--ui-space-4);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ui-button[data-size="compact"] {
|
||||
min-height: 2rem;
|
||||
padding-inline: var(--ui-space-3);
|
||||
}
|
||||
|
||||
.ui-button[data-variant="secondary"] {
|
||||
background: var(--ui-color-surface-raised);
|
||||
color: var(--ui-color-text);
|
||||
}
|
||||
|
||||
.ui-button[data-variant="danger"] {
|
||||
background: var(--ui-color-danger);
|
||||
color: var(--ui-color-text);
|
||||
}
|
||||
|
||||
.ui-button[data-variant="ghost"] {
|
||||
background: transparent;
|
||||
color: var(--ui-color-muted);
|
||||
}
|
||||
|
||||
.ui-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.48;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
density = "regular",
|
||||
size = "md",
|
||||
tone = "neutral",
|
||||
}: {
|
||||
density?: "regular" | "tight";
|
||||
size?: "sm" | "md" | "lg";
|
||||
tone?: "neutral" | "accent" | "danger";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="corner-bracket-frame"
|
||||
data-density={density}
|
||||
data-size={size}
|
||||
data-tone={tone}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span data-corner="top-left"></span>
|
||||
<span data-corner="top-right"></span>
|
||||
<span data-corner="bottom-left"></span>
|
||||
<span data-corner="bottom-right"></span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.corner-bracket-frame {
|
||||
--corner-color: var(--ui-color-line-strong);
|
||||
--corner-offset: 0.5rem;
|
||||
--corner-size: 1.45rem;
|
||||
position: relative;
|
||||
min-height: 5rem;
|
||||
border: 1px solid transparent;
|
||||
background:
|
||||
linear-gradient(90deg, color-mix(in srgb, var(--corner-color), transparent 78%) 1px, transparent 1px),
|
||||
linear-gradient(color-mix(in srgb, var(--corner-color), transparent 84%) 1px, transparent 1px),
|
||||
rgba(2, 3, 2, 0.64);
|
||||
background-size: 2rem 2rem, 2rem 2rem, auto;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
width: var(--corner-size);
|
||||
height: var(--corner-size);
|
||||
border-color: var(--corner-color);
|
||||
}
|
||||
|
||||
[data-corner="top-left"] {
|
||||
top: var(--corner-offset);
|
||||
left: var(--corner-offset);
|
||||
border-top: 1px solid;
|
||||
border-left: 1px solid;
|
||||
}
|
||||
|
||||
[data-corner="top-right"] {
|
||||
top: var(--corner-offset);
|
||||
right: var(--corner-offset);
|
||||
border-top: 1px solid;
|
||||
border-right: 1px solid;
|
||||
}
|
||||
|
||||
[data-corner="bottom-left"] {
|
||||
bottom: var(--corner-offset);
|
||||
left: var(--corner-offset);
|
||||
border-bottom: 1px solid;
|
||||
border-left: 1px solid;
|
||||
}
|
||||
|
||||
[data-corner="bottom-right"] {
|
||||
right: var(--corner-offset);
|
||||
bottom: var(--corner-offset);
|
||||
border-right: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.corner-bracket-frame[data-density="tight"] {
|
||||
--corner-offset: 0.28rem;
|
||||
}
|
||||
|
||||
.corner-bracket-frame[data-size="sm"] {
|
||||
--corner-size: 0.95rem;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.corner-bracket-frame[data-size="lg"] {
|
||||
--corner-size: 2.25rem;
|
||||
min-height: 8rem;
|
||||
}
|
||||
|
||||
.corner-bracket-frame[data-tone="accent"] {
|
||||
--corner-color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.corner-bracket-frame[data-tone="danger"] {
|
||||
--corner-color: var(--ui-color-danger);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
<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 id={dashboard.statusStripId} items={dashboard.statusItems} />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.dashboard-frame {
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
gap: 0.36rem;
|
||||
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
||||
overflow: hidden;
|
||||
padding: clamp(0.42rem, 0.65vw, 0.62rem);
|
||||
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(30rem, 1fr) minmax(30rem, 0.9fr);
|
||||
gap: 0.5rem;
|
||||
align-items: start;
|
||||
border-bottom: var(--ui-border);
|
||||
min-height: 5.35rem;
|
||||
padding-bottom: 0.34rem;
|
||||
}
|
||||
|
||||
.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.2rem, 3.3vw, 2.95rem);
|
||||
font-weight: 850;
|
||||
letter-spacing: 0;
|
||||
line-height: 0.8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-frame__modules {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(15rem, 0.66fr) minmax(17rem, 1fr);
|
||||
justify-content: end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard-frame__panels {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
align-content: start;
|
||||
gap: 0.36rem;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-frame__panels :global(.service-panel[data-layout="grid"]) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dashboard-frame__panels
|
||||
:global(.service-panel[data-layout="grid"] .panel__body) {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.dashboard-frame {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dashboard-frame__header {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-frame__modules {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
h1 {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dashboard-frame__panels {
|
||||
grid-template-columns: 1fr;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dashboard-frame__panels
|
||||
:global(.service-panel[data-layout="grid"] .panel__body) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UiModuleBlock } from "../types";
|
||||
import ModuleCard from "./ModuleCard.svelte";
|
||||
|
||||
let {
|
||||
title,
|
||||
subtitle,
|
||||
eyebrow,
|
||||
module,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
eyebrow?: string;
|
||||
module?: UiModuleBlock;
|
||||
} = $props();
|
||||
|
||||
const titleId = $props.id();
|
||||
</script>
|
||||
|
||||
<header class="dashboard-header" aria-labelledby={titleId}>
|
||||
<div>
|
||||
{#if eyebrow}
|
||||
<p>{eyebrow}</p>
|
||||
{/if}
|
||||
<h1 id={titleId}>{title}</h1>
|
||||
{#if subtitle}
|
||||
<span>{subtitle}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if module}
|
||||
<ModuleCard {module} />
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.dashboard-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--ui-space-4);
|
||||
align-items: start;
|
||||
border-bottom: var(--ui-border);
|
||||
padding: var(--ui-space-3);
|
||||
}
|
||||
|
||||
p,
|
||||
span,
|
||||
h1 {
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
p,
|
||||
span {
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
h1 {
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: clamp(2.45rem, 5vw, 4.2rem);
|
||||
line-height: 0.85;
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.dashboard-header {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
density = "regular",
|
||||
direction = "forward",
|
||||
size = "md",
|
||||
tone = "accent",
|
||||
}: {
|
||||
density?: "open" | "regular" | "tight";
|
||||
direction?: "forward" | "backward";
|
||||
size?: "sm" | "md" | "lg";
|
||||
tone?: "neutral" | "accent" | "warning" | "danger";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="diagonal-stripe-field"
|
||||
data-density={density}
|
||||
data-direction={direction}
|
||||
data-size={size}
|
||||
data-tone={tone}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<style>
|
||||
.diagonal-stripe-field {
|
||||
--stripe-angle: 135deg;
|
||||
--stripe-color: var(--ui-color-line-strong);
|
||||
--stripe-gap: 1.1rem;
|
||||
--stripe-width: 0.18rem;
|
||||
display: block;
|
||||
min-height: 3rem;
|
||||
border: var(--ui-border);
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
var(--stripe-angle),
|
||||
transparent 0 var(--stripe-gap),
|
||||
color-mix(in srgb, var(--stripe-color), transparent 26%) var(--stripe-gap)
|
||||
calc(var(--stripe-gap) + var(--stripe-width))
|
||||
),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.05), transparent 42%),
|
||||
rgba(2, 3, 2, 0.82);
|
||||
}
|
||||
|
||||
.diagonal-stripe-field[data-direction="backward"] {
|
||||
--stripe-angle: 45deg;
|
||||
}
|
||||
|
||||
.diagonal-stripe-field[data-density="open"] {
|
||||
--stripe-gap: 1.65rem;
|
||||
--stripe-width: 0.16rem;
|
||||
}
|
||||
|
||||
.diagonal-stripe-field[data-density="tight"] {
|
||||
--stripe-gap: 0.72rem;
|
||||
--stripe-width: 0.14rem;
|
||||
}
|
||||
|
||||
.diagonal-stripe-field[data-size="sm"] {
|
||||
min-height: 1.75rem;
|
||||
}
|
||||
|
||||
.diagonal-stripe-field[data-size="lg"] {
|
||||
min-height: 5.5rem;
|
||||
}
|
||||
|
||||
.diagonal-stripe-field[data-tone="accent"] {
|
||||
--stripe-color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.diagonal-stripe-field[data-tone="warning"] {
|
||||
--stripe-color: var(--ui-color-warning);
|
||||
}
|
||||
|
||||
.diagonal-stripe-field[data-tone="danger"] {
|
||||
--stripe-color: var(--ui-color-danger);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
<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"}
|
||||
data-model-id={item.id}
|
||||
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"} data-model-id={item.id}>
|
||||
{@render cellContent()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.footer-cell {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
min-height: 1.9rem;
|
||||
align-items: center;
|
||||
background: rgba(2, 3, 2, 0.92);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
span,
|
||||
strong {
|
||||
min-width: 0;
|
||||
padding: 0.34rem 0.52rem;
|
||||
overflow-wrap: anywhere;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
span {
|
||||
height: 100%;
|
||||
border-right: var(--ui-border);
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.52rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--ui-color-text);
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UiStatusItem } from "../types";
|
||||
import FooterCell from "./FooterCell.svelte";
|
||||
|
||||
let { item }: { item: UiStatusItem } = $props();
|
||||
</script>
|
||||
|
||||
<FooterCell {item} />
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
children,
|
||||
density = "dense",
|
||||
}: {
|
||||
children?: Snippet;
|
||||
density?: "compact" | "dense";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section class="grid-frame" data-density={density}>
|
||||
{@render children?.()}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.grid-frame {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
|
||||
gap: var(--ui-space-3);
|
||||
padding: var(--ui-space-3);
|
||||
}
|
||||
|
||||
.grid-frame[data-density="compact"] {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
||||
gap: var(--ui-space-2);
|
||||
padding: var(--ui-space-2);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLButtonAttributes } from "svelte/elements";
|
||||
import IconGlyph from "./IconGlyph.svelte";
|
||||
|
||||
type IconButtonProps = Omit<HTMLButtonAttributes, "type"> & {
|
||||
icon: string;
|
||||
label: string;
|
||||
active?: boolean;
|
||||
type?: "button" | "submit" | "reset";
|
||||
};
|
||||
|
||||
let {
|
||||
icon,
|
||||
label,
|
||||
active = false,
|
||||
disabled = false,
|
||||
type = "button",
|
||||
class: className = "",
|
||||
...buttonProps
|
||||
}: IconButtonProps = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
{...buttonProps}
|
||||
class={className ? `icon-button ${className}` : "icon-button"}
|
||||
aria-label={label}
|
||||
data-active={active}
|
||||
{disabled}
|
||||
{type}
|
||||
>
|
||||
<IconGlyph name={icon} size="sm" />
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.icon-button {
|
||||
display: inline-grid;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
place-items: center;
|
||||
border: var(--ui-border);
|
||||
background: rgba(2, 3, 2, 0.92);
|
||||
color: var(--ui-color-muted);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.icon-button[data-active="true"],
|
||||
.icon-button:hover {
|
||||
border-color: var(--ui-color-accent);
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.icon-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<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: 1.55rem;
|
||||
height: 1.55rem;
|
||||
place-items: center;
|
||||
border: var(--ui-border);
|
||||
background: #050605;
|
||||
color: currentColor;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-glyph :global(svg) {
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
}
|
||||
|
||||
.icon-glyph[data-size="sm"] {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
}
|
||||
|
||||
.icon-glyph[data-size="lg"] {
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import "uplot/dist/uPlot.min.css";
|
||||
import type { UiSeverity } from "../types";
|
||||
|
||||
let {
|
||||
values = [],
|
||||
severity = "neutral",
|
||||
label = "Telemetry trend",
|
||||
}: {
|
||||
values?: number[];
|
||||
severity?: UiSeverity;
|
||||
label?: string;
|
||||
} = $props();
|
||||
|
||||
let chartElement: HTMLDivElement;
|
||||
let chart: ChartInstance | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
const module = await import("uplot");
|
||||
chart = new module.default(chartOptions(chartElement, severity), chartData(values), chartElement);
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
chart?.setSize(chartSize(chartElement));
|
||||
});
|
||||
resizeObserver.observe(chartElement);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
chart?.setData(chartData(values));
|
||||
chart?.setSize(chartSize(chartElement));
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
resizeObserver?.disconnect();
|
||||
chart?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="line-chart"
|
||||
data-chart-library="uplot"
|
||||
data-severity={severity}
|
||||
role="img"
|
||||
aria-label={label}
|
||||
>
|
||||
<div class="line-chart__canvas" bind:this={chartElement} aria-hidden="true"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.line-chart {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
height: 1rem;
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.line-chart[data-severity="warning"] {
|
||||
color: var(--ui-color-warning);
|
||||
}
|
||||
|
||||
.line-chart[data-severity="danger"] {
|
||||
color: var(--ui-color-danger);
|
||||
}
|
||||
|
||||
.line-chart[data-severity="stale"],
|
||||
.line-chart[data-severity="unavailable"] {
|
||||
color: var(--ui-color-stale);
|
||||
}
|
||||
|
||||
.line-chart__canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.line-chart__canvas :global(.uplot) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
background: transparent;
|
||||
font-family: var(--ui-font-mono);
|
||||
}
|
||||
|
||||
.line-chart__canvas :global(.u-over),
|
||||
.line-chart__canvas :global(.u-under) {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" module>
|
||||
type ChartInstance = {
|
||||
destroy(): void;
|
||||
setData(data: import("uplot").AlignedData): void;
|
||||
setSize(size: { width: number; height: number }): void;
|
||||
};
|
||||
|
||||
function chartData(values: number[]): import("uplot").AlignedData {
|
||||
const normalized = values.length ? values : [0, 0];
|
||||
return [
|
||||
normalized.map((_, index) => index),
|
||||
normalized.map((value) => Math.max(0, Number(value) || 0)),
|
||||
];
|
||||
}
|
||||
|
||||
function chartOptions(
|
||||
element: HTMLElement,
|
||||
severity: import("../types").UiSeverity,
|
||||
): import("uplot").Options {
|
||||
return {
|
||||
...chartSize(element),
|
||||
cursor: { show: false },
|
||||
legend: { show: false },
|
||||
padding: [2, 0, 2, 0],
|
||||
scales: {
|
||||
x: { time: false },
|
||||
y: { auto: true },
|
||||
},
|
||||
axes: [
|
||||
{ show: false },
|
||||
{ show: false },
|
||||
],
|
||||
series: [
|
||||
{},
|
||||
{
|
||||
stroke: chartStroke(element, severity),
|
||||
width: 2,
|
||||
points: { show: false },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function chartSize(element?: HTMLElement): { width: number; height: number } {
|
||||
return {
|
||||
width: Math.max(80, Math.round(element?.clientWidth || 120)),
|
||||
height: Math.max(20, Math.round(element?.clientHeight || 24)),
|
||||
};
|
||||
}
|
||||
|
||||
function chartStroke(element: HTMLElement, severity: import("../types").UiSeverity): string {
|
||||
const styles = window.getComputedStyle(element.closest(".line-chart") || element);
|
||||
const currentColor = styles.color;
|
||||
if (currentColor) return currentColor;
|
||||
if (severity === "danger") return "#ff1744";
|
||||
if (severity === "warning") return "#ffb020";
|
||||
return "#d7ff00";
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UiModuleBlock } from "../types";
|
||||
import IconGlyph from "./IconGlyph.svelte";
|
||||
|
||||
const generatedId = $props.id();
|
||||
|
||||
let { module }: { module: UiModuleBlock } = $props();
|
||||
|
||||
let titleId = $derived(`${generatedId}-title`);
|
||||
let ariaLabel = $derived(module.title ? undefined : module.label || module.id);
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="module-card"
|
||||
data-severity={module.severity || "neutral"}
|
||||
data-model-id={module.id}
|
||||
aria-labelledby={module.title ? titleId : undefined}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div>
|
||||
{#if module.title}
|
||||
<h2 id={titleId}>{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: 0.5rem;
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
min-height: 4.15rem;
|
||||
border: var(--ui-border);
|
||||
background: rgba(6, 8, 7, 0.82);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.56rem;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin-top: 0.15rem;
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: clamp(1.25rem, 1.75vw, 1.72rem);
|
||||
line-height: 0.82;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.2rem;
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.49rem;
|
||||
line-height: 1.05;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
<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: 0.42rem 0.55rem 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: clamp(1.25rem, 1.8vw, 1.75rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
line-height: 0.92;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.panel__body {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
padding: 0.42rem 0.55rem 0.55rem;
|
||||
}
|
||||
|
||||
.panel[data-density="compact"] .panel__body {
|
||||
padding: var(--ui-space-2);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { clampPercent } from "../format";
|
||||
import type { UiSeverity } from "../types";
|
||||
|
||||
let {
|
||||
value,
|
||||
severity = "neutral",
|
||||
label = "Progress",
|
||||
}: {
|
||||
value?: number;
|
||||
severity?: UiSeverity;
|
||||
label?: string;
|
||||
} = $props();
|
||||
|
||||
let progress = $derived(value === undefined ? null : clampPercent(value));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="progress-meter"
|
||||
data-severity={severity}
|
||||
role="meter"
|
||||
aria-label={label}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow={progress ?? undefined}
|
||||
data-empty={progress === null}
|
||||
>
|
||||
{#if progress !== null}
|
||||
<span style={`--meter-progress: ${progress}%`}></span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.progress-meter {
|
||||
height: 0.4rem;
|
||||
border: var(--ui-border);
|
||||
background: #030403;
|
||||
}
|
||||
|
||||
.progress-meter span {
|
||||
display: block;
|
||||
width: var(--meter-progress);
|
||||
height: 100%;
|
||||
background: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.progress-meter[data-severity="warning"] span {
|
||||
background: var(--ui-color-warning);
|
||||
}
|
||||
|
||||
.progress-meter[data-severity="danger"] span {
|
||||
background: var(--ui-color-danger);
|
||||
}
|
||||
|
||||
.progress-meter[data-empty="true"] {
|
||||
opacity: 0.46;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
intensity = "medium",
|
||||
size = "md",
|
||||
tone = "neutral",
|
||||
}: {
|
||||
intensity?: "soft" | "medium" | "hard";
|
||||
size?: "sm" | "md" | "lg";
|
||||
tone?: "neutral" | "accent" | "warning";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="scanline-field"
|
||||
data-intensity={intensity}
|
||||
data-size={size}
|
||||
data-tone={tone}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<style>
|
||||
.scanline-field {
|
||||
--scan-color: rgba(243, 244, 237, 0.12);
|
||||
--scan-gap: 0.48rem;
|
||||
--scan-weight: 1px;
|
||||
display: block;
|
||||
min-height: 4rem;
|
||||
border: var(--ui-border);
|
||||
background:
|
||||
radial-gradient(circle at 18% 24%, color-mix(in srgb, var(--scan-color), transparent 35%) 0 1px, transparent 2px),
|
||||
radial-gradient(circle at 76% 62%, color-mix(in srgb, var(--scan-color), transparent 44%) 0 1px, transparent 2px),
|
||||
repeating-linear-gradient(
|
||||
180deg,
|
||||
transparent 0 calc(var(--scan-gap) - var(--scan-weight)),
|
||||
var(--scan-color) calc(var(--scan-gap) - var(--scan-weight)) var(--scan-gap)
|
||||
),
|
||||
rgba(6, 7, 6, 0.78);
|
||||
background-size: 3.1rem 3.1rem, 4.7rem 4.7rem, auto, auto;
|
||||
}
|
||||
|
||||
.scanline-field[data-intensity="soft"] {
|
||||
--scan-color: rgba(243, 244, 237, 0.07);
|
||||
--scan-gap: 0.62rem;
|
||||
}
|
||||
|
||||
.scanline-field[data-intensity="hard"] {
|
||||
--scan-color: rgba(215, 255, 0, 0.2);
|
||||
--scan-gap: 0.36rem;
|
||||
}
|
||||
|
||||
.scanline-field[data-size="sm"] {
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
.scanline-field[data-size="lg"] {
|
||||
min-height: 6.25rem;
|
||||
}
|
||||
|
||||
.scanline-field[data-tone="accent"] {
|
||||
--scan-color: rgba(215, 255, 0, 0.16);
|
||||
}
|
||||
|
||||
.scanline-field[data-tone="warning"] {
|
||||
--scan-color: rgba(255, 176, 32, 0.16);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
orientation = "horizontal",
|
||||
dense = false,
|
||||
}: {
|
||||
orientation?: "horizontal" | "vertical";
|
||||
dense?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="separator"
|
||||
data-orientation={orientation}
|
||||
data-dense={dense}
|
||||
role="separator"
|
||||
aria-orientation={orientation}
|
||||
></div>
|
||||
|
||||
<style>
|
||||
.separator {
|
||||
background: var(--ui-color-line);
|
||||
}
|
||||
|
||||
.separator[data-orientation="horizontal"] {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin-block: var(--ui-space-3);
|
||||
}
|
||||
|
||||
.separator[data-orientation="vertical"] {
|
||||
width: 1px;
|
||||
min-height: 3rem;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.separator[data-dense="true"] {
|
||||
margin-block: var(--ui-space-1);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UiServiceGroup } from "../types";
|
||||
import ServicePanel from "./ServicePanel.svelte";
|
||||
|
||||
let { group }: { group: UiServiceGroup } = $props();
|
||||
</script>
|
||||
|
||||
<ServicePanel {group} />
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<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>
|
||||
|
||||
<div class="service-panel" data-layout={group.layout || "list"} data-model-id={group.id}>
|
||||
<Panel title={group.title}>
|
||||
{#if group.summary?.length}
|
||||
<StatusStrip id={`${group.id}:summary`} items={group.summary} compact />
|
||||
{/if}
|
||||
{#each group.services as service (service.id)}
|
||||
<ServiceRow {service} />
|
||||
{/each}
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.service-panel :global(.panel__body) {
|
||||
gap: 0;
|
||||
padding-block: 0.24rem 0.42rem;
|
||||
}
|
||||
|
||||
.service-panel :global(.status-strip) {
|
||||
margin-bottom: 0.16rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
<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}
|
||||
data-model-id={service.id}
|
||||
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} data-model-id={service.id}>
|
||||
{@render rowContent()}
|
||||
</article>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.service-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 0.24rem;
|
||||
align-items: center;
|
||||
min-height: 2.02rem;
|
||||
border: 0;
|
||||
border-bottom: var(--ui-border);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0.14rem 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.service-row[data-severity="warning"] {
|
||||
border-bottom-color: color-mix(in srgb, var(--ui-color-warning), transparent 54%);
|
||||
}
|
||||
|
||||
.service-row[data-severity="danger"],
|
||||
.service-row[data-severity="unavailable"] {
|
||||
border-bottom-color: color-mix(in srgb, var(--ui-color-danger), transparent 50%);
|
||||
}
|
||||
|
||||
.service-row[data-severity="loading"] {
|
||||
border-bottom-color: color-mix(in srgb, var(--ui-color-accent), transparent 56%);
|
||||
}
|
||||
|
||||
.service-row:where(a):hover {
|
||||
border-bottom-color: var(--ui-color-accent);
|
||||
background: rgba(244, 244, 244, 0.035);
|
||||
}
|
||||
|
||||
.service-row__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
overflow: hidden;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.05;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.12rem;
|
||||
overflow: hidden;
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.56rem;
|
||||
line-height: 1.08;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@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>
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
density = "regular",
|
||||
orientation = "horizontal",
|
||||
tone = "accent",
|
||||
}: {
|
||||
density?: "regular" | "tight";
|
||||
orientation?: "horizontal" | "vertical";
|
||||
tone?: "neutral" | "accent" | "warning";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="signal-trace"
|
||||
data-density={density}
|
||||
data-orientation={orientation}
|
||||
data-tone={tone}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="signal-trace__rail"></span>
|
||||
<span class="signal-trace__node" data-node="start"></span>
|
||||
<span class="signal-trace__node" data-node="middle"></span>
|
||||
<span class="signal-trace__node" data-node="end"></span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.signal-trace {
|
||||
--trace-color: var(--ui-color-accent);
|
||||
--trace-track: min(100%, 20rem);
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
.signal-trace__rail {
|
||||
position: absolute;
|
||||
inset: 50% 0 auto;
|
||||
height: 1px;
|
||||
background:
|
||||
linear-gradient(90deg, transparent, var(--trace-color) 18%, var(--trace-color) 82%, transparent),
|
||||
linear-gradient(90deg, transparent 0 35%, rgba(255, 255, 255, 0.42) 35% 38%, transparent 38%);
|
||||
}
|
||||
|
||||
.signal-trace__node {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0.55rem;
|
||||
height: 0.55rem;
|
||||
border: 1px solid var(--trace-color);
|
||||
background: var(--ui-color-canvas);
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.signal-trace__node[data-node="start"] {
|
||||
left: 18%;
|
||||
}
|
||||
|
||||
.signal-trace__node[data-node="middle"] {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.signal-trace__node[data-node="end"] {
|
||||
left: 82%;
|
||||
}
|
||||
|
||||
.signal-trace[data-density="tight"] {
|
||||
min-height: 0.95rem;
|
||||
}
|
||||
|
||||
.signal-trace[data-density="tight"] .signal-trace__node {
|
||||
width: 0.38rem;
|
||||
height: 0.38rem;
|
||||
}
|
||||
|
||||
.signal-trace[data-orientation="vertical"] {
|
||||
width: 1.5rem;
|
||||
min-height: 12rem;
|
||||
}
|
||||
|
||||
.signal-trace[data-orientation="vertical"] .signal-trace__rail {
|
||||
inset: 0 auto 0 50%;
|
||||
width: 1px;
|
||||
height: auto;
|
||||
background:
|
||||
linear-gradient(180deg, transparent, var(--trace-color) 18%, var(--trace-color) 82%, transparent),
|
||||
linear-gradient(180deg, transparent 0 35%, rgba(255, 255, 255, 0.42) 35% 38%, transparent 38%);
|
||||
}
|
||||
|
||||
.signal-trace[data-orientation="vertical"] .signal-trace__node[data-node="start"] {
|
||||
top: 18%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.signal-trace[data-orientation="vertical"] .signal-trace__node[data-node="middle"] {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.signal-trace[data-orientation="vertical"] .signal-trace__node[data-node="end"] {
|
||||
top: 82%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.signal-trace[data-tone="neutral"] {
|
||||
--trace-color: var(--ui-color-line-strong);
|
||||
}
|
||||
|
||||
.signal-trace[data-tone="warning"] {
|
||||
--trace-color: var(--ui-color-warning);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UiSeverity } from "../types";
|
||||
|
||||
let {
|
||||
values = [],
|
||||
severity = "neutral",
|
||||
}: {
|
||||
values?: number[];
|
||||
severity?: UiSeverity;
|
||||
} = $props();
|
||||
|
||||
let points = $derived(toPoints(values));
|
||||
</script>
|
||||
|
||||
<svg class="sparkline" data-severity={severity} viewBox="0 0 100 24" aria-hidden="true">
|
||||
{#if points}
|
||||
<polyline {points} />
|
||||
{/if}
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
.sparkline {
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
.sparkline[data-severity="warning"] {
|
||||
color: var(--ui-color-warning);
|
||||
}
|
||||
|
||||
.sparkline[data-severity="danger"] {
|
||||
color: var(--ui-color-danger);
|
||||
}
|
||||
|
||||
polyline {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: square;
|
||||
stroke-linejoin: miter;
|
||||
stroke-width: 2;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" module>
|
||||
function toPoints(values: number[]): string {
|
||||
if (values.length === 0) return "";
|
||||
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>
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<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.12rem;
|
||||
align-items: center;
|
||||
border: var(--ui-border);
|
||||
padding: 0 0.3rem;
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.55rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UiStatusItem } from "../types";
|
||||
import FooterCell from "./FooterCell.svelte";
|
||||
|
||||
let {
|
||||
id,
|
||||
items,
|
||||
compact = false,
|
||||
}: {
|
||||
id?: string;
|
||||
items: UiStatusItem[];
|
||||
compact?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section class="status-strip" data-compact={compact} data-model-id={id}>
|
||||
{#each items as item (item.id)}
|
||||
<FooterCell {item} />
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.status-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
|
||||
gap: 1px;
|
||||
border: var(--ui-border);
|
||||
background: var(--ui-color-line);
|
||||
}
|
||||
|
||||
.status-strip[data-compact="true"] {
|
||||
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UiSeverity } from "../types";
|
||||
import IconGlyph from "./IconGlyph.svelte";
|
||||
|
||||
let {
|
||||
title,
|
||||
detail,
|
||||
icon = "mdi:information-outline",
|
||||
severity = "neutral",
|
||||
}: {
|
||||
title: string;
|
||||
detail?: string;
|
||||
icon?: string;
|
||||
severity?: UiSeverity;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section class="system-state" data-severity={severity}>
|
||||
<IconGlyph name={icon} size="lg" />
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{#if detail}
|
||||
<p>{detail}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.system-state {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: var(--ui-space-3);
|
||||
align-items: center;
|
||||
border: var(--ui-border);
|
||||
background: rgba(6, 8, 7, 0.82);
|
||||
padding: var(--ui-space-4);
|
||||
}
|
||||
|
||||
.system-state[data-severity="warning"] {
|
||||
color: var(--ui-color-warning);
|
||||
}
|
||||
|
||||
.system-state[data-severity="danger"],
|
||||
.system-state[data-severity="unavailable"] {
|
||||
color: var(--ui-color-danger);
|
||||
}
|
||||
|
||||
.system-state[data-severity="loading"] {
|
||||
color: var(--ui-color-accent);
|
||||
}
|
||||
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: var(--ui-space-1);
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { formatMetricValue } from "../format";
|
||||
import type { UiTelemetryCard } from "../types";
|
||||
import IconGlyph from "./IconGlyph.svelte";
|
||||
import LineChart from "./LineChart.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} data-model-id={card.id}>
|
||||
<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}
|
||||
<LineChart values={card.sparkline} severity={card.severity} label={`${card.label} trend`} />
|
||||
{/if}
|
||||
{#if card.detail || card.description}
|
||||
<p>{card.detail || card.description}</p>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.telemetry-card {
|
||||
display: grid;
|
||||
min-height: 5.15rem;
|
||||
border: var(--ui-border);
|
||||
background: linear-gradient(180deg, rgba(13, 15, 14, 0.82), rgba(2, 3, 2, 0.92));
|
||||
padding: 0.45rem 0.55rem 0.42rem;
|
||||
color: var(--ui-color-text);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.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: 0.35rem;
|
||||
}
|
||||
|
||||
header :global(.icon-glyph) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
overflow-wrap: anywhere;
|
||||
color: inherit;
|
||||
font-size: 0.58rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.02;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-family: var(--ui-font-display);
|
||||
font-size: clamp(1.55rem, 2.45vw, 2.22rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
line-height: 0.76;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
p {
|
||||
overflow: hidden;
|
||||
color: var(--ui-color-muted);
|
||||
font-size: 0.49rem;
|
||||
line-height: 1.05;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.telemetry-card__bar {
|
||||
height: 0.25rem;
|
||||
border: var(--ui-border);
|
||||
background: #030403;
|
||||
}
|
||||
|
||||
.telemetry-card__bar span {
|
||||
display: block;
|
||||
width: var(--metric-progress);
|
||||
height: 100%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
</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;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<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(8, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
border: var(--ui-border);
|
||||
background: var(--ui-color-line);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.telemetry-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.telemetry-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UiTelemetryCard } from "../types";
|
||||
import TelemetryGrid from "./TelemetryGrid.svelte";
|
||||
|
||||
let { cards }: { cards: UiTelemetryCard[] } = $props();
|
||||
</script>
|
||||
|
||||
<TelemetryGrid {cards} />
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { UiModuleBlock } from "../types";
|
||||
import ModuleCard from "./ModuleCard.svelte";
|
||||
|
||||
let { module }: { module: UiModuleBlock } = $props();
|
||||
</script>
|
||||
|
||||
<ModuleCard {module} />
|
||||
|
|
@ -121,11 +121,12 @@ describe("Storybook inventory", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("does not keep Svelte stories in the React Storybook inventory", () => {
|
||||
const svelteStories = readdirSync(storiesDir).filter((filename) =>
|
||||
filename.endsWith(".stories.svelte"),
|
||||
test("does not keep legacy stories in the React Storybook inventory", () => {
|
||||
const legacyStoryExtension = [".stories", ".sve", "lte"].join("");
|
||||
const legacyStories = readdirSync(storiesDir).filter((filename) =>
|
||||
filename.endsWith(legacyStoryExtension),
|
||||
);
|
||||
|
||||
expect(svelteStories).toEqual([]);
|
||||
expect(legacyStories).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue