react-cheminfo 0.34.0 → 0.36.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 (225) hide show
  1. package/README.md +28 -28
  2. package/bin/check-seo.mjs +333 -0
  3. package/lib/about/core/about.d.ts +1 -30
  4. package/lib/about/core/about.d.ts.map +1 -1
  5. package/lib/about/core/about.js +0 -16
  6. package/lib/about/core/about.js.map +1 -1
  7. package/lib/about/core/index.d.ts +0 -1
  8. package/lib/about/core/index.d.ts.map +1 -1
  9. package/lib/about/core/index.js +0 -1
  10. package/lib/about/core/index.js.map +1 -1
  11. package/lib/about/ui/AboutBuild.d.ts +2 -5
  12. package/lib/about/ui/AboutBuild.d.ts.map +1 -1
  13. package/lib/about/ui/AboutBuild.js +7 -19
  14. package/lib/about/ui/AboutBuild.js.map +1 -1
  15. package/lib/about/ui/AboutPage.d.ts +2 -2
  16. package/lib/about/ui/AboutPage.d.ts.map +1 -1
  17. package/lib/about/ui/AboutPage.js +3 -14
  18. package/lib/about/ui/AboutPage.js.map +1 -1
  19. package/lib/about/ui/AboutVersion.d.ts +0 -4
  20. package/lib/about/ui/AboutVersion.d.ts.map +1 -1
  21. package/lib/about/ui/AboutVersion.js +3 -31
  22. package/lib/about/ui/AboutVersion.js.map +1 -1
  23. package/lib/download/core/index.d.ts +0 -5
  24. package/lib/download/core/index.d.ts.map +1 -1
  25. package/lib/download/core/index.js +0 -3
  26. package/lib/download/core/index.js.map +1 -1
  27. package/lib/download/ui/FigureDownload.d.ts +4 -55
  28. package/lib/download/ui/FigureDownload.d.ts.map +1 -1
  29. package/lib/download/ui/FigureDownload.js +6 -66
  30. package/lib/download/ui/FigureDownload.js.map +1 -1
  31. package/lib/download/ui/FigureDownloadPanel.d.ts +16 -2
  32. package/lib/download/ui/FigureDownloadPanel.d.ts.map +1 -1
  33. package/lib/download/ui/FigureDownloadPanel.js +11 -30
  34. package/lib/download/ui/FigureDownloadPanel.js.map +1 -1
  35. package/lib/download/ui/figureDownloadHint.d.ts +30 -0
  36. package/lib/download/ui/figureDownloadHint.d.ts.map +1 -0
  37. package/lib/download/ui/figureDownloadHint.js +33 -0
  38. package/lib/download/ui/figureDownloadHint.js.map +1 -0
  39. package/lib/download/ui/index.d.ts +3 -10
  40. package/lib/download/ui/index.d.ts.map +1 -1
  41. package/lib/download/ui/index.js +1 -5
  42. package/lib/download/ui/index.js.map +1 -1
  43. package/lib/download/ui/useFigureActions.d.ts +50 -0
  44. package/lib/download/ui/useFigureActions.d.ts.map +1 -0
  45. package/lib/download/ui/useFigureActions.js +58 -0
  46. package/lib/download/ui/useFigureActions.js.map +1 -0
  47. package/lib/download/ui/useFigureDownload.d.ts +85 -0
  48. package/lib/download/ui/useFigureDownload.d.ts.map +1 -0
  49. package/lib/download/ui/useFigureDownload.js +75 -0
  50. package/lib/download/ui/useFigureDownload.js.map +1 -0
  51. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  52. package/lib/ecosystem/core/sites.js +0 -40
  53. package/lib/ecosystem/core/sites.js.map +1 -1
  54. package/lib/ecosystem/core/types.d.ts +0 -10
  55. package/lib/ecosystem/core/types.d.ts.map +1 -1
  56. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  57. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  58. package/lib/ir/core/irCommands.d.ts +2 -2
  59. package/lib/ir/core/irCommands.js +2 -2
  60. package/lib/ir/core/irCommands.js.map +1 -1
  61. package/lib/ir/ui/IrAboutDialog.d.ts.map +1 -1
  62. package/lib/ir/ui/IrAboutDialog.js +1 -1
  63. package/lib/ir/ui/IrAboutDialog.js.map +1 -1
  64. package/lib/ir/ui/IrCanvas.d.ts +6 -0
  65. package/lib/ir/ui/IrCanvas.d.ts.map +1 -1
  66. package/lib/ir/ui/IrCanvas.js +2 -2
  67. package/lib/ir/ui/IrCanvas.js.map +1 -1
  68. package/lib/ir/ui/IrShell.d.ts.map +1 -1
  69. package/lib/ir/ui/IrShell.js +16 -11
  70. package/lib/ir/ui/IrShell.js.map +1 -1
  71. package/lib/ir/ui/IrToolbar.d.ts +11 -1
  72. package/lib/ir/ui/IrToolbar.d.ts.map +1 -1
  73. package/lib/ir/ui/IrToolbar.js +12 -2
  74. package/lib/ir/ui/IrToolbar.js.map +1 -1
  75. package/lib/ir/ui/index.d.ts +1 -0
  76. package/lib/ir/ui/index.d.ts.map +1 -1
  77. package/lib/ir/ui/index.js.map +1 -1
  78. package/lib/language/core/index.d.ts +2 -0
  79. package/lib/language/core/index.d.ts.map +1 -1
  80. package/lib/language/core/index.js +1 -0
  81. package/lib/language/core/index.js.map +1 -1
  82. package/lib/language/core/languagePath.d.ts +52 -0
  83. package/lib/language/core/languagePath.d.ts.map +1 -0
  84. package/lib/language/core/languagePath.js +65 -0
  85. package/lib/language/core/languagePath.js.map +1 -0
  86. package/lib/locales/de.json +2 -4
  87. package/lib/locales/en.json +2 -4
  88. package/lib/locales/es.json +2 -4
  89. package/lib/locales/fr.json +2 -4
  90. package/lib/locales/it.json +2 -4
  91. package/lib/overlay/ui/OverlayPanel.d.ts +6 -0
  92. package/lib/overlay/ui/OverlayPanel.d.ts.map +1 -1
  93. package/lib/overlay/ui/OverlayPanel.js +2 -2
  94. package/lib/overlay/ui/OverlayPanel.js.map +1 -1
  95. package/lib/overlay/ui/overlayPanelStyles.d.ts +2 -1
  96. package/lib/overlay/ui/overlayPanelStyles.d.ts.map +1 -1
  97. package/lib/overlay/ui/overlayPanelStyles.js +3 -1
  98. package/lib/overlay/ui/overlayPanelStyles.js.map +1 -1
  99. package/lib/periodic/ui/ElementCell.d.ts +0 -6
  100. package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
  101. package/lib/periodic/ui/ElementCell.js +34 -30
  102. package/lib/periodic/ui/ElementCell.js.map +1 -1
  103. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
  104. package/lib/periodic/ui/PeriodicTable.js +12 -15
  105. package/lib/periodic/ui/PeriodicTable.js.map +1 -1
  106. package/lib/seo/core/alternates.d.ts +40 -0
  107. package/lib/seo/core/alternates.d.ts.map +1 -0
  108. package/lib/seo/core/alternates.js +42 -0
  109. package/lib/seo/core/alternates.js.map +1 -0
  110. package/lib/seo/core/index.d.ts +3 -0
  111. package/lib/seo/core/index.d.ts.map +1 -1
  112. package/lib/seo/core/index.js +2 -0
  113. package/lib/seo/core/index.js.map +1 -1
  114. package/lib/seo/core/noscript.d.ts +9 -0
  115. package/lib/seo/core/noscript.d.ts.map +1 -1
  116. package/lib/seo/core/noscript.js +7 -2
  117. package/lib/seo/core/noscript.js.map +1 -1
  118. package/lib/seo/core/pageMeta.d.ts +23 -0
  119. package/lib/seo/core/pageMeta.d.ts.map +1 -1
  120. package/lib/seo/core/pageMeta.js +52 -3
  121. package/lib/seo/core/pageMeta.js.map +1 -1
  122. package/lib/seo/core/pageProse.d.ts +76 -0
  123. package/lib/seo/core/pageProse.d.ts.map +1 -0
  124. package/lib/seo/core/pageProse.js +92 -0
  125. package/lib/seo/core/pageProse.js.map +1 -0
  126. package/lib/seo/core/routeProblems.d.ts +33 -0
  127. package/lib/seo/core/routeProblems.d.ts.map +1 -0
  128. package/lib/seo/core/routeProblems.js +89 -0
  129. package/lib/seo/core/routeProblems.js.map +1 -0
  130. package/lib/seo/core/routes.d.ts +14 -0
  131. package/lib/seo/core/routes.d.ts.map +1 -1
  132. package/lib/seo/core/routes.js.map +1 -1
  133. package/lib/seo/core/siteFiles.d.ts.map +1 -1
  134. package/lib/seo/core/siteFiles.js +2 -1
  135. package/lib/seo/core/siteFiles.js.map +1 -1
  136. package/lib/seo/vite/prerender.d.ts +13 -0
  137. package/lib/seo/vite/prerender.d.ts.map +1 -1
  138. package/lib/seo/vite/prerender.js +17 -10
  139. package/lib/seo/vite/prerender.js.map +1 -1
  140. package/package.json +2 -1
  141. package/src/about/core/about.ts +1 -51
  142. package/src/about/core/index.ts +0 -1
  143. package/src/about/ui/AboutBuild.tsx +8 -37
  144. package/src/about/ui/AboutPage.tsx +8 -64
  145. package/src/about/ui/AboutVersion.tsx +5 -45
  146. package/src/download/core/index.ts +0 -25
  147. package/src/download/ui/FigureDownload.tsx +11 -148
  148. package/src/download/ui/FigureDownloadPanel.tsx +46 -45
  149. package/src/download/ui/figureDownloadHint.ts +52 -0
  150. package/src/download/ui/index.ts +6 -10
  151. package/src/download/ui/useFigureActions.ts +113 -0
  152. package/src/download/ui/useFigureDownload.tsx +191 -0
  153. package/src/ecosystem/core/sites.ts +0 -40
  154. package/src/ecosystem/core/types.ts +0 -10
  155. package/src/ir/core/irCommands.ts +2 -2
  156. package/src/ir/ui/IrAboutDialog.tsx +0 -13
  157. package/src/ir/ui/IrCanvas.tsx +8 -2
  158. package/src/ir/ui/IrShell.tsx +23 -25
  159. package/src/ir/ui/IrToolbar.tsx +36 -6
  160. package/src/ir/ui/index.ts +1 -0
  161. package/src/language/core/index.ts +2 -0
  162. package/src/language/core/languagePath.ts +78 -0
  163. package/src/locales/de.json +2 -4
  164. package/src/locales/en.json +2 -4
  165. package/src/locales/es.json +2 -4
  166. package/src/locales/fr.json +2 -4
  167. package/src/locales/it.json +2 -4
  168. package/src/overlay/ui/OverlayPanel.tsx +8 -2
  169. package/src/overlay/ui/overlayPanelStyles.ts +3 -0
  170. package/src/periodic/ui/ElementCell.tsx +34 -37
  171. package/src/periodic/ui/PeriodicTable.tsx +12 -17
  172. package/src/seo/core/alternates.ts +65 -0
  173. package/src/seo/core/index.ts +3 -0
  174. package/src/seo/core/noscript.ts +18 -2
  175. package/src/seo/core/pageMeta.ts +74 -3
  176. package/src/seo/core/pageProse.ts +146 -0
  177. package/src/seo/core/routeProblems.ts +111 -0
  178. package/src/seo/core/routes.ts +14 -0
  179. package/src/seo/core/siteFiles.ts +2 -1
  180. package/src/seo/vite/prerender.ts +30 -11
  181. package/lib/about/core/repository.d.ts +0 -14
  182. package/lib/about/core/repository.d.ts.map +0 -1
  183. package/lib/about/core/repository.js +0 -21
  184. package/lib/about/core/repository.js.map +0 -1
  185. package/lib/download/core/exportStyles.d.ts +0 -22
  186. package/lib/download/core/exportStyles.d.ts.map +0 -1
  187. package/lib/download/core/exportStyles.js +0 -19
  188. package/lib/download/core/exportStyles.js.map +0 -1
  189. package/lib/download/core/imageResolution.d.ts +0 -88
  190. package/lib/download/core/imageResolution.d.ts.map +0 -1
  191. package/lib/download/core/imageResolution.js +0 -77
  192. package/lib/download/core/imageResolution.js.map +0 -1
  193. package/lib/download/core/pictureExport.d.ts +0 -93
  194. package/lib/download/core/pictureExport.d.ts.map +0 -1
  195. package/lib/download/core/pictureExport.js +0 -116
  196. package/lib/download/core/pictureExport.js.map +0 -1
  197. package/lib/download/ui/ExportDialogFrame.d.ts +0 -46
  198. package/lib/download/ui/ExportDialogFrame.d.ts.map +0 -1
  199. package/lib/download/ui/ExportDialogFrame.js +0 -24
  200. package/lib/download/ui/ExportDialogFrame.js.map +0 -1
  201. package/lib/download/ui/ExportImageDialog.d.ts +0 -47
  202. package/lib/download/ui/ExportImageDialog.d.ts.map +0 -1
  203. package/lib/download/ui/ExportImageDialog.js +0 -25
  204. package/lib/download/ui/ExportImageDialog.js.map +0 -1
  205. package/lib/download/ui/ExportNameField.d.ts +0 -29
  206. package/lib/download/ui/ExportNameField.d.ts.map +0 -1
  207. package/lib/download/ui/ExportNameField.js +0 -99
  208. package/lib/download/ui/ExportNameField.js.map +0 -1
  209. package/lib/download/ui/ExportPictureSection.d.ts +0 -38
  210. package/lib/download/ui/ExportPictureSection.d.ts.map +0 -1
  211. package/lib/download/ui/ExportPictureSection.js +0 -48
  212. package/lib/download/ui/ExportPictureSection.js.map +0 -1
  213. package/lib/download/ui/useDrawingBox.d.ts +0 -23
  214. package/lib/download/ui/useDrawingBox.d.ts.map +0 -1
  215. package/lib/download/ui/useDrawingBox.js +0 -21
  216. package/lib/download/ui/useDrawingBox.js.map +0 -1
  217. package/src/about/core/repository.ts +0 -22
  218. package/src/download/core/exportStyles.ts +0 -20
  219. package/src/download/core/imageResolution.ts +0 -118
  220. package/src/download/core/pictureExport.ts +0 -207
  221. package/src/download/ui/ExportDialogFrame.tsx +0 -86
  222. package/src/download/ui/ExportImageDialog.tsx +0 -91
  223. package/src/download/ui/ExportNameField.tsx +0 -148
  224. package/src/download/ui/ExportPictureSection.tsx +0 -142
  225. package/src/download/ui/useDrawingBox.ts +0 -37
@@ -86,10 +86,6 @@
86
86
  "about.whatYouCanDo": "Cosa si può fare qui",
87
87
  "about.builtOn": "Costruito su",
88
88
  "about.howToCite": "Come citare",
89
- "about.licenceAndSource": "Licenza e codice sorgente",
90
- "about.licence": "{license}, © cheminfo. Si può usare in un corso, farne un fork, o riprenderne un pezzo per altro. Il codice è su {sources}.",
91
- "about.foundAProblem": "Ha trovato un problema?",
92
- "about.reportIt": "Ce lo dica: una segnalazione che indichi cosa ha scritto e cosa ha ottenuto è la correzione più rapida che esista. {tracker}.",
93
89
  "about.citeWork": "Citare {what}",
94
90
  "about.built": "Compilato {builtAt}.",
95
91
  "about.builtFromCommit": "Compilato {builtAt} dal commit {commit}.",
@@ -189,6 +185,8 @@
189
185
  "download.hintRasterSvg": "Un SVG che contiene un'immagine di {pixels}: una scena 3D non ha forma vettoriale.",
190
186
  "download.hintSvg": "Si apre a {pixels}, e resta nitido a qualsiasi dimensione.",
191
187
  "download.hintPng": "Salvato {pixels}.",
188
+ "download.hintCopied": "Copiato negli appunti: PNG di {pixels}.",
189
+ "download.copyUnsupported": "Questo browser non può mettere un'immagine negli appunti. Salvala invece.",
192
190
  "download.size": "Dimensione",
193
191
  "download.sizeHelp": "La forma con cui la figura viene disegnata per il file. Viene ricomposta, così gli assi e le etichette si adattano a quella forma invece di essere stirati. Una colonna di rivista è larga 8,5 cm, e il suo testo si stampa alla dimensione con cui si legge sullo schermo.",
194
192
  "download.sizeScreen": "Come mostrata",
@@ -78,6 +78,12 @@ export interface OverlayPanelProps {
78
78
  * @default derived from the type size — 88 at the compact size
79
79
  */
80
80
  nameWidth?: number;
81
+ /**
82
+ * Fixed width of the panel, for one whose hint changes with its choices:
83
+ * left to its content, it would resize under the pointer at every pick.
84
+ * @default undefined — as wide as its content
85
+ */
86
+ width?: number | string;
81
87
  /**
82
88
  * Value of the `data-testid` attribute of the panel.
83
89
  * @default undefined
@@ -104,7 +110,7 @@ export interface OverlayPanelProps {
104
110
  export function OverlayPanel(props: OverlayPanelProps): ReactElement {
105
111
  const { title, children, onReset, resetLabel, help } = props;
106
112
  const t = useChromeT();
107
- const { actions, hint, nameWidth, testId } = props;
113
+ const { actions, hint, nameWidth, width: panelWidth, testId } = props;
108
114
  const { metrics } = useOverlaySurface();
109
115
  const titleId = useId();
110
116
  const reset = useOverlayInteraction();
@@ -120,7 +126,7 @@ export function OverlayPanel(props: OverlayPanelProps): ReactElement {
120
126
  role="group"
121
127
  aria-labelledby={titleId}
122
128
  data-testid={testId}
123
- style={overlayPanelSurfaceStyle(metrics)}
129
+ style={overlayPanelSurfaceStyle(metrics, panelWidth)}
124
130
  >
125
131
  <div style={overlayPanelHeaderStyle(metrics)}>
126
132
  <span style={overlayPanelTitleStyle(metrics)}>
@@ -26,14 +26,17 @@ import { overlayFocusRing } from './overlayValueStyles.ts';
26
26
  * The panel itself: a header, a body, and whatever the figure can be told to
27
27
  * do at the foot.
28
28
  * @param metrics - The measurements the card is drawn from.
29
+ * @param width - A fixed width, or `undefined` for as wide as its content.
29
30
  * @returns The panel's rules.
30
31
  */
31
32
  export function overlayPanelSurfaceStyle(
32
33
  metrics: OverlayMetrics,
34
+ width?: number | string,
33
35
  ): CSSProperties {
34
36
  return {
35
37
  display: 'flex',
36
38
  flexDirection: 'column',
39
+ width,
37
40
  minWidth: OVERLAY_PANEL_MIN_WIDTH,
38
41
  color: 'var(--text)',
39
42
  fontSize: metrics.fontSize,
@@ -37,12 +37,6 @@ export interface ElementCellProps {
37
37
  * @default '' — nothing is written
38
38
  */
39
39
  detail?: string;
40
- /**
41
- * Whether the table this cell belongs to writes a third line at all. A table
42
- * that writes none gives the room to the symbol, and its rows are shorter.
43
- * @default whether this cell has one
44
- */
45
- writesDetail?: boolean;
46
40
  /**
47
41
  * Whether the cell is the one the tools are pointed at.
48
42
  * @default false
@@ -82,7 +76,6 @@ export function ElementCell(props: ElementCellProps): ReactElement {
82
76
  row,
83
77
  onSelect,
84
78
  detail = '',
85
- writesDetail = detail !== '',
86
79
  isSelected = false,
87
80
  isIncluded = true,
88
81
  onHover,
@@ -137,7 +130,7 @@ export function ElementCell(props: ElementCellProps): ReactElement {
137
130
  <span
138
131
  style={{
139
132
  ...symbolStyle,
140
- fontSize: symbolFontSize(symbol, writesDetail),
133
+ fontSize: symbolFontSize(symbol),
141
134
  }}
142
135
  >
143
136
  {symbol}
@@ -158,12 +151,10 @@ export function ElementCell(props: ElementCellProps): ReactElement {
158
151
  * ones whose second letter is an `m` — which are written a tenth smaller
159
152
  * rather than run into the edges of their cell.
160
153
  * @param symbol - The chemical symbol.
161
- * @param writesDetail - Whether the table writes a third line under it.
162
154
  * @returns A CSS length.
163
155
  */
164
- function symbolFontSize(symbol: string, writesDetail: boolean): string {
165
- const cap = writesDetail ? SYMBOL_CQW : SYMBOL_ALONE_CQW;
166
- return fitted(symbolWidthInEm(symbol), cap, '0.5rem');
156
+ function symbolFontSize(symbol: string): string {
157
+ return fitted(symbolWidthInEm(symbol), SYMBOL_CQW, '0.5rem');
167
158
  }
168
159
 
169
160
  /**
@@ -233,17 +224,24 @@ const TABULAR_EM = 0.6;
233
224
  /** The size the symbol is written at wherever it fits, as a share of the table. */
234
225
  const SYMBOL_CQW = '2.4cqw';
235
226
 
236
- /** The same, in a table that writes nothing under the symbol. */
237
- const SYMBOL_ALONE_CQW = '2.8cqw';
238
-
239
227
  /**
240
228
  * The tallest the third line is ever written, as a share of the table.
241
229
  *
242
- * Close to three quarters of the symbol: the symbol is how a cell is found,
243
- * but the value is what the table is being read for, and a value half the size
244
- * of the symbol is the one thing on the cell a class cannot make out.
230
+ * Close to two thirds of the symbol: the symbol is how a cell is found, but
231
+ * the value is what the table is being read for, and a value half the size of
232
+ * the symbol is the one thing on the cell a class cannot make out.
233
+ */
234
+ const DETAIL_CQW = '1.6cqw';
235
+
236
+ /**
237
+ * The band at each end of a cell: the atomic number at the top, whatever the
238
+ * tool writes at the bottom.
239
+ *
240
+ * They are one length, not two, and that is what makes the symbol between them
241
+ * sit at the centre of the cell rather than near it — a bottom band wider than
242
+ * the top one by a third lifted every symbol in the table two pixels.
245
243
  */
246
- const DETAIL_CQW = '1.75cqw';
244
+ const EDGE_BAND = '1.8cqw';
247
245
 
248
246
  /** How wide one column is, as a share of the table's width. */
249
247
  const CELL_CQW = '4.85cqw';
@@ -251,49 +249,48 @@ const CELL_CQW = '4.85cqw';
251
249
  /** What the border and the padding of a cell take off that width. */
252
250
  const CELL_CHROME = '4px';
253
251
 
252
+ /**
253
+ * A cell is three bands of its own, and every cell has all three.
254
+ *
255
+ * The atomic number is in the top band, against its left edge; the symbol is
256
+ * centred in the middle one; whatever the tool writes is centred in the
257
+ * bottom one. The bands keep their height whether or not anything is written
258
+ * in the last of them, so a number sits at one height across the whole table
259
+ * and a symbol at one other — before this, both rode on a stack the cell
260
+ * centred and moved whenever a neighbour's value was written smaller.
261
+ */
254
262
  const cellStyle = {
255
- alignItems: 'center',
256
263
  border: '1px solid rgb(255 255 255 / 0.55)',
257
264
  borderRadius: 3,
258
265
  cursor: 'pointer',
259
- display: 'flex',
260
- flexDirection: 'column',
266
+ display: 'grid',
261
267
  font: 'inherit',
262
- justifyContent: 'center',
268
+ gridTemplateRows: `${EDGE_BAND} 1fr ${EDGE_BAND}`,
269
+ justifyItems: 'center',
263
270
  minWidth: 0,
264
271
  overflow: 'hidden',
265
272
  padding: '1px',
266
- // The corner the atomic number is pinned into.
267
- position: 'relative',
268
273
  transition: 'opacity 120ms ease, filter 120ms ease',
269
274
  } as const satisfies CSSProperties;
270
275
 
271
- /**
272
- * The atomic number sits out of the flow, in the corner.
273
- *
274
- * In the flow it rode on top of a stack the cell centres, so it moved down
275
- * whenever what is written under the symbol was written smaller — the numbers
276
- * of a row then sat at as many heights as the row had word lengths. Out of it,
277
- * the number is in the same corner of every cell, and the symbol is centred in
278
- * the cell rather than in what is left of it.
279
- */
280
276
  const numberStyle = {
277
+ alignSelf: 'start',
281
278
  fontSize: 'max(0.33rem, 1.1cqw)',
282
279
  fontVariantNumeric: 'tabular-nums',
283
- left: 'max(1px, 0.14cqw)',
280
+ justifySelf: 'start',
284
281
  lineHeight: 1,
285
282
  opacity: 0.8,
286
- position: 'absolute',
287
- top: 'max(1px, 0.14cqw)',
288
283
  } as const satisfies CSSProperties;
289
284
 
290
285
  const symbolStyle = {
286
+ alignSelf: 'center',
291
287
  fontWeight: 700,
292
288
  letterSpacing: '-0.02em',
293
289
  lineHeight: 1.02,
294
290
  } as const satisfies CSSProperties;
295
291
 
296
292
  const detailStyle = {
293
+ alignSelf: 'center',
297
294
  fontVariantNumeric: 'tabular-nums',
298
295
  lineHeight: 1,
299
296
  maxWidth: '100%',
@@ -147,7 +147,6 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
147
147
  } = props;
148
148
 
149
149
  const t = useChromeT();
150
- const writesDetail = detailOf !== undefined;
151
150
  const gridRef = useRef<HTMLDivElement>(null);
152
151
  const cameFromKeyRef = useRef(false);
153
152
  const offset = headers ? 1 : 0;
@@ -178,7 +177,7 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
178
177
  role="grid"
179
178
  aria-label={t('periodic.table')}
180
179
  data-testid="periodic-table"
181
- style={gridStyle(headers, writesDetail)}
180
+ style={gridStyle(headers)}
182
181
  onKeyDown={handleKeyDown}
183
182
  >
184
183
  {headers ? <HeaderStrips onSelectRange={onSelectRange} /> : null}
@@ -193,7 +192,6 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
193
192
  symbol={element.symbol}
194
193
  name={nameOf(element)}
195
194
  detail={detailOf?.(element)}
196
- writesDetail={writesDetail}
197
195
  swatch={swatchOf(element)}
198
196
  isSelected={element.symbol === selected}
199
197
  isIncluded={isIncluded?.(element)}
@@ -218,8 +216,9 @@ function insetStyle(offset: number): CSSProperties {
218
216
  return {
219
217
  alignItems: 'center',
220
218
  display: 'flex',
221
- // Narrower than the block so the writing never touches the cells beside it.
222
- padding: '0 1cqw',
219
+ // A column of air on each side, so the writing reads as sitting in the
220
+ // block rather than as running into the cells beside it.
221
+ padding: '0 2.4cqw',
223
222
  gridColumn: `${String(EMPTY_BLOCK.column + offset)} / span ${String(EMPTY_BLOCK.columnSpan)}`,
224
223
  gridRow: `${String(EMPTY_BLOCK.row + offset)} / span ${String(EMPTY_BLOCK.rowSpan)}`,
225
224
  // A share of the table, with a floor, exactly as a cell sizes its symbol:
@@ -251,14 +250,13 @@ const MIN_WIDTH = 280;
251
250
  /**
252
251
  * How tall one row of elements is, as a share of the table's width.
253
252
  *
254
- * A column is about 5.4% of that width, so a cell is a seventh taller than it
255
- * is wide — the proportion of a wall chart, and the room the three lines of a
256
- * cell need to be written at a size a class can read.
253
+ * A column is about 5.2% of that width, so a cell is a third taller than it is
254
+ * wide — the proportion of a wall chart, and the room the three bands of a
255
+ * cell need to be read from the back of a room. It is the same height in every
256
+ * table: a cell keeps the band it writes a value in whether or not that table
257
+ * writes one.
257
258
  */
258
- const ROW_HEIGHT = '6.1cqw';
259
-
260
- /** The same, in a table that writes nothing under the symbol. */
261
- const ROW_HEIGHT_ALONE = '4.9cqw';
259
+ const ROW_HEIGHT = '6.8cqw';
262
260
 
263
261
  /** The band the two inner-transition series were lifted out across. */
264
262
  const SERIES_GAP = 'max(6px, 1.1cqw)';
@@ -285,14 +283,11 @@ const baseGridStyle = {
285
283
  * The grid the cells are placed on.
286
284
  * @param headers - Whether a leading column and row hold the period and group
287
285
  * numbers.
288
- * @param writesDetail - Whether a cell writes a third line, which is what a
289
- * row is tall enough for.
290
286
  * @returns The style of the grid.
291
287
  */
292
- function gridStyle(headers: boolean, writesDetail: boolean): CSSProperties {
293
- const row = writesDetail ? ROW_HEIGHT : ROW_HEIGHT_ALONE;
288
+ function gridStyle(headers: boolean): CSSProperties {
294
289
  // The eighth row is the gap the inner-transition series are lifted out into.
295
- const rows = `repeat(7, ${row}) ${SERIES_GAP} repeat(2, ${row})`;
290
+ const rows = `repeat(7, ${ROW_HEIGHT}) ${SERIES_GAP} repeat(2, ${ROW_HEIGHT})`;
296
291
  if (!headers) {
297
292
  return {
298
293
  ...baseGridStyle,
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The tags that tell a search engine five addresses are one page written five
3
+ * times.
4
+ *
5
+ * Without them a translated site is read as five sites that happen to look
6
+ * alike: the French tutorial competes with the English one instead of being
7
+ * offered to a French reader, and a search engine picks one of them to keep.
8
+ * With them each address is served to the audience it was written for, and the
9
+ * four it was not stop diluting it.
10
+ *
11
+ * Every page names **every** language including itself — a set that does not
12
+ * list the page it is on is ignored whole — plus `x-default`, which is where a
13
+ * reader whose language we do not speak is sent.
14
+ */
15
+
16
+ import type { Language } from '../../i18n/core/languages.ts';
17
+ import { DEFAULT_LANGUAGE } from '../../i18n/core/languages.ts';
18
+ import { withLanguagePath } from '../../language/core/languagePath.ts';
19
+ import { escapeAttribute } from '../../share/core/escape.ts';
20
+
21
+ /** Which page, in which languages, served from where. */
22
+ export interface AlternateOptions {
23
+ /**
24
+ * Where the site is served, mount path included and no trailing slash, e.g.
25
+ * `https://3d.cheminfo.org`.
26
+ */
27
+ origin: string;
28
+ /**
29
+ * The page, from the site's own root and with no language prefix on it, e.g.
30
+ * `/tutorial`.
31
+ */
32
+ path: string;
33
+ /**
34
+ * Every language the site is written in. One language alone means there is
35
+ * nothing to point at, and no tag is written.
36
+ */
37
+ languages: readonly Language[];
38
+ }
39
+
40
+ /**
41
+ * The `rel="alternate"` block for one page of a translated site.
42
+ * @param options - The page, its languages and where the site is served.
43
+ * @returns The tags, one per line, or `''` when the site speaks one language.
44
+ */
45
+ export function alternateLinkTags(options: AlternateOptions): string {
46
+ const { origin, path, languages } = options;
47
+ if (languages.length < 2) return '';
48
+
49
+ const tags: string[] = [];
50
+ for (const language of languages) {
51
+ tags.push(linkTag(language, address(origin, language, path)));
52
+ }
53
+ // A reader we have no language for gets the unprefixed address, which is the
54
+ // one every link handed out before the site was translated already points at.
55
+ tags.push(linkTag('x-default', address(origin, DEFAULT_LANGUAGE, path)));
56
+ return tags.join('\n');
57
+ }
58
+
59
+ function address(origin: string, language: Language, path: string): string {
60
+ return `${origin}${withLanguagePath(language, path)}`;
61
+ }
62
+
63
+ function linkTag(hreflang: string, href: string): string {
64
+ return `<link rel="alternate" hreflang="${escapeAttribute(hreflang)}" href="${escapeAttribute(href)}" />`;
65
+ }
@@ -10,10 +10,13 @@ export type {
10
10
  export { noscriptIndex } from './noscript.ts';
11
11
  export type { PageMetaOptions } from './pageMeta.ts';
12
12
  export { injectPageMeta, pageDocumentMeta, pageHeadTags } from './pageMeta.ts';
13
+ export type { PageContent, PageSection, PageTable } from './pageProse.ts';
14
+ export { pageProseHtml } from './pageProse.ts';
13
15
  export type { PlainDescriptionOptions } from './plainDescription.ts';
14
16
  export { plainDescription, plainProse } from './plainDescription.ts';
15
17
  export type { RobotsDisallow } from './robots.ts';
16
18
  export { robotsTxt } from './robots.ts';
19
+ export { routeProblems, withheldPhrase } from './routeProblems.ts';
17
20
  export type { RouteMeta } from './routes.ts';
18
21
  export { assertRoutes, homeRoute, pageMetaFor, routeFor } from './routes.ts';
19
22
  export type { SiteFilesOptions } from './siteFiles.ts';
@@ -12,6 +12,8 @@ import { ECOSYSTEM_SITES, siteUrl } from '../../ecosystem/core/sites.ts';
12
12
  import { joinBasePath } from '../../router/core/basePath.ts';
13
13
  import { escapeAttribute, escapeText } from '../../share/core/escape.ts';
14
14
 
15
+ import type { PageContent } from './pageProse.ts';
16
+ import { pageProseHtml } from './pageProse.ts';
15
17
  import type { RouteMeta } from './routes.ts';
16
18
  import type { SiteFilesOptions } from './siteFiles.ts';
17
19
  import { mountPathOf, resolveSite } from './siteFiles.ts';
@@ -93,6 +95,14 @@ export interface NoscriptOptions
93
95
  extends Omit<SiteFilesOptions, 'routes'>, Omit<NoscriptText, 'routes'> {
94
96
  /** The addresses it links, each with the label it is linked under. */
95
97
  routes: readonly NoscriptRoute[];
98
+ /**
99
+ * What the page being written says, above the crawl path. The block is the
100
+ * only text a crawler is handed before anything runs, so a page with nothing
101
+ * of its own here is indexed on its title and on this site's menu — which
102
+ * every other page of the site carries too.
103
+ * @default undefined — the block is the menu alone
104
+ */
105
+ content?: PageContent;
96
106
  }
97
107
 
98
108
  /**
@@ -111,14 +121,20 @@ export function noscriptIndex(options: NoscriptOptions): string {
111
121
  const site = resolveSite(options.site);
112
122
  const hrefs = options.hrefs ?? 'absolute';
113
123
  const mount = hrefs === 'absolute' ? mountPathOf(options) : '';
114
- const heading = escapeText(options.heading ?? siteDisplayName(site));
124
+ const content = options.content;
125
+ // The page's own name answers the title it is indexed under, so it wins over
126
+ // a heading the site writes for every one of its pages at once.
127
+ const heading = escapeText(
128
+ content?.heading ?? options.heading ?? siteDisplayName(site),
129
+ );
115
130
  const intro = escapeText(
116
131
  options.intro ??
117
132
  `${site.tagline} This tool needs JavaScript; these are the pages it offers:`,
118
133
  );
134
+ const prose = content === undefined ? '' : pageProseHtml(content, ' ');
119
135
 
120
136
  return `<noscript>
121
- <h1>${heading}</h1>
137
+ <h1>${heading}</h1>${prose}
122
138
  <p>${intro}</p>${pageList(options.routes, mount, hrefs, ' ')}${familyList(site.id, options.ecosystem)}
123
139
  </noscript>`;
124
140
  }
@@ -14,14 +14,26 @@
14
14
 
15
15
  import { siteDisplayName } from '../../ecosystem/core/lookup.ts';
16
16
  import type { SiteId, SiteRecord } from '../../ecosystem/core/sites.ts';
17
+ import type { Language } from '../../i18n/core/languages.ts';
18
+ import { DEFAULT_LANGUAGE } from '../../i18n/core/languages.ts';
19
+ import {
20
+ readLanguagePath,
21
+ withLanguagePath,
22
+ } from '../../language/core/languagePath.ts';
23
+ import { withoutQueryOrFragment } from '../../router/core/address.ts';
24
+ import { stripBasePath } from '../../router/core/basePath.ts';
17
25
  import { escapeAttribute, escapeText } from '../../share/core/escape.ts';
18
26
 
27
+ import { alternateLinkTags } from './alternates.ts';
19
28
  import type { DocumentMeta } from './documentMeta.ts';
20
29
  import type { RouteMeta } from './routes.ts';
21
30
  import { pageMetaFor } from './routes.ts';
22
31
  import { mountPathOf, originOf, resolveSite } from './siteFiles.ts';
23
32
  import { PAGE_HEAD_MARKER, fill } from './template.ts';
24
33
 
34
+ // A scheme and an authority: what `location.href` hands out.
35
+ const ABSOLUTE_URL = /^[a-z][\d+.a-z-]*:\/\//i;
36
+
25
37
  /** Which site is being served, and what it answers. */
26
38
  export interface PageMetaOptions {
27
39
  /** The site, named or passed. */
@@ -48,6 +60,17 @@ export interface PageMetaOptions {
48
60
  * @default '/og.png'
49
61
  */
50
62
  image?: string;
63
+ /**
64
+ * Every language the site is written in, the default one included.
65
+ *
66
+ * The language is read off the address — `/fr/tutorial` is the French
67
+ * tutorial — so the canonical, the card and the `hreflang` set are written
68
+ * for the page actually being served, and the routes passed are the table in
69
+ * that language. A site writing one language leaves this out and nothing
70
+ * about its head changes.
71
+ * @default [the default language]
72
+ */
73
+ languages?: readonly Language[];
51
74
  }
52
75
 
53
76
  /**
@@ -74,12 +97,27 @@ export function injectPageMeta(html: string, options: PageMetaOptions): string {
74
97
  */
75
98
  export function pageHeadTags(options: PageMetaOptions): string {
76
99
  const name = siteDisplayName(resolveSite(options.site));
77
- const description = routeMetaOf(options).description;
100
+ const route = routeMetaOf(options);
101
+ const description = route.description;
78
102
  const origin = originOf(options);
79
103
  const { title, canonical } = pageDocumentMeta(options);
80
104
  const image = absolute(options.image ?? '/og.png', origin);
105
+ const alternates = alternateLinkTags({
106
+ origin,
107
+ path: route.path,
108
+ languages: languagesOf(options),
109
+ });
110
+
111
+ // A maintenance screen says so on the page itself. `robots.txt` cannot: it
112
+ // stops the crawl, and an address nobody crawled is still listed from
113
+ // whatever links to it, with no description to show for it.
114
+ const robots =
115
+ route.indexed === false
116
+ ? ['<meta name="robots" content="noindex, nofollow" />']
117
+ : [];
81
118
 
82
119
  return [
120
+ ...robots,
83
121
  `<title>${escapeText(title)}</title>`,
84
122
  `<meta name="description" content="${escapeAttribute(description)}" />`,
85
123
  `<link rel="canonical" href="${escapeAttribute(canonical)}" />`,
@@ -90,6 +128,7 @@ export function pageHeadTags(options: PageMetaOptions): string {
90
128
  `<meta property="og:url" content="${escapeAttribute(canonical)}" />`,
91
129
  `<meta property="og:image" content="${escapeAttribute(image)}" />`,
92
130
  '<meta name="twitter:card" content="summary_large_image" />',
131
+ ...(alternates === '' ? [] : [alternates]),
93
132
  ].join('\n');
94
133
  }
95
134
 
@@ -112,15 +151,47 @@ export function pageDocumentMeta(
112
151
  return {
113
152
  title: `${meta.title} — ${siteDisplayName(site)}`,
114
153
  description: meta.description,
115
- canonical: `${originOf(options)}${meta.path}`,
154
+ canonical: `${originOf(options)}${withLanguagePath(pageLanguage(options), meta.path)}`,
116
155
  };
117
156
  }
118
157
 
158
+ /**
159
+ * The language an address is being served in.
160
+ *
161
+ * Read off the address itself rather than passed beside it, so the catalog a
162
+ * server picks and the canonical it writes cannot disagree.
163
+ * @param options - Which site, which address, and the languages it speaks.
164
+ * @returns The language named by the address, or the default one.
165
+ * @throws {Error} When the deployment names an origin that is not an absolute
166
+ * address.
167
+ */
168
+ export function pageLanguage(options: PageMetaOptions): Language {
169
+ return readLanguagePath(ownPath(options), languagesOf(options)).language;
170
+ }
171
+
119
172
  // A server behind a mount is handed the address the browser asked for, and the
120
173
  // route table is written from the site's own root, so the mount the origin
121
174
  // carries is taken off it before the table is read.
122
175
  function routeMetaOf(options: PageMetaOptions): RouteMeta {
123
- return pageMetaFor(options.routes, options.url, mountPathOf(options));
176
+ const { path } = readLanguagePath(ownPath(options), languagesOf(options));
177
+ return pageMetaFor(options.routes, path);
178
+ }
179
+
180
+ // The address from the site's own root, the mount taken off, with the query
181
+ // string and the fragment gone: what the language prefix is read off. An app
182
+ // handing over `location.href` is answered too, so the one reading serves a
183
+ // server, a build and a click alike.
184
+ function ownPath(options: PageMetaOptions): string {
185
+ const url = options.url;
186
+ const path =
187
+ ABSOLUTE_URL.test(url) && URL.canParse(url)
188
+ ? new URL(url).pathname
189
+ : withoutQueryOrFragment(url);
190
+ return stripBasePath(mountPathOf(options), path);
191
+ }
192
+
193
+ function languagesOf(options: PageMetaOptions): readonly Language[] {
194
+ return options.languages ?? [DEFAULT_LANGUAGE];
124
195
  }
125
196
 
126
197
  function absolute(target: string, origin: string): string {