react-cheminfo 0.40.1 → 0.41.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 (48) hide show
  1. package/bin/check-deploy.mjs +3 -1
  2. package/lib/deploy/core/buildGate.d.ts +18 -0
  3. package/lib/deploy/core/buildGate.d.ts.map +1 -0
  4. package/lib/deploy/core/buildGate.js +115 -0
  5. package/lib/deploy/core/buildGate.js.map +1 -0
  6. package/lib/deploy/core/checkDeploy.d.ts +2 -1
  7. package/lib/deploy/core/checkDeploy.d.ts.map +1 -1
  8. package/lib/deploy/core/checkDeploy.js +4 -1
  9. package/lib/deploy/core/checkDeploy.js.map +1 -1
  10. package/lib/deploy/core/index.d.ts +1 -0
  11. package/lib/deploy/core/index.d.ts.map +1 -1
  12. package/lib/deploy/core/index.js +1 -0
  13. package/lib/deploy/core/index.js.map +1 -1
  14. package/lib/deploy/core/publishedImage.d.ts.map +1 -1
  15. package/lib/deploy/core/publishedImage.js +5 -1
  16. package/lib/deploy/core/publishedImage.js.map +1 -1
  17. package/lib/deploy/core/types.d.ts +1 -1
  18. package/lib/deploy/core/types.d.ts.map +1 -1
  19. package/lib/deploy/core/types.js.map +1 -1
  20. package/lib/periodic/ui/ElementCell.d.ts +1 -1
  21. package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
  22. package/lib/periodic/ui/ElementCell.js +12 -11
  23. package/lib/periodic/ui/ElementCell.js.map +1 -1
  24. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
  25. package/lib/periodic/ui/PeriodicTable.js +25 -8
  26. package/lib/periodic/ui/PeriodicTable.js.map +1 -1
  27. package/lib/periodic/ui/PeriodicTableChrome.d.ts.map +1 -1
  28. package/lib/periodic/ui/PeriodicTableChrome.js +3 -2
  29. package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -1
  30. package/lib/periodic/ui/unit.d.ts +41 -0
  31. package/lib/periodic/ui/unit.d.ts.map +1 -0
  32. package/lib/periodic/ui/unit.js +45 -0
  33. package/lib/periodic/ui/unit.js.map +1 -0
  34. package/lib/share/ui/SharePartRow.d.ts.map +1 -1
  35. package/lib/share/ui/SharePartRow.js +2 -1
  36. package/lib/share/ui/SharePartRow.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/deploy/core/buildGate.ts +122 -0
  39. package/src/deploy/core/checkDeploy.ts +5 -1
  40. package/src/deploy/core/index.ts +1 -0
  41. package/src/deploy/core/publishedImage.ts +5 -1
  42. package/src/deploy/core/types.ts +5 -1
  43. package/src/periodic/ui/ElementCell.tsx +14 -12
  44. package/src/periodic/ui/PeriodicTable.tsx +25 -8
  45. package/src/periodic/ui/PeriodicTableChrome.tsx +4 -2
  46. package/src/periodic/ui/unit.ts +48 -0
  47. package/src/share/ui/SharePartRow.tsx +6 -1
  48. package/styles/chrome.css +15 -6
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The length the whole table is drawn in multiples of.
3
+ *
4
+ * Every part of the drawing — the height of a row, the type in a cell, the
5
+ * band the inner-transition series were lifted out across — is a share of the
6
+ * table's own width, so the same table reads at 280px beside a chart and fills
7
+ * a lecture-hall screen at twice that.
8
+ *
9
+ * The share is a measured length rather than a container-query unit, because
10
+ * WebKit resolves `cqw` against the container's *zoomed* width and then applies
11
+ * the zoom to the result a second time: inside a host that zooms the page — a
12
+ * PowerPoint add-in frame runs at 1.34 — every cq length came out 34% too
13
+ * large, which fits none of the floors the type is held above and overran the
14
+ * cells. `ResizeObserver` reports the content box in the element's own CSS
15
+ * pixels, which is the space `font-size` is resolved in, so one hundredth of
16
+ * that number is the share, on every engine and at any zoom.
17
+ * `getBoundingClientRect` is not: it reports the zoomed width, which would
18
+ * reproduce the defect.
19
+ *
20
+ * `1cqw` stays as the fallback, so a render that never measures — a server
21
+ * render, a prerendered page before its script runs — is the drawing it has
22
+ * always been.
23
+ */
24
+ /** The custom property the measured share is published as. */
25
+ export declare const UNIT_PROPERTY = "--periodic-unit";
26
+ /** One per cent of the table's width, as a CSS length. */
27
+ export declare const UNIT = "var(--periodic-unit, 1cqw)";
28
+ /**
29
+ * A share of the table's width, as a CSS length.
30
+ * @param share - The share, in per cent of the width.
31
+ * @returns A CSS length, usable wherever a length is.
32
+ */
33
+ export declare function ofWidth(share: number): string;
34
+ /**
35
+ * The value the custom property takes for a table of the given width.
36
+ * @param width - The content-box width, in CSS pixels, as `ResizeObserver`
37
+ * reports it.
38
+ * @returns The length one per cent of that width is.
39
+ */
40
+ export declare function unitValue(width: number): string;
41
+ //# sourceMappingURL=unit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unit.d.ts","sourceRoot":"","sources":["../../../src/periodic/ui/unit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,8DAA8D;AAC9D,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAE/C,0DAA0D;AAC1D,eAAO,MAAM,IAAI,+BAAgC,CAAC;AAElD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The length the whole table is drawn in multiples of.
3
+ *
4
+ * Every part of the drawing — the height of a row, the type in a cell, the
5
+ * band the inner-transition series were lifted out across — is a share of the
6
+ * table's own width, so the same table reads at 280px beside a chart and fills
7
+ * a lecture-hall screen at twice that.
8
+ *
9
+ * The share is a measured length rather than a container-query unit, because
10
+ * WebKit resolves `cqw` against the container's *zoomed* width and then applies
11
+ * the zoom to the result a second time: inside a host that zooms the page — a
12
+ * PowerPoint add-in frame runs at 1.34 — every cq length came out 34% too
13
+ * large, which fits none of the floors the type is held above and overran the
14
+ * cells. `ResizeObserver` reports the content box in the element's own CSS
15
+ * pixels, which is the space `font-size` is resolved in, so one hundredth of
16
+ * that number is the share, on every engine and at any zoom.
17
+ * `getBoundingClientRect` is not: it reports the zoomed width, which would
18
+ * reproduce the defect.
19
+ *
20
+ * `1cqw` stays as the fallback, so a render that never measures — a server
21
+ * render, a prerendered page before its script runs — is the drawing it has
22
+ * always been.
23
+ */
24
+ /** The custom property the measured share is published as. */
25
+ export const UNIT_PROPERTY = '--periodic-unit';
26
+ /** One per cent of the table's width, as a CSS length. */
27
+ export const UNIT = `var(${UNIT_PROPERTY}, 1cqw)`;
28
+ /**
29
+ * A share of the table's width, as a CSS length.
30
+ * @param share - The share, in per cent of the width.
31
+ * @returns A CSS length, usable wherever a length is.
32
+ */
33
+ export function ofWidth(share) {
34
+ return `calc(${UNIT} * ${String(share)})`;
35
+ }
36
+ /**
37
+ * The value the custom property takes for a table of the given width.
38
+ * @param width - The content-box width, in CSS pixels, as `ResizeObserver`
39
+ * reports it.
40
+ * @returns The length one per cent of that width is.
41
+ */
42
+ export function unitValue(width) {
43
+ return `${(width / 100).toFixed(3)}px`;
44
+ }
45
+ //# sourceMappingURL=unit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unit.js","sourceRoot":"","sources":["../../../src/periodic/ui/unit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,8DAA8D;AAC9D,MAAM,CAAC,MAAM,aAAa,GAAG,iBAAiB,CAAC;AAE/C,0DAA0D;AAC1D,MAAM,CAAC,MAAM,IAAI,GAAG,OAAO,aAAa,SAAS,CAAC;AAElD;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,QAAQ,IAAI,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;AAC5C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"SharePartRow.d.ts","sourceRoot":"","sources":["../../../src/share/ui/SharePartRow.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAM1C;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,sCAAsC;IACtC,YAAY,EAAE,qBAAqB,CAAC;IACpC,kDAAkD;IAClD,QAAQ,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,IAAI,CAAC;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,CAkCnE"}
1
+ {"version":3,"file":"SharePartRow.d.ts","sourceRoot":"","sources":["../../../src/share/ui/SharePartRow.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAM1C;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,sCAAsC;IACtC,YAAY,EAAE,qBAAqB,CAAC;IACpC,kDAAkD;IAClD,QAAQ,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,IAAI,CAAC;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,CAuCnE"}
@@ -18,7 +18,8 @@ export function SharePartRow(props) {
18
18
  const { label, description, on, descriptions, onChange, onPointed } = props;
19
19
  const t = useChromeT();
20
20
  const id = useId();
21
- return (_jsxs("div", { className: "share-part", onMouseEnter: () => onPointed?.(true), onMouseLeave: () => onPointed?.(false), onFocus: () => onPointed?.(true), onBlur: () => onPointed?.(false), children: [_jsx("label", { className: "share-part__label", htmlFor: id, children: label }), descriptions === 'help' ? (_jsx(HelpIcon, { content: { title: label, body: description }, label: label })) : null, _jsx(Switch, { id: id, className: "share-part__switch", checked: on, innerLabel: t('share.off'), innerLabelChecked: t('share.on'), onChange: (event) => {
21
+ const labelId = `${id}-label`;
22
+ return (_jsxs("div", { className: "share-part", onMouseEnter: () => onPointed?.(true), onMouseLeave: () => onPointed?.(false), onFocus: () => onPointed?.(true), onBlur: () => onPointed?.(false), children: [_jsx("label", { className: "share-part__label", htmlFor: id, id: labelId, children: label }), descriptions === 'help' ? (_jsx(HelpIcon, { content: { title: label, body: description }, label: label })) : null, _jsx(Switch, { id: id, className: "share-part__switch", "aria-labelledby": labelId, checked: on, innerLabel: t('share.off'), innerLabelChecked: t('share.on'), onChange: (event) => {
22
23
  onChange(event.currentTarget.checked);
23
24
  } }), descriptions === 'inline' ? (_jsx("p", { className: "share-part__description", children: description })) : null] }));
24
25
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SharePartRow.js","sourceRoot":"","sources":["../../../src/share/ui/SharePartRow.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AA2BnD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAC5E,MAAM,CAAC,GAAG,UAAU,EAAE,CAAC;IACvB,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IAEnB,OAAO,CACL,eACE,SAAS,EAAC,YAAY,EACtB,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,EACrC,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,EACtC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,EAChC,MAAM,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,aAEhC,gBAAO,SAAS,EAAC,mBAAmB,EAAC,OAAO,EAAE,EAAE,YAC7C,KAAK,GACA,EACP,YAAY,KAAK,MAAM,CAAC,CAAC,CAAC,CACzB,KAAC,QAAQ,IAAC,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,KAAK,GAAI,CACzE,CAAC,CAAC,CAAC,IAAI,EACR,KAAC,MAAM,IACL,EAAE,EAAE,EAAE,EACN,SAAS,EAAC,oBAAoB,EAC9B,OAAO,EAAE,EAAE,EACX,UAAU,EAAE,CAAC,CAAC,WAAW,CAAC,EAC1B,iBAAiB,EAAE,CAAC,CAAC,UAAU,CAAC,EAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;oBAClB,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;gBACxC,CAAC,GACD,EACD,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,CAC3B,YAAG,SAAS,EAAC,yBAAyB,YAAE,WAAW,GAAK,CACzD,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"SharePartRow.js","sourceRoot":"","sources":["../../../src/share/ui/SharePartRow.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AA2BnD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAC5E,MAAM,CAAC,GAAG,UAAU,EAAE,CAAC;IACvB,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IACnB,MAAM,OAAO,GAAG,GAAG,EAAE,QAAQ,CAAC;IAE9B,OAAO,CACL,eACE,SAAS,EAAC,YAAY,EACtB,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,EACrC,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,EACtC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,EAChC,MAAM,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,aAEhC,gBAAO,SAAS,EAAC,mBAAmB,EAAC,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,YAC1D,KAAK,GACA,EACP,YAAY,KAAK,MAAM,CAAC,CAAC,CAAC,CACzB,KAAC,QAAQ,IAAC,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,KAAK,GAAI,CACzE,CAAC,CAAC,CAAC,IAAI,EACR,KAAC,MAAM,IACL,EAAE,EAAE,EAAE,EACN,SAAS,EAAC,oBAAoB,qBAIb,OAAO,EACxB,OAAO,EAAE,EAAE,EACX,UAAU,EAAE,CAAC,CAAC,WAAW,CAAC,EAC1B,iBAAiB,EAAE,CAAC,CAAC,UAAU,CAAC,EAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;oBAClB,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;gBACxC,CAAC,GACD,EACD,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,CAC3B,YAAG,SAAS,EAAC,yBAAyB,YAAE,WAAW,GAAK,CACzD,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-cheminfo",
3
- "version": "0.40.1",
3
+ "version": "0.41.0",
4
4
  "license": "MIT",
5
5
  "description": "React components, hooks and framework-free helpers shared by the cheminfo sites: site chrome, citations, sharing, routing, pedagogy, charts, and the structure, orbital and 3D molecule viewers",
6
6
  "author": "Luc Patiny",
@@ -0,0 +1,122 @@
1
+ import type { DeployProblem } from './types.ts';
2
+
3
+ /** The scripts a repository's own `package.json` declares, as npm would run them. */
4
+ type Scripts = Record<string, string>;
5
+
6
+ /**
7
+ * Report a repository that can bundle, and so publish, code nothing tested.
8
+ *
9
+ * A bundle is what gets published, so `npm run build` is the gate: the unit and
10
+ * static checks, then the browser suite, then `build-only`, which is the pure
11
+ * bundle. The split is what makes the gate possible at all — three callers need
12
+ * the bundle without the suites, and the image build is one of them: it has no
13
+ * browser, and a source build on a deploy host must not re-run a suite CI
14
+ * already ran. A Dockerfile left on `npm run build` therefore either dies in
15
+ * the container or, worse, the `build` it calls was never a gate and the image
16
+ * carries untested code to the registry. Neither is visible until a deploy.
17
+ * @param packageJson - The repository's root `package.json`, unparsed.
18
+ * @param dockerfile - Its Dockerfile, when it has one.
19
+ * @param problems - Where each problem found is appended.
20
+ */
21
+ export function readBuildGate(
22
+ packageJson: string | undefined,
23
+ dockerfile: string | undefined,
24
+ problems: DeployProblem[],
25
+ ): void {
26
+ if (packageJson === undefined) return;
27
+ const scripts = parseScripts(packageJson);
28
+ if (scripts === undefined) return;
29
+
30
+ const build = scripts.build;
31
+ if (build !== undefined && !/\bnpm run test\b/.test(build)) {
32
+ problems.push({
33
+ file: 'package.json',
34
+ line: lineOf(packageJson, '"build"'),
35
+ kind: 'ungated-build',
36
+ message:
37
+ 'build writes a bundle without running the tests, so a broken site can be published',
38
+ hint: 'build: "npm run test && npm run test-e2e && npm run build-only", with the bundle in build-only',
39
+ });
40
+ }
41
+ if (build !== undefined && scripts['build-only'] === undefined) {
42
+ problems.push({
43
+ file: 'package.json',
44
+ line: lineOf(packageJson, '"build"'),
45
+ kind: 'missing-build-only',
46
+ message: 'there is no build-only, so nothing can bundle without the gate',
47
+ hint: 'move the bundle to build-only and leave build as the gate in front of it',
48
+ });
49
+ }
50
+
51
+ const test = scripts.test;
52
+ if (test !== undefined && /\bnpm run test-e2e\b/.test(test)) {
53
+ problems.push({
54
+ file: 'package.json',
55
+ line: lineOf(packageJson, '"test"'),
56
+ kind: 'test-runs-e2e',
57
+ message:
58
+ 'test runs the browser suite, which build runs next: it runs twice and a failure does not say which gate it came from',
59
+ hint: 'leave test-e2e out of test; build is "npm run test && npm run test-e2e && npm run build-only"',
60
+ });
61
+ }
62
+
63
+ if (dockerfile === undefined) return;
64
+ const line = gatedBuildLine(dockerfile);
65
+ if (line !== undefined) {
66
+ problems.push({
67
+ file: 'Dockerfile',
68
+ line,
69
+ kind: 'gated-image-build',
70
+ message:
71
+ 'the image build runs the gated build, which has no browser to run the browser suite in',
72
+ hint: 'RUN npm run build-only — the image builds what CI tested, it does not test again',
73
+ });
74
+ }
75
+ }
76
+
77
+ /**
78
+ * The line running the repository's own gated `build`, if the Dockerfile has
79
+ * one. A workspace build (`-w frontend`) is a different script and stays pure.
80
+ * @param dockerfile - The Dockerfile text.
81
+ * @returns Its line number, counting from 1, or undefined.
82
+ */
83
+ function gatedBuildLine(dockerfile: string): number | undefined {
84
+ const lines = dockerfile.split('\n');
85
+ for (const [index, text] of lines.entries()) {
86
+ if (!/^\s*RUN\s.*\bnpm run build\b/.test(text)) continue;
87
+ if (/\bnpm run build[\w:-]/.test(text)) continue;
88
+ if (/(?:-w|--workspace)[\s=]/.test(text)) continue;
89
+ return index + 1;
90
+ }
91
+ return undefined;
92
+ }
93
+
94
+ /**
95
+ * The scripts block of a `package.json`.
96
+ * @param text - The file, unparsed.
97
+ * @returns Its scripts, or undefined when the file is not readable as one.
98
+ */
99
+ function parseScripts(text: string): Scripts | undefined {
100
+ let parsed: unknown;
101
+ try {
102
+ parsed = JSON.parse(text);
103
+ } catch {
104
+ return undefined;
105
+ }
106
+ if (typeof parsed !== 'object' || parsed === null) return undefined;
107
+ const scripts = (parsed as { scripts?: unknown }).scripts;
108
+ if (typeof scripts !== 'object' || scripts === null) return {};
109
+ return scripts as Scripts;
110
+ }
111
+
112
+ /**
113
+ * Where a report should point, so the reader lands on the script itself.
114
+ * @param text - The file to search.
115
+ * @param needle - The key to find.
116
+ * @returns Its line, counting from 1, or 0 when it is not there.
117
+ */
118
+ function lineOf(text: string, needle: string): number {
119
+ const at = text.indexOf(needle);
120
+ if (at === -1) return 0;
121
+ return text.slice(0, at).split('\n').length;
122
+ }
@@ -1,3 +1,4 @@
1
+ import { readBuildGate } from './buildGate.ts';
1
2
  import { readComposeFile } from './composeFile.ts';
2
3
  import { readEnvExample } from './envExample.ts';
3
4
  import { readPageCache } from './pageCache.ts';
@@ -18,7 +19,8 @@ import { DEPLOY_COMPOSE_FILES } from './types.ts';
18
19
  * reports it healthy. So a repository has to hand it a selectable tag on every
19
20
  * image we build, an `IMAGE_NAME` that names the package our own workflow
20
21
  * publishes, a compose file that parses on a checkout with no `.env` yet, and a
21
- * health probe to read. It also has to publish that image in the first place:
22
+ * health probe to read, and a `build` that cannot write a bundle until the
23
+ * tests have passed. It also has to publish that image in the first place:
22
24
  * with no `latest` to pull the script builds from source on the deploy host,
23
25
  * and the install that follows must survive a blip in the registry. It also has to hand the browser a page the browser will
24
26
  * not keep, or the next deploy leaves returning visitors on a blank one. Each
@@ -84,6 +86,8 @@ export function findDeployProblems(
84
86
  readInstallRetries(dockerfile, problems);
85
87
  }
86
88
 
89
+ readBuildGate(byPath.get('package.json'), dockerfile, problems);
90
+
87
91
  readPublishedImage(byPath.get(DOCKER_IMAGE_WORKFLOW), problems);
88
92
 
89
93
  const gitignore = byPath.get('.gitignore');
@@ -1,3 +1,4 @@
1
+ export { readBuildGate } from './buildGate.ts';
1
2
  export { findDeployProblems } from './checkDeploy.ts';
2
3
  export type {
3
4
  CheckDeployOptions,
@@ -53,7 +53,11 @@ export function readPublishedImage(
53
53
  }
54
54
 
55
55
  const value = match.groups.value ?? '';
56
- if (value.includes('github.ref_name') && !value.includes('event_name')) {
56
+ // Either guard keeps a branch name out: `event_name == 'push'` fires only for
57
+ // the tag push, and `ref_type == 'tag'` only when the ref is one. What is
58
+ // refused is the bare ref, which a manual run hands over as a branch.
59
+ const guarded = value.includes('event_name') || value.includes('ref_type');
60
+ if (value.includes('github.ref_name') && !guarded) {
57
61
  problems.push({
58
62
  file: DOCKER_IMAGE_WORKFLOW,
59
63
  line: lineOf(workflow, TAG_VERSION),
@@ -12,7 +12,11 @@ export type DeployProblemKind =
12
12
  | 'missing-deploy-ignore'
13
13
  | 'cacheable-page'
14
14
  | 'unpublishable-image'
15
- | 'brittle-install';
15
+ | 'brittle-install'
16
+ | 'ungated-build'
17
+ | 'missing-build-only'
18
+ | 'test-runs-e2e'
19
+ | 'gated-image-build';
16
20
 
17
21
  /** One way a repository would fail, or silently mis-deploy, on the server. */
18
22
  export interface DeployProblem {
@@ -5,7 +5,7 @@
5
5
  * A cell is a real `<button>`, so Tab reaches it, Enter and Space activate it,
6
6
  * and a screen reader reads the element's name rather than its symbol.
7
7
  *
8
- * Everything written in it is sized in container-query units, so a cell shows
8
+ * Everything written in it is a share of the table's width, so a cell shows
9
9
  * the same proportions at 280px beside a chart and across a lecture-hall
10
10
  * screen — the table a class reads is the same drawing, larger.
11
11
  */
@@ -16,6 +16,8 @@ import { useState } from 'react';
16
16
  import type { Swatch } from '../../color/core/interpolate.ts';
17
17
  import { useChromeT } from '../../i18n/ui/useT.ts';
18
18
 
19
+ import { UNIT, ofWidth } from './unit.ts';
20
+
19
21
  /** What {@link ElementCell} needs to draw one element. */
20
22
  export interface ElementCellProps {
21
23
  /** Atomic number, written small in the corner. */
@@ -120,8 +122,8 @@ export function ElementCell(props: ElementCellProps): ReactElement {
120
122
  outlineColor: swatch.foreground,
121
123
  // Thick enough to be seen from the back of a room, and drawn inside
122
124
  // the cell so a neighbour never covers it.
123
- outlineWidth: 'max(2px, 0.3cqw)',
124
- outlineOffset: 'max(-3px, -0.4cqw)',
125
+ outlineWidth: `max(2px, ${ofWidth(0.3)})`,
126
+ outlineOffset: `max(-3px, ${ofWidth(-0.4)})`,
125
127
  // The cell the pointer is on, for a class following a demonstration.
126
128
  filter: isHovered ? 'brightness(1.08)' : undefined,
127
129
  }}
@@ -154,7 +156,7 @@ export function ElementCell(props: ElementCellProps): ReactElement {
154
156
  * @returns A CSS length.
155
157
  */
156
158
  function symbolFontSize(symbol: string): string {
157
- return fitted(symbolWidthInEm(symbol), SYMBOL_CQW, '0.5rem');
159
+ return fitted(symbolWidthInEm(symbol), SYMBOL_CAP, '0.5rem');
158
160
  }
159
161
 
160
162
  /**
@@ -167,7 +169,7 @@ function symbolFontSize(symbol: string): string {
167
169
  * @returns A CSS length.
168
170
  */
169
171
  function detailFontSize(detail: string): string {
170
- return fitted(detailWidthInEm(detail), DETAIL_CQW, '0.25rem');
172
+ return fitted(detailWidthInEm(detail), DETAIL_CAP, '0.25rem');
171
173
  }
172
174
 
173
175
  /**
@@ -182,7 +184,7 @@ function detailFontSize(detail: string): string {
182
184
  * @returns A CSS length.
183
185
  */
184
186
  function fitted(widthInEm: number, cap: string, floor: string): string {
185
- const room = `calc((${CELL_CQW} - ${CELL_CHROME}) / ${widthInEm.toFixed(2)})`;
187
+ const room = `calc((${UNIT} * ${String(CELL_SHARE)} - ${CELL_CHROME}) / ${widthInEm.toFixed(2)})`;
186
188
  return `max(${floor}, min(${cap}, ${room}))`;
187
189
  }
188
190
 
@@ -222,7 +224,7 @@ function detailWidthInEm(detail: string): number {
222
224
  const TABULAR_EM = 0.6;
223
225
 
224
226
  /** The size the symbol is written at wherever it fits, as a share of the table. */
225
- const SYMBOL_CQW = '2.4cqw';
227
+ const SYMBOL_CAP = ofWidth(2.4);
226
228
 
227
229
  /**
228
230
  * The tallest the third line is ever written, as a share of the table.
@@ -231,7 +233,7 @@ const SYMBOL_CQW = '2.4cqw';
231
233
  * the value is what the table is being read for, and a value half the size of
232
234
  * the symbol is the one thing on the cell a class cannot make out.
233
235
  */
234
- const DETAIL_CQW = '1.6cqw';
236
+ const DETAIL_CAP = ofWidth(1.6);
235
237
 
236
238
  /**
237
239
  * The band at each end of a cell: the atomic number at the top, whatever the
@@ -241,10 +243,10 @@ const DETAIL_CQW = '1.6cqw';
241
243
  * sit at the centre of the cell rather than near it — a bottom band wider than
242
244
  * the top one by a third lifted every symbol in the table two pixels.
243
245
  */
244
- const EDGE_BAND = '1.8cqw';
246
+ const EDGE_BAND = ofWidth(1.8);
245
247
 
246
- /** How wide one column is, as a share of the table's width. */
247
- const CELL_CQW = '4.85cqw';
248
+ /** How wide one column is, in per cent of the table's width. */
249
+ const CELL_SHARE = 4.85;
248
250
 
249
251
  /** What the border and the padding of a cell take off that width. */
250
252
  const CELL_CHROME = '4px';
@@ -275,7 +277,7 @@ const cellStyle = {
275
277
 
276
278
  const numberStyle = {
277
279
  alignSelf: 'start',
278
- fontSize: 'max(0.33rem, 1.1cqw)',
280
+ fontSize: `max(0.33rem, ${ofWidth(1.1)})`,
279
281
  fontVariantNumeric: 'tabular-nums',
280
282
  justifySelf: 'start',
281
283
  lineHeight: 1,
@@ -19,6 +19,7 @@ import type {
19
19
  import { useEffect, useRef } from 'react';
20
20
 
21
21
  import type { Swatch } from '../../color/core/interpolate.ts';
22
+ import { useResizeObserver } from '../../hooks/ui/useResizeObserver.ts';
22
23
  import { useChromeT } from '../../i18n/ui/useT.ts';
23
24
  import { categorySwatch } from '../core/categories.ts';
24
25
  import type { PeriodicElement } from '../core/elements.ts';
@@ -36,6 +37,7 @@ import {
36
37
  HeaderStrips,
37
38
  InnerTransitionMarkers,
38
39
  } from './PeriodicTableChrome.tsx';
40
+ import { UNIT_PROPERTY, ofWidth, unitValue } from './unit.ts';
39
41
 
40
42
  /** What {@link PeriodicTable} needs. */
41
43
  export interface PeriodicTableProps {
@@ -147,10 +149,22 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
147
149
  } = props;
148
150
 
149
151
  const t = useChromeT();
152
+ const rootRef = useRef<HTMLDivElement>(null);
150
153
  const gridRef = useRef<HTMLDivElement>(null);
151
154
  const cameFromKeyRef = useRef(false);
152
155
  const offset = headers ? 1 : 0;
153
156
 
157
+ // The drawing is read off this one number, so it is written straight onto
158
+ // the element rather than held as state: a window being dragged would
159
+ // otherwise re-render all 118 cells per frame, and the engine recomputes
160
+ // every length from the custom property on its own. A width of zero is a
161
+ // table in a tab nobody has opened; the last measurement stands until it is
162
+ // shown, and the observer fires again then.
163
+ useResizeObserver(rootRef, ({ width }) => {
164
+ if (width === 0) return;
165
+ rootRef.current?.style.setProperty(UNIT_PROPERTY, unitValue(width));
166
+ });
167
+
154
168
  useEffect(() => {
155
169
  if (!cameFromKeyRef.current) return;
156
170
  cameFromKeyRef.current = false;
@@ -171,7 +185,7 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
171
185
  }
172
186
 
173
187
  return (
174
- <div className={className} style={rootStyle}>
188
+ <div ref={rootRef} className={className} style={rootStyle}>
175
189
  <div
176
190
  ref={gridRef}
177
191
  role="grid"
@@ -218,13 +232,13 @@ function insetStyle(offset: number): CSSProperties {
218
232
  display: 'flex',
219
233
  // A column of air on each side, so the writing reads as sitting in the
220
234
  // block rather than as running into the cells beside it.
221
- padding: '0 2.4cqw',
235
+ padding: `0 ${ofWidth(2.4)}`,
222
236
  gridColumn: `${String(EMPTY_BLOCK.column + offset)} / span ${String(EMPTY_BLOCK.columnSpan)}`,
223
237
  gridRow: `${String(EMPTY_BLOCK.row + offset)} / span ${String(EMPTY_BLOCK.rowSpan)}`,
224
238
  // A share of the table, with a floor, exactly as a cell sizes its symbol:
225
239
  // the block holds the same three rows at every width, so what is written
226
240
  // in it has to shrink with them.
227
- fontSize: 'max(0.6rem, 1.8cqw)',
241
+ fontSize: `max(0.6rem, ${ofWidth(1.8)})`,
228
242
  lineHeight: 1.35,
229
243
  minWidth: 0,
230
244
  overflow: 'hidden',
@@ -256,10 +270,10 @@ const MIN_WIDTH = 280;
256
270
  * table: a cell keeps the band it writes a value in whether or not that table
257
271
  * writes one.
258
272
  */
259
- const ROW_HEIGHT = '6.8cqw';
273
+ const ROW_HEIGHT = ofWidth(6.8);
260
274
 
261
275
  /** The band the two inner-transition series were lifted out across. */
262
- const SERIES_GAP = 'max(6px, 1.1cqw)';
276
+ const SERIES_GAP = `max(6px, ${ofWidth(1.1)})`;
263
277
 
264
278
  const rootStyle = {
265
279
  display: 'flex',
@@ -268,7 +282,10 @@ const rootStyle = {
268
282
  minWidth: MIN_WIDTH,
269
283
  // Everything inside — the height of a row as much as the type in a cell — is
270
284
  // a share of this box rather than of the page, so the same table reads at
271
- // 280px beside a chart and fills a lecture-hall screen at twice that.
285
+ // 280px beside a chart and fills a lecture-hall screen at twice that. The
286
+ // share is measured (see `unit.ts`); the container is what the `1cqw`
287
+ // fallback and a site's own `@container` rules are answered by, and it keeps
288
+ // this box's width independent of what is written inside it.
272
289
  containerType: 'inline-size',
273
290
  } as const satisfies CSSProperties;
274
291
 
@@ -297,7 +314,7 @@ function gridStyle(headers: boolean): CSSProperties {
297
314
  }
298
315
  return {
299
316
  ...baseGridStyle,
300
- gridTemplateColumns: `max(14px, 2.4cqw) repeat(${String(COLUMN_COUNT)}, minmax(0, 1fr))`,
301
- gridTemplateRows: `max(12px, 2cqw) ${rows}`,
317
+ gridTemplateColumns: `max(14px, ${ofWidth(2.4)}) repeat(${String(COLUMN_COUNT)}, minmax(0, 1fr))`,
318
+ gridTemplateRows: `max(12px, ${ofWidth(2)}) ${rows}`,
302
319
  };
303
320
  }
@@ -14,6 +14,8 @@ import {
14
14
  INNER_TRANSITION_ROWS,
15
15
  } from '../core/layout.ts';
16
16
 
17
+ import { ofWidth } from './unit.ts';
18
+
17
19
  /** What {@link HeaderStrips} needs. */
18
20
  interface HeaderStripsProps {
19
21
  /**
@@ -150,7 +152,7 @@ const headerStyle = {
150
152
  alignItems: 'center',
151
153
  color: TOKEN.textMuted,
152
154
  display: 'flex',
153
- fontSize: 'max(0.45rem, 1.35cqw)',
155
+ fontSize: `max(0.45rem, ${ofWidth(1.35)})`,
154
156
  justifyContent: 'center',
155
157
  padding: 0,
156
158
  } as const satisfies CSSProperties;
@@ -168,6 +170,6 @@ const markerStyle = {
168
170
  borderRadius: 3,
169
171
  color: TOKEN.textMuted,
170
172
  display: 'flex',
171
- fontSize: 'max(0.4rem, 1.15cqw)',
173
+ fontSize: `max(0.4rem, ${ofWidth(1.15)})`,
172
174
  justifyContent: 'center',
173
175
  } as const satisfies CSSProperties;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The length the whole table is drawn in multiples of.
3
+ *
4
+ * Every part of the drawing — the height of a row, the type in a cell, the
5
+ * band the inner-transition series were lifted out across — is a share of the
6
+ * table's own width, so the same table reads at 280px beside a chart and fills
7
+ * a lecture-hall screen at twice that.
8
+ *
9
+ * The share is a measured length rather than a container-query unit, because
10
+ * WebKit resolves `cqw` against the container's *zoomed* width and then applies
11
+ * the zoom to the result a second time: inside a host that zooms the page — a
12
+ * PowerPoint add-in frame runs at 1.34 — every cq length came out 34% too
13
+ * large, which fits none of the floors the type is held above and overran the
14
+ * cells. `ResizeObserver` reports the content box in the element's own CSS
15
+ * pixels, which is the space `font-size` is resolved in, so one hundredth of
16
+ * that number is the share, on every engine and at any zoom.
17
+ * `getBoundingClientRect` is not: it reports the zoomed width, which would
18
+ * reproduce the defect.
19
+ *
20
+ * `1cqw` stays as the fallback, so a render that never measures — a server
21
+ * render, a prerendered page before its script runs — is the drawing it has
22
+ * always been.
23
+ */
24
+
25
+ /** The custom property the measured share is published as. */
26
+ export const UNIT_PROPERTY = '--periodic-unit';
27
+
28
+ /** One per cent of the table's width, as a CSS length. */
29
+ export const UNIT = `var(${UNIT_PROPERTY}, 1cqw)`;
30
+
31
+ /**
32
+ * A share of the table's width, as a CSS length.
33
+ * @param share - The share, in per cent of the width.
34
+ * @returns A CSS length, usable wherever a length is.
35
+ */
36
+ export function ofWidth(share: number): string {
37
+ return `calc(${UNIT} * ${String(share)})`;
38
+ }
39
+
40
+ /**
41
+ * The value the custom property takes for a table of the given width.
42
+ * @param width - The content-box width, in CSS pixels, as `ResizeObserver`
43
+ * reports it.
44
+ * @returns The length one per cent of that width is.
45
+ */
46
+ export function unitValue(width: number): string {
47
+ return `${(width / 100).toFixed(3)}px`;
48
+ }
@@ -45,6 +45,7 @@ export function SharePartRow(props: SharePartRowProps): ReactElement {
45
45
  const { label, description, on, descriptions, onChange, onPointed } = props;
46
46
  const t = useChromeT();
47
47
  const id = useId();
48
+ const labelId = `${id}-label`;
48
49
 
49
50
  return (
50
51
  <div
@@ -54,7 +55,7 @@ export function SharePartRow(props: SharePartRowProps): ReactElement {
54
55
  onFocus={() => onPointed?.(true)}
55
56
  onBlur={() => onPointed?.(false)}
56
57
  >
57
- <label className="share-part__label" htmlFor={id}>
58
+ <label className="share-part__label" htmlFor={id} id={labelId}>
58
59
  {label}
59
60
  </label>
60
61
  {descriptions === 'help' ? (
@@ -63,6 +64,10 @@ export function SharePartRow(props: SharePartRowProps): ReactElement {
63
64
  <Switch
64
65
  id={id}
65
66
  className="share-part__switch"
67
+ // Blueprint wraps the input in a label of its own carrying the On/Off
68
+ // text, and a wrapping label beats an `htmlFor` one: without this the
69
+ // switch is named "OnOff" and a screen reader never says what is on.
70
+ aria-labelledby={labelId}
66
71
  checked={on}
67
72
  innerLabel={t('share.off')}
68
73
  innerLabelChecked={t('share.on')}