Files
JE-Skin/src/lib/components/HudPanel.svelte
2026-04-03 00:47:36 +08:00

1003 lines
25 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type {
ConnectionState,
HudConfigLink,
HudNoticeTone,
LocaleCode,
WindowControlAction
} from "$lib/types/hud";
export let appName = "";
export let suiteName = "";
export let controlAreaLabel = "";
export let locale: LocaleCode = "zh-CN";
export let connectionState: ConnectionState = "offline";
export let connectionLabel = "";
export let connectedLabel = "";
export let connectingLabel = "";
export let disconnectedLabel = "";
export let deviceLabel = "";
export let deviceValue = "";
export let sampleRateLabel = "";
export let sampleRateValue = "";
export let channelsLabel = "";
export let channelsValue = "";
export let serialPortLabel = "";
export let serialPortValue = "";
export let serialPortOptions: string[] = [];
export let refreshPortsLabel = "";
export let configLinksLabel = "";
export let configLinks: HudConfigLink[] = [];
export let connectActionLabel = "";
export let disconnectActionLabel = "";
export let exportActionLabel = "";
export let exportingActionLabel = "";
export let importActionLabel = "";
export let connectionNotice = "";
export let connectionNoticeTone: HudNoticeTone = "info";
export let isRefreshingPorts = false;
export let isConnectDisabled = false;
export let isExporting = false;
export let isExportDisabled = false;
export let isWindowMaximized = false;
const dispatch = createEventDispatcher<{
windowcontrol: WindowControlAction;
localechange: LocaleCode;
configlink: string;
portchange: string;
serialrefresh: void;
serialconnect: string;
serialexport: void;
csvimport: void;
noticeclear: void;
}>();
const connectionToneByState: Record<ConnectionState, "ok" | "warn" | "idle"> = {
online: "ok",
connecting: "warn",
offline: "idle"
};
$: connectionTone = connectionToneByState[connectionState];
$: connectionText =
connectionState === "online"
? connectedLabel
: connectionState === "connecting"
? connectingLabel
: disconnectedLabel;
$: connectButtonText =
connectionState === "online"
? disconnectActionLabel
: connectionState === "connecting"
? connectingLabel
: connectActionLabel;
$: exportButtonText = isExporting ? exportingActionLabel || exportActionLabel : exportActionLabel;
$: resolvedSerialPortOptions =
serialPortOptions.length > 0 ? serialPortOptions : serialPortValue ? [serialPortValue] : [];
function emitWindowControl(action: WindowControlAction): void {
dispatch("windowcontrol", action);
}
function switchLocale(nextLocale: LocaleCode): void {
dispatch("localechange", nextLocale);
}
function emitConfigLink(linkId: string): void {
dispatch("configlink", linkId);
}
function emitPortChange(event: Event): void {
const target = event.currentTarget as HTMLSelectElement;
dispatch("portchange", target.value);
}
function emitSerialConnect(): void {
dispatch("serialconnect", serialPortValue);
}
function emitSerialRefresh(): void {
dispatch("serialrefresh");
}
function emitSerialExport(): void {
dispatch("serialexport");
}
function emitCsvImport(): void {
dispatch("csvimport");
}
function emitNoticeClear(): void {
dispatch("noticeclear");
}
</script>
<section class="hud-panel" aria-label={controlAreaLabel}>
<div class="title-bar" role="group" aria-label="Title Bar" data-tauri-drag-region>
<div class="title-cluster" data-tauri-drag-region>
<span class="title-pulse" aria-hidden="true"></span>
<strong class="app-name">{appName}</strong>
<span class="suite-tag">{suiteName}</span>
</div>
<div class="window-controls" aria-label="Window Controls">
<button
type="button"
class="window-btn"
on:click={() => emitWindowControl("minimize")}
aria-label="Minimize window"
>
<svg viewBox="0 0 12 12" aria-hidden="true">
<path d="M2 6h8"></path>
</svg>
</button>
<button
type="button"
class="window-btn"
class:is-maximized={isWindowMaximized}
on:click={() => emitWindowControl("toggle-maximize")}
aria-label="Toggle maximize window"
>
<svg viewBox="0 0 12 12" aria-hidden="true">
<path d="M2.6 2.6h6.8v6.8h-6.8z"></path>
</svg>
</button>
<button
type="button"
class="window-btn is-close"
on:click={() => emitWindowControl("close")}
aria-label="Close window"
>
<svg viewBox="0 0 12 12" aria-hidden="true">
<path d="M2.6 2.6l6.8 6.8"></path>
<path d="M9.4 2.6l-6.8 6.8"></path>
</svg>
</button>
</div>
</div>
<div class="control-bar">
<div class="control-main-row">
<section class="config-links" aria-label={configLinksLabel}>
{#each configLinks as link (link.id)}
<button
type="button"
class="config-link tone-{link.tone ?? 'neutral'}"
class:is-active={Boolean(link.active)}
on:click={() => emitConfigLink(link.id)}
>
<span class="config-indicator" aria-hidden="true"></span>
<span class="config-label">{link.label}</span>
</button>
{/each}
</section>
<section class="state-card" aria-label={connectionLabel}>
<span class="state-dot" class:ok={connectionTone === "ok"} class:warn={connectionTone === "warn"}></span>
<span class="state-label">{connectionLabel}</span>
<strong class="state-value">{connectionText}</strong>
</section>
<label class="serial-select" aria-label={serialPortLabel}>
<span class="serial-tag">{serialPortLabel}</span>
<span class="serial-select-wrap">
<select
class="serial-select-input"
value={serialPortValue}
disabled={connectionState !== "offline" || isRefreshingPorts}
on:change={emitPortChange}
>
{#each resolvedSerialPortOptions as option}
<option value={option}>{option}</option>
{/each}
</select>
<span class="serial-select-caret" aria-hidden="true"></span>
</span>
</label>
<button
type="button"
class="refresh-btn"
disabled={isRefreshingPorts || connectionState !== "offline"}
on:click={emitSerialRefresh}
>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M13 4.8V1.8"></path>
<path d="M13 1.8H10"></path>
<path d="M13 1.8a5.7 5.7 0 0 0-9.7 3.4"></path>
<path d="M3 11.2v3"></path>
<path d="M3 14.2h3"></path>
<path d="M3 14.2a5.7 5.7 0 0 0 9.7-3.4"></path>
</svg>
<span>{isRefreshingPorts ? `${refreshPortsLabel}...` : refreshPortsLabel}</span>
</button>
<button
type="button"
class="connect-btn"
class:is-busy={connectionState === "connecting"}
class:is-connected={connectionState === "online"}
disabled={isConnectDisabled}
on:click={emitSerialConnect}
>
<span class="connect-btn-indicator" aria-hidden="true"></span>
<span>{connectButtonText}</span>
</button>
<button
type="button"
class="export-btn"
disabled={isExportDisabled || isExporting}
on:click={emitSerialExport}
>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 2.2v7.2"></path>
<path d="M5.4 7.8L8 10.5l2.6-2.7"></path>
<path d="M3.1 12.3h9.8"></path>
</svg>
<span>{exportButtonText}</span>
</button>
<button type="button" class="import-btn" on:click={emitCsvImport}>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 10.8V3.6"></path>
<path d="M5.4 6.3L8 3.6l2.6 2.7"></path>
<path d="M3.1 12.6h9.8"></path>
</svg>
<span>{importActionLabel}</span>
</button>
<section class="locale-switch" aria-label="Language">
<button
type="button"
class="locale-btn"
class:is-active={locale === "zh-CN"}
on:click={() => switchLocale("zh-CN")}
>
中文
</button>
<button
type="button"
class="locale-btn"
class:is-active={locale === "en-US"}
on:click={() => switchLocale("en-US")}
>
English
</button>
</section>
</div>
{#if connectionNotice}
<div class="connection-notice tone-{connectionNoticeTone}" role={connectionNoticeTone === "warn" ? "alert" : "status"}>
<p class="connection-notice-text">{connectionNotice}</p>
<button
type="button"
class="notice-close-btn"
aria-label={locale === "zh-CN" ? "关闭提示" : "Dismiss notice"}
on:click={emitNoticeClear}
>
×
</button>
</div>
{/if}
<section class="info-grid">
<article class="info-cell">
<p class="meta-label">{deviceLabel}</p>
<p class="meta-value">{deviceValue}</p>
</article>
<article class="info-cell">
<p class="meta-label">{sampleRateLabel}</p>
<p class="meta-value">{sampleRateValue}</p>
</article>
<article class="info-cell">
<p class="meta-label">{channelsLabel}</p>
<p class="meta-value">{channelsValue}</p>
</article>
</section>
</div>
</section>
<style>
.hud-panel {
display: grid;
grid-template-rows: auto auto;
gap: clamp(0.5rem, 1.2vw, 0.85rem);
}
.title-bar {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgb(108 143 166 / 0.22);
padding: 0.05rem 0.1rem 0.55rem 0.1rem;
background: linear-gradient(180deg, rgb(15 22 28 / 0.32), transparent);
}
.title-cluster {
display: inline-flex;
align-items: center;
gap: 0.55rem;
min-width: 0;
}
.title-pulse {
inline-size: 0.52rem;
block-size: 0.52rem;
border-radius: 50%;
background: rgb(133 255 68 / 0.95);
box-shadow: 0 0 0 2px rgb(133 255 68 / 0.14);
}
.app-name {
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.07em;
color: #f0fbff;
text-transform: uppercase;
}
.suite-tag {
font-size: 0.72rem;
color: var(--hud-text-dim);
letter-spacing: 0.1em;
text-transform: uppercase;
}
.window-controls {
display: inline-flex;
align-items: center;
gap: 0.38rem;
}
.window-btn {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: 1.8rem;
block-size: 1.52rem;
border: 1px solid rgb(82 120 146 / 0.36);
border-radius: 0.34rem;
color: rgb(179 245 255 / 0.92);
background: rgb(8 12 16 / 0.82);
cursor: pointer;
transition:
background-color 200ms ease,
border-color 200ms ease,
color 200ms ease;
}
.window-btn svg {
inline-size: 0.8rem;
block-size: 0.8rem;
stroke: currentColor;
stroke-width: 1.4;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.window-btn:hover {
border-color: rgb(62 232 255 / 0.42);
background: rgb(14 20 26 / 0.9);
color: #f3fdff;
}
.window-btn.is-maximized {
border-color: rgb(133 255 68 / 0.5);
color: rgb(211 255 190 / 0.92);
}
.window-btn.is-close:hover {
border-color: rgb(255 91 63 / 0.62);
background: rgb(27 11 10 / 0.9);
color: rgb(255 200 188 / 0.96);
}
.control-bar {
display: grid;
gap: 0.45rem;
padding: 0 0.1rem;
background: linear-gradient(90deg, rgb(62 232 255 / 0.02), transparent 45%, rgb(133 255 68 / 0.015));
}
.control-main-row {
display: flex;
align-items: center;
gap: 0.58rem;
flex-wrap: wrap;
}
.state-card {
display: inline-flex;
align-items: center;
gap: 0.42rem;
min-block-size: 2rem;
border: 1px solid rgb(95 132 158 / 0.3);
border-radius: 999px;
padding: 0.2rem 0.62rem 0.2rem 0.36rem;
background: rgb(10 16 20 / 0.68);
}
.state-dot {
inline-size: 0.55rem;
block-size: 0.55rem;
border-radius: 50%;
background: rgb(143 165 186 / 0.92);
box-shadow: 0 0 0 2px rgb(143 165 186 / 0.14);
}
.state-dot.ok {
background: var(--hud-lime);
box-shadow: 0 0 0 2px rgb(133 255 68 / 0.16);
}
.state-dot.warn {
background: var(--hud-orange);
box-shadow: 0 0 0 2px rgb(255 91 63 / 0.16);
}
.state-label {
color: rgb(154 176 194 / 0.84);
font-size: 0.66rem;
letter-spacing: 0.08em;
text-transform: uppercase;
line-height: 1;
}
.state-value {
color: #ecf8ff;
font-size: 0.92rem;
letter-spacing: 0.02em;
font-weight: 600;
line-height: 1;
}
.serial-select {
display: inline-flex;
align-items: center;
gap: 0.38rem;
min-block-size: 2rem;
border: 1px solid rgb(95 132 158 / 0.3);
border-radius: 999px;
padding: 0.18rem 0.2rem 0.18rem 0.56rem;
background: rgb(10 16 20 / 0.7);
min-inline-size: 0;
}
.serial-select-wrap {
position: relative;
display: inline-block;
min-inline-size: 0;
}
.serial-tag {
color: rgb(154 176 194 / 0.84);
font-size: 0.66rem;
letter-spacing: 0.08em;
text-transform: uppercase;
line-height: 1;
white-space: nowrap;
}
.serial-select-input {
appearance: none;
inline-size: 100%;
min-inline-size: 7rem;
border: 1px solid rgb(95 132 158 / 0.32);
border-radius: 999px;
padding: 0.3rem 1.5rem 0.3rem 0.6rem;
background: rgb(4 11 16 / 0.84);
color: #d5ebfb;
font-size: 0.84rem;
letter-spacing: 0.06em;
text-transform: uppercase;
outline: none;
transition:
border-color 180ms ease,
box-shadow 180ms ease;
}
.serial-select-input:hover {
border-color: rgb(62 232 255 / 0.36);
}
.serial-select-input:focus-visible {
border-color: rgb(62 232 255 / 0.5);
box-shadow: 0 0 0 2px rgb(62 232 255 / 0.14);
}
.serial-select-caret {
position: absolute;
inset-inline-end: 0.68rem;
inset-block-start: 50%;
inline-size: 0.42rem;
block-size: 0.42rem;
border-right: 1px solid rgb(153 189 214 / 0.82);
border-bottom: 1px solid rgb(153 189 214 / 0.82);
transform: translateY(-64%) rotate(45deg);
pointer-events: none;
}
.refresh-btn {
display: inline-flex;
align-items: center;
gap: 0.36rem;
min-block-size: 2rem;
border: 1px solid rgb(95 132 158 / 0.34);
border-radius: 999px;
padding: 0.24rem 0.64rem;
background:
linear-gradient(180deg, rgb(11 18 24 / 0.92), rgb(7 12 17 / 0.88)),
radial-gradient(circle at 50% 0, rgb(62 232 255 / 0.1), transparent 58%);
color: rgb(214 236 248 / 0.96);
font-size: 0.74rem;
letter-spacing: 0.05em;
cursor: pointer;
transition:
border-color 180ms ease,
background-color 180ms ease,
color 180ms ease,
box-shadow 200ms ease,
opacity 180ms ease;
}
.refresh-btn svg {
inline-size: 0.84rem;
block-size: 0.84rem;
stroke: currentColor;
stroke-width: 1.35;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.refresh-btn:hover:not(:disabled) {
border-color: rgb(62 232 255 / 0.44);
box-shadow:
inset 0 0 0 1px rgb(167 218 252 / 0.07),
0 0 10px rgb(62 232 255 / 0.1);
}
.refresh-btn:disabled {
opacity: 0.72;
cursor: default;
}
.connect-btn {
display: inline-flex;
align-items: center;
gap: 0.42rem;
min-block-size: 2rem;
border: 1px solid rgb(133 255 68 / 0.4);
border-radius: 999px;
padding: 0.24rem 0.76rem;
background:
linear-gradient(180deg, rgb(24 33 22 / 0.96), rgb(12 19 12 / 0.92)),
radial-gradient(circle at 50% 0, rgb(133 255 68 / 0.12), transparent 58%);
color: #f2ffe8;
font-size: 0.78rem;
letter-spacing: 0.05em;
cursor: pointer;
transition:
border-color 180ms ease,
background-color 180ms ease,
color 180ms ease,
box-shadow 200ms ease,
opacity 180ms ease;
}
.connect-btn:hover:not(:disabled) {
border-color: rgb(170 255 121 / 0.62);
box-shadow:
inset 0 0 0 1px rgb(231 255 214 / 0.08),
0 0 12px rgb(133 255 68 / 0.14);
}
.connect-btn:disabled {
opacity: 0.72;
cursor: default;
}
.connect-btn.is-busy {
border-color: rgb(255 91 63 / 0.48);
background:
linear-gradient(180deg, rgb(38 18 15 / 0.96), rgb(23 10 10 / 0.92)),
radial-gradient(circle at 50% 0, rgb(255 91 63 / 0.12), transparent 58%);
color: rgb(255 223 217 / 0.96);
}
.connect-btn.is-connected {
border-color: rgb(62 232 255 / 0.46);
background:
linear-gradient(180deg, rgb(10 28 32 / 0.96), rgb(6 15 18 / 0.92)),
radial-gradient(circle at 50% 0, rgb(62 232 255 / 0.14), transparent 58%);
color: rgb(227 251 255 / 0.98);
}
.connect-btn-indicator {
inline-size: 0.4rem;
block-size: 0.4rem;
border-radius: 999px;
background: var(--hud-lime);
box-shadow: 0 0 0 2px rgb(133 255 68 / 0.15);
}
.connect-btn.is-busy .connect-btn-indicator {
background: var(--hud-orange);
box-shadow: 0 0 0 2px rgb(255 91 63 / 0.16);
}
.connect-btn.is-connected .connect-btn-indicator {
background: var(--hud-cyan);
box-shadow: 0 0 0 2px rgb(62 232 255 / 0.16);
}
.export-btn {
display: inline-flex;
align-items: center;
gap: 0.36rem;
min-block-size: 2rem;
border: 1px solid rgb(62 232 255 / 0.4);
border-radius: 999px;
padding: 0.24rem 0.72rem;
background:
linear-gradient(180deg, rgb(10 28 32 / 0.96), rgb(6 15 18 / 0.92)),
radial-gradient(circle at 50% 0, rgb(62 232 255 / 0.14), transparent 58%);
color: rgb(227 251 255 / 0.98);
font-size: 0.74rem;
letter-spacing: 0.05em;
cursor: pointer;
transition:
border-color 180ms ease,
background-color 180ms ease,
color 180ms ease,
box-shadow 200ms ease,
opacity 180ms ease;
}
.export-btn svg {
inline-size: 0.84rem;
block-size: 0.84rem;
stroke: currentColor;
stroke-width: 1.35;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.export-btn:hover:not(:disabled) {
border-color: rgb(115 245 255 / 0.62);
box-shadow:
inset 0 0 0 1px rgb(231 255 255 / 0.08),
0 0 12px rgb(62 232 255 / 0.14);
}
.export-btn:disabled {
opacity: 0.72;
cursor: default;
}
.import-btn {
display: inline-flex;
align-items: center;
gap: 0.36rem;
min-block-size: 2rem;
border: 1px solid rgb(122 198 255 / 0.36);
border-radius: 999px;
padding: 0.24rem 0.72rem;
background:
linear-gradient(180deg, rgb(11 22 32 / 0.94), rgb(7 13 20 / 0.9)),
radial-gradient(circle at 50% 0, rgb(122 198 255 / 0.13), transparent 58%);
color: rgb(226 243 255 / 0.97);
font-size: 0.74rem;
letter-spacing: 0.05em;
cursor: pointer;
transition:
border-color 180ms ease,
background-color 180ms ease,
color 180ms ease,
box-shadow 200ms ease;
}
.import-btn svg {
inline-size: 0.84rem;
block-size: 0.84rem;
stroke: currentColor;
stroke-width: 1.35;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.import-btn:hover {
border-color: rgb(164 220 255 / 0.6);
box-shadow:
inset 0 0 0 1px rgb(231 244 255 / 0.08),
0 0 12px rgb(122 198 255 / 0.14);
}
.connection-notice {
margin: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
border: 1px solid rgb(95 132 158 / 0.32);
border-radius: 0.5rem;
padding: 0.38rem 0.45rem 0.38rem 0.7rem;
background: rgb(8 14 19 / 0.72);
}
.connection-notice-text {
margin: 0;
flex: 1;
min-width: 0;
color: rgb(214 236 248 / 0.96);
font-size: 0.72rem;
letter-spacing: 0.03em;
}
.connection-notice.tone-warn {
border-color: rgb(255 91 63 / 0.42);
background: rgb(28 12 11 / 0.78);
color: rgb(255 218 208 / 0.96);
}
.connection-notice.tone-ok {
border-color: rgb(133 255 68 / 0.4);
background: rgb(18 26 16 / 0.76);
color: rgb(228 255 214 / 0.96);
}
.connection-notice.tone-info {
border-color: rgb(62 232 255 / 0.34);
background: rgb(8 17 22 / 0.76);
}
.notice-close-btn {
inline-size: 1.36rem;
block-size: 1.36rem;
border: 1px solid rgb(116 151 176 / 0.4);
border-radius: 0.28rem;
background: rgb(7 12 16 / 0.82);
color: rgb(194 225 245 / 0.92);
font-size: 0.92rem;
line-height: 1;
cursor: pointer;
flex-shrink: 0;
transition:
border-color 180ms ease,
color 180ms ease,
background-color 180ms ease;
}
.notice-close-btn:hover {
border-color: rgb(62 232 255 / 0.5);
color: rgb(237 250 255 / 0.98);
background: rgb(9 16 22 / 0.92);
}
.connection-notice.tone-warn .notice-close-btn:hover {
border-color: rgb(255 91 63 / 0.6);
color: rgb(255 227 220 / 0.98);
background: rgb(34 13 12 / 0.9);
}
.connection-notice.tone-ok .notice-close-btn:hover {
border-color: rgb(133 255 68 / 0.56);
color: rgb(236 255 227 / 0.98);
background: rgb(17 28 14 / 0.9);
}
.info-grid {
display: flex;
align-items: center;
gap: 1.05rem;
flex-wrap: wrap;
padding-inline: 0.15rem;
}
.info-cell {
min-width: 0;
display: inline-flex;
align-items: baseline;
gap: 0.42rem;
}
.meta-label {
margin: 0;
font-size: 0.58rem;
letter-spacing: 0.1em;
color: rgb(140 163 181 / 0.82);
text-transform: uppercase;
}
.meta-value {
margin: 0;
font-size: 0.78rem;
letter-spacing: 0.03em;
color: rgb(205 228 245 / 0.92);
}
.locale-switch {
display: inline-flex;
align-items: center;
gap: 0.34rem;
border: 1px solid rgb(95 132 158 / 0.34);
border-radius: 999px;
padding: 0.18rem;
background: rgb(10 16 20 / 0.7);
}
.config-links {
display: inline-flex;
align-items: center;
gap: 0.2rem;
min-block-size: 2rem;
border: 1px solid rgb(95 132 158 / 0.36);
border-radius: 999px;
padding: 0.17rem 0.2rem;
background: linear-gradient(180deg, rgb(9 15 19 / 0.9), rgb(4 8 12 / 0.86));
box-shadow: inset 0 0 0 1px rgb(140 184 210 / 0.06);
}
.config-link {
display: inline-flex;
align-items: center;
gap: 0.34rem;
border: 1px solid transparent;
border-radius: 999px;
padding: 0.26rem 0.64rem;
background: transparent;
color: rgb(164 188 208 / 0.9);
font-size: 0.81rem;
letter-spacing: 0.05em;
cursor: pointer;
transition:
color 180ms ease,
border-color 180ms ease,
background-color 180ms ease,
box-shadow 220ms ease;
}
.config-indicator {
inline-size: 0.34rem;
block-size: 0.34rem;
border-radius: 999px;
background: rgb(136 157 174 / 0.88);
box-shadow: 0 0 0 2px rgb(136 157 174 / 0.16);
transition:
background-color 180ms ease,
box-shadow 200ms ease;
}
.config-label {
line-height: 1;
}
.config-link:hover {
color: #d7edfb;
border-color: rgb(62 232 255 / 0.26);
}
.config-link.is-active {
color: #f1fdff;
border-color: rgb(106 150 180 / 0.56);
background: rgb(18 27 35 / 0.9);
box-shadow:
inset 0 0 0 1px rgb(167 218 252 / 0.08),
0 0 10px rgb(62 232 255 / 0.12);
}
.config-link.tone-cyan.is-active {
border-color: rgb(62 232 255 / 0.48);
}
.config-link.tone-lime.is-active {
border-color: rgb(133 255 68 / 0.52);
}
.config-link.tone-orange.is-active {
border-color: rgb(255 91 63 / 0.52);
}
.config-link.tone-cyan.is-active .config-indicator {
background: var(--hud-cyan);
box-shadow: 0 0 0 2px rgb(62 232 255 / 0.17);
}
.config-link.tone-lime.is-active .config-indicator {
background: var(--hud-lime);
box-shadow: 0 0 0 2px rgb(133 255 68 / 0.17);
}
.config-link.tone-orange.is-active .config-indicator {
background: var(--hud-orange);
box-shadow: 0 0 0 2px rgb(255 91 63 / 0.18);
}
.locale-btn {
border: 1px solid transparent;
border-radius: 999px;
padding: 0.26rem 0.6rem;
background: transparent;
color: rgb(150 173 189 / 0.9);
font-size: 0.7rem;
letter-spacing: 0.04em;
cursor: pointer;
transition:
color 180ms ease,
border-color 180ms ease,
background-color 180ms ease;
}
.locale-btn:hover {
color: #d7edfb;
border-color: rgb(62 232 255 / 0.3);
}
.locale-btn.is-active {
color: #f1fdff;
border-color: rgb(133 255 68 / 0.48);
background: rgb(24 31 25 / 0.9);
}
@media (max-width: 1080px) {
.config-links {
flex-wrap: wrap;
}
}
@media (max-width: 820px) {
.control-main-row {
gap: 0.44rem;
}
.config-links {
flex-wrap: wrap;
}
.serial-select {
padding-inline-start: 0.45rem;
}
.info-grid {
gap: 0.84rem;
}
}
@media (max-width: 620px) {
.suite-tag {
display: none;
}
.control-main-row {
align-items: stretch;
}
.serial-select {
inline-size: 100%;
justify-content: space-between;
}
.info-grid {
gap: 0.42rem 0.8rem;
}
.info-cell {
inline-size: 100%;
justify-content: space-between;
}
.window-btn {
inline-size: 1.75rem;
}
}
</style>