@variance-authority/tribunal 0.1.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 (271) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/LICENSE +21 -0
  3. package/README.md +506 -0
  4. package/dist/__fixtures__/review.d.ts +47 -0
  5. package/dist/__fixtures__/review.js +145 -0
  6. package/dist/__fixtures__/review.js.map +1 -0
  7. package/dist/bindings.d.ts +134 -0
  8. package/dist/bindings.js +88 -0
  9. package/dist/bindings.js.map +1 -0
  10. package/dist/changelog.d.ts +132 -0
  11. package/dist/changelog.js +129 -0
  12. package/dist/changelog.js.map +1 -0
  13. package/dist/history-queries.d.ts +82 -0
  14. package/dist/history-queries.js +200 -0
  15. package/dist/history-queries.js.map +1 -0
  16. package/dist/history-rows.d.ts +42 -0
  17. package/dist/history-rows.js +148 -0
  18. package/dist/history-rows.js.map +1 -0
  19. package/dist/history.d.ts +40 -0
  20. package/dist/history.js +186 -0
  21. package/dist/history.js.map +1 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.js +12 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/migration-steps.d.ts +32 -0
  26. package/dist/migration-steps.js +303 -0
  27. package/dist/migration-steps.js.map +1 -0
  28. package/dist/migrations.d.ts +58 -0
  29. package/dist/migrations.js +283 -0
  30. package/dist/migrations.js.map +1 -0
  31. package/dist/next.d.ts +94 -0
  32. package/dist/next.js +44 -0
  33. package/dist/next.js.map +1 -0
  34. package/dist/node/bin.d.ts +120 -0
  35. package/dist/node/bin.js +337 -0
  36. package/dist/node/bin.js.map +1 -0
  37. package/dist/node/bucket.d.ts +58 -0
  38. package/dist/node/bucket.js +142 -0
  39. package/dist/node/bucket.js.map +1 -0
  40. package/dist/node/database.d.ts +33 -0
  41. package/dist/node/database.js +157 -0
  42. package/dist/node/database.js.map +1 -0
  43. package/dist/node/index.d.ts +43 -0
  44. package/dist/node/index.js +43 -0
  45. package/dist/node/index.js.map +1 -0
  46. package/dist/node/serve.d.ts +81 -0
  47. package/dist/node/serve.js +193 -0
  48. package/dist/node/serve.js.map +1 -0
  49. package/dist/node/ui-assets.d.ts +47 -0
  50. package/dist/node/ui-assets.js +60 -0
  51. package/dist/node/ui-assets.js.map +1 -0
  52. package/dist/review-ingest.d.ts +24 -0
  53. package/dist/review-ingest.js +184 -0
  54. package/dist/review-ingest.js.map +1 -0
  55. package/dist/review-read.d.ts +87 -0
  56. package/dist/review-read.js +361 -0
  57. package/dist/review-read.js.map +1 -0
  58. package/dist/review-rows.d.ts +30 -0
  59. package/dist/review-rows.js +67 -0
  60. package/dist/review-rows.js.map +1 -0
  61. package/dist/review-types.d.ts +414 -0
  62. package/dist/review-types.js +2 -0
  63. package/dist/review-types.js.map +1 -0
  64. package/dist/review-write.d.ts +35 -0
  65. package/dist/review-write.js +86 -0
  66. package/dist/review-write.js.map +1 -0
  67. package/dist/review.d.ts +53 -0
  68. package/dist/review.fixtures.d.ts +47 -0
  69. package/dist/review.fixtures.js +145 -0
  70. package/dist/review.fixtures.js.map +1 -0
  71. package/dist/review.js +245 -0
  72. package/dist/review.js.map +1 -0
  73. package/dist/schema.d.ts +36 -0
  74. package/dist/schema.js +39 -0
  75. package/dist/schema.js.map +1 -0
  76. package/dist/store.d.ts +58 -0
  77. package/dist/store.js +278 -0
  78. package/dist/store.js.map +1 -0
  79. package/dist/testing.d.ts +38 -0
  80. package/dist/testing.js +92 -0
  81. package/dist/testing.js.map +1 -0
  82. package/dist/ui/__probe.d.ts +3 -0
  83. package/dist/ui/__probe.js +5 -0
  84. package/dist/ui/__probe.js.map +1 -0
  85. package/dist/ui/attribution.d.ts +46 -0
  86. package/dist/ui/attribution.js +79 -0
  87. package/dist/ui/attribution.js.map +1 -0
  88. package/dist/ui/because.d.ts +51 -0
  89. package/dist/ui/because.js +52 -0
  90. package/dist/ui/because.js.map +1 -0
  91. package/dist/ui/browser-entry.d.ts +2 -0
  92. package/dist/ui/browser-entry.js +34 -0
  93. package/dist/ui/browser-entry.js.map +1 -0
  94. package/dist/ui/builds.d.ts +43 -0
  95. package/dist/ui/builds.js +79 -0
  96. package/dist/ui/builds.js.map +1 -0
  97. package/dist/ui/carried.d.ts +73 -0
  98. package/dist/ui/carried.js +63 -0
  99. package/dist/ui/carried.js.map +1 -0
  100. package/dist/ui/cause.d.ts +73 -0
  101. package/dist/ui/cause.js +133 -0
  102. package/dist/ui/cause.js.map +1 -0
  103. package/dist/ui/change-story.d.ts +153 -0
  104. package/dist/ui/change-story.js +253 -0
  105. package/dist/ui/change-story.js.map +1 -0
  106. package/dist/ui/change.d.ts +51 -0
  107. package/dist/ui/change.js +125 -0
  108. package/dist/ui/change.js.map +1 -0
  109. package/dist/ui/client.d.ts +74 -0
  110. package/dist/ui/client.js +88 -0
  111. package/dist/ui/client.js.map +1 -0
  112. package/dist/ui/consumers.d.ts +80 -0
  113. package/dist/ui/consumers.js +125 -0
  114. package/dist/ui/consumers.js.map +1 -0
  115. package/dist/ui/control.d.ts +63 -0
  116. package/dist/ui/control.js +39 -0
  117. package/dist/ui/control.js.map +1 -0
  118. package/dist/ui/crossing.d.ts +55 -0
  119. package/dist/ui/crossing.js +74 -0
  120. package/dist/ui/crossing.js.map +1 -0
  121. package/dist/ui/declarations.d.ts +36 -0
  122. package/dist/ui/declarations.js +73 -0
  123. package/dist/ui/declarations.js.map +1 -0
  124. package/dist/ui/distance.d.ts +154 -0
  125. package/dist/ui/distance.js +180 -0
  126. package/dist/ui/distance.js.map +1 -0
  127. package/dist/ui/divergence.d.ts +40 -0
  128. package/dist/ui/divergence.js +176 -0
  129. package/dist/ui/divergence.js.map +1 -0
  130. package/dist/ui/docket.d.ts +39 -0
  131. package/dist/ui/docket.js +172 -0
  132. package/dist/ui/docket.js.map +1 -0
  133. package/dist/ui/findings.d.ts +42 -0
  134. package/dist/ui/findings.js +110 -0
  135. package/dist/ui/findings.js.map +1 -0
  136. package/dist/ui/foreseen.d.ts +55 -0
  137. package/dist/ui/foreseen.js +91 -0
  138. package/dist/ui/foreseen.js.map +1 -0
  139. package/dist/ui/glance.d.ts +101 -0
  140. package/dist/ui/glance.js +101 -0
  141. package/dist/ui/glance.js.map +1 -0
  142. package/dist/ui/grouping.d.ts +158 -0
  143. package/dist/ui/grouping.js +187 -0
  144. package/dist/ui/grouping.js.map +1 -0
  145. package/dist/ui/handed.d.ts +60 -0
  146. package/dist/ui/handed.js +122 -0
  147. package/dist/ui/handed.js.map +1 -0
  148. package/dist/ui/history.d.ts +88 -0
  149. package/dist/ui/history.js +170 -0
  150. package/dist/ui/history.js.map +1 -0
  151. package/dist/ui/holding.d.ts +84 -0
  152. package/dist/ui/holding.js +90 -0
  153. package/dist/ui/holding.js.map +1 -0
  154. package/dist/ui/impact.d.ts +31 -0
  155. package/dist/ui/impact.js +132 -0
  156. package/dist/ui/impact.js.map +1 -0
  157. package/dist/ui/index.d.ts +21 -0
  158. package/dist/ui/index.js +21 -0
  159. package/dist/ui/index.js.map +1 -0
  160. package/dist/ui/lead.d.ts +35 -0
  161. package/dist/ui/lead.js +34 -0
  162. package/dist/ui/lead.js.map +1 -0
  163. package/dist/ui/look.d.ts +109 -0
  164. package/dist/ui/look.js +248 -0
  165. package/dist/ui/look.js.map +1 -0
  166. package/dist/ui/mark.d.ts +11 -0
  167. package/dist/ui/mark.js +13 -0
  168. package/dist/ui/mark.js.map +1 -0
  169. package/dist/ui/moved.d.ts +121 -0
  170. package/dist/ui/moved.js +143 -0
  171. package/dist/ui/moved.js.map +1 -0
  172. package/dist/ui/navigation.d.ts +26 -0
  173. package/dist/ui/navigation.js +53 -0
  174. package/dist/ui/navigation.js.map +1 -0
  175. package/dist/ui/order.d.ts +113 -0
  176. package/dist/ui/order.js +226 -0
  177. package/dist/ui/order.js.map +1 -0
  178. package/dist/ui/origins.d.ts +42 -0
  179. package/dist/ui/origins.js +149 -0
  180. package/dist/ui/origins.js.map +1 -0
  181. package/dist/ui/outcome.d.ts +93 -0
  182. package/dist/ui/outcome.js +162 -0
  183. package/dist/ui/outcome.js.map +1 -0
  184. package/dist/ui/parted.d.ts +68 -0
  185. package/dist/ui/parted.js +101 -0
  186. package/dist/ui/parted.js.map +1 -0
  187. package/dist/ui/rail.d.ts +31 -0
  188. package/dist/ui/rail.js +21 -0
  189. package/dist/ui/rail.js.map +1 -0
  190. package/dist/ui/reach.d.ts +66 -0
  191. package/dist/ui/reach.js +185 -0
  192. package/dist/ui/reach.js.map +1 -0
  193. package/dist/ui/regions.d.ts +82 -0
  194. package/dist/ui/regions.js +67 -0
  195. package/dist/ui/regions.js.map +1 -0
  196. package/dist/ui/review.bundle.js +69 -0
  197. package/dist/ui/review.d.ts +97 -0
  198. package/dist/ui/review.js +39 -0
  199. package/dist/ui/review.js.map +1 -0
  200. package/dist/ui/root.d.ts +72 -0
  201. package/dist/ui/root.js +102 -0
  202. package/dist/ui/root.js.map +1 -0
  203. package/dist/ui/route.d.ts +95 -0
  204. package/dist/ui/route.js +114 -0
  205. package/dist/ui/route.js.map +1 -0
  206. package/dist/ui/run.d.ts +26 -0
  207. package/dist/ui/run.js +12 -0
  208. package/dist/ui/run.js.map +1 -0
  209. package/dist/ui/sense.d.ts +187 -0
  210. package/dist/ui/sense.js +222 -0
  211. package/dist/ui/sense.js.map +1 -0
  212. package/dist/ui/settled.d.ts +43 -0
  213. package/dist/ui/settled.js +59 -0
  214. package/dist/ui/settled.js.map +1 -0
  215. package/dist/ui/shell.d.ts +76 -0
  216. package/dist/ui/shell.js +65 -0
  217. package/dist/ui/shell.js.map +1 -0
  218. package/dist/ui/shift.d.ts +52 -0
  219. package/dist/ui/shift.js +98 -0
  220. package/dist/ui/shift.js.map +1 -0
  221. package/dist/ui/styles-docket.d.ts +18 -0
  222. package/dist/ui/styles-docket.js +281 -0
  223. package/dist/ui/styles-docket.js.map +1 -0
  224. package/dist/ui/styles-stage.d.ts +18 -0
  225. package/dist/ui/styles-stage.js +92 -0
  226. package/dist/ui/styles-stage.js.map +1 -0
  227. package/dist/ui/styles.d.ts +12 -0
  228. package/dist/ui/styles.js +482 -0
  229. package/dist/ui/styles.js.map +1 -0
  230. package/dist/ui/subject.d.ts +36 -0
  231. package/dist/ui/subject.js +138 -0
  232. package/dist/ui/subject.js.map +1 -0
  233. package/dist/ui/text.d.ts +129 -0
  234. package/dist/ui/text.js +164 -0
  235. package/dist/ui/text.js.map +1 -0
  236. package/dist/ui/variations.d.ts +37 -0
  237. package/dist/ui/variations.js +60 -0
  238. package/dist/ui/variations.js.map +1 -0
  239. package/dist/ui/viewer.d.ts +67 -0
  240. package/dist/ui/viewer.js +246 -0
  241. package/dist/ui/viewer.js.map +1 -0
  242. package/dist/worker-auth.d.ts +48 -0
  243. package/dist/worker-auth.js +74 -0
  244. package/dist/worker-auth.js.map +1 -0
  245. package/dist/worker-entry.d.ts +58 -0
  246. package/dist/worker-entry.js +36 -0
  247. package/dist/worker-entry.js.map +1 -0
  248. package/dist/worker-http.d.ts +39 -0
  249. package/dist/worker-http.js +111 -0
  250. package/dist/worker-http.js.map +1 -0
  251. package/dist/worker-input.d.ts +81 -0
  252. package/dist/worker-input.js +296 -0
  253. package/dist/worker-input.js.map +1 -0
  254. package/dist/worker.d.ts +91 -0
  255. package/dist/worker.js +287 -0
  256. package/dist/worker.js.map +1 -0
  257. package/mark.svg +30 -0
  258. package/migrations/0001_initial.sql +193 -0
  259. package/migrations/0002_changelog.sql +30 -0
  260. package/migrations/0003_accessibility.sql +8 -0
  261. package/migrations/0004_variations.sql +19 -0
  262. package/migrations/0005_reach.sql +26 -0
  263. package/migrations/0006_baseline-size.sql +7 -0
  264. package/migrations/0007_declarations.sql +7 -0
  265. package/migrations/0008_green-because.sql +7 -0
  266. package/migrations/0009_moved-bands.sql +6 -0
  267. package/migrations/0010_composition.sql +15 -0
  268. package/migrations/0011_movements.sql +21 -0
  269. package/migrations/0012_control-size.sql +6 -0
  270. package/package.json +110 -0
  271. package/wrangler.jsonc +45 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The variation lattice, drawn — the half of a run that has no baseline in it.
3
+ *
4
+ * Apart from the build page because it answers a different question. A docket
5
+ * says *what changed since last time*; this says *does the flag do anything*, and
6
+ * nothing else in the product can: a subject added behind a flag is `new`, its
7
+ * diff is empty, and the flag's effect is visible only by putting two pictures
8
+ * side by side — the comparison nobody performs. The run already made it.
9
+ */
10
+ import type { VariationRecord } from '@variance-authority/report';
11
+ import type { ReactElement } from 'react';
12
+ /**
13
+ * Every subject that was read against another subject in the same run.
14
+ *
15
+ * Nothing here is a verdict, and the section is deliberately not part of the
16
+ * counts: a variation is a difference somebody declared on purpose, and reporting
17
+ * one as a regression would be reporting a subject for existing.
18
+ *
19
+ * **The order is the point, and it is not the report's.** Two states come first,
20
+ * because they are the two a reviewer can act on and both are silent everywhere
21
+ * else:
22
+ *
23
+ * - **reaches nothing** — the pair renders to one hash. An A/B arm whose flag
24
+ * changes no pixel is an experiment measuring nothing, and it looks exactly like
25
+ * a healthy arm in every screenshot tool there is: two subjects, both
26
+ * `unchanged`, both green.
27
+ * - **no parent in this run** — the declaration named a subject nothing observed.
28
+ * A broken link and an axis with nothing to say are not the same finding, so
29
+ * they are not drawn the same way.
30
+ *
31
+ * The rest follow by subject, which is the lattice's own order: a name that
32
+ * extends another name sorts after it.
33
+ */
34
+ export declare function Variations({ variations, }: {
35
+ readonly variations: readonly VariationRecord[];
36
+ }): ReactElement | null;
37
+ //# sourceMappingURL=variations.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Prose } from './shell.js';
3
+ /**
4
+ * Every subject that was read against another subject in the same run.
5
+ *
6
+ * Nothing here is a verdict, and the section is deliberately not part of the
7
+ * counts: a variation is a difference somebody declared on purpose, and reporting
8
+ * one as a regression would be reporting a subject for existing.
9
+ *
10
+ * **The order is the point, and it is not the report's.** Two states come first,
11
+ * because they are the two a reviewer can act on and both are silent everywhere
12
+ * else:
13
+ *
14
+ * - **reaches nothing** — the pair renders to one hash. An A/B arm whose flag
15
+ * changes no pixel is an experiment measuring nothing, and it looks exactly like
16
+ * a healthy arm in every screenshot tool there is: two subjects, both
17
+ * `unchanged`, both green.
18
+ * - **no parent in this run** — the declaration named a subject nothing observed.
19
+ * A broken link and an axis with nothing to say are not the same finding, so
20
+ * they are not drawn the same way.
21
+ *
22
+ * The rest follow by subject, which is the lattice's own order: a name that
23
+ * extends another name sorts after it.
24
+ */
25
+ export function Variations({ variations, }) {
26
+ if (variations.length === 0)
27
+ return null;
28
+ const ordered = [...variations].sort((left, right) => {
29
+ const byRank = rankOf(left) - rankOf(right);
30
+ if (byRank !== 0)
31
+ return byRank;
32
+ return left.subject < right.subject ? -1 : left.subject > right.subject ? 1 : 0;
33
+ });
34
+ return (_jsxs(_Fragment, { children: [_jsx("h2", { children: "Variations" }), _jsx("p", { className: "va-note", style: { marginBottom: '0.6rem' }, children: "Subjects read against the subject they vary from, in this run. Not verdicts \u2014 a variation is a difference somebody meant. What is worth reading is a variation that turns out to be no difference at all." }), _jsx("ul", { className: "va-variation-list", children: ordered.map((variation) => (_jsxs("li", { className: `va-variation ${STATES[rankOf(variation)] ?? ''}`, children: [_jsxs("p", { className: "va-variation-head", children: [_jsx("span", { className: "va-axis", children: axisOf(variation) }), _jsx("strong", { children: variation.subject }), variation.parent === undefined ? null : (_jsxs(_Fragment, { children: ["from ", _jsx("strong", { children: variation.parent })] })), variation.how === undefined ? (_jsx("span", { className: "va-how", children: "how this pair was found is unstated" })) : (_jsx("span", { className: "va-how", children: variation.how })), variation.unobserved === undefined || variation.unobserved.length === 0 ? null : (_jsxs("span", { className: "va-how", children: ["undecided in ", variation.unobserved.join(', ')] }))] }), _jsx("p", { className: "va-because", children: _jsx(Prose, { text: variation.because }) })] }, variation.subject))) })] }));
35
+ }
36
+ /** One class per rank, so the ordering and the drawing cannot disagree. */
37
+ const STATES = ['va-inert', 'va-unlinked', ''];
38
+ function rankOf(variation) {
39
+ if (variation.identical === true)
40
+ return 0;
41
+ if (variation.identical === undefined)
42
+ return 1;
43
+ return 2;
44
+ }
45
+ /**
46
+ * The axis, in the words the record earned.
47
+ *
48
+ * `identical === undefined` is not `differs`: nothing compared the pair, and the
49
+ * only honest short label for that is that nothing did. The distinction is the
50
+ * same one the column keeps nullable for.
51
+ */
52
+ function axisOf(variation) {
53
+ if (variation.identical === undefined)
54
+ return 'not compared';
55
+ if (variation.identical)
56
+ return 'reaches nothing';
57
+ const bands = variation.bands ?? [];
58
+ return bands.length === 0 ? 'differs' : bands.join(' · ');
59
+ }
60
+ //# sourceMappingURL=variations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variations.js","sourceRoot":"","sources":["../../src/ui/variations.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * The variation lattice, drawn — the half of a run that has no baseline in it.\n *\n * Apart from the build page because it answers a different question. A docket\n * says *what changed since last time*; this says *does the flag do anything*, and\n * nothing else in the product can: a subject added behind a flag is `new`, its\n * diff is empty, and the flag's effect is visible only by putting two pictures\n * side by side — the comparison nobody performs. The run already made it.\n */\n\nimport type { VariationRecord } from '@variance-authority/report';\nimport type { ReactElement } from 'react';\nimport { Prose } from './shell.js';\n\n/**\n * Every subject that was read against another subject in the same run.\n *\n * Nothing here is a verdict, and the section is deliberately not part of the\n * counts: a variation is a difference somebody declared on purpose, and reporting\n * one as a regression would be reporting a subject for existing.\n *\n * **The order is the point, and it is not the report's.** Two states come first,\n * because they are the two a reviewer can act on and both are silent everywhere\n * else:\n *\n * - **reaches nothing** — the pair renders to one hash. An A/B arm whose flag\n * changes no pixel is an experiment measuring nothing, and it looks exactly like\n * a healthy arm in every screenshot tool there is: two subjects, both\n * `unchanged`, both green.\n * - **no parent in this run** — the declaration named a subject nothing observed.\n * A broken link and an axis with nothing to say are not the same finding, so\n * they are not drawn the same way.\n *\n * The rest follow by subject, which is the lattice's own order: a name that\n * extends another name sorts after it.\n */\nexport function Variations({\n variations,\n}: {\n readonly variations: readonly VariationRecord[];\n}): ReactElement | null {\n if (variations.length === 0) return null;\n\n const ordered = [...variations].sort((left, right) => {\n const byRank = rankOf(left) - rankOf(right);\n if (byRank !== 0) return byRank;\n return left.subject < right.subject ? -1 : left.subject > right.subject ? 1 : 0;\n });\n\n return (\n <>\n <h2>Variations</h2>\n <p className=\"va-note\" style={{ marginBottom: '0.6rem' }}>\n Subjects read against the subject they vary from, in this run. Not verdicts — a variation is\n a difference somebody meant. What is worth reading is a variation that turns out to be no\n difference at all.\n </p>\n <ul className=\"va-variation-list\">\n {ordered.map((variation) => (\n <li key={variation.subject} className={`va-variation ${STATES[rankOf(variation)] ?? ''}`}>\n <p className=\"va-variation-head\">\n <span className=\"va-axis\">{axisOf(variation)}</span>\n <strong>{variation.subject}</strong>\n {variation.parent === undefined ? null : (\n <>\n from <strong>{variation.parent}</strong>\n </>\n )}\n {variation.how === undefined ? (\n <span className=\"va-how\">how this pair was found is unstated</span>\n ) : (\n <span className=\"va-how\">{variation.how}</span>\n )}\n {variation.unobserved === undefined || variation.unobserved.length === 0 ? null : (\n // Absent is not \"none\", so this appears only when the run said so.\n // A band no profile could decide is a band nobody should read this\n // variation as clean in.\n <span className=\"va-how\">undecided in {variation.unobserved.join(', ')}</span>\n )}\n </p>\n <p className=\"va-because\"><Prose text={variation.because} /></p>\n </li>\n ))}\n </ul>\n </>\n );\n}\n\n/** One class per rank, so the ordering and the drawing cannot disagree. */\nconst STATES: readonly string[] = ['va-inert', 'va-unlinked', ''];\n\nfunction rankOf(variation: VariationRecord): number {\n if (variation.identical === true) return 0;\n if (variation.identical === undefined) return 1;\n return 2;\n}\n\n/**\n * The axis, in the words the record earned.\n *\n * `identical === undefined` is not `differs`: nothing compared the pair, and the\n * only honest short label for that is that nothing did. The distinction is the\n * same one the column keeps nullable for.\n */\nfunction axisOf(variation: VariationRecord): string {\n if (variation.identical === undefined) return 'not compared';\n if (variation.identical) return 'reaches nothing';\n const bands = variation.bands ?? [];\n return bands.length === 0 ? 'differs' : bands.join(' · ');\n}\n"]}
@@ -0,0 +1,67 @@
1
+ import { type CSSProperties, type ReactElement } from 'react';
2
+ import type { SubjectView } from '../review.js';
3
+ import type { ReviewClient } from './client.js';
4
+ /**
5
+ * Looking at the change: the stage, the modes, and the zoom that makes any of it
6
+ * mean anything.
7
+ *
8
+ * Apart from [`review.tsx`](./review.tsx) because the two answer different
9
+ * questions and are read in that order. The docket decides *what is worth looking
10
+ * at* and is the part of this surface that refuses the category's habit;
11
+ * everything here is the looking itself, and is reached only once that question
12
+ * has been answered.
13
+ *
14
+ * Three positions this file exists to hold:
15
+ *
16
+ * **Every layer is drawn at one scale.** The plate has a width and each raster
17
+ * fills it, so the baseline and the candidate are always the same magnification.
18
+ * A wipe between two images at different scales is not a comparison; it is two
19
+ * pictures with a line between them, and it reads as a change everywhere the line
20
+ * happens to be.
21
+ *
22
+ * **Fit-to-width is a starting position, not the only one.** A route capture is
23
+ * 1280 pixels wide and arrives in a pane about half that, so a two-pixel shift is
24
+ * drawn at one pixel — under the resampler, which is to say not drawn. The whole
25
+ * reason the sheet refuses smoothing is so that a reviewer can go to 1:1 and see
26
+ * two pixels; without a zoom that refusal protects nothing.
27
+ *
28
+ * **A reviewer is taken to the finding.** The run measured where the differences
29
+ * are. On nine thousand pixels of route, asking somebody to scroll until they
30
+ * notice one is asking them to redo the measurement by eye — so the regions are
31
+ * a list you step through, and the stage scrolls to each.
32
+ */
33
+ export type ViewerMode = 'regions' | 'wipe' | 'blend' | 'blink' | 'side-by-side' | 'difference';
34
+ /** A raster's own pixels — never a rendered size, which the zoom decides. */
35
+ export type Size = {
36
+ readonly width: number;
37
+ readonly height: number;
38
+ };
39
+ export declare function Viewer({ client, build, subject, sourced, }: {
40
+ readonly client: ReviewClient;
41
+ readonly build: string;
42
+ readonly subject: SubjectView;
43
+ /** Whether the run resolved any source file — see {@link RegionTable}. */
44
+ readonly sourced?: boolean | undefined;
45
+ }): ReactElement;
46
+ /** The box that contains both captures, when either of them is known. */
47
+ export declare function union(candidate: Size | undefined, baseline: Size | undefined): Size | undefined;
48
+ /**
49
+ * One layer's share of the plate, as a percentage.
50
+ *
51
+ * A percentage rather than a pixel width so that nothing here has to know the
52
+ * magnification: the plate is sized once and every layer scales with it. Absent
53
+ * when this layer's own size was never recorded, which leaves the stylesheet's
54
+ * `width: 100%` — the old behaviour, and the honest one, since a layer nobody
55
+ * measured cannot be placed against one that was.
56
+ */
57
+ export declare function share(own: Size | undefined, box: Size | undefined): CSSProperties | undefined;
58
+ /**
59
+ * Which comparisons this build can actually offer.
60
+ *
61
+ * Derived from what the run kept rather than offered unconditionally: a mode
62
+ * whose image does not exist renders a broken frame, and a broken frame in a
63
+ * review surface reads as a subject that renders to nothing.
64
+ */
65
+ export declare function modesFor(subject: SubjectView): readonly ViewerMode[];
66
+ export { RegionOverlay, RegionTable } from './regions.js';
67
+ //# sourceMappingURL=viewer.d.ts.map
@@ -0,0 +1,246 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { RegionOverlay, RegionTable } from './regions.js';
4
+ import { count, magnitude, number } from './text.js';
5
+ const ZOOMS = ['fit', 1, 2, 4];
6
+ /**
7
+ * What each mode is called on the button.
8
+ *
9
+ * The union members are identifiers and two of them read as identifiers. A
10
+ * reviewer is not debugging this surface; nothing on it should ask them to
11
+ * translate.
12
+ */
13
+ const LABELS = {
14
+ regions: 'regions',
15
+ wipe: 'wipe',
16
+ blend: 'blend',
17
+ blink: 'blink',
18
+ 'side-by-side': 'side by side',
19
+ difference: 'difference',
20
+ };
21
+ /** Long enough to read either state, short enough that the eye holds both. */
22
+ const BLINK_MS = 700;
23
+ export function Viewer({ client, build, subject, sourced, }) {
24
+ const available = useMemo(() => modesFor(subject), [subject]);
25
+ const [mode, setMode] = useState(available[0] ?? 'regions');
26
+ const [seam, setSeam] = useState(50);
27
+ const [blend, setBlend] = useState(50);
28
+ const [zoom, setZoom] = useState('fit');
29
+ const [focus, setFocus] = useState(null);
30
+ const [flip, setFlip] = useState(true);
31
+ const [pull, setPull] = useState(0);
32
+ const [still] = useState(calm);
33
+ const stage = useRef(null);
34
+ /**
35
+ * The frame both readings are drawn in.
36
+ *
37
+ * The union of the two captures, which is what the comparison itself measured
38
+ * in — it pads to the union box, so the regions are already in these
39
+ * coordinates. Drawing each image at its own share of this box is the whole
40
+ * point: a baseline stretched to the candidate's width is a width change
41
+ * resampled into a hairline, and a width change is one of the largest things
42
+ * that can happen to a page.
43
+ */
44
+ const box = union(subject.size, subject.baseline);
45
+ // The alternation, driven here rather than by a keyframe: the stylesheet is
46
+ // pasted into somebody else's page and is held to hiding nothing, and a reader
47
+ // who never chose this mode should not have an animation running in it.
48
+ useEffect(() => {
49
+ if (mode !== 'blink' || still)
50
+ return;
51
+ const timer = setInterval(() => setFlip((was) => !was), BLINK_MS);
52
+ return () => clearInterval(timer);
53
+ }, [mode, still]);
54
+ // What survives the move to another subject and what does not. Mode and
55
+ // magnification are the reviewer's; a region number belongs to the picture it
56
+ // was counted in, and a mode this subject cannot make would render a frame with
57
+ // no image behind it.
58
+ useEffect(() => {
59
+ setFocus(null);
60
+ setPull(0);
61
+ setMode((was) => (available.includes(was) ? was : (available[0] ?? 'regions')));
62
+ stage.current?.scrollTo({ left: 0, top: 0 });
63
+ }, [available]);
64
+ useEffect(() => {
65
+ if (pull === 0 || focus === null)
66
+ return;
67
+ const region = subject.regions[focus];
68
+ const pane = stage.current;
69
+ if (region === undefined || pane === null)
70
+ return;
71
+ const at = zoom === 'fit' ? 1 : zoom;
72
+ pane.scrollTo({
73
+ left: Math.max(0, (region.x + region.width / 2) * at - pane.clientWidth / 2),
74
+ top: Math.max(0, (region.y + region.height / 2) * at - pane.clientHeight / 2),
75
+ behavior: still ? 'auto' : 'smooth',
76
+ });
77
+ // Deliberately keyed on the count alone: two jumps to the same region are two
78
+ // requests, and the zoom this reads is already the one the plate was drawn at.
79
+ // eslint-disable-next-line react-hooks/exhaustive-deps
80
+ }, [pull]);
81
+ // Every hook is above this line, and has to be: without a `key` forcing a
82
+ // remount, moving from a subject with images to one without would otherwise
83
+ // change how many hooks this component calls.
84
+ if (available.length === 0) {
85
+ return _jsx("p", { className: "va-note", children: "This run kept no images for this subject." });
86
+ }
87
+ const size = subject.size;
88
+ const url = (kind) => client.imageUrl(build, subject.subject, kind);
89
+ /**
90
+ * Centre a region in the pane.
91
+ *
92
+ * At fit the plate is smaller than the capture and there is nothing to scroll
93
+ * to, so this raises the zoom first — the request is *show me this*, and
94
+ * showing it at a third of size is answering a different question.
95
+ *
96
+ * The scroll itself is deferred to the effect below rather than done here.
97
+ * Raising the zoom changes the plate's height, and a scroll issued against the
98
+ * old height is clamped to it: from fit to 1:1 the plate shrinks by whatever the
99
+ * pane was scaling it, and the pane lands short of the region by that factor.
100
+ */
101
+ const jump = (index) => {
102
+ if (subject.regions[index] === undefined || box === undefined)
103
+ return;
104
+ setFocus(index);
105
+ if (zoom === 'fit')
106
+ setZoom(1);
107
+ setPull((count) => count + 1);
108
+ };
109
+ const zoomable = box !== undefined;
110
+ /**
111
+ * Fit is a ceiling rather than a stretch. A 390-wide capture in a 700-wide pane
112
+ * has nothing to gain from being drawn at 700: the run refuses smoothing, so the
113
+ * only thing an upscale adds is blocks the render never had. Below its own width
114
+ * the plate shrinks to the pane; at or above it, it stops.
115
+ *
116
+ * The ratio is declared rather than left to the images, because the images are
117
+ * taken out of flow the moment there are two of them at different sizes — and a
118
+ * plate with no height is a plate the region boxes have nothing to sit on.
119
+ */
120
+ const plate = box === undefined
121
+ ? {}
122
+ : {
123
+ aspectRatio: `${String(box.width)} / ${String(box.height)}`,
124
+ ...(zoom === 'fit'
125
+ ? { maxWidth: `${String(box.width)}px` }
126
+ : { width: `${String(box.width * zoom)}px` }),
127
+ };
128
+ const boxes = mode === 'side-by-side' ? null : (_jsx(RegionOverlay, { subject: subject, box: box, focus: focus, onFocus: setFocus }));
129
+ return (_jsxs("div", { className: "va-viewer", children: [_jsxs("div", { className: "va-viewer-bar", children: [_jsxs("div", { className: "va-viewer-controls", children: [_jsx("p", { className: "va-modes", children: available.map((option) => (_jsx("button", { type: "button", className: option === mode ? 'va-mode va-current' : 'va-mode', "aria-pressed": option === mode, onClick: () => setMode(option), children: LABELS[option] }, option))) }), zoomable ? (_jsx("p", { className: "va-zooms", children: ZOOMS.map((option) => (_jsx("button", { type: "button", className: option === zoom ? 'va-mode va-current' : 'va-mode', "aria-pressed": option === zoom, onClick: () => setZoom(option), children: option === 'fit' ? 'fit' : `${String(option)}×` }, String(option)))) })) : null] }), _jsxs("div", { className: "va-viewer-readout", children: [_jsx("p", { className: "va-showing", children: showing(mode, { seam, blend, flip }) }), size === undefined ? null : (_jsxs("p", { className: "va-measure", title: "the candidate\u2019s own pixels, which 1\u00D7 draws one for one", children: [number(size.width), " \u00D7 ", number(size.height), subject.baseline === undefined ||
130
+ (subject.baseline.width === size.width &&
131
+ subject.baseline.height === size.height) ? ('') : (_jsxs("span", { className: "va-resized", children: [' ', "\u00B7 was ", number(subject.baseline.width), " \u00D7 ", number(subject.baseline.height)] }))] })), subject.regions.length > 0 ? (_jsxs("p", { className: "va-steps", children: [_jsx("button", { type: "button", onClick: () => jump(step(focus, subject.regions.length, -1)), children: "\u2039" }), _jsxs("span", { className: "va-num", children: [focus === null ? '—' : String(focus + 1), " / ", String(subject.regions.length)] }), _jsx("button", { type: "button", onClick: () => jump(step(focus, subject.regions.length, 1)), children: "\u203A" })] })) : null] })] }), mode === 'side-by-side' ? (_jsxs("div", { className: "va-side-by-side", children: [_jsxs("figure", { children: [_jsx("figcaption", { children: "baseline" }), _jsx(Raster, { src: url('before'), alt: `${subject.subject} baseline` })] }), _jsxs("figure", { children: [_jsx("figcaption", { children: "this build" }), _jsx(Raster, { src: url('after'), alt: `${subject.subject} candidate`, size: size })] })] })) : (_jsx("div", { className: "va-loupe", ref: stage, children: _jsxs("div", { className: box === undefined ? 'va-plate' : 'va-plate va-boxed', style: plate, children: [mode === 'wipe' ? (_jsx("input", { type: "range", className: "va-seam", min: 0, max: 100, value: seam, "aria-label": "wipe between baseline and candidate", onChange: (event) => setSeam(Number(event.target.value)) })) : null, mode === 'difference' ? (_jsx(Raster, { src: url('diff'), alt: `${subject.subject} difference mask` })) : (_jsxs(_Fragment, { children: [subject.has.before ? (_jsx(Raster, { className: "va-under", src: url('before'), alt: `${subject.subject} baseline`, style: share(subject.baseline, box) })) : null, _jsx(Raster, { src: url('after'), alt: `${subject.subject} candidate`, size: size, style: { ...share(size, box), ...over(mode, { seam, blend, flip }) } })] })), boxes, mode === 'wipe' ? (_jsx("span", { className: "va-seam-line", "aria-hidden": "true", style: { left: `${String(seam)}%` } })) : null] }) })), mode === 'blink' && still ? (_jsxs("p", { className: "va-note", children: ["This display asked for reduced motion, so the two readings do not alternate on their own.", ' ', _jsxs("button", { type: "button", className: "va-mode", onClick: () => setFlip((was) => !was), children: ["show ", flip ? 'the baseline' : 'this build'] })] })) : null, mode === 'blend' ? (_jsx("input", { type: "range", className: "va-blend", min: 0, max: 100, value: blend, "aria-label": "fade between baseline and candidate", onChange: (event) => setBlend(Number(event.target.value)) })) : null, _jsxs("p", { className: "va-pixels", children: [magnitude(subject), subject.truncated === undefined ? null : (_jsxs(_Fragment, { children: [' ', "\u00B7 ", count(subject.truncated.regions, 'further region'), " (", number(subject.truncated.pixels), "px) were not recorded"] })), subject.missingFonts === undefined || subject.missingFonts.length === 0 ? null : (_jsxs(_Fragment, { children: [" \u00B7 fonts missing at render: ", subject.missingFonts.join(', ')] }))] }), _jsx(RegionTable, { subject: subject, sourced: sourced, focus: focus, onFocus: setFocus, onJump: jump })] }));
132
+ }
133
+ /**
134
+ * How the candidate is revealed, given the mode.
135
+ *
136
+ * One layer and one declaration, rather than a wrapper that clips: a clip path
137
+ * moves the seam without changing the box, so the candidate keeps its own height
138
+ * whatever the baseline underneath rendered to — which is frequently the change.
139
+ */
140
+ function over(mode, at) {
141
+ if (mode === 'wipe')
142
+ return { clipPath: `inset(0 0 0 ${String(at.seam)}%)` };
143
+ if (mode === 'blend')
144
+ return { opacity: at.blend / 100 };
145
+ if (mode === 'blink')
146
+ return { opacity: at.flip ? 1 : 0 };
147
+ return {};
148
+ }
149
+ /** The box that contains both captures, when either of them is known. */
150
+ export function union(candidate, baseline) {
151
+ if (candidate === undefined)
152
+ return baseline;
153
+ if (baseline === undefined)
154
+ return candidate;
155
+ return {
156
+ width: Math.max(candidate.width, baseline.width),
157
+ height: Math.max(candidate.height, baseline.height),
158
+ };
159
+ }
160
+ /**
161
+ * One layer's share of the plate, as a percentage.
162
+ *
163
+ * A percentage rather than a pixel width so that nothing here has to know the
164
+ * magnification: the plate is sized once and every layer scales with it. Absent
165
+ * when this layer's own size was never recorded, which leaves the stylesheet's
166
+ * `width: 100%` — the old behaviour, and the honest one, since a layer nobody
167
+ * measured cannot be placed against one that was.
168
+ */
169
+ export function share(own, box) {
170
+ if (own === undefined || box === undefined || own.width === box.width)
171
+ return undefined;
172
+ return { width: `${String((own.width / box.width) * 100)}%` };
173
+ }
174
+ /**
175
+ * Whether this display has asked for less movement.
176
+ *
177
+ * Read once, at mount, so a render is consistent with itself; `matchMedia` is
178
+ * absent in a static render, where there is no interval and no scroll to suppress
179
+ * and the answer is therefore no. What it governs is not decoration — blink *is*
180
+ * the alternation — so the mode is not withdrawn, it is handed over to the
181
+ * reviewer to step.
182
+ */
183
+ function calm() {
184
+ return globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
185
+ }
186
+ /** Which reading is on screen, said in the bar where a reviewer is looking. */
187
+ function showing(mode, at) {
188
+ if (mode === 'wipe')
189
+ return `baseline ◀ ${String(Math.round(at.seam))}% ▶ this build`;
190
+ if (mode === 'blend')
191
+ return `${String(Math.round(at.blend))}% this build`;
192
+ if (mode === 'blink')
193
+ return at.flip ? 'this build' : 'baseline';
194
+ if (mode === 'difference')
195
+ return 'difference mask';
196
+ return 'this build';
197
+ }
198
+ /** The next region to visit, wrapping, from nothing at either end. */
199
+ function step(focus, total, by) {
200
+ if (focus === null)
201
+ return by === 1 ? 0 : total - 1;
202
+ return (focus + by + total) % total;
203
+ }
204
+ /**
205
+ * One stored raster, fetched when it is about to be looked at.
206
+ *
207
+ * `loading="lazy"`, because a build page is a docket of every subject and a
208
+ * reviewer reads one at a time. A route suite's candidates are full-page: this
209
+ * project's four routes come to nine thousand pixels of height each, and twenty
210
+ * of them decoded at once is tens of thousands of rows of bitmap in one document
211
+ * — enough to stall the renderer before the first row of the table can be read.
212
+ * Nothing above the fold needs any of them.
213
+ *
214
+ * `size` reserves the box before the bytes arrive, and is passed only where the
215
+ * dimensions belong to *this* raster — the candidate. A baseline may have been a
216
+ * different height (that is frequently the change), and a diff mask is not
217
+ * measured at all, so those are deferred without a reservation rather than
218
+ * reserved wrongly. A wrong reservation is worse than none: the page settles at
219
+ * one height and then jumps.
220
+ */
221
+ function Raster({ src, alt, size, className, style, }) {
222
+ return (_jsx("img", { src: src, alt: alt, loading: "lazy", decoding: "async", ...(size === undefined ? {} : { width: size.width, height: size.height }), ...(className === undefined ? {} : { className }), ...(style === undefined ? {} : { style }) }));
223
+ }
224
+ /**
225
+ * Which comparisons this build can actually offer.
226
+ *
227
+ * Derived from what the run kept rather than offered unconditionally: a mode
228
+ * whose image does not exist renders a broken frame, and a broken frame in a
229
+ * review surface reads as a subject that renders to nothing.
230
+ */
231
+ export function modesFor(subject) {
232
+ const modes = [];
233
+ if (subject.has.after && subject.size !== undefined && subject.regions.length > 0) {
234
+ modes.push('regions');
235
+ }
236
+ if (subject.has.before && subject.has.after) {
237
+ modes.push('wipe', 'blend', 'blink', 'side-by-side');
238
+ }
239
+ if (subject.has.diff)
240
+ modes.push('difference');
241
+ if (modes.length === 0 && subject.has.after)
242
+ modes.push('regions');
243
+ return modes;
244
+ }
245
+ export { RegionOverlay, RegionTable } from './regions.js';
246
+ //# sourceMappingURL=viewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer.js","sourceRoot":"","sources":["../../src/ui/viewer.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactElement } from 'react';\nimport type { SubjectView } from '../review.js';\nimport type { ReviewClient } from './client.js';\nimport { RegionOverlay, RegionTable } from './regions.js';\nimport { count, magnitude, number } from './text.js';\n\n/**\n * Looking at the change: the stage, the modes, and the zoom that makes any of it\n * mean anything.\n *\n * Apart from [`review.tsx`](./review.tsx) because the two answer different\n * questions and are read in that order. The docket decides *what is worth looking\n * at* and is the part of this surface that refuses the category's habit;\n * everything here is the looking itself, and is reached only once that question\n * has been answered.\n *\n * Three positions this file exists to hold:\n *\n * **Every layer is drawn at one scale.** The plate has a width and each raster\n * fills it, so the baseline and the candidate are always the same magnification.\n * A wipe between two images at different scales is not a comparison; it is two\n * pictures with a line between them, and it reads as a change everywhere the line\n * happens to be.\n *\n * **Fit-to-width is a starting position, not the only one.** A route capture is\n * 1280 pixels wide and arrives in a pane about half that, so a two-pixel shift is\n * drawn at one pixel — under the resampler, which is to say not drawn. The whole\n * reason the sheet refuses smoothing is so that a reviewer can go to 1:1 and see\n * two pixels; without a zoom that refusal protects nothing.\n *\n * **A reviewer is taken to the finding.** The run measured where the differences\n * are. On nine thousand pixels of route, asking somebody to scroll until they\n * notice one is asking them to redo the measurement by eye — so the regions are\n * a list you step through, and the stage scrolls to each.\n */\n\nexport type ViewerMode = 'regions' | 'wipe' | 'blend' | 'blink' | 'side-by-side' | 'difference';\n\n/** `'fit'` scales the plate to the pane; a number is that multiple of 1:1. */\ntype Zoom = 'fit' | 1 | 2 | 4;\n\nconst ZOOMS: readonly Zoom[] = ['fit', 1, 2, 4];\n\n/**\n * What each mode is called on the button.\n *\n * The union members are identifiers and two of them read as identifiers. A\n * reviewer is not debugging this surface; nothing on it should ask them to\n * translate.\n */\nconst LABELS: Readonly<Record<ViewerMode, string>> = {\n regions: 'regions',\n wipe: 'wipe',\n blend: 'blend',\n blink: 'blink',\n 'side-by-side': 'side by side',\n difference: 'difference',\n};\n\n/** A raster's own pixels — never a rendered size, which the zoom decides. */\nexport type Size = { readonly width: number; readonly height: number };\n\n/** Long enough to read either state, short enough that the eye holds both. */\nconst BLINK_MS = 700;\n\nexport function Viewer({\n client,\n build,\n subject,\n sourced,\n}: {\n readonly client: ReviewClient;\n readonly build: string;\n readonly subject: SubjectView;\n /** Whether the run resolved any source file — see {@link RegionTable}. */\n readonly sourced?: boolean | undefined;\n}): ReactElement {\n const available = useMemo(() => modesFor(subject), [subject]);\n const [mode, setMode] = useState<ViewerMode>(available[0] ?? 'regions');\n const [seam, setSeam] = useState(50);\n const [blend, setBlend] = useState(50);\n const [zoom, setZoom] = useState<Zoom>('fit');\n const [focus, setFocus] = useState<number | null>(null);\n const [flip, setFlip] = useState(true);\n const [pull, setPull] = useState(0);\n const [still] = useState(calm);\n const stage = useRef<HTMLDivElement>(null);\n\n /**\n * The frame both readings are drawn in.\n *\n * The union of the two captures, which is what the comparison itself measured\n * in — it pads to the union box, so the regions are already in these\n * coordinates. Drawing each image at its own share of this box is the whole\n * point: a baseline stretched to the candidate's width is a width change\n * resampled into a hairline, and a width change is one of the largest things\n * that can happen to a page.\n */\n const box = union(subject.size, subject.baseline);\n\n // The alternation, driven here rather than by a keyframe: the stylesheet is\n // pasted into somebody else's page and is held to hiding nothing, and a reader\n // who never chose this mode should not have an animation running in it.\n useEffect(() => {\n if (mode !== 'blink' || still) return;\n const timer = setInterval(() => setFlip((was) => !was), BLINK_MS);\n return () => clearInterval(timer);\n }, [mode, still]);\n\n // What survives the move to another subject and what does not. Mode and\n // magnification are the reviewer's; a region number belongs to the picture it\n // was counted in, and a mode this subject cannot make would render a frame with\n // no image behind it.\n useEffect(() => {\n setFocus(null);\n setPull(0);\n setMode((was) => (available.includes(was) ? was : (available[0] ?? 'regions')));\n stage.current?.scrollTo({ left: 0, top: 0 });\n }, [available]);\n\n useEffect(() => {\n if (pull === 0 || focus === null) return;\n const region = subject.regions[focus];\n const pane = stage.current;\n if (region === undefined || pane === null) return;\n const at = zoom === 'fit' ? 1 : zoom;\n pane.scrollTo({\n left: Math.max(0, (region.x + region.width / 2) * at - pane.clientWidth / 2),\n top: Math.max(0, (region.y + region.height / 2) * at - pane.clientHeight / 2),\n behavior: still ? 'auto' : 'smooth',\n });\n // Deliberately keyed on the count alone: two jumps to the same region are two\n // requests, and the zoom this reads is already the one the plate was drawn at.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [pull]);\n\n // Every hook is above this line, and has to be: without a `key` forcing a\n // remount, moving from a subject with images to one without would otherwise\n // change how many hooks this component calls.\n if (available.length === 0) {\n return <p className=\"va-note\">This run kept no images for this subject.</p>;\n }\n\n const size = subject.size;\n const url = (kind: 'before' | 'after' | 'diff'): string =>\n client.imageUrl(build, subject.subject, kind);\n\n /**\n * Centre a region in the pane.\n *\n * At fit the plate is smaller than the capture and there is nothing to scroll\n * to, so this raises the zoom first — the request is *show me this*, and\n * showing it at a third of size is answering a different question.\n *\n * The scroll itself is deferred to the effect below rather than done here.\n * Raising the zoom changes the plate's height, and a scroll issued against the\n * old height is clamped to it: from fit to 1:1 the plate shrinks by whatever the\n * pane was scaling it, and the pane lands short of the region by that factor.\n */\n const jump = (index: number): void => {\n if (subject.regions[index] === undefined || box === undefined) return;\n setFocus(index);\n if (zoom === 'fit') setZoom(1);\n setPull((count) => count + 1);\n };\n\n const zoomable = box !== undefined;\n /**\n * Fit is a ceiling rather than a stretch. A 390-wide capture in a 700-wide pane\n * has nothing to gain from being drawn at 700: the run refuses smoothing, so the\n * only thing an upscale adds is blocks the render never had. Below its own width\n * the plate shrinks to the pane; at or above it, it stops.\n *\n * The ratio is declared rather than left to the images, because the images are\n * taken out of flow the moment there are two of them at different sizes — and a\n * plate with no height is a plate the region boxes have nothing to sit on.\n */\n const plate: CSSProperties =\n box === undefined\n ? {}\n : {\n aspectRatio: `${String(box.width)} / ${String(box.height)}`,\n ...(zoom === 'fit'\n ? { maxWidth: `${String(box.width)}px` }\n : { width: `${String(box.width * zoom)}px` }),\n };\n const boxes =\n mode === 'side-by-side' ? null : (\n <RegionOverlay subject={subject} box={box} focus={focus} onFocus={setFocus} />\n );\n\n return (\n <div className=\"va-viewer\">\n <div className=\"va-viewer-bar\">\n <div className=\"va-viewer-controls\">\n <p className=\"va-modes\">\n {available.map((option) => (\n <button\n key={option}\n type=\"button\"\n className={option === mode ? 'va-mode va-current' : 'va-mode'}\n aria-pressed={option === mode}\n onClick={() => setMode(option)}\n >\n {LABELS[option]}\n </button>\n ))}\n </p>\n {zoomable ? (\n <p className=\"va-zooms\">\n {ZOOMS.map((option) => (\n <button\n key={String(option)}\n type=\"button\"\n className={option === zoom ? 'va-mode va-current' : 'va-mode'}\n aria-pressed={option === zoom}\n onClick={() => setZoom(option)}\n >\n {option === 'fit' ? 'fit' : `${String(option)}×`}\n </button>\n ))}\n </p>\n ) : null}\n </div>\n <div className=\"va-viewer-readout\">\n <p className=\"va-showing\">{showing(mode, { seam, blend, flip })}</p>\n {size === undefined ? null : (\n <p className=\"va-measure\" title=\"the candidate’s own pixels, which 1× draws one for one\">\n {number(size.width)} × {number(size.height)}\n {subject.baseline === undefined ||\n (subject.baseline.width === size.width &&\n subject.baseline.height === size.height) ? (\n ''\n ) : (\n <span className=\"va-resized\">\n {' '}\n · was {number(subject.baseline.width)} × {number(subject.baseline.height)}\n </span>\n )}\n </p>\n )}\n {subject.regions.length > 0 ? (\n <p className=\"va-steps\">\n <button type=\"button\" onClick={() => jump(step(focus, subject.regions.length, -1))}>\n ‹\n </button>\n <span className=\"va-num\">\n {focus === null ? '—' : String(focus + 1)} / {String(subject.regions.length)}\n </span>\n <button type=\"button\" onClick={() => jump(step(focus, subject.regions.length, 1))}>\n ›\n </button>\n </p>\n ) : null}\n </div>\n </div>\n\n {mode === 'side-by-side' ? (\n <div className=\"va-side-by-side\">\n <figure>\n <figcaption>baseline</figcaption>\n <Raster src={url('before')} alt={`${subject.subject} baseline`} />\n </figure>\n <figure>\n <figcaption>this build</figcaption>\n <Raster src={url('after')} alt={`${subject.subject} candidate`} size={size} />\n </figure>\n </div>\n ) : (\n <div className=\"va-loupe\" ref={stage}>\n <div className={box === undefined ? 'va-plate' : 'va-plate va-boxed'} style={plate}>\n {mode === 'wipe' ? (\n <input\n type=\"range\"\n className=\"va-seam\"\n min={0}\n max={100}\n value={seam}\n aria-label=\"wipe between baseline and candidate\"\n onChange={(event) => setSeam(Number(event.target.value))}\n />\n ) : null}\n {mode === 'difference' ? (\n // Already in the union's coordinates: the mask is what the\n // comparison drew, on the box it padded both captures to.\n <Raster src={url('diff')} alt={`${subject.subject} difference mask`} />\n ) : (\n <>\n {subject.has.before ? (\n <Raster\n className=\"va-under\"\n src={url('before')}\n alt={`${subject.subject} baseline`}\n style={share(subject.baseline, box)}\n />\n ) : null}\n <Raster\n src={url('after')}\n alt={`${subject.subject} candidate`}\n size={size}\n style={{ ...share(size, box), ...over(mode, { seam, blend, flip }) }}\n />\n </>\n )}\n {boxes}\n {mode === 'wipe' ? (\n <span className=\"va-seam-line\" aria-hidden=\"true\" style={{ left: `${String(seam)}%` }} />\n ) : null}\n </div>\n </div>\n )}\n\n {mode === 'blink' && still ? (\n <p className=\"va-note\">\n This display asked for reduced motion, so the two readings do not alternate on their\n own.{' '}\n <button type=\"button\" className=\"va-mode\" onClick={() => setFlip((was) => !was)}>\n show {flip ? 'the baseline' : 'this build'}\n </button>\n </p>\n ) : null}\n\n {mode === 'blend' ? (\n <input\n type=\"range\"\n className=\"va-blend\"\n min={0}\n max={100}\n value={blend}\n aria-label=\"fade between baseline and candidate\"\n onChange={(event) => setBlend(Number(event.target.value))}\n />\n ) : null}\n\n <p className=\"va-pixels\">\n {magnitude(subject)}\n {subject.truncated === undefined ? null : (\n <>\n {' '}\n · {count(subject.truncated.regions, 'further region')} ({number(\n subject.truncated.pixels,\n )}px) were not recorded\n </>\n )}\n {subject.missingFonts === undefined || subject.missingFonts.length === 0 ? null : (\n <> · fonts missing at render: {subject.missingFonts.join(', ')}</>\n )}\n </p>\n\n <RegionTable\n subject={subject}\n sourced={sourced}\n focus={focus}\n onFocus={setFocus}\n onJump={jump}\n />\n </div>\n );\n}\n\n/**\n * How the candidate is revealed, given the mode.\n *\n * One layer and one declaration, rather than a wrapper that clips: a clip path\n * moves the seam without changing the box, so the candidate keeps its own height\n * whatever the baseline underneath rendered to — which is frequently the change.\n */\nfunction over(\n mode: ViewerMode,\n at: { readonly seam: number; readonly blend: number; readonly flip: boolean },\n): CSSProperties {\n if (mode === 'wipe') return { clipPath: `inset(0 0 0 ${String(at.seam)}%)` };\n if (mode === 'blend') return { opacity: at.blend / 100 };\n if (mode === 'blink') return { opacity: at.flip ? 1 : 0 };\n return {};\n}\n\n/** The box that contains both captures, when either of them is known. */\nexport function union(\n candidate: Size | undefined,\n baseline: Size | undefined,\n): Size | undefined {\n if (candidate === undefined) return baseline;\n if (baseline === undefined) return candidate;\n return {\n width: Math.max(candidate.width, baseline.width),\n height: Math.max(candidate.height, baseline.height),\n };\n}\n\n/**\n * One layer's share of the plate, as a percentage.\n *\n * A percentage rather than a pixel width so that nothing here has to know the\n * magnification: the plate is sized once and every layer scales with it. Absent\n * when this layer's own size was never recorded, which leaves the stylesheet's\n * `width: 100%` — the old behaviour, and the honest one, since a layer nobody\n * measured cannot be placed against one that was.\n */\nexport function share(own: Size | undefined, box: Size | undefined): CSSProperties | undefined {\n if (own === undefined || box === undefined || own.width === box.width) return undefined;\n return { width: `${String((own.width / box.width) * 100)}%` };\n}\n\n/**\n * Whether this display has asked for less movement.\n *\n * Read once, at mount, so a render is consistent with itself; `matchMedia` is\n * absent in a static render, where there is no interval and no scroll to suppress\n * and the answer is therefore no. What it governs is not decoration — blink *is*\n * the alternation — so the mode is not withdrawn, it is handed over to the\n * reviewer to step.\n */\nfunction calm(): boolean {\n return globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;\n}\n\n/** Which reading is on screen, said in the bar where a reviewer is looking. */\nfunction showing(\n mode: ViewerMode,\n at: { readonly seam: number; readonly blend: number; readonly flip: boolean },\n): string {\n if (mode === 'wipe') return `baseline ◀ ${String(Math.round(at.seam))}% ▶ this build`;\n if (mode === 'blend') return `${String(Math.round(at.blend))}% this build`;\n if (mode === 'blink') return at.flip ? 'this build' : 'baseline';\n if (mode === 'difference') return 'difference mask';\n return 'this build';\n}\n\n/** The next region to visit, wrapping, from nothing at either end. */\nfunction step(focus: number | null, total: number, by: 1 | -1): number {\n if (focus === null) return by === 1 ? 0 : total - 1;\n return (focus + by + total) % total;\n}\n\n/**\n * One stored raster, fetched when it is about to be looked at.\n *\n * `loading=\"lazy\"`, because a build page is a docket of every subject and a\n * reviewer reads one at a time. A route suite's candidates are full-page: this\n * project's four routes come to nine thousand pixels of height each, and twenty\n * of them decoded at once is tens of thousands of rows of bitmap in one document\n * — enough to stall the renderer before the first row of the table can be read.\n * Nothing above the fold needs any of them.\n *\n * `size` reserves the box before the bytes arrive, and is passed only where the\n * dimensions belong to *this* raster — the candidate. A baseline may have been a\n * different height (that is frequently the change), and a diff mask is not\n * measured at all, so those are deferred without a reservation rather than\n * reserved wrongly. A wrong reservation is worse than none: the page settles at\n * one height and then jumps.\n */\nfunction Raster({\n src,\n alt,\n size,\n className,\n style,\n}: {\n readonly src: string;\n readonly alt: string;\n readonly size?: { readonly width: number; readonly height: number } | undefined;\n readonly className?: string | undefined;\n readonly style?: CSSProperties | undefined;\n}): ReactElement {\n return (\n <img\n src={src}\n alt={alt}\n loading=\"lazy\"\n decoding=\"async\"\n {...(size === undefined ? {} : { width: size.width, height: size.height })}\n {...(className === undefined ? {} : { className })}\n {...(style === undefined ? {} : { style })}\n />\n );\n}\n\n/**\n * Which comparisons this build can actually offer.\n *\n * Derived from what the run kept rather than offered unconditionally: a mode\n * whose image does not exist renders a broken frame, and a broken frame in a\n * review surface reads as a subject that renders to nothing.\n */\nexport function modesFor(subject: SubjectView): readonly ViewerMode[] {\n const modes: ViewerMode[] = [];\n if (subject.has.after && subject.size !== undefined && subject.regions.length > 0) {\n modes.push('regions');\n }\n if (subject.has.before && subject.has.after) {\n modes.push('wipe', 'blend', 'blink', 'side-by-side');\n }\n if (subject.has.diff) modes.push('difference');\n if (modes.length === 0 && subject.has.after) modes.push('regions');\n return modes;\n}\n\nexport { RegionOverlay, RegionTable } from './regions.js';\n"]}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Which of the two secrets a request presented, whether the deployment has two of
3
+ * them at all, and whether the one presented may do what was asked.
4
+ *
5
+ * Apart from the router because none of it is about routes. Read together it is
6
+ * the whole authentication story — the check that runs before anything is parsed,
7
+ * the comparison that must not leak, the length below which a shared secret is a
8
+ * public one, and the capability rule that runs *after* a route is chosen. The
9
+ * argument for why there are two tokens rather than one lives in
10
+ * [`worker.ts`](./worker.ts), where an operator configuring a deployment meets it
11
+ * first.
12
+ */
13
+ /** Which of the two secrets a request presented, or nothing at all. */
14
+ export type Granted = 'ingest' | 'review';
15
+ /**
16
+ * The one sentence a caller holding neither token ever gets.
17
+ *
18
+ * Identical for a missing token, a wrong token, and a path that does not exist.
19
+ * Any variation between those three is an oracle.
20
+ */
21
+ export declare const UNAUTHENTICATED = "a valid bearer token is required";
22
+ /**
23
+ * The two secrets, and nothing else this file has any business reading.
24
+ *
25
+ * Structural rather than `TribunalOptions` on purpose: authentication cannot be
26
+ * made to depend on a project name or a retention window by a later edit, because
27
+ * it cannot see them.
28
+ */
29
+ interface Secrets {
30
+ readonly ingestToken: string;
31
+ readonly reviewToken: string;
32
+ }
33
+ /**
34
+ * Which token was presented, compared in constant time.
35
+ *
36
+ * Both are hashed to a fixed 32 bytes before comparison and the comparison never
37
+ * exits early. `===` on strings leaks the length of the common prefix through
38
+ * timing; comparing raw strings of different lengths leaks the token's length. A
39
+ * digest makes every comparison the same shape whatever arrives.
40
+ *
41
+ * Both candidates are always checked, even after the first one matches, so that
42
+ * "which token is this" costs the same either way.
43
+ */
44
+ export declare function grant(request: Request, secrets: Secrets): Promise<Granted | null>;
45
+ export declare function refuseWeakTokens(secrets: Secrets): void;
46
+ export declare function requires(granted: Granted, needed: Granted, path: string): void;
47
+ export {};
48
+ //# sourceMappingURL=worker-auth.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { Forbidden } from './worker-http.js';
2
+ /** The shortest token this will start with. A short shared secret is a public one. */
3
+ const MIN_TOKEN = 16;
4
+ /**
5
+ * The one sentence a caller holding neither token ever gets.
6
+ *
7
+ * Identical for a missing token, a wrong token, and a path that does not exist.
8
+ * Any variation between those three is an oracle.
9
+ */
10
+ export const UNAUTHENTICATED = 'a valid bearer token is required';
11
+ /**
12
+ * Which token was presented, compared in constant time.
13
+ *
14
+ * Both are hashed to a fixed 32 bytes before comparison and the comparison never
15
+ * exits early. `===` on strings leaks the length of the common prefix through
16
+ * timing; comparing raw strings of different lengths leaks the token's length. A
17
+ * digest makes every comparison the same shape whatever arrives.
18
+ *
19
+ * Both candidates are always checked, even after the first one matches, so that
20
+ * "which token is this" costs the same either way.
21
+ */
22
+ export async function grant(request, secrets) {
23
+ const header = request.headers.get('authorization');
24
+ if (header === null)
25
+ return null;
26
+ const match = /^Bearer (.+)$/i.exec(header.trim());
27
+ const presented = match?.[1];
28
+ if (presented === undefined)
29
+ return null;
30
+ const digest = await fingerprint(presented);
31
+ const isIngest = equal(digest, await fingerprint(secrets.ingestToken));
32
+ const isReview = equal(digest, await fingerprint(secrets.reviewToken));
33
+ return isIngest ? 'ingest' : isReview ? 'review' : null;
34
+ }
35
+ async function fingerprint(token) {
36
+ return new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(token)));
37
+ }
38
+ function equal(left, right) {
39
+ if (left.length !== right.length)
40
+ return false;
41
+ let difference = 0;
42
+ for (let index = 0; index < left.length; index += 1) {
43
+ difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
44
+ }
45
+ return difference === 0;
46
+ }
47
+ export function refuseWeakTokens(secrets) {
48
+ for (const [name, token] of [
49
+ ['ingestToken', secrets.ingestToken],
50
+ ['reviewToken', secrets.reviewToken],
51
+ ]) {
52
+ if (token.trim().length < MIN_TOKEN) {
53
+ throw new Error(`\`${name}\` is shorter than ${MIN_TOKEN} characters. This deployment holds every ` +
54
+ 'baseline and every observation a project has produced, and it can promote one; a ' +
55
+ 'guessable shared secret in front of that is not a configuration mistake anybody ' +
56
+ 'notices until it matters');
57
+ }
58
+ }
59
+ if (secrets.ingestToken === secrets.reviewToken) {
60
+ throw new Error('the ingest token and the review token are the same value, so this deployment has one ' +
61
+ 'secret and not two. The separation is the whole point: the ingest token lives in CI ' +
62
+ 'configuration, and approving promotes a baseline — anything that can read a build log ' +
63
+ 'would be able to approve a regression');
64
+ }
65
+ }
66
+ export function requires(granted, needed, path) {
67
+ if (granted === needed)
68
+ return;
69
+ throw new Forbidden(`${path} is served to the ${needed} token and this request presented the ${granted} one. ` +
70
+ (needed === 'review'
71
+ ? 'Deciding promotes a baseline, so it is not something a build log can do'
72
+ : 'Writing to this deployment is something CI does, not something a reviewer does'));
73
+ }
74
+ //# sourceMappingURL=worker-auth.js.map