feat: 自动更新
This commit is contained in:
@@ -27,6 +27,7 @@ This directory owns the platform_web visual system. Keep the console in a unifie
|
||||
- `defaultThemeBackgroundId` should point to a built-in mecha desktop preset that works without uploaded imagery.
|
||||
- Built-in presets use CSS variables named `--workspace-background-pattern-*` and render behind the app shell.
|
||||
- Uploaded backgrounds use `--workspace-background-image`, set `data-custom-background="true"`, and take visual precedence over the selected preset.
|
||||
- Magical-girl plus uploaded backgrounds must stay restrained: use muted translucent surfaces, low-opacity frame accessories, and reduced pink/gold glow so busy user imagery remains readable instead of becoming a saturated wash.
|
||||
- Removing an uploaded background must reveal the selected built-in preset again.
|
||||
- Theme palette switches must keep uploaded backgrounds intact. Changing from magical-girl to black mecha, or back again, must not clear `--workspace-background-image` or alter the custom-background fallback preset.
|
||||
- Any new preset must include an `id`, `label`, `summary`, `preview`, and all required `--workspace-background-pattern-*` variables. Current presets are 机甲格纳库 and 粉月魔法阵.
|
||||
@@ -45,6 +46,9 @@ This directory owns the platform_web visual system. Keep the console in a unifie
|
||||
- A visual region should have only one ornamental frame at a hierarchy level. If a `.state-view` is nested inside a shared framed parent such as `.console-panel`, `.catalog-card`, `.server-card`, `.resource-table-wrap`, `.provider-table-wrap`, `.server-table-wrap`, `.plugin-group`, or `.operation-item`, the parent owns the frame and the nested state view must render as transparent, borderless content with no `::before` or `::after` accessory.
|
||||
- Standalone `.state-view` instances may keep their own readable state treatment when they are not inside an already framed surface.
|
||||
- Menu frames should use `var(--menu-item-bg)`, `var(--menu-item-active-bg)`, `var(--menu-glyph-bg)`, `var(--menu-title-shadow)`, and `var(--menu-active-outline)` so each theme changes active-state treatment, icon material, and rail/sidebar structure.
|
||||
- Action dropdowns and contextual menus are small operational overlays, not decorative panels. They must stay anchored to the trigger, fit within the viewport, use compact rows, and avoid moving or resizing the parent card, row, grid, or table.
|
||||
- Do not render a dropdown as a tall vertical tower of large command buttons. Do not let a menu cover server metrics, progress bars, titles, status badges, or adjacent cards. If a server/resource has too many runtime actions for a compact menu, route those actions to a grouped drawer, detail page, or command dialog.
|
||||
- Menu items may use theme-appropriate icons for recognition, but repeated decorative glyph rails on every action row are forbidden. Icons must clarify action meaning or safety state, not become visual clutter.
|
||||
- Shared decoration variables are part of the contract: `--frosted-edge`, `--frosted-surface`, `--corner-sparkle`, `--jelly-highlight`, `--sugar-dust`, `--crystal-edge-glow`, and `--jelly-inset`. In mecha themes these become scanner/grid/bevel materials; in magical themes they become star, ribbon, and jelly-glass materials.
|
||||
- Full-screen ambient motifs use the shared `MagicalParticleLayer` background layer and global particle DOM layer. They should remain non-interactive, theme-colored, reduced-motion aware, and behind operational surfaces. Page code should not create one-off fixed decoration containers.
|
||||
- Keep framed repeated items at 8px radius or less. Pills and circular avatars are allowed for native pill/circle controls.
|
||||
@@ -57,5 +61,4 @@ This directory owns the platform_web visual system. Keep the console in a unifie
|
||||
2. If a new shared pattern is truly needed, add it in `base.css` and describe its intended use here.
|
||||
3. When placing empty/loading/error states inside an existing shared panel, verify the state view does not introduce a second framed panel or accessory layer.
|
||||
4. If a new palette or background preset is added, update `tokens.ts`, `tokens.test.ts`, and any CSS contract tests together.
|
||||
5. Run `npm run typecheck`, `npm test`, `npm run build`, `scripts/check-structure.sh`, and `openspec validate <change> --strict` before claiming completion.
|
||||
6. For page or interaction changes, perform a browser walkthrough before marking visual acceptance tasks complete. At minimum, switch black mecha -> magical-girl -> black mecha with both built-in and uploaded backgrounds when the change touches theme switching, frame accessories, or custom-background styling.
|
||||
5. Run the relevant focused checks before claiming completion. Use `npm run typecheck`, `npm test`, `npm run build`, and `scripts/check-structure.sh` when the scope warrants them; run `openspec validate <change> --strict` only when an OpenSpec change was created.
|
||||
|
||||
@@ -10,4 +10,70 @@ describe("platform web shared theme CSS", () => {
|
||||
expect(nestedStateReset).toContain("content: none");
|
||||
expect(nestedStateReset).toContain("background: transparent");
|
||||
});
|
||||
|
||||
it("keeps server card runtime actions as compact overlay menus", () => {
|
||||
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8");
|
||||
const popoverRule = themeCss.slice(themeCss.indexOf(".runtime-action-popover"), themeCss.indexOf("/* ---- catalog / detail lists ---- */"));
|
||||
|
||||
expect(popoverRule).toContain("position: fixed");
|
||||
expect(popoverRule).toContain("z-index: 45");
|
||||
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", () => {
|
||||
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8");
|
||||
const statRule = themeCss.slice(themeCss.indexOf(".server-card-stat {"), themeCss.indexOf(".server-card-meters"));
|
||||
|
||||
expect(statRule).toContain("border: 1px solid");
|
||||
expect(statRule).toContain("--surface-solid");
|
||||
expect(statRule).toContain("font-weight: 850");
|
||||
expect(themeCss).toContain(':root[data-custom-background="true"] .server-card-stat');
|
||||
});
|
||||
|
||||
it("keeps magical custom-background signal rows readable without the jelly wash", () => {
|
||||
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8");
|
||||
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(signalRule).toContain("backdrop-filter: blur(12px) saturate(0.92)");
|
||||
expect(signalRule).not.toContain("var(--jelly-highlight), var(--glass-wash), var(--surface-solid)");
|
||||
});
|
||||
|
||||
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 primaryCommandRule = themeCss.slice(themeCss.indexOf(".primary-command {"), themeCss.indexOf(".action-strip .primary-command"));
|
||||
|
||||
expect(themeCss).toContain("--control-surface");
|
||||
expect(themeCss).toContain("--primary-command-surface");
|
||||
expect(themeCss).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(primaryCommandRule).not.toContain("background-position: right 8px top 4px, center, center");
|
||||
});
|
||||
|
||||
it("keeps magical-girl custom-background panel overlays restrained", () => {
|
||||
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8");
|
||||
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(customMagicalRule).toContain("rgba(22, 22, 29, 0.64)");
|
||||
expect(customMagicalRule).not.toContain("255, 119, 200, 0.78");
|
||||
});
|
||||
|
||||
it("styles runtime task progress as a dialog with staged status", () => {
|
||||
const themeCss = readFileSync(new URL("./base.css", import.meta.url), "utf8");
|
||||
const progressRule = themeCss.slice(themeCss.indexOf(".runtime-task-backdrop"), themeCss.indexOf("/* ---- narrow screens ---- */"));
|
||||
|
||||
expect(progressRule).toContain(".runtime-task-panel");
|
||||
expect(progressRule).toContain(".runtime-task-meter-track");
|
||||
expect(progressRule).toContain(".runtime-task-stages");
|
||||
expect(progressRule).toContain(".runtime-task-log");
|
||||
expect(progressRule).toContain("prefers-reduced-motion");
|
||||
});
|
||||
});
|
||||
|
||||
+462
-73
@@ -61,6 +61,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-active-outline: linear-gradient(90deg, rgba(72, 230, 255, 0.95), rgba(255, 184, 77, 0.72), rgba(72, 230, 255, 0.95));
|
||||
--control-surface: linear-gradient(180deg, color-mix(in srgb, var(--surface-solid) 90%, rgba(255, 255, 255, 0.08)), color-mix(in srgb, var(--surface-solid) 76%, var(--accent-soft))), var(--glass-wash);
|
||||
--control-surface-active: linear-gradient(180deg, color-mix(in srgb, var(--surface-solid) 72%, var(--accent-soft)), color-mix(in srgb, var(--surface-solid) 82%, var(--pink-soft))), var(--glass-wash);
|
||||
--primary-command-surface: linear-gradient(180deg, color-mix(in srgb, var(--accent) 42%, rgba(255, 255, 255, 0.16)), color-mix(in srgb, var(--pink) 36%, var(--surface-solid)) 68%, color-mix(in srgb, var(--surface-solid) 84%, var(--pink-soft)));
|
||||
--panel-material: var(--sugar-dust), var(--glass-wash), linear-gradient(135deg, rgba(4, 8, 13, 0.78), rgba(13, 23, 32, 0.64));
|
||||
--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.08), 0 18px 42px rgba(0, 0, 0, 0.5), 0 0 34px rgba(72, 230, 255, 0.14);
|
||||
--ultimate-effect-alpha: 0.86;
|
||||
@@ -820,7 +823,7 @@ button {
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 8px;
|
||||
padding: 0 10px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background: var(--control-surface);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
box-shadow: var(--jelly-inset);
|
||||
@@ -839,14 +842,7 @@ button {
|
||||
gap: 6px;
|
||||
border: 1px solid var(--rim-light);
|
||||
border-radius: 999px;
|
||||
background:
|
||||
var(--sugar-dust),
|
||||
var(--corner-sparkle),
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.72), transparent 36%),
|
||||
linear-gradient(135deg, var(--accent), var(--pink));
|
||||
background-size: auto, 44px 44px, auto, auto;
|
||||
background-position: center, right 8px top 2px, center, center;
|
||||
background-repeat: no-repeat, no-repeat, no-repeat, no-repeat;
|
||||
background: var(--primary-command-surface);
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
@@ -962,13 +958,17 @@ button {
|
||||
.background-preset-option-active {
|
||||
border-color: var(--line-strong);
|
||||
color: var(--accent-deep);
|
||||
background: var(--corner-sparkle), var(--jelly-highlight), var(--glass-wash), var(--accent-soft);
|
||||
background-size: 46px 46px, auto, auto, auto;
|
||||
background-position: right 8px top 4px, center, center, center;
|
||||
background-repeat: no-repeat;
|
||||
background: var(--control-surface-active);
|
||||
box-shadow: inset 0 1px 0 var(--rim-light), inset 0 0 0 1px var(--diamond-line), 0 10px 24px var(--candy-glow);
|
||||
}
|
||||
|
||||
.background-preset-option-fallback {
|
||||
border-style: dashed;
|
||||
border-color: color-mix(in srgb, var(--line-strong) 72%, transparent);
|
||||
color: var(--ink-soft);
|
||||
background: var(--control-surface);
|
||||
}
|
||||
|
||||
.background-preset-preview {
|
||||
min-height: 34px;
|
||||
border: 1px solid var(--crystal-rim);
|
||||
@@ -985,6 +985,16 @@ button {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.background-preset-fallback-badge {
|
||||
padding: 1px 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--line-strong) 76%, transparent);
|
||||
border-radius: 999px;
|
||||
background: var(--gold-soft);
|
||||
color: var(--accent-deep);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.theme-background-note {
|
||||
display: block;
|
||||
padding: 7px 9px;
|
||||
@@ -1004,7 +1014,7 @@ button {
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background: var(--control-surface);
|
||||
color: var(--ink-soft);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
@@ -1107,7 +1117,7 @@ button {
|
||||
gap: 6px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background: var(--control-surface);
|
||||
color: var(--ink-soft);
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
@@ -1152,7 +1162,7 @@ button {
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 8px;
|
||||
padding: 0 10px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background: var(--control-surface);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
box-shadow: var(--jelly-inset);
|
||||
@@ -1218,7 +1228,7 @@ button {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background: var(--control-surface);
|
||||
color: var(--ink-soft);
|
||||
box-shadow: inset 0 1px 0 var(--crystal-rim);
|
||||
font-size: 12.5px;
|
||||
@@ -1364,7 +1374,7 @@ button {
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 8px;
|
||||
padding: 0 10px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background: var(--control-surface);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
box-shadow: var(--jelly-inset);
|
||||
@@ -1858,34 +1868,34 @@ button {
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] {
|
||||
--frame-accessory-opacity: 0.82;
|
||||
--surface: rgba(34, 27, 34, 0.5);
|
||||
--surface-solid: rgba(24, 20, 26, 0.78);
|
||||
--surface-raised: rgba(44, 34, 43, 0.58);
|
||||
--line: rgba(255, 185, 226, 0.52);
|
||||
--line-strong: rgba(255, 236, 249, 0.78);
|
||||
--glass-wash: linear-gradient(145deg, rgba(255, 236, 249, 0.16), rgba(38, 30, 38, 0.48) 46%, rgba(255, 221, 117, 0.06));
|
||||
--panel-material: linear-gradient(145deg, rgba(255, 246, 253, 0.14), rgba(39, 31, 39, 0.5) 44%, rgba(24, 21, 27, 0.46));
|
||||
--panel-shadow: inset 0 1px 0 rgba(255, 246, 253, 0.3), inset 0 0 0 1px rgba(255, 185, 226, 0.12), 0 16px 34px rgba(20, 10, 18, 0.26), 0 0 20px rgba(255, 119, 200, 0.14);
|
||||
--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"] .app-sidebar {
|
||||
border-right-color: rgba(255, 226, 244, 0.42);
|
||||
border-right-color: rgba(242, 224, 240, 0.26);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(52, 37, 50, 0.66), rgba(22, 20, 27, 0.58)),
|
||||
rgba(20, 18, 24, 0.42);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.04);
|
||||
backdrop-filter: blur(14px) saturate(1.04);
|
||||
box-shadow: inset -1px 0 0 rgba(255, 246, 253, 0.18), 10px 0 28px rgba(18, 10, 17, 0.22), 0 0 18px rgba(255, 119, 200, 0.1);
|
||||
linear-gradient(180deg, rgba(34, 31, 40, 0.72), rgba(18, 19, 26, 0.68)),
|
||||
rgba(18, 18, 24, 0.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, 0.12), 10px 0 28px rgba(12, 12, 18, 0.26);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .page-header > div:first-child {
|
||||
border-color: rgba(255, 226, 244, 0.28);
|
||||
border-color: rgba(242, 224, 240, 0.18);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 246, 253, 0.13), transparent 36%),
|
||||
rgba(38, 30, 38, 0.48);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(1.02);
|
||||
backdrop-filter: blur(8px) saturate(1.02);
|
||||
linear-gradient(135deg, rgba(255, 246, 253, 0.075), transparent 36%),
|
||||
rgba(22, 22, 29, 0.58);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(0.9);
|
||||
backdrop-filter: blur(8px) saturate(0.9);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"] .app-sidebar {
|
||||
@@ -2013,13 +2023,13 @@ button {
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .provider-table-wrap,
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .server-table-wrap {
|
||||
background:
|
||||
radial-gradient(circle at 100% 0, rgba(255, 185, 226, 0.18), transparent 30%),
|
||||
linear-gradient(135deg, rgba(255, 246, 253, 0.13), transparent 32%),
|
||||
linear-gradient(180deg, rgba(42, 32, 41, 0.42), rgba(24, 21, 27, 0.36));
|
||||
border-color: color-mix(in srgb, var(--line-strong) 76%, rgba(255, 255, 255, 0.2));
|
||||
radial-gradient(circle at 100% 0, rgba(255, 221, 242, 0.07), transparent 32%),
|
||||
linear-gradient(135deg, rgba(255, 246, 253, 0.065), transparent 34%),
|
||||
linear-gradient(180deg, rgba(24, 24, 31, 0.66), rgba(18, 19, 26, 0.58));
|
||||
border-color: color-mix(in srgb, var(--line-strong) 54%, rgba(255, 255, 255, 0.16));
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 246, 253, 0.28), inset 0 0 0 1px rgba(255, 185, 226, 0.1), 0 16px 32px rgba(18, 10, 17, 0.26), 0 0 20px rgba(255, 119, 200, 0.14);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 246, 253, 0.18), inset 0 0 0 1px rgba(255, 185, 226, 0.045), 0 16px 32px rgba(12, 12, 18, 0.28);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .server-toolbar,
|
||||
@@ -2057,8 +2067,8 @@ button {
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .catalog-card::before,
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .server-card::before,
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .server-detail-header::before {
|
||||
background: linear-gradient(180deg, rgba(255, 246, 253, 0.96), rgba(255, 221, 117, 0.72), rgba(255, 119, 200, 0.78));
|
||||
opacity: 0.84;
|
||||
background: linear-gradient(180deg, rgba(255, 246, 253, 0.68), rgba(255, 221, 117, 0.32), rgba(255, 143, 208, 0.36));
|
||||
opacity: 0.46;
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .metric-card::after,
|
||||
@@ -2082,7 +2092,7 @@ button {
|
||||
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, 0.62)) drop-shadow(0 0 2px rgba(255, 221, 117, 0.46));
|
||||
filter: drop-shadow(0 0 3px rgba(255, 180, 226, 0.24));
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"] .metric-card::after,
|
||||
@@ -2198,6 +2208,33 @@ button {
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28), 0 0 18px color-mix(in srgb, var(--accent) 24%, transparent);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"] .server-card-stat {
|
||||
border-color: color-mix(in srgb, var(--line-strong) 62%, rgba(255, 255, 255, 0.2));
|
||||
background:
|
||||
linear-gradient(180deg, rgba(5, 10, 16, 0.9), rgba(5, 10, 16, 0.78)),
|
||||
var(--glass-wash);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 9px 20px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"] .server-card-stat span,
|
||||
:root[data-custom-background="true"] .runtime-action-group-label {
|
||||
color: rgba(236, 249, 253, 0.9);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"] .server-card-stat strong,
|
||||
:root[data-custom-background="true"] .runtime-action-item {
|
||||
color: rgba(250, 254, 255, 0.98);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"] .runtime-action-popover {
|
||||
border-color: color-mix(in srgb, var(--line-strong) 56%, rgba(255, 255, 255, 0.18));
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.07), transparent 32%),
|
||||
rgba(5, 10, 16, 0.94);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(0.86);
|
||||
backdrop-filter: blur(14px) saturate(0.86);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"] .page-status,
|
||||
:root[data-custom-background="true"] .job-chip,
|
||||
:root[data-custom-background="true"] .status-disabled,
|
||||
@@ -2522,7 +2559,7 @@ button {
|
||||
.row-actions button,
|
||||
.table-link-button {
|
||||
border: 1px solid var(--line-strong);
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background: var(--control-surface);
|
||||
color: var(--ink-soft);
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 1px 0 var(--crystal-rim), inset 0 -1px 0 rgba(255, 255, 255, 0.28), 0 8px 18px var(--glass-shadow);
|
||||
@@ -2579,13 +2616,7 @@ button {
|
||||
|
||||
.primary-command {
|
||||
width: 100%;
|
||||
background:
|
||||
var(--corner-sparkle),
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.72), transparent 36%),
|
||||
linear-gradient(135deg, var(--accent), var(--pink));
|
||||
background-size: 48px 48px, auto, auto;
|
||||
background-position: right 8px top 4px, center, center;
|
||||
background-repeat: no-repeat;
|
||||
background: var(--primary-command-surface);
|
||||
border-color: var(--accent-deep);
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
@@ -2619,6 +2650,70 @@ button {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.runtime-action-popover {
|
||||
position: fixed;
|
||||
z-index: 45;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
max-height: min(320px, calc(100vh - 24px));
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
border: 1px solid color-mix(in srgb, var(--line-strong) 68%, rgba(255, 255, 255, 0.2));
|
||||
border-radius: 8px;
|
||||
background:
|
||||
var(--corner-sparkle),
|
||||
linear-gradient(145deg, color-mix(in srgb, var(--surface-solid) 94%, rgba(255, 255, 255, 0.04)), color-mix(in srgb, var(--surface-solid) 86%, var(--accent-soft)) 72%, color-mix(in srgb, var(--surface-solid) 94%, #000000 8%));
|
||||
background-size: 48px 48px, auto;
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
background-position: right 6px top 4px, center;
|
||||
-webkit-backdrop-filter: blur(18px) saturate(1.1);
|
||||
backdrop-filter: blur(18px) saturate(1.1);
|
||||
box-shadow: var(--jelly-inset), inset 0 0 0 1px color-mix(in srgb, var(--diamond-line) 52%, transparent), 0 18px 42px rgba(0, 0, 0, 0.36), 0 0 18px color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
}
|
||||
|
||||
.runtime-action-group {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.runtime-action-group-label {
|
||||
color: color-mix(in srgb, var(--ink) 76%, var(--accent-deep));
|
||||
font-size: 11px;
|
||||
font-weight: 850;
|
||||
line-height: 1.1;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.runtime-action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.runtime-action-item {
|
||||
min-height: 32px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid color-mix(in srgb, var(--line) 76%, rgba(255, 255, 255, 0.12));
|
||||
border-radius: 7px;
|
||||
background: var(--menu-item-bg);
|
||||
color: color-mix(in srgb, var(--ink) 92%, #ffffff);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 780;
|
||||
text-align: left;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--crystal-rim) 52%, transparent);
|
||||
}
|
||||
|
||||
.runtime-action-item:hover,
|
||||
.runtime-action-item:focus-visible {
|
||||
border-color: var(--accent);
|
||||
outline: none;
|
||||
color: var(--ink);
|
||||
background: var(--menu-item-active-bg);
|
||||
box-shadow: inset 0 1px 0 var(--crystal-rim), 0 0 0 2px var(--accent-soft), 0 10px 22px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
/* ---- catalog / detail lists ---- */
|
||||
|
||||
.catalog-grid {
|
||||
@@ -2892,7 +2987,7 @@ button {
|
||||
padding: 0 9px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background: var(--control-surface);
|
||||
color: var(--ink-soft);
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
@@ -2948,7 +3043,7 @@ button {
|
||||
|
||||
.provider-preset-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -2960,7 +3055,7 @@ button {
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background: var(--control-surface);
|
||||
color: var(--ink-soft);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
@@ -2978,12 +3073,68 @@ button {
|
||||
}
|
||||
|
||||
.provider-preset-option:hover,
|
||||
.provider-preset-option:focus-visible {
|
||||
.provider-preset-option:focus-visible,
|
||||
.provider-preset-option-active {
|
||||
border-color: var(--accent);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 0 var(--crystal-rim), 0 0 0 2px var(--accent-soft);
|
||||
}
|
||||
|
||||
.provider-generated-id {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 10px;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--surface-solid) 72%, transparent);
|
||||
color: var(--ink-soft);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.provider-generated-id > span {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.provider-generated-id code {
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
padding: 3px 6px;
|
||||
border-radius: 6px;
|
||||
background: var(--surface-solid);
|
||||
color: var(--ink);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.provider-generated-id small {
|
||||
color: var(--ink-faint);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.provider-advanced-settings {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--surface-solid) 62%, transparent);
|
||||
}
|
||||
|
||||
.provider-advanced-settings summary {
|
||||
cursor: pointer;
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.provider-advanced-settings[open] summary {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.provider-advanced-settings > label,
|
||||
.provider-advanced-settings > .form-grid {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.field-help {
|
||||
color: var(--ink-faint);
|
||||
font-size: 11.5px;
|
||||
@@ -3106,7 +3257,6 @@ button {
|
||||
background: var(--frosted-surface), var(--glass-tint), var(--surface);
|
||||
backdrop-filter: blur(22px) saturate(1.28);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: transform 120ms ease, border-color 120ms ease;
|
||||
box-shadow: var(--jelly-inset), inset 0 0 0 1px var(--diamond-line), 0 18px 42px var(--glass-shadow), 0 0 28px rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
@@ -3149,23 +3299,33 @@ button {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--line-strong) 56%, rgba(255, 255, 255, 0.16));
|
||||
border-radius: 8px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--accent-soft);
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--surface-solid) 88%, rgba(255, 255, 255, 0.06)), color-mix(in srgb, var(--surface-solid) 72%, var(--accent-soft))),
|
||||
var(--glass-wash);
|
||||
min-height: 52px;
|
||||
align-content: center;
|
||||
box-shadow: inset 0 1px 0 var(--crystal-rim);
|
||||
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--crystal-rim) 68%, transparent), 0 8px 18px rgba(0, 0, 0, 0.18);
|
||||
min-width: 0;
|
||||
backdrop-filter: blur(10px) saturate(0.92);
|
||||
}
|
||||
|
||||
.server-card-stat span {
|
||||
color: var(--ink-faint);
|
||||
color: color-mix(in srgb, var(--ink) 78%, var(--accent-deep));
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.48);
|
||||
}
|
||||
|
||||
.server-card-stat strong {
|
||||
font-size: 14px;
|
||||
color: var(--ink);
|
||||
color: color-mix(in srgb, var(--ink) 94%, #ffffff);
|
||||
font-size: 15px;
|
||||
font-weight: 850;
|
||||
line-height: 1.16;
|
||||
overflow-wrap: anywhere;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.56);
|
||||
}
|
||||
|
||||
.server-card-meters {
|
||||
@@ -3266,7 +3426,7 @@ button {
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 999px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background: var(--control-surface);
|
||||
color: var(--ink-soft);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
@@ -3279,13 +3439,7 @@ button {
|
||||
}
|
||||
|
||||
.section-tab-active {
|
||||
background:
|
||||
var(--corner-sparkle),
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.72), transparent 36%),
|
||||
linear-gradient(135deg, var(--accent), var(--pink));
|
||||
background-size: 46px 46px, auto, auto;
|
||||
background-position: right 8px top 4px, center, center;
|
||||
background-repeat: no-repeat;
|
||||
background: var(--primary-command-surface);
|
||||
border-color: var(--accent-deep);
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
@@ -3782,16 +3936,41 @@ button {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), var(--surface-solid);
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--surface-solid) 90%, rgba(255, 255, 255, 0.08)), color-mix(in srgb, var(--surface-solid) 76%, var(--accent-soft))),
|
||||
var(--glass-wash);
|
||||
color: var(--ink-soft);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--crystal-rim) 54%, transparent), 0 8px 18px color-mix(in srgb, var(--glass-shadow) 58%, transparent);
|
||||
}
|
||||
|
||||
.signal-item:hover,
|
||||
.signal-item:focus-visible {
|
||||
border-color: var(--accent);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 0 var(--crystal-rim), 0 0 0 2px var(--accent-soft), 0 10px 22px color-mix(in srgb, var(--glass-shadow) 62%, transparent);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .signal-item {
|
||||
border-color: color-mix(in srgb, var(--line-strong) 68%, rgba(255, 255, 255, 0.22));
|
||||
background:
|
||||
linear-gradient(90deg, rgba(255, 246, 253, 0.22), rgba(255, 246, 253, 0.1) 36%, rgba(255, 185, 226, 0.08)),
|
||||
linear-gradient(180deg, rgba(58, 44, 56, 0.72), rgba(36, 29, 39, 0.68));
|
||||
-webkit-backdrop-filter: blur(12px) saturate(0.92);
|
||||
backdrop-filter: blur(12px) saturate(0.92);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 246, 253, 0.32), inset 0 0 0 1px rgba(255, 185, 226, 0.1), 0 10px 22px rgba(18, 10, 17, 0.26);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .signal-item strong {
|
||||
color: #fffafd;
|
||||
text-shadow: 0 1px 2px rgba(48, 18, 36, 0.72);
|
||||
}
|
||||
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .signal-item p,
|
||||
:root[data-custom-background="true"][data-theme-palette="magical-girl"] .signal-item .provider-id {
|
||||
color: rgba(255, 232, 245, 0.9);
|
||||
text-shadow: 0 1px 2px rgba(48, 18, 36, 0.66);
|
||||
}
|
||||
|
||||
.signal-item strong {
|
||||
@@ -3893,6 +4072,215 @@ button {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ---- runtime task progress ---- */
|
||||
|
||||
.runtime-task-backdrop {
|
||||
z-index: 55;
|
||||
}
|
||||
|
||||
.runtime-task-panel {
|
||||
width: min(720px, 100%);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.runtime-task-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.runtime-task-header > span:first-child {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.runtime-task-header strong {
|
||||
color: var(--ink);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.runtime-task-header small,
|
||||
.runtime-task-current small,
|
||||
.runtime-task-stage-copy small {
|
||||
color: var(--ink-faint);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.runtime-task-meter {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.runtime-task-meter-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.runtime-task-meter-row strong {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.runtime-task-meter-track {
|
||||
height: 10px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--line-strong) 72%, transparent);
|
||||
border-radius: 999px;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.08), transparent 30%),
|
||||
color-mix(in srgb, var(--surface-solid) 82%, #000000 18%);
|
||||
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--crystal-rim) 42%, transparent);
|
||||
}
|
||||
|
||||
.runtime-task-meter-fill {
|
||||
height: 100%;
|
||||
display: block;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, var(--accent), var(--teal), var(--gold));
|
||||
box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 36%, transparent);
|
||||
transition: width 260ms ease;
|
||||
}
|
||||
|
||||
.runtime-task-meter-failed {
|
||||
background: linear-gradient(90deg, var(--danger), #ff9cb6);
|
||||
}
|
||||
|
||||
.runtime-task-current {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 46%, var(--line));
|
||||
border-radius: 8px;
|
||||
background: var(--jelly-highlight), var(--glass-wash), color-mix(in srgb, var(--surface) 82%, var(--accent-soft));
|
||||
box-shadow: inset 0 1px 0 var(--crystal-rim), 0 12px 28px color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
}
|
||||
|
||||
.runtime-task-current-icon,
|
||||
.runtime-task-stage-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid color-mix(in srgb, var(--line-strong) 66%, transparent);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--surface-solid) 78%, var(--accent-soft));
|
||||
color: var(--accent);
|
||||
box-shadow: inset 0 1px 0 var(--crystal-rim);
|
||||
}
|
||||
|
||||
.runtime-task-current span:last-child,
|
||||
.runtime-task-stage-copy {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.runtime-task-current strong,
|
||||
.runtime-task-stage-copy strong {
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.runtime-task-stages {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.runtime-task-stage {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 9px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--glass-wash), rgba(255, 255, 255, 0.16);
|
||||
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--crystal-rim) 62%, transparent);
|
||||
}
|
||||
|
||||
.runtime-task-stage-running {
|
||||
border-color: color-mix(in srgb, var(--accent) 56%, var(--line));
|
||||
}
|
||||
|
||||
.runtime-task-stage-completed .runtime-task-stage-icon {
|
||||
color: var(--success);
|
||||
border-color: color-mix(in srgb, var(--success) 52%, var(--line));
|
||||
}
|
||||
|
||||
.runtime-task-stage-failed .runtime-task-stage-icon {
|
||||
color: var(--danger);
|
||||
border-color: color-mix(in srgb, var(--danger) 52%, var(--line));
|
||||
}
|
||||
|
||||
.runtime-task-stage-status {
|
||||
color: var(--ink-faint);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.runtime-task-log {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
max-height: 132px;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
|
||||
border-radius: 8px;
|
||||
background: var(--code-surface);
|
||||
color: var(--code-ink);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.runtime-task-log span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.runtime-task-error {
|
||||
border-color: color-mix(in srgb, var(--danger) 58%, var(--line));
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.runtime-task-actions button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.runtime-task-spin {
|
||||
animation: runtime-task-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes runtime-task-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.runtime-task-spin {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.runtime-task-meter-fill {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- narrow screens ---- */
|
||||
|
||||
@media (max-width: 760px) {
|
||||
@@ -3906,6 +4294,7 @@ button {
|
||||
}
|
||||
|
||||
.management-form,
|
||||
.runtime-task-stages,
|
||||
.user-management-item,
|
||||
.workflow-hint-grid,
|
||||
.provider-preset-grid,
|
||||
|
||||
Reference in New Issue
Block a user