@bkazemi/verily 0.9.3 → 0.9.5

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.
package/README.md CHANGED
@@ -168,7 +168,7 @@ import { init } from '@bkazemi/verily';
168
168
  const client = init({ backendUrl: '/api/verily' });
169
169
  ```
170
170
 
171
- Without one, serve `node_modules/@bkazemi/verily/dist/verily.js` yourself, or load it from a CDN such as `https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.9.3/dist/verily.js`. The script defines a global `Verily`.
171
+ Without one, serve `node_modules/@bkazemi/verily/dist/verily.js` yourself, or load it from a CDN such as `https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.9.5/dist/verily.js`. The script defines a global `Verily`.
172
172
 
173
173
  Then place the badge wherever the account appears:
174
174
 
@@ -370,7 +370,7 @@ A static site only needs the script and the badge markup; the backend runs somew
370
370
 
371
371
  ```html
372
372
  <script
373
- src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.9.3/dist/verily.js"
373
+ src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.9.5/dist/verily.js"
374
374
  defer
375
375
  ></script>
376
376
  <verily-badge
@@ -641,6 +641,8 @@ setInterval(
641
641
 
642
642
  Clone the repository and run `npm ci`. `npm run preview` then shows the badge in every state at http://localhost:3001, with no other setup.
643
643
 
644
+ Stylesheets are the `.css` files under `src`. `npm run css` turns each into a `.css.ts` module the code imports, and the build, check and run scripts do this first. Run it yourself after editing a `.css` file when you run a file with `tsx` directly.
645
+
644
646
  The full example also needs Postgres and a GitHub OAuth app, a Discord application or a Google OAuth client (any of them) with the callback URL `http://localhost:3000/api/verily/callback`:
645
647
 
646
648
  ```sh
@@ -0,0 +1,2 @@
1
+ declare const _default: ":host {\n display: inline-block;\n max-width: 100%;\n vertical-align: middle;\n}\n* {\n box-sizing: border-box;\n}\n.badge,\n.peek {\n border: 1px solid var(--verily-border, #dce2e0);\n border-radius: 6px;\n background: var(--verily-surface, #fff);\n color: var(--verily-text, #202c29);\n font: var(--verily-font-size, 13px)/1.35\n var(--verily-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);\n}\n.badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n padding: 3px 5px;\n text-decoration: none;\n}\nbutton.badge {\n margin: 0;\n cursor: pointer;\n}\na[href]:hover,\nbutton.badge:hover {\n background: var(--verily-hover, #f3f6f4);\n border-color: var(--verily-border, #dce2e0);\n}\n.badge:focus-visible {\n outline: 2px solid #357ce5;\n outline-offset: 2px;\n}\n.name {\n font-weight: 600;\n overflow-wrap: anywhere;\n min-width: 0;\n}\n.more {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n color: var(--verily-muted, #65726c);\n}\n.label {\n display: none;\n font-size: 11px;\n color: var(--verily-muted, #65726c);\n}\n.badge:hover .label,\n.badge:focus-within .label {\n display: inline;\n}\n.icon {\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n font-size: 12px;\n font-weight: 750;\n}\n.icon .glyph {\n display: block;\n width: 12px;\n height: 12px;\n}\n.mark {\n display: block;\n flex-shrink: 0;\n width: 20px;\n height: 20px;\n}\n.mark.pending {\n opacity: 0.45;\n}\n/* A dim ring where the account will go; it only spins where motion is welcome. */\n.spinner {\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n border: 2px solid currentColor;\n opacity: 0.3;\n}\n.divider {\n flex-shrink: 0;\n width: 1px;\n height: 12px;\n background: var(--verily-border, #dce2e0);\n}\n/* Several accounts, one to a row. The mark stays beside them and the rule runs their height. */\n.badge.stacked {\n align-items: stretch;\n padding: 5px 8px 5px 6px;\n text-align: left;\n}\n.stacked .mark {\n align-self: center;\n}\n.stacked .divider {\n height: auto;\n}\n.rows {\n display: grid;\n gap: 4px;\n min-width: 0;\n}\n.row {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n/*\n * The accounts behind a short pill's count, opened beside it. Shut unless the browser\n * has it open as a popover, so one without popovers never shows it at all.\n */\n.peek {\n display: none;\n position: fixed;\n inset: auto;\n margin: 0;\n padding: 8px 10px;\n width: max-content;\n max-width: min(320px, calc(100vw - 16px));\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.14);\n pointer-events: none;\n overflow: hidden;\n}\n/* A row taken out to fit the window: its own display would otherwise keep it showing. */\n.row[hidden] {\n display: none;\n}\n.peek:popover-open {\n display: block;\n}\n.peek .via {\n display: block;\n margin-top: 6px;\n font-size: 11px;\n color: var(--verily-muted, #65726c);\n}\n.provider {\n display: block;\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n}\n.expired .icon {\n color: #815a12;\n background: #fbefce;\n}\n.revoked .icon,\n.retired .icon,\n.message .icon {\n color: #626d69;\n background: #edf0ee;\n}\n/* Nothing ran out and nothing wants attention: the holder chose this, so it is only quiet. */\n.retired .name {\n color: var(--verily-muted, #65726c);\n}\n.message {\n color: var(--verily-muted, #65726c);\n}\n.add {\n font-weight: 600;\n}\n@media (prefers-reduced-motion: no-preference) {\n a,\n button {\n transition:\n background 0.12s,\n border-color 0.12s;\n }\n /* The mark is drawn before the answer arrives, so it resolves rather than swaps. */\n .mark,\n .mark path {\n transition:\n opacity 0.18s ease,\n stroke 0.18s ease;\n }\n .spinner {\n border-top-color: transparent;\n opacity: 0.4;\n animation: verily-spin 0.7s linear infinite;\n }\n}\n@keyframes verily-spin {\n to {\n transform: rotate(360deg);\n }\n}\n";
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ // Generated from badge.css by scripts/css.mjs. Do not edit.
2
+ export default ":host {\n display: inline-block;\n max-width: 100%;\n vertical-align: middle;\n}\n* {\n box-sizing: border-box;\n}\n.badge,\n.peek {\n border: 1px solid var(--verily-border, #dce2e0);\n border-radius: 6px;\n background: var(--verily-surface, #fff);\n color: var(--verily-text, #202c29);\n font: var(--verily-font-size, 13px)/1.35\n var(--verily-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);\n}\n.badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n padding: 3px 5px;\n text-decoration: none;\n}\nbutton.badge {\n margin: 0;\n cursor: pointer;\n}\na[href]:hover,\nbutton.badge:hover {\n background: var(--verily-hover, #f3f6f4);\n border-color: var(--verily-border, #dce2e0);\n}\n.badge:focus-visible {\n outline: 2px solid #357ce5;\n outline-offset: 2px;\n}\n.name {\n font-weight: 600;\n overflow-wrap: anywhere;\n min-width: 0;\n}\n.more {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n color: var(--verily-muted, #65726c);\n}\n.label {\n display: none;\n font-size: 11px;\n color: var(--verily-muted, #65726c);\n}\n.badge:hover .label,\n.badge:focus-within .label {\n display: inline;\n}\n.icon {\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n font-size: 12px;\n font-weight: 750;\n}\n.icon .glyph {\n display: block;\n width: 12px;\n height: 12px;\n}\n.mark {\n display: block;\n flex-shrink: 0;\n width: 20px;\n height: 20px;\n}\n.mark.pending {\n opacity: 0.45;\n}\n/* A dim ring where the account will go; it only spins where motion is welcome. */\n.spinner {\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n border: 2px solid currentColor;\n opacity: 0.3;\n}\n.divider {\n flex-shrink: 0;\n width: 1px;\n height: 12px;\n background: var(--verily-border, #dce2e0);\n}\n/* Several accounts, one to a row. The mark stays beside them and the rule runs their height. */\n.badge.stacked {\n align-items: stretch;\n padding: 5px 8px 5px 6px;\n text-align: left;\n}\n.stacked .mark {\n align-self: center;\n}\n.stacked .divider {\n height: auto;\n}\n.rows {\n display: grid;\n gap: 4px;\n min-width: 0;\n}\n.row {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n/*\n * The accounts behind a short pill's count, opened beside it. Shut unless the browser\n * has it open as a popover, so one without popovers never shows it at all.\n */\n.peek {\n display: none;\n position: fixed;\n inset: auto;\n margin: 0;\n padding: 8px 10px;\n width: max-content;\n max-width: min(320px, calc(100vw - 16px));\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.14);\n pointer-events: none;\n overflow: hidden;\n}\n/* A row taken out to fit the window: its own display would otherwise keep it showing. */\n.row[hidden] {\n display: none;\n}\n.peek:popover-open {\n display: block;\n}\n.peek .via {\n display: block;\n margin-top: 6px;\n font-size: 11px;\n color: var(--verily-muted, #65726c);\n}\n.provider {\n display: block;\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n}\n.expired .icon {\n color: #815a12;\n background: #fbefce;\n}\n.revoked .icon,\n.retired .icon,\n.message .icon {\n color: #626d69;\n background: #edf0ee;\n}\n/* Nothing ran out and nothing wants attention: the holder chose this, so it is only quiet. */\n.retired .name {\n color: var(--verily-muted, #65726c);\n}\n.message {\n color: var(--verily-muted, #65726c);\n}\n.add {\n font-weight: 600;\n}\n@media (prefers-reduced-motion: no-preference) {\n a,\n button {\n transition:\n background 0.12s,\n border-color 0.12s;\n }\n /* The mark is drawn before the answer arrives, so it resolves rather than swaps. */\n .mark,\n .mark path {\n transition:\n opacity 0.18s ease,\n stroke 0.18s ease;\n }\n .spinner {\n border-top-color: transparent;\n opacity: 0.4;\n animation: verily-spin 0.7s linear infinite;\n }\n}\n@keyframes verily-spin {\n to {\n transform: rotate(360deg);\n }\n}\n";
@@ -1,72 +1,8 @@
1
1
  import { externalName, statusLabel } from '../core/index.js';
2
+ import badgeCss from './badge.css.js';
2
3
  import { boxMark, clockMark, dashMark, markStyles, paintMark, verificationMark } from './mark.js';
3
4
  import { providerMark } from './provider-mark.js';
4
- const styles = `
5
- :host { display: inline-block; max-width: 100%; vertical-align: middle; }
6
- * { box-sizing: border-box; }
7
- .badge, .peek {
8
- border: 1px solid var(--verily-border, #dce2e0); border-radius: 6px;
9
- background: var(--verily-surface, #fff); color: var(--verily-text, #202c29);
10
- font: var(--verily-font-size, 13px)/1.35
11
- var(--verily-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
12
- }
13
- .badge {
14
- display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 3px 5px;
15
- text-decoration: none;
16
- }
17
- button.badge { margin: 0; cursor: pointer; }
18
- a[href]:hover, button.badge:hover { background: var(--verily-hover, #f3f6f4); border-color: var(--verily-border, #dce2e0); }
19
- .badge:focus-visible { outline: 2px solid #357ce5; outline-offset: 2px; }
20
- .name { font-weight: 600; overflow-wrap: anywhere; min-width: 0; }
21
- .more { flex-shrink: 0; font-size: 11px; font-weight: 600; color: var(--verily-muted, #65726c); }
22
- .label { display: none; font-size: 11px; color: var(--verily-muted, #65726c); }
23
- .badge:hover .label, .badge:focus-within .label { display: inline; }
24
- .icon { display: grid; place-items: center; flex-shrink: 0; width: 20px; height: 20px; border-radius: 50%; font-size: 12px; font-weight: 750; }
25
- .icon .glyph { display: block; width: 12px; height: 12px; }
26
- .mark { display: block; flex-shrink: 0; width: 20px; height: 20px; }
27
- .mark.pending { opacity: .45; }
28
- /* A dim ring where the account will go; it only spins where motion is welcome. */
29
- .spinner {
30
- flex-shrink: 0; width: 14px; height: 14px; border-radius: 50%;
31
- border: 2px solid currentColor; opacity: .3;
32
- }
33
- .divider { flex-shrink: 0; width: 1px; height: 12px; background: var(--verily-border, #dce2e0); }
34
- /* Several accounts, one to a row. The mark stays beside them and the rule runs their height. */
35
- .badge.stacked { align-items: stretch; padding: 5px 8px 5px 6px; text-align: left; }
36
- .stacked .mark { align-self: center; }
37
- .stacked .divider { height: auto; }
38
- .rows { display: grid; gap: 4px; min-width: 0; }
39
- .row { display: flex; align-items: center; gap: 6px; min-width: 0; }
40
- /*
41
- * The accounts behind a short pill's count, opened beside it. Shut unless the browser
42
- * has it open as a popover, so one without popovers never shows it at all.
43
- */
44
- .peek {
45
- display: none; position: fixed; inset: auto; margin: 0; padding: 8px 10px;
46
- width: max-content; max-width: min(320px, calc(100vw - 16px));
47
- border-radius: 8px; box-shadow: 0 6px 20px rgba(0, 0, 0, .14); pointer-events: none;
48
- overflow: hidden;
49
- }
50
- /* A row taken out to fit the window: its own display would otherwise keep it showing. */
51
- .row[hidden] { display: none; }
52
- .peek:popover-open { display: block; }
53
- .peek .via { display: block; margin-top: 6px; font-size: 11px; color: var(--verily-muted, #65726c); }
54
- .provider { display: block; flex-shrink: 0; width: 14px; height: 14px; }
55
- .expired .icon { color: #815a12; background: #fbefce; }
56
- .revoked .icon, .retired .icon, .message .icon { color: #626d69; background: #edf0ee; }
57
- /* Nothing ran out and nothing wants attention: the holder chose this, so it is only quiet. */
58
- .retired .name { color: var(--verily-muted, #65726c); }
59
- .message { color: var(--verily-muted, #65726c); }
60
- .add { font-weight: 600; }
61
- @media (prefers-reduced-motion: no-preference) {
62
- a, button { transition: background .12s, border-color .12s; }
63
- /* The mark is drawn before the answer arrives, so it resolves rather than swaps. */
64
- .mark, .mark path { transition: opacity .18s ease, stroke .18s ease; }
65
- .spinner { border-top-color: transparent; opacity: .4; animation: verily-spin .7s linear infinite; }
66
- }
67
- @keyframes verily-spin { to { transform: rotate(360deg); } }
68
- ${markStyles}
69
- `;
5
+ const styles = badgeCss + markStyles;
70
6
  /** Built once: a re-render adopts the same sheet rather than reparsing the CSS. */
71
7
  let sheet;
72
8
  /** The shadow host already built inside an element, kept so refreshes reuse it. */
@@ -0,0 +1,2 @@
1
+ declare const _default: ".steps > p:first-child {\n margin-top: 0;\n}\n.choices {\n display: grid;\n gap: 8px;\n margin-top: 16px;\n}\n/* With a way back beside it, the heading sits by that and not in the middle. */\nheader.backed h2 {\n margin-right: auto;\n}\n.choices .action {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.choices .more {\n margin-left: auto;\n color: #6b786f;\n font-weight: 500;\n font-size: 12px;\n}\n.row.start {\n justify-content: start;\n}\n/* The block scrolls and its frame does not, so the copy button stays in its corner. */\n.code {\n position: relative;\n margin: 10px 0;\n}\npre {\n margin: 0;\n padding: 10px 64px 10px 12px;\n border: 1px solid #e0e6df;\n border-radius: 8px;\n background: #f6f8f5;\n font:\n 12px/1.5 ui-monospace,\n monospace;\n white-space: pre;\n overflow-x: auto;\n}\n.code .action {\n position: absolute;\n top: 9px;\n right: 9px;\n padding: 3px 8px;\n font-size: 11px;\n}\nlabel {\n display: grid;\n gap: 4px;\n margin-top: 12px;\n font-weight: 600;\n}\ninput[type='url'],\ninput[type='email'],\ninput[type='tel'],\ninput[type='text'],\ntextarea {\n width: 100%;\n padding: 8px 10px;\n border: 1px solid #cfd8d2;\n border-radius: 8px;\n font:\n 13px/1.4 system-ui,\n sans-serif;\n color: inherit;\n background: #fff;\n}\ntextarea {\n font-family: ui-monospace, monospace;\n font-size: 12px;\n resize: vertical;\n}\nfieldset {\n margin: 16px 0 0;\n padding: 10px 14px;\n border: 1px solid #e0e6df;\n border-radius: 10px;\n}\nlegend {\n padding: 0 4px;\n color: #6b786f;\n font-size: 11px;\n font-weight: 550;\n}\nfieldset label {\n display: flex;\n align-items: baseline;\n gap: 8px;\n margin-top: 4px;\n font-weight: 500;\n}\n/* A radio has no baseline to speak of, so it is set in the middle of the label's first line. */\nfieldset input {\n flex-shrink: 0;\n align-self: flex-start;\n width: 13px;\n height: 13px;\n font: inherit;\n margin-top: 4px;\n margin-top: calc((1lh - 13px) / 2);\n margin-bottom: 0;\n}\n.error {\n color: #9b2c33;\n}\n";
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ // Generated from connect-dialog.css by scripts/css.mjs. Do not edit.
2
+ export default ".steps > p:first-child {\n margin-top: 0;\n}\n.choices {\n display: grid;\n gap: 8px;\n margin-top: 16px;\n}\n/* With a way back beside it, the heading sits by that and not in the middle. */\nheader.backed h2 {\n margin-right: auto;\n}\n.choices .action {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.choices .more {\n margin-left: auto;\n color: #6b786f;\n font-weight: 500;\n font-size: 12px;\n}\n.row.start {\n justify-content: start;\n}\n/* The block scrolls and its frame does not, so the copy button stays in its corner. */\n.code {\n position: relative;\n margin: 10px 0;\n}\npre {\n margin: 0;\n padding: 10px 64px 10px 12px;\n border: 1px solid #e0e6df;\n border-radius: 8px;\n background: #f6f8f5;\n font:\n 12px/1.5 ui-monospace,\n monospace;\n white-space: pre;\n overflow-x: auto;\n}\n.code .action {\n position: absolute;\n top: 9px;\n right: 9px;\n padding: 3px 8px;\n font-size: 11px;\n}\nlabel {\n display: grid;\n gap: 4px;\n margin-top: 12px;\n font-weight: 600;\n}\ninput[type='url'],\ninput[type='email'],\ninput[type='tel'],\ninput[type='text'],\ntextarea {\n width: 100%;\n padding: 8px 10px;\n border: 1px solid #cfd8d2;\n border-radius: 8px;\n font:\n 13px/1.4 system-ui,\n sans-serif;\n color: inherit;\n background: #fff;\n}\ntextarea {\n font-family: ui-monospace, monospace;\n font-size: 12px;\n resize: vertical;\n}\nfieldset {\n margin: 16px 0 0;\n padding: 10px 14px;\n border: 1px solid #e0e6df;\n border-radius: 10px;\n}\nlegend {\n padding: 0 4px;\n color: #6b786f;\n font-size: 11px;\n font-weight: 550;\n}\nfieldset label {\n display: flex;\n align-items: baseline;\n gap: 8px;\n margin-top: 4px;\n font-weight: 500;\n}\n/* A radio has no baseline to speak of, so it is set in the middle of the label's first line. */\nfieldset input {\n flex-shrink: 0;\n align-self: flex-start;\n width: 13px;\n height: 13px;\n font: inherit;\n margin-top: 4px;\n margin-top: calc((1lh - 13px) / 2);\n margin-bottom: 0;\n}\n.error {\n color: #9b2c33;\n}\n";
@@ -1,29 +1,8 @@
1
- import { accountCard, backMark, closeMark, linkMark, node, namedCard, outward, stampLink, styles, } from './evidence-dialog.js';
1
+ import { accountCard, backMark, closeMark, holdPage, linkMark, node, namedCard, outward, stampLink, styles, } from './evidence-dialog.js';
2
2
  import { providerMark } from './provider-mark.js';
3
+ import connectCss from './connect-dialog.css.js';
3
4
  import { version } from '../version.js';
4
- const connectStyles = `
5
- ${styles}
6
- .steps > p:first-child { margin-top: 0; }
7
- .choices { display: grid; gap: 8px; margin-top: 16px; }
8
- /* With a way back beside it, the heading sits by that and not in the middle. */
9
- header.backed h2 { margin-right: auto; }
10
- .choices .action { display: flex; align-items: center; gap: 8px; }
11
- .choices .more { margin-left: auto; color: #6b786f; font-weight: 500; font-size: 12px; }
12
- .row.start { justify-content: start; }
13
- /* The block scrolls and its frame does not, so the copy button stays in its corner. */
14
- .code { position: relative; margin: 10px 0; }
15
- pre { margin: 0; padding: 10px 64px 10px 12px; border: 1px solid #e0e6df; border-radius: 8px; background: #f6f8f5; font: 12px/1.5 ui-monospace, monospace; white-space: pre; overflow-x: auto; }
16
- .code .action { position: absolute; top: 9px; right: 9px; padding: 3px 8px; font-size: 11px; }
17
- label { display: grid; gap: 4px; margin-top: 12px; font-weight: 600; }
18
- input[type=url], input[type=email], input[type=tel], input[type=text], textarea { width: 100%; padding: 8px 10px; border: 1px solid #cfd8d2; border-radius: 8px; font: 13px/1.4 system-ui, sans-serif; color: inherit; background: #fff; }
19
- textarea { font-family: ui-monospace, monospace; font-size: 12px; resize: vertical; }
20
- fieldset { margin: 16px 0 0; padding: 10px 14px; border: 1px solid #e0e6df; border-radius: 10px; }
21
- legend { padding: 0 4px; color: #6b786f; font-size: 11px; font-weight: 550; }
22
- fieldset label { display: flex; align-items: baseline; gap: 8px; margin-top: 4px; font-weight: 500; }
23
- /* A radio has no baseline to speak of, so it is set in the middle of the label's first line. */
24
- fieldset input { flex-shrink: 0; align-self: flex-start; width: 13px; height: 13px; font: inherit; margin-top: 4px; margin-top: calc((1lh - 13px) / 2); margin-bottom: 0; }
25
- .error { color: #9b2c33; }
26
- `;
5
+ const connectStyles = styles + connectCss;
27
6
  /** Puts a copy control on a block the holder must reproduce exactly, as the flow page does. */
28
7
  function codeBlock(code) {
29
8
  const block = node('div', '', 'code');
@@ -528,6 +507,7 @@ renew) {
528
507
  .catch(() => { })
529
508
  .then(() => resolve(outcome));
530
509
  }, { once: true }));
510
+ holdPage(dialog);
531
511
  dialog.showModal();
532
512
  void choose();
533
513
  return closed;
@@ -0,0 +1,2 @@
1
+ declare const _default: "* {\n box-sizing: border-box;\n}\ndialog {\n width: min(460px, calc(100vw - 32px));\n max-height: calc(100dvh - 40px);\n margin: auto;\n padding: 0;\n overflow: hidden;\n border: 1px solid #dce2de;\n border-radius: 16px;\n background: #fff;\n color: #23312b;\n box-shadow: 0 24px 90px #10201935;\n font:\n 13px/1.6 system-ui,\n sans-serif;\n}\n/* The pane scrolls inside the dialog, which clips its scrollbar to the rounded corners.\n Contain keeps a scroll past its end from scrolling the page behind. */\ndialog[open] {\n display: flex;\n flex-direction: column;\n}\n.pane {\n min-height: 0;\n padding: 24px;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\ndialog::backdrop {\n background: #15271f66;\n}\nheader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 20px;\n}\nh2 {\n margin: 0;\n font-size: 18px;\n font-weight: 650;\n letter-spacing: -0.3px;\n}\nbutton {\n width: 32px;\n height: 32px;\n border: 1px solid #dce2de;\n border-radius: 8px;\n background: #fff;\n color: #52645a;\n font:\n 20px system-ui,\n sans-serif;\n cursor: pointer;\n}\nbutton:hover {\n background: #f2f5f1;\n}\n.icon {\n display: grid;\n place-items: center;\n padding: 0;\n}\n.icon svg {\n width: 16px;\n height: 16px;\n}\n.title {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n.all {\n display: grid;\n place-items: center;\n width: 28px;\n height: 28px;\n border-radius: 8px;\n color: #52645a;\n}\n.all:hover {\n background: #f2f5f1;\n}\n.all svg {\n width: 16px;\n height: 16px;\n}\na {\n color: #245f43;\n text-underline-offset: 3px;\n overflow-wrap: anywhere;\n}\na:focus-visible,\nbutton:focus-visible,\nsummary:focus-visible {\n outline: 2px solid #357ce5;\n outline-offset: 3px;\n}\n/* An element given its own display would otherwise go on showing while hidden. */\n[hidden] {\n display: none !important;\n}\n.summary {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 16px;\n}\n.mark {\n width: 36px;\n height: 36px;\n flex-shrink: 0;\n}\n.provider {\n width: 14px;\n height: 14px;\n}\n.state {\n font-size: 14px;\n font-weight: 650;\n line-height: 1.4;\n}\n.muted {\n color: #6b786f;\n font-size: 12px;\n}\n.account {\n position: relative;\n}\n.veiled > :not(.checking) {\n filter: blur(4px);\n opacity: 0.55;\n user-select: none;\n}\n.checking {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: #23312b;\n font-size: 13px;\n font-weight: 600;\n}\n.spinner {\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spin 0.7s linear infinite;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .spinner {\n animation: none;\n }\n}\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.summary .signed {\n display: inline-block;\n margin-left: 5px;\n padding: 0 6px;\n border: 1px solid #cfdcd3;\n border-radius: 999px;\n font-size: 10px;\n font-weight: 550;\n line-height: 1.3;\n text-decoration: none;\n vertical-align: 1px;\n}\n.summary a.signed:hover {\n background: #f2f5f1;\n}\n.summary .signed.bad {\n border-color: #b3261e;\n color: #b3261e;\n font-weight: 650;\n cursor: help;\n}\n.caution {\n display: inline-block;\n margin-left: 3px;\n color: #a15c00;\n vertical-align: -1.5px;\n}\n.caution svg {\n display: block;\n width: 10px;\n height: 10px;\n}\n.account {\n padding: 14px 16px;\n border: 1px solid #e0e6df;\n border-radius: 10px;\n margin-top: 12px;\n}\n.account h3 {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0 0 3px;\n color: #6b786f;\n font-size: 11px;\n font-weight: 550;\n}\n.account a,\n.account strong {\n font-weight: 650;\n font-size: 14px;\n}\n/* The account's own name carries that weight; a link inside a line of prose does not. */\n.summary a,\n.method a,\n.additional a {\n font: inherit;\n}\n.reference {\n margin-top: 2px;\n}\n/* A fingerprint in fours: it breaks between groups, and copies as one unbroken word. */\n.fingerprint {\n font-family: ui-monospace, monospace;\n text-wrap: balance;\n}\n.fingerprint span {\n display: inline-block;\n margin-right: 0.5ch;\n}\n.method {\n margin-top: 8px;\n}\n/* In the corner the heading leaves empty, apart from everything that was checked. */\n.listing {\n position: absolute;\n top: 12px;\n right: 14px;\n padding: 1px 8px;\n border: 1px solid #e0e6df;\n border-radius: 999px;\n color: #6b786f;\n font-size: 11px;\n font-weight: 550;\n}\n.listing.preferred {\n border-color: #245f43;\n color: #245f43;\n}\n/* Quiet, as its pill is: the surface and the name step back, and the controls do not. */\n.account.retired {\n background: #f6f8f5;\n}\n.account.retired .identity > a,\n.account.retired .identity > strong,\n.account.retired .named > a,\n.account.retired .named > strong {\n color: #6b786f;\n}\n/* The name, then a dot and the account's creation date. The date wraps under a long name. */\n/* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */\n.named {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0 14px;\n margin-left: -4px;\n padding-left: 4px;\n overflow-x: clip;\n}\n.since {\n position: relative;\n white-space: nowrap;\n}\n/* The dot sits in the gap before the date. */\n.since::before {\n content: '\u00B7';\n position: absolute;\n left: -7px;\n transform: translateX(-50%);\n}\n.account.retired .mark {\n opacity: 0.45;\n}\n.group {\n margin: 20px 0 0;\n color: #6b786f;\n font-size: 11px;\n font-weight: 550;\n}\nsummary.group {\n display: flex;\n align-items: center;\n gap: 4px;\n width: fit-content;\n border-radius: 4px;\n list-style: none;\n cursor: pointer;\n}\nsummary::-webkit-details-marker {\n display: none;\n}\n.fold {\n display: flex;\n align-items: center;\n gap: 4px;\n width: auto;\n height: auto;\n padding: 2px 8px 2px 4px;\n margin-left: -4px;\n border: 0;\n background: none;\n font:\n 550 12px/1.6 system-ui,\n sans-serif;\n}\n.fold svg,\nsummary svg {\n width: 12px;\n height: 12px;\n}\n.fold[aria-expanded='true'] svg,\ndetails[open] > summary svg {\n transform: rotate(90deg);\n}\n@media (prefers-reduced-motion: no-preference) {\n .fold svg,\n summary svg {\n transition: transform 0.15s;\n }\n}\n/* The fold and what the holder may do share a line, until a question needs the whole of it. */\n.bar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 10px;\n}\n.account .bar .row {\n margin: 0 0 0 auto;\n}\n.additional {\n margin-top: 2px;\n padding-left: 12px;\n}\n.joiner {\n display: block;\n width: 20px;\n height: 20px;\n margin: 8px auto -4px;\n color: #90a096;\n}\ndl {\n margin: 16px 0 0;\n padding-top: 12px;\n border-top: 1px solid #e5e9e3;\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 5px 16px;\n font-size: 11px;\n}\ndt {\n color: #6b786f;\n}\ndd {\n margin: 0;\n text-align: right;\n overflow-wrap: anywhere;\n}\n.explanation {\n margin-top: 16px;\n}\n.action {\n width: auto;\n height: auto;\n padding: 9px 14px;\n font:\n 600 13px/1.3 system-ui,\n sans-serif;\n color: #23312b;\n text-align: left;\n}\n.action.primary {\n border-color: #245f43;\n background: #245f43;\n color: #fff;\n}\n.action.primary:hover {\n background: #1d4f37;\n}\n.action:disabled {\n opacity: 0.6;\n cursor: progress;\n}\n.action.danger:hover {\n border-color: #b3261e;\n background: #fdecea;\n color: #b3261e;\n}\n.row {\n display: flex;\n justify-content: end;\n gap: 8px;\n margin-top: 16px;\n}\n.account .row {\n align-items: center;\n justify-content: start;\n flex-wrap: wrap;\n margin-top: 12px;\n}\n.account .action {\n padding: 5px 10px;\n font-size: 12px;\n}\n.account .action.dots {\n display: grid;\n place-items: center;\n align-self: stretch;\n width: 30px;\n padding: 0;\n}\n.dots svg {\n width: 16px;\n height: 16px;\n}\n/* Placed under its button each time it opens, above the dialog and clipped by none of it. */\n.menu {\n position: fixed;\n inset: auto;\n margin: 0;\n min-width: 170px;\n padding: 6px;\n border: 1px solid #dce2de;\n border-radius: 10px;\n background: #fff;\n color: #23312b;\n box-shadow: 0 12px 40px #10201930;\n}\n.menu:popover-open {\n display: grid;\n}\n.account .menu .action {\n border: 0;\n border-radius: 6px;\n padding: 6px 10px;\n font-weight: 550;\n}\n.menu .danger {\n color: #b3261e;\n}\n.menu hr {\n width: 100%;\n margin: 4px 0;\n border: 0;\n border-top: 1px solid #e5e9e3;\n}\nfooter {\n display: flex;\n align-items: center;\n justify-content: start;\n gap: 6px;\n margin: 20px 0 -8px;\n color: #9aa9a0;\n font-size: 11px;\n}\nfooter a {\n display: flex;\n color: inherit;\n}\n.logo {\n display: block;\n height: 13px;\n}\n/* Four or more accounts on a wide screen: two equal cards to a row. */\n@media (min-width: 960px) {\n dialog.many {\n width: min(900px, calc(100vw - 32px));\n }\n dialog.many .tiles {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n margin-top: 12px;\n }\n /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall as\n the taller card's part, so the verdicts and details of neighbouring cards line up. */\n dialog.many .tiles > .account {\n display: grid;\n grid-row: span 3;\n grid-template-rows: subgrid;\n row-gap: 0;\n margin-top: 0;\n }\n /* The dates sit at the foot of the third part, so they line up too. */\n dialog.many .tiles .rest {\n display: flex;\n flex-direction: column;\n }\n dialog.many .tiles .rest > dl {\n margin-top: auto;\n }\n dialog.many .tiles .rest > :has(+ dl) {\n margin-bottom: 16px;\n }\n /* The site's card is one tile wide, centred over the two columns. */\n dialog.many .local {\n width: calc(50% - 6px);\n margin-inline: auto;\n text-align: center;\n }\n dialog.many .local h3,\n dialog.many .local .named {\n justify-content: center;\n }\n dialog.many .local .additional {\n padding-left: 0;\n }\n}\n";
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ // Generated from evidence-dialog.css by scripts/css.mjs. Do not edit.
2
+ export default "* {\n box-sizing: border-box;\n}\ndialog {\n width: min(460px, calc(100vw - 32px));\n max-height: calc(100dvh - 40px);\n margin: auto;\n padding: 0;\n overflow: hidden;\n border: 1px solid #dce2de;\n border-radius: 16px;\n background: #fff;\n color: #23312b;\n box-shadow: 0 24px 90px #10201935;\n font:\n 13px/1.6 system-ui,\n sans-serif;\n}\n/* The pane scrolls inside the dialog, which clips its scrollbar to the rounded corners.\n Contain keeps a scroll past its end from scrolling the page behind. */\ndialog[open] {\n display: flex;\n flex-direction: column;\n}\n.pane {\n min-height: 0;\n padding: 24px;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\ndialog::backdrop {\n background: #15271f66;\n}\nheader {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 20px;\n}\nh2 {\n margin: 0;\n font-size: 18px;\n font-weight: 650;\n letter-spacing: -0.3px;\n}\nbutton {\n width: 32px;\n height: 32px;\n border: 1px solid #dce2de;\n border-radius: 8px;\n background: #fff;\n color: #52645a;\n font:\n 20px system-ui,\n sans-serif;\n cursor: pointer;\n}\nbutton:hover {\n background: #f2f5f1;\n}\n.icon {\n display: grid;\n place-items: center;\n padding: 0;\n}\n.icon svg {\n width: 16px;\n height: 16px;\n}\n.title {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n.all {\n display: grid;\n place-items: center;\n width: 28px;\n height: 28px;\n border-radius: 8px;\n color: #52645a;\n}\n.all:hover {\n background: #f2f5f1;\n}\n.all svg {\n width: 16px;\n height: 16px;\n}\na {\n color: #245f43;\n text-underline-offset: 3px;\n overflow-wrap: anywhere;\n}\na:focus-visible,\nbutton:focus-visible,\nsummary:focus-visible {\n outline: 2px solid #357ce5;\n outline-offset: 3px;\n}\n/* An element given its own display would otherwise go on showing while hidden. */\n[hidden] {\n display: none !important;\n}\n.summary {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 16px;\n}\n.mark {\n width: 36px;\n height: 36px;\n flex-shrink: 0;\n}\n.provider {\n width: 14px;\n height: 14px;\n}\n.state {\n font-size: 14px;\n font-weight: 650;\n line-height: 1.4;\n}\n.muted {\n color: #6b786f;\n font-size: 12px;\n}\n.account {\n position: relative;\n}\n.veiled > :not(.checking) {\n filter: blur(4px);\n opacity: 0.55;\n user-select: none;\n}\n.checking {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: #23312b;\n font-size: 13px;\n font-weight: 600;\n}\n.spinner {\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spin 0.7s linear infinite;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .spinner {\n animation: none;\n }\n}\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.summary .signed {\n display: inline-block;\n margin-left: 5px;\n padding: 0 6px;\n border: 1px solid #cfdcd3;\n border-radius: 999px;\n font-size: 10px;\n font-weight: 550;\n line-height: 1.3;\n text-decoration: none;\n vertical-align: 1px;\n}\n.summary a.signed:hover {\n background: #f2f5f1;\n}\n.summary .signed.bad {\n border-color: #b3261e;\n color: #b3261e;\n font-weight: 650;\n cursor: help;\n}\n.caution {\n display: inline-block;\n margin-left: 3px;\n color: #a15c00;\n vertical-align: -1.5px;\n}\n.caution svg {\n display: block;\n width: 10px;\n height: 10px;\n}\n.account {\n padding: 14px 16px;\n border: 1px solid #e0e6df;\n border-radius: 10px;\n margin-top: 12px;\n}\n.account h3 {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0 0 3px;\n color: #6b786f;\n font-size: 11px;\n font-weight: 550;\n}\n.account a,\n.account strong {\n font-weight: 650;\n font-size: 14px;\n}\n/* The account's own name carries that weight; a link inside a line of prose does not. */\n.summary a,\n.method a,\n.additional a {\n font: inherit;\n}\n.reference {\n margin-top: 2px;\n}\n/* A fingerprint in fours: it breaks between groups, and copies as one unbroken word. */\n.fingerprint {\n font-family: ui-monospace, monospace;\n text-wrap: balance;\n}\n.fingerprint span {\n display: inline-block;\n margin-right: 0.5ch;\n}\n.method {\n margin-top: 8px;\n}\n/* In the corner the heading leaves empty, apart from everything that was checked. */\n.listing {\n position: absolute;\n top: 12px;\n right: 14px;\n padding: 1px 8px;\n border: 1px solid #e0e6df;\n border-radius: 999px;\n color: #6b786f;\n font-size: 11px;\n font-weight: 550;\n}\n.listing.preferred {\n border-color: #245f43;\n color: #245f43;\n}\n/* Quiet, as its pill is: the surface and the name step back, and the controls do not. */\n.account.retired {\n background: #f6f8f5;\n}\n.account.retired .identity > a,\n.account.retired .identity > strong,\n.account.retired .named > a,\n.account.retired .named > strong {\n color: #6b786f;\n}\n/* The name, then a dot and the account's creation date. The date wraps under a long name. */\n/* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */\n.named {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0 14px;\n margin-left: -4px;\n padding-left: 4px;\n overflow-x: clip;\n}\n.since {\n position: relative;\n white-space: nowrap;\n}\n/* The dot sits in the gap before the date. */\n.since::before {\n content: '·';\n position: absolute;\n left: -7px;\n transform: translateX(-50%);\n}\n.account.retired .mark {\n opacity: 0.45;\n}\n.group {\n margin: 20px 0 0;\n color: #6b786f;\n font-size: 11px;\n font-weight: 550;\n}\nsummary.group {\n display: flex;\n align-items: center;\n gap: 4px;\n width: fit-content;\n border-radius: 4px;\n list-style: none;\n cursor: pointer;\n}\nsummary::-webkit-details-marker {\n display: none;\n}\n.fold {\n display: flex;\n align-items: center;\n gap: 4px;\n width: auto;\n height: auto;\n padding: 2px 8px 2px 4px;\n margin-left: -4px;\n border: 0;\n background: none;\n font:\n 550 12px/1.6 system-ui,\n sans-serif;\n}\n.fold svg,\nsummary svg {\n width: 12px;\n height: 12px;\n}\n.fold[aria-expanded='true'] svg,\ndetails[open] > summary svg {\n transform: rotate(90deg);\n}\n@media (prefers-reduced-motion: no-preference) {\n .fold svg,\n summary svg {\n transition: transform 0.15s;\n }\n}\n/* The fold and what the holder may do share a line, until a question needs the whole of it. */\n.bar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 10px;\n}\n.account .bar .row {\n margin: 0 0 0 auto;\n}\n.additional {\n margin-top: 2px;\n padding-left: 12px;\n}\n.joiner {\n display: block;\n width: 20px;\n height: 20px;\n margin: 8px auto -4px;\n color: #90a096;\n}\ndl {\n margin: 16px 0 0;\n padding-top: 12px;\n border-top: 1px solid #e5e9e3;\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 5px 16px;\n font-size: 11px;\n}\ndt {\n color: #6b786f;\n}\ndd {\n margin: 0;\n text-align: right;\n overflow-wrap: anywhere;\n}\n.explanation {\n margin-top: 16px;\n}\n.action {\n width: auto;\n height: auto;\n padding: 9px 14px;\n font:\n 600 13px/1.3 system-ui,\n sans-serif;\n color: #23312b;\n text-align: left;\n}\n.action.primary {\n border-color: #245f43;\n background: #245f43;\n color: #fff;\n}\n.action.primary:hover {\n background: #1d4f37;\n}\n.action:disabled {\n opacity: 0.6;\n cursor: progress;\n}\n.action.danger:hover {\n border-color: #b3261e;\n background: #fdecea;\n color: #b3261e;\n}\n.row {\n display: flex;\n justify-content: end;\n gap: 8px;\n margin-top: 16px;\n}\n.account .row {\n align-items: center;\n justify-content: start;\n flex-wrap: wrap;\n margin-top: 12px;\n}\n.account .action {\n padding: 5px 10px;\n font-size: 12px;\n}\n.account .action.dots {\n display: grid;\n place-items: center;\n align-self: stretch;\n width: 30px;\n padding: 0;\n}\n.dots svg {\n width: 16px;\n height: 16px;\n}\n/* Placed under its button each time it opens, above the dialog and clipped by none of it. */\n.menu {\n position: fixed;\n inset: auto;\n margin: 0;\n min-width: 170px;\n padding: 6px;\n border: 1px solid #dce2de;\n border-radius: 10px;\n background: #fff;\n color: #23312b;\n box-shadow: 0 12px 40px #10201930;\n}\n.menu:popover-open {\n display: grid;\n}\n.account .menu .action {\n border: 0;\n border-radius: 6px;\n padding: 6px 10px;\n font-weight: 550;\n}\n.menu .danger {\n color: #b3261e;\n}\n.menu hr {\n width: 100%;\n margin: 4px 0;\n border: 0;\n border-top: 1px solid #e5e9e3;\n}\nfooter {\n display: flex;\n align-items: center;\n justify-content: start;\n gap: 6px;\n margin: 20px 0 -8px;\n color: #9aa9a0;\n font-size: 11px;\n}\nfooter a {\n display: flex;\n color: inherit;\n}\n.logo {\n display: block;\n height: 13px;\n}\n/* Four or more accounts on a wide screen: two equal cards to a row. */\n@media (min-width: 960px) {\n dialog.many {\n width: min(900px, calc(100vw - 32px));\n }\n dialog.many .tiles {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n margin-top: 12px;\n }\n /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall as\n the taller card's part, so the verdicts and details of neighbouring cards line up. */\n dialog.many .tiles > .account {\n display: grid;\n grid-row: span 3;\n grid-template-rows: subgrid;\n row-gap: 0;\n margin-top: 0;\n }\n /* The dates sit at the foot of the third part, so they line up too. */\n dialog.many .tiles .rest {\n display: flex;\n flex-direction: column;\n }\n dialog.many .tiles .rest > dl {\n margin-top: auto;\n }\n dialog.many .tiles .rest > :has(+ dl) {\n margin-bottom: 16px;\n }\n /* The site's card is one tile wide, centred over the two columns. */\n dialog.many .local {\n width: calc(50% - 6px);\n margin-inline: auto;\n text-align: center;\n }\n dialog.many .local h3,\n dialog.many .local .named {\n justify-content: center;\n }\n dialog.many .local .additional {\n padding-left: 0;\n }\n}\n";
@@ -1,7 +1,12 @@
1
1
  import { type Evidence, type ExternalAccount } from '../core/index.js';
2
2
  /** The logotype as a link to the project, as both dialogs carry it in their footer. */
3
3
  export declare function stampLink(): HTMLAnchorElement;
4
- export declare const styles = "\n * { box-sizing: border-box; }\n dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 0; overflow: hidden; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }\n /* The pane scrolls inside the dialog, which clips its scrollbar to the rounded corners. */\n dialog[open] { display: flex; flex-direction: column; }\n .pane { min-height: 0; padding: 24px; overflow-y: auto; }\n dialog::backdrop { background: #15271f66; }\n header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }\n h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }\n button { width: 32px; height: 32px; border: 1px solid #dce2de; border-radius: 8px; background: #fff; color: #52645a; font: 20px system-ui, sans-serif; cursor: pointer; }\n button:hover { background: #f2f5f1; }\n .icon { display: grid; place-items: center; padding: 0; }\n .icon svg { width: 16px; height: 16px; }\n .title { display: flex; align-items: center; gap: 6px; min-width: 0; }\n .all { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: #52645a; }\n .all:hover { background: #f2f5f1; }\n .all svg { width: 16px; height: 16px; }\n a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }\n a:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }\n /* An element given its own display would otherwise go on showing while hidden. */\n [hidden] { display: none !important; }\n .summary { display: flex; align-items: center; gap: 8px; margin-top: 16px; }\n .mark { width: 36px; height: 36px; flex-shrink: 0; }\n .provider { width: 14px; height: 14px; }\n .state { font-size: 14px; font-weight: 650; line-height: 1.4; }\n .muted { color: #6b786f; font-size: 12px; }\n .account { position: relative; }\n .veiled > :not(.checking) { filter: blur(4px); opacity: .55; user-select: none; }\n .checking { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: #23312b; font-size: 13px; font-weight: 600; }\n .spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }\n @keyframes spin { to { transform: rotate(360deg); } }\n @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }\n /* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n .summary .signed { display: inline-block; margin-left: 5px; padding: 0 6px; border: 1px solid #cfdcd3; border-radius: 999px; font-size: 10px; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: 1px; }\n .summary a.signed:hover { background: #f2f5f1; }\n .summary .signed.bad { border-color: #b3261e; color: #b3261e; font-weight: 650; cursor: help; }\n .caution { display: inline-block; margin-left: 3px; color: #a15c00; vertical-align: -1.5px; }\n .caution svg { display: block; width: 10px; height: 10px; }\n .account { padding: 14px 16px; border: 1px solid #e0e6df; border-radius: 10px; margin-top: 12px; }\n .account h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 3px; color: #6b786f; font-size: 11px; font-weight: 550; }\n .account a, .account strong { font-weight: 650; font-size: 14px; }\n /* The account's own name carries that weight; a link inside a line of prose does not. */\n .summary a, .method a, .additional a { font: inherit; }\n .reference { margin-top: 2px; }\n /* A fingerprint in fours: it breaks between groups, and copies as one unbroken word. */\n .fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n .fingerprint span { display: inline-block; margin-right: .5ch; }\n .method { margin-top: 8px; }\n /* In the corner the heading leaves empty, apart from everything that was checked. */\n .listing { position: absolute; top: 12px; right: 14px; padding: 1px 8px; border: 1px solid #e0e6df; border-radius: 999px; color: #6b786f; font-size: 11px; font-weight: 550; }\n .listing.preferred { border-color: #245f43; color: #245f43; }\n /* Quiet, as its pill is: the surface and the name step back, and the controls do not. */\n .account.retired { background: #f6f8f5; }\n .account.retired > a, .account.retired > strong, .account.retired > .named > a, .account.retired > .named > strong { color: #6b786f; }\n /* The name, then a dot and the account's creation date. The date wraps under a long name. */\n /* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */\n .named { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; margin-left: -4px; padding-left: 4px; overflow-x: clip; }\n .since { position: relative; white-space: nowrap; }\n /* The dot sits in the gap before the date. */\n .since::before { content: \"\u00B7\"; position: absolute; left: -7px; transform: translateX(-50%); }\n .account.retired .mark { opacity: .45; }\n .group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }\n summary.group { display: flex; align-items: center; gap: 4px; width: fit-content; border-radius: 4px; list-style: none; cursor: pointer; }\n summary::-webkit-details-marker { display: none; }\n .fold { display: flex; align-items: center; gap: 4px; width: auto; height: auto; padding: 2px 8px 2px 4px; margin-left: -4px; border: 0; background: none; font: 550 12px/1.6 system-ui, sans-serif; }\n .fold svg, summary svg { width: 12px; height: 12px; }\n .fold[aria-expanded=\"true\"] svg, details[open] > summary svg { transform: rotate(90deg); }\n @media (prefers-reduced-motion: no-preference) { .fold svg, summary svg { transition: transform .15s; } }\n /* The fold and what the holder may do share a line, until a question needs the whole of it. */\n .bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 10px; }\n .account .bar .row { margin: 0 0 0 auto; }\n .additional { margin-top: 2px; padding-left: 12px; }\n .joiner { display: block; width: 20px; height: 20px; margin: 8px auto -4px; color: #90a096; }\n dl { margin: 16px 0 0; padding-top: 12px; border-top: 1px solid #e5e9e3; display: grid; grid-template-columns: auto 1fr; gap: 5px 16px; font-size: 11px; }\n dt { color: #6b786f; }\n dd { margin: 0; text-align: right; overflow-wrap: anywhere; }\n .explanation { margin-top: 16px; }\n .action { width: auto; height: auto; padding: 9px 14px; font: 600 13px/1.3 system-ui, sans-serif; color: #23312b; text-align: left; }\n .action.primary { border-color: #245f43; background: #245f43; color: #fff; }\n .action.primary:hover { background: #1d4f37; }\n .action:disabled { opacity: .6; cursor: progress; }\n .action.danger:hover { border-color: #b3261e; background: #fdecea; color: #b3261e; }\n .row { display: flex; justify-content: end; gap: 8px; margin-top: 16px; }\n .account .row { align-items: center; justify-content: start; flex-wrap: wrap; margin-top: 12px; }\n .account .action { padding: 5px 10px; font-size: 12px; }\n .account .action.dots { display: grid; place-items: center; align-self: stretch; width: 30px; padding: 0; }\n .dots svg { width: 16px; height: 16px; }\n /* Placed under its button each time it opens, above the dialog and clipped by none of it. */\n .menu { position: fixed; inset: auto; margin: 0; min-width: 170px; padding: 6px; border: 1px solid #dce2de; border-radius: 10px; background: #fff; color: #23312b; box-shadow: 0 12px 40px #10201930; }\n .menu:popover-open { display: grid; }\n .account .menu .action { border: 0; border-radius: 6px; padding: 6px 10px; font-weight: 550; }\n .menu .danger { color: #b3261e; }\n .menu hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid #e5e9e3; }\n footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }\n footer a { display: flex; color: inherit; }\n .logo { display: block; height: 13px; }\n \n @media (prefers-reduced-motion: no-preference) {\n .mark.current path:last-child { animation: verily-draw .8s cubic-bezier(.65, 0, .35, 1) .1s backwards; }\n }\n @keyframes verily-draw { from { stroke-dasharray: 0 176; } to { stroke-dasharray: 108 176; } }\n\n";
4
+ /**
5
+ * Stops the page behind an open dialog from scrolling, and puts back the
6
+ * page's own inline styles once the last open dialog closes.
7
+ */
8
+ export declare function holdPage(dialog: HTMLDialogElement): void;
9
+ export declare const styles: string;
5
10
  export declare function node<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string, className?: string): HTMLElementTagNameMap[K];
6
11
  /**
7
12
  * Points an anchor out of the dialog. Every link here opens in a new tab: the dialog is
@@ -1,5 +1,6 @@
1
1
  import { attestationLabel, externalId, externalIdGroups, externalLink, externalName, externalSince, lastProved, localSide, proofTitle, statusLabel, } from '../core/index.js';
2
2
  import { markStyles, verificationMark } from './mark.js';
3
+ import dialogCss from './evidence-dialog.css.js';
3
4
  import { providerMark } from './provider-mark.js';
4
5
  import { onSignatureResult, signature, standing } from './signed.js';
5
6
  import { projectUrl, verilyLogo } from './logo.js';
@@ -11,104 +12,37 @@ export function stampLink() {
11
12
  return link;
12
13
  }
13
14
  const openDialogs = new WeakMap();
14
- export const styles = `
15
- * { box-sizing: border-box; }
16
- dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 0; overflow: hidden; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }
17
- /* The pane scrolls inside the dialog, which clips its scrollbar to the rounded corners. */
18
- dialog[open] { display: flex; flex-direction: column; }
19
- .pane { min-height: 0; padding: 24px; overflow-y: auto; }
20
- dialog::backdrop { background: #15271f66; }
21
- header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
22
- h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }
23
- button { width: 32px; height: 32px; border: 1px solid #dce2de; border-radius: 8px; background: #fff; color: #52645a; font: 20px system-ui, sans-serif; cursor: pointer; }
24
- button:hover { background: #f2f5f1; }
25
- .icon { display: grid; place-items: center; padding: 0; }
26
- .icon svg { width: 16px; height: 16px; }
27
- .title { display: flex; align-items: center; gap: 6px; min-width: 0; }
28
- .all { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: #52645a; }
29
- .all:hover { background: #f2f5f1; }
30
- .all svg { width: 16px; height: 16px; }
31
- a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }
32
- a:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }
33
- /* An element given its own display would otherwise go on showing while hidden. */
34
- [hidden] { display: none !important; }
35
- .summary { display: flex; align-items: center; gap: 8px; margin-top: 16px; }
36
- .mark { width: 36px; height: 36px; flex-shrink: 0; }
37
- .provider { width: 14px; height: 14px; }
38
- .state { font-size: 14px; font-weight: 650; line-height: 1.4; }
39
- .muted { color: #6b786f; font-size: 12px; }
40
- .account { position: relative; }
41
- .veiled > :not(.checking) { filter: blur(4px); opacity: .55; user-select: none; }
42
- .checking { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: #23312b; font-size: 13px; font-weight: 600; }
43
- .spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }
44
- @keyframes spin { to { transform: rotate(360deg); } }
45
- @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
46
- /* The "signed" pill after the verifier's name, one size smaller than the line it sits on. */
47
- .summary .signed { display: inline-block; margin-left: 5px; padding: 0 6px; border: 1px solid #cfdcd3; border-radius: 999px; font-size: 10px; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: 1px; }
48
- .summary a.signed:hover { background: #f2f5f1; }
49
- .summary .signed.bad { border-color: #b3261e; color: #b3261e; font-weight: 650; cursor: help; }
50
- .caution { display: inline-block; margin-left: 3px; color: #a15c00; vertical-align: -1.5px; }
51
- .caution svg { display: block; width: 10px; height: 10px; }
52
- .account { padding: 14px 16px; border: 1px solid #e0e6df; border-radius: 10px; margin-top: 12px; }
53
- .account h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 3px; color: #6b786f; font-size: 11px; font-weight: 550; }
54
- .account a, .account strong { font-weight: 650; font-size: 14px; }
55
- /* The account's own name carries that weight; a link inside a line of prose does not. */
56
- .summary a, .method a, .additional a { font: inherit; }
57
- .reference { margin-top: 2px; }
58
- /* A fingerprint in fours: it breaks between groups, and copies as one unbroken word. */
59
- .fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }
60
- .fingerprint span { display: inline-block; margin-right: .5ch; }
61
- .method { margin-top: 8px; }
62
- /* In the corner the heading leaves empty, apart from everything that was checked. */
63
- .listing { position: absolute; top: 12px; right: 14px; padding: 1px 8px; border: 1px solid #e0e6df; border-radius: 999px; color: #6b786f; font-size: 11px; font-weight: 550; }
64
- .listing.preferred { border-color: #245f43; color: #245f43; }
65
- /* Quiet, as its pill is: the surface and the name step back, and the controls do not. */
66
- .account.retired { background: #f6f8f5; }
67
- .account.retired > a, .account.retired > strong, .account.retired > .named > a, .account.retired > .named > strong { color: #6b786f; }
68
- /* The name, then a dot and the account's creation date. The date wraps under a long name. */
69
- /* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */
70
- .named { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; margin-left: -4px; padding-left: 4px; overflow-x: clip; }
71
- .since { position: relative; white-space: nowrap; }
72
- /* The dot sits in the gap before the date. */
73
- .since::before { content: "·"; position: absolute; left: -7px; transform: translateX(-50%); }
74
- .account.retired .mark { opacity: .45; }
75
- .group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }
76
- summary.group { display: flex; align-items: center; gap: 4px; width: fit-content; border-radius: 4px; list-style: none; cursor: pointer; }
77
- summary::-webkit-details-marker { display: none; }
78
- .fold { display: flex; align-items: center; gap: 4px; width: auto; height: auto; padding: 2px 8px 2px 4px; margin-left: -4px; border: 0; background: none; font: 550 12px/1.6 system-ui, sans-serif; }
79
- .fold svg, summary svg { width: 12px; height: 12px; }
80
- .fold[aria-expanded="true"] svg, details[open] > summary svg { transform: rotate(90deg); }
81
- @media (prefers-reduced-motion: no-preference) { .fold svg, summary svg { transition: transform .15s; } }
82
- /* The fold and what the holder may do share a line, until a question needs the whole of it. */
83
- .bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
84
- .account .bar .row { margin: 0 0 0 auto; }
85
- .additional { margin-top: 2px; padding-left: 12px; }
86
- .joiner { display: block; width: 20px; height: 20px; margin: 8px auto -4px; color: #90a096; }
87
- dl { margin: 16px 0 0; padding-top: 12px; border-top: 1px solid #e5e9e3; display: grid; grid-template-columns: auto 1fr; gap: 5px 16px; font-size: 11px; }
88
- dt { color: #6b786f; }
89
- dd { margin: 0; text-align: right; overflow-wrap: anywhere; }
90
- .explanation { margin-top: 16px; }
91
- .action { width: auto; height: auto; padding: 9px 14px; font: 600 13px/1.3 system-ui, sans-serif; color: #23312b; text-align: left; }
92
- .action.primary { border-color: #245f43; background: #245f43; color: #fff; }
93
- .action.primary:hover { background: #1d4f37; }
94
- .action:disabled { opacity: .6; cursor: progress; }
95
- .action.danger:hover { border-color: #b3261e; background: #fdecea; color: #b3261e; }
96
- .row { display: flex; justify-content: end; gap: 8px; margin-top: 16px; }
97
- .account .row { align-items: center; justify-content: start; flex-wrap: wrap; margin-top: 12px; }
98
- .account .action { padding: 5px 10px; font-size: 12px; }
99
- .account .action.dots { display: grid; place-items: center; align-self: stretch; width: 30px; padding: 0; }
100
- .dots svg { width: 16px; height: 16px; }
101
- /* Placed under its button each time it opens, above the dialog and clipped by none of it. */
102
- .menu { position: fixed; inset: auto; margin: 0; min-width: 170px; padding: 6px; border: 1px solid #dce2de; border-radius: 10px; background: #fff; color: #23312b; box-shadow: 0 12px 40px #10201930; }
103
- .menu:popover-open { display: grid; }
104
- .account .menu .action { border: 0; border-radius: 6px; padding: 6px 10px; font-weight: 550; }
105
- .menu .danger { color: #b3261e; }
106
- .menu hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid #e5e9e3; }
107
- footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }
108
- footer a { display: flex; color: inherit; }
109
- .logo { display: block; height: 13px; }
110
- ${markStyles}
111
- `;
15
+ let holds = 0;
16
+ let release = () => { };
17
+ /**
18
+ * Stops the page behind an open dialog from scrolling, and puts back the
19
+ * page's own inline styles once the last open dialog closes.
20
+ */
21
+ export function holdPage(dialog) {
22
+ if (holds++ === 0) {
23
+ const { style, clientWidth } = document.documentElement;
24
+ // Longhands, since the overflow shorthand reads empty when only one axis is set.
25
+ const held = ['overflow-x', 'overflow-y', 'scrollbar-gutter'];
26
+ const saved = held.map((name) => [
27
+ style.getPropertyValue(name),
28
+ style.getPropertyPriority(name),
29
+ ]);
30
+ // Keep the space of a hidden scrollbar so the page does not shift sideways.
31
+ if (innerWidth > clientWidth)
32
+ style.setProperty('scrollbar-gutter', 'stable', 'important');
33
+ style.setProperty('overflow-x', 'hidden', 'important');
34
+ style.setProperty('overflow-y', 'hidden', 'important');
35
+ release = () => held.forEach((name, i) => {
36
+ const [value, priority] = saved[i];
37
+ if (value)
38
+ style.setProperty(name, value, priority);
39
+ else
40
+ style.removeProperty(name);
41
+ });
42
+ }
43
+ dialog.addEventListener('close', () => --holds === 0 && release(), { once: true });
44
+ }
45
+ export const styles = dialogCss + markStyles;
112
46
  export function node(tag, text = '', className = '') {
113
47
  const element = document.createElement(tag);
114
48
  element.textContent = text;
@@ -259,6 +193,8 @@ export function namedCard(heading, external, ...extra) {
259
193
  }
260
194
  function side(heading, name, reference, url, since, extra) {
261
195
  const card = node('section', '', 'account');
196
+ // The heading, name and reference together, so a grid can line them up as one part.
197
+ const identity = node('div', '', 'identity');
262
198
  const title = node('h3');
263
199
  const value = node(url ? 'a' : 'strong', name);
264
200
  title.append(...heading);
@@ -268,18 +204,18 @@ function side(heading, name, reference, url, since, extra) {
268
204
  // The name and the date share a line, and the date wraps under a long name.
269
205
  const named = node('div', '', 'named');
270
206
  named.append(value, node('span', since, 'muted since'));
271
- card.append(title, named);
207
+ identity.append(title, named);
272
208
  }
273
209
  else
274
- card.append(title, value);
210
+ identity.append(title, value);
275
211
  if (typeof reference === 'string')
276
- card.append(node('div', reference, 'muted reference'));
212
+ identity.append(node('div', reference, 'muted reference'));
277
213
  else if (reference) {
278
214
  const groups = node('div', '', 'muted reference fingerprint');
279
215
  groups.append(...reference.map((group) => node('span', group)));
280
- card.append(groups);
216
+ identity.append(groups);
281
217
  }
282
- card.append(...extra);
218
+ card.append(identity, ...extra);
283
219
  return card;
284
220
  }
285
221
  /** What the holder is told before an account is retired, since it cannot be undone from here. */
@@ -535,6 +471,21 @@ function cautionMark() {
535
471
  * Opens and shuts what a card folds away: how its account was shown, every method of it
536
472
  * together so that none is put ahead of another, and its dates.
537
473
  */
474
+ /**
475
+ * Scrolls what a reader just opened into view, once it has been laid out. It moves only as
476
+ * far as needed, and puts the top in view when the whole does not fit in the pane.
477
+ */
478
+ function reveal(element) {
479
+ requestAnimationFrame(() => {
480
+ const pane = element.closest('.pane');
481
+ const tall = pane !== null && element.offsetHeight > pane.clientHeight;
482
+ const still = matchMedia('(prefers-reduced-motion: reduce)').matches;
483
+ element.scrollIntoView({
484
+ block: tall ? 'start' : 'nearest',
485
+ behavior: still ? 'auto' : 'smooth',
486
+ });
487
+ });
488
+ }
538
489
  function foldToggle(folded, fold) {
539
490
  const toggle = node('button', '', 'fold');
540
491
  let shown = fold.open;
@@ -550,6 +501,8 @@ function foldToggle(folded, fold) {
550
501
  toggle.onclick = () => {
551
502
  set(!shown);
552
503
  fold.toggle(shown);
504
+ if (shown)
505
+ reveal(folded.at(-1));
553
506
  };
554
507
  return toggle;
555
508
  }
@@ -626,22 +579,25 @@ function externalCard(evidence, manage, later = () => { }, fold, warned = () =>
626
579
  if (fold)
627
580
  bar.append(foldToggle([...methods, dates], fold), ...row);
628
581
  const logo = providerMark(evidence.provider, evidence.attestations.external[0].method);
582
+ // Everything under the verdict in one part, the third of the card's rows in a grid.
583
+ const rest = node('div', '', 'rest');
584
+ rest.append(...(fold ? [bar, ...methods, dates] : [...methods, dates, ...row]));
629
585
  const card = namedCard(
630
586
  // The mark and the name, or just the name. A mark that falls back to writing the name
631
587
  // would print it twice here, since this heading writes it either way.
632
588
  logo ? [logo, document.createTextNode(provider)] : [document.createTextNode(provider)], evidence.external,
633
589
  // Status first, then how it was shown, then when. The proof explains the state
634
590
  // above it, so it cannot sit before that state has been given.
635
- summary, ...(fold ? [] : methods),
591
+ summary,
636
592
  // How the holder lists the account, in their words and apart from how it was shown:
637
- // nothing checked it, so it is never among the methods above it.
593
+ // nothing checked it, so it is never among the methods below it.
638
594
  ...(evidence.status === 'revoked' || retired
639
595
  ? []
640
596
  : evidence.mark === 'preferred'
641
597
  ? [node('div', 'Preferred', 'listing preferred')]
642
598
  : evidence.mark === 'unused'
643
599
  ? [node('div', 'No longer used', 'listing')]
644
- : []), ...(fold ? [bar, ...methods, dates] : [dates, ...row]));
600
+ : []), rest);
645
601
  const surface = retired ? 'account retired' : 'account';
646
602
  card.className = surface;
647
603
  // A signed record is not read, or acted on, before its signature has been checked: until
@@ -678,7 +634,9 @@ function addRow(manage) {
678
634
  * The subject once, then each account linked to it on its own card, in the order given.
679
635
  * Retired accounts sit in a group of their own at the foot, under what says they are,
680
636
  * shut until it is opened unless no other account is left to read. Every record given is
681
- * of the one subject, which the caller has checked.
637
+ * of the one subject, which the caller has checked. Returns whether the cards a reader
638
+ * sees on opening are four or more: the accounts in use, or the retired ones when
639
+ * nothing else is left and their group starts open.
682
640
  */
683
641
  function render(content, records, manage, later, folds = { open: false, cards: new Set() }) {
684
642
  const [first] = records;
@@ -692,6 +650,7 @@ function render(content, records, manage, later, folds = { open: false, cards: n
692
650
  site: first.siteName,
693
651
  provider: first.providerName,
694
652
  }));
653
+ localCard.className += ' local';
695
654
  const retired = records.filter((record) => record.status === 'retired');
696
655
  const listed = records.filter((record) => record.status !== 'retired');
697
656
  // A card alone in its group has room for its dates. Among others it folds them.
@@ -708,9 +667,11 @@ function render(content, records, manage, later, folds = { open: false, cards: n
708
667
  const count = node('summary', '', 'group');
709
668
  count.append(foldMark(), document.createTextNode(`Retired accounts (${retired.length})`));
710
669
  // A signature that fails is a warning, and a warning is not left behind a fold.
711
- history.append(count, ...cards(retired, () => {
670
+ const gone = node('div', '', 'tiles');
671
+ gone.append(...cards(retired, () => {
712
672
  history.open = true;
713
673
  }));
674
+ history.append(count, gone);
714
675
  // Shut to keep history out of the way of the accounts in use. With none of those there
715
676
  // is nothing else to read, so it starts open.
716
677
  history.open = folds.retired ?? !listed.length;
@@ -718,11 +679,16 @@ function render(content, records, manage, later, folds = { open: false, cards: n
718
679
  // group turns, so what it will be is the opposite of what it is.
719
680
  count.addEventListener('click', () => {
720
681
  folds.retired = !history.open;
682
+ if (folds.retired)
683
+ reveal(history);
721
684
  });
722
- content.replaceChildren(localCard, linkMark(), ...cards(listed), ...(manage ? [addRow(manage)] : []), ...(retired.length ? [history] : []),
685
+ const accounts = node('div', '', 'tiles');
686
+ accounts.append(...cards(listed));
687
+ content.replaceChildren(localCard, linkMark(), accounts, ...(manage ? [addRow(manage)] : []), ...(retired.length ? [history] : []),
723
688
  // Nothing below the cards may name a provider. Approval, method and dates belong to
724
689
  // the card they came from, and a second provider on this subject gets its own card.
725
690
  node('p', 'Verification does not guarantee legal identity, trustworthiness, or permanent ownership.', 'muted explanation'));
691
+ return (listed.length || retired.length) >= 4;
726
692
  }
727
693
  /**
728
694
  * Fetch fresh, permitted evidence; native dialog supplies focus containment and Escape
@@ -896,7 +862,8 @@ detailsOpen = false) {
896
862
  drawn = key;
897
863
  // The cards are new, and what the old ones were waiting to try again went with them.
898
864
  retries.clear();
899
- render(content, records, actions, (retry) => retries.add(retry), folds);
865
+ const many = render(content, records, actions, (retry) => retries.add(retry), folds);
866
+ dialog.className = many ? 'many' : '';
900
867
  };
901
868
  /**
902
869
  * Redraws at the moment the next record runs out, where that comes before the next check
@@ -974,6 +941,7 @@ detailsOpen = false) {
974
941
  }, { once: true });
975
942
  if (opened && [opened].flat().length)
976
943
  draw(opened);
944
+ holdPage(dialog);
977
945
  dialog.showModal();
978
946
  void refresh();
979
947
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: "@media (prefers-reduced-motion: no-preference) {\n .mark.current path:last-child {\n animation: verily-draw 0.8s cubic-bezier(0.65, 0, 0.35, 1) 0.1s backwards;\n }\n}\n@keyframes verily-draw {\n from {\n stroke-dasharray: 0 176;\n }\n to {\n stroke-dasharray: 108 176;\n }\n}\n";
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ // Generated from mark.css by scripts/css.mjs. Do not edit.
2
+ export default "@media (prefers-reduced-motion: no-preference) {\n .mark.current path:last-child {\n animation: verily-draw 0.8s cubic-bezier(0.65, 0, 0.35, 1) 0.1s backwards;\n }\n}\n@keyframes verily-draw {\n from {\n stroke-dasharray: 0 176;\n }\n to {\n stroke-dasharray: 108 176;\n }\n}\n";
@@ -4,7 +4,7 @@ export type { MarkTone };
4
4
  * The check draws itself in from its left end once the verification holds: when a pill's
5
5
  * mark first becomes `current`, and each time the dialog opens on a current one.
6
6
  */
7
- export declare const markStyles = "\n @media (prefers-reduced-motion: no-preference) {\n .mark.current path:last-child { animation: verily-draw .8s cubic-bezier(.65, 0, .35, 1) .1s backwards; }\n }\n @keyframes verily-draw { from { stroke-dasharray: 0 176; } to { stroke-dasharray: 108 176; } }\n";
7
+ export declare const markStyles = "@media (prefers-reduced-motion: no-preference) {\n .mark.current path:last-child {\n animation: verily-draw 0.8s cubic-bezier(0.65, 0, 0.35, 1) 0.1s backwards;\n }\n}\n@keyframes verily-draw {\n from {\n stroke-dasharray: 0 176;\n }\n to {\n stroke-dasharray: 108 176;\n }\n}\n";
8
8
  /** Repaints a mark in place, so answering a check never replaces the drawing. */
9
9
  export declare function paintMark(svg: SVGSVGElement, tone: MarkTone): void;
10
10
  /** V2: the green check fills its section of the symmetric red V. */
@@ -1,14 +1,10 @@
1
1
  import { tones, verification } from '../marks.js';
2
+ import markCss from './mark.css.js';
2
3
  /**
3
4
  * The check draws itself in from its left end once the verification holds: when a pill's
4
5
  * mark first becomes `current`, and each time the dialog opens on a current one.
5
6
  */
6
- export const markStyles = `
7
- @media (prefers-reduced-motion: no-preference) {
8
- .mark.current path:last-child { animation: verily-draw .8s cubic-bezier(.65, 0, .35, 1) .1s backwards; }
9
- }
10
- @keyframes verily-draw { from { stroke-dasharray: 0 176; } to { stroke-dasharray: 108 176; } }
11
- `;
7
+ export const markStyles = markCss;
12
8
  /** Repaints a mark in place, so answering a check never replaces the drawing. */
13
9
  export function paintMark(svg, tone) {
14
10
  const colors = tones[tone];