@tollbooth-dpyc/web 1.3.0 → 1.3.1
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.
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* the query (server-side, or `filterRows` in the browser), so paging reflects
|
|
11
11
|
* the filtered set. The search is applied on Enter or its chip, never on each
|
|
12
12
|
* keystroke. Mechanics only: every visual choice is the site's, through
|
|
13
|
-
* `classNames`; the actions are chips. The panel is kept on screen:
|
|
14
|
-
* it is slid sideways (`
|
|
15
|
-
* of a phone.
|
|
13
|
+
* `classNames`; the actions are chips. The panel is kept on screen: while open,
|
|
14
|
+
* it is slid sideways (`reclampPanel`) so no part of it hangs past either edge
|
|
15
|
+
* of a phone, however its mark grows or the window changes.
|
|
16
16
|
*/
|
|
17
17
|
import { type ReactNode } from "react";
|
|
18
18
|
export interface DateFieldOption {
|
|
@@ -11,13 +11,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
* the query (server-side, or `filterRows` in the browser), so paging reflects
|
|
12
12
|
* the filtered set. The search is applied on Enter or its chip, never on each
|
|
13
13
|
* keystroke. Mechanics only: every visual choice is the site's, through
|
|
14
|
-
* `classNames`; the actions are chips. The panel is kept on screen:
|
|
15
|
-
* it is slid sideways (`
|
|
16
|
-
* of a phone.
|
|
14
|
+
* `classNames`; the actions are chips. The panel is kept on screen: while open,
|
|
15
|
+
* it is slid sideways (`reclampPanel`) so no part of it hangs past either edge
|
|
16
|
+
* of a phone, however its mark grows or the window changes.
|
|
17
17
|
*/
|
|
18
18
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
19
19
|
import { Search, SlidersHorizontal, X } from "lucide-react";
|
|
20
|
-
import { filterActive
|
|
20
|
+
import { filterActive } from "../table.js";
|
|
21
|
+
import { oncePerFrame, reclampPanel } from "./panelPlacement.js";
|
|
21
22
|
/** The panel keeps this many px from each edge of the viewport. */
|
|
22
23
|
const EDGE = 8;
|
|
23
24
|
function cx(...parts) {
|
|
@@ -47,26 +48,46 @@ function QuestionPanel({ q, clear, c, }) {
|
|
|
47
48
|
const [open, setOpen] = useState(false);
|
|
48
49
|
const box = useRef(null);
|
|
49
50
|
const panel = useRef(null);
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
51
|
+
const mark = useRef(null);
|
|
52
|
+
// Kept on screen, and kept there while open: the mark widens as questions
|
|
53
|
+
// are answered, the Clear chip joins the panel, the window turns or scrolls —
|
|
54
|
+
// each moves the panel, so each re-places it (at most once a frame). Slid by
|
|
55
|
+
// the `translate` property (a site's own `transform` is left alone); only a
|
|
56
|
+
// panel wider than the viewport gets a max-width.
|
|
53
57
|
useLayoutEffect(() => {
|
|
54
58
|
const el = panel.current;
|
|
55
59
|
if (!open || !el)
|
|
56
60
|
return;
|
|
61
|
+
let dx = 0;
|
|
57
62
|
const place = () => {
|
|
58
|
-
el.style.translate = "";
|
|
59
|
-
el.style.maxWidth = "";
|
|
60
63
|
const room = document.documentElement.clientWidth;
|
|
64
|
+
el.style.maxWidth = "";
|
|
61
65
|
if (el.getBoundingClientRect().width > room - 2 * EDGE)
|
|
62
66
|
el.style.maxWidth = `${room - 2 * EDGE}px`;
|
|
63
67
|
const r = el.getBoundingClientRect();
|
|
64
|
-
const
|
|
65
|
-
|
|
68
|
+
const next = reclampPanel(dx, r.left, r.width, room, EDGE);
|
|
69
|
+
if (next !== dx) {
|
|
70
|
+
dx = next;
|
|
71
|
+
el.style.translate = dx ? `${dx}px 0` : "";
|
|
72
|
+
}
|
|
66
73
|
};
|
|
67
74
|
place();
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
const frame = oncePerFrame(place);
|
|
76
|
+
const sized = new ResizeObserver(frame.schedule);
|
|
77
|
+
sized.observe(el);
|
|
78
|
+
for (const around of [mark.current, box.current])
|
|
79
|
+
if (around)
|
|
80
|
+
sized.observe(around);
|
|
81
|
+
window.addEventListener("resize", frame.schedule);
|
|
82
|
+
window.addEventListener("orientationchange", frame.schedule);
|
|
83
|
+
window.addEventListener("scroll", frame.schedule, { capture: true, passive: true });
|
|
84
|
+
return () => {
|
|
85
|
+
frame.cancel();
|
|
86
|
+
sized.disconnect();
|
|
87
|
+
window.removeEventListener("resize", frame.schedule);
|
|
88
|
+
window.removeEventListener("orientationchange", frame.schedule);
|
|
89
|
+
window.removeEventListener("scroll", frame.schedule, { capture: true });
|
|
90
|
+
};
|
|
70
91
|
}, [open]);
|
|
71
92
|
// Dismissed by a tap outside or Escape.
|
|
72
93
|
useEffect(() => {
|
|
@@ -89,7 +110,7 @@ function QuestionPanel({ q, clear, c, }) {
|
|
|
89
110
|
}, [open]);
|
|
90
111
|
const on = q.summary !== "";
|
|
91
112
|
const set = (key, v) => q.onChange({ ...q.value, [key]: v });
|
|
92
|
-
return (_jsxs("div", { className: c.questions, ref: box, children: [_jsxs("button", { type: "button", onClick: () => setOpen((o) => !o), "aria-expanded": open, title: on ? `Filtered: ${q.summary}` : "Filter", "aria-label": on ? `Filtered: ${q.summary}` : "Filter", className: cx(c.chip, on && c.chipActive), children: [q.icon ?? _jsx(SlidersHorizontal, { size: "1em", "aria-hidden": true }), on && _jsx("span", { className: c.summary, children: q.summary })] }), open && (_jsxs("div", { className: c.panel, ref: panel, role: "group", "aria-label": "Filter", children: [q.questions.map((question) => {
|
|
113
|
+
return (_jsxs("div", { className: c.questions, ref: box, children: [_jsxs("button", { ref: mark, type: "button", onClick: () => setOpen((o) => !o), "aria-expanded": open, title: on ? `Filtered: ${q.summary}` : "Filter", "aria-label": on ? `Filtered: ${q.summary}` : "Filter", className: cx(c.chip, on && c.chipActive), children: [q.icon ?? _jsx(SlidersHorizontal, { size: "1em", "aria-hidden": true }), on && _jsx("span", { className: c.summary, children: q.summary })] }), open && (_jsxs("div", { className: c.panel, ref: panel, role: "group", "aria-label": "Filter", children: [q.questions.map((question) => {
|
|
93
114
|
const id = `tf-${String(question.key)}`;
|
|
94
115
|
return question.kind === "toggle" ? (_jsxs("label", { className: c.question, children: [_jsx("input", { type: "checkbox", checked: !!q.value[question.key], onChange: (e) => set(question.key, e.target.checked), className: c.checkbox }), " ", question.label] }, id)) : (_jsxs("div", { className: c.question, children: [_jsx("label", { htmlFor: id, children: question.label }), " ", _jsx("input", { id: id, inputMode: "numeric", value: String(q.value[question.key] ?? ""), onChange: (e) => set(question.key, e.target.value), className: c.number }), " ", _jsx("span", { className: c.unit, children: question.unit })] }, id));
|
|
95
116
|
}), clear] }))] }));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping TableFilter's questions panel on screen while it is open.
|
|
3
|
+
*
|
|
4
|
+
* The panel hangs from the filter mark, so anything that moves the mark moves
|
|
5
|
+
* the panel: ticking a question widens the mark (its summary), the Clear chip
|
|
6
|
+
* appears inside the panel, the window turns or scrolls. So the panel is
|
|
7
|
+
* re-placed on every one of those, not only when it opens.
|
|
8
|
+
*
|
|
9
|
+
* It is slid by the CSS `translate` property, which `getBoundingClientRect`
|
|
10
|
+
* already includes. Taking the slide in force back off gives where the site's
|
|
11
|
+
* CSS alone puts the panel, and the new slide is worked out from there — no
|
|
12
|
+
* need to clear the slide and force a second layout just to measure.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The slide (px) that keeps the panel `margin` px inside both edges.
|
|
16
|
+
* `measuredLeft` is where it is drawn now, with `currentDx` already applied.
|
|
17
|
+
*/
|
|
18
|
+
export declare function reclampPanel(currentDx: number, measuredLeft: number, width: number, viewportWidth: number, margin?: number): number;
|
|
19
|
+
/**
|
|
20
|
+
* `run` at most once per animation frame, however many times the returned
|
|
21
|
+
* function is called before that frame. `cancel` drops a pending run.
|
|
22
|
+
*/
|
|
23
|
+
export declare function oncePerFrame(run: () => void, request?: (cb: () => void) => number, release?: (id: number) => void): {
|
|
24
|
+
schedule: () => void;
|
|
25
|
+
cancel: () => void;
|
|
26
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping TableFilter's questions panel on screen while it is open.
|
|
3
|
+
*
|
|
4
|
+
* The panel hangs from the filter mark, so anything that moves the mark moves
|
|
5
|
+
* the panel: ticking a question widens the mark (its summary), the Clear chip
|
|
6
|
+
* appears inside the panel, the window turns or scrolls. So the panel is
|
|
7
|
+
* re-placed on every one of those, not only when it opens.
|
|
8
|
+
*
|
|
9
|
+
* It is slid by the CSS `translate` property, which `getBoundingClientRect`
|
|
10
|
+
* already includes. Taking the slide in force back off gives where the site's
|
|
11
|
+
* CSS alone puts the panel, and the new slide is worked out from there — no
|
|
12
|
+
* need to clear the slide and force a second layout just to measure.
|
|
13
|
+
*/
|
|
14
|
+
import { nudgeIntoView } from "../table.js";
|
|
15
|
+
/**
|
|
16
|
+
* The slide (px) that keeps the panel `margin` px inside both edges.
|
|
17
|
+
* `measuredLeft` is where it is drawn now, with `currentDx` already applied.
|
|
18
|
+
*/
|
|
19
|
+
export function reclampPanel(currentDx, measuredLeft, width, viewportWidth, margin = 8) {
|
|
20
|
+
return nudgeIntoView(measuredLeft - currentDx, width, viewportWidth, margin);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `run` at most once per animation frame, however many times the returned
|
|
24
|
+
* function is called before that frame. `cancel` drops a pending run.
|
|
25
|
+
*/
|
|
26
|
+
export function oncePerFrame(run, request = (cb) => requestAnimationFrame(cb), release = (id) => cancelAnimationFrame(id)) {
|
|
27
|
+
let pending = null;
|
|
28
|
+
return {
|
|
29
|
+
schedule() {
|
|
30
|
+
if (pending !== null)
|
|
31
|
+
return;
|
|
32
|
+
pending = request(() => {
|
|
33
|
+
pending = null;
|
|
34
|
+
run();
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
cancel() {
|
|
38
|
+
if (pending === null)
|
|
39
|
+
return;
|
|
40
|
+
release(pending);
|
|
41
|
+
pending = null;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tollbooth-dpyc/web",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
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",
|