소스 검색

web topo: legible devices in both themes — theme-safe outlines (fg-mixed, not --border), soft drop shadow via --shade, stronger vents/plate, unpowered = hollow + dashed + grayscale at .7 instead of .32 opacity; contrast probe + skill note

Alexander Korolev 1 개월 전
부모
커밋
f9dd6c7f18
2개의 변경된 파일과 31개의 추가작업 그리고 18개의 파일을 삭제
  1. 12 0
      .agents/skills/web-topology/SKILL.md
  2. 19 18
      src/web/ui.html

+ 12 - 0
.agents/skills/web-topology/SKILL.md

@@ -94,6 +94,18 @@ this is the working manual.
    the floors), cable endpoints hit the right switch edges. Parse tags
    attr-wise — browsers re-serialize attribute order, positional regexes
    break; `</title>` inside a regex literal needs `<\/`.
+4b. **Contrast probe** (`/tmp/topo_contrast.mjs`): open the stage page,
+   toggle `data-theme`, read `getComputedStyle(...).fill/stroke` + group
+   `opacity`, resolve against `--panel` (devices sit on the window panel,
+   not on `--bg`) and print WCAG ratios. Gotcha: `.dev` transitions
+   `opacity/filter` for .5s — reading computed style right after adding a
+   class returns the *pre-transition* value (`filter: grayscale(0)`); wait
+   ~800ms. Reference ratios: powered outline/panel 5.3 dark / 3.6 light,
+   unpowered dashed outline 3.2 / 2.3. Never style with `var(--border)`
+   for outlines — on white it is ~1.4:1, i.e. invisible; use
+   `color-mix(in srgb, var(--fg) 55%, transparent)` (theme-safe) and a
+   theme-scoped `--shade` for `drop-shadow`.
+
 5. `cargo test ui_playwright_checks` (fake fleet: 2 devs, 2 lan cables,
    2 frames, 1 passive glyph; chip-click selector
    `#topo .fchip[data-cluster="fake2"]`; hide all other windows before

+ 19 - 18
src/web/ui.html

@@ -7,7 +7,7 @@
 <style>
   :root[data-theme="dark"] {
     --bg: #0d1117; --panel: #161b22; --fg: #e6edf3; --muted: #8b949e;
-    --border: #30363d; --accent: #58a6ff;
+    --border: #30363d; --accent: #58a6ff; --shade: rgba(0,0,0,.55);
     /* monospace content (raw/logs/jobs/node names) — the Alacritty font (~/.alacritty.toml) */
     --mono: "DMMono Nerd Font", Menlo, ui-monospace, SFMono-Regular, monospace;
     --led-on: #3fb950; --led-off: #f85149; --led-busy: #d29922; --led-free: #30363d;
@@ -16,9 +16,9 @@
   }
   :root[data-theme="light"] {
     --bg: #f6f8fa; --panel: #ffffff; --fg: #1f2328; --muted: #656d76;
-    --border: #d0d7de; --accent: #0969da;
+    --border: #d0d7de; --accent: #0969da; --shade: rgba(31,35,40,.20);
     --mono: "DMMono Nerd Font", Menlo, ui-monospace, SFMono-Regular, monospace;
-    --led-on: #1a7f37; --led-off: #cf222e; --led-busy: #9a6700; --led-free: #d0d7de;
+    --led-on: #1a7f37; --led-off: #cf222e; --led-busy: #9a6700; --led-free: #8c959f;
     --glow-on: 0 0 8px 2px rgba(26,127,55,.35); --glow-busy: 0 0 8px 2px rgba(154,103,0,.35);
     --btn: #f6f8fa; --btn-hover: #eaeef2;
   }
@@ -286,16 +286,17 @@
   #topo .topo-empty { height: 100%; display: grid; place-items: center; color: var(--muted); }
   #topo text { font: 12px/1.25 ui-sans-serif, system-ui; fill: var(--fg); }
   #topo .sub { fill: var(--muted); font-size: 10px; }
-  #topo .dev { transition: opacity .5s; }
-  #topo .dev.unpowered { opacity: .32; }
-  #topo .dev .body { fill: color-mix(in srgb, var(--panel) 72%, var(--border)); stroke: var(--border); stroke-width: 1.2; }
-  #topo .dev .top { fill: color-mix(in srgb, var(--fg) 10%, transparent); }
-  #topo .dev .vent { fill: color-mix(in srgb, var(--muted) 55%, transparent); }
-  #topo .dev .plate { fill: color-mix(in srgb, var(--bg) 45%, var(--panel)); }
+  #topo .dev { transition: opacity .5s, filter .5s; }
+  #topo .dev.unpowered { opacity: .7; filter: grayscale(1); }
+  #topo .dev.unpowered .body { fill: var(--bg); stroke-dasharray: 4 3; filter: none; }
+  #topo .dev .body { fill: color-mix(in srgb, var(--fg) 5%, var(--panel)); stroke: color-mix(in srgb, var(--fg) 55%, transparent); stroke-width: 1.4; filter: drop-shadow(0 1px 2px var(--shade)); }
+  #topo .dev .top { fill: color-mix(in srgb, var(--fg) 12%, transparent); }
+  #topo .dev .vent { fill: color-mix(in srgb, var(--fg) 32%, transparent); }
+  #topo .dev .plate { fill: color-mix(in srgb, var(--fg) 10%, var(--panel)); }
   #topo .dev .nm { font-size: 12.5px; font-weight: 600; }
   #topo .dev .ip { font-size: 10px; fill: var(--muted); font-family: var(--mono); }
-  #topo .dev .ctr { fill: color-mix(in srgb, var(--led-free) 65%, transparent); stroke: var(--border); }
-  #topo .dev .ctr.hot { fill: color-mix(in srgb, var(--led-busy) 30%, transparent); }
+  #topo .dev .ctr { fill: color-mix(in srgb, var(--muted) 22%, transparent); stroke: color-mix(in srgb, var(--muted) 60%, transparent); }
+  #topo .dev .ctr.hot { fill: color-mix(in srgb, var(--led-busy) 35%, transparent); }
   #topo .dev .ctrt { font-size: 8.5px; fill: var(--muted); }
   #topo .dev.planned .body { stroke-dasharray: 4 4; opacity: .5; }
   .pled { fill: var(--led-free); }
@@ -303,9 +304,9 @@
   .pled.b { fill: var(--led-busy); filter: drop-shadow(0 0 3px var(--led-busy)); animation: pulse 1.6s infinite; }
   .pled.off { fill: var(--led-off); }
   #topo .cable { fill: none; stroke-linecap: round; stroke-linejoin: round; }
-  #topo .c-200g { stroke: color-mix(in srgb, var(--accent) 60%, transparent); stroke-width: 2.6; }
-  #topo .c-dac { stroke: color-mix(in srgb, #a371f7 60%, transparent); stroke-width: 2.6; }
-  #topo .c-10g { stroke: color-mix(in srgb, var(--muted) 50%, transparent); stroke-width: 1.4; }
+  #topo .c-200g { stroke: color-mix(in srgb, var(--accent) 78%, transparent); stroke-width: 2.6; }
+  #topo .c-dac { stroke: color-mix(in srgb, #a371f7 78%, transparent); stroke-width: 2.6; }
+  #topo .c-10g { stroke: color-mix(in srgb, var(--muted) 68%, transparent); stroke-width: 1.5; }
   #topo .flow { opacity: 0; transition: opacity .6s; }
   #topo .flow.live { opacity: .9; }
   #topo .flow.c-200g { stroke: var(--accent); stroke-dasharray: 4 10; animation: flow 1.1s linear infinite; }
@@ -314,14 +315,14 @@
   @keyframes flow { to { stroke-dashoffset: -14; } }
   #topo .port { fill: var(--led-free); }
   #topo .port.on { fill: var(--led-on); }
-  #topo .swbox { fill: var(--panel); stroke: var(--border); stroke-width: 1.4; }
+  #topo .swbox { fill: color-mix(in srgb, var(--fg) 5%, var(--panel)); stroke: color-mix(in srgb, var(--fg) 55%, transparent); stroke-width: 1.4; }
   #topo .sw .swbox { transition: stroke .4s; }
   #topo .sw.ib.on .swbox { stroke: color-mix(in srgb, var(--led-on) 65%, transparent); filter: drop-shadow(0 0 5px color-mix(in srgb, var(--led-on) 35%, transparent)); }
   #topo .sw.ib.off .swbox { stroke: color-mix(in srgb, var(--led-off) 75%, transparent); }
   #topo .sw.ib.unk .swbox { stroke: color-mix(in srgb, var(--led-busy) 60%, transparent); }
-  #topo .fan .fbody { fill: var(--panel); stroke: var(--border); stroke-width: 1.4; }
-  #topo .fan .blade { fill: color-mix(in srgb, var(--muted) 75%, transparent); }
-  #topo .fan .hub { fill: var(--border); }
+  #topo .fan .fbody { fill: color-mix(in srgb, var(--fg) 5%, var(--panel)); stroke: color-mix(in srgb, var(--fg) 55%, transparent); stroke-width: 1.4; }
+  #topo .fan .blade { fill: color-mix(in srgb, var(--fg) 34%, transparent); }
+  #topo .fan .hub { fill: color-mix(in srgb, var(--fg) 50%, transparent); }
   #topo .fan .blades { transform-box: fill-box; transform-origin: center; animation: spin 1.4s linear infinite; }
   #topo .fan.off .blades { animation: none; opacity: .4; }
   #topo .fan.unk .blades { animation: spin 4s linear infinite; opacity: .55; }