@tollbooth-dpyc/web 0.5.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 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 near the root.
48
- Anything the site alone needs goes in as children, drawn above the log:
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";
@@ -115,6 +120,14 @@ them after:
115
120
  :root { --tb-accent: #4cc38a; }
116
121
  ```
117
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
+
118
131
  ## Develop
119
132
 
120
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 {
@@ -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
- const sev = debugSeverity(entry);
78
- const flag = sev === "ok" ? null : FLAGGED[sev];
79
- 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));
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
  }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tollbooth-dpyc/web",
3
- "version": "0.5.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",