@tollbooth-dpyc/web 0.4.0 → 1.0.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 +35 -3
- package/dist/react/DebugPanel.d.ts +6 -0
- package/dist/react/DebugPanel.js +33 -6
- package/dist/react/QuoteScroller.d.ts +36 -4
- package/dist/react/QuoteScroller.js +21 -9
- package/dist/react/SessionKeyClaim.d.ts +3 -2
- package/dist/react/SessionKeyClaim.js +5 -4
- package/dist/react/debugSpacer.d.ts +15 -0
- package/dist/react/debugSpacer.js +21 -0
- package/dist/react/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,8 +44,13 @@ export const onRequest = makeMcpProxy("https://chartremotely-mcp.fastmcp.app/mcp
|
|
|
44
44
|
### Debug panel
|
|
45
45
|
|
|
46
46
|
`callTool` logs to a shared, scrubbed ring buffer; `DebugPanel` shows it as a
|
|
47
|
-
bar along the bottom of the page with Copy and Clear. Put it once
|
|
48
|
-
|
|
47
|
+
bar along the bottom of the page with Copy and Clear. Put it once, **last** in
|
|
48
|
+
the app shell. The bar is fixed to the viewport, but it also keeps its own
|
|
49
|
+
room in the page: an in-flow spacer as tall as the bar is right now (collapsed
|
|
50
|
+
or open) plus the iOS safe-area inset, so the page's last line always scrolls
|
|
51
|
+
clear of it. Mounted last, that spacer is the page's last thing; mounted
|
|
52
|
+
earlier, the room opens up there instead. Anything the site alone needs goes
|
|
53
|
+
in as children, drawn above the log:
|
|
49
54
|
|
|
50
55
|
```tsx
|
|
51
56
|
import { configureDebugLog, debugPush } from "@tollbooth-dpyc/web";
|
|
@@ -66,7 +71,12 @@ zone the site stores under `<prefix>:timezone`, if any.
|
|
|
66
71
|
Something to read while a page loads. The quotes are the site's own; the
|
|
67
72
|
package only scrolls them. Name a remote corpus to edit the set without a
|
|
68
73
|
redeploy — it is fetched once per page and replaces the inline set when it
|
|
69
|
-
arrives, or never, if it cannot be had
|
|
74
|
+
arrives, or never, if it cannot be had.
|
|
75
|
+
|
|
76
|
+
The package owns the mechanics (timing, cross-fade, reduced motion, pausing
|
|
77
|
+
while the tab is hidden, `aria-live`, and a reserved height taken from the
|
|
78
|
+
tallest quote as *your* styles lay it out). How the quotes look is yours:
|
|
79
|
+
without `classNames` they are plain text inheriting from the page.
|
|
70
80
|
|
|
71
81
|
```tsx
|
|
72
82
|
import { QuoteScroller } from "@tollbooth-dpyc/web/react";
|
|
@@ -79,9 +89,23 @@ const QUOTES: Quote[] = [{ text: "Observe the seasons.", author: "Hesiod" }];
|
|
|
79
89
|
source="https://raw.githubusercontent.com/lonniev/dpyc-community/main/quotes-agrarian.json"
|
|
80
90
|
heading="Reading the ledger…"
|
|
81
91
|
spinner
|
|
92
|
+
classNames={{
|
|
93
|
+
root: "px-4 py-6 text-center",
|
|
94
|
+
heading: "mb-5 font-mono text-[11px] uppercase tracking-[0.3em] text-[var(--tb-accent)]",
|
|
95
|
+
figure: "mx-auto flex max-w-xl flex-col gap-3",
|
|
96
|
+
text: "font-serif text-[17px] italic leading-relaxed",
|
|
97
|
+
mark: "not-italic text-[var(--tb-accent)]",
|
|
98
|
+
author: "font-mono text-[10.5px] uppercase tracking-[0.22em] text-[var(--tb-muted)]",
|
|
99
|
+
}}
|
|
82
100
|
/>
|
|
83
101
|
```
|
|
84
102
|
|
|
103
|
+
Props: `quotes`, `source`, `heading`, `spinner`, `intervalMs` (default 3500),
|
|
104
|
+
`classNames` (`root`, `heading`, `spinner`, `figure`, `text`, `mark`,
|
|
105
|
+
`author`), `marks` (curly quotes by default; `false` for none, or
|
|
106
|
+
`[open, close]`), and `renderQuote(quote)` to draw a quote's markup yourself
|
|
107
|
+
(then `text`, `mark` and `author` are unused).
|
|
108
|
+
|
|
85
109
|
### Styling
|
|
86
110
|
|
|
87
111
|
Components use Tailwind classes coloured only through CSS variables. Tell
|
|
@@ -96,6 +120,14 @@ them after:
|
|
|
96
120
|
:root { --tb-accent: #4cc38a; }
|
|
97
121
|
```
|
|
98
122
|
|
|
123
|
+
## Stability
|
|
124
|
+
|
|
125
|
+
From 1.0.0 the public API — everything exported from `@tollbooth-dpyc/web`,
|
|
126
|
+
`/react`, `/pages-proxy` and the `theme.css` tokens — follows semver. Patch and
|
|
127
|
+
minor releases are safe to take unattended (the fleet's Renovate preset
|
|
128
|
+
auto-merges them); a breaking change ships only as a new major, which Renovate
|
|
129
|
+
holds for review.
|
|
130
|
+
|
|
99
131
|
## Develop
|
|
100
132
|
|
|
101
133
|
```sh
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Touch-first: every control is a real button at least 40px square, nothing
|
|
15
15
|
* hides behind hover, and the page under the bar stays tappable.
|
|
16
|
+
*
|
|
17
|
+
* The panel keeps its own space: beside the fixed bar it renders an in-flow
|
|
18
|
+
* spacer as tall as the bar is right now (collapsed or open, measured with a
|
|
19
|
+
* ResizeObserver) plus the iOS safe-area inset, so the page's last line can
|
|
20
|
+
* always scroll clear of it. Mount it LAST in the app shell so that spacer is
|
|
21
|
+
* the last thing in the page's flow.
|
|
16
22
|
*/
|
|
17
23
|
import { type ReactNode } from "react";
|
|
18
24
|
export interface DebugPanelProps {
|
package/dist/react/DebugPanel.js
CHANGED
|
@@ -14,11 +14,18 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
14
14
|
*
|
|
15
15
|
* Touch-first: every control is a real button at least 40px square, nothing
|
|
16
16
|
* hides behind hover, and the page under the bar stays tappable.
|
|
17
|
+
*
|
|
18
|
+
* The panel keeps its own space: beside the fixed bar it renders an in-flow
|
|
19
|
+
* spacer as tall as the bar is right now (collapsed or open, measured with a
|
|
20
|
+
* ResizeObserver) plus the iOS safe-area inset, so the page's last line can
|
|
21
|
+
* always scroll clear of it. Mount it LAST in the app shell so that spacer is
|
|
22
|
+
* the last thing in the page's flow.
|
|
17
23
|
*/
|
|
18
|
-
import { useEffect, useState } from "react";
|
|
24
|
+
import { useEffect, useRef, useState } from "react";
|
|
19
25
|
import { onProofExpired } from "../client.js";
|
|
20
26
|
import { captureGlobalErrors, clearDebug, debugLogText, debugPush, debugSeverity, } from "../debugLog.js";
|
|
21
27
|
import { readStored, writeStored } from "../storage.js";
|
|
28
|
+
import { debugSpacerHeight, debugSpacerPx } from "./debugSpacer.js";
|
|
22
29
|
import { useDebugLog } from "./useDebugLog.js";
|
|
23
30
|
const OPEN_KEY = "debug-open";
|
|
24
31
|
const TYPE_INK = {
|
|
@@ -40,6 +47,26 @@ export default function DebugPanel({ title = "Debug", defaultOpen = false, captu
|
|
|
40
47
|
return saved === "" ? defaultOpen : saved === "1";
|
|
41
48
|
});
|
|
42
49
|
const [copied, setCopied] = useState("");
|
|
50
|
+
const barRef = useRef(null);
|
|
51
|
+
// Null until measured (and on the server, or with no ResizeObserver): the
|
|
52
|
+
// spacer then reserves the collapsed bar.
|
|
53
|
+
const [barHeight, setBarHeight] = useState(null);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const bar = barRef.current;
|
|
56
|
+
const RO = globalThis.ResizeObserver;
|
|
57
|
+
if (!bar || !RO)
|
|
58
|
+
return;
|
|
59
|
+
// The observer reports once on observe and after every resize — opening,
|
|
60
|
+
// closing, a new log line, rotation — batched to the frame, and a state
|
|
61
|
+
// write only when the whole-pixel height actually changes.
|
|
62
|
+
const ro = new RO((entries) => {
|
|
63
|
+
const box = entries[entries.length - 1]?.borderBoxSize?.[0];
|
|
64
|
+
const next = debugSpacerPx(box ? box.blockSize : bar.getBoundingClientRect().height);
|
|
65
|
+
setBarHeight((prev) => (prev === next ? prev : next));
|
|
66
|
+
});
|
|
67
|
+
ro.observe(bar);
|
|
68
|
+
return () => ro.disconnect();
|
|
69
|
+
}, []);
|
|
43
70
|
useEffect(() => {
|
|
44
71
|
if (!captureErrors)
|
|
45
72
|
return;
|
|
@@ -73,9 +100,9 @@ export default function DebugPanel({ title = "Debug", defaultOpen = false, captu
|
|
|
73
100
|
: notices > 0
|
|
74
101
|
? "bg-[var(--tb-notice-bg)] text-[var(--tb-notice-ink)] border-[var(--tb-notice-line)]"
|
|
75
102
|
: "bg-[var(--tb-surface-2)] text-[var(--tb-ink)] border-[var(--tb-line)]";
|
|
76
|
-
return (_jsxs("div", { className: "pointer-events-none fixed inset-x-0 bottom-0 z-50 flex flex-col items-end", children: [_jsxs("div", { className: "pointer-events-auto flex flex-wrap justify-end gap-1 pr-3", children: [open && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: copy, className: plainTab, "aria-label": "Copy the whole log", children: copied === "ok" ? "Copied" : copied === "failed" ? "Copy failed" : "Copy" }), _jsx("button", { type: "button", onClick: clearDebug, className: plainTab, children: "Clear" })] })), _jsxs("button", { type: "button", onClick: toggle, "aria-expanded": open, className: `${tab} ${toggleTone}`, children: [open ? "Hide" : title, " (", log.length, failures > 0 ? ` · ${failures} err` : "", notices > 0 ? ` · ${notices} notice` : "", ")"] })] }), open && (_jsxs("div", { className: "pointer-events-auto max-h-[40vh] w-full overflow-y-auto border-t border-[var(--tb-line)] bg-[var(--tb-surface)] p-3 font-mono text-xs text-[var(--tb-ink)]", children: [children && _jsx("div", { className: "mb-2 border-b border-[var(--tb-line)] pb-2 font-sans", children: children }), log.length === 0 && _jsx("div", { className: "text-[var(--tb-muted)]", children: "No MCP activity yet." }), log.map((entry, i) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": "true", "data-tb-debug-spacer": "", style: { height: debugSpacerHeight(barHeight), flexShrink: 0 } }), _jsxs("div", { ref: barRef, className: "pointer-events-none fixed inset-x-0 bottom-0 z-50 flex flex-col items-end", children: [_jsxs("div", { className: "pointer-events-auto flex flex-wrap justify-end gap-1 pr-3", children: [open && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: copy, className: plainTab, "aria-label": "Copy the whole log", children: copied === "ok" ? "Copied" : copied === "failed" ? "Copy failed" : "Copy" }), _jsx("button", { type: "button", onClick: clearDebug, className: plainTab, children: "Clear" })] })), _jsxs("button", { type: "button", onClick: toggle, "aria-expanded": open, className: `${tab} ${toggleTone}`, children: [open ? "Hide" : title, " (", log.length, failures > 0 ? ` · ${failures} err` : "", notices > 0 ? ` · ${notices} notice` : "", ")"] })] }), open && (_jsxs("div", { className: "pointer-events-auto max-h-[40vh] w-full overflow-y-auto border-t border-[var(--tb-line)] bg-[var(--tb-surface)] p-3 font-mono text-xs text-[var(--tb-ink)]", children: [children && _jsx("div", { className: "mb-2 border-b border-[var(--tb-line)] pb-2 font-sans", children: children }), log.length === 0 && _jsx("div", { className: "text-[var(--tb-muted)]", children: "No MCP activity yet." }), log.map((entry, i) => {
|
|
104
|
+
const sev = debugSeverity(entry);
|
|
105
|
+
const flag = sev === "ok" ? null : FLAGGED[sev];
|
|
106
|
+
return (_jsxs("div", { className: `-mx-1 flex gap-2 rounded px-1 py-0.5 ${flag?.row ?? ""}`, children: [_jsx("span", { className: "shrink-0 text-[var(--tb-muted)]", children: entry.ts }), _jsxs("span", { className: `w-14 shrink-0 ${flag?.ink ?? TYPE_INK[entry.type]}`, children: [sev === "notice" ? "notice" : entry.type, sev === "failure" && entry.type !== "error" ? " !" : ""] }), _jsx("span", { className: `min-w-0 break-all ${flag?.ink ?? ""}`, children: entry.message })] }, log.length - i));
|
|
107
|
+
})] }))] })] }));
|
|
81
108
|
}
|
|
@@ -11,8 +11,16 @@
|
|
|
11
11
|
* The order is shuffled and then walked, so nothing repeats until every quote
|
|
12
12
|
* has shown. It cross-fades (instantly under reduced motion), stops while the
|
|
13
13
|
* tab is hidden, and reserves the height of its longest quote so the page
|
|
14
|
-
* does not jump as quotes change.
|
|
14
|
+
* does not jump as quotes change.
|
|
15
|
+
*
|
|
16
|
+
* Mechanics only. The package owns the timing, the fade, the cache and the
|
|
17
|
+
* reserved height; every visual choice — type, size, colour, spacing,
|
|
18
|
+
* alignment — belongs to the site, through `classNames` or `renderQuote`.
|
|
19
|
+
* With neither, the quotes are plain text that inherits from the page. The
|
|
20
|
+
* reserved height is the tallest quote as the SITE styles it: every quote is
|
|
21
|
+
* laid out, invisibly, in the same grid cell as the one on show.
|
|
15
22
|
*/
|
|
23
|
+
import { type ReactNode } from "react";
|
|
16
24
|
import { type Quote } from "../quotes.ts";
|
|
17
25
|
export interface QuoteScrollerProps {
|
|
18
26
|
/** The site's own quotes: shown at once, and kept if `source` fails. Pass a
|
|
@@ -27,7 +35,31 @@ export interface QuoteScrollerProps {
|
|
|
27
35
|
spinner?: boolean;
|
|
28
36
|
/** How long each quote stays, in ms. Default 3500. */
|
|
29
37
|
intervalMs?: number;
|
|
30
|
-
/** Classes for
|
|
31
|
-
|
|
38
|
+
/** Classes for each part. The package adds no typography or colour of its
|
|
39
|
+
* own to any of them. `text`, `mark` and `author` are unused when
|
|
40
|
+
* `renderQuote` is given. */
|
|
41
|
+
classNames?: QuoteScrollerClassNames;
|
|
42
|
+
/** Full control of one quote's markup. The package still wraps it for the
|
|
43
|
+
* fade and the reserved height. */
|
|
44
|
+
renderQuote?: (quote: Quote) => ReactNode;
|
|
45
|
+
/** Quotation marks around the text: the curly pair by default, `false` for
|
|
46
|
+
* none, or your own `[open, close]`. */
|
|
47
|
+
marks?: boolean | readonly [open: string, close: string];
|
|
48
|
+
}
|
|
49
|
+
export interface QuoteScrollerClassNames {
|
|
50
|
+
/** The outer box. */
|
|
51
|
+
root?: string;
|
|
52
|
+
/** The status line. */
|
|
53
|
+
heading?: string;
|
|
54
|
+
/** The spinner icon. Given, it replaces the default 1em size. */
|
|
55
|
+
spinner?: string;
|
|
56
|
+
/** Each quote's `<figure>`. */
|
|
57
|
+
figure?: string;
|
|
58
|
+
/** The `<blockquote>` holding the text and its marks. */
|
|
59
|
+
text?: string;
|
|
60
|
+
/** Each quotation mark. */
|
|
61
|
+
mark?: string;
|
|
62
|
+
/** The `<figcaption>` naming the author. */
|
|
63
|
+
author?: string;
|
|
32
64
|
}
|
|
33
|
-
export default function QuoteScroller({ quotes, source, heading, spinner, intervalMs,
|
|
65
|
+
export default function QuoteScroller({ quotes, source, heading, spinner, intervalMs, classNames, renderQuote, marks, }: QuoteScrollerProps): import("react").JSX.Element;
|
|
@@ -12,11 +12,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* The order is shuffled and then walked, so nothing repeats until every quote
|
|
13
13
|
* has shown. It cross-fades (instantly under reduced motion), stops while the
|
|
14
14
|
* tab is hidden, and reserves the height of its longest quote so the page
|
|
15
|
-
* does not jump as quotes change.
|
|
15
|
+
* does not jump as quotes change.
|
|
16
|
+
*
|
|
17
|
+
* Mechanics only. The package owns the timing, the fade, the cache and the
|
|
18
|
+
* reserved height; every visual choice — type, size, colour, spacing,
|
|
19
|
+
* alignment — belongs to the site, through `classNames` or `renderQuote`.
|
|
20
|
+
* With neither, the quotes are plain text that inherits from the page. The
|
|
21
|
+
* reserved height is the tallest quote as the SITE styles it: every quote is
|
|
22
|
+
* laid out, invisibly, in the same grid cell as the one on show.
|
|
16
23
|
*/
|
|
17
|
-
import { useEffect,
|
|
24
|
+
import { useEffect, useRef, useState } from "react";
|
|
18
25
|
import { Loader2 } from "lucide-react";
|
|
19
26
|
import { loadQuotes, peekQuotes, shuffle } from "../quotes.js";
|
|
27
|
+
const CURLY = ["\u201c", "\u201d"];
|
|
20
28
|
const FADE_MS = 450;
|
|
21
29
|
function useReducedMotion() {
|
|
22
30
|
const query = "(prefers-reduced-motion: reduce)";
|
|
@@ -40,7 +48,7 @@ function usePageVisible() {
|
|
|
40
48
|
}, []);
|
|
41
49
|
return shown;
|
|
42
50
|
}
|
|
43
|
-
export default function QuoteScroller({ quotes, source, heading, spinner = false, intervalMs = 3500,
|
|
51
|
+
export default function QuoteScroller({ quotes, source, heading, spinner = false, intervalMs = 3500, classNames = {}, renderQuote, marks = true, }) {
|
|
44
52
|
const reduced = useReducedMotion();
|
|
45
53
|
const pageVisible = usePageVisible();
|
|
46
54
|
// Content, not identity: a site that passes an inline array still gets one
|
|
@@ -94,13 +102,17 @@ export default function QuoteScroller({ quotes, source, heading, spinner = false
|
|
|
94
102
|
setShown(true);
|
|
95
103
|
};
|
|
96
104
|
}, [list, intervalMs, reduced, pageVisible]);
|
|
97
|
-
const longest = useMemo(() => list.reduce((a, q) => (!a || q.text.length + q.author.length > a.text.length + a.author.length ? q : a), undefined), [list]);
|
|
98
105
|
const q = list[index % Math.max(list.length, 1)];
|
|
99
|
-
|
|
106
|
+
const pair = marks === true ? CURLY : marks === false ? null : marks;
|
|
107
|
+
const draw = renderQuote ??
|
|
108
|
+
((quote) => (_jsxs("figure", { className: classNames.figure, children: [_jsxs("blockquote", { className: classNames.text, children: [pair && _jsx("span", { className: classNames.mark, children: pair[0] }), quote.text, pair && _jsx("span", { className: classNames.mark, children: pair[1] })] }), _jsx("figcaption", { className: classNames.author, children: quote.author })] })));
|
|
109
|
+
return (_jsxs("div", { className: classNames.root, children: [heading && (_jsxs("div", { className: classNames.heading, children: [spinner && (_jsx(Loader2, { "aria-hidden": true, className: `${classNames.spinner ?? "inline-block h-[1em] w-[1em]"} motion-safe:animate-spin` })), spinner && " ", heading] })), q && (_jsxs("div", { style: STAGE, children: [list.map((each, i) => (_jsx("div", { style: HOLDER, "aria-hidden": true, children: draw(each) }, i))), _jsx("div", { style: {
|
|
110
|
+
...CELL,
|
|
100
111
|
opacity: shown ? 1 : 0,
|
|
101
112
|
transition: reduced ? "none" : `opacity ${FADE_MS}ms ease`,
|
|
102
|
-
}, live: true })] }))] }));
|
|
103
|
-
}
|
|
104
|
-
function QuoteText({ quote, className, style, hidden = false, live = false, }) {
|
|
105
|
-
return (_jsxs("figure", { className: `m-0 flex flex-col justify-center gap-3 ${className}`, style: style, "aria-hidden": hidden || undefined, "aria-live": live ? "polite" : undefined, "aria-atomic": live || undefined, children: [_jsxs("blockquote", { className: "m-0 font-serif text-[17px] italic leading-relaxed text-[var(--tb-ink)]", children: [_jsx("span", { className: "not-italic text-[var(--tb-accent)]", children: "\u201C" }), quote.text, _jsx("span", { className: "not-italic text-[var(--tb-accent)]", children: "\u201D" })] }), _jsx("figcaption", { className: "font-mono text-[10.5px] uppercase tracking-[0.22em] text-[var(--tb-muted)]", children: quote.author })] }));
|
|
113
|
+
}, "aria-live": "polite", "aria-atomic": true, children: draw(q) })] }))] }));
|
|
106
114
|
}
|
|
115
|
+
// Structure for the mechanism only, inline so it needs no stylesheet.
|
|
116
|
+
const STAGE = { display: "grid" };
|
|
117
|
+
const CELL = { gridArea: "1 / 1", minWidth: 0 };
|
|
118
|
+
const HOLDER = { ...CELL, visibility: "hidden" };
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Renders ONLY when this browser holds the session nsec for the signed-in
|
|
5
5
|
* npub. NIP-07 and courier-proof sign-ins see nothing: no disabled control,
|
|
6
|
-
* no placeholder, no mention.
|
|
7
|
-
*
|
|
6
|
+
* no placeholder, no mention. Painting the key takes two deliberate taps —
|
|
7
|
+
* "Claim your session key", then the Reveal chip — so an account page opened
|
|
8
|
+
* in front of other people does not show it.
|
|
8
9
|
*
|
|
9
10
|
* Destinations: copy, download .env, password-manager prompt, and an
|
|
10
11
|
* encrypted DM to another npub the patron names. The nsec never reaches a
|
|
@@ -4,8 +4,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
*
|
|
5
5
|
* Renders ONLY when this browser holds the session nsec for the signed-in
|
|
6
6
|
* npub. NIP-07 and courier-proof sign-ins see nothing: no disabled control,
|
|
7
|
-
* no placeholder, no mention.
|
|
8
|
-
*
|
|
7
|
+
* no placeholder, no mention. Painting the key takes two deliberate taps —
|
|
8
|
+
* "Claim your session key", then the Reveal chip — so an account page opened
|
|
9
|
+
* in front of other people does not show it.
|
|
9
10
|
*
|
|
10
11
|
* Destinations: copy, download .env, password-manager prompt, and an
|
|
11
12
|
* encrypted DM to another npub the patron names. The nsec never reaches a
|
|
@@ -161,10 +162,10 @@ function SessionKeyClaimInner() {
|
|
|
161
162
|
setDmOpen(false);
|
|
162
163
|
setFlash(null);
|
|
163
164
|
setRecipient("");
|
|
164
|
-
}, className: `text-[12px] ${muted} hover:text-[var(--tb-ink)]`, children: "Hide" })] }), _jsxs("p", { className: `mb-3 text-[12.5px] leading-relaxed ${muted}`, children: ["Anyone who holds this key is you. It leaves this page only when you choose a destination below \u2014 never logged, never sent to ", appName, "."] }),
|
|
165
|
+
}, className: `text-[12px] ${muted} hover:text-[var(--tb-ink)]`, children: "Hide" })] }), _jsxs("p", { className: `mb-3 text-[12.5px] leading-relaxed ${muted}`, children: ["Anyone who holds this key is you. It leaves this page only when you choose a destination below \u2014 never logged, never sent to ", appName, "."] }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(DestButton, { onClick: () => setShowKey((v) => !v), active: showKey, children: showKey ? "Conceal" : "Reveal" }), _jsx(DestButton, { onClick: () => void copyKey(), disabled: busy, children: copied ? "Copied" : "Copy" }), _jsx(DestButton, { onClick: () => void downloadEnv(), disabled: busy, children: "Download .env" }), _jsx(DestButton, { onClick: () => void passwordManagerSave(), disabled: busy, children: "Password manager" }), _jsx(DestButton, { onClick: () => {
|
|
165
166
|
setDmOpen((v) => !v);
|
|
166
167
|
setFlash(null);
|
|
167
|
-
}, disabled: busy, active: dmOpen, children: "Send by DM" })] }), dmOpen && (_jsxs("div", { className: "mt-3 space-y-2 rounded-lg border border-[var(--tb-line)] bg-[var(--tb-surface-2)] p-3", children: [_jsx("p", { className: `text-[12px] leading-relaxed ${muted}`, children: "Encrypts the key to an npub you own and publishes the sealed message to public relays. Only that npub can open it." }), _jsxs("div", { className: "flex flex-wrap items-end gap-2", children: [_jsxs("label", { className: `block min-w-[16rem] flex-1 text-[12px] ${muted}`, children: ["Recipient npub", _jsx("input", { value: recipient, onChange: (e) => setRecipient(e.target.value), spellCheck: false, autoCapitalize: "off", autoCorrect: "off", placeholder: "npub1\u2026", className: `mt-1 h-11 ${input}` })] }), _jsx("button", { type: "button", onClick: () => void sendDm(), disabled: busy || recipient.trim().length < 60, className: "min-h-11 shrink-0 rounded-full border border-[var(--tb-accent)] bg-[var(--tb-accent)] px-4 text-[13px] font-semibold text-[var(--tb-on-accent)] disabled:opacity-40", children: busy ? "Sending…" : "Send encrypted DM" })] })] })), _jsxs("form", { ref: formRef, className: "absolute h-px w-px overflow-hidden opacity-0", "aria-hidden": "true", autoComplete: "on", onSubmit: (e) => {
|
|
168
|
+
}, disabled: busy, active: dmOpen, children: "Send by DM" })] }), showKey && (_jsx("div", { className: "mt-3 break-all rounded-lg border border-[var(--tb-line)] bg-[var(--tb-surface-2)] px-3 py-2 font-mono text-[12px] text-[var(--tb-ink)]", children: getSessionNsec() })), dmOpen && (_jsxs("div", { className: "mt-3 space-y-2 rounded-lg border border-[var(--tb-line)] bg-[var(--tb-surface-2)] p-3", children: [_jsx("p", { className: `text-[12px] leading-relaxed ${muted}`, children: "Encrypts the key to an npub you own and publishes the sealed message to public relays. Only that npub can open it." }), _jsxs("div", { className: "flex flex-wrap items-end gap-2", children: [_jsxs("label", { className: `block min-w-[16rem] flex-1 text-[12px] ${muted}`, children: ["Recipient npub", _jsx("input", { value: recipient, onChange: (e) => setRecipient(e.target.value), spellCheck: false, autoCapitalize: "off", autoCorrect: "off", placeholder: "npub1\u2026", className: `mt-1 h-11 ${input}` })] }), _jsx("button", { type: "button", onClick: () => void sendDm(), disabled: busy || recipient.trim().length < 60, className: "min-h-11 shrink-0 rounded-full border border-[var(--tb-accent)] bg-[var(--tb-accent)] px-4 text-[13px] font-semibold text-[var(--tb-on-accent)] disabled:opacity-40", children: busy ? "Sending…" : "Send encrypted DM" })] })] })), _jsxs("form", { ref: formRef, className: "absolute h-px w-px overflow-hidden opacity-0", "aria-hidden": "true", autoComplete: "on", onSubmit: (e) => {
|
|
168
169
|
e.preventDefault();
|
|
169
170
|
}, children: [_jsx("input", { ref: userRef, name: "username", autoComplete: "username", defaultValue: "", readOnly: true, tabIndex: -1 }), _jsx("input", { ref: passRef, name: "password", type: "password", autoComplete: "new-password", defaultValue: "", readOnly: true, tabIndex: -1 })] }), flash && (_jsx("p", { className: `mt-3 text-[12.5px] leading-relaxed ${flash.tone === "ok" ? "text-[var(--tb-ink)]" : "text-[var(--tb-err-ink)]"}`, role: "status", children: flash.text }))] }));
|
|
170
171
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The room the DebugPanel keeps for itself.
|
|
3
|
+
*
|
|
4
|
+
* The panel is fixed to the bottom of the viewport, so on its own it would sit
|
|
5
|
+
* over whatever the page puts last — on a phone, a loading quote or a Save
|
|
6
|
+
* button. Beside it the panel renders an in-flow spacer as tall as the panel is
|
|
7
|
+
* right now (the collapsed tab, or the open log), plus the iOS home-indicator
|
|
8
|
+
* inset, so the page can always scroll its last line clear of the bar.
|
|
9
|
+
*/
|
|
10
|
+
/** The collapsed tab: one `min-h-10` (2.5rem) button. Used until measured. */
|
|
11
|
+
export declare const DEBUG_BAR_PX = 40;
|
|
12
|
+
/** Whole CSS pixels for a measured height; the collapsed bar when unmeasured. */
|
|
13
|
+
export declare function debugSpacerPx(measured: number | null | undefined): number;
|
|
14
|
+
/** The spacer's CSS height: the panel's height plus the safe-area inset. */
|
|
15
|
+
export declare function debugSpacerHeight(measured: number | null | undefined): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The room the DebugPanel keeps for itself.
|
|
3
|
+
*
|
|
4
|
+
* The panel is fixed to the bottom of the viewport, so on its own it would sit
|
|
5
|
+
* over whatever the page puts last — on a phone, a loading quote or a Save
|
|
6
|
+
* button. Beside it the panel renders an in-flow spacer as tall as the panel is
|
|
7
|
+
* right now (the collapsed tab, or the open log), plus the iOS home-indicator
|
|
8
|
+
* inset, so the page can always scroll its last line clear of the bar.
|
|
9
|
+
*/
|
|
10
|
+
/** The collapsed tab: one `min-h-10` (2.5rem) button. Used until measured. */
|
|
11
|
+
export const DEBUG_BAR_PX = 40;
|
|
12
|
+
/** Whole CSS pixels for a measured height; the collapsed bar when unmeasured. */
|
|
13
|
+
export function debugSpacerPx(measured) {
|
|
14
|
+
if (typeof measured !== "number" || !Number.isFinite(measured) || measured <= 0)
|
|
15
|
+
return DEBUG_BAR_PX;
|
|
16
|
+
return Math.ceil(measured);
|
|
17
|
+
}
|
|
18
|
+
/** The spacer's CSS height: the panel's height plus the safe-area inset. */
|
|
19
|
+
export function debugSpacerHeight(measured) {
|
|
20
|
+
return `calc(${debugSpacerPx(measured)}px + env(safe-area-inset-bottom, 0px))`;
|
|
21
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { default as AvatarPicker } from "./AvatarPicker.tsx";
|
|
|
3
3
|
export { default as DebugPanel, type DebugPanelProps } from "./DebugPanel.tsx";
|
|
4
4
|
export { default as NpubGate } from "./NpubGate.tsx";
|
|
5
5
|
export { default as NostrProfilePanel } from "./NostrProfilePanel.tsx";
|
|
6
|
-
export { default as QuoteScroller, type QuoteScrollerProps } from "./QuoteScroller.tsx";
|
|
6
|
+
export { default as QuoteScroller, type QuoteScrollerProps, type QuoteScrollerClassNames } from "./QuoteScroller.tsx";
|
|
7
7
|
export { default as SessionKeyClaim } from "./SessionKeyClaim.tsx";
|
|
8
8
|
export { default as WalletCard } from "./WalletCard.tsx";
|
|
9
9
|
export { useSession, type Session } from "./useSession.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tollbooth-dpyc/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Shared browser SDK for Tollbooth DPYC operator front ends: npub sign-in, proofs, MCP client, profile and wallet",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Lonnie VanZandt",
|