cbrowser 18.78.0 → 18.80.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.
Files changed (235) hide show
  1. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  2. package/dist/analysis/accessibility-empathy.js +395 -66
  3. package/dist/analysis/accessibility-empathy.js.map +1 -1
  4. package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
  5. package/dist/analysis/agent-ready-audit.js +78 -0
  6. package/dist/analysis/agent-ready-audit.js.map +1 -1
  7. package/dist/analysis/page-understanding.d.ts +139 -1
  8. package/dist/analysis/page-understanding.d.ts.map +1 -1
  9. package/dist/analysis/page-understanding.js +179 -12
  10. package/dist/analysis/page-understanding.js.map +1 -1
  11. package/dist/artifact-store.d.ts +19 -5
  12. package/dist/artifact-store.d.ts.map +1 -1
  13. package/dist/artifact-store.js +41 -11
  14. package/dist/artifact-store.js.map +1 -1
  15. package/dist/browser.d.ts +9 -0
  16. package/dist/browser.d.ts.map +1 -1
  17. package/dist/browser.js +81 -5
  18. package/dist/browser.js.map +1 -1
  19. package/dist/cli.js +55 -10
  20. package/dist/cli.js.map +1 -1
  21. package/dist/cognitive/index.d.ts.map +1 -1
  22. package/dist/cognitive/index.js +3 -2
  23. package/dist/cognitive/index.js.map +1 -1
  24. package/dist/config.d.ts +57 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +234 -7
  27. package/dist/config.js.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/mcp-server-remote.d.ts.map +1 -1
  31. package/dist/mcp-server-remote.js +108 -4
  32. package/dist/mcp-server-remote.js.map +1 -1
  33. package/dist/mcp-server.d.ts.map +1 -1
  34. package/dist/mcp-server.js +62 -938
  35. package/dist/mcp-server.js.map +1 -1
  36. package/dist/mcp-tools/account-personas.d.ts +41 -0
  37. package/dist/mcp-tools/account-personas.d.ts.map +1 -0
  38. package/dist/mcp-tools/account-personas.js +98 -0
  39. package/dist/mcp-tools/account-personas.js.map +1 -0
  40. package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
  41. package/dist/mcp-tools/base/analysis-tools.js +12 -2
  42. package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
  43. package/dist/mcp-tools/base/artifact-tools.d.ts +3 -0
  44. package/dist/mcp-tools/base/artifact-tools.d.ts.map +1 -0
  45. package/dist/mcp-tools/base/artifact-tools.js +78 -0
  46. package/dist/mcp-tools/base/artifact-tools.js.map +1 -0
  47. package/dist/mcp-tools/base/audit-tools.d.ts +11 -0
  48. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  49. package/dist/mcp-tools/base/audit-tools.js +595 -340
  50. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  51. package/dist/mcp-tools/base/browser-management-tools.d.ts.map +1 -1
  52. package/dist/mcp-tools/base/browser-management-tools.js +18 -0
  53. package/dist/mcp-tools/base/browser-management-tools.js.map +1 -1
  54. package/dist/mcp-tools/base/bug-analysis-tools.d.ts.map +1 -1
  55. package/dist/mcp-tools/base/bug-analysis-tools.js +59 -3
  56. package/dist/mcp-tools/base/bug-analysis-tools.js.map +1 -1
  57. package/dist/mcp-tools/base/capture-tools.d.ts +37 -0
  58. package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
  59. package/dist/mcp-tools/base/capture-tools.js +267 -6
  60. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  61. package/dist/mcp-tools/base/cognitive-tools.d.ts +12 -0
  62. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
  63. package/dist/mcp-tools/base/cognitive-tools.js +63 -15
  64. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
  65. package/dist/mcp-tools/base/extraction-tools.d.ts.map +1 -1
  66. package/dist/mcp-tools/base/extraction-tools.js +33 -2
  67. package/dist/mcp-tools/base/extraction-tools.js.map +1 -1
  68. package/dist/mcp-tools/base/index.d.ts +2 -0
  69. package/dist/mcp-tools/base/index.d.ts.map +1 -1
  70. package/dist/mcp-tools/base/index.js +9 -0
  71. package/dist/mcp-tools/base/index.js.map +1 -1
  72. package/dist/mcp-tools/base/marketing-tools.js +3 -3
  73. package/dist/mcp-tools/base/marketing-tools.js.map +1 -1
  74. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
  75. package/dist/mcp-tools/base/persona-comparison-tools.js +529 -40
  76. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
  77. package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
  78. package/dist/mcp-tools/base/site-knowledge-tools.js +36 -3
  79. package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
  80. package/dist/mcp-tools/base/values-tools.d.ts +113 -0
  81. package/dist/mcp-tools/base/values-tools.d.ts.map +1 -1
  82. package/dist/mcp-tools/base/values-tools.js +868 -78
  83. package/dist/mcp-tools/base/values-tools.js.map +1 -1
  84. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  85. package/dist/mcp-tools/base/visual-testing-tools.js +130 -10
  86. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  87. package/dist/mcp-tools/index.d.ts +3 -1
  88. package/dist/mcp-tools/index.d.ts.map +1 -1
  89. package/dist/mcp-tools/index.js +3 -28
  90. package/dist/mcp-tools/index.js.map +1 -1
  91. package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -1
  92. package/dist/mcp-tools/persona-creation-tools.js +363 -20
  93. package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
  94. package/dist/mcp-tools/persona-lifecycle-tools.d.ts +3 -0
  95. package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -0
  96. package/dist/mcp-tools/persona-lifecycle-tools.js +406 -0
  97. package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -0
  98. package/dist/mcp-tools/screenshot-utils.d.ts.map +1 -1
  99. package/dist/mcp-tools/screenshot-utils.js +56 -10
  100. package/dist/mcp-tools/screenshot-utils.js.map +1 -1
  101. package/dist/mcp-tools/tier-gate.js +2 -2
  102. package/dist/mcp-tools/tier-gate.js.map +1 -1
  103. package/dist/mcp-tools/tool-categories.d.ts.map +1 -1
  104. package/dist/mcp-tools/tool-categories.js +4 -1
  105. package/dist/mcp-tools/tool-categories.js.map +1 -1
  106. package/dist/mcp-tools/ui-resources.d.ts +167 -0
  107. package/dist/mcp-tools/ui-resources.d.ts.map +1 -0
  108. package/dist/mcp-tools/ui-resources.js +707 -0
  109. package/dist/mcp-tools/ui-resources.js.map +1 -0
  110. package/dist/mcp-tools/widget-kit.d.ts +299 -0
  111. package/dist/mcp-tools/widget-kit.d.ts.map +1 -0
  112. package/dist/mcp-tools/widget-kit.js +2034 -0
  113. package/dist/mcp-tools/widget-kit.js.map +1 -0
  114. package/dist/mcp-ui-resources.d.ts.map +1 -1
  115. package/dist/mcp-ui-resources.js +14 -4
  116. package/dist/mcp-ui-resources.js.map +1 -1
  117. package/dist/persona-questionnaire.d.ts +69 -1
  118. package/dist/persona-questionnaire.d.ts.map +1 -1
  119. package/dist/persona-questionnaire.js +177 -76
  120. package/dist/persona-questionnaire.js.map +1 -1
  121. package/dist/persona-scope.d.ts +20 -0
  122. package/dist/persona-scope.d.ts.map +1 -0
  123. package/dist/persona-scope.js +59 -0
  124. package/dist/persona-scope.js.map +1 -0
  125. package/dist/personas.d.ts +51 -17
  126. package/dist/personas.d.ts.map +1 -1
  127. package/dist/personas.js +947 -286
  128. package/dist/personas.js.map +1 -1
  129. package/dist/recording/capture-player.d.ts +68 -0
  130. package/dist/recording/capture-player.d.ts.map +1 -0
  131. package/dist/recording/capture-player.js +298 -0
  132. package/dist/recording/capture-player.js.map +1 -0
  133. package/dist/recording/engine.d.ts +52 -0
  134. package/dist/recording/engine.d.ts.map +1 -1
  135. package/dist/recording/engine.js +241 -3
  136. package/dist/recording/engine.js.map +1 -1
  137. package/dist/recording/inline-player.d.ts +39 -0
  138. package/dist/recording/inline-player.d.ts.map +1 -0
  139. package/dist/recording/inline-player.js +272 -0
  140. package/dist/recording/inline-player.js.map +1 -0
  141. package/dist/recording/saliency-overlay.d.ts +133 -0
  142. package/dist/recording/saliency-overlay.d.ts.map +1 -0
  143. package/dist/recording/saliency-overlay.js +360 -0
  144. package/dist/recording/saliency-overlay.js.map +1 -0
  145. package/dist/recording/types.d.ts +32 -0
  146. package/dist/recording/types.d.ts.map +1 -1
  147. package/dist/recording/types.js +46 -0
  148. package/dist/recording/types.js.map +1 -1
  149. package/dist/site-model/manager.d.ts +20 -0
  150. package/dist/site-model/manager.d.ts.map +1 -1
  151. package/dist/site-model/manager.js +52 -0
  152. package/dist/site-model/manager.js.map +1 -1
  153. package/dist/trait-reference.d.ts +26 -2
  154. package/dist/trait-reference.d.ts.map +1 -1
  155. package/dist/trait-reference.js +129 -19
  156. package/dist/trait-reference.js.map +1 -1
  157. package/dist/types.d.ts +105 -18
  158. package/dist/types.d.ts.map +1 -1
  159. package/dist/types.js.map +1 -1
  160. package/dist/url-resolution.d.ts +58 -0
  161. package/dist/url-resolution.d.ts.map +1 -0
  162. package/dist/url-resolution.js +143 -0
  163. package/dist/url-resolution.js.map +1 -0
  164. package/dist/values/big-five-inference.d.ts +121 -0
  165. package/dist/values/big-five-inference.d.ts.map +1 -0
  166. package/dist/values/big-five-inference.js +241 -0
  167. package/dist/values/big-five-inference.js.map +1 -0
  168. package/dist/values/big-five-values.d.ts +76 -0
  169. package/dist/values/big-five-values.d.ts.map +1 -0
  170. package/dist/values/big-five-values.js +152 -0
  171. package/dist/values/big-five-values.js.map +1 -0
  172. package/dist/values/index.d.ts +1 -1
  173. package/dist/values/index.d.ts.map +1 -1
  174. package/dist/values/index.js +1 -1
  175. package/dist/values/index.js.map +1 -1
  176. package/dist/values/persona-completeness.d.ts +91 -0
  177. package/dist/values/persona-completeness.d.ts.map +1 -0
  178. package/dist/values/persona-completeness.js +114 -0
  179. package/dist/values/persona-completeness.js.map +1 -0
  180. package/dist/values/persona-diff.d.ts +50 -0
  181. package/dist/values/persona-diff.d.ts.map +1 -0
  182. package/dist/values/persona-diff.js +126 -0
  183. package/dist/values/persona-diff.js.map +1 -0
  184. package/dist/values/persona-values.d.ts +16 -0
  185. package/dist/values/persona-values.d.ts.map +1 -1
  186. package/dist/values/persona-values.js +33 -0
  187. package/dist/values/persona-values.js.map +1 -1
  188. package/dist/values/schwartz-values.d.ts +9 -1
  189. package/dist/values/schwartz-values.d.ts.map +1 -1
  190. package/dist/values/schwartz-values.js +10 -2
  191. package/dist/values/schwartz-values.js.map +1 -1
  192. package/dist/values/value-mappings.d.ts +41 -2
  193. package/dist/values/value-mappings.d.ts.map +1 -1
  194. package/dist/values/value-mappings.js +65 -8
  195. package/dist/values/value-mappings.js.map +1 -1
  196. package/dist/visual/attention-quality.d.ts +27 -0
  197. package/dist/visual/attention-quality.d.ts.map +1 -1
  198. package/dist/visual/attention-quality.js +164 -2
  199. package/dist/visual/attention-quality.js.map +1 -1
  200. package/dist/visual/attention-transport.d.ts +41 -2
  201. package/dist/visual/attention-transport.d.ts.map +1 -1
  202. package/dist/visual/attention-transport.js +98 -26
  203. package/dist/visual/attention-transport.js.map +1 -1
  204. package/dist/visual/barrier-overlay-html.d.ts +10 -0
  205. package/dist/visual/barrier-overlay-html.d.ts.map +1 -1
  206. package/dist/visual/barrier-overlay-html.js.map +1 -1
  207. package/dist/visual/cognitive-models.d.ts +109 -13
  208. package/dist/visual/cognitive-models.d.ts.map +1 -1
  209. package/dist/visual/cognitive-models.js +166 -3
  210. package/dist/visual/cognitive-models.js.map +1 -1
  211. package/dist/visual/cognitive-transport-chain.d.ts +149 -5
  212. package/dist/visual/cognitive-transport-chain.d.ts.map +1 -1
  213. package/dist/visual/cognitive-transport-chain.js +558 -30
  214. package/dist/visual/cognitive-transport-chain.js.map +1 -1
  215. package/dist/visual/cognitive-transport.d.ts +45 -3
  216. package/dist/visual/cognitive-transport.d.ts.map +1 -1
  217. package/dist/visual/cognitive-transport.js +126 -12
  218. package/dist/visual/cognitive-transport.js.map +1 -1
  219. package/dist/visual/fixation-corpus.d.ts +52 -0
  220. package/dist/visual/fixation-corpus.d.ts.map +1 -1
  221. package/dist/visual/fixation-corpus.js +95 -0
  222. package/dist/visual/fixation-corpus.js.map +1 -1
  223. package/dist/visual/llm-relevance.d.ts +193 -0
  224. package/dist/visual/llm-relevance.d.ts.map +1 -0
  225. package/dist/visual/llm-relevance.js +480 -0
  226. package/dist/visual/llm-relevance.js.map +1 -0
  227. package/dist/visual/perceptual-transport.d.ts +60 -0
  228. package/dist/visual/perceptual-transport.d.ts.map +1 -1
  229. package/dist/visual/perceptual-transport.js +228 -19
  230. package/dist/visual/perceptual-transport.js.map +1 -1
  231. package/dist/visual/persona-context.d.ts +23 -0
  232. package/dist/visual/persona-context.d.ts.map +1 -0
  233. package/dist/visual/persona-context.js +44 -0
  234. package/dist/visual/persona-context.js.map +1 -0
  235. package/package.json +2 -1
@@ -0,0 +1,2034 @@
1
+ /**
2
+ * Widget kit for MCP Apps views.
3
+ *
4
+ * An MCP Apps resource is static: the host fetches it once, caches it against
5
+ * its URI, and hydrates it at runtime with the tool result. So a widget cannot
6
+ * be a rendered document -- it has to be a renderer plus a description of what
7
+ * to render, with the data arriving later.
8
+ *
9
+ * That shape is what makes a kit possible. Every widget ships the same chrome
10
+ * (tokens, hero, tables, chips, accordions), the same runtime, and the same
11
+ * hydration and safety rules; what differs per tool is a small declarative
12
+ * spec naming which fields become which blocks. Adding a view is writing a
13
+ * spec, not another copy of 400 lines of CSS that will drift from its
14
+ * siblings by the third one.
15
+ *
16
+ * The alternative -- one hand-written template per tool -- was rejected after
17
+ * the status widget, where the CSS, the ext-apps bundling, the theme handling,
18
+ * the textContent discipline and three separate blank-render fixes would all
19
+ * have had to be repeated verbatim eight more times.
20
+ */
21
+ import { readFileSync } from "node:fs";
22
+ import { createRequire } from "node:module";
23
+ import { dirname, join } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { TRAIT_DEFINITIONS } from "../trait-reference.js";
26
+ /** The MIME the host keys on to render a resource as an interactive view. */
27
+ export const MCP_APP_MIME = "text/html;profile=mcp-app";
28
+ export const widgetUri = (id) => `ui://cbrowser/${id}`;
29
+ // ---------------------------------------------------------------------------
30
+ // Inlined assets
31
+ // ---------------------------------------------------------------------------
32
+ const here = () => dirname(fileURLToPath(import.meta.url));
33
+ /**
34
+ * The ext-apps browser bundle, inlined into every widget.
35
+ *
36
+ * Not a CDN import. The iframe CSP blocks esm.sh from fetching the transitive
37
+ * SDK dependencies, and the failure is a blank rectangle whose error appears
38
+ * only in the iframe's own devtools console -- nothing surfaces host-side,
39
+ * which is why an esm.sh import looks correct right up until nothing renders.
40
+ *
41
+ * The replacer is a function, not a string: String.replace interprets
42
+ * $-sequences and the minified bundle is full of them.
43
+ */
44
+ let extAppsBundle;
45
+ export function getExtAppsBundle() {
46
+ if (extAppsBundle !== undefined)
47
+ return extAppsBundle;
48
+ try {
49
+ const req = createRequire(import.meta.url);
50
+ const raw = readFileSync(req.resolve("@modelcontextprotocol/ext-apps/app-with-deps"), "utf8");
51
+ extAppsBundle = raw.replace(/export\{([^}]+)\};?\s*$/, (_m, body) => "globalThis.ExtApps={" +
52
+ body.split(",").map((pair) => {
53
+ const [local, exported] = pair.split(" as ").map((x) => x.trim());
54
+ return `${exported ?? local}:${local}`;
55
+ }).join(",") + "};");
56
+ }
57
+ catch {
58
+ extAppsBundle = "globalThis.ExtApps=undefined;";
59
+ }
60
+ return extAppsBundle;
61
+ }
62
+ /**
63
+ * The cbrowser mark as a data URI, brightened for badge size.
64
+ *
65
+ * The sandbox will not fetch cbrowser.ai, so an external src renders broken.
66
+ * The asset is ~100 rects whose opacity ramps to 0.31 -- deliberate at poster
67
+ * size, and at 27px each dot renders around a pixel tall, so the faint end
68
+ * drops out of the raster entirely. Opacity is floored, fills lifted, and each
69
+ * dot stroked, because at that size a stroke is what gives a dot enough mass
70
+ * to survive rasterisation; brightness alone cannot save something that rounds
71
+ * away. Applied here rather than to the file so the brand asset stays canonical.
72
+ */
73
+ let logoUri;
74
+ export function getLogoDataUri() {
75
+ if (logoUri !== undefined)
76
+ return logoUri;
77
+ try {
78
+ const raw = readFileSync(join(here(), "..", "..", "assets", "cbrowser-logo.svg"), "utf8")
79
+ .replace(/\s*\n\s*/g, " ");
80
+ const brighten = (c) => Math.round(c + (255 - c) * 0.34);
81
+ const svg = raw
82
+ .replace(/opacity="([\d.]+)"/g, (_m, v) => `opacity="${Math.min(1, 0.62 + parseFloat(v) * 0.38).toFixed(3)}"`)
83
+ .replace(/fill="rgb\((\d+),\s*(\d+),\s*(\d+)\)"/g, (_m, r, g, b) => {
84
+ const c = `rgb(${brighten(+r)},${brighten(+g)},${brighten(+b)})`;
85
+ return `fill="${c}" stroke="${c}" stroke-width="5" stroke-linejoin="round"`;
86
+ })
87
+ .replace(/stdDeviation="5"/, 'stdDeviation="7"')
88
+ .replace(/"/g, "'");
89
+ logoUri = "data:image/svg+xml," + encodeURIComponent(svg).replace(/'/g, "%27");
90
+ }
91
+ catch {
92
+ logoUri = "";
93
+ }
94
+ return logoUri;
95
+ }
96
+ /**
97
+ * Soft arcs and an angled facet over the hero, as an inline SVG.
98
+ *
99
+ * preserveAspectRatio=none so one small asset stretches to any hero width.
100
+ * This layering, rather than a flatter ramp, is what reads as a lit surface.
101
+ */
102
+ const HERO_ART = (() => {
103
+ const svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 200' preserveAspectRatio='none'>" +
104
+ "<path d='M0,128 C150,58 330,168 505,96 C645,38 730,78 800,52 L800,200 L0,200 Z' fill='#ffffff' opacity='.10'/>" +
105
+ "<path d='M0,171 C190,116 372,190 548,142 C688,104 754,132 800,116 L800,200 L0,200 Z' fill='#ffffff' opacity='.07'/>" +
106
+ "<path d='M556,0 L800,0 L800,200 L678,200 Z' fill='#ffffff' opacity='.05'/>" +
107
+ "</svg>";
108
+ return "data:image/svg+xml," + encodeURIComponent(svg);
109
+ })();
110
+ // ---------------------------------------------------------------------------
111
+ // Chrome
112
+ // ---------------------------------------------------------------------------
113
+ /**
114
+ * Shared stylesheet.
115
+ *
116
+ * Hero stops are darker than the site's brand tokens on purpose: the white
117
+ * bloom lifts the ground under white text, and the contrast floor is measured
118
+ * off composited pixels rather than these declarations. Horizontal inset never
119
+ * drops below the host card's corner radius, or narrow widths put text back
120
+ * inside the arc.
121
+ */
122
+ const CSS = `
123
+ :root{
124
+ --ink:var(--color-text-primary,#14161a);
125
+ --sub:var(--color-text-secondary,#606770);
126
+ --line:var(--color-border-default,#e0e3e8);
127
+ --brand:oklch(0.55 0.18 250);
128
+ --brand-2:oklch(0.6 0.18 180);
129
+ --hero-a:oklch(0.415 0.19 258);
130
+ --hero-b:oklch(0.45 0.13 202);
131
+ --accent:var(--color-accent-primary,oklch(0.55 0.18 250));
132
+ --warn:oklch(0.55 0.16 45);
133
+ --ok:oklch(0.62 0.16 150);
134
+ --bad:oklch(0.52 0.19 25);
135
+ --raise:color-mix(in srgb, var(--ink) 4%, transparent);
136
+ --r:var(--border-radius-md,8px);
137
+ --mono:ui-monospace,SFMono-Regular,Menlo,monospace;
138
+ }
139
+ :root.dark{--ink:#e9ebee;--sub:#98a0aa;--line:#333840;
140
+ --brand:oklch(0.65 0.18 250);--brand-2:oklch(0.7 0.15 180);
141
+ --hero-a:oklch(0.385 0.18 258);--hero-b:oklch(0.42 0.12 202);
142
+ --accent:oklch(0.65 0.18 250);--warn:oklch(0.72 0.14 55);--ok:oklch(0.72 0.15 150);
143
+ --bad:oklch(0.68 0.17 25);
144
+ --raise:color-mix(in srgb, #fff 6%, transparent)}
145
+ *{box-sizing:border-box}
146
+ html,body{background:transparent;color:var(--ink)}
147
+ body{margin:0;padding:0;font:15px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif}
148
+
149
+ .hero{padding:16px 18px 15px;color:#fff}
150
+ .hero.gradient{background:
151
+ radial-gradient(115% 95% at 10% -12%, rgba(255,255,255,.20), transparent 58%),
152
+ radial-gradient(95% 85% at 98% 118%, rgba(0,0,0,.26), transparent 60%),
153
+ url("${HERO_ART}") center/100% 100% no-repeat,
154
+ linear-gradient(118deg,var(--hero-a),var(--hero-b))}
155
+ .hero.solid{background:var(--hero-a)}
156
+ .hero.bare{background:transparent;color:var(--ink);padding-bottom:8px}
157
+ .hrow{display:flex;align-items:center;gap:.6rem}
158
+ .badge{flex:0 0 auto;width:37px;height:37px;border-radius:50%;display:grid;place-items:center;
159
+ background:#14171d;box-shadow:0 0 0 1px rgba(255,255,255,.18), 0 1px 3px rgba(0,0,0,.28)}
160
+ .hero.bare .badge{box-shadow:0 0 0 1px rgba(0,0,0,.12), 0 1px 3px rgba(0,0,0,.14)}
161
+ .badge img{width:27px;height:27px;display:block}
162
+ .htitle{font-size:1.08rem;font-weight:650;letter-spacing:-.01em}
163
+ .health{margin-left:auto;display:inline-flex;align-items:center;gap:.4rem;
164
+ padding:.28rem .6rem;border-radius:999px;background:rgba(255,255,255,.94);
165
+ font:650 .7rem/1 var(--mono);letter-spacing:.09em;color:#1b3a2b}
166
+ .hero.bare .health{background:color-mix(in srgb,var(--ok) 16%,transparent);color:var(--ink)}
167
+ .health .dot{width:7px;height:7px;border-radius:50%;background:var(--ok);flex:0 0 auto}
168
+ .health.bad{color:#4a2410} .health.bad .dot{background:var(--warn)}
169
+ .health.unknown{color:#33383f} .health.unknown .dot{background:#8a9099}
170
+ .hnote{margin:.5rem 0 0;font-size:.79rem;color:#fff}
171
+ .hero.bare .hnote{color:var(--sub)}
172
+
173
+ .subttl{font-size:.8rem;color:rgba(255,255,255,.92);margin:.3rem 0 0}
174
+ .hero.bare .subttl{color:var(--sub)}
175
+ .acts{display:flex;flex-wrap:wrap;gap:.4rem;margin:.75rem 0 0}
176
+ .act{font:600 .74rem/1 ui-sans-serif,system-ui,sans-serif;padding:.42rem .7rem;border-radius:6px;
177
+ border:1px solid rgba(255,255,255,.5);background:rgba(255,255,255,.96);color:#123;
178
+ cursor:pointer;transition:background 150ms ease}
179
+ .act:hover{background:#fff}
180
+ .act:focus-visible{outline:2px solid #fff;outline-offset:2px}
181
+ .hero.bare .act{background:var(--brand);color:#fff;border-color:transparent}
182
+ .facts{display:flex;flex-wrap:wrap;gap:.34rem;margin:.7rem 0 0}
183
+ .fact{display:inline-flex;align-items:baseline;gap:.34rem;padding:.24rem .52rem;
184
+ border:1px solid rgba(255,255,255,.32);border-radius:6px;background:rgba(255,255,255,.10)}
185
+ .fact b{font:650 .82rem/1 var(--mono);font-variant-numeric:tabular-nums;color:#fff}
186
+ .fact span{font-size:.74rem;color:#fff}
187
+ .fact.attn{background:rgba(255,255,255,.94);border-color:transparent}
188
+ .fact.attn b{color:#7a3410} .fact.attn span{color:#5d3520}
189
+ .hero.bare .fact{border-color:var(--line);background:var(--raise)}
190
+ .hero.bare .fact b{color:var(--brand)} .hero.bare .fact span{color:var(--sub)}
191
+
192
+ .body{padding:0 18px 16px}
193
+ .drawer{border-top:1px solid var(--line)}
194
+ .drawer>summary{display:flex;align-items:center;gap:.5rem;padding:.7rem 18px;cursor:pointer;
195
+ list-style:none;user-select:none;font-size:.87rem;font-weight:560}
196
+ .drawer>summary::-webkit-details-marker{display:none}
197
+ .drawer>summary:hover{color:var(--accent)}
198
+ .drawer>summary:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}
199
+ .dcount{margin-left:auto;font:.75rem/1 var(--mono);color:var(--sub)}
200
+ .inner{padding:0 18px 16px}
201
+
202
+ details.sec{border-top:1px solid var(--line)}
203
+ details.sec:first-of-type{border-top:0}
204
+ details.sec>summary{display:flex;align-items:center;gap:.5rem;padding:.55rem 0;cursor:pointer;
205
+ list-style:none;user-select:none}
206
+ details.sec>summary::-webkit-details-marker{display:none}
207
+ details.sec>summary:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;border-radius:4px}
208
+ details.sec>summary:hover .stitle{color:var(--accent)}
209
+ .chev{flex:0 0 auto;width:8px;height:8px;border-right:1.6px solid var(--sub);
210
+ border-bottom:1.6px solid var(--sub);transform:rotate(-45deg);
211
+ transition:transform 180ms cubic-bezier(.22,1,.36,1)}
212
+ details[open]>summary .chev{transform:rotate(45deg)}
213
+ .stitle{font-size:.88rem;font-weight:560;transition:color 150ms ease}
214
+ .scount{margin-left:auto;font:.74rem/1 var(--mono);color:var(--sub);font-variant-numeric:tabular-nums}
215
+ .sbody{padding:0 0 .65rem}
216
+ .btitle{font-size:.88rem;font-weight:560;margin:.9rem 0 .4rem}
217
+
218
+ .scroll{overflow-x:auto;max-width:100%}
219
+ table{border-collapse:collapse;width:100%;font-size:.85rem}
220
+ /* Fixed layout with a capped label column. The label was width:1% plus
221
+ white-space:nowrap -- shrink-to-fit, which works until a label cannot
222
+ shrink. Run info flattens nested keys into "A > B > C" strings, and an
223
+ unwrappable one took the whole row, leaving the value column 10-20px
224
+ wide. Labels wrap now and the split is enforced rather than negotiated. */
225
+ .kv{table-layout:fixed}
226
+ .kv th{width:38%;text-align:left;padding:.24rem .9rem .24rem 0;
227
+ font:.77rem/1.45 var(--mono);color:var(--sub);font-weight:400;vertical-align:top;
228
+ white-space:normal;overflow-wrap:anywhere}
229
+ .kv td{padding:.24rem 0;vertical-align:top;word-break:break-word;overflow-wrap:anywhere}
230
+ /* Below ~30rem a 38/62 split leaves neither side usable: stack instead. */
231
+ @media (max-width:30rem){
232
+ .kv,.kv tbody,.kv tr,.kv th,.kv td{display:block;width:auto}
233
+ .kv th{padding:.24rem 0 0;}
234
+ .kv td{padding:0 0 .35rem;border-bottom:1px solid color-mix(in srgb,var(--line) 45%,transparent)}
235
+ }
236
+ .grid{table-layout:auto}
237
+ .grid thead th{text-align:left;padding:.18rem .7rem .3rem 0;font:.67rem/1.3 var(--mono);
238
+ color:var(--sub);font-weight:400;text-transform:uppercase;letter-spacing:.07em;
239
+ border-bottom:1px solid var(--line);white-space:normal}
240
+ /* Cells wrap. Long URLs and selectors used to push the table past the panel
241
+ and rely on the horizontal scroller to hide it. */
242
+ .grid td{overflow-wrap:anywhere;word-break:break-word;vertical-align:top;max-width:22rem}
243
+ .grid td{padding:.28rem .7rem .28rem 0;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent)}
244
+ .grid tr:last-child td{border-bottom:0}
245
+ .grid tbody tr:hover td{background:var(--raise)}
246
+ .num{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--mono);font-size:.79rem}
247
+ .path{font-family:var(--mono);font-size:.77rem;color:var(--sub);word-break:break-all}
248
+ .chip{display:inline-block;font:.67rem/1 var(--mono);padding:.15rem .36rem;border-radius:3px;
249
+ background:color-mix(in srgb,var(--ok) 18%,transparent);color:color-mix(in srgb,var(--ok) 80%,var(--ink))}
250
+ .chip.no{background:color-mix(in srgb,var(--warn) 18%,transparent);color:color-mix(in srgb,var(--warn) 82%,var(--ink))}
251
+
252
+ /* Findings: severity is encoded as a leading bar and a chip, so the ranking
253
+ reads without parsing the text of every row. */
254
+ .finds{list-style:none;padding:0;margin:0}
255
+ .finds li{display:grid;grid-template-columns:auto 1fr;gap:.55rem;align-items:baseline;
256
+ padding:.4rem 0;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent)}
257
+ .finds li:last-child{border-bottom:0}
258
+ .sev{font:650 .62rem/1 var(--mono);letter-spacing:.07em;text-transform:uppercase;
259
+ padding:.2rem .36rem;border-radius:3px;white-space:nowrap}
260
+ .sev.critical,.sev.blocker{background:color-mix(in srgb,var(--bad) 22%,transparent);color:color-mix(in srgb,var(--bad) 85%,var(--ink))}
261
+ .sev.high,.sev.major,.sev.serious{background:color-mix(in srgb,var(--warn) 22%,transparent);color:color-mix(in srgb,var(--warn) 85%,var(--ink))}
262
+ .sev.medium,.sev.moderate{background:color-mix(in srgb,var(--brand) 18%,transparent);color:color-mix(in srgb,var(--brand) 85%,var(--ink))}
263
+ .sev.low,.sev.minor,.sev.info,.sev.notice{background:var(--raise);color:var(--sub)}
264
+ .fdetail{grid-column:2;font-size:.79rem;color:var(--sub);margin:.15rem 0 0}
265
+
266
+ /* Score: one headline grade, then weighted categories as proportional bars.
267
+ The weight is shown because a 90 in a 15%-weighted category is not the
268
+ same finding as a 90 in a 35%-weighted one. */
269
+ .score{display:flex;align-items:center;gap:.9rem;margin:.2rem 0 .7rem}
270
+ .grade{font:700 2.1rem/1 ui-sans-serif,system-ui,sans-serif;letter-spacing:-.03em}
271
+ .grade.a{color:var(--ok)} .grade.b{color:var(--ok)} .grade.c{color:var(--warn)}
272
+ .grade.d{color:var(--bad)} .grade.f{color:var(--bad)}
273
+ .scoresub{font:.78rem/1.4 var(--mono);color:var(--sub)}
274
+ .cats{list-style:none;padding:0;margin:0}
275
+ .cats li{display:grid;grid-template-columns:1fr auto;gap:.4rem .6rem;padding:.28rem 0}
276
+ .catname{font-size:.82rem}
277
+ .catval{font:.78rem/1 var(--mono);color:var(--sub);font-variant-numeric:tabular-nums}
278
+ .bar{grid-column:1/-1;height:5px;border-radius:3px;background:var(--raise);overflow:hidden}
279
+ .bar i{display:block;height:100%;border-radius:3px;background:var(--brand)}
280
+
281
+ /* Trait meters, matching cbrowser.ai's persona editor: fixed-width right
282
+ aligned label, a pill track with a baseline tick at 0.5 so above- and
283
+ below-average read instantly, and a two-decimal monospace readout. */
284
+ .traits{display:flex;flex-direction:column;gap:.28rem}
285
+ .trait{display:flex;align-items:center;gap:.5rem}
286
+ /* Editor. The dirty state is a left border rather than a colour change on the
287
+ value, so it survives both themes and does not rely on hue alone --
288
+ WCAG 1.4.1: colour is not the only channel carrying the information. */
289
+ .trait.dirty{border-left:3px solid var(--accent);padding-left:.5rem;margin-left:-.75rem}
290
+ .mgrcols{display:flex;gap:1rem;align-items:flex-start;flex-wrap:wrap}
291
+ .mgrlist{flex:0 0 13rem;display:flex;flex-direction:column;gap:.15rem;
292
+ max-height:22rem;overflow-y:auto}
293
+ .mgrpane{flex:1 1 18rem;min-width:0}
294
+ .mgrrow{display:flex;flex-direction:column;align-items:flex-start;gap:.1rem;
295
+ font:inherit;text-align:left;background:none;border:0;border-radius:.375rem;
296
+ padding:.4rem .55rem;cursor:pointer;color:var(--ink)}
297
+ .mgrrow:hover{background:color-mix(in oklch,var(--accent) 12%,transparent)}
298
+ .mgrrow.on{background:color-mix(in oklch,var(--accent) 22%,transparent);
299
+ box-shadow:inset 3px 0 0 var(--accent)}
300
+ .mgrrow:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
301
+ .mgrname{font-size:.85rem;font-weight:600}
302
+ .modetabs{display:flex;gap:.3rem;flex-wrap:wrap;margin:.4rem 0 .5rem}
303
+ .modetab{font:inherit;font-size:.78rem;padding:.3rem .6rem;border-radius:999px;
304
+ border:1px solid var(--line);background:none;color:var(--sub);cursor:pointer}
305
+ .modetab.on{background:var(--accent);border-color:var(--accent);color:#fff}
306
+ .modetab:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
307
+ .tin{width:100%;box-sizing:border-box;font:inherit;font-size:.85rem;
308
+ padding:.4rem .5rem;border-radius:.375rem;border:1px solid var(--line);
309
+ background:transparent;color:var(--ink);margin-bottom:.5rem}
310
+ .tin:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
311
+ .fields{max-height:20rem;overflow-y:auto}
312
+ .mgrh{font-size:1rem;margin:0 0 .15rem}
313
+ /* Single column on narrow viewports: a 13rem list beside an 18rem pane
314
+ cannot both fit on a phone, and side-by-side would force a horizontal
315
+ scroll on the whole widget. */
316
+ @media (max-width:34rem){ .mgrlist{flex-basis:100%;max-height:11rem} }
317
+ .tedit{flex:1;min-width:6rem;accent-color:var(--accent);height:1.25rem}
318
+ .tedit:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
319
+ .tval{font-variant-numeric:tabular-nums;font-size:.78rem;width:2.6rem;text-align:right}
320
+ .editbar{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-top:.75rem;
321
+ padding-top:.75rem;border-top:1px solid var(--line)}
322
+ .btn{font:inherit;font-size:.8rem;padding:.35rem .7rem;border-radius:.375rem;
323
+ border:1px solid var(--line);background:var(--accent);color:#fff;cursor:pointer}
324
+ .btn:disabled{opacity:.45;cursor:default}
325
+ .btn.ghost{background:none;color:var(--sub)}
326
+ .btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
327
+ /* Labels wrap instead of truncating, in a column wide enough to hold a real
328
+ trait name. At .68rem in 5.6rem with nowrap+ellipsis these read
329
+ "Metacognitive...", "Interrupt Reco...", "Fear Of Missin..." -- 10.88px
330
+ text that also would not say which trait the bar belonged to. A label you
331
+ cannot read is not a label. (2026-07-31) */
332
+ .tname{font-size:.78rem;line-height:1.25;color:var(--sub);width:9rem;text-align:right;
333
+ flex:0 0 auto;white-space:normal;overflow-wrap:break-word;hyphens:auto}
334
+ .track{flex:1;height:16px;border-radius:999px;background:var(--raise);position:relative;overflow:hidden}
335
+ .track .base{position:absolute;top:0;bottom:0;left:50%;width:1px;background:color-mix(in srgb,var(--ink) 22%,transparent);z-index:1}
336
+ .track i{display:block;height:100%;border-radius:999px}
337
+ .tval{font:.74rem/1 var(--mono);width:2.4rem;text-align:right;flex:0 0 auto;font-variant-numeric:tabular-nums}
338
+ button.tname{background:none;border:0;padding:0;font:inherit;font-size:.78rem;line-height:1.25;
339
+ color:var(--sub);cursor:help;text-align:right;white-space:normal;overflow-wrap:break-word;
340
+ text-decoration:underline dotted currentColor;text-underline-offset:2px}
341
+ /* Narrow viewports cannot afford 9rem of label AND a readable bar. */
342
+ @media (max-width:30rem){
343
+ .tname,button.tname{width:6.6rem;font-size:.72rem}
344
+ .tval{width:2.1rem;font-size:.7rem}
345
+ }
346
+ button.tname:hover,button.tname:focus-visible{color:var(--ink)}
347
+ button.tname:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px}
348
+ /* position:fixed, not absolute: the trait list sits inside scroll and
349
+ details containers, and an absolutely positioned tip would be clipped by
350
+ the first one with overflow set. */
351
+ .tip{position:fixed;z-index:50;max-width:270px;padding:.55rem .65rem;border-radius:7px;
352
+ background:#14171d;color:#f2f4f7;font-size:.76rem;line-height:1.45;
353
+ box-shadow:0 6px 20px rgba(0,0,0,.3);pointer-events:none;opacity:0;transition:opacity 120ms ease}
354
+ .tip.on{opacity:1}
355
+ .tip b{display:block;font-size:.72rem;letter-spacing:.04em;text-transform:uppercase;
356
+ color:#9fb4d8;margin-bottom:.2rem}
357
+ .tip .ends{margin-top:.35rem;color:#aab4c2;font-size:.72rem}
358
+ @media (prefers-reduced-motion:reduce){.tip{transition:none}}
359
+ /* Banded scale. Each band is tinted by its own position on the same
360
+ red-to-green ramp the persona meters use, so a value reads the same way in
361
+ both views, and the active band is raised rather than recoloured -- moving
362
+ the hue would break that correspondence. */
363
+ /* Top margin is the marker's room. Without it the value label collides with
364
+ the block heading above, and the tick ran the full height of the band and
365
+ struck through the label it was pointing at. */
366
+ .chain{margin:1rem 0}
367
+ .chain .seg{display:flex;align-items:center;gap:.5rem;margin:.35rem 0}
368
+ .chain .lbl{flex:0 0 9.5rem;font-size:.8rem;opacity:.85;text-align:right}
369
+ .chain .track{flex:1;height:1.35rem;background:rgba(127,127,127,.16);border-radius:3px;position:relative;overflow:hidden}
370
+ .chain .fill{height:100%;border-radius:3px}
371
+ .chain .num{flex:0 0 3.6rem;font-size:.78rem;font-variant-numeric:tabular-nums;opacity:.8}
372
+ .chain .seg.peak .lbl{font-weight:700;opacity:1}
373
+ .chain .peaktag{font-size:.68rem;letter-spacing:.04em;text-transform:uppercase;padding:.05rem .3rem;border:1px solid currentColor;border-radius:3px;margin-left:.35rem}
374
+ .chain button.seg{width:100%;background:none;border:0;padding:.1rem 0;font:inherit;color:inherit;cursor:pointer;border-radius:4px}
375
+ .chain button.seg:hover .track,.chain button.seg:focus-visible .track{outline:2px solid var(--brand);outline-offset:2px}
376
+ .chain button.seg:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
377
+ .chain button.seg[aria-expanded="true"] .lbl{text-decoration:underline}
378
+ .chain .noov{flex:0 0 auto;font-size:.7rem;opacity:.55;margin-left:.35rem}
379
+ .ovpane{margin:.6rem 0 1rem;padding:.6rem;border:1px solid rgba(127,127,127,.28);border-radius:6px}
380
+ .ovpane img{max-width:100%;height:auto;display:block;border-radius:4px}
381
+ .ovpane .cap{font-size:.8rem;opacity:.8;margin:.45rem 0 0}
382
+ @media (max-width:520px){
383
+ /* The fixed 9.5rem label column took a third of a 393px viewport and left
384
+ the bars as stubs — the one thing the block exists to show. Labels move
385
+ above their track so the bar gets the full width. */
386
+ .chain .seg{flex-wrap:wrap;row-gap:.15rem}
387
+ .chain .lbl{flex:1 1 100%;text-align:left;font-size:.78rem}
388
+ .chain .track{flex:1 1 auto;min-width:0}
389
+ .chain .num{flex:0 0 3.2rem;text-align:right}
390
+ .chain button.seg{text-align:left}
391
+ }
392
+ .amp{margin:1.1rem 0 .3rem;padding-top:.8rem;border-top:1px solid rgba(127,127,127,.22)}
393
+ .amp .seg .lbl{flex:0 0 9.5rem}
394
+ .ampnote{font-size:.8rem;opacity:.75;margin:.45rem 0 0}
395
+ .scale{margin:1.5rem 0 .8rem}
396
+ .bands{display:flex;gap:3px;position:relative}
397
+ .band{flex:1;height:30px;border-radius:5px;display:grid;place-items:center;
398
+ font:.62rem/1 var(--mono);text-transform:uppercase;letter-spacing:.05em;
399
+ color:color-mix(in srgb, var(--ink) 78%, transparent);opacity:.42;
400
+ border:2px solid transparent;overflow:hidden;white-space:nowrap;padding:0 .2rem}
401
+ .band.on{opacity:1;border-color:var(--ink);font-weight:650}
402
+ .marker{position:absolute;top:-17px;transform:translateX(-50%);font:600 .64rem/1 var(--mono);
403
+ color:var(--ink);white-space:nowrap;pointer-events:none}
404
+ /* Stops at the band's top edge rather than crossing it. */
405
+ .marker::after{content:"";display:block;width:2px;height:7px;border-radius:1px;
406
+ background:var(--ink);margin:3px auto 0}
407
+ .behav{list-style:none;padding:0;margin:.6rem 0 0}
408
+ .behav li{position:relative;padding:.22rem 0 .22rem .95rem;font-size:.86rem}
409
+ .behav li::before{content:"";position:absolute;left:.15rem;top:.72rem;
410
+ width:5px;height:5px;border-radius:50%;background:var(--accent)}
411
+ .lvlname{font-size:.8rem;font-weight:600;margin:.7rem 0 0}
412
+ .ovwrap{position:relative;display:block;max-width:100%;border:1px solid var(--line);
413
+ border-radius:6px;overflow:hidden;background:var(--raise)}
414
+ .ovwrap img{display:block;width:100%;height:auto}
415
+ .bx{position:absolute;border:2px solid;border-radius:2px}
416
+ .bxn{position:absolute;top:-9px;left:-9px;min-width:17px;height:17px;
417
+ border-radius:9px;color:#fff;font-size:11px;font-weight:700;line-height:17px;
418
+ text-align:center;padding:0 4px;box-sizing:border-box}
419
+ .lg{list-style:none;display:flex;flex-wrap:wrap;gap:.7rem;padding:0;margin:.5rem 0 0;font-size:.76rem}
420
+ .lg li{display:flex;align-items:center;gap:.35rem;color:var(--sub)}
421
+ .lgnote{font-size:.72rem;color:var(--sub);margin:.35rem 0 0;opacity:.85}
422
+ .fmeta{display:flex;align-items:center;gap:.35rem}
423
+ /* Outlined, not filled. This read color:var(--bg) -- and --bg is defined
424
+ nowhere in this stylesheet, so the declaration was invalid at computed-
425
+ value time and the colour fell back to inherited: dark text on the
426
+ mid-dark --sub fill, in both themes. There is no --bg to define, either:
427
+ the widget body is transparent and sits on the host's background, so the
428
+ only foreground guaranteed to contrast is the one the body already uses.
429
+ Text is --ink on the page itself, which is exactly that. (2026-07-31) */
430
+ .fnum{min-width:17px;height:17px;border-radius:9px;
431
+ background:transparent;color:var(--ink);border:1px solid var(--line);
432
+ font-size:11px;font-weight:700;line-height:15px;text-align:center;
433
+ padding:0 4px;box-sizing:border-box;flex:none}
434
+ .tabs{display:flex;flex-wrap:wrap;gap:.3rem;margin:0 0 .7rem;padding:0;border-bottom:1px solid var(--line)}
435
+ .tab{background:none;border:0;border-bottom:2px solid transparent;padding:.35rem .6rem;
436
+ font:inherit;font-size:.8rem;color:var(--sub);cursor:pointer;border-radius:4px 4px 0 0;
437
+ display:flex;align-items:center;gap:.4rem;max-width:100%;overflow-wrap:anywhere;text-align:left}
438
+ .tab:hover{color:var(--ink)}
439
+ .tab[aria-selected="true"]{color:var(--ink);border-bottom-color:var(--accent);font-weight:600}
440
+ .tab:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
441
+ .tabn{font:.68rem/1 var(--mono);color:var(--sub);background:var(--raise);
442
+ border-radius:999px;padding:.15rem .35rem;flex:none}
443
+ .tab[aria-selected="true"] .tabn{color:var(--ink)}
444
+ .sw{width:11px;height:11px;border-radius:2px;display:inline-block}
445
+ .offlist{margin:.6rem 0 0;padding:0;list-style:none;font-size:.8rem}
446
+ .offlist li{padding:.18rem 0;color:var(--sub)}
447
+ .shot{max-width:100%;height:auto;display:block;border-radius:6px;border:1px solid var(--line)}
448
+ .cap{font-size:.77rem;color:var(--sub);margin:.35rem 0 0}
449
+ .foot{padding:0 18px 14px;font-size:.77rem;color:var(--sub)}
450
+ .msg{padding:16px 18px;font-size:.85rem;color:var(--sub)}
451
+ @media (max-width:420px){
452
+ .hero{padding:14px 18px 13px}
453
+ .health{letter-spacing:.06em}
454
+ }
455
+ @media (prefers-reduced-motion:reduce){.chev,.stitle{transition:none}}
456
+ `;
457
+ /**
458
+ * Shared runtime.
459
+ *
460
+ * Interprets the spec against structuredContent. Every value is written with
461
+ * textContent and never innerHTML: views are hydrated from tool output that
462
+ * contains strings this server does not control -- paths, page titles, audit
463
+ * findings scraped off third-party sites -- and interpolating those into
464
+ * markup is how a status panel becomes an injection sink.
465
+ */
466
+ const RUNTIME = String.raw `
467
+ (async () => {
468
+ var SPEC = __SPEC__;
469
+ var LOGO = "__LOGO__";
470
+ var msg = document.getElementById("msg");
471
+ var root = document.getElementById("root");
472
+ if (!globalThis.ExtApps) { msg.textContent = "Widget runtime unavailable."; return; }
473
+ var App = globalThis.ExtApps.App;
474
+ var applyHostStyleVariables = globalThis.ExtApps.applyHostStyleVariables;
475
+ var app = new App({ name: SPEC.id, version: "1.0.0" }, {}, { autoResize: true });
476
+
477
+ var el = function (tag, cls, text) {
478
+ var n = document.createElement(tag);
479
+ if (cls) n.className = cls;
480
+ if (text !== undefined) n.textContent = text;
481
+ return n;
482
+ };
483
+ var at = function (obj, path) {
484
+ if (!path) return undefined;
485
+ return path.split(".").reduce(function (o, k) {
486
+ return (o === null || o === undefined) ? undefined : o[k];
487
+ }, obj);
488
+ };
489
+ var isObjArray = function (v) {
490
+ return Array.isArray(v) && v.length > 0 &&
491
+ v.every(function (x) { return x && typeof x === "object" && !Array.isArray(x); });
492
+ };
493
+ // Floating-point residue is not precision. computeTimeMs arrived as
494
+ // 574.0467359999966 and attentionMismatch as 0.300375; the trailing digits
495
+ // are artefacts of binary floating point, and printing them implies a
496
+ // confidence the measurement does not have. Trait values like 0.25 and
497
+ // integers are untouched.
498
+ var num = function (n) {
499
+ if (!isFinite(n)) return String(n);
500
+ if (Number.isInteger(n)) return String(n);
501
+ return String(Math.round(n * 1e4) / 1e4);
502
+ };
503
+ var fmt = function (v) {
504
+ if (v === true) return "yes";
505
+ if (v === false) return "no";
506
+ if (typeof v === "number") return num(v);
507
+ if (v === "" || v === null || v === undefined) return "—";
508
+ if (Array.isArray(v)) {
509
+ // An array of records has no useful join; String() on it yields a row of
510
+ // "[object Object]" separated by commas.
511
+ if (isObjArray(v)) return v.length + (v.length === 1 ? " entry" : " entries");
512
+ return v.length ? v.join(", ") : "—";
513
+ }
514
+ // Backstop for an object that reached a cell. Naming its fields beats both
515
+ // "[object Object]" (says nothing) and a raw JSON dump (says everything,
516
+ // illegibly, and blows the column width out). The empathy per-persona
517
+ // table carried six such columns -- perceptualTransport, scoreContext,
518
+ // cognitiveLoad and friends -- each printing serialised JSON into a table
519
+ // cell. (2026-07-31)
520
+ if (typeof v === "object") {
521
+ try {
522
+ var keys = Object.keys(v);
523
+ if (!keys.length) return "—";
524
+ var shown = keys.slice(0, 3).map(titleize).join(", ");
525
+ return keys.length > 3 ? shown + " +" + (keys.length - 3) + " more" : shown;
526
+ } catch (_) { return "(unreadable)"; }
527
+ }
528
+ return String(v);
529
+ };
530
+ var titleize = function (k) {
531
+ return k.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, function (c) { return c.toUpperCase(); });
532
+ };
533
+ var used = {};
534
+ var GLOSSARY = __GLOSSARY__;
535
+
536
+ // One tip element reused by every label, created lazily.
537
+ var tip = null;
538
+ function ensureTip() {
539
+ if (tip) return tip;
540
+ tip = el("div", "tip");
541
+ tip.id = "kit-tip";
542
+ tip.setAttribute("role", "tooltip");
543
+ document.body.appendChild(tip);
544
+ return tip;
545
+ }
546
+ function showTip(btn, info) {
547
+ var t = ensureTip();
548
+ t.replaceChildren();
549
+ t.appendChild(el("b", null, info.label));
550
+ t.appendChild(document.createTextNode(info.description || ""));
551
+ if (info.lowEnd || info.highEnd) {
552
+ var e = el("div", "ends");
553
+ if (info.lowEnd) e.appendChild(el("div", null, "0.0 — " + info.lowEnd));
554
+ if (info.highEnd) e.appendChild(el("div", null, "1.0 — " + info.highEnd));
555
+ t.appendChild(e);
556
+ }
557
+ t.classList.add("on");
558
+ var r = btn.getBoundingClientRect();
559
+ var tr = t.getBoundingClientRect();
560
+ // Flip below when there is not room above, and clamp horizontally so the
561
+ // tip never leaves the frame.
562
+ var top = r.top - tr.height - 8;
563
+ if (top < 4) top = r.bottom + 8;
564
+ var left = Math.min(Math.max(4, r.left), Math.max(4, window.innerWidth - tr.width - 4));
565
+ t.style.top = top + "px";
566
+ t.style.left = left + "px";
567
+ }
568
+ function hideTip() { if (tip) tip.classList.remove("on"); }
569
+ document.addEventListener("keydown", function (e) { if (e.key === "Escape") hideTip(); });
570
+
571
+ /**
572
+ * Flatten an object into label/value pairs, following nested objects.
573
+ *
574
+ * kv and rest previously descended exactly one level, so anything deeper
575
+ * reached fmt() as an object and rendered "[object Object]" -- scoreContext
576
+ * is several levels deep, and every branch of it came out as that string.
577
+ *
578
+ * Arrays of records are left intact for the caller to table; arrays of
579
+ * scalars join. Depth-limited so a cyclic or pathological payload cannot
580
+ * hang the view, and the cutoff is reported rather than silently truncating.
581
+ */
582
+ function flattenPairs(obj, prefix, out, depth) {
583
+ out = out || [];
584
+ depth = depth === undefined ? 4 : depth;
585
+ Object.keys(obj || {}).forEach(function (k) {
586
+ var v = obj[k];
587
+ var label = prefix ? prefix + " › " + titleize(k) : titleize(k);
588
+ if (v && typeof v === "object" && !Array.isArray(v)) {
589
+ if (depth > 0) flattenPairs(v, label, out, depth - 1);
590
+ else out.push([label, "(nested further)"]);
591
+ } else if (isObjArray(v)) {
592
+ out.push([label, v.length + (v.length === 1 ? " entry" : " entries")]);
593
+ } else {
594
+ out.push([label, v]);
595
+ }
596
+ });
597
+ return out;
598
+ }
599
+
600
+ function kvTable(pairs) {
601
+ var t = el("table", "kv"), tb = el("tbody");
602
+ pairs.forEach(function (p) {
603
+ var tr = el("tr");
604
+ tr.appendChild(el("th", null, p[0]));
605
+ var td = el("td");
606
+ if (/^\//.test(String(p[1]))) td.className = "path";
607
+ td.textContent = fmt(p[1]);
608
+ tr.appendChild(td); tb.appendChild(tr);
609
+ });
610
+ t.appendChild(tb); return t;
611
+ }
612
+
613
+ function gridTable(rows, only) {
614
+ var cols = [];
615
+ if (only && only.length) {
616
+ // Keep only columns that at least one row actually carries, so a spec
617
+ // naming an optional field does not produce a column of dashes.
618
+ only.forEach(function (k) {
619
+ if (rows.some(function (r) { return r[k] !== undefined; })) cols.push(k);
620
+ });
621
+ } else {
622
+ rows.forEach(function (r) {
623
+ Object.keys(r).forEach(function (k) { if (cols.indexOf(k) < 0) cols.push(k); });
624
+ });
625
+ }
626
+ var t = el("table", "grid"), thead = el("thead"), htr = el("tr");
627
+ cols.forEach(function (c) { htr.appendChild(el("th", null, c)); });
628
+ thead.appendChild(htr);
629
+ var tb = el("tbody");
630
+ rows.forEach(function (r) {
631
+ var tr = el("tr");
632
+ cols.forEach(function (c) {
633
+ var v = r[c], td = el("td");
634
+ if (typeof v === "boolean") td.appendChild(el("span", "chip" + (v ? "" : " no"), v ? "yes" : "no"));
635
+ else if (typeof v === "number") { td.className = "num"; td.textContent = num(v); }
636
+ else if (/path/i.test(c) || /^\//.test(String(v))) { td.className = "path"; td.textContent = fmt(v); }
637
+ else td.textContent = fmt(v);
638
+ tr.appendChild(td);
639
+ });
640
+ tb.appendChild(tr);
641
+ });
642
+ t.appendChild(thead); t.appendChild(tb);
643
+ var box = el("div", "scroll"); box.appendChild(t); return box;
644
+ }
645
+
646
+ // Two vocabularies, because the tools genuinely use two. Bug hunting emits
647
+ // critical/high/medium/low; the accessibility audit emits major/minor. An
648
+ // unrecognised severity sorts last and renders in the low style, so omitting
649
+ // major/minor would have shown every accessibility barrier as least-severe
650
+ // -- the opposite of what an audit is for.
651
+ var SEV_ORDER = {
652
+ critical: 0, blocker: 0,
653
+ high: 1, major: 1, serious: 1,
654
+ medium: 2, moderate: 2,
655
+ low: 3, minor: 3,
656
+ info: 4, notice: 4,
657
+ };
658
+ function findingsList(rows, b) {
659
+ var sorted = rows.slice().sort(function (x, y) {
660
+ var a = SEV_ORDER[String(x[b.severityKey] || "").toLowerCase()];
661
+ var c = SEV_ORDER[String(y[b.severityKey] || "").toLowerCase()];
662
+ return (a === undefined ? 9 : a) - (c === undefined ? 9 : c);
663
+ });
664
+ var ul = el("ul", "finds");
665
+ sorted.forEach(function (r) {
666
+ var li = el("li");
667
+ var sev = String(r[b.severityKey] || "").toLowerCase();
668
+ // Number and severity travel together as ONE grid child. Appending the
669
+ // number as a third child put it in a column sized by the description
670
+ // text on the next row, so the badge stretched into a full-width bar.
671
+ var meta = el("span", "fmeta");
672
+ // The number drawn on this finding's boxes in the overlay above.
673
+ if (r.finding) meta.appendChild(el("span", "fnum", String(r.finding)));
674
+ meta.appendChild(el("span", "sev " + (SEV_ORDER[sev] !== undefined ? sev : "low"), sev || "note"));
675
+ li.appendChild(meta);
676
+ li.appendChild(el("span", null, fmt(r[b.textKey])));
677
+ if (b.detailKey && r[b.detailKey]) li.appendChild(el("p", "fdetail", fmt(r[b.detailKey])));
678
+ ul.appendChild(li);
679
+ });
680
+ return ul;
681
+ }
682
+
683
+ /**
684
+ * Editable trait sliders that write back through the server.
685
+ *
686
+ * The widget sandbox has no network of its own -- it cannot POST anywhere --
687
+ * but app.callServerTool reaches the MCP server that rendered it, which is
688
+ * the same channel the image blocks already use to fetch artifacts. So an
689
+ * editor is possible here without any new transport.
690
+ *
691
+ * Save is explicit rather than live-on-drag. Every drag would otherwise be a
692
+ * write to two stores, and a slider dragged across its range would produce a
693
+ * dozen persona versions of which only the last was meant.
694
+ */
695
+ function editorBlock(rows, b, data) {
696
+ var wrap = el("div", "traits");
697
+ var personaName = at(data, b.personaField || "persona") || at(data, "persona_name");
698
+ var edited = {};
699
+ var controls = [];
700
+
701
+ rows.forEach(function (r) {
702
+ var name = String(r.name), v = Number(r.value);
703
+ if (!isFinite(v)) return;
704
+ var line = el("div", "trait");
705
+ var label = el("span", "tname", titleize(name));
706
+ var input = document.createElement("input");
707
+ input.type = "range"; input.min = "0"; input.max = "1"; input.step = "0.05";
708
+ input.value = String(v); input.className = "tedit";
709
+ input.setAttribute("aria-label", titleize(name));
710
+ var out = el("span", "tval", v.toFixed(2));
711
+ // The original stays visible next to the new value: an editor that hides
712
+ // what you started from makes "did I mean to move that?" unanswerable.
713
+ var orig = el("span", "cap", " was " + v.toFixed(2));
714
+ orig.style.display = "none";
715
+ input.addEventListener("input", function () {
716
+ var nv = Number(input.value);
717
+ out.textContent = nv.toFixed(2);
718
+ if (Math.abs(nv - v) < 1e-9) { delete edited[name]; orig.style.display = "none"; line.classList.remove("dirty"); }
719
+ else { edited[name] = nv; orig.style.display = ""; line.classList.add("dirty"); }
720
+ status.textContent = Object.keys(edited).length
721
+ ? Object.keys(edited).length + " unsaved change(s)" : "";
722
+ save.disabled = !Object.keys(edited).length;
723
+ });
724
+ line.appendChild(label); line.appendChild(input); line.appendChild(out); line.appendChild(orig);
725
+ wrap.appendChild(line);
726
+ controls.push({ name: name, input: input, out: out, base: v });
727
+ });
728
+
729
+ var bar = el("div", "editbar");
730
+ var save = el("button", "btn", "Save changes");
731
+ save.type = "button"; save.disabled = true;
732
+ var reset = el("button", "btn ghost", "Reset");
733
+ reset.type = "button";
734
+ var status = el("span", "cap", "");
735
+ bar.appendChild(save); bar.appendChild(reset); bar.appendChild(status);
736
+ wrap.appendChild(bar);
737
+
738
+ reset.addEventListener("click", function () {
739
+ controls.forEach(function (c) {
740
+ c.input.value = String(c.base); c.out.textContent = c.base.toFixed(2);
741
+ c.input.parentNode.classList.remove("dirty");
742
+ c.input.parentNode.querySelectorAll(".cap").forEach(function (n) { n.style.display = "none"; });
743
+ });
744
+ edited = {}; status.textContent = ""; save.disabled = true;
745
+ });
746
+
747
+ save.addEventListener("click", async function () {
748
+ if (!personaName) { status.textContent = "No persona name in this result — cannot save."; return; }
749
+ save.disabled = true; status.textContent = "Saving…";
750
+ try {
751
+ var res = await app.callServerTool({
752
+ name: b.saveTool || "persona_update",
753
+ arguments: { persona_name: personaName, traits: edited },
754
+ });
755
+ var txt = ((res && res.content) || []).filter(function (c) { return c.type === "text"; })[0];
756
+ var out = txt ? JSON.parse(txt.text) : {};
757
+ if (out.error) { status.textContent = "Not saved: " + (out.message || out.error); save.disabled = false; return; }
758
+ // The drift check comes back with the save, so the moment a trait edit
759
+ // contradicts the description is the moment it is said -- not the next
760
+ // time somebody happens to look.
761
+ var drift = (out.descriptionDrift || []).length;
762
+ status.textContent = "Saved. " + (out.changed || []).length + " field(s) changed"
763
+ + (drift ? " — " + drift + " description claim(s) now contradicted" : "")
764
+ + (out.stores && out.stores.cms !== "written" ? " — CMS NOT written" : "");
765
+ controls.forEach(function (c) {
766
+ if (edited[c.name] !== undefined) { c.base = edited[c.name]; }
767
+ c.input.parentNode.classList.remove("dirty");
768
+ });
769
+ edited = {};
770
+ // Tell the conversation, so the model knows the persona moved under it
771
+ // rather than answering later from the values it was handed.
772
+ app.sendMessage({ role: "user", content: [{ type: "text",
773
+ text: "Updated persona " + personaName + " — " + (out.changed || []).join(", ")
774
+ + (drift ? ". Description now contradicts: " + out.descriptionDrift.map(function (d) { return d.trait; }).join(", ") : "") }] });
775
+ } catch (e) {
776
+ status.textContent = "Save failed: " + (e && e.message ? e.message : e);
777
+ save.disabled = false;
778
+ }
779
+ });
780
+ return wrap;
781
+ }
782
+
783
+ /**
784
+ * The whole persona CRUD surface in one block.
785
+ *
786
+ * A roster on the left, the selected persona's editable traits on the right,
787
+ * and save / delete / new. Every write goes out through app.callServerTool to
788
+ * the tools that already do the work -- persona_update, persona_delete,
789
+ * persona_create_submit_traits -- so this is a surface over them rather than
790
+ * a second implementation of them. That matters: a duplicate write path is
791
+ * the exact defect that has produced nine divergent tool families here.
792
+ *
793
+ * The roster ships with the result, so switching personas is instant and
794
+ * costs no round trip. Only writes go back to the server.
795
+ */
796
+ function managerBlock(data, b) {
797
+ var wrap = el("div", "mgr");
798
+ var roster = at(data, b.field) || [];
799
+ if (!roster.length) return wrap;
800
+ var selected = roster[0];
801
+ var edited = {};
802
+
803
+ var cols = el("div", "mgrcols");
804
+ var list = el("div", "mgrlist");
805
+ var pane = el("div", "mgrpane");
806
+ cols.appendChild(list); cols.appendChild(pane);
807
+ wrap.appendChild(cols);
808
+
809
+ var status = el("p", "cap", "");
810
+ wrap.appendChild(status);
811
+
812
+ function renderList() {
813
+ list.innerHTML = "";
814
+ roster.forEach(function (p, i) {
815
+ var row = el("button", "mgrrow" + (p === selected ? " on" : ""), "");
816
+ row.type = "button";
817
+ row.setAttribute("aria-pressed", p === selected ? "true" : "false");
818
+ row.appendChild(el("span", "mgrname", String(p.name || "(unnamed)")));
819
+ var meta = [];
820
+ if (p.builtin) meta.push("built-in");
821
+ if (p.valuesRoute) meta.push(String(p.valuesRoute));
822
+ if (meta.length) row.appendChild(el("span", "cap", meta.join(" · ")));
823
+ row.addEventListener("click", function () {
824
+ if (Object.keys(edited).length &&
825
+ !confirm("Discard " + Object.keys(edited).length + " unsaved change(s)?")) return;
826
+ selected = p; edited = {}; renderList(); renderPane();
827
+ });
828
+ list.appendChild(row);
829
+ });
830
+ }
831
+
832
+ function renderPane() {
833
+ pane.innerHTML = "";
834
+ var p = selected;
835
+ pane.appendChild(el("h3", "mgrh", String(p.name)));
836
+ if (p.description) pane.appendChild(el("p", "cap", String(p.description)));
837
+
838
+ var traits = p.traits || {};
839
+ var names = Object.keys(traits);
840
+ var box = el("div", "traits");
841
+ names.forEach(function (k) {
842
+ var v = Number(traits[k]);
843
+ if (!isFinite(v)) return;
844
+ var line = el("div", "trait");
845
+ line.appendChild(el("span", "tname", titleize(k)));
846
+ var input = document.createElement("input");
847
+ input.type = "range"; input.min = "0"; input.max = "1"; input.step = "0.05";
848
+ input.value = String(v); input.className = "tedit";
849
+ input.setAttribute("aria-label", titleize(k));
850
+ var out = el("span", "tval", v.toFixed(2));
851
+ input.addEventListener("input", function () {
852
+ var nv = Number(input.value);
853
+ out.textContent = nv.toFixed(2);
854
+ if (Math.abs(nv - v) < 1e-9) { delete edited[k]; line.classList.remove("dirty"); }
855
+ else { edited[k] = nv; line.classList.add("dirty"); }
856
+ save.disabled = !Object.keys(edited).length;
857
+ status.textContent = Object.keys(edited).length
858
+ ? Object.keys(edited).length + " unsaved change(s) on " + p.name : "";
859
+ });
860
+ line.appendChild(input); line.appendChild(out);
861
+ box.appendChild(line);
862
+ });
863
+ pane.appendChild(box);
864
+
865
+ var bar = el("div", "editbar");
866
+ var save = el("button", "btn", "Save");
867
+ save.type = "button"; save.disabled = true;
868
+ var del = el("button", "btn ghost", "Delete");
869
+ del.type = "button";
870
+ // A built-in is shared by every install; the server refuses to edit or
871
+ // delete one, so the UI does not offer to.
872
+ if (p.builtin) { save.disabled = true; del.disabled = true; }
873
+ bar.appendChild(save); bar.appendChild(del);
874
+ // The same three routes on update: revise from a new description,
875
+ // re-answer the survey, or supply a Big Five profile. Which fields are
876
+ // meaningful depends entirely on which route you are taking.
877
+ var via = el("button", "btn ghost", "Update via…");
878
+ via.type = "button";
879
+ via.disabled = !!p.builtin;
880
+ via.addEventListener("click", function () { renderForm("description", p); });
881
+ bar.appendChild(via);
882
+ pane.appendChild(bar);
883
+
884
+ save.addEventListener("click", async function () {
885
+ save.disabled = true; status.textContent = "Saving…";
886
+ try {
887
+ var res = await app.callServerTool({ name: "persona_update",
888
+ arguments: { persona_name: p.name, traits: edited } });
889
+ var t = ((res && res.content) || []).filter(function (c) { return c.type === "text"; })[0];
890
+ var out = t ? JSON.parse(t.text) : {};
891
+ if (out.error) { status.textContent = "Not saved: " + (out.message || out.error); save.disabled = false; return; }
892
+ Object.keys(edited).forEach(function (k) { p.traits[k] = edited[k]; });
893
+ var drift = (out.descriptionDrift || []).length;
894
+ status.textContent = "Saved " + p.name + ". " + (out.changed || []).length + " field(s)"
895
+ + (drift ? " — " + drift + " description claim(s) now contradicted" : "")
896
+ + (out.stores && out.stores.cms !== "written" ? " — CMS NOT written" : "");
897
+ edited = {}; renderPane();
898
+ app.sendMessage({ role: "user", content: [{ type: "text",
899
+ text: "Updated persona " + p.name + ": " + (out.changed || []).join(", ") }] });
900
+ } catch (e) { status.textContent = "Save failed: " + (e && e.message ? e.message : e); save.disabled = false; }
901
+ });
902
+
903
+ del.addEventListener("click", async function () {
904
+ if (!confirm("Delete " + p.name + " from both stores? This cannot be undone here.")) return;
905
+ del.disabled = true; status.textContent = "Deleting…";
906
+ try {
907
+ var res = await app.callServerTool({ name: "persona_delete",
908
+ arguments: { persona_name: p.name, confirm: true } });
909
+ var t = ((res && res.content) || []).filter(function (c) { return c.type === "text"; })[0];
910
+ var out = t ? JSON.parse(t.text) : {};
911
+ if (out.error) { status.textContent = "Not deleted: " + (out.message || out.error); del.disabled = false; return; }
912
+ roster = roster.filter(function (x) { return x !== p; });
913
+ status.textContent = "Deleted " + p.name + ". " + (out.note || "");
914
+ app.sendMessage({ role: "user", content: [{ type: "text", text: "Deleted persona " + p.name }] });
915
+ if (!roster.length) { pane.innerHTML = ""; list.innerHTML = ""; return; }
916
+ selected = roster[0]; edited = {}; renderList(); renderPane();
917
+ } catch (e) { status.textContent = "Delete failed: " + (e && e.message ? e.message : e); del.disabled = false; }
918
+ });
919
+ }
920
+
921
+ /**
922
+ * Create / update in three modes, because a persona's values come from one
923
+ * of three routes and the route decides which fields are even meaningful.
924
+ * A single form with every field would ask for a Big Five profile and a
925
+ * survey and a description at once, when supplying any ONE of them is the
926
+ * whole job.
927
+ */
928
+ function renderForm(mode, existing) {
929
+ pane.innerHTML = "";
930
+ var modes = data.modes || {};
931
+ var spec = modes[mode] || {};
932
+ var isUpdate = !!existing;
933
+
934
+ var tabs = el("div", "modetabs");
935
+ ["description", "survey", "bigfive"].forEach(function (m) {
936
+ var t = el("button", "modetab" + (m === mode ? " on" : ""), (modes[m] && modes[m].label) || m);
937
+ t.type = "button";
938
+ t.setAttribute("aria-pressed", m === mode ? "true" : "false");
939
+ t.addEventListener("click", function () { renderForm(m, existing); });
940
+ tabs.appendChild(t);
941
+ });
942
+ pane.appendChild(el("h3", "mgrh", isUpdate ? "Update " + existing.name : "New persona"));
943
+ pane.appendChild(tabs);
944
+ if (spec.note) pane.appendChild(el("p", "cap", spec.note));
945
+
946
+ var nameIn;
947
+ if (!isUpdate) {
948
+ nameIn = document.createElement("input");
949
+ nameIn.type = "text"; nameIn.className = "tin"; nameIn.placeholder = "persona name";
950
+ nameIn.setAttribute("aria-label", "Persona name");
951
+ pane.appendChild(nameIn);
952
+ }
953
+
954
+ var fields = el("div", "fields");
955
+ pane.appendChild(fields);
956
+ var collect = function () { return {}; };
957
+
958
+ if (mode === "description") {
959
+ var ta = document.createElement("textarea");
960
+ ta.className = "tin"; ta.rows = 5;
961
+ ta.placeholder = "A cautious retiree who double-checks everything before buying…";
962
+ ta.value = isUpdate ? String(existing.description || "") : "";
963
+ ta.setAttribute("aria-label", "Description");
964
+ fields.appendChild(ta);
965
+ collect = function () { return { description: ta.value.trim() }; };
966
+ } else if (mode === "survey") {
967
+ var criteria = data.traitCriteria || {};
968
+ var names = data.allTraits || Object.keys(criteria);
969
+ var vals = {};
970
+ names.forEach(function (k) {
971
+ var base = isUpdate && existing.traits && typeof existing.traits[k] === "number"
972
+ ? Number(existing.traits[k]) : 0.5;
973
+ vals[k] = base;
974
+ var line = el("div", "trait");
975
+ var lab = el("span", "tname", titleize(k));
976
+ if (criteria[k]) lab.title = criteria[k];
977
+ var input = document.createElement("input");
978
+ input.type = "range"; input.min = "0"; input.max = "1"; input.step = "0.05";
979
+ input.value = String(base); input.className = "tedit";
980
+ input.setAttribute("aria-label", titleize(k));
981
+ var out = el("span", "tval", base.toFixed(2));
982
+ input.addEventListener("input", function () {
983
+ vals[k] = Number(input.value); out.textContent = vals[k].toFixed(2);
984
+ });
985
+ line.appendChild(lab); line.appendChild(input); line.appendChild(out);
986
+ fields.appendChild(line);
987
+ });
988
+ collect = function () { return { answers: vals }; };
989
+ } else {
990
+ var bf = {};
991
+ (data.bigFiveFactors || []).forEach(function (f) {
992
+ var base = isUpdate && existing.bigFive && typeof existing.bigFive[f.factor] === "number"
993
+ ? Number(existing.bigFive[f.factor]) : 0.5;
994
+ bf[f.factor] = base;
995
+ var line = el("div", "trait");
996
+ var lab = el("span", "tname", titleize(f.factor));
997
+ lab.title = "low: " + f.low + "\nhigh: " + f.high + "\n" + (f.doNotConfuse || "");
998
+ var input = document.createElement("input");
999
+ input.type = "range"; input.min = "0"; input.max = "1"; input.step = "0.01";
1000
+ input.value = String(base); input.className = "tedit";
1001
+ input.setAttribute("aria-label", titleize(f.factor));
1002
+ var out = el("span", "tval", base.toFixed(2));
1003
+ input.addEventListener("input", function () {
1004
+ bf[f.factor] = Number(input.value); out.textContent = bf[f.factor].toFixed(2);
1005
+ });
1006
+ line.appendChild(lab); line.appendChild(input); line.appendChild(out);
1007
+ fields.appendChild(line);
1008
+ });
1009
+ collect = function () { return { bigFive: bf }; };
1010
+ }
1011
+
1012
+ var bar = el("div", "editbar");
1013
+ var go = el("button", "btn", isUpdate ? "Apply" : "Create");
1014
+ go.type = "button";
1015
+ var back = el("button", "btn ghost", "Cancel");
1016
+ back.type = "button";
1017
+ back.addEventListener("click", function () { renderPane(); });
1018
+ bar.appendChild(go); bar.appendChild(back);
1019
+ pane.appendChild(bar);
1020
+
1021
+ go.addEventListener("click", async function () {
1022
+ var name = isUpdate ? existing.name : (nameIn.value || "").trim();
1023
+ if (!name) { status.textContent = "A name is required."; return; }
1024
+ var payload = collect();
1025
+ go.disabled = true; status.textContent = "Working…";
1026
+ try {
1027
+ if (mode === "description") {
1028
+ // Handed to the conversation rather than faked. The widget has no
1029
+ // model, and a form that silently did nothing would be worse than
1030
+ // one that says where the work happens.
1031
+ app.sendMessage({ role: "user", content: [{ type: "text",
1032
+ text: (isUpdate ? "Update the persona " + name + " from this description" : "Create a persona named " + name + " from this description")
1033
+ + ", using persona_create_from_description and the completeness contract: " + payload.description }] });
1034
+ status.textContent = "Handed to the assistant — it will infer the traits and write the persona.";
1035
+ go.disabled = false;
1036
+ return;
1037
+ }
1038
+ var res, out, t;
1039
+ if (isUpdate) {
1040
+ res = await app.callServerTool({ name: "persona_update",
1041
+ arguments: mode === "survey"
1042
+ ? { persona_name: name, traits: payload.answers }
1043
+ : { persona_name: name, bigFive: payload.bigFive } });
1044
+ } else if (mode === "survey") {
1045
+ res = await app.callServerTool({ name: "persona_questionnaire_build",
1046
+ arguments: { name: name, description: "Created from a survey in persona_manager", answers: payload.answers, save: true } });
1047
+ } else {
1048
+ // Two calls, both real: build establishes the persona, update
1049
+ // attaches the Big Five and moves it to that values route.
1050
+ await app.callServerTool({ name: "persona_questionnaire_build",
1051
+ arguments: { name: name, description: "Created from a Big Five profile in persona_manager", answers: {}, save: true } });
1052
+ res = await app.callServerTool({ name: "persona_update",
1053
+ arguments: { persona_name: name, bigFive: payload.bigFive } });
1054
+ }
1055
+ t = ((res && res.content) || []).filter(function (c) { return c.type === "text"; })[0];
1056
+ out = t ? JSON.parse(t.text) : {};
1057
+ if (out.error) { status.textContent = "Failed: " + (out.message || out.error); go.disabled = false; return; }
1058
+ status.textContent = (isUpdate ? "Updated " : "Created ") + name
1059
+ + (out.valuesRoute ? " (" + out.valuesRoute + ")" : "")
1060
+ + ((out.descriptionDrift || []).length ? " — " + out.descriptionDrift.length + " description claim(s) now contradicted" : "");
1061
+ app.sendMessage({ role: "user", content: [{ type: "text",
1062
+ text: (isUpdate ? "Updated" : "Created") + " persona " + name + " via " + mode }] });
1063
+ renderPane();
1064
+ } catch (e) {
1065
+ status.textContent = "Failed: " + (e && e.message ? e.message : e);
1066
+ go.disabled = false;
1067
+ }
1068
+ });
1069
+ }
1070
+
1071
+ var newBar = el("div", "editbar");
1072
+ var newBtn = el("button", "btn", "New persona");
1073
+ newBtn.type = "button";
1074
+ newBtn.addEventListener("click", function () { renderForm("description", null); });
1075
+ newBar.appendChild(newBtn);
1076
+ wrap.insertBefore(newBar, status);
1077
+
1078
+ renderList(); renderPane();
1079
+ return wrap;
1080
+ }
1081
+
1082
+ function traitsBlock(rows, b) {
1083
+ var wrap = el("div", "traits");
1084
+ rows.forEach(function (r) {
1085
+ var name = r.name, v = Number(r.value);
1086
+ if (!isFinite(v)) return;
1087
+ var line = el("div", "trait");
1088
+ var label = titleize(String(name));
1089
+ var info = b.describe ? GLOSSARY[String(name)] : null;
1090
+ if (info) {
1091
+ // A button, not a span: the definition has to be reachable by keyboard,
1092
+ // and hover-only would hide it from anyone not using a mouse.
1093
+ var btn = el("button", "tname", label);
1094
+ btn.type = "button";
1095
+ btn.setAttribute("aria-describedby", "kit-tip");
1096
+ var payload = { label: label, description: info.d, lowEnd: info.l, highEnd: info.h };
1097
+ btn.addEventListener("mouseenter", function () { showTip(btn, payload); });
1098
+ btn.addEventListener("focus", function () { showTip(btn, payload); });
1099
+ btn.addEventListener("mouseleave", hideTip);
1100
+ btn.addEventListener("blur", hideTip);
1101
+ line.appendChild(btn);
1102
+ } else {
1103
+ line.appendChild(el("span", "tname", label));
1104
+ }
1105
+ var track = el("div", "track");
1106
+ track.appendChild(el("span", "base"));
1107
+ var fill = el("i");
1108
+ var pct = Math.max(0, Math.min(1, v)) * 100;
1109
+ fill.style.width = pct + "%";
1110
+ fill.style.backgroundColor =
1111
+ b.ramp === "value" ? "hsl(" + (220 + v * 80) + ", 55%, 55%)"
1112
+ : b.ramp === "accessibility" ? "hsl(" + (55 - v * 30) + ", 78%, 50%)"
1113
+ : "hsl(" + (v * 120) + ", 65%, 50%)";
1114
+ track.appendChild(fill);
1115
+ line.appendChild(track);
1116
+ line.appendChild(el("span", "tval", v.toFixed(2)));
1117
+ wrap.appendChild(line);
1118
+ });
1119
+ return wrap;
1120
+ }
1121
+
1122
+ // Traits arrive either as {name: number} or as [{name, value}]; both are in
1123
+ // use across the persona tools, so both are accepted rather than forcing a
1124
+ // shape the callers do not already produce.
1125
+ function normaliseTraits(v, b) {
1126
+ if (isObjArray(v)) {
1127
+ return v.map(function (r) {
1128
+ return { name: r[b.nameKey || "name"] || r.trait || r.key,
1129
+ value: r[b.valueKey || "value"] !== undefined ? r[b.valueKey || "value"] : r.score };
1130
+ });
1131
+ }
1132
+ if (v && typeof v === "object" && !Array.isArray(v)) {
1133
+ return Object.keys(v).filter(function (k) { return typeof v[k] === "number"; })
1134
+ .map(function (k) { return { name: k, value: v[k] }; });
1135
+ }
1136
+ return [];
1137
+ }
1138
+
1139
+ async function chainBlock(data, b) {
1140
+ var layers = at(data, b.field);
1141
+ if (!isObjArray(layers) || !layers.length) return null;
1142
+ var nameKey = b.nameKey || "name";
1143
+ var costKey = b.costKey || "cost";
1144
+ var bottleneck = b.bottleneckField ? at(data, b.bottleneckField) : undefined;
1145
+ var additive = b.additiveField ? Number(at(data, b.additiveField)) : NaN;
1146
+ var total = b.totalField ? Number(at(data, b.totalField)) : NaN;
1147
+
1148
+ // One scale across every bar, including the two summary bars, or the
1149
+ // comparison the block exists to make would be drawn to different rulers.
1150
+ var costs = layers.map(function (l) { return Number(l[costKey]) || 0; });
1151
+ var scaleMax = Math.max.apply(null, costs.concat(
1152
+ [isFinite(additive) ? additive : 0, isFinite(total) ? total : 0]));
1153
+ if (!(scaleMax > 0)) scaleMax = 1;
1154
+
1155
+ var wrap = el("div", "chain");
1156
+
1157
+ // Overlays keyed by layer name, so a bar can find its own evidence.
1158
+ var ovs = {};
1159
+ var ovList = b.overlaysField ? at(data, b.overlaysField) : null;
1160
+ if (isObjArray(ovList)) {
1161
+ ovList.forEach(function (o) { if (o && o.layer) ovs[String(o.layer)] = o; });
1162
+ }
1163
+ var pane = el("div");
1164
+
1165
+ function bar(label, value, hue, isPeak, ov) {
1166
+ var interactive = !!(ov && ov.available && ov.file);
1167
+ // A real <button> when there is something behind it, so it is keyboard
1168
+ // operable and announced as a control. A plain div when there is not --
1169
+ // styling a dead row to look clickable is the lie this avoids.
1170
+ var seg = interactive
1171
+ ? document.createElement("button")
1172
+ : el("div");
1173
+ seg.className = "seg" + (isPeak ? " peak" : "");
1174
+ if (interactive) { seg.type = "button"; seg.setAttribute("aria-expanded", "false"); }
1175
+ var lbl = el("div", "lbl", label);
1176
+ if (isPeak) lbl.appendChild(el("span", "peaktag", "bottleneck"));
1177
+ var track = el("div", "track");
1178
+ var fill = el("div", "fill");
1179
+ fill.style.width = Math.max(1, (value / scaleMax) * 100) + "%";
1180
+ fill.style.backgroundColor = "hsl(" + hue + ", 62%, 48%)";
1181
+ track.appendChild(fill);
1182
+ var num = el("div", "num", value.toFixed(3));
1183
+ seg.appendChild(lbl); seg.appendChild(track); seg.appendChild(num);
1184
+ if (ov && !ov.available) {
1185
+ var why = el("span", "noov", "no overlay");
1186
+ why.title = String(ov.reason || "no overlay for this layer");
1187
+ seg.appendChild(why);
1188
+ }
1189
+ if (interactive) {
1190
+ seg.addEventListener("click", function () {
1191
+ var open = seg.getAttribute("aria-expanded") === "true";
1192
+ // One pane, so opening a layer replaces the last rather than stacking
1193
+ // six screenshots down the panel.
1194
+ wrap.querySelectorAll("button.seg").forEach(function (n) { n.setAttribute("aria-expanded", "false"); });
1195
+ pane.innerHTML = "";
1196
+ if (open) return;
1197
+ seg.setAttribute("aria-expanded", "true");
1198
+ showOverlay(ov, label);
1199
+ });
1200
+ }
1201
+ return seg;
1202
+ }
1203
+
1204
+ async function showOverlay(ov, label) {
1205
+ pane.className = "ovpane";
1206
+ pane.appendChild(el("p", "cap", "Loading " + label + " overlay…"));
1207
+ try {
1208
+ var res = await app.callServerTool({
1209
+ name: b.fetchTool || "artifact_fetch",
1210
+ arguments: { file: ov.file },
1211
+ });
1212
+ var img = ((res && res.content) || []).filter(function (c) { return c.type === "image"; })[0];
1213
+ pane.innerHTML = "";
1214
+ if (!img) {
1215
+ pane.appendChild(el("p", "cap", "The " + label + " overlay could not be loaded."));
1216
+ return;
1217
+ }
1218
+ var node = document.createElement("img");
1219
+ node.src = "data:" + (img.mimeType || "image/png") + ";base64," + img.data;
1220
+ // Named for what it shows, not "overlay image" -- a screen reader user
1221
+ // gets the same sentence a sighted one reads under it.
1222
+ node.alt = label + " overlay for this page: " + (ov.legend || "");
1223
+ pane.appendChild(node);
1224
+ if (ov.legend) pane.appendChild(el("p", "cap", String(ov.legend)));
1225
+ } catch (e) {
1226
+ pane.innerHTML = "";
1227
+ pane.appendChild(el("p", "cap", "Could not load the " + label + " overlay: " + (e && e.message ? e.message : e)));
1228
+ }
1229
+ }
1230
+
1231
+ // Width and colour deliberately use DIFFERENT denominators.
1232
+ //
1233
+ // Width is on the shared scale, so a layer bar and the sequential-total bar
1234
+ // below it can be compared honestly. Colour is relative to the largest
1235
+ // LAYER, because the shared scale made every layer green: the biggest layer
1236
+ // was 0.171 against a 0.742 total, which is 23% of the ramp, so a page at
1237
+ // 63% abandonment risk rendered as six healthy green bars. Ramping colour
1238
+ // within the layers is what makes the bottleneck the hottest thing on
1239
+ // screen -- which is the one thing this block exists to show.
1240
+ var layerMax = Math.max.apply(null, costs);
1241
+ if (!(layerMax > 0)) layerMax = 1;
1242
+ layers.forEach(function (l) {
1243
+ var name = String(l[nameKey] == null ? "" : l[nameKey]);
1244
+ var cost = Number(l[costKey]) || 0;
1245
+ var isPeak = bottleneck != null && String(bottleneck) === name;
1246
+ // Cost, not capability: high is bad, so the ramp runs green to red.
1247
+ wrap.appendChild(bar(name, cost, 120 - Math.min(1, cost / layerMax) * 120, isPeak, ovs[name]));
1248
+ });
1249
+ wrap.appendChild(pane);
1250
+
1251
+ if (isFinite(additive) && isFinite(total) && additive > 0) {
1252
+ var amp = el("div", "amp");
1253
+ // Computed BEFORE the bars that read it. var hoists, so declaring this
1254
+ // below its first use left the guard comparing undefined !== null, which
1255
+ // is true -- the guard passed and the bar was drawn with no value.
1256
+ //
1257
+ // raw, not total: total is a sigmoid of raw, so plotting additive against
1258
+ // it drew a 1.034 bar beside a 0.410 bar and told the 60%-less story in
1259
+ // pixels while the caption underneath said +2%. Both bars are in
1260
+ // layer-cost units or the comparison is omitted.
1261
+ var rawTotal = at(data, "cognitiveTransportCost.raw");
1262
+ var ratio = typeof rawTotal === "number" ? rawTotal / additive : null;
1263
+ var pct = ratio === null ? 0 : Math.round(Math.abs(1 - ratio) * 100);
1264
+ if (ratio !== null) {
1265
+ amp.appendChild(bar("sum of layers", additive, 205, false, null));
1266
+ amp.appendChild(bar("actual, in sequence", rawTotal, 205, false, null));
1267
+ }
1268
+ // Describes the coefficient it actually measured.
1269
+ //
1270
+ // The first version assumed the chain AMPLIFIES, matching the old
1271
+ // sequentialAmplification name, and fell back to "close to additive"
1272
+ // otherwise. NOTE: no backticks anywhere in this block -- it is inside a
1273
+ // template literal, so one silently ends the string and the whole widget
1274
+ // stops compiling. Against real data the coefficient is 0.30-0.39, so
1275
+ // the widget confidently called a 61-70% compression "close to additive".
1276
+ // The mock payload used to build it had additive < total, which is the
1277
+ // one shape the tool has never actually produced. (2026-08-02)
1278
+ amp.appendChild(el("p", "ampnote",
1279
+ // No raw total, no claim. Falling back to the sigmoid-squashed total
1280
+ // here is what produced "about 60% less" on a chain that actually adds
1281
+ // ~1%: total is a sigmoid and additive is not, so their ratio measures
1282
+ // the squash, not the chain. A payload from before raw existed cannot
1283
+ // answer this, and saying so beats answering it wrongly.
1284
+ ratio === null
1285
+ ? "This result predates the raw cost being reported, so the chain's effect cannot be computed from it. The bars above are still exact; only the comparison below them is unavailable."
1286
+ : ratio > 1.0005
1287
+ ? "The layers run in sequence, so each one spends what the last left over. That adds "
1288
+ + pct + "% over their sum (" + ratio.toFixed(3) + "x)"
1289
+ + (bottleneck ? ", concentrated in " + bottleneck + "." : ".")
1290
+ : ratio < 0.995
1291
+ ? "Running the layers in sequence costs " + ratio.toFixed(3) + "x their sum, about "
1292
+ + pct + "% less"
1293
+ + (bottleneck ? ", and " + bottleneck + " is where the cost concentrates." : ".")
1294
+ : "Sequential and additive cost agree to within a rounding error: no layer is arriving on a materially depleted budget"
1295
+ + (bottleneck ? ", and " + bottleneck + " carries the chain on its own." : ".")));
1296
+ wrap.appendChild(amp);
1297
+ }
1298
+ return wrap;
1299
+ }
1300
+
1301
+ function levelsBlock(data, b) {
1302
+ var levels = at(data, b.field);
1303
+ if (!isObjArray(levels)) return null;
1304
+ var value = b.valueField ? at(data, b.valueField) : undefined;
1305
+ var label = b.labelField ? at(data, b.labelField) : undefined;
1306
+ var behaviors = b.behaviorsField ? at(data, b.behaviorsField) : undefined;
1307
+
1308
+ var wrap = el("div", "scale");
1309
+ var bands = el("div", "bands");
1310
+ levels.forEach(function (lv) {
1311
+ var band = el("div", "band", String(lv.label || ""));
1312
+ var v = Number(lv.value);
1313
+ if (isFinite(v)) band.style.backgroundColor = "hsl(" + (v * 120) + ", 65%, 50%)";
1314
+ // Active band chosen by nearest level, since the queried value rarely
1315
+ // lands exactly on a band's own value.
1316
+ if (label && String(lv.label) === String(label)) band.classList.add("on");
1317
+ band.title = String(lv.label || "");
1318
+ bands.appendChild(band);
1319
+ });
1320
+ if (typeof value === "number" && isFinite(value)) {
1321
+ var m = el("div", "marker", value.toFixed(2));
1322
+ m.style.left = Math.max(2, Math.min(98, value * 100)) + "%";
1323
+ bands.appendChild(m);
1324
+ }
1325
+ wrap.appendChild(bands);
1326
+
1327
+ if (label) wrap.appendChild(el("p", "lvlname", String(label)));
1328
+ if (Array.isArray(behaviors) && behaviors.length) {
1329
+ var ul = el("ul", "behav");
1330
+ behaviors.forEach(function (x) { ul.appendChild(el("li", null, String(x))); });
1331
+ wrap.appendChild(ul);
1332
+ }
1333
+ return wrap;
1334
+ }
1335
+
1336
+ function scoreBlock(data, b) {
1337
+ var wrap = el("div");
1338
+ var grade = at(data, b.gradeField);
1339
+ var score = at(data, b.scoreField);
1340
+ if (grade !== undefined || score !== undefined) {
1341
+ var row = el("div", "score");
1342
+ if (grade !== undefined) {
1343
+ row.appendChild(el("span", "grade " + String(grade).toLowerCase().charAt(0), String(grade)));
1344
+ }
1345
+ if (score !== undefined) row.appendChild(el("span", "scoresub", String(score)));
1346
+ wrap.appendChild(row);
1347
+ }
1348
+ var cats = at(data, b.categoriesField);
1349
+ if (isObjArray(cats)) {
1350
+ var ul = el("cats" === "" ? "ul" : "ul", "cats");
1351
+ // Normalised against the largest value present, so bars stay comparable
1352
+ // even when a source reports out of something other than 100.
1353
+ var max = Math.max.apply(null, cats.map(function (c) { return Number(c[b.valueKey]) || 0; }).concat([1]));
1354
+ cats.forEach(function (c) {
1355
+ var li = el("li");
1356
+ li.appendChild(el("span", "catname", fmt(c[b.labelKey])));
1357
+ var v = Number(c[b.valueKey]) || 0;
1358
+ var w = b.weightKey && c[b.weightKey] !== undefined ? " · weight " + c[b.weightKey] : "";
1359
+ li.appendChild(el("span", "catval", v + w));
1360
+ var bar = el("div", "bar"), fill = el("i");
1361
+ fill.style.width = Math.max(2, Math.round((v / max) * 100)) + "%";
1362
+ bar.appendChild(fill); li.appendChild(bar);
1363
+ ul.appendChild(li);
1364
+ });
1365
+ wrap.appendChild(ul);
1366
+ }
1367
+ return wrap;
1368
+ }
1369
+
1370
+ async function imageBlock(data, b) {
1371
+ var wrap = el("div");
1372
+ var img = document.createElement("img");
1373
+ img.className = "shot"; img.alt = "";
1374
+ var inline = b.dataField ? at(data, b.dataField) : undefined;
1375
+ if (inline) {
1376
+ img.src = String(inline).startsWith("data:") ? inline : "data:image/png;base64," + inline;
1377
+ wrap.appendChild(img);
1378
+ } else if (b.fetchTool) {
1379
+ // Fetched after mount rather than shipped in the result: hosts cap tool
1380
+ // results near 150k characters and swap in a file pointer, which reaches
1381
+ // the widget as unparseable text. Heavy assets have to come over this
1382
+ // channel instead.
1383
+ var note = el("p", "cap", "Loading image…");
1384
+ wrap.appendChild(img); wrap.appendChild(note);
1385
+ try {
1386
+ var args = b.fetchArgsField ? at(data, b.fetchArgsField) : {};
1387
+ if (b.fetchArgs) {
1388
+ args = {};
1389
+ Object.keys(b.fetchArgs).forEach(function (k) { args[k] = at(data, b.fetchArgs[k]); });
1390
+ }
1391
+ var res = await app.callServerTool({ name: b.fetchTool, arguments: args || {} });
1392
+ var blk = (res && res.content || []).filter(function (c) { return c.type === "image"; })[0];
1393
+ if (blk && blk.data) { img.src = "data:" + (blk.mimeType || "image/png") + ";base64," + blk.data; note.remove(); }
1394
+ else { note.textContent = "No image was returned."; }
1395
+ } catch (e) {
1396
+ note.textContent = "Could not load image: " + (e && e.message ? e.message : e);
1397
+ }
1398
+ } else {
1399
+ var url = at(data, b.urlField);
1400
+ // A URL on a host outside the sandbox allowlist cannot load here, so it
1401
+ // is offered as a link rather than an <img> that would render broken.
1402
+ var a = document.createElement("a");
1403
+ a.textContent = url ? "Open image" : "No image available";
1404
+ if (url) { a.href = url; a.addEventListener("click", function (ev) { ev.preventDefault(); app.openLink({ url: url }); }); }
1405
+ wrap.appendChild(a);
1406
+ return wrap;
1407
+ }
1408
+ if (b.caption) { var c = at(data, b.caption); if (c) wrap.appendChild(el("p", "cap", fmt(c))); }
1409
+ return wrap;
1410
+ }
1411
+
1412
+ var SEV_COLOR = {
1413
+ critical: "#dc2626", blocker: "#dc2626",
1414
+ major: "#ea580c", high: "#ea580c", serious: "#ea580c",
1415
+ medium: "#ca8a04", moderate: "#ca8a04",
1416
+ minor: "#2563eb", low: "#2563eb", info: "#64748b", notice: "#64748b",
1417
+ };
1418
+
1419
+ // Whichever of near-white / near-black contrasts better with a fill, by WCAG
1420
+ // relative luminance rather than by eye.
1421
+ function readableOn(hex) {
1422
+ var c = String(hex).replace("#", "");
1423
+ if (c.length !== 6) return "#fff";
1424
+ var lin = function (v) {
1425
+ v = parseInt(v, 16) / 255;
1426
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
1427
+ };
1428
+ var L = 0.2126 * lin(c.slice(0, 2)) + 0.7152 * lin(c.slice(2, 4)) + 0.0722 * lin(c.slice(4, 6));
1429
+ var onWhite = 1.05 / (L + 0.05);
1430
+ var onBlack = (L + 0.05) / 0.05;
1431
+ return onWhite >= onBlack ? "#ffffff" : "#111418";
1432
+ }
1433
+
1434
+ async function overlayBlock(data, b) {
1435
+ var shots = at(data, b.field);
1436
+ var shot = Array.isArray(shots) ? shots[0] : shots;
1437
+ if (!shot) return null;
1438
+ var rects = shot[b.rectsKey || "barrierRects"] || [];
1439
+ // The region of the document the image actually covers. captureSize is
1440
+ // read from the PNG header server-side, so it cannot disagree with the
1441
+ // picture; viewportSize is the fallback for older payloads.
1442
+ var cs = shot.captureSize || {};
1443
+ var origin = shot.captureOrigin || { x: 0, y: 0 };
1444
+ var ox = origin.x || 0, oy = origin.y || 0;
1445
+ var w = cs.width || (shot[b.widthKey || "viewportSize"] || {}).width;
1446
+ var h = cs.height || shot.captureHeight ||
1447
+ (shot[b.widthKey || "viewportSize"] || {}).height;
1448
+ if (!w || !h) return null;
1449
+
1450
+ var wrap = el("div");
1451
+ var frame = el("div", "ovwrap");
1452
+ var img = document.createElement("img");
1453
+ img.alt = "Page screenshot with accessibility barriers outlined";
1454
+ frame.appendChild(img);
1455
+ wrap.appendChild(frame);
1456
+
1457
+ var drawn = [], off = [];
1458
+ rects.forEach(function (r) {
1459
+ var rc = r.rect || r;
1460
+ // Image space: document coordinate minus the capture origin.
1461
+ var ix = rc ? rc.x - ox : 0, iy = rc ? rc.y - oy : 0;
1462
+ var inBounds = rc && typeof rc.x === "number" &&
1463
+ ix + (rc.width || 0) > 0 && iy + (rc.height || 0) > 0 &&
1464
+ ix < w && iy < h && !r.outsideScreenshot;
1465
+ (inBounds ? drawn : off).push(r);
1466
+ });
1467
+
1468
+ drawn.forEach(function (r) {
1469
+ var rc = r.rect || r;
1470
+ // The persona-weighted grade, which is what the findings list is ranked
1471
+ // and coloured by. Falling back to the raw grade keeps older payloads
1472
+ // rendering; using the raw grade FIRST is what made the map disagree
1473
+ // with the list directly beneath it.
1474
+ var sev = String(r.severityForPersona || r.severity || "").toLowerCase();
1475
+ var color = SEV_COLOR[sev] || "#64748b";
1476
+ var box = el("div", "bx");
1477
+ // Percentages, so the boxes track the image at whatever width the host
1478
+ // renders it -- pixel offsets would drift the moment the frame resized.
1479
+ box.style.left = (((rc.x - ox) / w) * 100).toFixed(3) + "%";
1480
+ box.style.top = (((rc.y - oy) / h) * 100).toFixed(3) + "%";
1481
+ box.style.width = (((rc.width || 0) / w) * 100).toFixed(3) + "%";
1482
+ box.style.height = (((rc.height || 0) / h) * 100).toFixed(3) + "%";
1483
+ box.style.borderColor = color;
1484
+ box.style.boxShadow = "0 0 0 1px " + color + "55";
1485
+ // The finding number, so a box on the map can be found in the list
1486
+ // below it. Ten boxes group into five findings, so the numbers repeat --
1487
+ // that repetition is the information: it says these four boxes are one
1488
+ // problem, not four.
1489
+ if (r.finding) {
1490
+ var tag = el("span", "bxn", String(r.finding));
1491
+ tag.style.background = color;
1492
+ // White was hardcoded, which fails on the lighter severity fills:
1493
+ // white on the amber #ca8a04 is 2.6:1 and on the orange #ea580c is
1494
+ // 3.4:1, both under the 4.5:1 an 11px bold label needs. Chosen by
1495
+ // measured luminance so a new severity colour cannot reintroduce it.
1496
+ tag.style.color = readableOn(color);
1497
+ box.appendChild(tag);
1498
+ }
1499
+ var label = (r.finding ? "Finding " + r.finding + " — " : "") +
1500
+ (r.severityForPersona || r.severity || "barrier") + ": " + (r.type || "issue") +
1501
+ ((r.wcag || r.wcagCriteria || []).length ? " (WCAG " + (r.wcag || r.wcagCriteria).join(", ") + ")" : "") +
1502
+ (r.element ? " — " + r.element : "");
1503
+ box.title = label;
1504
+ box.setAttribute("aria-label", label);
1505
+ box.setAttribute("role", "img");
1506
+ frame.appendChild(box);
1507
+ });
1508
+
1509
+ var sevs = [];
1510
+ var anyWeighted = false;
1511
+ drawn.forEach(function (r) {
1512
+ if (r.severityForPersona) anyWeighted = true;
1513
+ var s2 = String(r.severityForPersona || r.severity || "").toLowerCase();
1514
+ if (s2 && sevs.indexOf(s2) < 0) sevs.push(s2);
1515
+ });
1516
+ sevs.sort(function (a, c) {
1517
+ return (SEV_ORDER[a] === undefined ? 9 : SEV_ORDER[a]) -
1518
+ (SEV_ORDER[c] === undefined ? 9 : SEV_ORDER[c]);
1519
+ });
1520
+ if (sevs.length) {
1521
+ var lg = el("ul", "lg");
1522
+ sevs.forEach(function (s2) {
1523
+ var li = el("li");
1524
+ var sw = el("span", "sw"); sw.style.background = SEV_COLOR[s2] || "#64748b";
1525
+ li.appendChild(sw);
1526
+ // Counted by the SAME grade the swatch is labelled with. Counting the
1527
+ // raw grade under a weighted label printed "critical (0)" next to ten
1528
+ // red boxes.
1529
+ li.appendChild(document.createTextNode(s2 + " (" + drawn.filter(function (r) {
1530
+ return String(r.severityForPersona || r.severity || "").toLowerCase() === s2;
1531
+ }).length + ")"));
1532
+ lg.appendChild(li);
1533
+ });
1534
+ wrap.appendChild(lg);
1535
+ // Name the scale. The same colours on an unweighted map would mean
1536
+ // something else, and a legend that does not say which is which is why
1537
+ // the colours looked like they aligned with nothing.
1538
+ var counted = drawn.filter(function (r) { return r.finding; }).length;
1539
+ wrap.appendChild(el("p", "lgnote",
1540
+ (anyWeighted
1541
+ ? "Coloured by severity for this persona — the same grade the findings below are ranked by."
1542
+ : "Coloured by WCAG severity.") +
1543
+ (counted ? " Numbers match the findings list; boxes sharing a number are one finding." : "")));
1544
+ }
1545
+
1546
+ // Barriers that fall outside the capture are listed, not silently dropped.
1547
+ // They are real findings; the image just cannot show them.
1548
+ if (off.length) {
1549
+ var ol = el("ul", "offlist");
1550
+ ol.appendChild(el("li", null, off.length + " barrier" + (off.length === 1 ? "" : "s") +
1551
+ " outside the captured area, listed rather than drawn:"));
1552
+ off.slice(0, 8).forEach(function (r) {
1553
+ ol.appendChild(el("li", null, "• " + (r.severity || "barrier") + " " + (r.type || "") +
1554
+ (r.element ? " — " + r.element : "")));
1555
+ });
1556
+ wrap.appendChild(ol);
1557
+ }
1558
+
1559
+ var file = shot[b.fileKey || "screenshotFile"];
1560
+ if (b.fetchTool && file) {
1561
+ try {
1562
+ var res = await app.callServerTool({ name: b.fetchTool, arguments: { file: file } });
1563
+ var blk = (res && res.content || []).filter(function (c) { return c.type === "image"; })[0];
1564
+ if (blk && blk.data) img.src = "data:" + (blk.mimeType || "image/png") + ";base64," + blk.data;
1565
+ } catch (e) { /* boxes still convey positions even without the picture */ }
1566
+ }
1567
+ if (!img.src && shot.screenshot) {
1568
+ img.src = String(shot.screenshot).startsWith("data:")
1569
+ ? shot.screenshot : "data:image/png;base64," + shot.screenshot;
1570
+ }
1571
+ if (!img.src) {
1572
+ // Never leave boxes floating over nothing: say the image is missing.
1573
+ img.remove();
1574
+ frame.appendChild(el("p", "cap", "Screenshot unavailable — barrier positions listed below rather than drawn."));
1575
+ }
1576
+ return wrap;
1577
+ }
1578
+
1579
+ function section(title, count, node, open) {
1580
+ var d = el("details", "sec");
1581
+ if (open) d.open = true;
1582
+ var sm = el("summary");
1583
+ sm.appendChild(el("span", "chev"));
1584
+ sm.appendChild(el("span", "stitle", title));
1585
+ if (count !== null && count !== undefined) sm.appendChild(el("span", "scount", String(count)));
1586
+ d.appendChild(sm);
1587
+ var body = el("div", "sbody"); body.appendChild(node); d.appendChild(body);
1588
+ return d;
1589
+ }
1590
+
1591
+ var tabSeq = 0;
1592
+ // Tabs, built to the WAI-ARIA tabs pattern rather than as styled buttons:
1593
+ // roles, aria-selected, aria-controls, a roving tabindex so Tab enters the
1594
+ // strip once, and Arrow/Home/End to move between tabs. This widget reports
1595
+ // accessibility barriers; it does not get to introduce one.
1596
+ async function tabsBlock(data, b) {
1597
+ var rows = at(data, b.field);
1598
+ if (!isObjArray(rows)) return null;
1599
+ var order = [], groups = {};
1600
+ rows.forEach(function (r) {
1601
+ var key = String(r[b.groupBy] === undefined ? "unknown" : r[b.groupBy]);
1602
+ if (!groups[key]) { groups[key] = []; order.push(key); }
1603
+ groups[key].push(r);
1604
+ });
1605
+ if (order.length < 2) {
1606
+ // One group is not a tab strip; render the inner block on its own.
1607
+ var solo = await buildBlock(data, Object.assign({}, b.render, { field: b.field }));
1608
+ return solo ? solo.node : null;
1609
+ }
1610
+
1611
+ var wrap = el("div");
1612
+ var strip = el("div", "tabs");
1613
+ strip.setAttribute("role", "tablist");
1614
+ var uid = "tb" + (++tabSeq);
1615
+ var panels = [];
1616
+
1617
+ // A full URL is a poor tab label; the path is what distinguishes them.
1618
+ var shortLabel = function (v) {
1619
+ try {
1620
+ var u = new URL(v);
1621
+ return u.pathname === "/" ? u.hostname : u.pathname;
1622
+ } catch (_) { return titleize(v); }
1623
+ };
1624
+
1625
+ for (var i = 0; i < order.length; i++) {
1626
+ var key = order[i];
1627
+ var btn = el("button", "tab");
1628
+ btn.type = "button";
1629
+ btn.id = uid + "-t" + i;
1630
+ btn.setAttribute("role", "tab");
1631
+ btn.setAttribute("aria-controls", uid + "-p" + i);
1632
+ btn.setAttribute("aria-selected", i === 0 ? "true" : "false");
1633
+ btn.tabIndex = i === 0 ? 0 : -1;
1634
+ btn.appendChild(document.createTextNode(shortLabel(key)));
1635
+ btn.appendChild(el("span", "tabn", String(groups[key].length)));
1636
+ // The full value stays reachable for anything the label abbreviates.
1637
+ btn.title = key;
1638
+ strip.appendChild(btn);
1639
+
1640
+ var panel = el("div");
1641
+ panel.id = uid + "-p" + i;
1642
+ panel.setAttribute("role", "tabpanel");
1643
+ panel.setAttribute("aria-labelledby", btn.id);
1644
+ if (i !== 0) panel.hidden = true;
1645
+ // The inner block reads its rows from a scoped object, so any block type
1646
+ // works here without knowing it is inside a tab.
1647
+ var scoped = {};
1648
+ scoped[b.groupBy + "__rows"] = groups[key];
1649
+ var inner = await buildBlock(scoped, Object.assign({}, b.render, { field: b.groupBy + "__rows" }));
1650
+ if (inner) panel.appendChild(inner.node);
1651
+ panels.push(panel);
1652
+ }
1653
+
1654
+ var tabEls = [].slice.call(strip.children);
1655
+ var select = function (idx) {
1656
+ tabEls.forEach(function (t, j) {
1657
+ var on = j === idx;
1658
+ t.setAttribute("aria-selected", on ? "true" : "false");
1659
+ t.tabIndex = on ? 0 : -1;
1660
+ panels[j].hidden = !on;
1661
+ });
1662
+ tabEls[idx].focus();
1663
+ };
1664
+ tabEls.forEach(function (t, j) {
1665
+ t.addEventListener("click", function () { select(j); });
1666
+ t.addEventListener("keydown", function (e) {
1667
+ var k = e.key, next = null;
1668
+ if (k === "ArrowRight" || k === "ArrowDown") next = (j + 1) % tabEls.length;
1669
+ else if (k === "ArrowLeft" || k === "ArrowUp") next = (j - 1 + tabEls.length) % tabEls.length;
1670
+ else if (k === "Home") next = 0;
1671
+ else if (k === "End") next = tabEls.length - 1;
1672
+ if (next !== null) { e.preventDefault(); select(next); }
1673
+ });
1674
+ });
1675
+
1676
+ wrap.appendChild(strip);
1677
+ panels.forEach(function (p) { wrap.appendChild(p); });
1678
+ return wrap;
1679
+ }
1680
+
1681
+ async function buildBlock(data, b) {
1682
+ if (b.type === "kv") {
1683
+ var pairs = [];
1684
+ if (b.field) {
1685
+ used[b.field.split(".")[0]] = true;
1686
+ var o = at(data, b.field);
1687
+ if (o && typeof o === "object") flattenPairs(o, "", pairs);
1688
+ }
1689
+ (b.fields || []).forEach(function (f) {
1690
+ used[f.split(".")[0]] = true;
1691
+ var v = at(data, f);
1692
+ if (v !== undefined) pairs.push([titleize(f.split(".").pop()), v]);
1693
+ });
1694
+ return pairs.length ? { node: kvTable(pairs), count: pairs.length } : null;
1695
+ }
1696
+ if (b.type === "table") {
1697
+ used[b.field.split(".")[0]] = true;
1698
+ var rows = at(data, b.field);
1699
+ if (!isObjArray(rows)) return null;
1700
+ // One row is a record, not a table. Laying a single result out
1701
+ // horizontally gives every field a column, so sixteen fields became
1702
+ // sixteen columns of a one-row grid -- unreadable at any width, and the
1703
+ // nested ones printed JSON. Flattened into label/value pairs it reads
1704
+ // like the rest of the widget.
1705
+ if (rows.length === 1) {
1706
+ var recPairs = [];
1707
+ flattenPairs(rows[0], "", recPairs);
1708
+ return { node: kvTable(recPairs), count: 1 };
1709
+ }
1710
+ return { node: gridTable(rows, b.columns), count: rows.length };
1711
+ }
1712
+ if (b.type === "findings") {
1713
+ used[b.field.split(".")[0]] = true;
1714
+ var f = at(data, b.field);
1715
+ return isObjArray(f) ? { node: findingsList(f, b), count: f.length } : null;
1716
+ }
1717
+ if (b.type === "score") {
1718
+ [b.gradeField, b.scoreField, b.categoriesField].forEach(function (p) { if (p) used[p.split(".")[0]] = true; });
1719
+ var node = scoreBlock(data, b);
1720
+ return node.childNodes.length ? { node: node, count: null } : null;
1721
+ }
1722
+ if (b.type === "image") {
1723
+ [b.urlField, b.dataField, b.fetchArgsField, b.caption].forEach(function (p) { if (p) used[p.split(".")[0]] = true; });
1724
+ return { node: await imageBlock(data, b), count: null };
1725
+ }
1726
+ if (b.type === "traits") {
1727
+ used[b.field.split(".")[0]] = true;
1728
+ var rows = normaliseTraits(at(data, b.field), b);
1729
+ return rows.length ? { node: traitsBlock(rows, b), count: rows.length } : null;
1730
+ }
1731
+ if (b.type === "manager") {
1732
+ used[b.field.split(".")[0]] = true;
1733
+ var mnode = managerBlock(data, b);
1734
+ return mnode.childNodes.length ? { node: mnode, count: null } : null;
1735
+ }
1736
+ if (b.type === "editor") {
1737
+ used[b.field.split(".")[0]] = true;
1738
+ if (b.personaField) used[b.personaField.split(".")[0]] = true;
1739
+ var erows = normaliseTraits(at(data, b.field), b);
1740
+ return erows.length ? { node: editorBlock(erows, b, data), count: erows.length } : null;
1741
+ }
1742
+ if (b.type === "chain") {
1743
+ [b.field, b.bottleneckField, b.additiveField, b.totalField, b.overlaysField].forEach(function (f) {
1744
+ if (f) used[f.split(".")[0]] = true;
1745
+ });
1746
+ var node = await chainBlock(data, b);
1747
+ return node ? { node: node, count: null } : null;
1748
+ }
1749
+ if (b.type === "levels") {
1750
+ [b.field, b.valueField, b.labelField, b.behaviorsField].forEach(function (f) {
1751
+ if (f) used[f.split(".")[0]] = true;
1752
+ });
1753
+ var node = levelsBlock(data, b);
1754
+ return node ? { node: node, count: null } : null;
1755
+ }
1756
+ if (b.type === "tabs") {
1757
+ used[b.field.split(".")[0]] = true;
1758
+ var node = await tabsBlock(data, b);
1759
+ return node ? { node: node, count: null } : null;
1760
+ }
1761
+ if (b.type === "overlay") {
1762
+ used[b.field.split(".")[0]] = true;
1763
+ var node = await overlayBlock(data, b);
1764
+ return node ? { node: node, count: null } : null;
1765
+ }
1766
+ if (b.type === "note") {
1767
+ used[b.field.split(".")[0]] = true;
1768
+ var t = at(data, b.field);
1769
+ return t ? { node: el("p", null, fmt(t)), count: null } : null;
1770
+ }
1771
+ if (b.type === "rest") {
1772
+ var rest = [];
1773
+ Object.keys(data).forEach(function (k) {
1774
+ if (used[k]) return;
1775
+ var v = data[k];
1776
+ if (isObjArray(v)) return;
1777
+ if (v && typeof v === "object" && !Array.isArray(v)) {
1778
+ flattenPairs(v, titleize(k), rest);
1779
+ } else rest.push([titleize(k), v]);
1780
+ });
1781
+ return rest.length ? { node: kvTable(rest), count: rest.length } : null;
1782
+ }
1783
+ return null;
1784
+ }
1785
+
1786
+ async function buildSections(data, blocks) {
1787
+ var out = [];
1788
+ for (var i = 0; i < blocks.length; i++) {
1789
+ var b = blocks[i];
1790
+ if (b.type === "drawer") continue;
1791
+ var built = await buildBlock(data, b);
1792
+ if (built) out.push(section(b.title || titleize(b.type), built.count, built.node, false));
1793
+ }
1794
+ return out;
1795
+ }
1796
+
1797
+ function healthOf(data, spec) {
1798
+ if (!spec || !spec.checks || !spec.checks.length) return null;
1799
+ var issues = [], measured = false;
1800
+ spec.checks.forEach(function (c) {
1801
+ var arr = at(data, c.field);
1802
+ if (!isObjArray(arr)) return;
1803
+ measured = true;
1804
+ arr.forEach(function (row) {
1805
+ if (!row[c.flag]) issues.push(c.failLabel + ": " + (row[c.nameKey || "name"] || "?"));
1806
+ });
1807
+ });
1808
+ // Three states: "measured nothing" is a different claim from "nothing is
1809
+ // wrong", and collapsing them reports healthy on no evidence.
1810
+ if (!measured) return { state: "unknown", label: "UNKNOWN", issues: [] };
1811
+ return issues.length ? { state: "bad", label: "DEGRADED", issues: issues }
1812
+ : { state: "ok", label: "HEALTHY", issues: [] };
1813
+ }
1814
+
1815
+ // Renders are generation-stamped because render() is async: image blocks
1816
+ // await callServerTool, so two overlapping renders interleave -- the second
1817
+ // clears the root, then the first resumes and appends its blocks into it,
1818
+ // and the panel shows everything twice. Any render whose generation is stale
1819
+ // stops appending.
1820
+ var renderGen = 0;
1821
+
1822
+ async function render(data) {
1823
+ if (!data) return;
1824
+ var gen = ++renderGen;
1825
+ root.replaceChildren();
1826
+ used = {};
1827
+
1828
+ var hero = SPEC.hero || {};
1829
+ var variant = hero.variant || "gradient";
1830
+ var band = el("div", "hero " + variant);
1831
+ var hrow = el("div", "hrow");
1832
+ if (LOGO) {
1833
+ var badge = el("span", "badge");
1834
+ var im = document.createElement("img"); im.src = LOGO; im.alt = "";
1835
+ badge.appendChild(im); hrow.appendChild(badge);
1836
+ }
1837
+ // Payload title when the view is about a named thing; the spec title is the
1838
+ // fallback and the label for views that are about the tool itself.
1839
+ var titleResolved = SPEC.titleField && at(data, SPEC.titleField);
1840
+ var titleText = titleResolved
1841
+ ? (SPEC.titlePrefix || "") + fmt(titleResolved)
1842
+ : SPEC.title;
1843
+ hrow.appendChild(el("span", "htitle", String(titleText)));
1844
+
1845
+ var h = healthOf(data, hero.health);
1846
+ if (h) {
1847
+ var pill = el("span", "health " + h.state);
1848
+ pill.appendChild(el("i", "dot"));
1849
+ pill.appendChild(document.createTextNode(h.label));
1850
+ hrow.appendChild(pill);
1851
+ }
1852
+ band.appendChild(hrow);
1853
+
1854
+ if (h && h.issues.length) {
1855
+ band.appendChild(el("p", "hnote", h.issues.length === 1 ? h.issues[0]
1856
+ : h.issues.length + " problems: " + h.issues.join("; ")));
1857
+ } else if (hero.subtitle) {
1858
+ var sub = typeof hero.subtitle === "string" ? hero.subtitle : at(data, hero.subtitle.field);
1859
+ if (sub) band.appendChild(el("p", "hnote", fmt(sub)));
1860
+ }
1861
+
1862
+ var actionList = (hero.actions || []).slice();
1863
+ if (SPEC.toolPage) actionList.push({ label: "View tool docs", url: SPEC.toolPage });
1864
+ if (actionList.length) {
1865
+ var acts = el("div", "acts");
1866
+ actionList.forEach(function (a) {
1867
+ var url = a.urlField ? at(data, a.urlField) : a.url;
1868
+ if (!url) return;
1869
+ var btn = el("button", "act", a.label);
1870
+ btn.type = "button";
1871
+ btn.addEventListener("click", function () { app.openLink({ url: String(url) }); });
1872
+ acts.appendChild(btn);
1873
+ });
1874
+ if (acts.childNodes.length) band.appendChild(acts);
1875
+ }
1876
+
1877
+ if (hero.facts && hero.facts.length) {
1878
+ var strip = el("div", "facts");
1879
+ hero.facts.forEach(function (f) {
1880
+ var v = at(data, f.field);
1881
+ var value = null, attn = false;
1882
+ if (f.countFlag && isObjArray(v)) {
1883
+ var ok = v.filter(function (x) { return x[f.countFlag]; }).length;
1884
+ value = ok + "/" + v.length;
1885
+ attn = !!f.attnWhenShort && ok < v.length;
1886
+ } else if (Array.isArray(v)) {
1887
+ value = String(v.length);
1888
+ } else if (v && typeof v === "object") {
1889
+ // A bag of named numbers is a countable collection too. Without this
1890
+ // a fact pointing at a traits object silently produced nothing, which
1891
+ // reads as a missing field rather than an unhandled shape.
1892
+ value = String(Object.keys(v).length);
1893
+ } else if (v !== undefined) {
1894
+ value = String(v);
1895
+ }
1896
+ if (value === null) return;
1897
+ used[f.field.split(".")[0]] = true;
1898
+ var chip = el("span", "fact" + (attn ? " attn" : ""));
1899
+ chip.appendChild(el("b", null, value));
1900
+ chip.appendChild(el("span", null, f.label));
1901
+ strip.appendChild(chip);
1902
+ });
1903
+ if (strip.childNodes.length) band.appendChild(strip);
1904
+ }
1905
+ root.appendChild(band);
1906
+
1907
+ // Top-level blocks render inline; a drawer collects its children so the
1908
+ // default state stays a card rather than a page.
1909
+ for (var i = 0; i < SPEC.blocks.length; i++) {
1910
+ if (gen !== renderGen) return;
1911
+ var b = SPEC.blocks[i];
1912
+ if (b.type === "drawer") {
1913
+ var kids = await buildSections(data, b.blocks || []);
1914
+ if (gen !== renderGen) return;
1915
+ if (!kids.length) continue;
1916
+ var d = el("details", "drawer");
1917
+ if (b.open) d.open = true;
1918
+ var sm = el("summary");
1919
+ sm.appendChild(el("span", "chev"));
1920
+ sm.appendChild(el("span", null, b.title || "Details"));
1921
+ sm.appendChild(el("span", "dcount", kids.length + (kids.length === 1 ? " section" : " sections")));
1922
+ d.appendChild(sm);
1923
+ var inner = el("div", "inner");
1924
+ kids.forEach(function (k) { inner.appendChild(k); });
1925
+ d.appendChild(inner);
1926
+ root.appendChild(d);
1927
+ } else {
1928
+ var built = await buildBlock(data, b);
1929
+ if (gen !== renderGen) return;
1930
+ if (!built) continue;
1931
+ var wrap = el("div", "body");
1932
+ if (b.title) wrap.appendChild(el("h2", "btitle", b.title));
1933
+ wrap.appendChild(built.node);
1934
+ root.appendChild(wrap);
1935
+ }
1936
+ }
1937
+
1938
+ if (SPEC.footer) {
1939
+ var fv = at(data, SPEC.footer.field);
1940
+ if (fv) {
1941
+ var p = el("p", "foot");
1942
+ p.appendChild(document.createTextNode(SPEC.footer.label + " "));
1943
+ p.appendChild(el("span", "path", fmt(fv)));
1944
+ root.appendChild(p);
1945
+ }
1946
+ }
1947
+ }
1948
+
1949
+ function applyHostContext(ctx) {
1950
+ document.documentElement.classList.toggle("dark", ctx && ctx.theme === "dark");
1951
+ if (ctx && ctx.styles && ctx.styles.variables && applyHostStyleVariables) {
1952
+ applyHostStyleVariables(ctx.styles.variables);
1953
+ }
1954
+ }
1955
+
1956
+ // Handlers before connect(), or the first result is delivered into nothing.
1957
+ var lastPayload = null;
1958
+
1959
+ app.ontoolresult = function (res) {
1960
+ try {
1961
+ var data = res && res.structuredContent;
1962
+ if (!data && res && res.content && res.content[0] && res.content[0].text) {
1963
+ data = JSON.parse(res.content[0].text);
1964
+ }
1965
+ // Hosts may deliver the same result more than once (re-mount, replay,
1966
+ // reconnect). Re-rendering identical data is pure churn, and while the
1967
+ // generation guard keeps it correct, skipping it avoids refetching every
1968
+ // image over callServerTool for a panel that already shows them.
1969
+ var sig = null;
1970
+ try { sig = JSON.stringify(data); } catch (_) { sig = null; }
1971
+ if (sig !== null && sig === lastPayload) return;
1972
+ lastPayload = sig;
1973
+ render(data);
1974
+ } catch (e) {
1975
+ msg.textContent = "Could not read the payload: " + (e && e.message ? e.message : e);
1976
+ }
1977
+ };
1978
+ app.onhostcontextchanged = applyHostContext;
1979
+
1980
+ try {
1981
+ await app.connect();
1982
+ applyHostContext(app.getHostContext());
1983
+ } catch (e) {
1984
+ msg.textContent = "Could not connect to the host: " + (e && e.message ? e.message : e);
1985
+ }
1986
+ })();
1987
+ `;
1988
+ /**
1989
+ * Render a spec to a complete MCP Apps resource document.
1990
+ *
1991
+ * Top-level await is deliberately absent from the emitted script: older iframe
1992
+ * contexts throw on it, and the throw surfaces only as a blank widget.
1993
+ */
1994
+ /**
1995
+ * Definitions baked into every widget, keyed by trait name.
1996
+ *
1997
+ * Sourced from TRAIT_DEFINITIONS, the authoritative reference matrix, rather
1998
+ * than written fresh -- a tooltip that disagrees with the docs is worse than
1999
+ * no tooltip. Keys are short because this ships inside every view; ~4KB for
2000
+ * all 26 traits, against a 300KB runtime bundle.
2001
+ *
2002
+ * Schwartz values have no runtime description map (they are interfaces with
2003
+ * JSDoc), so value meters currently render without tooltips rather than with
2004
+ * invented ones.
2005
+ */
2006
+ function buildGlossary() {
2007
+ const out = {};
2008
+ for (const [key, def] of Object.entries(TRAIT_DEFINITIONS)) {
2009
+ out[key] = {
2010
+ d: def.description,
2011
+ ...(def.lowEnd ? { l: def.lowEnd } : {}),
2012
+ ...(def.highEnd ? { h: def.highEnd } : {}),
2013
+ };
2014
+ }
2015
+ return out;
2016
+ }
2017
+ let glossaryCache;
2018
+ export function buildWidget(spec) {
2019
+ if (glossaryCache === undefined)
2020
+ glossaryCache = JSON.stringify(buildGlossary());
2021
+ const script = RUNTIME
2022
+ .replace("__SPEC__", () => JSON.stringify(spec))
2023
+ .replace("__GLOSSARY__", () => glossaryCache)
2024
+ .replace("__LOGO__", () => getLogoDataUri());
2025
+ return `<!doctype html><meta charset="utf-8">
2026
+ <meta name="color-scheme" content="light dark">
2027
+ <style>${CSS}</style>
2028
+ <div id="root"><p class="msg" id="msg">Waiting for the host&hellip;</p></div>
2029
+ <script type="module">
2030
+ ${getExtAppsBundle()}
2031
+ ${script}
2032
+ </script>`;
2033
+ }
2034
+ //# sourceMappingURL=widget-kit.js.map