first commit

This commit is contained in:
lennlouisgeek
2026-03-30 02:59:56 +08:00
commit eec9927ae6
60 changed files with 15953 additions and 0 deletions

View File

@@ -0,0 +1,970 @@
<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;
let csvInputEl: HTMLInputElement | undefined;
const dispatch = createEventDispatcher<{
windowcontrol: WindowControlAction;
localechange: LocaleCode;
configlink: string;
portchange: string;
serialrefresh: void;
serialconnect: string;
serialexport: void;
csvimport: File;
}>();
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 openCsvPicker(): void {
csvInputEl?.click();
}
function emitCsvImport(event: Event): void {
const target = event.currentTarget as HTMLInputElement;
const file = target.files?.[0];
if (file) {
dispatch("csvimport", file);
}
target.value = "";
}
</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={openCsvPicker}>
<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>
<input
bind:this={csvInputEl}
class="hidden-input"
type="file"
accept=".csv,text/csv"
on:change={emitCsvImport}
/>
<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}
<p class="connection-notice tone-{connectionNoticeTone}" role={connectionNoticeTone === "warn" ? "alert" : "status"}>
{connectionNotice}
</p>
{/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);
}
.hidden-input {
position: absolute;
inline-size: 0;
block-size: 0;
opacity: 0;
pointer-events: none;
}
.connection-notice {
margin: 0;
border: 1px solid rgb(95 132 158 / 0.32);
border-radius: 0.5rem;
padding: 0.45rem 0.7rem;
background: rgb(8 14 19 / 0.72);
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);
color: rgb(214 236 248 / 0.96);
}
.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>