@bkazemi/verily 0.5.0 → 0.6.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 CHANGED
@@ -145,7 +145,7 @@ import { init } from '@bkazemi/verily'; // also registers <verily-badge>
145
145
  const client = init({ backendUrl: '/api/verily' });
146
146
  ```
147
147
 
148
- 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.5.0/dist/verily.js`. The script defines a global `Verily`.
148
+ 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.6.0/dist/verily.js`. The script defines a global `Verily`.
149
149
 
150
150
  Then place the badge wherever the account appears:
151
151
 
@@ -184,6 +184,8 @@ Add `stacked` to name the accounts in the badge itself, one to a row:
184
184
 
185
185
  It lists up to four verified accounts in the order they were connected, then counts the rest as `+2 more`. It is still one badge that opens the one set of details. It is as tall as its rows, so give it a line of its own. With one verified account it is the ordinary badge. `mountBadges` and `presentConnections` take the same choices as `{ stacked: true }` and `{ peek: false }`.
186
186
 
187
+ In the details of several accounts, each account is a short card: its name, its status and who verified it. How it was shown and its dates sit behind a Details toggle on the card, and retired accounts are folded into a group at the foot. Add `details-open` to the badge, or pass `{ detailsOpen: true }`, to start every card open where your readers come for the proof. A reader can still open or shut each one. A single account always shows everything.
188
+
187
189
  The badge refreshes every 30 seconds, and only ever reads public evidence. The client can also start and end connections, and draw a badge into an element of your own:
188
190
 
189
191
  ```js
@@ -299,7 +301,7 @@ A static site only needs the script and the badge markup; the backend runs somew
299
301
  4. Add the script and the badge to your site:
300
302
 
301
303
  ```html
302
- <script src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.5.0/dist/verily.js" defer></script>
304
+ <script src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.6.0/dist/verily.js" defer></script>
303
305
  <verily-badge
304
306
  backend-url="https://your-backend.example"
305
307
  connection-id="CONNECTION_ID"
@@ -15,17 +15,20 @@ export declare function renderBadgeMessage(element: HTMLElement, message: string
15
15
  * `more` is how many other accounts of the same subject stand behind the one shown, said
16
16
  * as a count beside it. `linked` is whether the record has a page of its own a reader can
17
17
  * open: an unlisted one has none, so its pill is a button and never a link to nowhere.
18
+ * `href` is that page, where it is not the record's own: a pill standing for several
19
+ * accounts links to all of them.
18
20
  *
19
21
  * `rows` are those of the other accounts to name, in order. Stacked, each has a row of
20
22
  * its own in the pill, which is then as tall as its rows. Otherwise the pill stays short
21
23
  * and, with `peek`, names them in a panel that opens beside it while a pointer rests on it
22
24
  * or the keyboard is on it. Either way the accounts past the rows are a count.
23
25
  */
24
- export declare function renderBadge(element: HTMLElement, evidence: Evidence, { more, linked, rows, peek, }?: {
26
+ export declare function renderBadge(element: HTMLElement, evidence: Evidence, { more, linked, rows, peek, href, }?: {
25
27
  more?: number;
26
28
  linked?: boolean;
27
29
  rows?: Evidence[];
28
30
  peek?: boolean;
31
+ href?: string;
29
32
  }): HTMLElement | null;
30
33
  /**
31
34
  * Shuts a host's panel and lets go of what it was listening for. For a host leaving the
@@ -155,11 +155,12 @@ export function badgeShown(element) {
155
155
  return intact(element) && shown.has(element);
156
156
  }
157
157
  /** Everything the pill draws: equal keys mean an identical pill. */
158
- function renderKey(evidence, current, label, more, linked, rows, peek) {
158
+ function renderKey(evidence, current, label, more, linked, rows, peek, href) {
159
159
  return JSON.stringify([
160
160
  more,
161
161
  linked,
162
162
  peek,
163
+ href,
163
164
  rows.map((row) => [row.provider, row.providerName, externalName(row.external)]),
164
165
  evidence.provider,
165
166
  evidence.providerName,
@@ -220,13 +221,15 @@ export function renderBadgeMessage(element, message) {
220
221
  * `more` is how many other accounts of the same subject stand behind the one shown, said
221
222
  * as a count beside it. `linked` is whether the record has a page of its own a reader can
222
223
  * open: an unlisted one has none, so its pill is a button and never a link to nowhere.
224
+ * `href` is that page, where it is not the record's own: a pill standing for several
225
+ * accounts links to all of them.
223
226
  *
224
227
  * `rows` are those of the other accounts to name, in order. Stacked, each has a row of
225
228
  * its own in the pill, which is then as tall as its rows. Otherwise the pill stays short
226
229
  * and, with `peek`, names them in a panel that opens beside it while a pointer rests on it
227
230
  * or the keyboard is on it. Either way the accounts past the rows are a count.
228
231
  */
229
- export function renderBadge(element, evidence, { more = 0, linked = true, rows = [], peek = false, } = {}) {
232
+ export function renderBadge(element, evidence, { more = 0, linked = true, rows = [], peek = false, href = evidence.evidenceUrl, } = {}) {
230
233
  const provider = evidence.providerName;
231
234
  const current = evidence.status === 'verified' && evidence.expiresAt > Date.now();
232
235
  const retired = evidence.status === 'retired';
@@ -238,7 +241,7 @@ export function renderBadge(element, evidence, { more = 0, linked = true, rows =
238
241
  ? 'revoked'
239
242
  : 'expired';
240
243
  const label = statusLabel(evidence, Date.now());
241
- const key = renderKey(evidence, current, label, more, linked, rows, peek);
244
+ const key = renderKey(evidence, current, label, more, linked, rows, peek, href);
242
245
  if (intact(element) && shown.get(element) === key)
243
246
  return null;
244
247
  const handle = externalName(evidence.external);
@@ -250,7 +253,7 @@ export function renderBadge(element, evidence, { more = 0, linked = true, rows =
250
253
  // is: in the page's text colour, and not as a verification that failed.
251
254
  paintMark(mark, current ? 'current' : retired ? 'pending' : 'inactive');
252
255
  if (linked) {
253
- badge.href = evidence.evidenceUrl;
256
+ badge.href = href;
254
257
  badge.rel = 'noreferrer';
255
258
  }
256
259
  else
@@ -1,5 +1,5 @@
1
- import { externalId, externalLink, externalName } from '../core/index.js';
2
- import { accountCard, linkMark, node, outward, stampLink, styles } from './evidence-dialog.js';
1
+ import { externalId, externalIdGroups, externalLink, externalName } from '../core/index.js';
2
+ import { accountCard, backMark, closeMark, linkMark, node, outward, stampLink, styles, } from './evidence-dialog.js';
3
3
  import { providerMark } from './provider-mark.js';
4
4
  import { version } from '../version.js';
5
5
  const connectStyles = `
@@ -14,13 +14,15 @@ const connectStyles = `
14
14
  /* The block scrolls and its frame does not, so the copy button stays in its corner. */
15
15
  .code { position: relative; margin: 10px 0; }
16
16
  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; }
17
- .code .action { position: absolute; top: 6px; right: 6px; padding: 3px 8px; font-size: 11px; }
17
+ .code .action { position: absolute; top: 9px; right: 9px; padding: 3px 8px; font-size: 11px; }
18
18
  label { display: grid; gap: 4px; margin-top: 12px; font-weight: 600; }
19
19
  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; }
20
20
  textarea { font-family: ui-monospace, monospace; font-size: 12px; resize: vertical; }
21
21
  fieldset { margin: 16px 0 0; padding: 10px 14px; border: 1px solid #e0e6df; border-radius: 10px; }
22
22
  legend { padding: 0 4px; color: #6b786f; font-size: 11px; font-weight: 550; }
23
23
  fieldset label { display: flex; align-items: baseline; gap: 8px; margin-top: 4px; font-weight: 500; }
24
+ /* A radio has no baseline to speak of, so it is set in the middle of the label's first line. */
25
+ 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; }
24
26
  .error { color: #9b2c33; }
25
27
  `;
26
28
  /** Puts a copy control on a block the holder must reproduce exactly, as the flow page does. */
@@ -110,7 +112,7 @@ renew) {
110
112
  root.adoptedStyleSheets = [sheet];
111
113
  const dialog = node('dialog');
112
114
  const heading = node('h2', 'Verify an account');
113
- const close = node('button', '×');
115
+ const close = node('button', '', 'icon');
114
116
  const header = node('header');
115
117
  const stamp = node('footer');
116
118
  const content = node('div', 'Loading…', 'steps');
@@ -118,6 +120,7 @@ renew) {
118
120
  dialog.setAttribute('aria-labelledby', heading.id);
119
121
  close.type = 'button';
120
122
  close.setAttribute('aria-label', 'Close');
123
+ close.append(closeMark());
121
124
  content.setAttribute('aria-live', 'polite');
122
125
  stamp.append(stampLink(), node('span', version));
123
126
  header.append(heading, close);
@@ -127,9 +130,10 @@ renew) {
127
130
  openDialogs.set(opener, dialog);
128
131
  let returning = false;
129
132
  if (back) {
130
- const before = node('button', '‹');
133
+ const before = node('button', '', 'icon');
131
134
  before.type = 'button';
132
135
  before.setAttribute('aria-label', 'Back');
136
+ before.append(backMark());
133
137
  header.className = 'backed';
134
138
  header.replaceChildren(before, heading, close);
135
139
  before.onclick = () => {
@@ -433,7 +437,7 @@ renew) {
433
437
  const logo = providerMark(flow.provider.id, flow.provider.method);
434
438
  const external = accountCard(logo
435
439
  ? [logo, document.createTextNode(flow.provider.name)]
436
- : [document.createTextNode(flow.provider.name)], externalName(flow.external), externalId(flow.external), externalLink(flow.external));
440
+ : [document.createTextNode(flow.provider.name)], externalName(flow.external), externalIdGroups(flow.external) ?? externalId(flow.external), externalLink(flow.external));
437
441
  const parts = [subjectCard(flow.local), linkMark(), external];
438
442
  const choice = node('fieldset');
439
443
  const visibilityText = {
@@ -1,7 +1,7 @@
1
1
  import { type Evidence } 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: 24px; 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 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 a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }\n a:focus-visible, button:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }\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 .caution { display: inline-block; margin-left: 4px; color: #a15c00; cursor: help; vertical-align: middle; }\n .caution svg { display: block; width: 12px; height: 12px; }\n .bad { color: #b3261e; font-weight: 650; text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }\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 .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 { color: #6b786f; }\n .account.retired .mark { opacity: .45; }\n .group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }\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 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
+ 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: 24px; 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 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 .caution { display: inline-block; margin-left: 4px; color: #a15c00; cursor: help; vertical-align: -1.5px; }\n .caution svg { display: block; width: 12px; height: 12px; }\n .bad { color: #b3261e; font-weight: 650; text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }\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 { color: #6b786f; }\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";
5
5
  export declare function node<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string, className?: string): HTMLElementTagNameMap[K];
6
6
  /**
7
7
  * Points an anchor out of the dialog. Every link here opens in a new tab: the dialog is
@@ -9,6 +9,15 @@ export declare function node<K extends keyof HTMLElementTagNameMap>(tag: K, text
9
9
  * take the reader off the page the pill was on. `noreferrer` keeps the opener unreachable.
10
10
  */
11
11
  export declare function outward(anchor: HTMLAnchorElement, url: string): HTMLAnchorElement;
12
+ /**
13
+ * Where the verifier lists every public record of a record's subject. A record whose
14
+ * address is not the usual one is left linking to itself.
15
+ */
16
+ export declare function subjectUrl(e: Evidence): string;
17
+ /** A cross drawn in a square, so it sits in the middle of its button in any font. */
18
+ export declare const closeMark: () => SVGSVGElement;
19
+ /** Points back the way the reader came, drawn for the same reason the cross is. */
20
+ export declare const backMark: () => SVGSVGElement;
12
21
  /** Joins the two cards: the link itself, drawn rather than described. */
13
22
  export declare function linkMark(): SVGSVGElement;
14
23
  /**
@@ -16,7 +25,7 @@ export declare function linkMark(): SVGSVGElement;
16
25
  * `extra` carries evidence belonging to that side alone: a provider authenticates and
17
26
  * expires on its own terms, and a second provider on the same subject would differ.
18
27
  */
19
- export declare function accountCard(heading: Node[], name: string, reference: string | undefined, url?: string, ...extra: HTMLElement[]): HTMLElement;
28
+ export declare function accountCard(heading: Node[], name: string, reference: string | string[] | undefined, url?: string, ...extra: HTMLElement[]): HTMLElement;
20
29
  /**
21
30
  * A record as a card shows it. Where the card speaks for several records of one account,
22
31
  * `links` names every one not yet revoked, whether or not it is shown, so removing the
@@ -74,4 +83,9 @@ arrange?: (records: Evidence[]) => Account[],
74
83
  * Given where whoever is looking holds these accounts: each then has its renewal and
75
84
  * removal under it, and the dialog offers to connect another.
76
85
  */
77
- manage?: Manage): void;
86
+ manage?: Manage,
87
+ /**
88
+ * Whether the cards of several accounts start with their details open. They start shut
89
+ * unless the page asked otherwise, and a reader may turn each either way from there.
90
+ */
91
+ detailsOpen?: boolean): void;