@uniqbit/mate-core 0.18.4 → 1.0.0-canary.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 (231) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/opencode-plugin/src/add-dir.ts +76 -10
  4. package/opencode-plugin/src/companion.ts +1 -8
  5. package/opencode-plugin/src/server.ts +2 -15
  6. package/package.json +10 -2
  7. package/src/brand/art.generated.ts +69 -0
  8. package/src/brand/index.ts +12 -0
  9. package/src/brand/palette.ts +19 -0
  10. package/src/brand/svg.generated.ts +8 -0
  11. package/src/brand/terminal.ts +83 -0
  12. package/src/brand/types.ts +33 -0
  13. package/src/cli/commands/artifact/artifact.ts +8 -0
  14. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  15. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  16. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  17. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  18. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  19. package/src/cli/commands/artifact/finish/command.ts +33 -3
  20. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  21. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  22. package/src/cli/commands/artifact/finish/openspec.ts +200 -129
  23. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  24. package/src/cli/commands/artifact/pending/command.ts +31 -3
  25. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  26. package/src/cli/commands/cap/graphify.ts +52 -3
  27. package/src/cli/commands/cap/index-cmd.ts +8 -1
  28. package/src/cli/commands/cap/openspec.ts +13 -25
  29. package/src/cli/commands/companion/open.ts +56 -39
  30. package/src/cli/commands/companion/update.ts +75 -42
  31. package/src/cli/commands/doctor.ts +257 -78
  32. package/src/cli/commands/install.ts +0 -8
  33. package/src/cli/commands/plugin/install.ts +9 -4
  34. package/src/cli/commands/plugin/plugin.ts +3 -2
  35. package/src/cli/commands/report/adapter.ts +11 -2
  36. package/src/cli/commands/report/contract.ts +21 -0
  37. package/src/cli/commands/report/highlight.ts +34 -4
  38. package/src/cli/commands/report/markdown.ts +110 -0
  39. package/src/cli/commands/report/palette.ts +118 -0
  40. package/src/cli/commands/report/renderer.ts +456 -153
  41. package/src/cli/commands/report/types.ts +16 -0
  42. package/src/cli/commands/setup.ts +7 -15
  43. package/src/cli/commands/studio/client-asset.ts +16 -0
  44. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  45. package/src/cli/commands/studio/routes.ts +2 -0
  46. package/src/cli/commands/studio/selection.ts +19 -1
  47. package/src/cli/commands/studio/server.ts +42 -1
  48. package/src/cli/commands/studio/terminal-launch.ts +7 -16
  49. package/src/cli/commands/studio/views/client.ts +6 -616
  50. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  51. package/src/cli/commands/studio/views/document.tsx +137 -69
  52. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  53. package/src/cli/commands/studio/views/palette.ts +134 -0
  54. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  55. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  56. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  57. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  58. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  59. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  60. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  61. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  62. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  63. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  64. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  65. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  66. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  67. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  68. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  69. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  70. package/src/cli/commands/studio/views/styles.ts +217 -206
  71. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  72. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  73. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  74. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  75. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  76. package/src/cli/commands/wrap.ts +9 -42
  77. package/src/cli/main.ts +4 -1
  78. package/src/cli/parse-flags.ts +25 -7
  79. package/src/cli/setup-selector.tsx +38 -162
  80. package/src/cli/usage.ts +5 -2
  81. package/src/hooks/companion-instructions.ts +190 -0
  82. package/src/hooks/hook-io.ts +63 -0
  83. package/src/hooks/session-banner.ts +21 -29
  84. package/src/hooks/session-guidance.ts +10 -36
  85. package/src/hooks/session-prepare.ts +115 -28
  86. package/src/hooks/validate-artifact-path.ts +82 -24
  87. package/src/index.ts +4 -0
  88. package/src/lib/change-layout.ts +154 -0
  89. package/src/lib/components/wizard-header.tsx +3 -1
  90. package/src/lib/context-mode-package.ts +24 -11
  91. package/src/lib/delivery/archive-change.ts +154 -0
  92. package/src/lib/delivery/branches.ts +108 -0
  93. package/src/lib/delivery/environment.ts +29 -0
  94. package/src/lib/delivery/forge.ts +279 -0
  95. package/src/lib/delivery/overlap.ts +202 -0
  96. package/src/lib/delivery/record-delivery.ts +196 -0
  97. package/src/lib/delivery/record.ts +343 -0
  98. package/src/lib/delivery/refresh.ts +174 -0
  99. package/src/lib/delivery/repository-access.ts +97 -0
  100. package/src/lib/delivery/stages.ts +111 -0
  101. package/src/lib/delivery/token.ts +58 -0
  102. package/src/lib/frontmatter.ts +18 -0
  103. package/src/lib/opencode-plugin-package.ts +5 -5
  104. package/src/lib/openspec-cli.ts +116 -0
  105. package/src/lib/openspec-scopes.ts +86 -0
  106. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  107. package/src/lib/orchestrator/adapters/claude.ts +60 -162
  108. package/src/lib/orchestrator/adapters/opencode.ts +9 -24
  109. package/src/lib/orchestrator/config-store.ts +2 -70
  110. package/src/lib/orchestrator/editor.ts +9 -9
  111. package/src/lib/orchestrator/openspec-store.ts +14 -54
  112. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  113. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  114. package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
  115. package/src/lib/orchestrator/projection-entries.ts +45 -15
  116. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  117. package/src/lib/orchestrator/projection-types.ts +3 -2
  118. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  119. package/src/lib/orchestrator/session-prepare.ts +49 -87
  120. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  121. package/src/lib/orchestrator/types.ts +2 -2
  122. package/src/lib/orchestrator/wrap-pass.ts +22 -88
  123. package/src/lib/package-paths.ts +8 -0
  124. package/src/opencode/companion-hooks.ts +64 -5
  125. package/src/opencode/companion-policy.ts +37 -18
  126. package/src/opencode/projected-guidance.ts +4 -1
  127. package/src/opencode/session-prepare.ts +2 -1
  128. package/src/opencode/tui.tsx +4 -2
  129. package/src/runtime/artifact-refusal.ts +63 -0
  130. package/src/runtime/claude-delivery-plan.ts +89 -7
  131. package/src/runtime/commit-attribution-guard.ts +181 -0
  132. package/src/runtime/companion-guidance.ts +15 -3
  133. package/src/runtime/companion-instructions.ts +340 -0
  134. package/src/runtime/error-message.ts +4 -0
  135. package/src/runtime/guidance.ts +0 -17
  136. package/src/runtime/index.ts +5 -0
  137. package/src/runtime/managed-block.ts +117 -0
  138. package/src/runtime/markdown-scan.ts +266 -0
  139. package/src/runtime/policy.ts +16 -15
  140. package/src/runtime/projected-flag.ts +7 -0
  141. package/src/runtime/projected-guidance.ts +1 -0
  142. package/src/runtime/runtime-document-manifest.ts +62 -28
  143. package/src/runtime/secret-guard.ts +148 -0
  144. package/src/runtime/secret-patterns.ts +164 -0
  145. package/src/runtime/secret-scan.ts +90 -0
  146. package/src/runtime/session-kind.ts +13 -0
  147. package/src/runtime/shell-heredoc.ts +81 -0
  148. package/src/runtime/wrap-state.ts +36 -6
  149. package/src/runtime/write-atomic.ts +64 -0
  150. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  151. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  152. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  153. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  154. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  155. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  156. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  157. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  158. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  159. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  160. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  161. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  162. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  163. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  164. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  165. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  166. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  167. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  168. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  169. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  170. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  171. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  172. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  173. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  174. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  175. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  176. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  177. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  178. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  179. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  180. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  181. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  182. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  183. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  184. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  185. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  186. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  187. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  188. package/src/templates/mate-skills/openspec-references.ts +140 -0
  189. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  190. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  191. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  192. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  193. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  194. package/src/tools/setup/capabilities/graphify.ts +12 -0
  195. package/src/tools/setup/capabilities/openspec.ts +273 -270
  196. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  197. package/src/tools/setup/context-services.ts +9 -17
  198. package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
  199. package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
  200. package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
  201. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  202. package/src/tools/setup/engine.ts +93 -148
  203. package/src/tools/setup/managed-output.ts +51 -0
  204. package/src/tools/setup/mate.ts +86 -11
  205. package/src/tools/setup/plugin.ts +37 -17
  206. package/src/tools/setup/plugins/gitignore.ts +1 -7
  207. package/src/tools/setup/plugins/guidance.ts +7 -4
  208. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  209. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  210. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  211. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  212. package/src/tools/setup/providers/claude.ts +285 -34
  213. package/src/tools/setup/providers/opencode.ts +180 -11
  214. package/src/tools/setup/registry.ts +2 -24
  215. package/src/tools/setup/surface-target.ts +4 -13
  216. package/src/tools/setup/utils.ts +14 -4
  217. package/src/tools/setup.ts +131 -18
  218. package/wrappers/bin/graphify +52 -4
  219. package/wrappers/bin/openspec +33 -9
  220. package/wrappers/lib/graphify-gate.mjs +8 -0
  221. package/opencode-plugin/src/audience-layer.ts +0 -91
  222. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  223. package/src/runtime/audience-layer.ts +0 -84
  224. package/src/runtime/opencode-audience-layer.ts +0 -176
  225. package/src/tools/setup/audience-isolation.fixture.ts +0 -325
  226. package/src/tools/setup/audience-layer.ts +0 -207
  227. package/src/tools/setup/audience-registrations.ts +0 -52
  228. package/src/tools/setup/contribution-ledger.ts +0 -207
  229. package/src/tools/setup/isolation-diagnostics.ts +0 -139
  230. package/src/tools/setup/runtime-documents.ts +0 -291
  231. package/src/tools/setup/upgrade-sweep.ts +0 -153
@@ -0,0 +1,110 @@
1
+ import { highlightInline } from "./highlight";
2
+
3
+ /**
4
+ * Safe Markdown subset for report text and captions: paragraphs, headings shifted
5
+ * below the section heading, one-level lists, code spans, strong, emphasis,
6
+ * `http(s)` links, and highlighted fenced code. Every literal is escaped before
7
+ * markup is added, so raw HTML and any other syntax degrade to visible text.
8
+ */
9
+
10
+ export function escapeHTML(value: string): string {
11
+ return value.replace(/[&<>"']/g, (character) => {
12
+ switch (character) {
13
+ case "&":
14
+ return "&amp;";
15
+ case "<":
16
+ return "&lt;";
17
+ case ">":
18
+ return "&gt;";
19
+ case '"':
20
+ return "&quot;";
21
+ default:
22
+ return "&#39;";
23
+ }
24
+ });
25
+ }
26
+
27
+ const FENCE = /^\s{0,3}```\s*([\w+#.-]*)\s*$/;
28
+ const HEADING = /^\s{0,3}(#{1,6})\s+(.*)$/;
29
+ const LIST_ITEM = /^\s{0,3}([-*]|\d+[.)])\s+(.*)$/;
30
+ const INLINE_TOKEN = /`([^`\n]+)`|\[([^\]\n]+)\]\(([^()\s]+)\)/g;
31
+ const SAFE_URL = /^https?:\/\/\S+$/i;
32
+
33
+ /** Emphasis runs over already-escaped text, which carries no `*` or `_` of its own. */
34
+ function emphasize(escaped: string): string {
35
+ return escaped
36
+ .replace(/\*\*(?=\S)([^*]*?\S)\*\*/g, "<strong>$1</strong>")
37
+ .replace(/\*(?=\S)([^*]*?\S)\*/g, "<em>$1</em>")
38
+ .replace(/(^|\W)_(?=\S)([^_]*?\S)_(?!\w)/g, "$1<em>$2</em>");
39
+ }
40
+
41
+ function renderInline(source: string): string {
42
+ let html = "";
43
+ let last = 0;
44
+ for (const match of source.matchAll(INLINE_TOKEN)) {
45
+ html += emphasize(escapeHTML(source.slice(last, match.index)));
46
+ const [whole, code, label, url] = match;
47
+ if (code !== undefined) {
48
+ html += `<code>${escapeHTML(code)}</code>`;
49
+ } else if (SAFE_URL.test(url)) {
50
+ html += `<a href="${escapeHTML(url)}" rel="noopener noreferrer">${emphasize(escapeHTML(label))}</a>`;
51
+ } else {
52
+ html += emphasize(escapeHTML(whole));
53
+ }
54
+ last = match.index + whole.length;
55
+ }
56
+ return html + emphasize(escapeHTML(source.slice(last)));
57
+ }
58
+
59
+ function renderFence(code: string[], language: string): string {
60
+ return `<pre class="report-code">${highlightInline(code.join("\n"), language || undefined)}</pre>`;
61
+ }
62
+
63
+ export function renderMarkdownSubset(source: string): string {
64
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
65
+ const blocks: string[] = [];
66
+ let paragraph: string[] = [];
67
+ let list: { tag: "ul" | "ol"; items: string[] } | undefined;
68
+
69
+ const flush = (): void => {
70
+ if (paragraph.length) blocks.push(`<p>${paragraph.map(renderInline).join("\n")}</p>`);
71
+ if (list) blocks.push(`<${list.tag}>${list.items.join("")}</${list.tag}>`);
72
+ paragraph = [];
73
+ list = undefined;
74
+ };
75
+
76
+ for (let index = 0; index < lines.length; index++) {
77
+ const line = lines[index];
78
+ const fence = FENCE.exec(line);
79
+ if (fence) {
80
+ flush();
81
+ const code: string[] = [];
82
+ while (++index < lines.length && !FENCE.test(lines[index])) code.push(lines[index]);
83
+ blocks.push(renderFence(code, fence[1]));
84
+ continue;
85
+ }
86
+ if (line.trim() === "") {
87
+ flush();
88
+ continue;
89
+ }
90
+ const heading = HEADING.exec(line);
91
+ if (heading) {
92
+ flush();
93
+ const level = Math.min(heading[1].length + 2, 6);
94
+ blocks.push(`<h${level}>${renderInline(heading[2].trim())}</h${level}>`);
95
+ continue;
96
+ }
97
+ const item = LIST_ITEM.exec(line);
98
+ if (item) {
99
+ const tag = /\d/.test(item[1]) ? "ol" : "ul";
100
+ if (paragraph.length || (list && list.tag !== tag)) flush();
101
+ list ??= { tag, items: [] };
102
+ list.items.push(`<li>${renderInline(item[2])}</li>`);
103
+ continue;
104
+ }
105
+ if (list) flush();
106
+ paragraph.push(line);
107
+ }
108
+ flush();
109
+ return blocks.join("");
110
+ }
@@ -0,0 +1,118 @@
1
+ import { BRAND_COLORS } from "../../../brand";
2
+
3
+ const { ink, mateGreen, cream, lightGreen, night } = BRAND_COLORS;
4
+
5
+ /**
6
+ * Report colours from the Mate brand kit as CSS custom properties. Light uses Ink on
7
+ * white (Mate green on Cream falls below 4.5:1), dark uses Cream on Night with Light
8
+ * green, and print forces dark text on white whatever the screen scheme.
9
+ */
10
+
11
+ export interface ReportPalette {
12
+ ink: string;
13
+ bg: string;
14
+ muted: string;
15
+ rule: string;
16
+ accent: string;
17
+ surface: string;
18
+ ok: string;
19
+ warn: string;
20
+ error: string;
21
+ info: string;
22
+ neutral: string;
23
+ addBg: string;
24
+ delBg: string;
25
+ hunkBg: string;
26
+ }
27
+
28
+ /** Colours read as text or markers on `bg`; each must reach 4.5:1 against it. */
29
+ export const CONTRAST_TOKENS = [
30
+ "ink",
31
+ "muted",
32
+ "accent",
33
+ "ok",
34
+ "warn",
35
+ "error",
36
+ "info",
37
+ "neutral",
38
+ ] as const satisfies readonly (keyof ReportPalette)[];
39
+
40
+ export const LIGHT_PALETTE: ReportPalette = {
41
+ ink: ink.hex,
42
+ bg: "#FFFFFF",
43
+ muted: "#59625E",
44
+ rule: "#E2DED5",
45
+ accent: mateGreen.hex,
46
+ surface: "#F7F5F0",
47
+ ok: mateGreen.hex,
48
+ warn: "#8A5A00",
49
+ error: "#B3261E",
50
+ info: "#2F5F9E",
51
+ neutral: "#66706B",
52
+ addBg: "#E4F3EA",
53
+ delBg: "#FBE8E6",
54
+ hunkBg: "#F1EEE7",
55
+ };
56
+
57
+ export const DARK_PALETTE: ReportPalette = {
58
+ ink: cream.hex,
59
+ bg: night.hex,
60
+ muted: "#A5AEA9",
61
+ rule: "#2D3433",
62
+ accent: lightGreen.hex,
63
+ surface: "#1C2223",
64
+ ok: lightGreen.hex,
65
+ warn: "#E3B45C",
66
+ error: "#F28B82",
67
+ info: "#8DB8F0",
68
+ neutral: "#A5AEA9",
69
+ addBg: "#16382A",
70
+ delBg: "#46211E",
71
+ hunkBg: "#232A2B",
72
+ };
73
+
74
+ export const PRINT_PALETTE: ReportPalette = {
75
+ ...LIGHT_PALETTE,
76
+ ink: "#000000",
77
+ muted: "#444444",
78
+ rule: "#BBBBBB",
79
+ accent: "#1F5C41",
80
+ surface: "#FFFFFF",
81
+ };
82
+
83
+ const kebab = (name: string): string => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
84
+
85
+ function declarations(palette: ReportPalette): string {
86
+ return Object.entries(palette)
87
+ .map(([name, value]) => `--${kebab(name)}: ${value};`)
88
+ .join(" ");
89
+ }
90
+
91
+ /**
92
+ * Light by default; dark on screen when the browser prefers it, unless the page sets
93
+ * `data-scheme="light"`; dark on screen whenever it sets `data-scheme="dark"`. Both dark
94
+ * rules are screen-only, so print always falls back to the print tokens.
95
+ */
96
+ export function paletteCss(): string {
97
+ return `:root { color-scheme: light dark; ${declarations(LIGHT_PALETTE)} }
98
+ @media screen and (prefers-color-scheme: dark) { :root:not([data-scheme="light"]) { ${declarations(DARK_PALETTE)} } }
99
+ @media screen { :root[data-scheme="dark"] { color-scheme: dark; ${declarations(DARK_PALETTE)} } }
100
+ :root[data-scheme="light"] { color-scheme: light; }
101
+ @media print { :root { color-scheme: light; ${declarations(PRINT_PALETTE)} } }`;
102
+ }
103
+
104
+ function channel(value: number): number {
105
+ const c = value / 255;
106
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
107
+ }
108
+
109
+ function luminance(hex: string): number {
110
+ const [r, g, b] = [1, 3, 5].map((start) => Number.parseInt(hex.slice(start, start + 2), 16));
111
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
112
+ }
113
+
114
+ /** WCAG 2 contrast ratio between two `#RRGGBB` colours. */
115
+ export function contrastRatio(foreground: string, background: string): number {
116
+ const [light, dark] = [luminance(foreground), luminance(background)].toSorted((a, b) => b - a);
117
+ return (light + 0.05) / (dark + 0.05);
118
+ }