fix 前端优化

This commit is contained in:
npc0-hue
2026-07-21 13:44:23 +08:00
parent 0cb91b5f88
commit b06623d0ce
11 changed files with 386 additions and 149 deletions
+15
View File
@@ -37,6 +37,21 @@ describe("AppShell mobile navigation", () => {
expect(html).toContain("关闭导航菜单"); expect(html).toContain("关闭导航菜单");
}); });
it("renders the animation layer above the workspace background", () => {
const html = renderToStaticMarkup(
<AppShell routes={routes} currentPage="home" session={session} operations={[]} onNavigate={() => undefined}>
<p></p>
</AppShell>
);
expect(html.indexOf("workspace-background-layer")).toBeLessThan(html.indexOf("global-particle-layer"));
expect(html).toContain("global-orbital-construct");
expect(html).toContain("global-particle-lightfall");
expect(html).toContain("global-particle-side-ray");
expect(html).not.toContain("global-particle-orbit");
expect(html).not.toContain("global-particle-frame");
});
it("uses bounded edge-swipe thresholds and accessible dismissal", () => { it("uses bounded edge-swipe thresholds and accessible dismissal", () => {
expect(appShellSource).toContain("start.x <= 28 && deltaX >= 56"); expect(appShellSource).toContain("start.x <= 28 && deltaX >= 56");
expect(appShellSource).toContain("deltaX <= -56"); expect(appShellSource).toContain("deltaX <= -56");
@@ -11,23 +11,53 @@ const particleSlots = [
{ x: "92%", y: "86%" } { x: "92%", y: "86%" }
]; ];
const orbitalConstructSlots = [
{ x: "76%", y: "82%", size: "min(54vw, 680px)", drift: "-6deg" },
{ x: "14%", y: "58%", size: "min(34vw, 420px)", drift: "18deg" },
{ x: "94%", y: "22%", size: "min(24vw, 300px)", drift: "-26deg" }
];
type ParticleStyle = CSSProperties & { type ParticleStyle = CSSProperties & {
"--particle-index": string; "--particle-index": string;
"--particle-x": string; "--particle-x": string;
"--particle-y": string; "--particle-y": string;
}; };
type OrbitalConstructStyle = CSSProperties & {
"--construct-index": string;
"--construct-x": string;
"--construct-y": string;
"--construct-size": string;
"--construct-drift": string;
};
export function MagicalParticleLayer() { export function MagicalParticleLayer() {
return ( return (
<> <>
<div className="workspace-background-layer" aria-hidden="true" /> <div className="workspace-background-layer" aria-hidden="true" />
<div className="global-particle-layer" aria-hidden="true"> <div className="global-particle-layer" aria-hidden="true">
<span className="global-particle-orbit" /> <span className="global-particle-lightfall global-particle-lightfall-a" />
<span className="global-particle-orbit global-particle-orbit-secondary" /> <span className="global-particle-lightfall global-particle-lightfall-b" />
<span className="global-particle-lightfall global-particle-lightfall-c" />
<span className="global-particle-side-ray global-particle-side-ray-left" />
<span className="global-particle-side-ray global-particle-side-ray-right" />
{orbitalConstructSlots.map((construct, index) => (
<span
key={`${construct.x}-${construct.y}-${construct.size}`}
className="global-orbital-construct"
style={
{
"--construct-index": String(index),
"--construct-x": construct.x,
"--construct-y": construct.y,
"--construct-size": construct.size,
"--construct-drift": construct.drift
} as OrbitalConstructStyle
}
/>
))}
<span className="global-particle-sweep" /> <span className="global-particle-sweep" />
<span className="global-particle-ribbon" /> <span className="global-particle-ribbon" />
<span className="global-particle-frame global-particle-frame-tl" />
<span className="global-particle-frame global-particle-frame-br" />
{particleSlots.map((slot, index) => ( {particleSlots.map((slot, index) => (
<span <span
key={`${slot.x}-${slot.y}`} key={`${slot.x}-${slot.y}`}
+2
View File
@@ -164,6 +164,8 @@ describe("first-party console pages", () => {
expect(serversPageSource).toContain('className="provider-form dialog-form"'); expect(serversPageSource).toContain('className="provider-form dialog-form"');
expect(serversPageSource).toContain('name="profileKey"'); expect(serversPageSource).toContain('name="profileKey"');
expect(serversPageSource).toContain("runtimeBindingFields"); expect(serversPageSource).toContain("runtimeBindingFields");
expect(serversPageSource).toContain("createProfileUnavailable");
expect(serversPageSource).toContain("当前插件没有声明运行配置");
expect(serversPageSource).toContain("updateBinding(field.key"); expect(serversPageSource).toContain("updateBinding(field.key");
expect(serversPageSource).toContain('type={field.sensitive ? "password" : "text"}'); expect(serversPageSource).toContain('type={field.sensitive ? "password" : "text"}');
for (const forbidden of ["secret://", "/Users/", "/var/run/", "unix://", "tcp://"]) { for (const forbidden of ["secret://", "/Users/", "/var/run/", "unix://", "tcp://"]) {
+10 -2
View File
@@ -139,6 +139,8 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
const createPending = operations.isPending("platform", "创建服务器"); const createPending = operations.isPending("platform", "创建服务器");
const canManageServers = session.capabilities.includes("servers.manage"); const canManageServers = session.capabilities.includes("servers.manage");
const selectedCreatePlugin = plugins.find((plugin) => plugin.id === form.pluginId); const selectedCreatePlugin = plugins.find((plugin) => plugin.id === form.pluginId);
const createProfileOptions = selectedCreatePlugin?.runtimeProfiles?.lifecycleProfiles ?? [];
const createProfileUnavailable = Boolean(selectedCreatePlugin && createProfileOptions.length === 0);
const createBindingFields = runtimeBindingFields(selectedCreatePlugin, form.profileKey); const createBindingFields = runtimeBindingFields(selectedCreatePlugin, form.profileKey);
function updateForm(event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) { function updateForm(event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
@@ -442,13 +444,19 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
<label> <label>
<select name="profileKey" value={form.profileKey} onChange={updateForm} required> <select name="profileKey" value={form.profileKey} onChange={updateForm} required>
{(selectedCreatePlugin?.runtimeProfiles?.lifecycleProfiles ?? []).map((profile) => ( {createProfileOptions.map((profile) => (
<option key={profile.key} value={profile.key}> <option key={profile.key} value={profile.key}>
{profile.key} · {profile.mode} {profile.key} · {profile.mode}
</option> </option>
))} ))}
</select> </select>
</label> </label>
{createProfileUnavailable && (
<div className="operations-inline-warning" role="status">
<AlertTriangle size={14} />
<span> manifest </span>
</div>
)}
{createBindingFields.map((field) => ( {createBindingFields.map((field) => (
<label key={field.key}> <label key={field.key}>
{field.key}{field.required ? "(必填)" : ""} {field.key}{field.required ? "(必填)" : ""}
@@ -465,7 +473,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
</div> </div>
<div className="confirm-actions"> <div className="confirm-actions">
<button type="button" disabled={createPending} onClick={() => setShowCreate(false)}></button> <button type="button" disabled={createPending} onClick={() => setShowCreate(false)}></button>
<button type="submit" className="confirm-primary" disabled={createPending || !form.profileKey} title="创建服务器"> <button type="submit" className="confirm-primary" disabled={createPending || !form.profileKey || createProfileUnavailable} title="创建服务器">
<Sparkles size={16} /> <Sparkles size={16} />
<span>{createPending ? "创建中…" : "创建并安装"}</span> <span>{createPending ? "创建中…" : "创建并安装"}</span>
</button> </button>
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
<g stroke-linecap="round" stroke-linejoin="round">
<circle cx="256" cy="256" r="178" stroke="#fff6fb" stroke-opacity=".64" stroke-width="2.4"/>
<circle cx="256" cy="256" r="139" stroke="#ffd66d" stroke-opacity=".58" stroke-width="1.8"/>
<circle cx="256" cy="256" r="84" stroke="#ff8fd2" stroke-opacity=".62" stroke-width="1.7"/>
<path d="M256 91v330M91 256h330M139 139l234 234M373 139 139 373" stroke="#fff6fb" stroke-opacity=".24" stroke-width="1.2"/>
<path d="m256 125 37 82 89 9-66 60 19 87-79-44-79 44 19-87-66-60 89-9z" stroke="#ffd66d" stroke-opacity=".54" stroke-width="1.9"/>
<path d="M165 194c28-42 72-65 123-62M346 318c-28 42-72 65-123 62" stroke="#ff8fd2" stroke-opacity=".62" stroke-width="4"/>
<path d="M155 318c-18-30-26-62-22-96M358 194c18 30 26 62 22 96" stroke="#fff6fb" stroke-opacity=".42" stroke-width="3"/>
<path d="m256 174 19 39 43 6-31 31 7 44-38-21-38 21 7-44-31-31 43-6z" stroke="#fff6fb" stroke-opacity=".78" stroke-width="2"/>
<path d="M256 50l8 17 19 3-14 13 4 19-17-9-17 9 4-19-14-13 19-3zM256 410l8 17 19 3-14 13 4 19-17-9-17 9 4-19-14-13 19-3zM50 256l17-8 3-19 13 14 19-4-9 17 9 17-19-4-13 14-3-19zM410 256l17-8 3-19 13 14 19-4-9 17 9 17-19-4-13 14-3-19z" stroke="#ffd66d" stroke-opacity=".72" stroke-width="1.7"/>
<path d="M103 178c22-40 52-70 92-91M409 334c-22 40-52 70-92 91" stroke="#8cf0ff" stroke-opacity=".32" stroke-width="1.8" stroke-dasharray="10 13"/>
<path d="M103 334c-22-40-28-82-19-126M409 178c22 40 28 82 19 126" stroke="#ff8fd2" stroke-opacity=".32" stroke-width="1.8" stroke-dasharray="8 12"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
<g stroke-linecap="round" stroke-linejoin="round">
<circle cx="256" cy="256" r="34" stroke="#ffd66d" stroke-opacity=".66" stroke-width="2"/>
<circle cx="256" cy="256" r="48" stroke="#48e6ff" stroke-opacity=".42" stroke-width="1.5" stroke-dasharray="6 11"/>
<path d="M79 260a177 177 0 0 1 69-140 177 177 0 0 1 164-31" stroke="#48e6ff" stroke-opacity=".5" stroke-width="2.4"/>
<path d="M432 252a177 177 0 0 1-63 135 177 177 0 0 1-153 39" stroke="#48e6ff" stroke-opacity=".46" stroke-width="2.4"/>
<path d="M108 344a173 173 0 0 1-27-66M375 111a173 173 0 0 1 48 83" stroke="#ffd66d" stroke-opacity=".34" stroke-width="1.8"/>
<path d="M134 158l244 196M152 363l215-225M91 260h90M331 256h92M256 90v79M256 342v82" stroke="#8cf0ff" stroke-opacity=".22" stroke-width="1.2"/>
<path d="M174 119l32-18 38 9 11 32-23 29-39-6-25-23z" stroke="#48e6ff" stroke-opacity=".44" stroke-width="1.8"/>
<path d="M337 339l39-11 31 19 3 37-31 23-38-11-15-31z" stroke="#48e6ff" stroke-opacity=".4" stroke-width="1.8"/>
<path d="M360 117l29 8 14 29-16 28-31 6-23-22 2-33z" stroke="#ffd66d" stroke-opacity=".32" stroke-width="1.6"/>
<path d="M118 313l22 8 9 23-14 21-25 2-16-19 3-25z" stroke="#ffd66d" stroke-opacity=".3" stroke-width="1.6"/>
<path d="M106 226h54l18 30-18 30h-54l-17-30zM352 226h54l17 30-17 30h-54l-18-30z" stroke="#48e6ff" stroke-opacity=".34" stroke-width="1.4"/>
<path d="M230 84h52M230 428h52M83 230v52M429 230v52" stroke="#8cf0ff" stroke-opacity=".5" stroke-width="6"/>
<path d="M198 97l-8 31M318 415l8-31M415 198l-31-8M97 318l31 8" stroke="#48e6ff" stroke-opacity=".48" stroke-width="5"/>
<path d="M256 208l42 24v48l-42 24-42-24v-48z" stroke="#48e6ff" stroke-opacity=".38" stroke-width="1.7"/>
<path d="M256 208v96M214 232l84 48M298 232l-84 48" stroke="#ffd66d" stroke-opacity=".26" stroke-width="1.1"/>
<path d="M60 256h24M428 256h24M256 60v24M256 428v24" stroke="#ffd66d" stroke-opacity=".5" stroke-width="2.4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+113 -100
View File
@@ -1,156 +1,169 @@
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
function readThemeCss() {
return readFileSync(new URL("./base.css", import.meta.url), "utf8");
}
function compact(value) {
return value.replace(/\s+/g, "");
}
describe("platform web shared theme CSS", () => { describe("platform web shared theme CSS", () => {
it("keeps production lifecycle and alert controls bounded at narrow width", () => { it("keeps production lifecycle and alert controls bounded at narrow width", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const narrow = themeCss.slice(themeCss.indexOf("@media (max-width: 640px)", themeCss.indexOf(".production-governance-panel")));
expect(narrow).toContain(".plugin-lifecycle-controls"); expect(css).toContain("@media(max-width:640px){.plugin-lifecycle-controls{grid-template-columns:minmax(0,1fr)}");
expect(narrow).toContain("grid-template-columns: minmax(0, 1fr)"); expect(css).toContain(".production-alert-actions,.production-alert-actionsbutton{width:100%}");
expect(narrow).toContain(".production-alert-actions button"); expect(css).toContain("overflow-wrap:anywhere");
expect(themeCss).toContain("overflow-wrap: anywhere");
}); });
it("suppresses nested state frames inside shared framed surfaces", () => { it("suppresses nested state frames inside shared framed surfaces", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const nestedStateReset = themeCss.slice(themeCss.indexOf("A state view inside an already framed surface"));
expect(nestedStateReset).toContain(".state-view::after"); expect(css).toContain(".state-view::after,:root[data-theme-palette]:where(");
expect(nestedStateReset).toContain("content: none"); expect(css).toContain(".state-view::before{content:none;background:00;box-shadow:none;filter:none}");
expect(nestedStateReset).toContain("background: transparent");
}); });
it("keeps server card runtime actions as compact overlay menus", () => { it("keeps server card runtime actions as compact overlay menus", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const popoverRule = themeCss.slice(themeCss.indexOf(".runtime-action-popover"), themeCss.indexOf("/* ---- catalog / detail lists ---- */"));
expect(popoverRule).toContain("position: fixed"); expect(css).toContain(".runtime-action-popover{position:fixed;z-index:45");
expect(popoverRule).toContain("z-index: 45"); expect(css).toContain(".runtime-action-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))");
expect(popoverRule).toContain(".runtime-action-grid");
expect(popoverRule).toContain("grid-template-columns: repeat(2, minmax(0, 1fr))");
}); });
it("keeps server card stat tiles readable over busy backgrounds", () => { it("keeps server card stat tiles readable over busy backgrounds", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const statRule = themeCss.slice(themeCss.indexOf(".server-card-stat {"), themeCss.indexOf(".server-card-meters"));
expect(statRule).toContain("border: 1px solid"); expect(css).toContain(".server-card-stat{display:grid;gap:2px;padding:8px;border:1pxsolid");
expect(statRule).toContain("--surface-solid"); expect(css).toContain("--surface-solid");
expect(statRule).toContain("font-weight: 850"); expect(css).toContain("font-weight:850");
expect(themeCss).toContain(':root[data-custom-background="true"] .server-card-stat'); expect(css).toContain(":root[data-custom-background=true].server-card-stat");
}); });
it("keeps magical custom-background signal rows readable without the jelly wash", () => { it("keeps magical custom-background signal rows readable without the jelly wash", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const signalRule = themeCss.slice(
themeCss.indexOf(':root[data-custom-background="true"][data-theme-palette="magical-girl"] .signal-item'),
themeCss.indexOf(':root[data-custom-background="true"][data-theme-palette="magical-girl"] .signal-item strong')
);
expect(signalRule).toContain("rgba(58, 44, 56, 0.72)"); expect(css).toContain(":root[data-custom-background=true][data-theme-palette=magical-girl].signal-item");
expect(signalRule).toContain("backdrop-filter: blur(12px) saturate(0.92)"); expect(css).toContain("rgba(58,44,56,.72)");
expect(signalRule).not.toContain("var(--jelly-highlight), var(--glass-wash), var(--surface-solid)"); expect(css).toContain("backdrop-filter:blur(12px)saturate(0.92)");
expect(css).not.toContain("var(--jelly-highlight),var(--glass-wash),var(--surface-solid)");
}); });
it("keeps shared controls off the heavy jelly and corner-sparkle button wash", () => { it("keeps shared controls off the heavy jelly and corner-sparkle button wash", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = readThemeCss();
const primaryCommandRule = themeCss.slice(themeCss.indexOf(".primary-command {"), themeCss.indexOf(".action-strip .primary-command"));
expect(themeCss).toContain("--control-surface"); expect(css).toContain("--control-surface");
expect(themeCss).toContain("--primary-command-surface"); expect(css).toContain("--primary-command-surface");
expect(themeCss).not.toContain("background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid)"); expect(css).not.toContain("background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid)");
expect(themeCss).not.toContain("linear-gradient(145deg, rgba(255, 255, 255, 0.72), transparent 36%)"); expect(css).not.toContain("linear-gradient(145deg, rgba(255, 255, 255, 0.72), transparent 36%)");
expect(primaryCommandRule).not.toContain("background-position: right 8px top 4px, center, center"); });
it("uses shared orbital construct backdrop and button ring effects", () => {
const themeCss = readThemeCss();
const constructRule = themeCss.slice(themeCss.indexOf(".global-orbital-construct"), themeCss.indexOf(".mobile-sidebar-backdrop"));
expect(constructRule).toContain("clip-path:circle(49% at 50% 50%)");
expect(constructRule).toContain("background:var(--orbital-construct-image) center/contain no-repeat transparent");
expect(constructRule).toContain("filter:var(--orbital-construct-filter)");
expect(constructRule).toContain(".global-orbital-construct::before,.global-orbital-construct::after{content:none}");
expect(constructRule).toContain(":root[data-theme-palette=mecha-black] .global-orbital-construct{display:block;opacity:.22}");
expect(constructRule).toContain(":root[data-theme-palette=magical-girl] .global-orbital-construct{display:block;opacity:.24}");
expect(constructRule).toContain(".global-particle-lightfall");
expect(constructRule).toContain(".global-particle-side-ray");
expect(themeCss).not.toContain("global-particle-frame");
expect(themeCss).not.toContain("global-particle-orbit");
expect(themeCss).not.toContain("particle-orbit-drift");
expect(constructRule).toContain("orbital-construct-drift");
expect(constructRule).toContain("prefers-reduced-motion");
expect(constructRule).toContain("var(--command-ring-sheen");
expect(constructRule).toContain(".app-nav-group-button::after");
expect(constructRule).toContain("magic-side-ray-pulse-right");
expect(constructRule).toContain("transform:translateY(1px)");
expect(themeCss).toContain(":root[data-custom-background=true] .global-particle-layer{opacity:.42}");
expect(themeCss).toContain(":root[data-custom-background=true][data-theme-palette=magical-girl] .global-particle-layer{opacity:.54}");
expect(themeCss).not.toContain(":root[data-custom-background=true] .global-particle-layer{opacity:.03}");
}); });
it("keeps magical-girl custom-background panel overlays restrained", () => { it("keeps magical-girl custom-background panel overlays restrained", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const customMagicalRule = themeCss.slice(
themeCss.indexOf(':root[data-custom-background="true"][data-theme-palette="magical-girl"] {'),
themeCss.indexOf(':root[data-custom-background="true"][data-theme-palette="magical-girl"] .app-sidebar')
);
expect(customMagicalRule).toContain("--frame-accessory-opacity: 0.22"); expect(css).toContain(":root[data-custom-background=true][data-theme-palette=magical-girl]{--frame-accessory-opacity:0.22");
expect(customMagicalRule).toContain("rgba(22, 22, 29, 0.64)"); expect(css).toContain("--surface:rgba(22,22,29,0.42)");
expect(customMagicalRule).not.toContain("255, 119, 200, 0.78"); expect(css).toContain("linear-gradient(180deg,rgba(24,24,31,.22),rgba(18,19,26,.18))");
expect(css).not.toContain("255,119,200,0.78");
});
it("keeps catalog and console panels translucent without fading content", () => {
const css = compact(readThemeCss());
expect(css).toContain(".catalog-card,.console-panel{border:1pxsolidvar(--line);border-radius:8px;background:var(--sugar-dust),linear-gradient(135deg,color-mix(insrgb,var(--surface)22%,transparent),color-mix(insrgb,var(--surface-raised)14%,transparent))");
expect(css).toContain("-webkit-backdrop-filter:blur(10px)saturate(.9);backdrop-filter:blur(10px)saturate(.9)");
expect(css).toContain(":root[data-custom-background=true].catalog-card");
expect(css).toContain("linear-gradient(180deg,rgba(5,10,16,.42),rgba(5,10,16,.34))");
expect(css).not.toContain(".catalog-card,.console-panel{opacity:");
expect(css).not.toContain(".catalog-card,.console-panel{border:1pxsolidvar(--line);border-radius:8px;background:var(--panel-material);backdrop-filter:blur(22px)saturate(1.28)");
}); });
it("styles runtime task progress as a dialog with staged status", () => { it("styles runtime task progress as a dialog with staged status", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = readThemeCss();
const progressRule = themeCss.slice(themeCss.indexOf(".runtime-task-backdrop"), themeCss.indexOf("/* ---- narrow screens ---- */"));
expect(progressRule).toContain(".runtime-task-panel"); expect(css).toContain(".runtime-task-panel");
expect(progressRule).toContain(".runtime-task-meter-track"); expect(css).toContain(".runtime-task-meter-track");
expect(progressRule).toContain(".runtime-task-stages"); expect(css).toContain(".runtime-task-stages");
expect(progressRule).toContain(".runtime-task-log"); expect(css).toContain(".runtime-task-log");
expect(progressRule).toContain("prefers-reduced-motion"); expect(css).toContain("prefers-reduced-motion");
}); });
it("styles operations surfaces with shared theme materials and narrow-screen collapse", () => { it("styles operations surfaces with shared theme materials and narrow-screen collapse", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const operationsRule = themeCss.slice(themeCss.indexOf("/* ---- operations console enrichment ---- */"));
expect(operationsRule).toContain("var(--panel-material)"); expect(css).toContain(".operations-tray{position:relative;z-index:2");
expect(operationsRule).toContain(".operations-tray-panel"); expect(css).toContain("background:var(--panel-material),var(--surface)");
expect(operationsRule).toContain("@media (max-width: 760px)"); expect(css).toContain(".operations-tray-panel{position:absolute");
expect(operationsRule).toContain("grid-template-columns: 1fr"); expect(css).toContain("@media(max-width:760px){.operations-command-grid{grid-template-columns:1fr}");
expect(operationsRule).toContain("prefers-reduced-motion"); expect(css).toContain(".operations-job-row-failed");
expect(operationsRule).toContain(".operations-job-row-failed"); expect(css).toContain("prefers-reduced-motion");
}); });
it("keeps mobile navigation off-canvas with readable vertical labels", () => { it("keeps mobile navigation off-canvas with readable vertical labels", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const mobileRule = themeCss.slice(themeCss.indexOf("/* ---- narrow screens ---- */"), themeCss.indexOf("/* A state view inside an already framed surface"));
expect(mobileRule).toContain(".mobile-sidebar-handle"); expect(css).toContain(".mobile-sidebar-handle{position:fixed");
expect(mobileRule).toContain("position: fixed"); expect(css).toContain(".app-sidebar{position:fixed;inset:0auto00");
expect(mobileRule).toContain("padding: 14px 14px 14px 52px"); expect(css).toContain("transform:translateX(calc(-100%-18px))");
expect(mobileRule).toContain("transform: translateX(calc(-100% - 18px))"); expect(css).toContain(".app-sidebar-mobile-open");
expect(mobileRule).toContain(".app-sidebar-mobile-open"); expect(css).toContain("grid-template-columns:40pxminmax(0,1fr)");
expect(mobileRule).toContain("grid-template-columns: 40px minmax(0, 1fr)"); expect(css).toContain(".app-nav-copystrong{display:block");
expect(mobileRule).toContain(".app-nav-copy strong");
expect(mobileRule).toContain("display: block");
}); });
it("keeps desktop sidebar controls pinned inside the viewport", () => { it("keeps desktop sidebar controls pinned inside the viewport", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const shellRule = themeCss.slice(themeCss.indexOf(".app-shell {"), themeCss.indexOf(".workspace-background-layer"));
const sidebarRule = themeCss.slice(themeCss.indexOf(".app-sidebar {"), themeCss.indexOf(".app-brand {"));
expect(shellRule).toContain("min-height: 100dvh"); expect(css).toContain(".app-shell{min-height:100dvh");
expect(sidebarRule).toContain("position: sticky"); expect(css).toContain(".app-sidebar{position:sticky");
expect(sidebarRule).toContain("height: 100dvh"); expect(css).toContain("height:100dvh");
expect(sidebarRule).toContain("overflow-y: auto"); expect(css).toContain("overflow-y:auto");
expect(sidebarRule).toContain("overscroll-behavior: contain"); expect(css).toContain("overscroll-behavior:contain");
}); });
it("keeps dialogs and drawers internally scrollable on short viewports", () => { it("keeps dialogs and drawers internally scrollable on short viewports", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const dialogRule = themeCss.slice(themeCss.indexOf("/* ---- drawer ---- */"), themeCss.indexOf(".plugin-detail-actions"));
const confirmStart = themeCss.indexOf("/* ---- confirm dialog ---- */");
const confirmRule = themeCss.slice(confirmStart, themeCss.indexOf(".confirm-actions {", confirmStart));
const mobileRule = themeCss.slice(themeCss.indexOf("/* ---- narrow screens ---- */"), themeCss.indexOf("/* A state view inside an already framed surface"));
expect(dialogRule).toContain(".dialog-panel-header"); expect(css).toContain(".dialog-panel-header,.drawer-panel>.panel-header{position:sticky");
expect(dialogRule).toContain(".drawer-panel > .panel-header"); expect(css).toContain("overflow-y:auto");
expect(dialogRule).toContain("position: sticky"); expect(css).toContain("overscroll-behavior:contain");
expect(dialogRule).toContain("overflow-y: auto"); expect(css).toContain("max-height:calc(100dvh-32px)");
expect(dialogRule).toContain("overscroll-behavior: contain"); expect(css).toContain("max-height:calc(100dvh-20px)");
expect(confirmRule).toContain("max-height: calc(100dvh - 32px)");
expect(confirmRule).toContain("overflow-y: auto");
expect(mobileRule).toContain("max-height: calc(100dvh - 20px)");
}); });
it("keeps magical card decoration from becoming square background patches", () => { it("keeps magical card decoration from becoming square background patches", () => {
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8"); const css = compact(readThemeCss());
const magicalSurfaceRule = themeCss.slice(
themeCss.indexOf(':root[data-theme-palette="magical-girl"] .metric-card'),
themeCss.indexOf(':root[data-theme-palette="magical-girl"] :where(')
);
expect(magicalSurfaceRule).toContain("background: var(--panel-material)"); expect(css).toContain(":root[data-theme-palette=magical-girl].catalog-card");
expect(magicalSurfaceRule).not.toContain("background-size: 56px 56px"); expect(css).toContain("background:var(--sugar-dust),linear-gradient(180deg,color-mix(insrgb,var(--surface)16%,transparent),color-mix(insrgb,var(--surface-raised)10%,transparent));overflow:visible");
expect(magicalSurfaceRule).not.toContain("background-image: var(--corner-sparkle), var(--panel-material)"); expect(css).not.toContain("background-size:56px56px");
expect(css).not.toContain("background-image:var(--corner-sparkle),var(--panel-material)");
expect(css).not.toContain(".global-magic-ring");
}); });
}); });
+68 -27
View File
@@ -8,42 +8,82 @@ button{font:inherit}
:root[data-custom-background=true] .workspace-background-layer::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(4,7,11,.28) 0,rgba(4,7,11,.1) 216px,transparent 42%),linear-gradient(180deg,rgba(4,7,11,.02),rgba(4,7,11,.1));pointer-events:none} :root[data-custom-background=true] .workspace-background-layer::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(4,7,11,.28) 0,rgba(4,7,11,.1) 216px,transparent 42%),linear-gradient(180deg,rgba(4,7,11,.02),rgba(4,7,11,.1));pointer-events:none}
:root[data-custom-background=true] .workspace-background-layer{filter:saturate(.92) contrast(.96) brightness(.94);opacity:1} :root[data-custom-background=true] .workspace-background-layer{filter:saturate(.92) contrast(.96) brightness(.94);opacity:1}
.global-particle-layer{position:fixed;inset:0;z-index:0;width:100%;height:100%;pointer-events:none;opacity:var(--ultimate-effect-alpha);mix-blend-mode:screen;overflow:hidden} .global-particle-layer{position:fixed;inset:0;z-index:0;width:100%;height:100%;pointer-events:none;opacity:var(--ultimate-effect-alpha);mix-blend-mode:screen;overflow:hidden}
:root[data-custom-background=true] .global-particle-layer{opacity:.08} :root[data-custom-background=true] .global-particle-layer{opacity:.42}
.global-particle-layer span{position:absolute;display:block;pointer-events:none} .global-particle-layer span{position:absolute;display:block;pointer-events:none}
.global-particle-orbit{right:9vw;bottom:6vh;width:min(36vw,440px);aspect-ratio:1;border:2px solid color-mix(in srgb,var(--accent) 42%,transparent);border-radius:50%;box-shadow:0 0 34px color-mix(in srgb,var(--accent) 18%,transparent),inset 0 0 42px color-mix(in srgb,var(--accent) 12%,transparent);opacity:.62;transform:rotate(12deg)}
.global-particle-orbit::after,.global-particle-orbit::before{content:"";position:absolute;inset:18%;border:1px solid color-mix(in srgb,var(--gold) 34%,transparent);border-radius:inherit}
.global-particle-orbit::after{inset:34%;border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
.global-particle-orbit-secondary{right:23vw;top:9vh;bottom:auto;width:min(18vw,230px);opacity:.36;transform:rotate(-18deg)}
.global-particle-sweep{top:-12vh;left:63vw;width:28px;height:118vh;background:linear-gradient(180deg,transparent,color-mix(in srgb,var(--accent) 42%,transparent),transparent);opacity:.42;transform:rotate(24deg);filter:blur(1px)} .global-particle-sweep{top:-12vh;left:63vw;width:28px;height:118vh;background:linear-gradient(180deg,transparent,color-mix(in srgb,var(--accent) 42%,transparent),transparent);opacity:.42;transform:rotate(24deg);filter:blur(1px)}
.global-particle-ribbon{left:-12vw;bottom:24vh;width:128vw;height:120px;background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--pink) 24%,transparent),color-mix(in srgb,var(--gold) 16%,transparent),transparent);opacity:.28;transform:rotate(-11deg);filter:blur(18px)} .global-particle-ribbon{left:-12vw;bottom:24vh;width:128vw;height:120px;background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--pink) 24%,transparent),color-mix(in srgb,var(--gold) 16%,transparent),transparent);opacity:.28;transform:rotate(-11deg);filter:blur(18px)}
:root[data-theme-palette=magical-girl] .global-particle-ribbon{display:none} :root[data-theme-palette=magical-girl] .global-particle-ribbon{display:none}
.global-particle-frame{width:120px;height:120px;border:2px solid color-mix(in srgb,var(--gold) 44%,transparent);opacity:.46}
.global-particle-frame-tl{top:18px;left:20px;border-right:0;border-bottom:0}
.global-particle-frame-br{right:22px;bottom:24px;border-left:0;border-top:0}
.global-particle-glint{--particle-index:0;--particle-x:12%;--particle-y:16%;left:var(--particle-x);top:var(--particle-y);width:9px;height:9px;opacity:.46;transform:rotate(45deg)} .global-particle-glint{--particle-index:0;--particle-x:12%;--particle-y:16%;left:var(--particle-x);top:var(--particle-y);width:9px;height:9px;opacity:.46;transform:rotate(45deg)}
.global-particle-glint::after,.global-particle-glint::before{content:"";position:absolute;inset:0;background:color-mix(in srgb,var(--sparkle) 76%,transparent);border-radius:999px} .global-particle-glint::after,.global-particle-glint::before{content:"";position:absolute;inset:0;background:color-mix(in srgb,var(--sparkle) 76%,transparent);border-radius:999px}
.global-particle-glint::after{transform:rotate(90deg)} .global-particle-glint::after{transform:rotate(90deg)}
:root[data-theme-palette=mecha-black] .global-particle-ribbon{background:repeating-linear-gradient(90deg,transparent 0 80px,color-mix(in srgb,var(--gold) 24%,transparent) 81px 86px,transparent 87px 150px);opacity:.32;filter:blur(2px)} :root[data-theme-palette=mecha-black] .global-particle-ribbon{background:repeating-linear-gradient(90deg,transparent 0 80px,color-mix(in srgb,var(--gold) 24%,transparent) 81px 86px,transparent 87px 150px);opacity:.32;filter:blur(2px)}
:root[data-theme-palette=mecha-black] .global-particle-glint{width:6px;height:18px;border:1px solid color-mix(in srgb,var(--accent) 54%,transparent);background:color-mix(in srgb,var(--accent) 18%,transparent)} :root[data-theme-palette=mecha-black] .global-particle-glint{width:6px;height:18px;border:1px solid color-mix(in srgb,var(--accent) 54%,transparent);background:color-mix(in srgb,var(--accent) 18%,transparent)}
:root[data-theme-palette=mecha-black] .global-particle-glint::after,:root[data-theme-palette=mecha-black] .global-particle-glint::before{display:none} :root[data-theme-palette=mecha-black] .global-particle-glint::after,:root[data-theme-palette=mecha-black] .global-particle-glint::before{display:none}
:root[data-theme-palette=magical-girl] .global-particle-orbit{border-color:color-mix(in srgb,var(--sparkle) 62%,transparent);box-shadow:0 0 44px color-mix(in srgb,var(--pink) 28%,transparent),inset 0 0 34px color-mix(in srgb,var(--gold) 14%,transparent)} @media (prefers-reduced-motion:no-preference){.global-particle-sweep{animation:particle-sweep 12s ease-in-out infinite}
:root[data-theme-palette=magical-girl] .global-particle-frame{border-radius:22px;box-shadow:0 0 22px color-mix(in srgb,var(--gold) 22%,transparent)}
@media (prefers-reduced-motion:no-preference){.global-particle-orbit{animation:particle-orbit-drift 22s linear infinite}
.global-particle-orbit-secondary{animation-duration:30s;animation-direction:reverse}
.global-particle-sweep{animation:particle-sweep 12s ease-in-out infinite}
.global-particle-glint{animation:particle-glint 4.8s ease-in-out infinite;animation-delay:calc(var(--particle-index) * -.42s)} .global-particle-glint{animation:particle-glint 4.8s ease-in-out infinite;animation-delay:calc(var(--particle-index) * -.42s)}
} }
@media (prefers-reduced-motion:reduce){.global-particle-layer *{animation:none!important} @media (prefers-reduced-motion:reduce){.global-particle-layer *{animation:none!important}
} }
@keyframes particle-orbit-drift{from{transform:rotate(12deg)}
to{transform:rotate(372deg)}
}
@keyframes particle-sweep{0%,100%{transform:translateX(-18vw) rotate(24deg);opacity:.18} @keyframes particle-sweep{0%,100%{transform:translateX(-18vw) rotate(24deg);opacity:.18}
48%{transform:translateX(18vw) rotate(24deg);opacity:.46} 48%{transform:translateX(18vw) rotate(24deg);opacity:.46}
} }
@keyframes particle-glint{0%,100%{opacity:.18;transform:scale(.72) rotate(45deg)} @keyframes particle-glint{0%,100%{opacity:.18;transform:scale(.72) rotate(45deg)}
50%{opacity:.68;transform:scale(1.16) rotate(45deg)} 50%{opacity:.68;transform:scale(1.16) rotate(45deg)}
} }
.global-orbital-construct{--construct-index:0;--construct-x:78%;--construct-y:78%;--construct-size:min(42vw,520px);--construct-drift:0deg;left:var(--construct-x);top:var(--construct-y);width:var(--construct-size);aspect-ratio:1;border-radius:50%;clip-path:circle(49% at 50% 50%);opacity:0;overflow:hidden;transform:translate(-50%,-50%) rotate(var(--construct-drift));background:var(--orbital-construct-image) center/contain no-repeat transparent;filter:var(--orbital-construct-filter);mix-blend-mode:screen}
.global-orbital-construct::before,.global-orbital-construct::after{content:none}
.global-particle-lightfall{top:-28vh;width:clamp(54px,8vw,112px);height:152vh;background:var(--magic-lightfall,linear-gradient(180deg,transparent,rgba(154,247,255,.34),transparent));opacity:.32;filter:blur(10px);transform:translate3d(0,-8vh,0) rotate(8deg);mix-blend-mode:screen}
.global-particle-lightfall-a{left:18vw}
.global-particle-lightfall-b{left:54vw;width:clamp(72px,10vw,136px);opacity:.24;transform:translate3d(0,-18vh,0) rotate(11deg)}
.global-particle-lightfall-c{left:82vw;width:clamp(42px,6vw,84px);opacity:.2;transform:translate3d(0,-4vh,0) rotate(6deg)}
.global-particle-side-ray{display:none;top:9vh;width:min(42vw,520px);height:74vh;background:var(--magic-side-rays,linear-gradient(90deg,color-mix(in srgb,var(--accent) 24%,transparent),transparent));opacity:.34;filter:blur(11px);transform:translate3d(0,0,0) skewY(-12deg);mix-blend-mode:screen}
.global-particle-side-ray-left{left:-11vw}
.global-particle-side-ray-right{right:-11vw;transform:scaleX(-1) skewY(-12deg)}
:root[data-theme-palette=mecha-black] .global-orbital-construct{display:block;opacity:.22}
:root[data-theme-palette=mecha-black] .global-particle-side-ray{display:none}
:root[data-theme-palette=magical-girl] .global-orbital-construct{display:block;opacity:.24}
:root[data-theme-palette=magical-girl] .global-particle-lightfall{opacity:.14;filter:blur(15px)}
:root[data-theme-palette=magical-girl] .global-particle-side-ray{display:block}
:root[data-custom-background=true] .global-orbital-construct{opacity:.2}
:root[data-custom-background=true][data-theme-palette=mecha-black] .global-orbital-construct{display:block;opacity:.16}
:root[data-custom-background=true][data-theme-palette=magical-girl] .global-orbital-construct{opacity:.18}
:root[data-custom-background=true] .global-particle-lightfall{opacity:.2}
:root[data-custom-background=true] .global-particle-side-ray{opacity:.28}
@media (prefers-reduced-motion:no-preference){.global-orbital-construct{animation:orbital-construct-drift 26s linear infinite;animation-delay:calc(var(--construct-index) * -5s)}
.global-particle-lightfall{animation:magic-lightfall-fall 13s ease-in-out infinite}
.global-particle-lightfall-b{animation-duration:17s;animation-delay:-7s}
.global-particle-lightfall-c{animation-duration:19s;animation-delay:-3s}
.global-particle-side-ray{animation:magic-side-ray-pulse 7s ease-in-out infinite}
.global-particle-side-ray-right{animation-name:magic-side-ray-pulse-right;animation-delay:-3.5s}
}
@keyframes orbital-construct-drift{from{transform:translate(-50%,-50%) rotate(var(--construct-drift))}
to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))}
}
@keyframes magic-lightfall-fall{0%,100%{opacity:.14;transform:translate3d(0,-24vh,0) rotate(8deg)}
45%{opacity:.42;transform:translate3d(0,8vh,0) rotate(8deg)}
}
@keyframes magic-side-ray-pulse{0%,100%{opacity:.16;transform:translate3d(-4vw,0,0) skewY(-12deg)}
50%{opacity:.44;transform:translate3d(2vw,0,0) skewY(-12deg)}
}
@keyframes magic-side-ray-pulse-right{0%,100%{opacity:.16;transform:translate3d(4vw,0,0) scaleX(-1) skewY(-12deg)}
50%{opacity:.44;transform:translate3d(-2vw,0,0) scaleX(-1) skewY(-12deg)}
}
.action-strip button,.app-account-button,.app-menu-toggle,.auth-mode-tabs button,.confirm-actions button,.icon-command,.primary-command,.profile-save-button,.row-actions button,.runtime-action-item,.section-tab,.segmented-button,.state-action,.table-link-button,.theme-upload{position:relative;isolation:isolate;overflow:hidden;transition:border-color 140ms ease,box-shadow 160ms ease,color 140ms ease,filter 160ms ease,transform 120ms ease}
.action-strip button::before,.app-account-button::before,.app-menu-toggle::before,.auth-mode-tabs button::before,.confirm-actions button::before,.icon-command::before,.primary-command::before,.profile-save-button::before,.row-actions button::before,.runtime-action-item::before,.section-tab::before,.segmented-button::before,.state-action::before,.table-link-button::before,.theme-upload::before{content:"";position:absolute;inset:-64%;z-index:0;border-radius:inherit;background:var(--command-ring-sheen,radial-gradient(circle at 50% 50%,color-mix(in srgb,var(--accent) 26%,transparent),transparent 64%)),conic-gradient(from 0deg,transparent 0 18%,color-mix(in srgb,var(--accent) 34%,transparent) 20% 22%,transparent 24% 54%,color-mix(in srgb,var(--gold) 26%,transparent) 56% 58%,transparent 60% 100%);opacity:0;pointer-events:none;transform:scale(.68) rotate(-14deg);transition:opacity 150ms ease,transform 420ms cubic-bezier(.16,1,.3,1);mix-blend-mode:screen}
.action-strip button>* ,.app-account-button>* ,.app-menu-toggle>* ,.auth-mode-tabs button>* ,.confirm-actions button>* ,.icon-command>* ,.primary-command>* ,.profile-save-button>* ,.row-actions button>* ,.runtime-action-item>* ,.section-tab>* ,.segmented-button>* ,.state-action>* ,.table-link-button>* ,.theme-upload>*{position:relative;z-index:1}
.action-strip button:focus-visible::before,.action-strip button:hover::before,.app-account-button:focus-visible::before,.app-account-button:hover::before,.app-menu-toggle:focus-visible::before,.app-menu-toggle:hover::before,.auth-mode-tabs button:focus-visible::before,.auth-mode-tabs button:hover::before,.confirm-actions button:focus-visible::before,.confirm-actions button:hover::before,.icon-command:focus-visible::before,.icon-command:hover::before,.primary-command:focus-visible::before,.primary-command:hover::before,.profile-save-button:focus-visible::before,.profile-save-button:hover::before,.row-actions button:focus-visible::before,.row-actions button:hover::before,.runtime-action-item:focus-visible::before,.runtime-action-item:hover::before,.section-tab:focus-visible::before,.section-tab:hover::before,.segmented-button:focus-visible::before,.segmented-button:hover::before,.state-action:focus-visible::before,.state-action:hover::before,.table-link-button:focus-visible::before,.table-link-button:hover::before,.theme-upload:focus-visible::before,.theme-upload:hover::before{opacity:.72;transform:scale(1) rotate(18deg)}
.action-strip button:active,.app-account-button:active,.app-menu-toggle:active,.auth-mode-tabs button:active,.confirm-actions button:active,.icon-command:active,.primary-command:active,.profile-save-button:active,.row-actions button:active,.runtime-action-item:active,.section-tab:active,.segmented-button:active,.state-action:active,.table-link-button:active,.theme-upload:active{transform:translateY(1px)}
.action-strip button:disabled::before,.icon-command:disabled::before,.primary-command:disabled::before,.row-actions button:disabled::before,.theme-upload:disabled::before{content:none}
.app-nav-group-button{overflow:hidden;isolation:isolate;transition:border-color 140ms ease,box-shadow 160ms ease,color 140ms ease,transform 120ms ease}
.app-nav-group-button::after{content:"";position:absolute;inset:-70%;z-index:0;border-radius:inherit;background:var(--command-ring-sheen,radial-gradient(circle,color-mix(in srgb,var(--accent) 24%,transparent),transparent 66%));opacity:0;transform:scale(.64) rotate(-16deg);transition:opacity 150ms ease,transform 420ms cubic-bezier(.16,1,.3,1);pointer-events:none;mix-blend-mode:screen}
.app-nav-group-button:focus-visible::after,.app-nav-group-button:hover::after,.app-nav-item-active::after{opacity:.62;transform:scale(1) rotate(14deg)}
.app-nav-group-button:active{transform:translateY(1px)}
.app-nav-copy{position:relative;z-index:1}
.app-nav-group-button::before{z-index:1}
.mobile-sidebar-handle::after{background:var(--command-ring-sheen),var(--frame-accessory-top) center top/28px 10px no-repeat;background-repeat:no-repeat;opacity:.78}
@media (prefers-reduced-motion:reduce){.action-strip button,.app-account-button,.app-menu-toggle,.app-nav-group-button,.auth-mode-tabs button,.confirm-actions button,.icon-command,.primary-command,.profile-save-button,.row-actions button,.runtime-action-item,.section-tab,.segmented-button,.state-action,.table-link-button,.theme-upload{transition:none}
.action-strip button::before,.app-account-button::before,.app-menu-toggle::before,.app-nav-group-button::after,.auth-mode-tabs button::before,.confirm-actions button::before,.icon-command::before,.primary-command::before,.profile-save-button::before,.row-actions button::before,.runtime-action-item::before,.section-tab::before,.segmented-button::before,.state-action::before,.table-link-button::before,.theme-upload::before{transition:none;transform:none}
}
.mobile-sidebar-backdrop,.mobile-sidebar-close,.mobile-sidebar-handle{display:none} .mobile-sidebar-backdrop,.mobile-sidebar-close,.mobile-sidebar-handle{display:none}
@keyframes mobile-sidebar-beacon{0%,100%{opacity:.48;transform:scaleY(.72)} @keyframes mobile-sidebar-beacon{0%,100%{opacity:.48;transform:scaleY(.72)}
50%{opacity:1;transform:scaleY(1)} 50%{opacity:1;transform:scaleY(1)}
@@ -193,7 +233,8 @@ to{transform:rotate(372deg)}
.summary-tone-warning{border-left-color:var(--gold)} .summary-tone-warning{border-left-color:var(--gold)}
.summary-tone-neutral{border-left-color:var(--accent)} .summary-tone-neutral{border-left-color:var(--accent)}
.metric-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px} .metric-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
.metric-card,.overview-card{min-height:104px;display:grid;align-content:space-between;gap:12px;padding:16px;border:1px solid var(--line);border-radius:8px;background:var(--panel-material);backdrop-filter:blur(22px) saturate(1.28);box-shadow:var(--panel-shadow);position:relative;isolation:isolate;overflow:hidden} .catalog-card,.console-panel{border:1px solid var(--line);border-radius:8px;background:var(--sugar-dust),linear-gradient(135deg,color-mix(in srgb,var(--surface) 22%,transparent),color-mix(in srgb,var(--surface-raised) 14%,transparent));-webkit-backdrop-filter:blur(10px) saturate(.9);backdrop-filter:blur(10px) saturate(.9);box-shadow:var(--panel-shadow);padding:16px;position:relative;isolation:isolate;overflow:hidden}
.metric-card,.overview-card{min-height:104px;display:grid;align-content:space-between;gap:12px;padding:16px;border:1px solid var(--line);border-radius:8px;background:var(--sugar-dust),linear-gradient(135deg,color-mix(in srgb,var(--surface) 22%,transparent),color-mix(in srgb,var(--surface-raised) 14%,transparent));-webkit-backdrop-filter:blur(10px) saturate(.9);backdrop-filter:blur(10px) saturate(.9);box-shadow:var(--panel-shadow);position:relative;isolation:isolate;overflow:hidden}
.catalog-card::before,.console-panel::before,.metric-card::before,.overview-card::before,.server-card::before,.server-detail-header::before{content:"";position:absolute;inset:0;border-radius:inherit;padding:1px;background:linear-gradient(135deg,rgba(255,255,255,.9),color-mix(in srgb,var(--accent) 35%,transparent),color-mix(in srgb,var(--pink) 24%,transparent),rgba(255,255,255,.46));mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude;opacity:.86;z-index:2;pointer-events:none} .catalog-card::before,.console-panel::before,.metric-card::before,.overview-card::before,.server-card::before,.server-detail-header::before{content:"";position:absolute;inset:0;border-radius:inherit;padding:1px;background:linear-gradient(135deg,rgba(255,255,255,.9),color-mix(in srgb,var(--accent) 35%,transparent),color-mix(in srgb,var(--pink) 24%,transparent),rgba(255,255,255,.46));mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude;opacity:.86;z-index:2;pointer-events:none}
.catalog-card::after,.confirm-panel::after,.console-panel::after,.drawer-panel::after,.metric-card::after,.overview-card::after,.server-card::after,.server-detail-header::after{content:"";position:absolute;inset:0;background:var(--frame-accessory-top),var(--frame-accessory-bottom),var(--shine-sweep),var(--jelly-highlight);background-size:var(--frame-accessory-size),34px 34px,auto;background-repeat:no-repeat,no-repeat,no-repeat,no-repeat;background-position:right 6px top 5px,left 6px bottom 5px,right 10px top 8px,center;opacity:var(--frame-accessory-opacity);z-index:0;pointer-events:none} .catalog-card::after,.confirm-panel::after,.console-panel::after,.drawer-panel::after,.metric-card::after,.overview-card::after,.server-card::after,.server-detail-header::after{content:"";position:absolute;inset:0;background:var(--frame-accessory-top),var(--frame-accessory-bottom),var(--shine-sweep),var(--jelly-highlight);background-size:var(--frame-accessory-size),34px 34px,auto;background-repeat:no-repeat,no-repeat,no-repeat,no-repeat;background-position:right 6px top 5px,left 6px bottom 5px,right 10px top 8px,center;opacity:var(--frame-accessory-opacity);z-index:0;pointer-events:none}
.metric-label{color:var(--ink-faint);font-size:13px} .metric-label{color:var(--ink-faint);font-size:13px}
@@ -207,12 +248,11 @@ to{transform:rotate(372deg)}
.console-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px} .console-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
.overview-card{min-height:132px;gap:10px} .overview-card{min-height:132px;gap:10px}
.overview-card p{margin:0;color:var(--ink-faint);font-size:13px} .overview-card p{margin:0;color:var(--ink-faint);font-size:13px}
.catalog-card,.console-panel{border:1px solid var(--line);border-radius:8px;background:var(--panel-material);backdrop-filter:blur(22px) saturate(1.28);box-shadow:var(--panel-shadow);padding:16px;position:relative;isolation:isolate;overflow:hidden}
:where( :where(
.metric-card,.overview-card,.console-panel,.catalog-card,.server-card,.server-detail-header,.resource-table-wrap,.provider-table-wrap,.server-table-wrap,.plugin-group,.operation-item,.confirm-panel,.drawer-panel .metric-card,.overview-card,.console-panel,.catalog-card,.server-card,.server-detail-header,.resource-table-wrap,.provider-table-wrap,.server-table-wrap,.plugin-group,.operation-item,.confirm-panel,.drawer-panel
)>*{position:relative;z-index:1} )>*{position:relative;z-index:1}
:root[data-theme-palette=mecha-black] .catalog-card,:root[data-theme-palette=mecha-black] .confirm-panel,:root[data-theme-palette=mecha-black] .console-panel,:root[data-theme-palette=mecha-black] .drawer-panel,:root[data-theme-palette=mecha-black] .metric-card,:root[data-theme-palette=mecha-black] .operation-item,:root[data-theme-palette=mecha-black] .overview-card,:root[data-theme-palette=mecha-black] .plugin-group,:root[data-theme-palette=mecha-black] .server-card{clip-path:polygon(0 0,calc(100% - 12px) 0,100% 12px,100% 100%,12px 100%,0 calc(100% - 12px))} :root[data-theme-palette=mecha-black] .catalog-card,:root[data-theme-palette=mecha-black] .confirm-panel,:root[data-theme-palette=mecha-black] .console-panel,:root[data-theme-palette=mecha-black] .drawer-panel,:root[data-theme-palette=mecha-black] .metric-card,:root[data-theme-palette=mecha-black] .operation-item,:root[data-theme-palette=mecha-black] .overview-card,:root[data-theme-palette=mecha-black] .plugin-group,:root[data-theme-palette=mecha-black] .server-card{clip-path:polygon(0 0,calc(100% - 12px) 0,100% 12px,100% 100%,12px 100%,0 calc(100% - 12px))}
:root[data-theme-palette=magical-girl] .catalog-card,:root[data-theme-palette=magical-girl] .confirm-panel,:root[data-theme-palette=magical-girl] .console-panel,:root[data-theme-palette=magical-girl] .drawer-panel,:root[data-theme-palette=magical-girl] .metric-card,:root[data-theme-palette=magical-girl] .operation-item,:root[data-theme-palette=magical-girl] .overview-card,:root[data-theme-palette=magical-girl] .plugin-group,:root[data-theme-palette=magical-girl] .provider-table-wrap,:root[data-theme-palette=magical-girl] .resource-table-wrap,:root[data-theme-palette=magical-girl] .server-card,:root[data-theme-palette=magical-girl] .server-detail-header,:root[data-theme-palette=magical-girl] .server-table-wrap{background:var(--panel-material);overflow:visible} :root[data-theme-palette=magical-girl] .catalog-card,:root[data-theme-palette=magical-girl] .confirm-panel,:root[data-theme-palette=magical-girl] .console-panel,:root[data-theme-palette=magical-girl] .drawer-panel,:root[data-theme-palette=magical-girl] .metric-card,:root[data-theme-palette=magical-girl] .operation-item,:root[data-theme-palette=magical-girl] .overview-card,:root[data-theme-palette=magical-girl] .plugin-group,:root[data-theme-palette=magical-girl] .provider-table-wrap,:root[data-theme-palette=magical-girl] .resource-table-wrap,:root[data-theme-palette=magical-girl] .server-card,:root[data-theme-palette=magical-girl] .server-detail-header,:root[data-theme-palette=magical-girl] .server-table-wrap{background:var(--sugar-dust),linear-gradient(180deg,color-mix(in srgb,var(--surface) 16%,transparent),color-mix(in srgb,var(--surface-raised) 10%,transparent));overflow:visible}
:root[data-theme-palette=magical-girl] :where( :root[data-theme-palette=magical-girl] :where(
.metric-card,.overview-card,.console-panel,.catalog-card,.server-card,.server-detail-header,.resource-table-wrap,.provider-table-wrap,.server-table-wrap,.drawer-panel,.confirm-panel,.plugin-group,.operation-item,.server-toolbar,.state-view .metric-card,.overview-card,.console-panel,.catalog-card,.server-card,.server-detail-header,.resource-table-wrap,.provider-table-wrap,.server-table-wrap,.drawer-panel,.confirm-panel,.plugin-group,.operation-item,.server-toolbar,.state-view
){--frame-accessory-image:var(--frame-accessory-heart)} ){--frame-accessory-image:var(--frame-accessory-heart)}
@@ -240,8 +280,8 @@ to{transform:rotate(372deg)}
:root[data-theme-palette=magical-girl] .catalog-card::after,:root[data-theme-palette=magical-girl] .confirm-panel::after,:root[data-theme-palette=magical-girl] .console-panel::after,:root[data-theme-palette=magical-girl] .drawer-panel::after,:root[data-theme-palette=magical-girl] .metric-card::after,:root[data-theme-palette=magical-girl] .overview-card::after,:root[data-theme-palette=magical-girl] .server-card::after,:root[data-theme-palette=magical-girl] .server-detail-header::after{inset:-14px;background-image:var(--frame-accessory-image);background-size:var(--frame-accessory-size);background-repeat:no-repeat;background-position:right -4px top -6px;opacity:var(--frame-accessory-opacity);filter:drop-shadow(0 0 4px rgba(255, 119, 200, .58)) drop-shadow(0 0 2px rgba(255, 221, 117, .42))} :root[data-theme-palette=magical-girl] .catalog-card::after,:root[data-theme-palette=magical-girl] .confirm-panel::after,:root[data-theme-palette=magical-girl] .console-panel::after,:root[data-theme-palette=magical-girl] .drawer-panel::after,:root[data-theme-palette=magical-girl] .metric-card::after,:root[data-theme-palette=magical-girl] .overview-card::after,:root[data-theme-palette=magical-girl] .server-card::after,:root[data-theme-palette=magical-girl] .server-detail-header::after{inset:-14px;background-image:var(--frame-accessory-image);background-size:var(--frame-accessory-size);background-repeat:no-repeat;background-position:right -4px top -6px;opacity:var(--frame-accessory-opacity);filter:drop-shadow(0 0 4px rgba(255, 119, 200, .58)) drop-shadow(0 0 2px rgba(255, 221, 117, .42))}
:root[data-theme-palette=mecha-black] .catalog-card::before,:root[data-theme-palette=mecha-black] .console-panel::before,:root[data-theme-palette=mecha-black] .metric-card::before,:root[data-theme-palette=mecha-black] .overview-card::before,:root[data-theme-palette=mecha-black] .server-card::before,:root[data-theme-palette=mecha-black] .server-detail-header::before{background:linear-gradient(135deg,rgba(72,230,255,.72),rgba(255,184,77,.42),rgba(72,230,255,.18));opacity:.76} :root[data-theme-palette=mecha-black] .catalog-card::before,:root[data-theme-palette=mecha-black] .console-panel::before,:root[data-theme-palette=mecha-black] .metric-card::before,:root[data-theme-palette=mecha-black] .overview-card::before,:root[data-theme-palette=mecha-black] .server-card::before,:root[data-theme-palette=mecha-black] .server-detail-header::before{background:linear-gradient(135deg,rgba(72,230,255,.72),rgba(255,184,77,.42),rgba(72,230,255,.18));opacity:.76}
:root[data-custom-background=true] .app-sidebar{background:radial-gradient(circle at 92% 3%,rgba(255,255,255,.18) 0 2px,transparent 3px),linear-gradient(180deg,rgba(8,10,16,.42),rgba(8,10,16,.3))} :root[data-custom-background=true] .app-sidebar{background:radial-gradient(circle at 92% 3%,rgba(255,255,255,.18) 0 2px,transparent 3px),linear-gradient(180deg,rgba(8,10,16,.42),rgba(8,10,16,.3))}
:root[data-custom-background=true]{--frame-accessory-opacity:0.04;--surface:rgba(5, 10, 16, 0.76);--surface-solid:rgba(5, 10, 16, 0.92);--surface-raised:rgba(7, 13, 20, 0.82);--glass-tint:transparent;--glass-wash:linear-gradient(145deg, rgba(255, 255, 255, 0.08), rgba(5, 10, 16, 0.76) 44%, rgba(255, 255, 255, 0.03));--panel-material:linear-gradient(145deg, rgba(255, 255, 255, 0.075), rgba(5, 10, 16, 0.86) 48%, rgba(5, 10, 16, 0.76));--panel-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.13),inset 0 0 0 1px rgba(255, 255, 255, 0.045),0 18px 38px rgba(0, 0, 0, 0.32)} :root[data-custom-background=true]{--frame-accessory-opacity:0.04;--surface:rgba(5, 10, 16, 0.64);--surface-solid:rgba(5, 10, 16, 0.84);--surface-raised:rgba(7, 13, 20, 0.68);--glass-tint:transparent;--glass-wash:linear-gradient(145deg, rgba(255, 255, 255, 0.06), rgba(5, 10, 16, 0.58) 44%, rgba(255, 255, 255, 0.025));--panel-material:linear-gradient(145deg, rgba(255, 255, 255, 0.055), rgba(5, 10, 16, 0.68) 48%, rgba(5, 10, 16, 0.56));--panel-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.13),inset 0 0 0 1px rgba(255, 255, 255, 0.045),0 18px 38px rgba(0, 0, 0, 0.32)}
:root[data-custom-background=true][data-theme-palette=magical-girl]{--frame-accessory-opacity:0.22;--surface:rgba(22, 22, 29, 0.64);--surface-solid:rgba(18, 19, 26, 0.88);--surface-raised:rgba(25, 24, 32, 0.72);--line:rgba(238, 215, 236, 0.34);--line-strong:rgba(250, 240, 249, 0.5);--glass-wash:linear-gradient(145deg, rgba(255, 246, 253, 0.08), rgba(22, 22, 29, 0.66) 48%, rgba(255, 221, 117, 0.025));--panel-material:linear-gradient(145deg, rgba(255, 246, 253, 0.08), rgba(22, 22, 29, 0.72) 44%, rgba(18, 19, 26, 0.64));--panel-shadow:inset 0 1px 0 rgba(255, 246, 253, 0.18),inset 0 0 0 1px rgba(255, 185, 226, 0.06),0 16px 34px rgba(12, 12, 18, 0.3)} :root[data-custom-background=true][data-theme-palette=magical-girl]{--frame-accessory-opacity:0.22;--surface:rgba(22, 22, 29, 0.42);--surface-solid:rgba(18, 19, 26, 0.66);--surface-raised:rgba(25, 24, 32, 0.5);--line:rgba(238, 215, 236, 0.34);--line-strong:rgba(250, 240, 249, 0.5);--glass-wash:linear-gradient(145deg, rgba(255, 246, 253, 0.045), rgba(22, 22, 29, 0.36) 48%, rgba(255, 221, 117, 0.018));--panel-material:linear-gradient(145deg, rgba(255, 246, 253, 0.045), rgba(22, 22, 29, 0.4) 44%, rgba(18, 19, 26, 0.34));--panel-shadow:inset 0 1px 0 rgba(255, 246, 253, 0.18),inset 0 0 0 1px rgba(255, 185, 226, 0.06),0 16px 34px rgba(12, 12, 18, 0.3)}
:root[data-custom-background=true][data-theme-palette=magical-girl] .app-sidebar{border-right-color:rgba(242,224,240,.26);background:linear-gradient(180deg,rgba(34,31,40,.72),rgba(18,19,26,.68)),rgba(18,18,24,.5);-webkit-backdrop-filter:blur(14px) saturate(0.92);backdrop-filter:blur(14px) saturate(0.92);box-shadow:inset -1px 0 0 rgba(255,246,253,.12),10px 0 28px rgba(12,12,18,.26)} :root[data-custom-background=true][data-theme-palette=magical-girl] .app-sidebar{border-right-color:rgba(242,224,240,.26);background:linear-gradient(180deg,rgba(34,31,40,.72),rgba(18,19,26,.68)),rgba(18,18,24,.5);-webkit-backdrop-filter:blur(14px) saturate(0.92);backdrop-filter:blur(14px) saturate(0.92);box-shadow:inset -1px 0 0 rgba(255,246,253,.12),10px 0 28px rgba(12,12,18,.26)}
:root[data-custom-background=true][data-theme-palette=magical-girl] .page-header>div:first-child{border-color:rgba(242,224,240,.18);background:linear-gradient(135deg,rgba(255,246,253,.075),transparent 36%),rgba(22,22,29,.58);-webkit-backdrop-filter:blur(8px) saturate(0.9);backdrop-filter:blur(8px) saturate(0.9)} :root[data-custom-background=true][data-theme-palette=magical-girl] .page-header>div:first-child{border-color:rgba(242,224,240,.18);background:linear-gradient(135deg,rgba(255,246,253,.075),transparent 36%),rgba(22,22,29,.58);-webkit-backdrop-filter:blur(8px) saturate(0.9);backdrop-filter:blur(8px) saturate(0.9)}
:root[data-custom-background=true] .app-sidebar{border-right-color:rgba(210,239,246,.24);background:linear-gradient(180deg,rgba(5,10,16,.76),rgba(5,9,14,.68)),rgba(4,8,12,.58);-webkit-backdrop-filter:blur(18px) saturate(0.9);backdrop-filter:blur(18px) saturate(0.9);box-shadow:inset -1px 0 0 rgba(255,255,255,.12),10px 0 28px rgba(0,0,0,.22)} :root[data-custom-background=true] .app-sidebar{border-right-color:rgba(210,239,246,.24);background:linear-gradient(180deg,rgba(5,10,16,.76),rgba(5,9,14,.68)),rgba(4,8,12,.58);-webkit-backdrop-filter:blur(18px) saturate(0.9);backdrop-filter:blur(18px) saturate(0.9);box-shadow:inset -1px 0 0 rgba(255,255,255,.12),10px 0 28px rgba(0,0,0,.22)}
@@ -251,9 +291,9 @@ to{transform:rotate(372deg)}
:root[data-custom-background=true] .page-header>div:first-child{display:grid;justify-items:start;padding:7px 12px 9px;border:1px solid rgba(255,255,255,.08);border-radius:8px;background:linear-gradient(135deg,rgba(255,255,255,.055),transparent 36%),rgba(5,10,16,.38);-webkit-backdrop-filter:blur(10px) saturate(0.8);backdrop-filter:blur(10px) saturate(0.8);box-shadow:0 10px 22px rgba(0,0,0,.18)} :root[data-custom-background=true] .page-header>div:first-child{display:grid;justify-items:start;padding:7px 12px 9px;border:1px solid rgba(255,255,255,.08);border-radius:8px;background:linear-gradient(135deg,rgba(255,255,255,.055),transparent 36%),rgba(5,10,16,.38);-webkit-backdrop-filter:blur(10px) saturate(0.8);backdrop-filter:blur(10px) saturate(0.8);box-shadow:0 10px 22px rgba(0,0,0,.18)}
:root[data-custom-background=true] .page-kicker{margin-bottom:4px;text-shadow:0 1px 2px rgba(0,0,0,.42)} :root[data-custom-background=true] .page-kicker{margin-bottom:4px;text-shadow:0 1px 2px rgba(0,0,0,.42)}
:root[data-custom-background=true] .app-nav-item-active{background:linear-gradient(90deg,color-mix(in srgb,var(--accent) 72%,transparent) 0 3px,transparent 3px),linear-gradient(135deg,rgba(7,16,23,.78),rgba(7,14,20,.56));border-color:color-mix(in srgb,var(--accent) 64%,rgba(255,255,255,.18));box-shadow:inset 0 0 0 1px rgba(255,255,255,.12),0 8px 22px rgba(0,0,0,.22)} :root[data-custom-background=true] .app-nav-item-active{background:linear-gradient(90deg,color-mix(in srgb,var(--accent) 72%,transparent) 0 3px,transparent 3px),linear-gradient(135deg,rgba(7,16,23,.78),rgba(7,14,20,.56));border-color:color-mix(in srgb,var(--accent) 64%,rgba(255,255,255,.18));box-shadow:inset 0 0 0 1px rgba(255,255,255,.12),0 8px 22px rgba(0,0,0,.22)}
:root[data-custom-background=true] .catalog-card,:root[data-custom-background=true] .confirm-panel,:root[data-custom-background=true] .console-panel,:root[data-custom-background=true] .drawer-panel,:root[data-custom-background=true] .metric-card,:root[data-custom-background=true] .operation-item,:root[data-custom-background=true] .overview-card,:root[data-custom-background=true] .plugin-group,:root[data-custom-background=true] .provider-table-wrap,:root[data-custom-background=true] .resource-table-wrap,:root[data-custom-background=true] .server-card,:root[data-custom-background=true] .server-detail-header,:root[data-custom-background=true] .server-table-wrap,:root[data-custom-background=true] .server-toolbar,:root[data-custom-background=true] .state-view{-webkit-backdrop-filter:blur(16px) saturate(0.78);backdrop-filter:blur(16px) saturate(0.78)} :root[data-custom-background=true] .catalog-card,:root[data-custom-background=true] .confirm-panel,:root[data-custom-background=true] .console-panel,:root[data-custom-background=true] .drawer-panel,:root[data-custom-background=true] .metric-card,:root[data-custom-background=true] .operation-item,:root[data-custom-background=true] .overview-card,:root[data-custom-background=true] .plugin-group,:root[data-custom-background=true] .provider-table-wrap,:root[data-custom-background=true] .resource-table-wrap,:root[data-custom-background=true] .server-card,:root[data-custom-background=true] .server-detail-header,:root[data-custom-background=true] .server-table-wrap,:root[data-custom-background=true] .server-toolbar,:root[data-custom-background=true] .state-view{-webkit-backdrop-filter:blur(10px) saturate(0.86);backdrop-filter:blur(10px) saturate(0.86)}
:root[data-custom-background=true] .catalog-card,:root[data-custom-background=true] .confirm-panel,:root[data-custom-background=true] .console-panel,:root[data-custom-background=true] .drawer-panel,:root[data-custom-background=true] .metric-card,:root[data-custom-background=true] .operation-item,:root[data-custom-background=true] .overview-card,:root[data-custom-background=true] .plugin-group,:root[data-custom-background=true] .provider-table-wrap,:root[data-custom-background=true] .resource-table-wrap,:root[data-custom-background=true] .server-card,:root[data-custom-background=true] .server-detail-header,:root[data-custom-background=true] .server-table-wrap,:root[data-custom-background=true] .server-toolbar,:root[data-custom-background=true] .state-view{background:linear-gradient(135deg,rgba(255,255,255,.065),transparent 30%),linear-gradient(180deg,rgba(5,10,16,.86),rgba(5,10,16,.78));border-color:color-mix(in srgb,var(--line-strong) 44%,rgba(255,255,255,.16));-webkit-backdrop-filter:blur(16px) saturate(0.78);backdrop-filter:blur(16px) saturate(0.78);box-shadow:inset 0 1px 0 rgba(255,255,255,.12),inset 0 0 0 1px rgba(255,255,255,.04),0 16px 34px rgba(0,0,0,.3)} :root[data-custom-background=true] .catalog-card,:root[data-custom-background=true] .confirm-panel,:root[data-custom-background=true] .console-panel,:root[data-custom-background=true] .drawer-panel,:root[data-custom-background=true] .metric-card,:root[data-custom-background=true] .operation-item,:root[data-custom-background=true] .overview-card,:root[data-custom-background=true] .plugin-group,:root[data-custom-background=true] .provider-table-wrap,:root[data-custom-background=true] .resource-table-wrap,:root[data-custom-background=true] .server-card,:root[data-custom-background=true] .server-detail-header,:root[data-custom-background=true] .server-table-wrap,:root[data-custom-background=true] .server-toolbar,:root[data-custom-background=true] .state-view{background:linear-gradient(135deg,rgba(255,255,255,.026),transparent 30%),linear-gradient(180deg,rgba(5,10,16,.42),rgba(5,10,16,.34));border-color:color-mix(in srgb,var(--line-strong) 44%,rgba(255,255,255,.16));-webkit-backdrop-filter:blur(10px) saturate(0.86);backdrop-filter:blur(10px) saturate(0.86);box-shadow:inset 0 1px 0 rgba(255,255,255,.12),inset 0 0 0 1px rgba(255,255,255,.04),0 16px 34px rgba(0,0,0,.3)}
:root[data-custom-background=true][data-theme-palette=magical-girl] .catalog-card,:root[data-custom-background=true][data-theme-palette=magical-girl] .confirm-panel,:root[data-custom-background=true][data-theme-palette=magical-girl] .console-panel,:root[data-custom-background=true][data-theme-palette=magical-girl] .drawer-panel,:root[data-custom-background=true][data-theme-palette=magical-girl] .metric-card,:root[data-custom-background=true][data-theme-palette=magical-girl] .operation-item,:root[data-custom-background=true][data-theme-palette=magical-girl] .overview-card,:root[data-custom-background=true][data-theme-palette=magical-girl] .plugin-group,:root[data-custom-background=true][data-theme-palette=magical-girl] .provider-table-wrap,:root[data-custom-background=true][data-theme-palette=magical-girl] .resource-table-wrap,:root[data-custom-background=true][data-theme-palette=magical-girl] .server-card,:root[data-custom-background=true][data-theme-palette=magical-girl] .server-detail-header,:root[data-custom-background=true][data-theme-palette=magical-girl] .server-table-wrap,:root[data-custom-background=true][data-theme-palette=magical-girl] .server-toolbar,:root[data-custom-background=true][data-theme-palette=magical-girl] .state-view{background:radial-gradient(circle at 100% 0,rgba(255,221,242,.07),transparent 32%),linear-gradient(135deg,rgba(255,246,253,.065),transparent 34%),linear-gradient(180deg,rgba(24,24,31,.66),rgba(18,19,26,.58));border-color:color-mix(in srgb,var(--line-strong) 54%,rgba(255,255,255,.16));-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:inset 0 1px 0 rgba(255,246,253,.18),inset 0 0 0 1px rgba(255,185,226,.045),0 16px 32px rgba(12,12,18,.28)} :root[data-custom-background=true][data-theme-palette=magical-girl] .catalog-card,:root[data-custom-background=true][data-theme-palette=magical-girl] .confirm-panel,:root[data-custom-background=true][data-theme-palette=magical-girl] .console-panel,:root[data-custom-background=true][data-theme-palette=magical-girl] .drawer-panel,:root[data-custom-background=true][data-theme-palette=magical-girl] .metric-card,:root[data-custom-background=true][data-theme-palette=magical-girl] .operation-item,:root[data-custom-background=true][data-theme-palette=magical-girl] .overview-card,:root[data-custom-background=true][data-theme-palette=magical-girl] .plugin-group,:root[data-custom-background=true][data-theme-palette=magical-girl] .provider-table-wrap,:root[data-custom-background=true][data-theme-palette=magical-girl] .resource-table-wrap,:root[data-custom-background=true][data-theme-palette=magical-girl] .server-card,:root[data-custom-background=true][data-theme-palette=magical-girl] .server-detail-header,:root[data-custom-background=true][data-theme-palette=magical-girl] .server-table-wrap,:root[data-custom-background=true][data-theme-palette=magical-girl] .server-toolbar,:root[data-custom-background=true][data-theme-palette=magical-girl] .state-view{background:radial-gradient(circle at 100% 0,rgba(255,221,242,.026),transparent 32%),linear-gradient(135deg,rgba(255,246,253,.022),transparent 34%),linear-gradient(180deg,rgba(24,24,31,.22),rgba(18,19,26,.18));border-color:color-mix(in srgb,var(--line-strong) 54%,rgba(255,255,255,.16));-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:inset 0 1px 0 rgba(255,246,253,.18),inset 0 0 0 1px rgba(255,185,226,.045),0 16px 32px rgba(12,12,18,.28)}
:root[data-custom-background=true][data-theme-palette=magical-girl] .server-toolbar,:root[data-custom-background=true][data-theme-palette=magical-girl] .state-view{position:relative;isolation:isolate;overflow:visible} :root[data-custom-background=true][data-theme-palette=magical-girl] .server-toolbar,:root[data-custom-background=true][data-theme-palette=magical-girl] .state-view{position:relative;isolation:isolate;overflow:visible}
:root[data-custom-background=true][data-theme-palette=magical-girl] .server-toolbar::after,:root[data-custom-background=true][data-theme-palette=magical-girl] .state-view::after{content:"";position:absolute;inset:-14px;border-radius:inherit;background-image:var(--frame-accessory-image);background-size:var(--frame-accessory-size);background-repeat:no-repeat;background-position:right -4px top -6px;opacity:var(--frame-accessory-opacity);filter:drop-shadow(0 0 4px rgba(255, 119, 200, .62)) drop-shadow(0 0 2px rgba(255, 221, 117, .46));z-index:0;pointer-events:none} :root[data-custom-background=true][data-theme-palette=magical-girl] .server-toolbar::after,:root[data-custom-background=true][data-theme-palette=magical-girl] .state-view::after{content:"";position:absolute;inset:-14px;border-radius:inherit;background-image:var(--frame-accessory-image);background-size:var(--frame-accessory-size);background-repeat:no-repeat;background-position:right -4px top -6px;opacity:var(--frame-accessory-opacity);filter:drop-shadow(0 0 4px rgba(255, 119, 200, .62)) drop-shadow(0 0 2px rgba(255, 221, 117, .46));z-index:0;pointer-events:none}
:root[data-custom-background=true][data-theme-palette=magical-girl] .server-toolbar>*,:root[data-custom-background=true][data-theme-palette=magical-girl] .state-view>*{position:relative;z-index:1} :root[data-custom-background=true][data-theme-palette=magical-girl] .server-toolbar>*,:root[data-custom-background=true][data-theme-palette=magical-girl] .state-view>*{position:relative;z-index:1}
@@ -265,7 +305,8 @@ to{transform:rotate(372deg)}
:root[data-theme-palette=magical-girl] :where( :root[data-theme-palette=magical-girl] :where(
.metric-card,.overview-card,.console-panel,.catalog-card,.server-card,.server-detail-header,.plugin-group,.operation-item,.confirm-panel .metric-card,.overview-card,.console-panel,.catalog-card,.server-card,.server-detail-header,.plugin-group,.operation-item,.confirm-panel
){overflow:visible} ){overflow:visible}
:root[data-custom-background=true] .global-particle-layer{opacity:.03} :root[data-custom-background=true] .global-particle-layer{opacity:.42}
:root[data-custom-background=true][data-theme-palette=magical-girl] .global-particle-layer{opacity:.54}
:root[data-custom-background=true] .log-filter-bar input,:root[data-custom-background=true] .log-filter-bar select,:root[data-custom-background=true] .profile-form input,:root[data-custom-background=true] .profile-settings-form input,:root[data-custom-background=true] .profile-settings-form textarea,:root[data-custom-background=true] .provider-form input,:root[data-custom-background=true] .provider-form select,:root[data-custom-background=true] .provider-form textarea,:root[data-custom-background=true] .server-toolbar input[type=search],:root[data-custom-background=true] .server-toolbar select{background:rgba(5,10,16,.94);border-color:color-mix(in srgb,var(--accent) 48%,rgba(255,255,255,.18));color:rgba(247,253,255,.96);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 8px 18px rgba(0,0,0,.18)} :root[data-custom-background=true] .log-filter-bar input,:root[data-custom-background=true] .log-filter-bar select,:root[data-custom-background=true] .profile-form input,:root[data-custom-background=true] .profile-settings-form input,:root[data-custom-background=true] .profile-settings-form textarea,:root[data-custom-background=true] .provider-form input,:root[data-custom-background=true] .provider-form select,:root[data-custom-background=true] .provider-form textarea,:root[data-custom-background=true] .server-toolbar input[type=search],:root[data-custom-background=true] .server-toolbar select{background:rgba(5,10,16,.94);border-color:color-mix(in srgb,var(--accent) 48%,rgba(255,255,255,.18));color:rgba(247,253,255,.96);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 8px 18px rgba(0,0,0,.18)}
:root[data-custom-background=true] .action-strip button,:root[data-custom-background=true] .app-account-button,:root[data-custom-background=true] .app-session,:root[data-custom-background=true] .background-preset-option,:root[data-custom-background=true] .icon-command,:root[data-custom-background=true] .palette-option,:root[data-custom-background=true] .row-actions button,:root[data-custom-background=true] .segmented-button,:root[data-custom-background=true] .state-action,:root[data-custom-background=true] .theme-upload{background:linear-gradient(135deg,rgba(255,255,255,.055),transparent 34%),rgba(5,10,16,.84);border-color:color-mix(in srgb,var(--line-strong) 40%,rgba(255,255,255,.14));color:rgba(229,246,251,.9);box-shadow:inset 0 1px 0 rgba(255,255,255,.12),0 7px 16px rgba(0,0,0,.16)} :root[data-custom-background=true] .action-strip button,:root[data-custom-background=true] .app-account-button,:root[data-custom-background=true] .app-session,:root[data-custom-background=true] .background-preset-option,:root[data-custom-background=true] .icon-command,:root[data-custom-background=true] .palette-option,:root[data-custom-background=true] .row-actions button,:root[data-custom-background=true] .segmented-button,:root[data-custom-background=true] .state-action,:root[data-custom-background=true] .theme-upload{background:linear-gradient(135deg,rgba(255,255,255,.055),transparent 34%),rgba(5,10,16,.84);border-color:color-mix(in srgb,var(--line-strong) 40%,rgba(255,255,255,.14));color:rgba(229,246,251,.9);box-shadow:inset 0 1px 0 rgba(255,255,255,.12),0 7px 16px rgba(0,0,0,.16)}
:root[data-custom-background=true] .background-preset-option-active,:root[data-custom-background=true] .palette-option-active,:root[data-custom-background=true] .segmented-button-active{background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 10%,transparent),rgba(5,10,16,.78));border-color:color-mix(in srgb,var(--accent) 58%,rgba(255,255,255,.18));color:rgba(247,253,255,.96);box-shadow:inset 0 1px 0 rgba(255,255,255,.16),0 0 0 1px color-mix(in srgb,var(--accent) 18%,transparent),0 9px 20px rgba(0,0,0,.2)} :root[data-custom-background=true] .background-preset-option-active,:root[data-custom-background=true] .palette-option-active,:root[data-custom-background=true] .segmented-button-active{background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 10%,transparent),rgba(5,10,16,.78));border-color:color-mix(in srgb,var(--accent) 58%,rgba(255,255,255,.18));color:rgba(247,253,255,.96);box-shadow:inset 0 1px 0 rgba(255,255,255,.16),0 0 0 1px color-mix(in srgb,var(--accent) 18%,transparent),0 9px 20px rgba(0,0,0,.2)}
@@ -443,7 +484,7 @@ to{transform:rotate(372deg)}
.log-level-info{color:#7ce3d3} .log-level-info{color:#7ce3d3}
.log-line span:last-child{overflow-wrap:anywhere} .log-line span:last-child{overflow-wrap:anywhere}
.drawer-backdrop{position:fixed;inset:0;z-index:40;background:rgba(61,36,65,.34);backdrop-filter:blur(6px);display:flex;justify-content:flex-end;overflow-y:auto;overscroll-behavior:contain} .drawer-backdrop{position:fixed;inset:0;z-index:40;background:rgba(61,36,65,.34);backdrop-filter:blur(6px);display:flex;justify-content:flex-end;overflow-y:auto;overscroll-behavior:contain}
.drawer-panel{width:min(440px,100%);height:100%;max-height:100dvh;display:grid;align-content:start;gap:14px;padding:20px;background:var(--glass-wash),var(--glass-tint),var(--surface-raised);backdrop-filter:blur(24px) saturate(1.28);border-left:1px solid var(--line);box-shadow:inset 1px 0 0 var(--crystal-rim),-18px 0 42px var(--glass-shadow);position:relative;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain} .drawer-panel{width:min(440px,100%);height:100%;max-height:100dvh;display:grid;align-content:start;gap:14px;padding:20px;background:var(--glass-wash),var(--glass-tint),var(--surface-raised);backdrop-filter:blur(14px) saturate(0.28);border-left:1px solid var(--line);box-shadow:inset 1px 0 0 var(--crystal-rim),-18px 0 42px var(--glass-shadow);position:relative;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain}
.plugin-detail-panel{width:100%;height:auto;overflow:visible;border:1px solid var(--line);border-left:1px solid var(--line);border-radius:8px;box-shadow:var(--panel-shadow)} .plugin-detail-panel{width:100%;height:auto;overflow:visible;border:1px solid var(--line);border-left:1px solid var(--line);border-radius:8px;box-shadow:var(--panel-shadow)}
.management-dialog-panel{width:min(720px,calc(100vw - 32px));height:auto;max-height:min(760px,calc(100dvh - 32px));overflow-x:hidden;overflow-y:auto;border:1px solid var(--line);border-left:1px solid var(--line);border-radius:8px;box-shadow:var(--jelly-inset),inset 0 0 0 1px var(--diamond-line),0 18px 48px var(--glass-shadow),0 0 34px var(--moonbeam)} .management-dialog-panel{width:min(720px,calc(100vw - 32px));height:auto;max-height:min(760px,calc(100dvh - 32px));overflow-x:hidden;overflow-y:auto;border:1px solid var(--line);border-left:1px solid var(--line);border-radius:8px;box-shadow:var(--jelly-inset),inset 0 0 0 1px var(--diamond-line),0 18px 48px var(--glass-shadow),0 0 34px var(--moonbeam)}
.management-dialog-wide{width:min(920px,calc(100vw - 32px))} .management-dialog-wide{width:min(920px,calc(100vw - 32px))}
@@ -539,7 +580,7 @@ to{transform:rotate(372deg)}
.signal-tone-error svg{color:var(--danger)} .signal-tone-error svg{color:var(--danger)}
.signal-tone-info svg{color:var(--teal)} .signal-tone-info svg{color:var(--teal)}
.confirm-backdrop{position:fixed;inset:0;z-index:50;display:grid;place-items:center;padding:16px;background:rgba(61,36,65,.38);backdrop-filter:blur(6px);overflow-y:auto;overscroll-behavior:contain} .confirm-backdrop{position:fixed;inset:0;z-index:50;display:grid;place-items:center;padding:16px;background:rgba(61,36,65,.38);backdrop-filter:blur(6px);overflow-y:auto;overscroll-behavior:contain}
.confirm-panel{width:min(460px,100%);max-height:calc(100dvh - 32px);display:grid;gap:14px;padding:20px;border:1px solid var(--line);border-radius:8px;background:var(--frosted-surface),var(--glass-tint),var(--surface-raised);backdrop-filter:blur(24px) saturate(1.28);box-shadow:var(--jelly-inset),inset 0 0 0 1px var(--diamond-line),0 18px 48px var(--glass-shadow),0 0 34px var(--moonbeam);position:relative;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain} .confirm-panel{width:min(460px,100%);max-height:calc(100dvh - 32px);display:grid;gap:14px;padding:20px;border:1px solid var(--line);border-radius:8px;background:var(--frosted-surface),var(--glass-tint),var(--surface-raised);backdrop-filter:blur(14px) saturate(0.28);box-shadow:var(--jelly-inset),inset 0 0 0 1px var(--diamond-line),0 18px 48px var(--glass-shadow),0 0 34px var(--moonbeam);position:relative;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain}
.confirm-panel h2{margin:0;font-size:18px;color:var(--ink)} .confirm-panel h2{margin:0;font-size:18px;color:var(--ink)}
.confirm-panel p{margin:0;color:var(--ink-soft);font-size:14px} .confirm-panel p{margin:0;color:var(--ink-soft);font-size:14px}
.confirm-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap} .confirm-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
+14
View File
@@ -28,6 +28,17 @@ describe("platform web theme palettes", () => {
expect(palette.variables["--sparkle-gold"]).toContain("rgba"); expect(palette.variables["--sparkle-gold"]).toContain("rgba");
expect(palette.variables["--panel-material"]).toContain("var"); expect(palette.variables["--panel-material"]).toContain("var");
expect(palette.variables["--menu-item-bg"]).toContain("gradient"); expect(palette.variables["--menu-item-bg"]).toContain("gradient");
expect(palette.variables["--orbital-construct-image"]).toBe(
palette.id === "magical-girl" ? "url(\"/theme/magical-circle.svg\")" : "url(\"/theme/stellar-megastructure.svg\")"
);
expect(palette.variables["--orbital-construct-filter"]).toContain("drop-shadow");
expect(palette.variables["--orbital-construct-filter"]).not.toContain("invert(");
expect(palette.variables["--orbital-construct-filter"]).not.toContain("hue-rotate(");
expect(palette.variables["--orbital-construct-filter"]).not.toContain("sepia(");
expect(palette.variables["--orbital-construct-filter"]).not.toContain("brightness(");
expect(palette.variables["--magic-lightfall"]).toContain("linear-gradient");
expect(palette.variables["--magic-side-rays"]).toContain("linear-gradient");
expect(palette.variables["--command-ring-sheen"]).toContain("radial-gradient");
expect(palette.variables["--frame-accessory-top"]).toContain("data:image/svg+xml"); expect(palette.variables["--frame-accessory-top"]).toContain("data:image/svg+xml");
expect(palette.variables["--frame-accessory-bottom"]).toContain("data:image/svg+xml"); expect(palette.variables["--frame-accessory-bottom"]).toContain("data:image/svg+xml");
expect(palette.variables["--frame-accessory-opacity"]).toMatch(/^0\.\d{2}$/); expect(palette.variables["--frame-accessory-opacity"]).toMatch(/^0\.\d{2}$/);
@@ -92,7 +103,10 @@ describe("platform web theme palettes", () => {
applyThemePalette("mecha-black"); applyThemePalette("mecha-black");
expect(removeProperty).toHaveBeenCalledWith("--frame-accessory-heart"); expect(removeProperty).toHaveBeenCalledWith("--frame-accessory-heart");
expect(removeProperty).toHaveBeenCalledWith("--orbital-construct-image");
expect(removeProperty).toHaveBeenCalledWith("--magic-side-rays");
expect(setProperty).toHaveBeenCalledWith("--accent", "#48e6ff"); expect(setProperty).toHaveBeenCalledWith("--accent", "#48e6ff");
expect(setProperty).toHaveBeenCalledWith("--orbital-construct-image", "url(\"/theme/stellar-megastructure.svg\")");
expect(dispatchEvent).toHaveBeenCalledWith( expect(dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
type: themePaletteChangeEvent, type: themePaletteChangeEvent,
+24 -7
View File
@@ -90,6 +90,14 @@ const mechaMaterialVariables = {
"--frosted-surface": "linear-gradient(145deg, rgba(14, 28, 38, 0.62), rgba(5, 12, 18, 0.46) 48%, rgba(72, 230, 255, 0.07))", "--frosted-surface": "linear-gradient(145deg, rgba(14, 28, 38, 0.62), rgba(5, 12, 18, 0.46) 48%, rgba(72, 230, 255, 0.07))",
"--frame-corner": "linear-gradient(135deg, rgba(72, 230, 255, 0.9) 0 2px, transparent 2px), linear-gradient(315deg, rgba(255, 184, 77, 0.72) 0 2px, transparent 2px)", "--frame-corner": "linear-gradient(135deg, rgba(72, 230, 255, 0.9) 0 2px, transparent 2px), linear-gradient(315deg, rgba(255, 184, 77, 0.72) 0 2px, transparent 2px)",
"--frame-accent": "linear-gradient(90deg, rgba(72, 230, 255, 0), rgba(72, 230, 255, 0.78), rgba(255, 184, 77, 0.58), rgba(72, 230, 255, 0))", "--frame-accent": "linear-gradient(90deg, rgba(72, 230, 255, 0), rgba(72, 230, 255, 0.78), rgba(255, 184, 77, 0.58), rgba(72, 230, 255, 0))",
"--orbital-construct-image": "url(\"/theme/stellar-megastructure.svg\")",
"--orbital-construct-filter":
"drop-shadow(0 0 18px rgba(72, 230, 255, 0.28)) drop-shadow(0 0 34px rgba(255, 184, 77, 0.1))",
"--magic-lightfall":
"linear-gradient(180deg, transparent 0 4%, rgba(154, 247, 255, 0.34) 18%, rgba(72, 230, 255, 0.1) 42%, transparent 74%)",
"--magic-side-rays": "linear-gradient(90deg, rgba(72, 230, 255, 0.3), transparent 72%)",
"--command-ring-sheen":
"radial-gradient(circle at 50% 50%, rgba(154, 247, 255, 0.32), rgba(72, 230, 255, 0.12) 42%, transparent 68%)",
"--frame-accessory-top": mechaFrameAccessoryTop, "--frame-accessory-top": mechaFrameAccessoryTop,
"--frame-accessory-bottom": mechaFrameAccessoryBottom, "--frame-accessory-bottom": mechaFrameAccessoryBottom,
"--frame-accessory-opacity": "0.22", "--frame-accessory-opacity": "0.22",
@@ -99,15 +107,15 @@ const mechaMaterialVariables = {
"--menu-glyph-bg": "linear-gradient(135deg, rgba(72, 230, 255, 0.28), rgba(255, 184, 77, 0.14)), repeating-linear-gradient(90deg, rgba(137, 239, 255, 0.18) 0 1px, transparent 1px 6px), rgba(5, 9, 15, 0.9)", "--menu-glyph-bg": "linear-gradient(135deg, rgba(72, 230, 255, 0.28), rgba(255, 184, 77, 0.14)), repeating-linear-gradient(90deg, rgba(137, 239, 255, 0.18) 0 1px, transparent 1px 6px), rgba(5, 9, 15, 0.9)",
"--menu-title-shadow": "0 0 14px rgba(72, 230, 255, 0.5), 0 0 2px rgba(255, 184, 77, 0.8)", "--menu-title-shadow": "0 0 14px rgba(72, 230, 255, 0.5), 0 0 2px rgba(255, 184, 77, 0.8)",
"--menu-active-outline": "linear-gradient(90deg, rgba(72, 230, 255, 0.95), rgba(255, 184, 77, 0.72), rgba(72, 230, 255, 0.95))", "--menu-active-outline": "linear-gradient(90deg, rgba(72, 230, 255, 0.95), rgba(255, 184, 77, 0.72), rgba(72, 230, 255, 0.95))",
"--panel-material": "var(--sugar-dust), var(--glass-wash), linear-gradient(135deg, rgba(5, 12, 18, 0.46), rgba(10, 24, 32, 0.38))", "--panel-material": "var(--sugar-dust), var(--glass-wash), linear-gradient(135deg, rgba(5, 12, 18, 0.3), rgba(10, 24, 32, 0.22))",
"--panel-shadow": "var(--jelly-inset), inset 0 0 0 1px rgba(119, 237, 255, 0.24), inset 9px 0 0 rgba(72, 230, 255, 0.06), 0 18px 42px rgba(0, 0, 0, 0.42), 0 0 28px rgba(72, 230, 255, 0.11)" "--panel-shadow": "var(--jelly-inset), inset 0 0 0 1px rgba(119, 237, 255, 0.24), inset 9px 0 0 rgba(72, 230, 255, 0.06), 0 18px 42px rgba(0, 0, 0, 0.42), 0 0 28px rgba(72, 230, 255, 0.11)"
} as const; } as const;
const magicalMaterialVariables = { const magicalMaterialVariables = {
"--surface": "rgba(88, 18, 52, 0.48)", "--surface": "rgba(88, 18, 52, 0.38)",
"--surface-solid": "rgba(78, 14, 46, 0.82)", "--surface-solid": "rgba(78, 14, 46, 0.7)",
"--surface-raised": "rgba(118, 26, 68, 0.54)", "--surface-raised": "rgba(118, 26, 68, 0.42)",
"--glass-wash": "linear-gradient(180deg, rgba(255, 226, 242, 0.2), rgba(120, 24, 68, 0.32) 56%, rgba(72, 12, 42, 0.28))", "--glass-wash": "linear-gradient(180deg, rgba(255, 226, 242, 0.13), rgba(120, 24, 68, 0.22) 56%, rgba(72, 12, 42, 0.18))",
"--shine-sweep": "radial-gradient(circle at 88% 12%, rgba(255, 255, 255, 0.58) 0 2px, transparent 3px)", "--shine-sweep": "radial-gradient(circle at 88% 12%, rgba(255, 255, 255, 0.58) 0 2px, transparent 3px)",
"--glass-tint": "rgba(255, 119, 200, 0.1)", "--glass-tint": "rgba(255, 119, 200, 0.1)",
"--crystal-rim": "rgba(255, 236, 250, 0.92)", "--crystal-rim": "rgba(255, 236, 250, 0.92)",
@@ -125,9 +133,18 @@ const magicalMaterialVariables = {
"--crystal-edge-glow": "0 0 0 1px rgba(255, 217, 239, 0.78), 0 0 18px rgba(255, 119, 200, 0.46), 0 0 34px rgba(255, 221, 117, 0.18)", "--crystal-edge-glow": "0 0 0 1px rgba(255, 217, 239, 0.78), 0 0 18px rgba(255, 119, 200, 0.46), 0 0 34px rgba(255, 221, 117, 0.18)",
"--jelly-inset": "inset 0 1px 0 rgba(255, 255, 255, 0.52), inset 0 -1px 0 rgba(255, 119, 200, 0.28), inset 0 0 20px rgba(255, 255, 255, 0.18)", "--jelly-inset": "inset 0 1px 0 rgba(255, 255, 255, 0.52), inset 0 -1px 0 rgba(255, 119, 200, 0.28), inset 0 0 20px rgba(255, 255, 255, 0.18)",
"--frosted-edge": "linear-gradient(180deg, rgba(255, 255, 255, 0.58), rgba(255, 119, 200, 0.2) 48%, rgba(255, 221, 117, 0.18))", "--frosted-edge": "linear-gradient(180deg, rgba(255, 255, 255, 0.58), rgba(255, 119, 200, 0.2) 48%, rgba(255, 221, 117, 0.18))",
"--frosted-surface": "linear-gradient(180deg, rgba(255, 226, 242, 0.18), rgba(118, 24, 66, 0.36) 52%, rgba(72, 12, 42, 0.28))", "--frosted-surface": "linear-gradient(180deg, rgba(255, 226, 242, 0.12), rgba(118, 24, 66, 0.24) 52%, rgba(72, 12, 42, 0.18))",
"--frame-corner": "radial-gradient(circle at 0 0, rgba(255, 221, 117, 0.9) 0 4px, transparent 5px), radial-gradient(circle at 100% 100%, rgba(255, 119, 200, 0.9) 0 4px, transparent 5px)", "--frame-corner": "radial-gradient(circle at 0 0, rgba(255, 221, 117, 0.9) 0 4px, transparent 5px), radial-gradient(circle at 100% 100%, rgba(255, 119, 200, 0.9) 0 4px, transparent 5px)",
"--frame-accent": "linear-gradient(90deg, rgba(255, 221, 117, 0), rgba(255, 221, 117, 0.86), rgba(255, 119, 200, 0.78), rgba(255, 221, 117, 0))", "--frame-accent": "linear-gradient(90deg, rgba(255, 221, 117, 0), rgba(255, 221, 117, 0.86), rgba(255, 119, 200, 0.78), rgba(255, 221, 117, 0))",
"--orbital-construct-image": "url(\"/theme/magical-circle.svg\")",
"--orbital-construct-filter":
"drop-shadow(0 0 18px rgba(255, 119, 200, 0.24)) drop-shadow(0 0 28px rgba(255, 221, 117, 0.16))",
"--magic-lightfall":
"linear-gradient(180deg, transparent 0 6%, rgba(255, 246, 253, 0.32) 18%, rgba(255, 119, 200, 0.12) 48%, transparent 76%)",
"--magic-side-rays":
"linear-gradient(90deg, rgba(255, 246, 253, 0.44), rgba(255, 221, 117, 0.18) 34%, rgba(255, 119, 200, 0.08) 62%, transparent)",
"--command-ring-sheen":
"radial-gradient(circle at 50% 50%, rgba(255, 246, 253, 0.5), rgba(255, 221, 117, 0.18) 42%, transparent 70%)",
"--frame-accessory-top": magicalFrameAccessoryWand, "--frame-accessory-top": magicalFrameAccessoryWand,
"--frame-accessory-bottom": magicalFrameAccessoryCircle, "--frame-accessory-bottom": magicalFrameAccessoryCircle,
"--frame-accessory-heart": magicalFrameAccessoryHeart, "--frame-accessory-heart": magicalFrameAccessoryHeart,
@@ -143,7 +160,7 @@ const magicalMaterialVariables = {
"--menu-glyph-bg": "radial-gradient(circle at 30% 22%, rgba(255, 255, 255, 0.78), transparent 34%), radial-gradient(circle at 72% 80%, rgba(255, 221, 117, 0.42), transparent 30%), linear-gradient(135deg, rgba(255, 119, 200, 0.82), rgba(255, 221, 117, 0.44))", "--menu-glyph-bg": "radial-gradient(circle at 30% 22%, rgba(255, 255, 255, 0.78), transparent 34%), radial-gradient(circle at 72% 80%, rgba(255, 221, 117, 0.42), transparent 30%), linear-gradient(135deg, rgba(255, 119, 200, 0.82), rgba(255, 221, 117, 0.44))",
"--menu-title-shadow": "0 2px 0 rgba(96, 24, 24, 0.86), 1px 0 0 rgba(96, 24, 24, 0.86), -1px 0 0 rgba(96, 24, 24, 0.86), 0 0 12px rgba(255, 221, 117, 0.48)", "--menu-title-shadow": "0 2px 0 rgba(96, 24, 24, 0.86), 1px 0 0 rgba(96, 24, 24, 0.86), -1px 0 0 rgba(96, 24, 24, 0.86), 0 0 12px rgba(255, 221, 117, 0.48)",
"--menu-active-outline": "linear-gradient(90deg, rgba(255, 221, 117, 0.96), rgba(255, 246, 253, 0.92), rgba(255, 119, 200, 0.9), rgba(255, 221, 117, 0.96))", "--menu-active-outline": "linear-gradient(90deg, rgba(255, 221, 117, 0.96), rgba(255, 246, 253, 0.92), rgba(255, 119, 200, 0.9), rgba(255, 221, 117, 0.96))",
"--panel-material": "var(--sugar-dust), var(--glass-wash), linear-gradient(180deg, rgba(106, 22, 60, 0.3), rgba(72, 12, 42, 0.3))", "--panel-material": "var(--sugar-dust), var(--glass-wash), linear-gradient(180deg, rgba(106, 22, 60, 0.11), rgba(72, 12, 42, 0.09))",
"--panel-shadow": "var(--jelly-inset), inset 0 0 0 1px rgba(255, 217, 239, 0.5), 0 18px 42px rgba(72, 0, 34, 0.28), 0 0 30px rgba(255, 119, 200, 0.2), 0 0 16px rgba(255, 221, 117, 0.1)" "--panel-shadow": "var(--jelly-inset), inset 0 0 0 1px rgba(255, 217, 239, 0.5), 0 18px 42px rgba(72, 0, 34, 0.28), 0 0 30px rgba(255, 119, 200, 0.2), 0 0 16px rgba(255, 221, 117, 0.1)"
} as const; } as const;
+70 -8
View File
@@ -141,7 +141,9 @@ json_post() {
return 0 return 0
;; ;;
*) *)
if [[ ! -s "$output_file" ]] || ! response_code_is_duplicate "$output_file"; then
printf 'POST %s failed with HTTP %s\n' "$url" "$status" >&2 printf 'POST %s failed with HTTP %s\n' "$url" "$status" >&2
fi
return 1 return 1
;; ;;
esac esac
@@ -154,6 +156,28 @@ json_get() {
curl -fsS "$@" "$url" >"$output_file" curl -fsS "$@" "$url" >"$output_file"
} }
response_code_is_duplicate() {
local file="$1"
node -e 'const fs=require("fs"); const data=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.exit(data.code === "duplicate" || data.code === "duplicate_resource" ? 0 : 1);' "$file" 2>/dev/null
}
register_plugin_manifest() {
local label="$1"
local plugin_id="$2"
local request_file="$3"
local response_file="$4"
if json_post "$API_URL/game-plugins/register-manifest" "$request_file" "$response_file" "${AUTH_HEADER[@]}"; then
return 0
fi
if [[ -s "$response_file" ]] && response_code_is_duplicate "$response_file"; then
json_get "$API_URL/game-plugins/$plugin_id" "$response_file" "${AUTH_HEADER[@]}"
return 0
fi
printf '%s manifest registration failed; platform response:\n' "$label" >&2
sed -n '1,160p' "$response_file" >&2
exit 1
}
require_file_contains() { require_file_contains() {
local file="$1" local file="$1"
local pattern="$2" local pattern="$2"
@@ -183,6 +207,43 @@ json_id() {
node -e 'const fs=require("fs"); const data=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const id=data.instance?.id || data.serverInstance?.id || data.id || ((data.items||[])[0]||{}).id; if (!id) process.exit(2); process.stdout.write(id);' "$1" node -e 'const fs=require("fs"); const data=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const id=data.instance?.id || data.serverInstance?.id || data.id || ((data.items||[])[0]||{}).id; if (!id) process.exit(2); process.stdout.write(id);' "$1"
} }
require_scum_manifest_contract() {
local file="$1"
node - "$file" <<'NODE'
const fs = require("fs");
const file = process.argv[2];
const plugin = JSON.parse(fs.readFileSync(file, "utf8"));
const missing = [];
const declaredPermissions = Array.isArray(plugin.declaredPermissions) ? plugin.declaredPermissions : [];
const bridgeActions = Array.isArray(plugin.bridgeActions) ? plugin.bridgeActions : [];
const lifecycleProfiles = plugin.runtimeProfiles?.lifecycleProfiles ?? [];
const clientManagers = plugin.runtimeProfiles?.clientManagers ?? [];
if (!declaredPermissions.includes("server.run.distribution")) {
missing.push("server.run.distribution permission");
}
if (!declaredPermissions.includes("server.client-manager.manage")) {
missing.push("server.client-manager.manage permission");
}
if (!bridgeActions.includes("run.distribution.request")) {
missing.push("run.distribution.request bridge action");
}
if (!lifecycleProfiles.some((profile) => profile.key === "run-local")) {
missing.push("run-local runtime profile");
}
if (!clientManagers.some((profile) => profile.key === "scum-client-manager")) {
missing.push("scum-client-manager runtime profile");
}
if (!plugin.gameClientBridge?.commands?.length) {
missing.push("game client bridge declarations");
}
if (missing.length > 0) {
console.error(`SCUM plugin registration is stale or incomplete: missing ${missing.join(", ")}`);
console.error("Run scripts/local-debug-reset.sh, restart the local debug stack, then rerun scripts/local-debug-smoke.sh.");
process.exit(1);
}
NODE
}
wait_for_distribution_build() { wait_for_distribution_build() {
local distribution_file="$1" local distribution_file="$1"
local job_file="$2" local job_file="$2"
@@ -311,9 +372,7 @@ fs.writeFileSync(outputPath, JSON.stringify({
NODE NODE
printf 'registering dev plugin through platform API\n' printf 'registering dev plugin through platform API\n'
if ! json_post "$API_URL/game-plugins/register-manifest" "$WORK_DIR/register-plugin.request.json" "$WORK_DIR/register-plugin.response.json" "${AUTH_HEADER[@]}"; then register_plugin_manifest "dev plugin" "game.example" "$WORK_DIR/register-plugin.request.json" "$WORK_DIR/register-plugin.response.json"
json_get "$API_URL/game-plugins/game.example" "$WORK_DIR/register-plugin.response.json" "${AUTH_HEADER[@]}"
fi
reject_forbidden_fragments "$WORK_DIR/register-plugin.response.json" reject_forbidden_fragments "$WORK_DIR/register-plugin.response.json"
node - "$ROOT_DIR/plugins/examples/scum-server-plugin/manifest.json" "$WORK_DIR/register-scum-plugin.request.json" <<'NODE' node - "$ROOT_DIR/plugins/examples/scum-server-plugin/manifest.json" "$WORK_DIR/register-scum-plugin.request.json" <<'NODE'
@@ -330,6 +389,7 @@ const localRunCapabilities = [
"dependencies.check", "dependencies.check",
"dependencies.install", "dependencies.install",
"logs.backfill", "logs.backfill",
...source.runtimeProfiles.clientManagers.flatMap((manager) => manager.deployment?.requiredRunCapabilities ?? []),
...source.capabilities.filter((capability) => capability.startsWith("remote.")) ...source.capabilities.filter((capability) => capability.startsWith("remote."))
]; ];
const localRuntimeProfiles = { const localRuntimeProfiles = {
@@ -342,7 +402,9 @@ const localRuntimeProfiles = {
start: "actions/start.json", start: "actions/start.json",
stop: "actions/stop.json" stop: "actions/stop.json"
} }
}] }],
transportProfiles: source.runtimeProfiles?.transportProfiles ?? [],
clientManagers: source.runtimeProfiles?.clientManagers ?? []
}; };
const manifest = { const manifest = {
id: source.id, id: source.id,
@@ -365,7 +427,8 @@ const manifest = {
ai: source.ai, ai: source.ai,
productionLifecycle: source.productionLifecycle, productionLifecycle: source.productionLifecycle,
remoteAccess: source.remoteAccess, remoteAccess: source.remoteAccess,
runtimeProfiles: localRuntimeProfiles runtimeProfiles: localRuntimeProfiles,
gameClientBridge: source.gameClientBridge
}; };
fs.writeFileSync(outputPath, JSON.stringify({ fs.writeFileSync(outputPath, JSON.stringify({
manifestRef: "artifact://manifests/game.scum/0.1.0", manifestRef: "artifact://manifests/game.scum/0.1.0",
@@ -374,10 +437,9 @@ fs.writeFileSync(outputPath, JSON.stringify({
NODE NODE
printf 'registering SCUM plugin through platform API\n' printf 'registering SCUM plugin through platform API\n'
if ! json_post "$API_URL/game-plugins/register-manifest" "$WORK_DIR/register-scum-plugin.request.json" "$WORK_DIR/register-scum-plugin.response.json" "${AUTH_HEADER[@]}"; then register_plugin_manifest "SCUM plugin" "game.scum" "$WORK_DIR/register-scum-plugin.request.json" "$WORK_DIR/register-scum-plugin.response.json"
json_get "$API_URL/game-plugins/game.scum" "$WORK_DIR/register-scum-plugin.response.json" "${AUTH_HEADER[@]}"
fi
reject_forbidden_fragments "$WORK_DIR/register-scum-plugin.response.json" reject_forbidden_fragments "$WORK_DIR/register-scum-plugin.response.json"
require_scum_manifest_contract "$WORK_DIR/register-scum-plugin.response.json"
printf 'waiting for run endpoint heartbeat\n' printf 'waiting for run endpoint heartbeat\n'
for _ in $(seq 1 20); do for _ in $(seq 1 20); do