@bkazemi/verily 0.9.4 → 0.10.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.
- package/README.md +9 -5
- package/dist/browser/badge.css.d.ts +2 -0
- package/dist/browser/badge.css.js +2 -0
- package/dist/browser/badge.js +2 -66
- package/dist/browser/connect-dialog.css.d.ts +2 -0
- package/dist/browser/connect-dialog.css.js +2 -0
- package/dist/browser/connect-dialog.js +4 -24
- package/dist/browser/evidence-dialog.css.d.ts +2 -0
- package/dist/browser/evidence-dialog.css.js +2 -0
- package/dist/browser/evidence-dialog.d.ts +6 -1
- package/dist/browser/evidence-dialog.js +42 -119
- package/dist/browser/index.d.ts +7 -1
- package/dist/browser/index.js +37 -8
- package/dist/browser/mark.css.d.ts +2 -0
- package/dist/browser/mark.css.js +2 -0
- package/dist/browser/mark.d.ts +1 -1
- package/dist/browser/mark.js +2 -6
- package/dist/core/index.d.ts +24 -0
- package/dist/core/index.js +40 -0
- package/dist/instance/bin.js +31 -1
- package/dist/instance/github-app.d.ts +26 -0
- package/dist/instance/github-app.js +94 -0
- package/dist/instance/index.js +4 -2
- package/dist/instance/starter.js +1 -1
- package/dist/server/index.js +4 -2
- package/dist/server/service.d.ts +2 -0
- package/dist/server/service.js +23 -2
- package/dist/server/style.css.d.ts +2 -0
- package/dist/server/style.css.js +2 -0
- package/dist/server/style.d.ts +1 -1
- package/dist/server/style.js +2 -455
- package/dist/site/index.d.ts +6 -3
- package/dist/site/index.js +6 -3
- package/dist/verily.js +632 -223
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ const verily = createVerily({
|
|
|
118
118
|
|
|
119
119
|
Sign-in methods need an app registered with the provider. The callback URL is always `<baseUrl>/callback`, and every place you run Verily, development included, needs its own registered.
|
|
120
120
|
|
|
121
|
-
- **GitHub:** [register an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
|
121
|
+
- **GitHub:** run `npx verily github-app <callback>...`, open the address it prints in a browser signed in to GitHub, and click **Create GitHub App**. It prints the client id and secret. One app can list up to 10 callback URLs, so development and production can share it. Add `--org <name>` for an organization to own the app, or `--name <name>` if the default name is taken. You can instead [register an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) by hand, which has only one callback URL.
|
|
122
122
|
- **Discord:** create an application in the [Discord developer portal](https://discord.com/developers/applications) and add the callback URL under **OAuth2 → Redirects**. One application can list several. Discord shows the client secret only when you reset it.
|
|
123
123
|
- **YouTube:** in the [Google Cloud console](https://console.cloud.google.com/), enable the YouTube Data API v3, create an OAuth client of type **Web application**, and add the callback URL as an authorized redirect URI. One client can list several. The `youtube.readonly` scope is sensitive: until Google verifies your app, users see a warning and only 100 accounts can ever use it, so apply for verification before offering it publicly.
|
|
124
124
|
|
|
@@ -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.
|
|
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.10.0/dist/verily.js`. The script defines a global `Verily`.
|
|
172
172
|
|
|
173
173
|
Then place the badge wherever the account appears:
|
|
174
174
|
|
|
@@ -217,6 +217,8 @@ It lists up to four verified accounts, then counts the rest, such as `+2 more`.
|
|
|
217
217
|
|
|
218
218
|
With several accounts, each card shows the name, status and verifier, with the method and dates behind a Details toggle. Add `details-open`, or pass `{ detailsOpen: true }`, to start every card open.
|
|
219
219
|
|
|
220
|
+
A sign-in counts until its record expires, a year by default. To ask for more recent proof, add `max-age`, such as `max-age="30d"`, or pass `{ maxAgeMs }`. A record whose account was last proved before then reads as Unconfirmed until its holder renews it. A bare number is seconds, and `s`, `m`, `h` and `d` set the unit. A published proof is reread on a schedule, so `max-age` mostly affects sign-ins and mailed codes. Each card shows when its account was last proved. For a published proof that is Last checked. For a sign-in or code it is Approved, or Authenticated where a renewal by another method moved the approval later.
|
|
221
|
+
|
|
220
222
|
The badge refreshes every 30 seconds and reads only public evidence. The client can also start and end connections, and draw a badge into your own element:
|
|
221
223
|
|
|
222
224
|
```js
|
|
@@ -370,7 +372,7 @@ A static site only needs the script and the badge markup; the backend runs somew
|
|
|
370
372
|
|
|
371
373
|
```html
|
|
372
374
|
<script
|
|
373
|
-
src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.
|
|
375
|
+
src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.10.0/dist/verily.js"
|
|
374
376
|
defer
|
|
375
377
|
></script>
|
|
376
378
|
<verily-badge
|
|
@@ -460,7 +462,7 @@ const { [user.id]: connections } = await verily.connections([user.id]);
|
|
|
460
462
|
const verified = connections.filter((c) => c.status === 'verified');
|
|
461
463
|
```
|
|
462
464
|
|
|
463
|
-
Show only `verified` records as verified. A `retired` record carries `retiredAt` and can be shown as history, never as a current account. `mark` is `preferred` or `unused` where the user set one; it is their ordering, not something checked.
|
|
465
|
+
Show only `verified` records as verified. Pass `{ maxAgeMs }` to read a record whose account was last proved longer ago than that as `unconfirmed`. A `retired` record carries `retiredAt` and can be shown as history, never as a current account. `mark` is `preferred` or `unused` where the user set one; it is their ordering, not something checked.
|
|
464
466
|
|
|
465
467
|
To show them, hand the records to the badge. It draws them as given and fetches nothing, so it works for unlisted links, which a browser cannot read for itself:
|
|
466
468
|
|
|
@@ -598,7 +600,7 @@ An instance sets `dns: true`.
|
|
|
598
600
|
|
|
599
601
|
## Operations
|
|
600
602
|
|
|
601
|
-
Verification lasts
|
|
603
|
+
Verification lasts a year, flows 10 minutes, and sharing links 7 days. Change them with `validityMs`, `flowTtlMs` and `shareTtlMs`. Evidence is never cached. A published proof that `recheck()` still finds extends its record once half the term is gone, so only a sign-in or a mailed code needs renewing. Raising `validityMs` lengthens records still running on the next `recheck()`; lowering it shortens nothing.
|
|
602
604
|
|
|
603
605
|
A user's DNS setup is saved for seven days. If propagation outlasts a flow's ten minutes, they choose DNS again and get the same TXT value and the last domain entered. Each return reads the proof afresh. The TXT record has to stay after verification, for the ongoing checks.
|
|
604
606
|
|
|
@@ -641,6 +643,8 @@ setInterval(
|
|
|
641
643
|
|
|
642
644
|
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
645
|
|
|
646
|
+
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.
|
|
647
|
+
|
|
644
648
|
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
649
|
|
|
646
650
|
```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";
|
package/dist/browser/badge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
import { attestationLabel, externalId, externalIdGroups, externalLink, externalName, externalSince, lastProved, localSide, proofTitle, statusLabel, } from '../core/index.js';
|
|
1
|
+
import { attestationLabel, externalId, externalIdGroups, externalLink, externalName, externalSince, authenticatedEarlier, 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,120 +12,37 @@ export function stampLink() {
|
|
|
11
12
|
return link;
|
|
12
13
|
}
|
|
13
14
|
const openDialogs = new WeakMap();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 .identity > a, .account.retired .identity > 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
|
-
/* Four or more accounts on a wide screen: two equal cards to a row. */
|
|
111
|
-
@media (min-width: 960px) {
|
|
112
|
-
dialog.many { width: min(900px, calc(100vw - 32px)); }
|
|
113
|
-
dialog.many .tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 12px; }
|
|
114
|
-
/* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall as
|
|
115
|
-
the taller card's part, so the verdicts and details of neighbouring cards line up. */
|
|
116
|
-
dialog.many .tiles > .account { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0; margin-top: 0; }
|
|
117
|
-
/* The dates sit at the foot of the third part, so they line up too. */
|
|
118
|
-
dialog.many .tiles .rest { display: flex; flex-direction: column; }
|
|
119
|
-
dialog.many .tiles .rest > dl { margin-top: auto; }
|
|
120
|
-
dialog.many .tiles .rest > :has(+ dl) { margin-bottom: 16px; }
|
|
121
|
-
/* The site's card is one tile wide, centred over the two columns. */
|
|
122
|
-
dialog.many .local { width: calc(50% - 6px); margin-inline: auto; text-align: center; }
|
|
123
|
-
dialog.many .local h3, dialog.many .local .named { justify-content: center; }
|
|
124
|
-
dialog.many .local .additional { padding-left: 0; }
|
|
125
|
-
}
|
|
126
|
-
${markStyles}
|
|
127
|
-
`;
|
|
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;
|
|
128
46
|
export function node(tag, text = '', className = '') {
|
|
129
47
|
const element = document.createElement(tag);
|
|
130
48
|
element.textContent = text;
|
|
@@ -643,11 +561,15 @@ function externalCard(evidence, manage, later = () => { }, fold, warned = () =>
|
|
|
643
561
|
evidence.expiresAt,
|
|
644
562
|
]);
|
|
645
563
|
}
|
|
646
|
-
// When
|
|
647
|
-
//
|
|
564
|
+
// When the account was last proved: a published proof when it was last read, and a
|
|
565
|
+
// sign-in or mailed code when it happened, where that is not already the approval date.
|
|
648
566
|
const main = evidence.attestations.external[0];
|
|
649
|
-
if (
|
|
650
|
-
|
|
567
|
+
if (!retired) {
|
|
568
|
+
if (main.artifactUrl)
|
|
569
|
+
dateRows.push(['Last checked', main.confirmedAt]);
|
|
570
|
+
else if (authenticatedEarlier(evidence))
|
|
571
|
+
dateRows.push(['Authenticated', evidence.authenticatedAt]);
|
|
572
|
+
}
|
|
651
573
|
for (const [label, time] of dateRows) {
|
|
652
574
|
dates.append(node('dt', label), node('dd', moment(time)));
|
|
653
575
|
}
|
|
@@ -1023,6 +945,7 @@ detailsOpen = false) {
|
|
|
1023
945
|
}, { once: true });
|
|
1024
946
|
if (opened && [opened].flat().length)
|
|
1025
947
|
draw(opened);
|
|
948
|
+
holdPage(dialog);
|
|
1026
949
|
dialog.showModal();
|
|
1027
950
|
void refresh();
|
|
1028
951
|
}
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export interface Arrangement {
|
|
|
20
20
|
* open or shut each either way.
|
|
21
21
|
*/
|
|
22
22
|
detailsOpen?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* How recently an account must have been proved to count as verified, in milliseconds.
|
|
25
|
+
* An older record reads as unconfirmed. Unset, a record counts until it expires.
|
|
26
|
+
*/
|
|
27
|
+
maxAgeMs?: number;
|
|
23
28
|
}
|
|
24
29
|
/**
|
|
25
30
|
* Presents records the page already holds, without asking any backend for them. This is
|
|
@@ -51,9 +56,10 @@ export declare function init({ backendUrl, handoffUrl }: {
|
|
|
51
56
|
/** The site's current public connections, so an embed need not hardcode ids. */
|
|
52
57
|
listPublished(): Promise<Evidence[]>;
|
|
53
58
|
getConnection(id: string): Promise<Evidence>;
|
|
54
|
-
mountBadge(element: HTMLElement, { connectionId, evidence }: {
|
|
59
|
+
mountBadge(element: HTMLElement, { connectionId, evidence, maxAgeMs, }: {
|
|
55
60
|
connectionId: string;
|
|
56
61
|
evidence?: Evidence;
|
|
62
|
+
maxAgeMs?: number;
|
|
57
63
|
}): Promise<void>;
|
|
58
64
|
/**
|
|
59
65
|
* Several of one subject's public connections as a single pill. One that cannot be
|