@thessor/widget 0.1.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 ADDED
@@ -0,0 +1,94 @@
1
+ # @thessor/widget
2
+
3
+ Framework-agnostic verification badge web component for Thessor. A pill showing
4
+ verification status that expands into an inline cryptographic proof panel —
5
+ no React, Vue, or other framework required.
6
+
7
+ ## Install
8
+
9
+ ### Script tag (no build step)
10
+
11
+ ```html
12
+ <script src="https://unpkg.com/@thessor/widget"></script>
13
+ <thessor-badge decision-id="env_8f3a2c1d94e6"></thessor-badge>
14
+ ```
15
+
16
+ ### npm
17
+
18
+ ```bash
19
+ npm install @thessor/widget
20
+ ```
21
+
22
+ ```js
23
+ import '@thessor/widget';
24
+ ```
25
+
26
+ ```html
27
+ <thessor-badge decision-id="env_8f3a2c1d94e6"></thessor-badge>
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```html
33
+ <thessor-badge decision-id="env_8f3a2c1d94e6"></thessor-badge>
34
+ ```
35
+
36
+ On mount, the badge fetches `GET https://api.thessor.com/verify/{decision-id}`
37
+ to populate its proof panel with the envelope ID, canonical hash, signature
38
+ suite, and sealed timestamp. Clicking the badge expands the panel inline,
39
+ directly below it.
40
+
41
+ ### Theme
42
+
43
+ The Thessor mark auto-detects whether it's sitting on a light or dark
44
+ background by walking up the DOM for the nearest ancestor with a
45
+ non-transparent `background-color`. To force it instead of relying on
46
+ detection, set `data-theme`:
47
+
48
+ ```html
49
+ <thessor-badge decision-id="env_8f3a2c1d94e6" data-theme="dark"></thessor-badge>
50
+ ```
51
+
52
+ The expanded proof panel is always dark (`#0B0B0E`), regardless of host theme.
53
+
54
+ ### States
55
+
56
+ - **Loading** — badge renders immediately with a pulsing status dot; the proof
57
+ panel shows a skeleton until the fetch resolves.
58
+ - **Success** — panel shows envelope ID, sealed timestamp, and signature suite,
59
+ plus the hashed → signed → Merkle checkpoint → Bitcoin stage chain.
60
+ - **Error** — if the verify request fails, the badge switches to a muted
61
+ "Unable to verify" state. No exception is thrown.
62
+
63
+ ## Local development
64
+
65
+ ```bash
66
+ npm install
67
+ npm run build # bundles src/ to dist/ (ESM + IIFE) and emits .d.ts files
68
+ npm run demo # serves the repo root at http://localhost:4173/demo.html
69
+ ```
70
+
71
+ Open `demo.html` for visual QA: badges on light and dark hosts, a manual
72
+ `data-theme` override, a loading skeleton, and an error state. The demo mocks
73
+ `window.fetch` for requests to `api.thessor.com` since that API isn't reachable
74
+ from a local checkout — swap in the real endpoint once it's deployed.
75
+
76
+ `npm run dev` runs the same build in `--watch` mode.
77
+
78
+ ## Build output
79
+
80
+ - `dist/thessor-widget.js` — ES module (`import '@thessor/widget'`)
81
+ - `dist/thessor-widget.umd.js` — minified, self-executing bundle for
82
+ `<script src>` / unpkg / jsDelivr use. The element registers itself via
83
+ `customElements.define` as a side effect, so this is a plain IIFE rather
84
+ than a true CJS/AMD/global UMD wrapper — the practical equivalent for a
85
+ script-tag-loaded custom element.
86
+ - `dist/index.d.ts` (+ per-module `.d.ts` files) — TypeScript types
87
+
88
+ ## Notes before publishing
89
+
90
+ - `license` in `package.json` is currently set to `UNLICENSED` — pick a real
91
+ license before publishing publicly.
92
+ - Confirm the production shape of the `GET /verify/{decision-id}` response
93
+ matches `ProofData` in `src/types.ts` (`envelope_id`, `canonical_hash`,
94
+ `signature_suite`, `created_at`).
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Framework-agnostic verification badge. Renders a pill showing Thessor's
3
+ * verification status; clicking it expands an inline proof panel populated
4
+ * from `GET /verify/{decision-id}`.
5
+ *
6
+ * Usage: <thessor-badge decision-id="env_..."></thessor-badge>
7
+ * Theme override: <thessor-badge decision-id="..." data-theme="dark">
8
+ */
9
+ export declare class ThessorBadge extends HTMLElement {
10
+ static observedAttributes: string[];
11
+ private shadow;
12
+ private state;
13
+ private proof;
14
+ private isOpen;
15
+ private abortController;
16
+ private hasRendered;
17
+ constructor();
18
+ connectedCallback(): void;
19
+ disconnectedCallback(): void;
20
+ attributeChangedCallback(name: string): void;
21
+ private applyTheme;
22
+ private fetchProof;
23
+ private setState;
24
+ private togglePanel;
25
+ private render;
26
+ private renderBadgeContent;
27
+ private renderPanelContent;
28
+ }
package/dist/logo.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare const THESSOR_LOGO_LIGHT: string;
2
+ export declare const THESSOR_LOGO_DARK: string;
@@ -0,0 +1 @@
1
+ export declare const styles = "\n@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');\n\n:host {\n all: initial;\n display: inline-block;\n font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;\n -webkit-font-smoothing: antialiased;\n}\n\n* {\n box-sizing: border-box;\n}\n\n.root {\n position: relative;\n display: inline-block;\n}\n\n.badge {\n display: inline-flex;\n align-items: center;\n gap: 11px;\n padding: 10px 14px;\n border-radius: 10px;\n border: 1px solid var(--tw-border, rgba(0, 0, 0, 0.12));\n background: var(--tw-bg, #ffffff);\n color: var(--tw-fg, #16171a);\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.2;\n user-select: none;\n transition: border-color 150ms ease, background 150ms ease;\n}\n\n.badge:hover {\n border-color: var(--tw-border-hover, rgba(0, 0, 0, 0.22));\n}\n\n.badge:focus-visible {\n outline: 2px solid #6c8cf5;\n outline-offset: 2px;\n}\n\n:host([data-theme='dark']) .badge,\n.root.theme-dark .badge {\n --tw-bg: #17181c;\n --tw-fg: #f3f3f5;\n --tw-border: rgba(255, 255, 255, 0.14);\n --tw-border-hover: rgba(255, 255, 255, 0.26);\n}\n\n.dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #2fbf6c;\n flex-shrink: 0;\n}\n\n.dot.dot-muted {\n background: #9a9aa2;\n}\n\n.dot.dot-loading {\n animation: pulse 1.4s ease-in-out infinite;\n}\n\n.logo {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n height: 16px;\n}\n\n.logo-img {\n display: block;\n height: 100%;\n width: auto;\n}\n\n.logo-dark {\n display: none;\n}\n\n:host([data-theme='dark']) .logo-light,\n.root.theme-dark .logo-light {\n display: none;\n}\n\n:host([data-theme='dark']) .logo-dark,\n.root.theme-dark .logo-dark {\n display: block;\n}\n\n.label {\n white-space: nowrap;\n}\n\n.label .muted-text {\n color: var(--tw-fg);\n opacity: 0.6;\n}\n\n/* --- Proof panel --- */\n\n.panel-wrapper {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 220ms cubic-bezier(0.4, 0, 0.2, 1);\n position: absolute;\n top: calc(100% + 8px);\n left: 0;\n width: max-content;\n min-width: 320px;\n max-width: min(380px, 92vw);\n z-index: 2147483000;\n}\n\n.panel-wrapper.open {\n grid-template-rows: 1fr;\n}\n\n.panel-inner {\n overflow: hidden;\n min-height: 0;\n}\n\n.panel {\n background: #0b0b0e;\n color: #f3f3f5;\n border-radius: 14px;\n border: 1px solid rgba(255, 255, 255, 0.08);\n padding: 16px;\n opacity: 0;\n transform: scale(0.96);\n transform-origin: top left;\n transition: opacity 220ms cubic-bezier(0.4, 0, 0.2, 1),\n transform 220ms cubic-bezier(0.4, 0, 0.2, 1);\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);\n}\n\n.panel-wrapper.open .panel {\n opacity: 1;\n transform: scale(1);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .panel-wrapper,\n .panel {\n transition: none !important;\n }\n}\n\n.panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 14px;\n}\n\n.panel-title {\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.01em;\n}\n\n.live-indicator {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: #6adf9a;\n font-weight: 500;\n}\n\n.live-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: #2fbf6c;\n box-shadow: 0 0 0 0 rgba(47, 191, 108, 0.5);\n animation: live-pulse 2s ease-out infinite;\n}\n\n@keyframes live-pulse {\n 0% {\n box-shadow: 0 0 0 0 rgba(47, 191, 108, 0.45);\n }\n 70% {\n box-shadow: 0 0 0 6px rgba(47, 191, 108, 0);\n }\n 100% {\n box-shadow: 0 0 0 0 rgba(47, 191, 108, 0);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .live-dot {\n animation: none;\n }\n}\n\n/* stage chain */\n\n.stage-chain {\n display: grid;\n grid-template-columns: auto 1fr auto 1fr auto 1fr auto;\n row-gap: 8px;\n margin-bottom: 16px;\n}\n\n.stage-dot {\n grid-row: 1;\n align-self: center;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.stage-track {\n grid-row: 1;\n align-self: center;\n height: 2px;\n min-width: 14px;\n}\n\n.stage-label {\n grid-row: 2;\n font-size: 10px;\n text-align: center;\n color: rgba(255, 255, 255, 0.6);\n line-height: 1.3;\n padding: 0 2px;\n}\n\n/* data rows */\n\n.rows {\n display: flex;\n flex-direction: column;\n gap: 9px;\n padding: 12px 0;\n border-top: 1px solid rgba(255, 255, 255, 0.08);\n border-bottom: 1px solid rgba(255, 255, 255, 0.08);\n}\n\n.row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 12px;\n}\n\n.row-label {\n font-size: 11px;\n color: rgba(255, 255, 255, 0.5);\n flex-shrink: 0;\n}\n\n.row-value {\n font-family: 'IBM Plex Mono', ui-monospace, monospace;\n font-size: 11px;\n color: #f3f3f5;\n text-align: right;\n overflow-wrap: anywhere;\n}\n\n/* footer */\n\n.panel-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-top: 12px;\n}\n\n.view-link {\n font-size: 12px;\n color: #a3b4f7;\n text-decoration: none;\n font-weight: 500;\n}\n\n.view-link:hover {\n text-decoration: underline;\n}\n\n.watermark {\n font-size: 10px;\n color: rgba(255, 255, 255, 0.28);\n}\n\n/* skeleton */\n\n.skeleton-line {\n height: 10px;\n border-radius: 4px;\n background: linear-gradient(\n 90deg,\n rgba(255, 255, 255, 0.06) 25%,\n rgba(255, 255, 255, 0.14) 37%,\n rgba(255, 255, 255, 0.06) 63%\n );\n background-size: 400% 100%;\n animation: shimmer 1.4s ease infinite;\n}\n\n@keyframes shimmer {\n 0% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0 50%;\n }\n}\n\n@keyframes pulse {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.4;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .skeleton-line,\n .dot-loading {\n animation: none;\n }\n}\n\n.error-message {\n font-size: 12px;\n color: rgba(255, 255, 255, 0.65);\n padding: 8px 0 4px;\n line-height: 1.5;\n}\n";
@@ -0,0 +1,7 @@
1
+ import type { ThemeMode } from './types';
2
+ /**
3
+ * Walks up from the host element looking for the nearest ancestor with a
4
+ * non-transparent background-color to decide light vs. dark. A
5
+ * `data-theme` attribute on the badge itself always wins.
6
+ */
7
+ export declare function detectHostTheme(el: HTMLElement): ThemeMode;