@allxsmith/bestax-bulma 5.63.0 → 5.65.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/dist/bestax.css +1 -1
- package/dist/bestax.css.map +1 -1
- package/dist/esm/components/Carousel.js +12 -66
- package/dist/esm/components/Carousel.js.map +1 -1
- package/dist/esm/components/Lightbox.js +756 -0
- package/dist/esm/components/Lightbox.js.map +1 -0
- package/dist/esm/components/Reveal.js +1 -24
- package/dist/esm/components/Reveal.js.map +1 -1
- package/dist/esm/components/ScrollArea.js +7 -7
- package/dist/esm/components/ScrollArea.js.map +1 -1
- package/dist/esm/components/TableOfContents.js +85 -0
- package/dist/esm/components/TableOfContents.js.map +1 -0
- package/dist/esm/form/PasswordInput.js +105 -0
- package/dist/esm/form/PasswordInput.js.map +1 -0
- package/dist/esm/form/PasswordInputBase.js +177 -0
- package/dist/esm/form/PasswordInputBase.js.map +1 -0
- package/dist/esm/helpers/chevronIcons.js +24 -0
- package/dist/esm/helpers/chevronIcons.js.map +1 -0
- package/dist/esm/helpers/usePrefersReducedMotion.js +34 -0
- package/dist/esm/helpers/usePrefersReducedMotion.js.map +1 -0
- package/dist/esm/helpers/useScrollSpy.js +286 -0
- package/dist/esm/helpers/useScrollSpy.js.map +1 -0
- package/dist/esm/helpers/useSwipe.js +82 -0
- package/dist/esm/helpers/useSwipe.js.map +1 -0
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +1582 -198
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +5 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/Lightbox.d.ts +244 -0
- package/dist/types/components/TableOfContents.d.ts +87 -0
- package/dist/types/form/PasswordInput.d.ts +98 -0
- package/dist/types/form/PasswordInputBase.d.ts +91 -0
- package/dist/types/helpers/chevronIcons.d.ts +14 -0
- package/dist/types/helpers/usePrefersReducedMotion.d.ts +10 -0
- package/dist/types/helpers/useScrollSpy.d.ts +61 -0
- package/dist/types/helpers/useSwipe.d.ts +66 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types-cjs/components/Lightbox.d.ts +244 -0
- package/dist/types-cjs/components/TableOfContents.d.ts +87 -0
- package/dist/types-cjs/form/PasswordInput.d.ts +98 -0
- package/dist/types-cjs/form/PasswordInputBase.d.ts +91 -0
- package/dist/types-cjs/helpers/chevronIcons.d.ts +14 -0
- package/dist/types-cjs/helpers/usePrefersReducedMotion.d.ts +10 -0
- package/dist/types-cjs/helpers/useScrollSpy.d.ts +61 -0
- package/dist/types-cjs/helpers/useSwipe.d.ts +66 -0
- package/dist/types-cjs/index.d.ts +6 -0
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/CLAUDE.md +3 -0
- package/src/scss/_input-trigger.scss +76 -0
- package/src/scss/components/_index.scss +2 -0
- package/src/scss/components/_lightbox.scss +392 -0
- package/src/scss/components/_tableofcontents.scss +42 -0
- package/src/scss/form/_index.scss +1 -0
- package/src/scss/form/_passwordinput.scss +112 -0
- package/src/scss/form/_picker-popover.scss +24 -63
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
'use client';
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { useLatestRef } from './useLatestRef.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How long the page has to stay still before a jump counts as over: long
|
|
8
|
+
* enough to span the gap between two frames of a smooth scroll.
|
|
9
|
+
*/
|
|
10
|
+
const SETTLE_MS = 100;
|
|
11
|
+
/**
|
|
12
|
+
* A pixel of slack at the line, for a jump that lands a fraction of a pixel
|
|
13
|
+
* away from it.
|
|
14
|
+
*/
|
|
15
|
+
const SLACK = 1;
|
|
16
|
+
/** The page's `scroll-padding-top` in pixels, or 0 for `auto`. */
|
|
17
|
+
function scrollPaddingTop() {
|
|
18
|
+
const root = document.documentElement;
|
|
19
|
+
const value = getComputedStyle(root).scrollPaddingTop;
|
|
20
|
+
const number = parseFloat(value);
|
|
21
|
+
if (!Number.isFinite(number))
|
|
22
|
+
return 0;
|
|
23
|
+
// A percentage is of the page's scrollport: the viewport less a classic
|
|
24
|
+
// scrollbar.
|
|
25
|
+
return value.endsWith('%') ? (number * root.clientHeight) / 100 : number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The height of the viewport the reader sees the page through, less a
|
|
29
|
+
* classic scrollbar: the larger of the root's `clientHeight` and the visual
|
|
30
|
+
* viewport's height. The visual viewport grows when a phone hides its
|
|
31
|
+
* toolbar, where the root keeps the height it had with the toolbar shown;
|
|
32
|
+
* under a pinch zoom it's the smaller, and the root's stands. Its `pageTop`
|
|
33
|
+
* isn't read, since Firefox can leave it behind `scrollY` for a frame after
|
|
34
|
+
* it puts back the scroll position on a reload.
|
|
35
|
+
*/
|
|
36
|
+
function viewportHeight() {
|
|
37
|
+
return Math.max(document.documentElement.clientHeight, window.visualViewport?.height ?? 0);
|
|
38
|
+
}
|
|
39
|
+
/** Whether the page is scrolled as far down as it goes. */
|
|
40
|
+
const atEnd = () => window.scrollY > 0 &&
|
|
41
|
+
window.scrollY + viewportHeight() >=
|
|
42
|
+
document.documentElement.scrollHeight - 1;
|
|
43
|
+
/** Whether the browser draws an element: `display: none` draws no box. */
|
|
44
|
+
const isDrawn = (element) => element.getClientRects().length > 0;
|
|
45
|
+
/** The id a URL fragment names, such as `#caf%C3%A9`. */
|
|
46
|
+
function fragmentId(hash) {
|
|
47
|
+
const raw = hash.slice(1);
|
|
48
|
+
try {
|
|
49
|
+
return decodeURIComponent(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return raw;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether the page was opened afresh, as by a link, rather than reloaded or
|
|
57
|
+
* reached by Back or Forward, where the browser puts it back where it was.
|
|
58
|
+
* Where the navigation isn't recorded (jsdom), it counts as opened afresh.
|
|
59
|
+
*/
|
|
60
|
+
function openedAfresh() {
|
|
61
|
+
const [entry] = performance.getEntriesByType?.('navigation') ?? [];
|
|
62
|
+
const type = entry?.type;
|
|
63
|
+
return type !== 'reload' && type !== 'back_forward';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The link a click was on, if it's a plain click that follows it in this
|
|
67
|
+
* page: the main button, no modifier key, and no `target` sending it
|
|
68
|
+
* elsewhere. The path is read rather than the target, so a link inside a
|
|
69
|
+
* shadow root counts.
|
|
70
|
+
*/
|
|
71
|
+
function followedLink(event) {
|
|
72
|
+
if (event.button !== 0 ||
|
|
73
|
+
event.metaKey ||
|
|
74
|
+
event.ctrlKey ||
|
|
75
|
+
event.shiftKey ||
|
|
76
|
+
event.altKey) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const link = event
|
|
80
|
+
.composedPath()
|
|
81
|
+
.find((node) => node instanceof HTMLAnchorElement);
|
|
82
|
+
return link && (!link.target || link.target === '_self') ? link : undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The `useScrollSpy` hook returns the id of the section being read: the last
|
|
86
|
+
* of `ids` whose top is at or above the offset line, or the last one once the
|
|
87
|
+
* page is scrolled to its end. After a jump to one of them, it's that one
|
|
88
|
+
* until the reader scrolls on. `null` above the first section, on the server
|
|
89
|
+
* and until the page hydrates.
|
|
90
|
+
*
|
|
91
|
+
* It checks once per frame while the page scrolls, and on resize. A jump is
|
|
92
|
+
* a plain click on a link to one of the ids (the main button, no modifier
|
|
93
|
+
* key, opening in this page), noted and never stopped, so the browser jumps
|
|
94
|
+
* as it always does; and the `#id` a page opened afresh opens at. Back,
|
|
95
|
+
* Forward and a reload aren't jumps. That `#id` is read once, when `ids`
|
|
96
|
+
* first has some: a section rendered after that isn't held, even if the
|
|
97
|
+
* page opened at it, and the reading rule marks where the page is. Once the
|
|
98
|
+
* page has been still for a
|
|
99
|
+
* moment, the section stays current only if it's where a jump puts it: its
|
|
100
|
+
* top at the line, give or take a shift of the layout smaller than its own
|
|
101
|
+
* height, or, at the end of the page, on screen below the line. Otherwise,
|
|
102
|
+
* as after a flick right after the click, the reading rule takes over. The
|
|
103
|
+
* next scroll after that lets it go, the browser's own included, so when
|
|
104
|
+
* content loading above it makes the browser scroll to keep the reader's
|
|
105
|
+
* place, the rule takes over too.
|
|
106
|
+
*
|
|
107
|
+
* A section that isn't drawn (`display: none`) is never current. A section
|
|
108
|
+
* that moves while nothing scrolls is seen at the next scroll or resize. It
|
|
109
|
+
* watches the document only: not a panel that scrolls inside it, and not
|
|
110
|
+
* sections inside a shadow root, which an `#id` link can't reach either.
|
|
111
|
+
* Whether a page was opened afresh is read from how the document loaded, so
|
|
112
|
+
* in an app that changes routes on the client, a route change can briefly
|
|
113
|
+
* mark the address bar's section until the page settles, when the reading
|
|
114
|
+
* rule takes over, and after a reload a route change to an `#id` holds
|
|
115
|
+
* nothing.
|
|
116
|
+
*
|
|
117
|
+
* @function
|
|
118
|
+
* @param {UseScrollSpyOptions} options - The ids to watch, the offset, and a change callback.
|
|
119
|
+
* @returns {string | null} The current section's id, or `null`.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* const current = useScrollSpy({ ids: ['profile', 'security', 'billing'] });
|
|
123
|
+
*/
|
|
124
|
+
function useScrollSpy({ ids, offset, onCurrentChange, }) {
|
|
125
|
+
const [active, setActive] = useState(null);
|
|
126
|
+
// What was last reported, kept across a re-subscribe so the same id isn't
|
|
127
|
+
// reported twice.
|
|
128
|
+
const reportedRef = useRef(null);
|
|
129
|
+
// The section a jump went to. It belongs to the hook rather than to one run
|
|
130
|
+
// of the effect, so new ids, a new offset or React's development double run
|
|
131
|
+
// don't lose it, and the `#id` the page opened at is read once, when the
|
|
132
|
+
// ids first include some.
|
|
133
|
+
const heldRef = useRef(null);
|
|
134
|
+
const openedRef = useRef(false);
|
|
135
|
+
const onCurrentChangeRef = useLatestRef(onCurrentChange);
|
|
136
|
+
// The ids as a value, so a new array with the same ids each render doesn't
|
|
137
|
+
// re-subscribe.
|
|
138
|
+
const key = JSON.stringify(ids);
|
|
139
|
+
const fixedOffset = typeof offset === 'number' && Number.isFinite(offset) ? offset : undefined;
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const watched = JSON.parse(key);
|
|
142
|
+
let line = fixedOffset ?? scrollPaddingTop();
|
|
143
|
+
let settled = true;
|
|
144
|
+
let settleTimer;
|
|
145
|
+
let frame = 0;
|
|
146
|
+
const report = (id) => {
|
|
147
|
+
if (id === reportedRef.current)
|
|
148
|
+
return;
|
|
149
|
+
reportedRef.current = id;
|
|
150
|
+
setActive(id);
|
|
151
|
+
onCurrentChangeRef.current?.(id);
|
|
152
|
+
};
|
|
153
|
+
/** The element for one of the ids, if it's on the page and drawn. */
|
|
154
|
+
const sectionOf = (id) => {
|
|
155
|
+
const element = watched.includes(id) ? document.getElementById(id) : null;
|
|
156
|
+
return element && isDrawn(element) ? element : null;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Whether a section is where a jump puts it: its top at the line, give or
|
|
160
|
+
* take a shift of the layout smaller than the section's own height, or,
|
|
161
|
+
* at the end of the page, as near as the page lets it come, on screen
|
|
162
|
+
* below the line.
|
|
163
|
+
*/
|
|
164
|
+
const atJump = (id) => {
|
|
165
|
+
const element = sectionOf(id);
|
|
166
|
+
if (!element)
|
|
167
|
+
return false;
|
|
168
|
+
const { top, bottom, height } = element.getBoundingClientRect();
|
|
169
|
+
if (top <= line + height && bottom >= line)
|
|
170
|
+
return true;
|
|
171
|
+
return top > line && top < viewportHeight() && atEnd();
|
|
172
|
+
};
|
|
173
|
+
const check = () => {
|
|
174
|
+
if (heldRef.current !== null)
|
|
175
|
+
return report(heldRef.current);
|
|
176
|
+
const drawn = [];
|
|
177
|
+
for (const id of watched) {
|
|
178
|
+
const element = document.getElementById(id);
|
|
179
|
+
if (element && isDrawn(element))
|
|
180
|
+
drawn.push([id, element]);
|
|
181
|
+
}
|
|
182
|
+
let current = null;
|
|
183
|
+
if (atEnd()) {
|
|
184
|
+
// A short last section can't reach the line, so the end of the page
|
|
185
|
+
// stands in for it.
|
|
186
|
+
if (drawn.length)
|
|
187
|
+
current = drawn[drawn.length - 1][0];
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
for (const [id, element] of drawn) {
|
|
191
|
+
if (element.getBoundingClientRect().top <= line + SLACK)
|
|
192
|
+
current = id;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
report(current);
|
|
196
|
+
};
|
|
197
|
+
/** Runs the check in the next frame, once however many events ask. */
|
|
198
|
+
const schedule = () => {
|
|
199
|
+
if (frame)
|
|
200
|
+
return;
|
|
201
|
+
frame = requestAnimationFrame(() => {
|
|
202
|
+
frame = 0;
|
|
203
|
+
check();
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Starts, or starts again, the wait for the page to be still, when a
|
|
208
|
+
* held section that isn't where a jump puts it is let go.
|
|
209
|
+
*/
|
|
210
|
+
const waitToSettle = () => {
|
|
211
|
+
settled = false;
|
|
212
|
+
clearTimeout(settleTimer);
|
|
213
|
+
settleTimer = setTimeout(() => {
|
|
214
|
+
settled = true;
|
|
215
|
+
if (heldRef.current !== null && !atJump(heldRef.current)) {
|
|
216
|
+
heldRef.current = null;
|
|
217
|
+
}
|
|
218
|
+
check();
|
|
219
|
+
}, SETTLE_MS);
|
|
220
|
+
};
|
|
221
|
+
/** Holds the section a jump goes to, if it's one of these on the page. */
|
|
222
|
+
const hold = (id) => {
|
|
223
|
+
if (!sectionOf(id))
|
|
224
|
+
return;
|
|
225
|
+
heldRef.current = id;
|
|
226
|
+
waitToSettle();
|
|
227
|
+
};
|
|
228
|
+
const onScroll = () => {
|
|
229
|
+
if (heldRef.current !== null) {
|
|
230
|
+
// Once the page has been still, the next scroll lets the jump go;
|
|
231
|
+
// until then, each scroll pushes the end of the jump back.
|
|
232
|
+
if (settled)
|
|
233
|
+
heldRef.current = null;
|
|
234
|
+
else
|
|
235
|
+
waitToSettle();
|
|
236
|
+
}
|
|
237
|
+
schedule();
|
|
238
|
+
};
|
|
239
|
+
const onResize = () => {
|
|
240
|
+
line = fixedOffset ?? scrollPaddingTop();
|
|
241
|
+
schedule();
|
|
242
|
+
};
|
|
243
|
+
const onClick = (event) => {
|
|
244
|
+
const link = followedLink(event);
|
|
245
|
+
if (link?.hash &&
|
|
246
|
+
link.href.split('#')[0] === window.location.href.split('#')[0]) {
|
|
247
|
+
hold(fragmentId(link.hash));
|
|
248
|
+
check();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
if (!openedRef.current) {
|
|
252
|
+
// Ids built from content fetched after mount arrive later, so the read
|
|
253
|
+
// waits until there are some.
|
|
254
|
+
if (watched.length) {
|
|
255
|
+
openedRef.current = true;
|
|
256
|
+
if (openedAfresh())
|
|
257
|
+
hold(fragmentId(window.location.hash));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else if (heldRef.current !== null) {
|
|
261
|
+
// A jump held across new ids carries on if its section still counts.
|
|
262
|
+
if (sectionOf(heldRef.current))
|
|
263
|
+
waitToSettle();
|
|
264
|
+
else
|
|
265
|
+
heldRef.current = null;
|
|
266
|
+
}
|
|
267
|
+
check();
|
|
268
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
269
|
+
window.addEventListener('resize', onResize);
|
|
270
|
+
document.addEventListener('click', onClick, {
|
|
271
|
+
capture: true,
|
|
272
|
+
passive: true,
|
|
273
|
+
});
|
|
274
|
+
return () => {
|
|
275
|
+
cancelAnimationFrame(frame);
|
|
276
|
+
clearTimeout(settleTimer);
|
|
277
|
+
window.removeEventListener('scroll', onScroll);
|
|
278
|
+
window.removeEventListener('resize', onResize);
|
|
279
|
+
document.removeEventListener('click', onClick, { capture: true });
|
|
280
|
+
};
|
|
281
|
+
}, [key, fixedOffset, onCurrentChangeRef]);
|
|
282
|
+
return active;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export { useScrollSpy };
|
|
286
|
+
//# sourceMappingURL=useScrollSpy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useScrollSpy.js","sources":["../../../src/helpers/useScrollSpy.ts"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport { useLatestRef } from './useLatestRef';\n\n/** Options for useScrollSpy. */\nexport interface UseScrollSpyOptions {\n /**\n * **Required.** The ids of the sections to watch, without the `#`, in the\n * order they come on the page. Each one is looked up each time the hook\n * checks, so an id with no element on the page is skipped until one\n * appears, and a section replaced under the same id keeps working.\n */\n ids: string[];\n /**\n * Pixels from the viewport's top where a section counts as reached. Unset,\n * it's the page's `scroll-padding-top`, read when the hook mounts and on\n * resize, so the highlight agrees with where a jump lands under a sticky\n * header. A `scroll-padding-top` that mixes a percentage with a length in\n * `calc()` isn't read: pass `offset` then.\n */\n offset?: number;\n /** Called with the new current id when it changes, or `null` above the first section. */\n onCurrentChange?: (id: string | null) => void;\n}\n\n/**\n * How long the page has to stay still before a jump counts as over: long\n * enough to span the gap between two frames of a smooth scroll.\n */\nconst SETTLE_MS = 100;\n\n/**\n * A pixel of slack at the line, for a jump that lands a fraction of a pixel\n * away from it.\n */\nconst SLACK = 1;\n\n/** The page's `scroll-padding-top` in pixels, or 0 for `auto`. */\nfunction scrollPaddingTop(): number {\n const root = document.documentElement;\n const value = getComputedStyle(root).scrollPaddingTop;\n const number = parseFloat(value);\n if (!Number.isFinite(number)) return 0;\n // A percentage is of the page's scrollport: the viewport less a classic\n // scrollbar.\n return value.endsWith('%') ? (number * root.clientHeight) / 100 : number;\n}\n\n/**\n * The height of the viewport the reader sees the page through, less a\n * classic scrollbar: the larger of the root's `clientHeight` and the visual\n * viewport's height. The visual viewport grows when a phone hides its\n * toolbar, where the root keeps the height it had with the toolbar shown;\n * under a pinch zoom it's the smaller, and the root's stands. Its `pageTop`\n * isn't read, since Firefox can leave it behind `scrollY` for a frame after\n * it puts back the scroll position on a reload.\n */\nfunction viewportHeight(): number {\n return Math.max(\n document.documentElement.clientHeight,\n window.visualViewport?.height ?? 0\n );\n}\n\n/** Whether the page is scrolled as far down as it goes. */\nconst atEnd = () =>\n window.scrollY > 0 &&\n window.scrollY + viewportHeight() >=\n document.documentElement.scrollHeight - 1;\n\n/** Whether the browser draws an element: `display: none` draws no box. */\nconst isDrawn = (element: Element) => element.getClientRects().length > 0;\n\n/** The id a URL fragment names, such as `#caf%C3%A9`. */\nfunction fragmentId(hash: string): string {\n const raw = hash.slice(1);\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Whether the page was opened afresh, as by a link, rather than reloaded or\n * reached by Back or Forward, where the browser puts it back where it was.\n * Where the navigation isn't recorded (jsdom), it counts as opened afresh.\n */\nfunction openedAfresh(): boolean {\n const [entry] = performance.getEntriesByType?.('navigation') ?? [];\n const type = (entry as PerformanceNavigationTiming | undefined)?.type;\n return type !== 'reload' && type !== 'back_forward';\n}\n\n/**\n * The link a click was on, if it's a plain click that follows it in this\n * page: the main button, no modifier key, and no `target` sending it\n * elsewhere. The path is read rather than the target, so a link inside a\n * shadow root counts.\n */\nfunction followedLink(event: MouseEvent): HTMLAnchorElement | undefined {\n if (\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n ) {\n return undefined;\n }\n const link = event\n .composedPath()\n .find(\n (node): node is HTMLAnchorElement => node instanceof HTMLAnchorElement\n );\n return link && (!link.target || link.target === '_self') ? link : undefined;\n}\n\n/**\n * The `useScrollSpy` hook returns the id of the section being read: the last\n * of `ids` whose top is at or above the offset line, or the last one once the\n * page is scrolled to its end. After a jump to one of them, it's that one\n * until the reader scrolls on. `null` above the first section, on the server\n * and until the page hydrates.\n *\n * It checks once per frame while the page scrolls, and on resize. A jump is\n * a plain click on a link to one of the ids (the main button, no modifier\n * key, opening in this page), noted and never stopped, so the browser jumps\n * as it always does; and the `#id` a page opened afresh opens at. Back,\n * Forward and a reload aren't jumps. That `#id` is read once, when `ids`\n * first has some: a section rendered after that isn't held, even if the\n * page opened at it, and the reading rule marks where the page is. Once the\n * page has been still for a\n * moment, the section stays current only if it's where a jump puts it: its\n * top at the line, give or take a shift of the layout smaller than its own\n * height, or, at the end of the page, on screen below the line. Otherwise,\n * as after a flick right after the click, the reading rule takes over. The\n * next scroll after that lets it go, the browser's own included, so when\n * content loading above it makes the browser scroll to keep the reader's\n * place, the rule takes over too.\n *\n * A section that isn't drawn (`display: none`) is never current. A section\n * that moves while nothing scrolls is seen at the next scroll or resize. It\n * watches the document only: not a panel that scrolls inside it, and not\n * sections inside a shadow root, which an `#id` link can't reach either.\n * Whether a page was opened afresh is read from how the document loaded, so\n * in an app that changes routes on the client, a route change can briefly\n * mark the address bar's section until the page settles, when the reading\n * rule takes over, and after a reload a route change to an `#id` holds\n * nothing.\n *\n * @function\n * @param {UseScrollSpyOptions} options - The ids to watch, the offset, and a change callback.\n * @returns {string | null} The current section's id, or `null`.\n *\n * @example\n * const current = useScrollSpy({ ids: ['profile', 'security', 'billing'] });\n */\nexport function useScrollSpy({\n ids,\n offset,\n onCurrentChange,\n}: UseScrollSpyOptions): string | null {\n const [active, setActive] = useState<string | null>(null);\n // What was last reported, kept across a re-subscribe so the same id isn't\n // reported twice.\n const reportedRef = useRef<string | null>(null);\n // The section a jump went to. It belongs to the hook rather than to one run\n // of the effect, so new ids, a new offset or React's development double run\n // don't lose it, and the `#id` the page opened at is read once, when the\n // ids first include some.\n const heldRef = useRef<string | null>(null);\n const openedRef = useRef(false);\n const onCurrentChangeRef = useLatestRef(onCurrentChange);\n // The ids as a value, so a new array with the same ids each render doesn't\n // re-subscribe.\n const key = JSON.stringify(ids);\n const fixedOffset =\n typeof offset === 'number' && Number.isFinite(offset) ? offset : undefined;\n\n useEffect(() => {\n const watched = JSON.parse(key) as string[];\n let line = fixedOffset ?? scrollPaddingTop();\n let settled = true;\n let settleTimer: ReturnType<typeof setTimeout> | undefined;\n let frame = 0;\n\n const report = (id: string | null) => {\n if (id === reportedRef.current) return;\n reportedRef.current = id;\n setActive(id);\n onCurrentChangeRef.current?.(id);\n };\n\n /** The element for one of the ids, if it's on the page and drawn. */\n const sectionOf = (id: string) => {\n const element = watched.includes(id) ? document.getElementById(id) : null;\n return element && isDrawn(element) ? element : null;\n };\n\n /**\n * Whether a section is where a jump puts it: its top at the line, give or\n * take a shift of the layout smaller than the section's own height, or,\n * at the end of the page, as near as the page lets it come, on screen\n * below the line.\n */\n const atJump = (id: string) => {\n const element = sectionOf(id);\n if (!element) return false;\n const { top, bottom, height } = element.getBoundingClientRect();\n if (top <= line + height && bottom >= line) return true;\n return top > line && top < viewportHeight() && atEnd();\n };\n\n const check = () => {\n if (heldRef.current !== null) return report(heldRef.current);\n const drawn: [string, HTMLElement][] = [];\n for (const id of watched) {\n const element = document.getElementById(id);\n if (element && isDrawn(element)) drawn.push([id, element]);\n }\n let current: string | null = null;\n if (atEnd()) {\n // A short last section can't reach the line, so the end of the page\n // stands in for it.\n if (drawn.length) current = drawn[drawn.length - 1][0];\n } else {\n for (const [id, element] of drawn) {\n if (element.getBoundingClientRect().top <= line + SLACK) current = id;\n }\n }\n report(current);\n };\n\n /** Runs the check in the next frame, once however many events ask. */\n const schedule = () => {\n if (frame) return;\n frame = requestAnimationFrame(() => {\n frame = 0;\n check();\n });\n };\n\n /**\n * Starts, or starts again, the wait for the page to be still, when a\n * held section that isn't where a jump puts it is let go.\n */\n const waitToSettle = () => {\n settled = false;\n clearTimeout(settleTimer);\n settleTimer = setTimeout(() => {\n settled = true;\n if (heldRef.current !== null && !atJump(heldRef.current)) {\n heldRef.current = null;\n }\n check();\n }, SETTLE_MS);\n };\n\n /** Holds the section a jump goes to, if it's one of these on the page. */\n const hold = (id: string) => {\n if (!sectionOf(id)) return;\n heldRef.current = id;\n waitToSettle();\n };\n\n const onScroll = () => {\n if (heldRef.current !== null) {\n // Once the page has been still, the next scroll lets the jump go;\n // until then, each scroll pushes the end of the jump back.\n if (settled) heldRef.current = null;\n else waitToSettle();\n }\n schedule();\n };\n\n const onResize = () => {\n line = fixedOffset ?? scrollPaddingTop();\n schedule();\n };\n\n const onClick = (event: MouseEvent) => {\n const link = followedLink(event);\n if (\n link?.hash &&\n link.href.split('#')[0] === window.location.href.split('#')[0]\n ) {\n hold(fragmentId(link.hash));\n check();\n }\n };\n\n if (!openedRef.current) {\n // Ids built from content fetched after mount arrive later, so the read\n // waits until there are some.\n if (watched.length) {\n openedRef.current = true;\n if (openedAfresh()) hold(fragmentId(window.location.hash));\n }\n } else if (heldRef.current !== null) {\n // A jump held across new ids carries on if its section still counts.\n if (sectionOf(heldRef.current)) waitToSettle();\n else heldRef.current = null;\n }\n check();\n window.addEventListener('scroll', onScroll, { passive: true });\n window.addEventListener('resize', onResize);\n document.addEventListener('click', onClick, {\n capture: true,\n passive: true,\n });\n return () => {\n cancelAnimationFrame(frame);\n clearTimeout(settleTimer);\n window.removeEventListener('scroll', onScroll);\n window.removeEventListener('resize', onResize);\n document.removeEventListener('click', onClick, { capture: true });\n };\n }, [key, fixedOffset, onCurrentChangeRef]);\n\n return active;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
'use client';
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
6
|
+
/** How far, in CSS pixels, a drag has to travel along an axis to count. */
|
|
7
|
+
const SWIPE_THRESHOLD = 50;
|
|
8
|
+
const AT_REST = { x: 0, y: 0 };
|
|
9
|
+
/**
|
|
10
|
+
* Follows a mouse or touch drag on a surface and reports a swipe when it
|
|
11
|
+
* ends far enough from where it started. Carousel's drag, shared: it hands
|
|
12
|
+
* back the handlers to put on the surface and the live offset to draw the
|
|
13
|
+
* drag with, and leaves what a swipe does to the component.
|
|
14
|
+
*
|
|
15
|
+
* @function
|
|
16
|
+
* @param {UseSwipeOptions} options - Whether it's on, the axes, and the callback.
|
|
17
|
+
* @returns {UseSwipeResult} The drag's state, its handlers, and a way to drop it.
|
|
18
|
+
*/
|
|
19
|
+
function useSwipe({ enabled = true, vertical = false, onSwipe, }) {
|
|
20
|
+
const [isSwiping, setIsSwiping] = useState(false);
|
|
21
|
+
const [start, setStart] = useState(AT_REST);
|
|
22
|
+
const [offset, setOffset] = useState(AT_REST);
|
|
23
|
+
const begin = useCallback((x, y) => {
|
|
24
|
+
if (!enabled)
|
|
25
|
+
return;
|
|
26
|
+
setIsSwiping(true);
|
|
27
|
+
setStart({ x, y: vertical ? y : 0 });
|
|
28
|
+
setOffset(AT_REST);
|
|
29
|
+
}, [enabled, vertical]);
|
|
30
|
+
const follow = useCallback((x, y) => {
|
|
31
|
+
if (!isSwiping)
|
|
32
|
+
return;
|
|
33
|
+
setOffset({ x: x - start.x, y: vertical ? y - start.y : 0 });
|
|
34
|
+
}, [isSwiping, start, vertical]);
|
|
35
|
+
const end = useCallback(() => {
|
|
36
|
+
if (!isSwiping)
|
|
37
|
+
return;
|
|
38
|
+
setIsSwiping(false);
|
|
39
|
+
if (vertical && Math.abs(offset.y) > Math.abs(offset.x)) {
|
|
40
|
+
if (offset.y > SWIPE_THRESHOLD)
|
|
41
|
+
onSwipe('down');
|
|
42
|
+
else if (offset.y < -SWIPE_THRESHOLD)
|
|
43
|
+
onSwipe('up');
|
|
44
|
+
}
|
|
45
|
+
else if (offset.x < -SWIPE_THRESHOLD) {
|
|
46
|
+
onSwipe('left');
|
|
47
|
+
}
|
|
48
|
+
else if (offset.x > SWIPE_THRESHOLD) {
|
|
49
|
+
onSwipe('right');
|
|
50
|
+
}
|
|
51
|
+
setOffset(AT_REST);
|
|
52
|
+
}, [isSwiping, vertical, offset, onSwipe]);
|
|
53
|
+
const cancel = useCallback(() => {
|
|
54
|
+
setIsSwiping(false);
|
|
55
|
+
setOffset(AT_REST);
|
|
56
|
+
}, []);
|
|
57
|
+
return {
|
|
58
|
+
isSwiping,
|
|
59
|
+
offsetX: offset.x,
|
|
60
|
+
offsetY: offset.y,
|
|
61
|
+
handlers: {
|
|
62
|
+
onMouseDown: event => {
|
|
63
|
+
// Disabled, the press is left as it is: a click still focuses and
|
|
64
|
+
// text can still be selected.
|
|
65
|
+
if (!enabled)
|
|
66
|
+
return;
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
begin(event.clientX, event.clientY);
|
|
69
|
+
},
|
|
70
|
+
onMouseMove: event => follow(event.clientX, event.clientY),
|
|
71
|
+
onMouseUp: end,
|
|
72
|
+
onMouseLeave: end,
|
|
73
|
+
onTouchStart: event => begin(event.touches[0].clientX, event.touches[0].clientY),
|
|
74
|
+
onTouchMove: event => follow(event.touches[0].clientX, event.touches[0].clientY),
|
|
75
|
+
onTouchEnd: end,
|
|
76
|
+
},
|
|
77
|
+
cancel,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { useSwipe };
|
|
82
|
+
//# sourceMappingURL=useSwipe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSwipe.js","sources":["../../../src/helpers/useSwipe.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useCallback, useState } from 'react';\nimport type React from 'react';\n\n/** Which way a finished swipe went, on screen: where the pointer travelled. */\nexport type SwipeDirection = 'left' | 'right' | 'up' | 'down';\n\n/** How far, in CSS pixels, a drag has to travel along an axis to count. */\nconst SWIPE_THRESHOLD = 50;\n\n/** Options for `useSwipe`. */\nexport interface UseSwipeOptions {\n /**\n * Whether a press starts a drag. Off, the handlers leave a press alone,\n * `preventDefault()` included. A drag already under way still finishes.\n * @defaultValue true\n */\n enabled?: boolean;\n /**\n * Follow the vertical axis too, so a swipe up or down is reported. Off,\n * only the horizontal distance counts and `offsetY` stays `0`.\n * @defaultValue false\n */\n vertical?: boolean;\n /**\n * Called when a drag ends more than 50px from where it started along an\n * axis. With `vertical`, the axis it travelled further along decides.\n * The direction is physical, so a caller that follows the reading\n * direction maps it itself.\n */\n onSwipe: (direction: SwipeDirection) => void;\n}\n\n/** The mouse and touch handlers `useSwipe` hands back, to spread on the surface. */\nexport interface SwipeHandlers<T extends Element = Element> {\n /** Starts a drag. Calls `preventDefault()` while enabled, so the press neither selects text nor drags an image. */\n onMouseDown: (event: React.MouseEvent<T>) => void;\n /** Follows a drag. */\n onMouseMove: (event: React.MouseEvent<T>) => void;\n /** Ends a drag. */\n onMouseUp: () => void;\n /** Ends a drag when the pointer leaves the surface, as a release would. */\n onMouseLeave: () => void;\n /** Starts a drag with the first finger. */\n onTouchStart: (event: React.TouchEvent<T>) => void;\n /** Follows the first finger. */\n onTouchMove: (event: React.TouchEvent<T>) => void;\n /** Ends a drag. */\n onTouchEnd: () => void;\n}\n\n/** What `useSwipe` returns. */\nexport interface UseSwipeResult<T extends Element = Element> {\n /** Whether a drag is under way. */\n isSwiping: boolean;\n /** How far the drag has gone across, in CSS pixels, `0` between drags. */\n offsetX: number;\n /** How far the drag has gone down (up is negative), `0` without `vertical`. */\n offsetY: number;\n /** The handlers to put on the surface that takes the drag. */\n handlers: SwipeHandlers<T>;\n /** Drops the drag under way without reporting a swipe, as when a second finger lands. */\n cancel: () => void;\n}\n\nconst AT_REST = { x: 0, y: 0 };\n\n/**\n * Follows a mouse or touch drag on a surface and reports a swipe when it\n * ends far enough from where it started. Carousel's drag, shared: it hands\n * back the handlers to put on the surface and the live offset to draw the\n * drag with, and leaves what a swipe does to the component.\n *\n * @function\n * @param {UseSwipeOptions} options - Whether it's on, the axes, and the callback.\n * @returns {UseSwipeResult} The drag's state, its handlers, and a way to drop it.\n */\nexport function useSwipe<T extends Element = Element>({\n enabled = true,\n vertical = false,\n onSwipe,\n}: UseSwipeOptions): UseSwipeResult<T> {\n const [isSwiping, setIsSwiping] = useState(false);\n const [start, setStart] = useState(AT_REST);\n const [offset, setOffset] = useState(AT_REST);\n\n const begin = useCallback(\n (x: number, y: number) => {\n if (!enabled) return;\n setIsSwiping(true);\n setStart({ x, y: vertical ? y : 0 });\n setOffset(AT_REST);\n },\n [enabled, vertical]\n );\n\n const follow = useCallback(\n (x: number, y: number) => {\n if (!isSwiping) return;\n setOffset({ x: x - start.x, y: vertical ? y - start.y : 0 });\n },\n [isSwiping, start, vertical]\n );\n\n const end = useCallback(() => {\n if (!isSwiping) return;\n setIsSwiping(false);\n\n if (vertical && Math.abs(offset.y) > Math.abs(offset.x)) {\n if (offset.y > SWIPE_THRESHOLD) onSwipe('down');\n else if (offset.y < -SWIPE_THRESHOLD) onSwipe('up');\n } else if (offset.x < -SWIPE_THRESHOLD) {\n onSwipe('left');\n } else if (offset.x > SWIPE_THRESHOLD) {\n onSwipe('right');\n }\n\n setOffset(AT_REST);\n }, [isSwiping, vertical, offset, onSwipe]);\n\n const cancel = useCallback(() => {\n setIsSwiping(false);\n setOffset(AT_REST);\n }, []);\n\n return {\n isSwiping,\n offsetX: offset.x,\n offsetY: offset.y,\n handlers: {\n onMouseDown: event => {\n // Disabled, the press is left as it is: a click still focuses and\n // text can still be selected.\n if (!enabled) return;\n event.preventDefault();\n begin(event.clientX, event.clientY);\n },\n onMouseMove: event => follow(event.clientX, event.clientY),\n onMouseUp: end,\n onMouseLeave: end,\n onTouchStart: event =>\n begin(event.touches[0].clientX, event.touches[0].clientY),\n onTouchMove: event =>\n follow(event.touches[0].clientX, event.touches[0].clientY),\n onTouchEnd: end,\n },\n cancel,\n };\n}\n"],"names":[],"mappings":"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"}
|