@bkazemi/verily 0.9.1 → 0.9.3

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.1/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.3/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.1/dist/verily.js"
373
+ src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.9.3/dist/verily.js"
374
374
  defer
375
375
  ></script>
376
376
  <verily-badge
@@ -115,6 +115,7 @@ renew) {
115
115
  const header = node('header');
116
116
  const stamp = node('footer');
117
117
  const content = node('div', 'Loading…', 'steps');
118
+ const pane = node('div', '', 'pane');
118
119
  heading.id = 'verily-connect-title';
119
120
  dialog.setAttribute('aria-labelledby', heading.id);
120
121
  close.type = 'button';
@@ -123,7 +124,8 @@ renew) {
123
124
  content.setAttribute('aria-live', 'polite');
124
125
  stamp.append(stampLink(), node('span', version));
125
126
  header.append(heading, close);
126
- dialog.append(header, content, stamp);
127
+ pane.append(header, content, stamp);
128
+ dialog.append(pane);
127
129
  root.append(dialog);
128
130
  document.body.append(host);
129
131
  openDialogs.set(opener, dialog);
@@ -1,7 +1,7 @@
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: 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 /* 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
+ 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";
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
@@ -13,7 +13,10 @@ export function stampLink() {
13
13
  const openDialogs = new WeakMap();
14
14
  export const styles = `
15
15
  * { box-sizing: border-box; }
16
- 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; }
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; }
17
20
  dialog::backdrop { background: #15271f66; }
18
21
  header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
19
22
  h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }
@@ -337,7 +340,7 @@ function actionsMenu(more, choices) {
337
340
  if (bottom > innerHeight - edge)
338
341
  menu.style.top = `${Math.max(edge, more.getBoundingClientRect().top - gap - height)}px`;
339
342
  // It does not follow its button, so it does not stay open once the button moves.
340
- menu.closest('dialog')?.addEventListener('scroll', shut, { once: true });
343
+ menu.closest('.pane')?.addEventListener('scroll', shut, { once: true });
341
344
  choices[0].focus();
342
345
  });
343
346
  menu.addEventListener('click', shut);
@@ -766,6 +769,7 @@ detailsOpen = false) {
766
769
  // What drew the record, under it. The record is about the pair, not about us.
767
770
  const stamp = node('footer');
768
771
  const content = node('div', 'Checking verification…');
772
+ const pane = node('div', '', 'pane');
769
773
  heading.id = 'verily-dialog-title';
770
774
  dialog.setAttribute('aria-labelledby', heading.id);
771
775
  close.type = 'button';
@@ -778,7 +782,8 @@ detailsOpen = false) {
778
782
  close.autofocus = true;
779
783
  title.append(heading);
780
784
  header.append(title, close);
781
- dialog.append(header, content, stamp);
785
+ pane.append(header, content, stamp);
786
+ dialog.append(pane);
782
787
  /** Links removed from this dialog, shown as removed before the page hands over new records. */
783
788
  const removed = new Map();
784
789
  /**
@@ -520,10 +520,12 @@ export declare class Absent extends Refused {
520
520
  }
521
521
  /**
522
522
  * A piece of what the holder is told: a paragraph, a paragraph with links in it, or
523
- * something they run or publish as is.
523
+ * something they run or publish as is. A code block marked `language: 'html'` is
524
+ * syntax highlighted on the flow page.
524
525
  */
525
526
  export type Instruction = string | {
526
527
  code: string;
528
+ language?: 'html';
527
529
  } | Inline[];
528
530
  /**
529
531
  * A piece of a paragraph: text, or text that links somewhere the holder goes to do what
@@ -1,5 +1,6 @@
1
1
  import { dialogPath, dialogRoute, lastProved, localSide, routeOf, routes, statusLabel, } from '../core/index.js';
2
2
  import { html, raw } from '../server/html.js';
3
+ import { highlightHtml } from '../server/highlight.js';
3
4
  import { card, date, distance, externalCard, marked, page, verdict } from '../server/render.js';
4
5
  import { toneOf } from '../marks.js';
5
6
  import { version } from '../version.js';
@@ -907,7 +908,7 @@ class Server {
907
908
  e.visibility === 'public' &&
908
909
  html `
909
910
  <p class="fine">Embed on your site</p>
910
- <pre><code>${embed}</code></pre>
911
+ <pre><code>${highlightHtml(embed)}</code></pre>
911
912
  `, html `
912
913
  ${(goes.length > 0 || listing) && html `<div class="actions">${goes}${listing}</div>`}
913
914
  ${ending}
@@ -0,0 +1,6 @@
1
+ import { type Html } from './html.js';
2
+ /**
3
+ * HTML source marked up with highlight.js classes, for a `<code>` block. highlight.js
4
+ * escapes the source itself, so its output goes into the page as it is.
5
+ */
6
+ export declare const highlightHtml: (source: string) => Html;
@@ -0,0 +1,9 @@
1
+ import hljs from 'highlight.js/lib/core';
2
+ import xml from 'highlight.js/lib/languages/xml';
3
+ import { raw } from './html.js';
4
+ hljs.registerLanguage('xml', xml);
5
+ /**
6
+ * HTML source marked up with highlight.js classes, for a `<code>` block. highlight.js
7
+ * escapes the source itself, so its output goes into the page as it is.
8
+ */
9
+ export const highlightHtml = (source) => raw(hljs.highlight(source, { language: 'xml' }).value);
@@ -2,6 +2,7 @@ import { attestationLabel, externalName, isArtifactProvider, isCodeProvider, isR
2
2
  import { artifactAttempts, codeAttempts, VerilyService, Unavailable, } from './service.js';
3
3
  import { copyScript, waitScript } from './copy.js';
4
4
  import { html } from './html.js';
5
+ import { highlightHtml } from './highlight.js';
5
6
  import { card, externalCard, marked, moment, page, safeUrl, times, verdict } from './render.js';
6
7
  import { toneOf } from '../marks.js';
7
8
  import { stylesheet, styleVersion } from './style.js';
@@ -243,7 +244,7 @@ function instructions(parts) {
243
244
  ? html `<p>${part}</p>`
244
245
  : Array.isArray(part)
245
246
  ? html `<p>${part.map(inline)}</p>`
246
- : html `<pre><code>${part.code}</code></pre>`)}`;
247
+ : html `<pre><code>${part.language === 'html' ? highlightHtml(part.code) : part.code}</code></pre>`)}`;
247
248
  }
248
249
  /** A piece of a paragraph. A link that is not http(s) is written as its text alone. */
249
250
  function inline(piece) {
@@ -62,7 +62,7 @@ export function linkProvider(options) {
62
62
  'Add a link to this address on a page you control, marked rel="me", then paste that page’s address below.',
63
63
  { code: expect },
64
64
  'Writing the page yourself, that is one element:',
65
- { code: `<a rel="me" href="${expect}">${expect}</a>` },
65
+ { code: `<a rel="me" href="${expect}">${expect}</a>`, language: 'html' },
66
66
  ],
67
67
  async verify({ artifact, expect }) {
68
68
  const page = address(artifact, hosts);
@@ -8,7 +8,7 @@
8
8
  * leaves. So the styling does the two things that helps with, a measured column and a
9
9
  * legible hierarchy, and nothing that would make a record look like a product.
10
10
  */
11
- export declare const stylesheet = ":root {\n --ink: #23312b;\n --muted: #6b786f;\n --line: #dce2de;\n --surface: #fff;\n --raised: #f7f9f7;\n --accent: #245f43;\n --warn: #a4442b;\n --caution: #b07a12;\n color-scheme: light dark;\n accent-color: var(--accent);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --ink: #dde5e0;\n --muted: #96a39b;\n --line: #2f3b35;\n --surface: #161a18;\n --raised: #1d2220;\n --accent: #7fc4a2;\n --warn: #e59a82;\n --caution: #d9a843;\n }\n}\n\n* { box-sizing: border-box; }\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font: 16px/1.65 system-ui, -apple-system, 'Segoe UI', sans-serif;\n text-rendering: optimizeLegibility;\n}\n\nmain {\n max-width: 34rem;\n margin: 0 auto;\n padding-top: 3rem;\n}\n\n/*\n * The logotype, above the page's own heading, on its own dark plate. Sized so the word is\n * the height it was without the plate, whose margin makes the box that much taller. The\n * word is white in either scheme: the plate is what it is read against, not the page.\n */\n.logo {\n display: block;\n height: 2.5rem;\n margin-bottom: 1.5rem;\n}\n\nh1 {\n margin: 0 0 1.75rem;\n padding-bottom: 1rem;\n border-bottom: 1px solid var(--line);\n font-size: 1.5rem;\n font-weight: 620;\n letter-spacing: -.02em;\n line-height: 1.25;\n}\n\n/*\n * A page holding one thing needs no rule under its heading. The rule divides a heading\n * from the record beneath it, and a page that only asks for a key has no record to divide.\n */\nmain.single h1 { margin-bottom: 1.25rem; padding-bottom: 0; border-bottom: none; }\n\n/*\n * A page holding one thing sits in the middle of the window rather than at its top,\n * where it would leave most of the window empty below it.\n */\nbody:has(main.single) { display: flex; min-height: 100vh; min-height: 100dvh; }\nmain.single { width: 100%; max-width: 24rem; margin: auto; padding-bottom: 3rem; }\n\nh2 { margin: 2rem 0 .75rem; font-size: 1.125rem; font-weight: 620; letter-spacing: -.01em; }\n\nh3 { margin: 0 0 .5rem; font-size: 1rem; font-weight: 620; }\n\np { margin: 0 0 1rem; overflow-wrap: break-word; }\n\na { color: var(--accent); text-underline-offset: 3px; overflow-wrap: anywhere; }\n\na:hover { text-decoration-thickness: 2px; }\n\n/*\n * Something to be run or published exactly as written. Its own line breaks are kept and a\n * long line is soft-wrapped rather than cut off: a soft wrap is drawn, not inserted, so\n * what is copied is still character for character what was written. Selecting inside the\n * block takes all of it, since half a command is worse than none.\n */\npre {\n position: relative;\n margin: 0 0 1rem;\n /* Room along the top line for the copy button, which sits over the block rather than in it. */\n padding: .75rem 5.25rem .75rem .875rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--raised);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n user-select: all;\n}\n\ncode { font: .8125rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }\n\n/* Made by a script, so it exists only where it works. Quiet: the block is the point. */\n.copy {\n position: absolute;\n top: .5rem;\n right: .5rem;\n padding: .1875rem .5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font: 500 .75rem/1.5 system-ui, sans-serif;\n user-select: none;\n}\n\n.copy:hover { color: var(--ink); border-color: var(--muted); }\n\n/*\n * The two sides of the link, which is what a reader came to read. Each fact about a side\n * gets its own line: what it is, what it is called, the identifier behind that, and how it\n * was shown. Everything else on the page qualifies the pair, so it is set quieter.\n */\n.side {\n margin-bottom: .75rem;\n padding: .875rem 1rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--raised);\n}\n\n/*\n * A subject's accounts are peers, so where there is room they sit two across. The page\n * that lists them is wider for it, and a narrow one keeps them in a single column.\n */\nmain:has(.accounts) { max-width: 54rem; }\n\n.accounts {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));\n gap: .75rem;\n margin-bottom: .75rem;\n}\n\n.accounts .side { position: relative; display: flex; flex-direction: column; margin-bottom: 0; }\n\n/* How the holder lists the account, in the card's corner and out of the lines below. */\n.listing {\n position: absolute;\n top: .75rem;\n right: .875rem;\n padding: .0625rem .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: .6875rem;\n font-weight: 550;\n}\n\n.listing.preferred { border-color: var(--accent); color: var(--accent); }\n\n/* The times sit at the foot of the card, so they line up along a row of uneven cards. */\n.accounts .side dl { margin-top: auto; }\n.accounts .side p:last-of-type { margin-bottom: .625rem; }\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner { display: block; width: 1.25rem; height: 1.25rem; margin: 0 auto .75rem; color: var(--muted); }\n\n.who {\n margin: 0 0 .125rem;\n color: var(--muted);\n font-size: .75rem;\n font-weight: 550;\n letter-spacing: .02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider { display: inline-block; width: .875rem; height: .875rem; margin-right: .375rem; vertical-align: -.125rem; }\n\n/* Clipped 4px left of the text, which hides the date's dot when the date wraps to a new line. */\n.name { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1rem; margin: 0 0 0 -4px; padding-left: 4px; overflow-x: clip; font-size: 1.0625rem; font-weight: 620; letter-spacing: -.01em; }\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since { position: relative; color: var(--muted); font-size: .8125rem; font-weight: 400; letter-spacing: 0; white-space: nowrap; }\n\n/* The dot sits in the gap before the date. */\n.name .since::before { content: \"\u00B7\"; position: absolute; left: -.5rem; transform: translateX(-50%); }\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a { text-decoration: none; }\n\n.name a:hover { text-decoration: underline; }\n\n.reference, .how { margin: 0; color: var(--muted); font-size: .8125rem; line-height: 1.5; }\n\n.name + .how, .reference + .how { margin-top: .5rem; }\n.fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n.fingerprint span { display: inline-block; margin-right: .5ch; }\n.how.additional { padding-left: .75rem; }\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark { display: inline-block; margin-left: .3125rem; padding: 0 .375rem; border: 1px solid var(--line); border-radius: 999px; font-size: .625rem; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: .0625rem; }\n\n.fine { color: var(--muted); font-size: .8125rem; line-height: 1.55; margin-bottom: .625rem; }\n\n/* That the record is signed is a fact about it, so it is set as one and not as fine print. */\n.signed { font-size: .875rem; line-height: 1.55; margin-bottom: .875rem; }\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target { margin-inline: -.75rem; padding: .625rem .75rem; border-radius: 8px; background: var(--raised); }\n\n/* Every time on the record in one column, so they can be compared rather than hunted for. */\ndl {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: .3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: .8125rem;\n}\n\ndt { color: var(--muted); }\n\ndd { margin: 0; font-variant-numeric: tabular-nums; }\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl { margin: .5rem 0 1rem; padding-top: 0; border-top: 0; }\n\n/* Right under the heading, the heading's own rule is the one above it. */\nh1 + dl { padding-top: 0; border-top: 0; }\n.side dl { margin: .625rem 0 0; padding-top: .625rem; }\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line); }\nfooter > :last-child { margin-bottom: 0; }\n\nsection {\n margin: 0 0 1.25rem;\n padding: 1.25rem 1.375rem;\n border: 1px solid var(--line);\n border-radius: 10px;\n}\n\nsection > :last-child { margin-bottom: 0; }\n\n/* What holds a control that cannot be undone. */\nsection.danger, section.caution { margin-top: 2rem; }\nsection.danger { border-color: var(--warn); }\n\n/* What holds a control that breaks something for a while, but can be recovered from. */\nsection.caution { border-color: var(--caution); }\nsection > h2:first-child { margin-top: 0; }\n\nform { margin: 0 0 1.5rem; }\n\nlabel { display: block; margin: 0 0 .75rem; color: var(--muted); font-size: .8125rem; }\n\ninput, textarea {\n display: block;\n width: 100%;\n margin-top: .375rem;\n padding: .5rem .625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font: .875rem/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea { resize: vertical; }\n\ninput:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }\n\nbutton {\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font: 500 .9375rem/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif;\n cursor: pointer;\n}\n\n/* A link that starts something, drawn as a button. */\na.button {\n display: inline-block;\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font-weight: 500;\n font-size: .9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button { margin-left: .5rem; border-color: var(--line); background: none; color: var(--ink); }\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary { border-color: var(--line); background: none; color: var(--ink); }\n\n/* A page's action and its alternative, side by side. */\n.choice { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.5rem; }\n.choice form { margin: 0; }\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices { display: grid; gap: .5rem; margin-bottom: 1.5rem; }\n.choices form { margin: 0; }\n\n.choices button {\n width: 100%;\n text-align: left;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n}\n\n.choices button:hover { border-color: var(--muted); }\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider { width: 1rem; height: 1rem; margin-right: .5rem; vertical-align: -.1875rem; }\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger { border-color: var(--warn); background: var(--warn); }\n\n/*\n * A connection on the settings page: the account's card, then what can be done with it.\n * Its state sits in the card's corner, on the line that names the method.\n */\n.connection { position: relative; margin-bottom: 1rem; }\n\n.state {\n position: absolute;\n top: .75rem;\n right: .875rem;\n display: flex;\n align-items: center;\n gap: .375rem;\n height: 1.625rem;\n padding: 0 .75rem 0 .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n font-weight: 550;\n}\n\n/*\n * The V the badge draws, in the tone that says how the record stands. A record that claims\n * nothing is drawn faint in the text colour, as the badge draws it.\n */\n.verdict { flex-shrink: 0; width: 1rem; height: 1rem; }\n.verdict.pending { opacity: .45; }\n.how .verdict { width: .875rem; height: .875rem; margin-right: .3125rem; vertical-align: -.125rem; }\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) { display: flex; align-items: center; gap: .75rem; }\nh1 .verdict { width: auto; height: 2.25rem; }\n\n/*\n * In a state pill the V is cropped to its strokes and fills the pill's height, so its arms\n * and its foot meet the border. The pill's left padding keeps the arms clear of its curve.\n */\n.state .verdict { width: auto; height: 100%; }\n\n/*\n * A pill's line includes room for descenders below the letters, which leaves its text\n * sitting high. Trimmed to the letters, the padding alone sets the space above and below.\n * In a state pill the word alone is trimmed, so it centres on the V by its letters.\n */\n@supports (text-box: trim-both cap alphabetic) {\n .listing { padding: .3125rem .5rem; text-box: trim-both cap alphabetic; }\n .state > span { text-box: trim-both cap alphabetic; }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1.25rem; font-size: .875rem; }\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence { margin: .375rem 0 0; font-size: .8125rem; }\n\n.connection dl { margin-bottom: .75rem; }\n.connection .side > :last-child { margin-bottom: 0; }\n.connection pre { margin: .75rem 0; background: var(--surface); }\n.connection .actions { margin-top: .875rem; }\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions { display: flex; flex-wrap: wrap; gap: .5rem; }\n.actions form { display: contents; }\n\n.actions button, .actions a.button {\n margin: 0;\n padding: .3125rem .75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n}\n\n.actions button:hover, .actions a.button:hover { border-color: var(--muted); }\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--line); }\n.ending form { margin: 0; }\n\n.ending button.danger {\n padding: .3125rem .75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: .8125rem;\n}\n\nfieldset { margin: 0 0 1.25rem; padding: 1rem 1.125rem .25rem; border: 1px solid var(--line); border-radius: 8px; }\n\nlegend { padding: 0 .375rem; color: var(--muted); font-size: .8125rem; }\n\nfieldset label { display: flex; align-items: baseline; gap: .5rem; color: var(--ink); font-size: .9375rem; }\n\nfieldset input { width: auto; margin: 0; }\n\nfieldset p { margin: .25rem 0 .875rem 1.5rem; color: var(--muted); font-size: .8125rem; }\n";
11
+ export declare const stylesheet = ":root {\n --ink: #23312b;\n --muted: #6b786f;\n --line: #dce2de;\n --surface: #fff;\n --raised: #f7f9f7;\n --accent: #245f43;\n --warn: #a4442b;\n --caution: #b07a12;\n --syntax-tag: #245f43;\n --syntax-attr: #8a5a0b;\n --syntax-string: #2b5f8a;\n color-scheme: light dark;\n accent-color: var(--accent);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --ink: #dde5e0;\n --muted: #96a39b;\n --line: #2f3b35;\n --surface: #161a18;\n --raised: #1d2220;\n --accent: #7fc4a2;\n --warn: #e59a82;\n --caution: #d9a843;\n --syntax-tag: #7fc4a2;\n --syntax-attr: #d9a843;\n --syntax-string: #8fbbe0;\n }\n}\n\n* { box-sizing: border-box; }\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font: 16px/1.65 system-ui, -apple-system, 'Segoe UI', sans-serif;\n text-rendering: optimizeLegibility;\n}\n\nmain {\n max-width: 34rem;\n margin: 0 auto;\n padding-top: 3rem;\n}\n\n/*\n * The logotype, above the page's own heading, on its own dark plate. Sized so the word is\n * the height it was without the plate, whose margin makes the box that much taller. The\n * word is white in either scheme: the plate is what it is read against, not the page.\n */\n.logo {\n display: block;\n height: 2.5rem;\n margin-bottom: 1.5rem;\n}\n\nh1 {\n margin: 0 0 1.75rem;\n padding-bottom: 1rem;\n border-bottom: 1px solid var(--line);\n font-size: 1.5rem;\n font-weight: 620;\n letter-spacing: -.02em;\n line-height: 1.25;\n}\n\n/*\n * A page holding one thing needs no rule under its heading. The rule divides a heading\n * from the record beneath it, and a page that only asks for a key has no record to divide.\n */\nmain.single h1 { margin-bottom: 1.25rem; padding-bottom: 0; border-bottom: none; }\n\n/*\n * A page holding one thing sits in the middle of the window rather than at its top,\n * where it would leave most of the window empty below it.\n */\nbody:has(main.single) { display: flex; min-height: 100vh; min-height: 100dvh; }\nmain.single { width: 100%; max-width: 24rem; margin: auto; padding-bottom: 3rem; }\n\nh2 { margin: 2rem 0 .75rem; font-size: 1.125rem; font-weight: 620; letter-spacing: -.01em; }\n\nh3 { margin: 0 0 .5rem; font-size: 1rem; font-weight: 620; }\n\np { margin: 0 0 1rem; overflow-wrap: break-word; }\n\na { color: var(--accent); text-underline-offset: 3px; overflow-wrap: anywhere; }\n\na:hover { text-decoration-thickness: 2px; }\n\n/*\n * Something to be run or published exactly as written. Its own line breaks are kept and a\n * long line is soft-wrapped rather than cut off: a soft wrap is drawn, not inserted, so\n * what is copied is still character for character what was written. Selecting inside the\n * block takes all of it, since half a command is worse than none.\n */\npre {\n position: relative;\n margin: 0 0 1rem;\n /* Room along the top line for the copy button, which sits over the block rather than in it. */\n padding: .75rem 5.25rem .75rem .875rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--raised);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n user-select: all;\n}\n\ncode { font: .8125rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }\n\n/* Highlighted HTML, marked up by highlight.js. Brackets stay muted so the names read first. */\n.hljs-tag { color: var(--muted); }\n.hljs-name { color: var(--syntax-tag); }\n.hljs-attr { color: var(--syntax-attr); }\n.hljs-string { color: var(--syntax-string); }\n\n/* Made by a script, so it exists only where it works. Quiet: the block is the point. */\n.copy {\n position: absolute;\n top: .5rem;\n right: .5rem;\n padding: .1875rem .5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font: 500 .75rem/1.5 system-ui, sans-serif;\n user-select: none;\n}\n\n.copy:hover { color: var(--ink); border-color: var(--muted); }\n\n/*\n * The two sides of the link, which is what a reader came to read. Each fact about a side\n * gets its own line: what it is, what it is called, the identifier behind that, and how it\n * was shown. Everything else on the page qualifies the pair, so it is set quieter.\n */\n.side {\n margin-bottom: .75rem;\n padding: .875rem 1rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--raised);\n}\n\n/*\n * A subject's accounts are peers, so where there is room they sit two across. The page\n * that lists them is wider for it, and a narrow one keeps them in a single column.\n */\nmain:has(.accounts) { max-width: 54rem; }\n\n.accounts {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));\n gap: .75rem;\n margin-bottom: .75rem;\n}\n\n.accounts .side { position: relative; display: flex; flex-direction: column; margin-bottom: 0; }\n\n/* How the holder lists the account, in the card's corner and out of the lines below. */\n.listing {\n position: absolute;\n top: .75rem;\n right: .875rem;\n padding: .0625rem .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: .6875rem;\n font-weight: 550;\n}\n\n.listing.preferred { border-color: var(--accent); color: var(--accent); }\n\n/* The times sit at the foot of the card, so they line up along a row of uneven cards. */\n.accounts .side dl { margin-top: auto; }\n.accounts .side p:last-of-type { margin-bottom: .625rem; }\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner { display: block; width: 1.25rem; height: 1.25rem; margin: 0 auto .75rem; color: var(--muted); }\n\n.who {\n margin: 0 0 .125rem;\n color: var(--muted);\n font-size: .75rem;\n font-weight: 550;\n letter-spacing: .02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider { display: inline-block; width: .875rem; height: .875rem; margin-right: .375rem; vertical-align: -.125rem; }\n\n/* Clipped 4px left of the text, which hides the date's dot when the date wraps to a new line. */\n.name { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1rem; margin: 0 0 0 -4px; padding-left: 4px; overflow-x: clip; font-size: 1.0625rem; font-weight: 620; letter-spacing: -.01em; }\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since { position: relative; color: var(--muted); font-size: .8125rem; font-weight: 400; letter-spacing: 0; white-space: nowrap; }\n\n/* The dot sits in the gap before the date. */\n.name .since::before { content: \"\u00B7\"; position: absolute; left: -.5rem; transform: translateX(-50%); }\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a { text-decoration: none; }\n\n.name a:hover { text-decoration: underline; }\n\n.reference, .how { margin: 0; color: var(--muted); font-size: .8125rem; line-height: 1.5; }\n\n.name + .how, .reference + .how { margin-top: .5rem; }\n.fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n.fingerprint span { display: inline-block; margin-right: .5ch; }\n.how.additional { padding-left: .75rem; }\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark { display: inline-block; margin-left: .3125rem; padding: 0 .375rem; border: 1px solid var(--line); border-radius: 999px; font-size: .625rem; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: .0625rem; }\n\n.fine { color: var(--muted); font-size: .8125rem; line-height: 1.55; margin-bottom: .625rem; }\n\n/* That the record is signed is a fact about it, so it is set as one and not as fine print. */\n.signed { font-size: .875rem; line-height: 1.55; margin-bottom: .875rem; }\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target { margin-inline: -.75rem; padding: .625rem .75rem; border-radius: 8px; background: var(--raised); }\n\n/* Every time on the record in one column, so they can be compared rather than hunted for. */\ndl {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: .3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: .8125rem;\n}\n\ndt { color: var(--muted); }\n\ndd { margin: 0; font-variant-numeric: tabular-nums; }\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl { margin: .5rem 0 1rem; padding-top: 0; border-top: 0; }\n\n/* Right under the heading, the heading's own rule is the one above it. */\nh1 + dl { padding-top: 0; border-top: 0; }\n.side dl { margin: .625rem 0 0; padding-top: .625rem; }\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line); }\nfooter > :last-child { margin-bottom: 0; }\n\nsection {\n margin: 0 0 1.25rem;\n padding: 1.25rem 1.375rem;\n border: 1px solid var(--line);\n border-radius: 10px;\n}\n\nsection > :last-child { margin-bottom: 0; }\n\n/* What holds a control that cannot be undone. */\nsection.danger, section.caution { margin-top: 2rem; }\nsection.danger { border-color: var(--warn); }\n\n/* What holds a control that breaks something for a while, but can be recovered from. */\nsection.caution { border-color: var(--caution); }\nsection > h2:first-child { margin-top: 0; }\n\nform { margin: 0 0 1.5rem; }\n\nlabel { display: block; margin: 0 0 .75rem; color: var(--muted); font-size: .8125rem; }\n\ninput, textarea {\n display: block;\n width: 100%;\n margin-top: .375rem;\n padding: .5rem .625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font: .875rem/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea { resize: vertical; }\n\ninput:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }\n\nbutton {\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font: 500 .9375rem/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif;\n cursor: pointer;\n}\n\n/* A link that starts something, drawn as a button. */\na.button {\n display: inline-block;\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font-weight: 500;\n font-size: .9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button { margin-left: .5rem; border-color: var(--line); background: none; color: var(--ink); }\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary { border-color: var(--line); background: none; color: var(--ink); }\n\n/* A page's action and its alternative, side by side. */\n.choice { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.5rem; }\n.choice form { margin: 0; }\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices { display: grid; gap: .5rem; margin-bottom: 1.5rem; }\n.choices form { margin: 0; }\n\n.choices button {\n width: 100%;\n text-align: left;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n}\n\n.choices button:hover { border-color: var(--muted); }\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider { width: 1rem; height: 1rem; margin-right: .5rem; vertical-align: -.1875rem; }\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger { border-color: var(--warn); background: var(--warn); }\n\n/*\n * A connection on the settings page: the account's card, then what can be done with it.\n * Its state sits in the card's corner, on the line that names the method.\n */\n.connection { position: relative; margin-bottom: 1rem; }\n\n.state {\n position: absolute;\n top: .75rem;\n right: .875rem;\n display: flex;\n align-items: center;\n gap: .375rem;\n height: 1.625rem;\n padding: 0 .75rem 0 .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n font-weight: 550;\n}\n\n/*\n * The V the badge draws, in the tone that says how the record stands. A record that claims\n * nothing is drawn faint in the text colour, as the badge draws it.\n */\n.verdict { flex-shrink: 0; width: 1rem; height: 1rem; }\n.verdict.pending { opacity: .45; }\n.how .verdict { width: .875rem; height: .875rem; margin-right: .3125rem; vertical-align: -.125rem; }\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) { display: flex; align-items: center; gap: .75rem; }\nh1 .verdict { width: auto; height: 2.25rem; }\n\n/*\n * In a state pill the V is cropped to its strokes and fills the pill's height, so its arms\n * and its foot meet the border. The pill's left padding keeps the arms clear of its curve.\n */\n.state .verdict { width: auto; height: 100%; }\n\n/*\n * A pill's line includes room for descenders below the letters, which leaves its text\n * sitting high. Trimmed to the letters, the padding alone sets the space above and below.\n * In a state pill the word alone is trimmed, so it centres on the V by its letters.\n */\n@supports (text-box: trim-both cap alphabetic) {\n .listing { padding: .3125rem .5rem; text-box: trim-both cap alphabetic; }\n .state > span { text-box: trim-both cap alphabetic; }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1.25rem; font-size: .875rem; }\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence { margin: .375rem 0 0; font-size: .8125rem; }\n\n.connection dl { margin-bottom: .75rem; }\n.connection .side > :last-child { margin-bottom: 0; }\n.connection pre { margin: .75rem 0; background: var(--surface); }\n.connection .actions { margin-top: .875rem; }\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions { display: flex; flex-wrap: wrap; gap: .5rem; }\n.actions form { display: contents; }\n\n.actions button, .actions a.button {\n margin: 0;\n padding: .3125rem .75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n}\n\n.actions button:hover, .actions a.button:hover { border-color: var(--muted); }\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--line); }\n.ending form { margin: 0; }\n\n.ending button.danger {\n padding: .3125rem .75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: .8125rem;\n}\n\nfieldset { margin: 0 0 1.25rem; padding: 1rem 1.125rem .25rem; border: 1px solid var(--line); border-radius: 8px; }\n\nlegend { padding: 0 .375rem; color: var(--muted); font-size: .8125rem; }\n\nfieldset label { display: flex; align-items: baseline; gap: .5rem; color: var(--ink); font-size: .9375rem; }\n\nfieldset input { width: auto; margin: 0; }\n\nfieldset p { margin: .25rem 0 .875rem 1.5rem; color: var(--muted); font-size: .8125rem; }\n";
12
12
  /**
13
13
  * A short digest of the sheet above, put in the link's query so a release that changes the
14
14
  * styling changes its address. Without that the only safe cache is no cache: a reader who
@@ -17,6 +17,9 @@ export const stylesheet = `:root {
17
17
  --accent: #245f43;
18
18
  --warn: #a4442b;
19
19
  --caution: #b07a12;
20
+ --syntax-tag: #245f43;
21
+ --syntax-attr: #8a5a0b;
22
+ --syntax-string: #2b5f8a;
20
23
  color-scheme: light dark;
21
24
  accent-color: var(--accent);
22
25
  }
@@ -31,6 +34,9 @@ export const stylesheet = `:root {
31
34
  --accent: #7fc4a2;
32
35
  --warn: #e59a82;
33
36
  --caution: #d9a843;
37
+ --syntax-tag: #7fc4a2;
38
+ --syntax-attr: #d9a843;
39
+ --syntax-string: #8fbbe0;
34
40
  }
35
41
  }
36
42
 
@@ -116,6 +122,12 @@ pre {
116
122
 
117
123
  code { font: .8125rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
118
124
 
125
+ /* Highlighted HTML, marked up by highlight.js. Brackets stay muted so the names read first. */
126
+ .hljs-tag { color: var(--muted); }
127
+ .hljs-name { color: var(--syntax-tag); }
128
+ .hljs-attr { color: var(--syntax-attr); }
129
+ .hljs-string { color: var(--syntax-string); }
130
+
119
131
  /* Made by a script, so it exists only where it works. Quiet: the block is the point. */
120
132
  .copy {
121
133
  position: absolute;
package/dist/verily.js CHANGED
@@ -10520,7 +10520,7 @@ ${claims(evidence)}`;
10520
10520
  }
10521
10521
 
10522
10522
  // src/version.ts
10523
- var version = "v0.9.1";
10523
+ var version = "v0.9.3";
10524
10524
 
10525
10525
  // src/browser/evidence-dialog.ts
10526
10526
  function stampLink() {
@@ -10531,7 +10531,10 @@ ${claims(evidence)}`;
10531
10531
  var openDialogs = /* @__PURE__ */ new WeakMap();
10532
10532
  var styles2 = `
10533
10533
  * { box-sizing: border-box; }
10534
- 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; }
10534
+ 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; }
10535
+ /* The pane scrolls inside the dialog, which clips its scrollbar to the rounded corners. */
10536
+ dialog[open] { display: flex; flex-direction: column; }
10537
+ .pane { min-height: 0; padding: 24px; overflow-y: auto; }
10535
10538
  dialog::backdrop { background: #15271f66; }
10536
10539
  header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
10537
10540
  h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }
@@ -10799,7 +10802,7 @@ ${claims(evidence)}`;
10799
10802
  const { height, bottom } = menu.getBoundingClientRect();
10800
10803
  if (bottom > innerHeight - edge)
10801
10804
  menu.style.top = `${Math.max(edge, more.getBoundingClientRect().top - gap - height)}px`;
10802
- menu.closest("dialog")?.addEventListener("scroll", shut, { once: true });
10805
+ menu.closest(".pane")?.addEventListener("scroll", shut, { once: true });
10803
10806
  choices[0].focus();
10804
10807
  });
10805
10808
  menu.addEventListener("click", shut);
@@ -11134,6 +11137,7 @@ ${record2.external.id}`;
11134
11137
  const header = node("header");
11135
11138
  const stamp = node("footer");
11136
11139
  const content = node("div", "Checking verification\u2026");
11140
+ const pane = node("div", "", "pane");
11137
11141
  heading.id = "verily-dialog-title";
11138
11142
  dialog.setAttribute("aria-labelledby", heading.id);
11139
11143
  close.type = "button";
@@ -11145,7 +11149,8 @@ ${record2.external.id}`;
11145
11149
  close.autofocus = true;
11146
11150
  title.append(heading);
11147
11151
  header.append(title, close);
11148
- dialog.append(header, content, stamp);
11152
+ pane.append(header, content, stamp);
11153
+ dialog.append(pane);
11149
11154
  const removed = /* @__PURE__ */ new Map();
11150
11155
  const listed = [];
11151
11156
  const listing = (record2) => JSON.stringify([record2.status, record2.mark, record2.retiredAt, record2.approvedAt]);
@@ -11385,6 +11390,7 @@ ${record2.external.id}`;
11385
11390
  const header = node("header");
11386
11391
  const stamp = node("footer");
11387
11392
  const content = node("div", "Loading\u2026", "steps");
11393
+ const pane = node("div", "", "pane");
11388
11394
  heading.id = "verily-connect-title";
11389
11395
  dialog.setAttribute("aria-labelledby", heading.id);
11390
11396
  close.type = "button";
@@ -11393,7 +11399,8 @@ ${record2.external.id}`;
11393
11399
  content.setAttribute("aria-live", "polite");
11394
11400
  stamp.append(stampLink(), node("span", version));
11395
11401
  header.append(heading, close);
11396
- dialog.append(header, content, stamp);
11402
+ pane.append(header, content, stamp);
11403
+ dialog.append(pane);
11397
11404
  root.append(dialog);
11398
11405
  document.body.append(host);
11399
11406
  openDialogs2.set(opener, dialog);
package/dist/version.d.ts CHANGED
@@ -7,4 +7,4 @@
7
7
  * output stays what tsc emitted and nothing has to be substituted into it. A version bump
8
8
  * changes this line with package.json.
9
9
  */
10
- export declare const version = "v0.9.1";
10
+ export declare const version = "v0.9.3";
package/dist/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  * output stays what tsc emitted and nothing has to be substituted into it. A version bump
8
8
  * changes this line with package.json.
9
9
  */
10
- export const version = 'v0.9.1';
10
+ export const version = 'v0.9.3';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkazemi/verily",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Self-hosted account-to-account verification for Node.js",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,6 +53,7 @@
53
53
  "@exodus/bytes": "^1.16.0",
54
54
  "@types/node": "^22.0.0",
55
55
  "@types/pg": "^8.15.5",
56
+ "highlight.js": "^11.12.0",
56
57
  "html-encoding-sniffer": "^7.0.0",
57
58
  "openpgp": "^6.3.2",
58
59
  "parse5": "^8.0.1",