@weiping/mbridge-page 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weiping/mbridge-page",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "mbridge's usage at a glance inside magpie: what magpie's ledger recorded, and where mbridge's own status page is.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -23,12 +23,12 @@
23
23
  ],
24
24
  "magpie": {
25
25
  "page": {
26
- "title": "mbridge",
26
+ "title": "鹊桥",
27
27
  "titles": {
28
- "zh": "mbridge",
29
- "zh-TW": "mbridge",
30
- "ja": "mbridge",
31
- "de": "mbridge"
28
+ "zh": "鹊桥",
29
+ "zh-TW": "鵲橋",
30
+ "ja": "鵲橋",
31
+ "de": "Magpie Bridge"
32
32
  },
33
33
  "file": "page/index.html",
34
34
  "icon": "page/icon.svg"
package/page/app.js CHANGED
@@ -1,191 +1,115 @@
1
- // app.js reads what mbridge published into the plugin's options, and
2
- // draws it. It never reaches mbridge: the sandbox a magpie page runs in
3
- // has no network at all (connect-src 'none'), so mbridge pushes its
4
- // state into the options and the page reads them in-process (SP14 §7).
5
- //
6
- // A classic script, like the community's usage page: it reads the view
7
- // from globalThis.
1
+ // app.js powers the 鹊桥 plugin page inside magpie's window.
2
+ // It reads the snapshot mbridge published into magpie.options,
3
+ // renders the full dashboard, and dispatches user actions (window tabs,
4
+ // restart, and refresh).
5
+
8
6
  (async () => {
9
7
  const V = globalThis.MbridgeView;
10
- const $ = (id) => document.getElementById(id);
11
- // el makes an element with text and class
12
- function el(tag, attrs, kids) {
13
- const n = document.createElement(tag);
14
- for (const [k, v] of Object.entries(attrs || {})) {
15
- if (v === undefined || v === null) continue;
16
- if (k === "text") n.textContent = v;
17
- else if (k === "class") n.className = v;
18
- else n.setAttribute(k, v);
19
- }
20
- for (const kid of [].concat(kids || [])) {
21
- if (kid === undefined || kid === null) continue;
22
- n.appendChild(typeof kid === "object" ? kid : document.createTextNode(String(kid)));
8
+ const theme = await magpie.ready;
9
+ let lang = "en";
10
+ if (theme && theme.lang) {
11
+ if (theme.lang.toLowerCase().startsWith("zh")) {
12
+ lang = "zh";
23
13
  }
24
- return n;
14
+ } else if (typeof navigator !== "undefined" && (navigator.language || "").toLowerCase().startsWith("zh")) {
15
+ lang = "zh";
25
16
  }
26
- const theme = await magpie.ready;
27
- let lang = theme.lang;
28
17
 
29
- // the page's own keys in the options (kept so mbridge's key survives
30
- // a write)
31
18
  let opts = {};
32
- try {
33
- opts = (await magpie.options.get()) || {};
34
- } catch {
35
- // no options yet: mbridge publishes them
36
- }
37
- const state = {
38
- mbridge: null,
39
- cmdSeq: 0,
40
- };
19
+ const current = { window: "7d" };
41
20
 
42
- // the address of mbridge's own page: selectable text, not a link (the
43
- // sandbox may navigate itself, but that is a one-way ticket, SP14 §2)
44
- function words() {
45
- const t = V.text(lang);
46
- document.querySelectorAll("[data-t]").forEach((e) => {
47
- e.textContent = t[e.dataset.t];
48
- });
49
- $("status").textContent = t.status;
50
- $("restart").textContent = t.restart;
51
- $("cmd").textContent = "mbridge service restart";
52
- $("addr").textContent = V.STATUS_URL;
53
- $("mbridge-ask").textContent = t.mbridgeAsk;
54
- $("mbridge-restart").textContent = t.mbridgeRestart;
21
+ function fail(message) {
22
+ const root = document.getElementById("root");
23
+ root.textContent = "";
24
+ const h1 = document.createElement("h1");
25
+ h1.textContent = "鹊桥";
26
+ const banner = document.createElement("div");
27
+ banner.className = "banner bad err";
28
+ banner.textContent = message;
29
+ root.append(h1, banner);
55
30
  }
56
31
 
57
- // readOptions reads the options: what mbridge published. What we write
58
- // back must be what we just read, so mbridge's snapshot is not put back
59
- // stale.
60
- async function readOptions() {
32
+ async function command(id) {
61
33
  try {
62
- opts = (await magpie.options.get()) || {};
63
- state.mbridge = opts.mbridge || null;
34
+ const o = (await magpie.options.get()) || {};
35
+ o.cmd = { id: id, seq: Date.now() };
36
+ await magpie.options.set(o);
37
+ return true;
64
38
  } catch {
65
- // the options are not readable: keep what we have
39
+ return false;
66
40
  }
67
41
  }
68
42
 
69
- function paintMbridge() {
70
- const t = V.text(lang);
71
- const v = V.mbridgeView(state.mbridge, lang);
72
- $("mbridge-age").textContent = v.none ? "" : (t.mbridgeAge + " " + v.ageText);
73
- $("mbridge-empty").textContent = v.none ? v.text : (v.noDecisions ? t.noDecisions : "");
74
- $("mbridge-empty").hidden = !(v.none || v.noDecisions);
75
- const stats = $("mbridge-stats");
76
- stats.textContent = "";
77
- if (!v.none) {
78
- const items = [
79
- [String(v.turnCount), t.mbridgeTurns],
80
- [v.coverage, t.mbridgeCoverage],
81
- [String(v.failures), t.mbridgeFailures],
82
- [v.topReason || "—", t.reason],
83
- ];
84
- for (const [value, label] of items) {
85
- const cell = document.createElement("div");
86
- cell.className = "stat";
87
- const b = document.createElement("b");
88
- b.textContent = value;
89
- const s2 = document.createElement("span");
90
- s2.textContent = label;
91
- cell.append(b, s2);
92
- stats.append(cell);
43
+ async function setWindow(kind) {
44
+ current.window = kind;
45
+ try {
46
+ const o = (await magpie.options.get()) || {};
47
+ o.period = kind;
48
+ await magpie.options.set(o);
49
+ // Ask mbridge to immediately publish the updated window
50
+ await command("refresh");
51
+ } catch {}
52
+ }
53
+
54
+ async function readOptions() {
55
+ try {
56
+ opts = (await magpie.options.get()) || {};
57
+ if (opts.period && (opts.period === "1d" || opts.period === "7d" || opts.period === "30d")) {
58
+ current.window = opts.period;
93
59
  }
60
+ return opts.mbridge || null;
61
+ } catch (e) {
62
+ return null;
94
63
  }
95
- const live = $("mbridge-live");
96
- live.textContent = "";
97
- if (v.none) {
98
- live.append(el("div", { class: "muted", text: t.mbridgeNone }));
99
- } else if (v.emptyLive) {
100
- live.append(el("div", { class: "muted", text: t.mbridgeNoLive }));
101
- } else {
102
- for (const c of v.live) {
103
- const bits = [];
104
- if (c.reason) bits.push(c.reason);
105
- if (c.conf !== undefined && c.conf !== null) bits.push("conf " + c.conf.toFixed(2));
106
- if (c.tools !== null && c.tools !== undefined) bits.push(t.tools + " " + c.tools);
107
- live.append(el("div", { class: "card" }, [
108
- el("div", { class: "row1" }, [
109
- el("span", { text: c.harness + " · " + c.agent }),
110
- el("span", { text: c.tier + (c.group ? " → " + c.group : "") }),
111
- ]),
112
- el("div", { class: "row2", text: c.session + " · " + bits.join(" · ") }),
113
- ]));
114
- }
64
+ }
65
+
66
+ function renderView(data) {
67
+ // When mbridge is not running or not installed (empty options or no valid snapshot with a running pid):
68
+ // Display the setup and installation guide instead of empty dashboard!
69
+ const isLive = data && data.pid && (data.service ? data.service.running !== false : true);
70
+ if (!isLive) {
71
+ V.renderGuide(null, lang);
72
+ return;
115
73
  }
116
- const body = $("mbridge-decisions");
117
- body.textContent = "";
118
- for (const d of v.decisions || []) {
119
- const tr = document.createElement("tr");
120
- const cells = [
121
- [d.clock, "muted"],
122
- [d.harness, ""],
123
- [d.agent, ""],
124
- [d.tier, ""],
125
- [d.advised, d.differs ? "differs" : "muted"],
126
- [d.reason, "muted"],
127
- [d.conf === undefined || d.conf === null ? "—" : d.conf.toFixed(2), "num"],
128
- ];
129
- for (const [text, cls] of cells) {
130
- const td = document.createElement("td");
131
- if (cls) td.className = cls;
132
- td.textContent = text;
133
- tr.append(td);
74
+
75
+ V.render(data);
76
+
77
+ // Wire up tab switching in the rendered view
78
+ const tabButtons = document.querySelectorAll(".tabs button");
79
+ tabButtons.forEach((btn) => {
80
+ const kind = btn.textContent.trim();
81
+ if (kind === "1d" || kind === "7d" || kind === "30d") {
82
+ btn.onclick = async () => {
83
+ await setWindow(kind);
84
+ tabButtons.forEach((b) => b.setAttribute("aria-pressed", String(b.textContent.trim() === kind)));
85
+ };
134
86
  }
135
- body.append(tr);
136
- }
137
- }
87
+ });
138
88
 
139
- function paint() {
140
- paintMbridge();
89
+ // Wire up the restart button in the rendered view
90
+ const restartBtn = document.querySelector(".restart");
91
+ if (restartBtn) {
92
+ restartBtn.onclick = async () => {
93
+ restartBtn.disabled = true;
94
+ restartBtn.textContent = "正在重启…";
95
+ const ok = await command("restart");
96
+ if (ok) {
97
+ restartBtn.textContent = "已发出重启指令";
98
+ } else {
99
+ restartBtn.textContent = "指令发送失败";
100
+ restartBtn.disabled = false;
101
+ }
102
+ };
103
+ }
141
104
  }
142
105
 
143
- // command asks mbridge to do something. The sequence number makes it
144
- // one-shot: the page repeats it until mbridge answers, and mbridge
145
- // runs it once.
146
- async function command(id) {
147
- state.cmdSeq += 1;
148
- const cmd = { id: id, seq: state.cmdSeq };
149
- try {
150
- const o = (await magpie.options.get()) || {};
151
- o.cmd = cmd;
152
- await magpie.options.set(o);
153
- } catch {
154
- // mbridge will not hear it; the page says so
155
- return false;
156
- }
157
- return true;
106
+ async function poll() {
107
+ const snap = await readOptions();
108
+ renderView(snap);
158
109
  }
159
110
 
160
- $("mbridge-ask").textContent = V.text(lang).mbridgeAsk;
161
- $("mbridge-restart").textContent = V.text(lang).mbridgeRestart;
162
- $("mbridge-ask").addEventListener("click", async () => {
163
- const b = $("mbridge-ask");
164
- const t = V.text(lang);
165
- b.disabled = true;
166
- b.textContent = t.mbridgeAsking;
167
- const ok = await command("refresh");
168
- if (!ok) { b.disabled = false; b.textContent = t.mbridgeAsk; }
169
- // mbridge publishes and the next poll picks it up
170
- setTimeout(() => { b.disabled = false; b.textContent = t.mbridgeAsk; load(); }, 3000);
171
- });
172
- $("mbridge-restart").addEventListener("click", async () => {
173
- const b = $("mbridge-restart");
174
- const t = V.text(lang);
175
- b.disabled = true;
176
- b.textContent = t.mbridgeRestarting;
177
- const ok = await command("restart");
178
- if (!ok) { b.disabled = false; b.textContent = t.mbridgeRestart; return; }
179
- b.textContent = t.mbridgeRestartAsked;
180
- });
111
+ globalThis.pollMbridge = poll;
181
112
 
182
- words();
183
- await readOptions();
184
- paint();
185
- // mbridge's snapshot moves every fifteen seconds, and reading the
186
- // options is free
187
- setInterval(async () => {
188
- await readOptions();
189
- paint();
190
- }, 5000);
113
+ await poll();
114
+ setInterval(poll, 5000);
191
115
  })();
package/page/index.html CHANGED
@@ -3,55 +3,13 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>mbridge</title>
6
+ <title>鹊桥</title>
7
7
  <link rel="stylesheet" href="style.css">
8
8
  <script src="_magpie/bridge.js"></script>
9
9
  <script src="view.js"></script>
10
10
  </head>
11
11
  <body>
12
- <main class="root">
13
- <header class="head">
14
- <h1 data-t="title">mbridge</h1>
15
- <p class="subtitle"></p>
16
- </header>
17
-
18
- <section class="mbridge">
19
- <div class="mbridge-head">
20
- <h2 data-t="mbridge">mbridge</h2>
21
- <span id="mbridge-age" class="muted"></span>
22
- </div>
23
- <div id="mbridge-stats" class="stats"></div>
24
- <h3 data-t="mbridgeLive">sessions running now</h3>
25
- <div id="mbridge-live" class="cards"></div>
26
- <h3 data-t="mbridgeDecisions">recent decisions</h3>
27
- <table class="table mbridge-table">
28
- <thead>
29
- <tr>
30
- <th></th><th data-t="harness">harness</th><th data-t="agent">agent</th>
31
- <th data-t="tier">tier</th><th data-t="advised">advised</th>
32
- <th data-t="reason">reason</th><th class="num" data-t="conf">confidence</th>
33
- </tr>
34
- </thead>
35
- <tbody id="mbridge-decisions"></tbody>
36
- </table>
37
- <p id="mbridge-empty" class="muted"></p>
38
- <div class="mbridge-actions">
39
- <button type="button" id="mbridge-ask" class="btn"></button>
40
- <button type="button" id="mbridge-restart" class="btn"></button>
41
- </div>
42
- </section>
43
-
44
- <section class="notes">
45
- <p class="note">
46
- <span id="status"></span>
47
- <span class="addr" id="addr" title="select and copy it"></span>
48
- <span id="restart"></span>
49
- <code class="cmd" id="cmd"></code>
50
- </p>
51
- </section>
52
-
53
- <p class="error" id="error" hidden></p>
54
- </main>
12
+ <main id="root"></main>
55
13
  <script src="app.js" defer></script>
56
14
  </body>
57
15
  </html>
package/page/style.css CHANGED
@@ -1,67 +1,113 @@
1
- /* Only magpie's own variables, so the page follows the window's theme in
2
- both light and dark without knowing either. */
3
- .root {
4
- --pad: 12px;
5
- padding: var(--pad);
6
- font: 12px/1.5 var(--magpie-font, system-ui, -apple-system, "Segoe UI", sans-serif);
7
- color: var(--magpie-fg, #1a1a1a);
8
- background: var(--magpie-bg, transparent);
9
- }
10
- h1 { margin: 0; font-size: 1.05em; font-weight: 600; }
11
- .subtitle { margin: 2px 0 0; color: var(--magpie-fg-muted, #6b6b6b); }
12
- .since { margin: 0 0 8px; color: var(--magpie-fg-muted, #6b6b6b); font-size: .9em; }
13
- .bar { display: flex; gap: 8px; justify-content: space-between; flex-wrap: wrap; margin-bottom: 10px; }
14
- .segmented { display: flex; gap: 2px; }
15
- .segmented button {
16
- font: inherit; color: inherit; cursor: pointer; padding: 2px 8px; border-radius: 5px;
17
- border: 1px solid var(--magpie-border, #d8d8d8);
18
- background: var(--magpie-bg-subtle, transparent);
1
+ :root {
2
+ color-scheme: light dark;
3
+ --bg: var(--magpie-bg, #ffffff); --fg: var(--magpie-fg, #1a1a1a); --dim: var(--magpie-fg-muted, #6b6b6b); --line: var(--magpie-border, #e3e3e3);
4
+ --ok: #1a7f37; --warn: #9a6700; --bad: #b42318; --chip: var(--magpie-bg-subtle, #f2f2f2);
5
+ }
6
+ @media (prefers-color-scheme: dark) {
7
+ :root { --bg: #17181a; --fg: #e8e8e8; --dim: #9a9a9a; --line: #2e2f33;
8
+ --ok: #57ab5a; --warn: #d0a44a; --bad: #e5534b; --chip: #232427; }
9
+ }
10
+ * { box-sizing: border-box; }
11
+ body { margin: 0; padding: 1.25rem; background: var(--bg); color: var(--fg);
12
+ font: 13px/1.5 var(--magpie-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif); }
13
+ h1 { font-size: 1.1rem; margin: 0 0 .2rem; }
14
+ h2 { font-size: .85rem; margin: 1.4rem 0 .5rem; text-transform: uppercase;
15
+ letter-spacing: .06em; color: var(--dim); }
16
+ .sub { color: var(--dim); margin: 0 0 1rem; }
17
+ .banner { padding: .5rem .7rem; border-radius: 6px; margin: .6rem 0;
18
+ background: color-mix(in srgb, var(--warn) 14%, transparent);
19
+ border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent); }
20
+ .banner.bad { background: color-mix(in srgb, var(--bad) 12%, transparent);
21
+ border-color: color-mix(in srgb, var(--bad) 45%, transparent); }
22
+ table { border-collapse: collapse; width: 100%; }
23
+ th, td { text-align: left; padding: .3rem .5rem; border-bottom: 1px solid var(--line);
24
+ vertical-align: top; }
25
+ th { color: var(--dim); font-weight: 500; white-space: nowrap; }
26
+ td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
27
+ .cards { display: grid; gap: .5rem; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
28
+ .card { border: 1px solid var(--line); border-radius: 8px; padding: .6rem .7rem; background: var(--chip); }
29
+ .card .row1 { display: flex; justify-content: space-between; gap: .5rem; font-weight: 600; }
30
+ .card .row2 { color: var(--dim); font-size: .85em; margin-top: .15rem;
31
+ overflow-wrap: anywhere; }
32
+ .card .row3 { margin-top: .3rem; }
33
+ .card.off { opacity: .5; }
34
+ .chip { display: inline-block; padding: .05rem .4rem; border-radius: 4px;
35
+ background: var(--chip); border: 1px solid var(--line); font-size: .85em; }
36
+ .chip.tier-fast { border-color: var(--ok); }
37
+ .chip.tier-balanced { border-color: var(--warn); }
38
+ .chip.tier-performance { border-color: var(--bad); }
39
+ .muted { color: var(--dim); }
40
+ .off { color: var(--dim); }
41
+ .bar { height: 4px; background: var(--line); border-radius: 2px; overflow: hidden; }
42
+ .bar > i { display: block; height: 100%; background: var(--fg); }
43
+ .err { color: var(--bad); }
44
+ .tabs { display: flex; gap: .3rem; margin: 0 0 .8rem; }
45
+ .tabs button { font: inherit; padding: .2rem .55rem; border-radius: 5px; cursor: pointer;
46
+ border: 1px solid var(--line); background: var(--bg); color: var(--fg); }
47
+ .tabs button[aria-pressed="true"] { background: var(--fg); color: var(--bg); }
48
+ .kv { display: grid; grid-template-columns: max-content 1fr; gap: .15rem .8rem; }
49
+ .kv > :nth-child(odd) { color: var(--dim); }
50
+ .foot { margin-top: 2rem; color: var(--dim); font-size: .85em; }
51
+ .foot ul { margin: .3rem 0; padding-left: 1.1rem; }
52
+
53
+ .head { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
54
+ h1 { font-size: 1.15rem; margin: 0; }
55
+ h1 .version { color: var(--dim); font-weight: 400; font-size: .8em; }
56
+ h3 { font-size: .8rem; margin: .8rem 0 .3rem; color: var(--dim); font-weight: 500; }
57
+ .pulse { display: flex; gap: 1.4rem; align-items: baseline; flex-wrap: wrap;
58
+ margin: .2rem 0 .9rem; padding: .55rem .7rem; border-radius: 8px;
59
+ border: 1px solid var(--line); background: var(--chip); }
60
+ .pulse .stat { display: flex; flex-direction: column; }
61
+ .pulse .stat b { font-size: 1.25em; font-weight: 600; font-variant-numeric: tabular-nums; }
62
+ .pulse .stat span { color: var(--dim); font-size: .8em; }
63
+ .pulse .tabs { margin-left: auto; }
64
+ .procline { display: flex; gap: 1rem; flex-wrap: wrap; color: var(--dim);
65
+ font-size: .85em; margin-bottom: .4rem; }
66
+ .procline span { white-space: nowrap; }
67
+ .dists { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
68
+ .distrow { display: grid; grid-template-columns: 8rem 1fr auto; gap: .5rem;
69
+ align-items: center; padding: 1px 0; }
70
+ .distname { overflow-wrap: anywhere; font-size: .9em; }
71
+ .distnum { font-variant-numeric: tabular-nums; font-size: .85em; color: var(--dim); }
72
+ .fixes { margin: .3rem 0 .6rem; padding-left: 1.2rem; color: var(--dim); }
73
+ .cal { margin: .5rem 0; }
74
+ .cfg { margin-top: 1.2rem; border-top: 1px solid var(--line); padding-top: .6rem; }
75
+ .cfg summary { cursor: pointer; color: var(--dim); }
76
+ .restartrow { margin-top: 1rem; }
77
+ .restart { font: inherit; padding: .3rem .8rem; border-radius: 6px; cursor: pointer;
78
+ border: 1px solid var(--line); background: var(--bg); color: var(--fg); }
79
+ .restart[disabled] { opacity: .6; cursor: default; }
80
+ table.wide { font-size: .92em; }
81
+ table.wide td { padding: .2rem .45rem; }
82
+ td.reason { color: var(--dim); }
83
+ td.differs { color: var(--fg); font-weight: 600; }
84
+ .chip.tier-fast { border-color: var(--ok); }
85
+ .chip.tier-balanced { border-color: var(--warn); }
86
+ .chip.tier-performance { border-color: var(--bad); }
87
+ .chip.differs { border-color: var(--fg); }
88
+
89
+ /* Setup guide view (when mbridge is not running or not installed) */
90
+ .setup-guide { max-width: 760px; margin: 0 auto; padding: 1rem 0; }
91
+ .guide-card {
92
+ border: 1px solid var(--line); border-radius: 8px;
93
+ padding: 1.25rem; margin-bottom: 1.25rem; background: var(--chip);
19
94
  }
20
- .segmented button[aria-selected="true"] {
21
- background: var(--magpie-accent, #2f6feb); border-color: transparent; color: #fff;
95
+ .guide-card h2 { margin: 0 0 .5rem; font-size: 1rem; color: var(--fg); text-transform: none; letter-spacing: normal; }
96
+ .guide-desc { color: var(--dim); margin: 0 0 1rem; font-size: .9em; }
97
+ .guide-steps { display: flex; flex-direction: column; gap: 1rem; }
98
+ .guide-step { display: flex; flex-direction: column; gap: .3rem; }
99
+ .step-title { font-weight: 600; font-size: .9em; }
100
+ .step-cmd { position: relative; display: flex; align-items: center; }
101
+ .step-cmd pre {
102
+ margin: 0; padding: .6rem .8rem; background: var(--bg); border: 1px solid var(--line);
103
+ border-radius: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
104
+ font-size: .88em; width: 100%; overflow-x: auto; color: var(--fg);
22
105
  }
23
- .tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 14px; }
24
- .tile {
25
- padding: 6px 8px; border-radius: 6px;
26
- border: 1px solid var(--magpie-border, #e3e3e3);
27
- background: var(--magpie-bg-subtle, transparent);
106
+ .guide-tip { margin-top: .6rem; font-size: .85em; color: var(--dim); }
107
+ .guide-tip a { color: var(--ok); text-decoration: none; }
108
+ .guide-refresh {
109
+ display: inline-flex; align-items: center; gap: .5rem;
110
+ padding: .4rem .9rem; border-radius: 6px; border: 1px solid var(--line);
111
+ background: var(--bg); color: var(--fg); cursor: pointer; font: inherit;
28
112
  }
29
- .tile .k { color: var(--magpie-fg-muted, #6b6b6b); }
30
- .tile .v { font-size: 1.15em; font-weight: 600; font-variant-numeric: tabular-nums; }
31
- .tile .s { color: var(--magpie-fg-muted, #6b6b6b); }
32
- .models h2 { margin: 0 0 4px; font-size: 1em; font-weight: 600; }
33
- .muted { color: var(--magpie-fg-muted, #6b6b6b); }
34
- .table { width: 100%; border-collapse: collapse; }
35
- .table th, .table td { text-align: left; padding: 3px 4px; border-bottom: 1px solid var(--magpie-border, #e3e3e3); }
36
- .table .num { text-align: right; font-variant-numeric: tabular-nums; }
37
- .table th.num { text-align: right; color: var(--magpie-fg-muted, #6b6b6b); font-weight: 500; }
38
- .model { width: 55%; overflow-wrap: anywhere; }
39
- .bar { display: block; height: 2px; margin-top: 2px; background: var(--magpie-border, #e3e3e3); border-radius: 1px; overflow: hidden; }
40
- .bar > i { display: block; height: 100%; background: var(--magpie-accent, #2f6feb); }
41
- .empty { margin: 6px 0; }
42
- .notes { margin-top: 14px; }
43
- .note { margin: 4px 0; color: var(--magpie-fg-muted, #6b6b6b); }
44
- /* the address is selectable text, not a link: the sandbox may navigate
45
- itself, but that is a one-way ticket (SP14 §2) */
46
- .addr { user-select: all; color: var(--magpie-fg, inherit); }
47
- .cmd { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--magpie-fg, inherit); }
48
- .error { color: var(--magpie-danger, #b42318); margin: 8px 0 0; }
49
-
50
- /* the mbridge section: what mbridge published into the options */
51
- .mbridge { margin-top: 16px; padding-top: 10px; border-top: 1px solid var(--magpie-border, #e3e3e3); }
52
- .mbridge-head { display: flex; justify-content: space-between; align-items: baseline; }
53
- .mbridge-head h2 { margin: 0 0 4px; font-size: 1em; font-weight: 600; }
54
- .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin: 6px 0 10px; }
55
- .stat { padding: 5px 7px; border: 1px solid var(--magpie-border, #e3e3e3); border-radius: 6px; }
56
- .stat b { display: block; font-size: 1.1em; font-variant-numeric: tabular-nums; }
57
- .stat span { color: var(--magpie-fg-muted, #6b6b6b); font-size: .85em; }
58
- .cards { display: grid; gap: 5px; margin-bottom: 10px; }
59
- .card { padding: 5px 7px; border: 1px solid var(--magpie-border, #e3e3e3); border-radius: 6px; }
60
- .card .row1 { display: flex; justify-content: space-between; gap: .5rem; }
61
- .card .row2 { color: var(--magpie-fg-muted, #6b6b6b); font-size: .85em; }
62
- .mbridge-table td { padding: 2px 4px; }
63
- .mbridge-table td.differs { font-weight: 600; }
64
- .mbridge-actions { display: flex; gap: 6px; margin-top: 8px; }
65
- .btn { font: inherit; padding: 2px 9px; border-radius: 5px; cursor: pointer;
66
- border: 1px solid var(--magpie-border, #d8d8d8); background: var(--magpie-bg-subtle, transparent); color: inherit; }
67
- .btn[disabled] { opacity: .6; cursor: default; }
113
+ .guide-refresh:hover { background: var(--chip); }