react-cheminfo 0.59.0 → 0.61.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 (140) hide show
  1. package/README.md +36 -2
  2. package/lib/about/ui/AboutBuildDetails.d.ts +15 -0
  3. package/lib/about/ui/AboutBuildDetails.d.ts.map +1 -0
  4. package/lib/about/ui/AboutBuildDetails.js +17 -0
  5. package/lib/about/ui/AboutBuildDetails.js.map +1 -0
  6. package/lib/about/ui/AboutPage.d.ts +8 -0
  7. package/lib/about/ui/AboutPage.d.ts.map +1 -1
  8. package/lib/about/ui/AboutPage.js +12 -3
  9. package/lib/about/ui/AboutPage.js.map +1 -1
  10. package/lib/about/ui/AboutSignIn.d.ts +33 -0
  11. package/lib/about/ui/AboutSignIn.d.ts.map +1 -0
  12. package/lib/about/ui/AboutSignIn.js +28 -0
  13. package/lib/about/ui/AboutSignIn.js.map +1 -0
  14. package/lib/about/ui/AboutVersion.d.ts +4 -2
  15. package/lib/about/ui/AboutVersion.d.ts.map +1 -1
  16. package/lib/about/ui/AboutVersion.js +21 -3
  17. package/lib/about/ui/AboutVersion.js.map +1 -1
  18. package/lib/about/ui/index.d.ts +2 -2
  19. package/lib/about/ui/index.d.ts.map +1 -1
  20. package/lib/about/ui/index.js +1 -1
  21. package/lib/about/ui/index.js.map +1 -1
  22. package/lib/chrome/ui/NavEntry.d.ts +16 -0
  23. package/lib/chrome/ui/NavEntry.d.ts.map +1 -0
  24. package/lib/chrome/ui/NavEntry.js +27 -0
  25. package/lib/chrome/ui/NavEntry.js.map +1 -0
  26. package/lib/chrome/ui/SiteHeader.d.ts +11 -4
  27. package/lib/chrome/ui/SiteHeader.d.ts.map +1 -1
  28. package/lib/chrome/ui/SiteHeader.js +4 -26
  29. package/lib/chrome/ui/SiteHeader.js.map +1 -1
  30. package/lib/core.d.ts +1 -0
  31. package/lib/core.d.ts.map +1 -1
  32. package/lib/core.js +1 -0
  33. package/lib/core.js.map +1 -1
  34. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  35. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  36. package/lib/locales/de.json +11 -3
  37. package/lib/locales/en.json +11 -3
  38. package/lib/locales/es.json +11 -3
  39. package/lib/locales/fr.json +11 -3
  40. package/lib/locales/it.json +11 -3
  41. package/lib/moleculesSources/core/index.d.ts +3 -0
  42. package/lib/moleculesSources/core/index.d.ts.map +1 -0
  43. package/lib/moleculesSources/core/index.js +2 -0
  44. package/lib/moleculesSources/core/index.js.map +1 -0
  45. package/lib/moleculesSources/core/moleculesSources.d.ts +55 -0
  46. package/lib/moleculesSources/core/moleculesSources.d.ts.map +1 -0
  47. package/lib/moleculesSources/core/moleculesSources.js +63 -0
  48. package/lib/moleculesSources/core/moleculesSources.js.map +1 -0
  49. package/lib/moleculesSources/ui/MoleculesSourceBadge.d.ts +18 -0
  50. package/lib/moleculesSources/ui/MoleculesSourceBadge.d.ts.map +1 -0
  51. package/lib/moleculesSources/ui/MoleculesSourceBadge.js +22 -0
  52. package/lib/moleculesSources/ui/MoleculesSourceBadge.js.map +1 -0
  53. package/lib/moleculesSources/ui/SourceBadge.d.ts +57 -0
  54. package/lib/moleculesSources/ui/SourceBadge.d.ts.map +1 -0
  55. package/lib/moleculesSources/ui/SourceBadge.js +29 -0
  56. package/lib/moleculesSources/ui/SourceBadge.js.map +1 -0
  57. package/lib/moleculesSources/ui/index.d.ts +7 -0
  58. package/lib/moleculesSources/ui/index.d.ts.map +1 -0
  59. package/lib/moleculesSources/ui/index.js +4 -0
  60. package/lib/moleculesSources/ui/index.js.map +1 -0
  61. package/lib/moleculesSources/ui/useSourceLooks.d.ts +24 -0
  62. package/lib/moleculesSources/ui/useSourceLooks.d.ts.map +1 -0
  63. package/lib/moleculesSources/ui/useSourceLooks.js +53 -0
  64. package/lib/moleculesSources/ui/useSourceLooks.js.map +1 -0
  65. package/lib/moleculesToken/core/index.d.ts +1 -0
  66. package/lib/moleculesToken/core/index.d.ts.map +1 -1
  67. package/lib/moleculesToken/core/index.js +1 -0
  68. package/lib/moleculesToken/core/index.js.map +1 -1
  69. package/lib/moleculesToken/core/tokenFix.d.ts +18 -0
  70. package/lib/moleculesToken/core/tokenFix.d.ts.map +1 -0
  71. package/lib/moleculesToken/core/tokenFix.js +27 -0
  72. package/lib/moleculesToken/core/tokenFix.js.map +1 -0
  73. package/lib/moleculesToken/ui/MoleculesTokenFix.d.ts +22 -0
  74. package/lib/moleculesToken/ui/MoleculesTokenFix.d.ts.map +1 -0
  75. package/lib/moleculesToken/ui/MoleculesTokenFix.js +35 -0
  76. package/lib/moleculesToken/ui/MoleculesTokenFix.js.map +1 -0
  77. package/lib/moleculesToken/ui/MoleculesTokenTable.d.ts +6 -1
  78. package/lib/moleculesToken/ui/MoleculesTokenTable.d.ts.map +1 -1
  79. package/lib/moleculesToken/ui/MoleculesTokenTable.js +7 -75
  80. package/lib/moleculesToken/ui/MoleculesTokenTable.js.map +1 -1
  81. package/lib/moleculesToken/ui/index.d.ts +2 -0
  82. package/lib/moleculesToken/ui/index.d.ts.map +1 -1
  83. package/lib/moleculesToken/ui/index.js +1 -0
  84. package/lib/moleculesToken/ui/index.js.map +1 -1
  85. package/lib/moleculesToken/ui/tokenDetails.d.ts +13 -0
  86. package/lib/moleculesToken/ui/tokenDetails.d.ts.map +1 -0
  87. package/lib/moleculesToken/ui/tokenDetails.js +73 -0
  88. package/lib/moleculesToken/ui/tokenDetails.js.map +1 -0
  89. package/lib/server/siteFiles.d.ts +28 -0
  90. package/lib/server/siteFiles.d.ts.map +1 -0
  91. package/lib/server/siteFiles.js +57 -0
  92. package/lib/server/siteFiles.js.map +1 -0
  93. package/lib/server/staticSite.d.ts +42 -0
  94. package/lib/server/staticSite.d.ts.map +1 -0
  95. package/lib/server/staticSite.js +114 -0
  96. package/lib/server/staticSite.js.map +1 -0
  97. package/lib/server.d.ts +4 -0
  98. package/lib/server.d.ts.map +1 -0
  99. package/lib/server.js +3 -0
  100. package/lib/server.js.map +1 -0
  101. package/lib/ui.d.ts +1 -0
  102. package/lib/ui.d.ts.map +1 -1
  103. package/lib/ui.js +1 -0
  104. package/lib/ui.js.map +1 -1
  105. package/package.json +12 -1
  106. package/src/about/ui/AboutBuildDetails.tsx +45 -0
  107. package/src/about/ui/AboutPage.tsx +27 -13
  108. package/src/about/ui/AboutSignIn.tsx +63 -0
  109. package/src/about/ui/AboutVersion.tsx +27 -9
  110. package/src/about/ui/index.ts +2 -2
  111. package/src/chrome/ui/NavEntry.tsx +41 -0
  112. package/src/chrome/ui/SiteHeader.tsx +15 -41
  113. package/src/core.ts +1 -0
  114. package/src/locales/de.json +11 -3
  115. package/src/locales/en.json +11 -3
  116. package/src/locales/es.json +11 -3
  117. package/src/locales/fr.json +11 -3
  118. package/src/locales/it.json +11 -3
  119. package/src/moleculesSources/core/index.ts +11 -0
  120. package/src/moleculesSources/core/moleculesSources.ts +102 -0
  121. package/src/moleculesSources/ui/MoleculesSourceBadge.tsx +44 -0
  122. package/src/moleculesSources/ui/SourceBadge.tsx +117 -0
  123. package/src/moleculesSources/ui/index.ts +6 -0
  124. package/src/moleculesSources/ui/useSourceLooks.ts +70 -0
  125. package/src/moleculesToken/core/index.ts +1 -0
  126. package/src/moleculesToken/core/tokenFix.ts +37 -0
  127. package/src/moleculesToken/ui/MoleculesTokenFix.tsx +95 -0
  128. package/src/moleculesToken/ui/MoleculesTokenTable.tsx +20 -94
  129. package/src/moleculesToken/ui/index.ts +2 -0
  130. package/src/moleculesToken/ui/tokenDetails.tsx +91 -0
  131. package/src/server/siteFiles.ts +64 -0
  132. package/src/server/staticSite.ts +188 -0
  133. package/src/server.ts +7 -0
  134. package/src/ui.ts +1 -0
  135. package/styles/chrome.css +159 -0
  136. package/lib/about/ui/AboutBuild.d.ts +0 -18
  137. package/lib/about/ui/AboutBuild.d.ts.map +0 -1
  138. package/lib/about/ui/AboutBuild.js +0 -37
  139. package/lib/about/ui/AboutBuild.js.map +0 -1
  140. package/src/about/ui/AboutBuild.tsx +0 -54
@@ -0,0 +1,42 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ /** Options of {@link staticSite}. */
3
+ export interface StaticSiteOptions {
4
+ /** The directory the build wrote, e.g. `dist`. */
5
+ root: string;
6
+ /**
7
+ * The analytics snippet, put at the end of the head of every page, as
8
+ * `TRACKING_SCRIPT` holds it.
9
+ * @default undefined - no snippet
10
+ */
11
+ trackingScript?: string;
12
+ /**
13
+ * Addresses the site does not have any more, and the one that does their
14
+ * work. The query is carried over, so a link handed out before the move
15
+ * keeps its configuration.
16
+ * @default {}
17
+ */
18
+ redirects?: Readonly<Record<string, string>>;
19
+ /**
20
+ * Address prefixes another plugin answers, such as the API, which never
21
+ * answer a page: an unknown address there is the API's own 404.
22
+ * @default ['/v1/']
23
+ */
24
+ reserved?: readonly string[];
25
+ }
26
+ /**
27
+ * Serve a site's build from Node, with the rules every static host of the
28
+ * family follows (static-web-server's `sws.toml`, Cloudflare Pages): a real
29
+ * file per address and no fallback page, `/titration` answered without a
30
+ * redirect, pages checked on every visit and hashed bundles kept for a year,
31
+ * the analytics snippet in every page, `404.html` for an address the site does
32
+ * not have, and `/health` for the deploy script.
33
+ *
34
+ * Register it next to the API; a build missing (a server started before the
35
+ * frontend was built) leaves the API alone:
36
+ *
37
+ * await app.register(staticSite, { root: 'dist', trackingScript });
38
+ * @param fastify - The server.
39
+ * @param options - The build, the snippet and the retired addresses.
40
+ */
41
+ export declare function staticSite(fastify: FastifyInstance, options: StaticSiteOptions): Promise<void>;
42
+ //# sourceMappingURL=staticSite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staticSite.d.ts","sourceRoot":"","sources":["../../src/server/staticSite.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EAIhB,MAAM,SAAS,CAAC;AAMjB,qCAAqC;AACrC,MAAM,WAAW,iBAAiB;IAChC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAiBD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CA4Df"}
@@ -0,0 +1,114 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join, relative, resolve, sep } from 'node:path';
3
+ import { injectTrackingScript } from "../seo/core/trackingScript.js";
4
+ import { isHashedBundle, listSiteFiles, resolveAddress } from "./siteFiles.js";
5
+ /** A page is checked on every visit: it names the bundles of its own build. */
6
+ const PAGE_CACHE = 'no-cache';
7
+ /** A bundle is named by its content, so it never changes under its name. */
8
+ const BUNDLE_CACHE = 'public, max-age=31536000, immutable';
9
+ /** Kept out of the API's documentation (`@fastify/swagger` reads `hide`). */
10
+ const HIDDEN = { hide: true };
11
+ /**
12
+ * Serve a site's build from Node, with the rules every static host of the
13
+ * family follows (static-web-server's `sws.toml`, Cloudflare Pages): a real
14
+ * file per address and no fallback page, `/titration` answered without a
15
+ * redirect, pages checked on every visit and hashed bundles kept for a year,
16
+ * the analytics snippet in every page, `404.html` for an address the site does
17
+ * not have, and `/health` for the deploy script.
18
+ *
19
+ * Register it next to the API; a build missing (a server started before the
20
+ * frontend was built) leaves the API alone:
21
+ *
22
+ * await app.register(staticSite, { root: 'dist', trackingScript });
23
+ * @param fastify - The server.
24
+ * @param options - The build, the snippet and the retired addresses.
25
+ */
26
+ export async function staticSite(fastify, options) {
27
+ const { trackingScript, redirects = {}, reserved = ['/v1/'] } = options;
28
+ const root = resolve(options.root);
29
+ fastify.get('/health', { logLevel: 'silent', schema: HIDDEN }, (_request, reply) => reply.send({ status: 'ok' }));
30
+ const files = await listSiteFiles(root);
31
+ if (files === undefined) {
32
+ fastify.log.warn({ root }, 'no built site: serving the API alone');
33
+ return;
34
+ }
35
+ const pages = await readPages(root, files, trackingScript);
36
+ // An optional peer, installed only by a site that has a server: importing
37
+ // react-cheminfo/server must not need it.
38
+ const { default: fastifyStatic } = await import('@fastify/static');
39
+ await fastify.register(fastifyStatic, {
40
+ root,
41
+ serve: false,
42
+ cacheControl: false,
43
+ setHeaders: (reply, path) => {
44
+ const file = relative(root, path).split(sep).join('/');
45
+ if (isHashedBundle(file)) {
46
+ reply.header('cache-control', BUNDLE_CACHE);
47
+ // A page framed by a sandboxed view may run under an opaque origin,
48
+ // and a module bundle is then fetched cross-origin.
49
+ reply.header('access-control-allow-origin', '*');
50
+ }
51
+ else {
52
+ reply.header('cache-control', PAGE_CACHE);
53
+ }
54
+ },
55
+ });
56
+ const answer = (request, reply) => {
57
+ const url = new URL(request.url, 'http://site');
58
+ const { pathname, search } = url;
59
+ if (reserved.some((prefix) => pathname.startsWith(prefix))) {
60
+ return reply.callNotFound();
61
+ }
62
+ const destination = redirects[pathname] ?? redirects[pathname.replace(/\/$/, '')];
63
+ if (destination !== undefined) {
64
+ return reply.redirect(`${destination}${search}`, 301);
65
+ }
66
+ const file = resolveAddress(files, pathname);
67
+ if (file === undefined) {
68
+ return sendPage(request, reply, pages.get('404.html'), 404);
69
+ }
70
+ const page = pages.get(file);
71
+ if (page)
72
+ return sendPage(request, reply, page, 200);
73
+ return reply.sendFile(file);
74
+ };
75
+ fastify.get('/', { schema: HIDDEN }, answer);
76
+ fastify.get('/*', { schema: HIDDEN }, answer);
77
+ }
78
+ /**
79
+ * Read every page of the build once, with the snippet in its head.
80
+ * @param root - The build.
81
+ * @param files - Its files.
82
+ * @param trackingScript - The snippet, if any.
83
+ * @returns The pages, by file.
84
+ */
85
+ async function readPages(root, files, trackingScript) {
86
+ const htmlFiles = [...files].filter((file) => file.endsWith('.html'));
87
+ const pages = await Promise.all(htmlFiles.map(async (file) => {
88
+ const html = injectTrackingScript(await readFile(join(root, file), 'utf8'), trackingScript);
89
+ return [file, { html, etag: await etagOf(html) }];
90
+ }));
91
+ return new Map(pages);
92
+ }
93
+ function sendPage(request, reply, page, status) {
94
+ if (page === undefined) {
95
+ return reply
96
+ .code(status)
97
+ .type('text/plain; charset=utf-8')
98
+ .send('Not found');
99
+ }
100
+ reply.header('cache-control', PAGE_CACHE).header('etag', page.etag);
101
+ if (status === 200 && request.headers['if-none-match'] === page.etag) {
102
+ return reply.code(304).send();
103
+ }
104
+ return reply.code(status).type('text/html; charset=utf-8').send(page.html);
105
+ }
106
+ async function etagOf(text) {
107
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
108
+ const bytes = new Uint8Array(digest).subarray(0, 12);
109
+ let hex = '';
110
+ for (const byte of bytes)
111
+ hex += byte.toString(16).padStart(2, '0');
112
+ return `"${hex}"`;
113
+ }
114
+ //# sourceMappingURL=staticSite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staticSite.js","sourceRoot":"","sources":["../../src/server/staticSite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AASzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAErE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAiC/E,+EAA+E;AAC/E,MAAM,UAAU,GAAG,UAAU,CAAC;AAE9B,4EAA4E;AAC5E,MAAM,YAAY,GAAG,qCAAqC,CAAC;AAE3D,6EAA6E;AAC7E,MAAM,MAAM,GAAsC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAEjE;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAAwB,EACxB,OAA0B;IAE1B,MAAM,EAAE,cAAc,EAAE,SAAS,GAAG,EAAE,EAAE,QAAQ,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC;IACxE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC,OAAO,CAAC,GAAG,CACT,SAAS,EACT,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,EACtC,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAClD,CAAC;IAEF,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,sCAAsC,CAAC,CAAC;QACnE,OAAO;IACT,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC;IAE3D,0EAA0E;IAC1E,0CAA0C;IAC1C,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;IACnE,MAAM,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE;QACpC,IAAI;QACJ,KAAK,EAAE,KAAK;QACZ,YAAY,EAAE,KAAK;QACnB,UAAU,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;YAC1B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACvD,IAAI,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzB,KAAK,CAAC,MAAM,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;gBAC5C,oEAAoE;gBACpE,oDAAoD;gBACpD,KAAK,CAAC,MAAM,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;YACnD,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,MAAM,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;KACF,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,CAAC,OAAuB,EAAE,KAAmB,EAAE,EAAE;QAC9D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;QAChD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;QACjC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YAC3D,OAAO,KAAK,CAAC,YAAY,EAAE,CAAC;QAC9B,CAAC;QACD,MAAM,WAAW,GACf,SAAS,CAAC,QAAQ,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QAChE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,OAAO,KAAK,CAAC,QAAQ,CAAC,GAAG,WAAW,GAAG,MAAM,EAAE,EAAE,GAAG,CAAC,CAAC;QACxD,CAAC;QAED,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAC7C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,OAAO,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC,CAAC;QAC9D,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,IAAI;YAAE,OAAO,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;QACrD,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC,CAAC;IAEF,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,SAAS,CACtB,IAAY,EACZ,KAA0B,EAC1B,cAAkC;IAElC,MAAM,SAAS,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IACtE,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QAC3B,MAAM,IAAI,GAAG,oBAAoB,CAC/B,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,EACxC,cAAc,CACf,CAAC;QACF,OAAO,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,CAAU,CAAC;IAC7D,CAAC,CAAC,CACH,CAAC;IACF,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,QAAQ,CACf,OAAuB,EACvB,KAAmB,EACnB,IAAsB,EACtB,MAAc;IAEd,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,KAAK;aACT,IAAI,CAAC,MAAM,CAAC;aACZ,IAAI,CAAC,2BAA2B,CAAC;aACjC,IAAI,CAAC,WAAW,CAAC,CAAC;IACvB,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACpE,IAAI,MAAM,KAAK,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QACrE,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAChC,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7E,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,IAAY;IAChC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CACvC,SAAS,EACT,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAC/B,CAAC;IACF,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACrD,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACpE,OAAO,IAAI,GAAG,GAAG,CAAC;AACpB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export type { StaticSiteOptions } from './server/staticSite.ts';
2
+ export { staticSite } from './server/staticSite.ts';
3
+ export { isHashedBundle, listSiteFiles, resolveAddress, } from './server/siteFiles.ts';
4
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EACL,cAAc,EACd,aAAa,EACb,cAAc,GACf,MAAM,uBAAuB,CAAC"}
package/lib/server.js ADDED
@@ -0,0 +1,3 @@
1
+ export { staticSite } from "./server/staticSite.js";
2
+ export { isHashedBundle, listSiteFiles, resolveAddress, } from "./server/siteFiles.js";
3
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EACL,cAAc,EACd,aAAa,EACb,cAAc,GACf,MAAM,uBAAuB,CAAC"}
package/lib/ui.d.ts CHANGED
@@ -18,6 +18,7 @@ export * from './help/ui/index.ts';
18
18
  export * from './hooks/ui/index.ts';
19
19
  export * from './i18n/ui/index.ts';
20
20
  export * from './language/ui/index.ts';
21
+ export * from './moleculesSources/ui/index.ts';
21
22
  export * from './moleculesToken/ui/index.ts';
22
23
  export * from './number/ui/index.ts';
23
24
  export * from './overlay/ui/index.ts';
package/lib/ui.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AAKvC,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,eAAe,GAChB,MAAM,oCAAoC,CAAC"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AAKvC,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,eAAe,GAChB,MAAM,oCAAoC,CAAC"}
package/lib/ui.js CHANGED
@@ -18,6 +18,7 @@ export * from "./help/ui/index.js";
18
18
  export * from "./hooks/ui/index.js";
19
19
  export * from "./i18n/ui/index.js";
20
20
  export * from "./language/ui/index.js";
21
+ export * from "./moleculesSources/ui/index.js";
21
22
  export * from "./moleculesToken/ui/index.js";
22
23
  export * from "./number/ui/index.js";
23
24
  export * from "./overlay/ui/index.js";
package/lib/ui.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AAMvC,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAmC,CAAC;AAEnE,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,eAAe,GAChB,MAAM,oCAAoC,CAAC"}
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AAMvC,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAmC,CAAC;AAEnE,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,eAAe,GAChB,MAAM,oCAAoC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-cheminfo",
3
- "version": "0.59.0",
3
+ "version": "0.61.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",
@@ -25,6 +25,7 @@
25
25
  "./molstar/*": "./lib/molstar/*/index.js",
26
26
  "./orbital": "./lib/orbital.js",
27
27
  "./pages": "./lib/pages.js",
28
+ "./server": "./lib/server.js",
28
29
  "./slides": "./lib/slides.js",
29
30
  "./spectra": "./lib/spectra.js",
30
31
  "./structure": "./lib/structure.js",
@@ -85,6 +86,7 @@
85
86
  },
86
87
  "devDependencies": {
87
88
  "@blueprintjs/core": "^6.18.0",
89
+ "@fastify/static": "^10.1.6",
88
90
  "@playwright/test": "^1.62.1",
89
91
  "@storybook/addon-docs": "^10.5.8",
90
92
  "@storybook/react-vite": "^10.5.8",
@@ -99,6 +101,7 @@
99
101
  "eslint": "^9.39.5",
100
102
  "eslint-config-cheminfo-react": "^20.1.0",
101
103
  "eslint-config-cheminfo-typescript": "^22.1.0",
104
+ "fastify": "^5.12.7",
102
105
  "jsdom": "^30.1.1",
103
106
  "ml-dataset-iris": "^1.2.1",
104
107
  "ml-pca": "^4.1.1",
@@ -119,6 +122,8 @@
119
122
  },
120
123
  "peerDependencies": {
121
124
  "@blueprintjs/core": "^6.18.0",
125
+ "@fastify/static": "^10.0.0",
126
+ "fastify": "^5.0.0",
122
127
  "ml-pca": "^4.1.1",
123
128
  "ml-signal-processing": "^2.2.2",
124
129
  "molstar": "^5.11.0",
@@ -139,6 +144,12 @@
139
144
  },
140
145
  "homepage": "https://github.com/cheminfo/react-cheminfo#readme",
141
146
  "peerDependenciesMeta": {
147
+ "@fastify/static": {
148
+ "optional": true
149
+ },
150
+ "fastify": {
151
+ "optional": true
152
+ },
142
153
  "ml-pca": {
143
154
  "optional": true
144
155
  },
@@ -0,0 +1,45 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ import type { BuildInfo } from '../../build/core/buildInfo.ts';
4
+ import { formatBuiltAt, shortCommit } from '../../build/core/buildInfo.ts';
5
+ import { useChromeT } from '../../i18n/ui/useT.ts';
6
+
7
+ /**
8
+ * What the badge of a release leaves to its hover: the commit it was built
9
+ * from, the instant to the second, and the chrome it was built against.
10
+ * @param props - The build and its released version.
11
+ * @param props.build - What the build published about itself.
12
+ * @param props.version - The release the badge names.
13
+ * @returns The card's contents.
14
+ */
15
+ export function AboutBuildDetails(props: {
16
+ build: BuildInfo;
17
+ version: string;
18
+ }): ReactElement {
19
+ const { build, version } = props;
20
+ const t = useChromeT();
21
+
22
+ return (
23
+ <>
24
+ <div className="build-tooltip__header">
25
+ {t('about.buildVersion', { version })}
26
+ </div>
27
+ <dl className="build-tooltip__rows">
28
+ <dt>{t('about.buildBuilt')}</dt>
29
+ <dd>{formatBuiltAt(build.builtAt)}</dd>
30
+ {build.commit === undefined ? null : (
31
+ <>
32
+ <dt>{t('about.buildCommit')}</dt>
33
+ <dd>{shortCommit(build.commit)}</dd>
34
+ </>
35
+ )}
36
+ {build.reactCheminfo === undefined ? null : (
37
+ <>
38
+ <dt>react-cheminfo</dt>
39
+ <dd>{build.reactCheminfo}</dd>
40
+ </>
41
+ )}
42
+ </dl>
43
+ </>
44
+ );
45
+ }
@@ -1,5 +1,6 @@
1
1
  import type { CSSProperties, ReactElement, ReactNode } from 'react';
2
2
 
3
+ import { buildLabel } from '../../build/core/buildInfo.ts';
3
4
  import { CreditsList } from '../../credits/ui/CreditsList.tsx';
4
5
  import { Wordmark } from '../../ecosystem/ui/Wordmark.tsx';
5
6
  import { SiteMark } from '../../ecosystem/ui/marks.tsx';
@@ -7,10 +8,11 @@ import { useChromeT } from '../../i18n/ui/useT.ts';
7
8
  import type { AboutContent } from '../core/about.ts';
8
9
  import { resolveAbout } from '../core/about.ts';
9
10
 
10
- import { AboutBuild } from './AboutBuild.tsx';
11
11
  import { AboutCitations } from './AboutCitations.tsx';
12
12
  import { AboutProvidedBy } from './AboutProvidedBy.tsx';
13
13
  import { AboutSection } from './AboutSection.tsx';
14
+ import type { AboutSignInProps } from './AboutSignIn.tsx';
15
+ import { AboutSignIn } from './AboutSignIn.tsx';
14
16
  import { AboutVersion } from './AboutVersion.tsx';
15
17
 
16
18
  export interface AboutPageProps {
@@ -43,6 +45,13 @@ export interface AboutPageProps {
43
45
  * @default undefined
44
46
  */
45
47
  logo?: ReactNode;
48
+ /**
49
+ * Where the people who run the site sign in, drawn under the build badge in
50
+ * the corner of the hero. A site passes it while nobody is signed in, and
51
+ * leaves it out once somebody is: the account is then in the bar.
52
+ * @default undefined — the site has no account to sign in to
53
+ */
54
+ signIn?: AboutSignInProps;
46
55
  }
47
56
 
48
57
  /**
@@ -58,10 +67,12 @@ export interface AboutPageProps {
58
67
  * @throws {Error} When the record names a site or a credit that does not exist.
59
68
  */
60
69
  export function AboutPage(props: AboutPageProps): ReactElement {
61
- const { content, className, children, logo, mark } = props;
70
+ const { content, className, children, logo, mark, signIn } = props;
62
71
  const t = useChromeT();
63
72
  const about = resolveAbout(content);
64
73
  const site = about.site;
74
+ const hasCorner =
75
+ buildLabel(about.build) !== undefined || signIn !== undefined;
65
76
 
66
77
  return (
67
78
  <div
@@ -83,7 +94,12 @@ export function AboutPage(props: AboutPageProps): ReactElement {
83
94
  </p>
84
95
  <p style={WHAT_STYLE}>{about.what}</p>
85
96
  </div>
86
- <AboutVersion build={about.build} />
97
+ {hasCorner ? (
98
+ <div className="about-corner" style={CORNER_STYLE}>
99
+ <AboutVersion build={about.build} />
100
+ {signIn === undefined ? null : <AboutSignIn {...signIn} />}
101
+ </div>
102
+ ) : null}
87
103
  </header>
88
104
 
89
105
  {about.people.length === 0 && about.providedBy.length === 0 ? null : (
@@ -127,16 +143,6 @@ export function AboutPage(props: AboutPageProps): ReactElement {
127
143
  <AboutCitations works={about.cite} />
128
144
  </AboutSection>
129
145
  )}
130
-
131
- {/*
132
- The build the reader has open, last and under no heading: it is what a
133
- report has to name, not something anybody came to the page to read.
134
- */}
135
- {about.build === undefined ? null : (
136
- <AboutSection className="about-build">
137
- <AboutBuild build={about.build} />
138
- </AboutSection>
139
- )}
140
146
  </div>
141
147
  );
142
148
  }
@@ -166,6 +172,14 @@ const HERO_STYLE = {
166
172
  lineHeight: 1.55,
167
173
  } as const satisfies CSSProperties;
168
174
 
175
+ const CORNER_STYLE = {
176
+ display: 'flex',
177
+ flex: '0 0 auto',
178
+ flexDirection: 'column',
179
+ alignItems: 'flex-end',
180
+ gap: 2,
181
+ } as const satisfies CSSProperties;
182
+
169
183
  const NAME_STYLE = {
170
184
  margin: 0,
171
185
  fontSize: 26,
@@ -0,0 +1,63 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ import { isModifiedClick } from '../../chrome/ui/navItem.ts';
4
+ import { useChromeT } from '../../i18n/ui/useT.ts';
5
+
6
+ export interface AboutSignInProps {
7
+ /**
8
+ * The address signing in happens at. Writing it keeps the entry a real link,
9
+ * so a middle click opens a tab.
10
+ * @default undefined
11
+ */
12
+ href?: string;
13
+ /**
14
+ * What the site does when the entry is picked — route to that page, or open
15
+ * a credentials dialog for a site that has no page. A modified click is left
16
+ * to the browser.
17
+ * @default undefined
18
+ */
19
+ onSignIn?: () => void;
20
+ /**
21
+ * What the entry reads, for a site whose accounts are for one kind of person.
22
+ * @default the chrome's own word for it, in the language of the page
23
+ */
24
+ label?: string;
25
+ }
26
+
27
+ /**
28
+ * Where the people who run a site sign in: one quiet line under the build
29
+ * badge, in the corner of the About.
30
+ *
31
+ * A visitor has nothing to sign in to, so the bar carries no invitation; the
32
+ * About is the page the operators already know, and the corner is where they
33
+ * already look for the build they run.
34
+ * @param props - See {@link AboutSignInProps}.
35
+ * @returns The entry.
36
+ */
37
+ export function AboutSignIn(props: AboutSignInProps): ReactElement {
38
+ const { href, onSignIn, label } = props;
39
+ const t = useChromeT();
40
+ const text = label ?? t('account.signIn');
41
+
42
+ if (href === undefined) {
43
+ return (
44
+ <button type="button" className="about-sign-in" onClick={onSignIn}>
45
+ {text}
46
+ </button>
47
+ );
48
+ }
49
+
50
+ return (
51
+ <a
52
+ className="about-sign-in"
53
+ href={href}
54
+ onClick={(event) => {
55
+ if (onSignIn === undefined || isModifiedClick(event)) return;
56
+ event.preventDefault();
57
+ onSignIn();
58
+ }}
59
+ >
60
+ {text}
61
+ </a>
62
+ );
63
+ }
@@ -4,18 +4,22 @@
4
4
  * A report of something going wrong is worth answering only when we know what
5
5
  * was running, and that is the one thing an About page cannot say from a
6
6
  * hand-written record. It sits at the top of the page rather than at the
7
- * bottom because it is the one line a reader is asked to quote back.
7
+ * bottom because it is the one line a reader is asked to quote back, and it is
8
+ * said once: nothing further down the page repeats it.
8
9
  */
9
10
 
11
+ import { Tooltip } from '@blueprintjs/core';
10
12
  import type { CSSProperties, ReactElement } from 'react';
11
13
 
12
14
  import type { BuildInfo } from '../../build/core/buildInfo.ts';
13
15
  import {
14
16
  buildLabel,
15
17
  buildStamp,
16
- buildSummary,
18
+ releasedVersion,
17
19
  } from '../../build/core/buildInfo.ts';
18
20
 
21
+ import { AboutBuildDetails } from './AboutBuildDetails.tsx';
22
+
19
23
  export interface AboutVersionProps {
20
24
  /** What the build published about itself, or `undefined` when it published none. */
21
25
  build: BuildInfo | undefined;
@@ -23,7 +27,8 @@ export interface AboutVersionProps {
23
27
 
24
28
  /**
25
29
  * The build badge: the release a site is serving, or the commit it was built
26
- * from while it has no release yet, and when it was made.
30
+ * from while it has no release yet, and when it was made. A release names its
31
+ * commit and the exact instant in the card its hover opens.
27
32
  * @param props - See {@link AboutVersionProps}.
28
33
  * @returns The badge, or `null` when the build says nothing worth showing.
29
34
  */
@@ -34,15 +39,28 @@ export function AboutVersion(props: AboutVersionProps): ReactElement | null {
34
39
  // The instant belongs on the badge rather than in its hover: a version
35
40
  // nobody can date says nothing about how old the page they read is.
36
41
  const badge = `${label} · ${buildStamp(build.builtAt)}`;
42
+ const version = releasedVersion(build);
43
+
44
+ if (version === undefined) {
45
+ return (
46
+ <span style={BADGE_STYLE} className="about-version">
47
+ {badge}
48
+ </span>
49
+ );
50
+ }
37
51
 
38
52
  return (
39
- <span
40
- style={BADGE_STYLE}
41
- className="about-version"
42
- title={buildSummary(build)}
53
+ <Tooltip
54
+ content={<AboutBuildDetails build={build} version={version} />}
55
+ // To the left, so the card never covers the way to sign in under it.
56
+ placement="left-start"
57
+ popoverClassName="help-tooltip build-tooltip"
58
+ hoverOpenDelay={150}
43
59
  >
44
- {badge}
45
- </span>
60
+ <span style={BADGE_STYLE} className="about-version" tabIndex={0}>
61
+ {badge}
62
+ </span>
63
+ </Tooltip>
46
64
  );
47
65
  }
48
66
 
@@ -1,5 +1,3 @@
1
- export type { AboutBuildProps } from './AboutBuild.tsx';
2
- export { AboutBuild } from './AboutBuild.tsx';
3
1
  export type { AboutVersionProps } from './AboutVersion.tsx';
4
2
  export { AboutVersion } from './AboutVersion.tsx';
5
3
  export type { AboutCitationsProps } from './AboutCitations.tsx';
@@ -10,3 +8,5 @@ export type { AboutProvidedByProps } from './AboutProvidedBy.tsx';
10
8
  export { AboutProvidedBy } from './AboutProvidedBy.tsx';
11
9
  export type { AboutSectionProps } from './AboutSection.tsx';
12
10
  export { AboutSection } from './AboutSection.tsx';
11
+ export type { AboutSignInProps } from './AboutSignIn.tsx';
12
+ export { AboutSignIn } from './AboutSignIn.tsx';
@@ -0,0 +1,41 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ import { NavLink } from './NavLink.tsx';
4
+ import { NavMenuButton } from './NavMenuButton.tsx';
5
+ import type { NavItem } from './navItem.ts';
6
+
7
+ /**
8
+ * One page of the bar: a link, or the menu it opens when the entry folds pages
9
+ * of its own. A menu stands where its pages would, among them and not among the
10
+ * utilities, because picking one of them changes what the page shows.
11
+ * @param props - The entry and the page on show.
12
+ * @param props.item - The entry.
13
+ * @param props.activeId - The page on show.
14
+ * @returns The entry.
15
+ */
16
+ export function NavEntry(props: {
17
+ item: NavItem;
18
+ activeId: string | undefined;
19
+ }): ReactElement {
20
+ const { item, activeId } = props;
21
+ if (item.items === undefined) {
22
+ return <NavLink item={item} active={item.id === activeId} />;
23
+ }
24
+
25
+ return (
26
+ <NavMenuButton
27
+ label={menuLabel(item)}
28
+ icon={typeof item.icon === 'string' ? item.icon : undefined}
29
+ items={item.items}
30
+ activeId={activeId}
31
+ placement="bottom-start"
32
+ />
33
+ );
34
+ }
35
+
36
+ // The trigger names itself to a screen reader, so an entry drawn as a glyph
37
+ // alone says what it opens through its title rather than through its id.
38
+ function menuLabel(item: NavItem): string {
39
+ if (typeof item.label === 'string') return item.label;
40
+ return item.title ?? item.id;
41
+ }