@design-edito/tools 0.5.6 → 0.5.8
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/agnostic/arrays/index.d.ts +2 -2
- package/agnostic/arrays/index.js +2 -2
- package/agnostic/colors/index.d.ts +2 -2
- package/agnostic/colors/index.js +2 -2
- package/agnostic/css/index.d.ts +1 -1
- package/agnostic/css/index.js +1 -1
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +10 -10
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +10 -10
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +2 -2
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +2 -2
- package/agnostic/html/index.d.ts +1 -1
- package/agnostic/html/index.js +1 -1
- package/agnostic/index.d.ts +5 -4
- package/agnostic/index.js +5 -4
- package/agnostic/misc/index.d.ts +3 -3
- package/agnostic/misc/index.js +3 -3
- package/agnostic/numbers/index.d.ts +1 -1
- package/agnostic/numbers/index.js +1 -1
- package/agnostic/objects/index.d.ts +2 -2
- package/agnostic/objects/index.js +2 -2
- package/agnostic/strings/index.d.ts +1 -1
- package/agnostic/strings/index.js +1 -1
- package/agnostic/subtitles/index.d.ts +2 -0
- package/agnostic/subtitles/index.js +2 -0
- package/agnostic/subtitles/parse-srt/index.d.ts +17 -0
- package/agnostic/subtitles/parse-srt/index.js +67 -0
- package/agnostic/subtitles/parse-srt/index.test.d.ts +1 -0
- package/agnostic/subtitles/parse-srt/index.test.js +94 -0
- package/agnostic/subtitles/parse-srt/types.d.ts +22 -0
- package/agnostic/subtitles/parse-srt/types.js +1 -0
- package/agnostic/subtitles/srt-timecode-to-ms/index.d.ts +14 -0
- package/agnostic/subtitles/srt-timecode-to-ms/index.js +36 -0
- package/agnostic/subtitles/srt-timecode-to-ms/index.test.d.ts +1 -0
- package/agnostic/subtitles/srt-timecode-to-ms/index.test.js +44 -0
- package/components/BeforeAfter/index.controlled.d.ts +5 -4
- package/components/BeforeAfter/index.controlled.js +6 -6
- package/components/Drawer/index.d.ts +3 -1
- package/components/Drawer/index.js +5 -2
- package/components/Iframe/index.d.ts +4 -0
- package/components/Iframe/index.js +8 -1
- package/components/Input/index.d.ts +4 -0
- package/components/Input/index.js +9 -1
- package/components/ListLoader/index.controlled.d.ts +11 -3
- package/components/ListLoader/index.controlled.js +6 -3
- package/components/ListLoader/index.d.ts +1 -1
- package/components/ListLoader/index.js +2 -2
- package/components/ResizeObserver/index.d.ts +6 -1
- package/components/ResizeObserver/index.js +7 -2
- package/components/Scrllgngn/index.d.ts +37 -7
- package/components/Scrllgngn/index.js +81 -24
- package/components/Scrllgngn/styles.module.css +30 -5
- package/components/Scrllgngn/utils.d.ts +87 -4
- package/components/Scrllgngn/utils.js +142 -8
- package/components/ScrollListener/index.d.ts +12 -3
- package/components/ScrollListener/index.js +12 -3
- package/components/Select/index.d.ts +4 -0
- package/components/Select/index.js +9 -1
- package/components/Sequencer/index.d.ts +10 -1
- package/components/Sequencer/index.js +7 -3
- package/components/ShadowRoot/index.d.ts +4 -0
- package/components/ShadowRoot/index.js +5 -1
- package/components/Subtitles/index.d.ts +53 -62
- package/components/Subtitles/index.js +90 -158
- package/components/Subtitles/utils.d.ts +60 -0
- package/components/Subtitles/utils.js +76 -0
- package/components/Textarea/index.d.ts +4 -0
- package/components/Textarea/index.js +9 -1
- package/components/Video/index.controlled.d.ts +13 -2
- package/components/Video/index.controlled.js +37 -6
- package/components/Video/index.d.ts +47 -9
- package/components/Video/index.js +71 -20
- package/components/Video/utils.d.ts +14 -0
- package/components/Video/utils.js +18 -0
- package/components/index.d.ts +5 -5
- package/components/index.js +5 -5
- package/components/utils/types.d.ts +32 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/node/@aws-s3/storage/directory/index.d.ts +1 -1
- package/node/@aws-s3/storage/directory/index.js +1 -1
- package/node/@aws-s3/storage/file/index.d.ts +1 -1
- package/node/@aws-s3/storage/file/index.js +1 -1
- package/node/@google-cloud/storage/directory/index.d.ts +1 -1
- package/node/@google-cloud/storage/directory/index.js +1 -1
- package/node/@google-cloud/storage/file/index.d.ts +3 -3
- package/node/@google-cloud/storage/file/index.js +3 -3
- package/node/cloud-storage/operations/index.d.ts +2 -2
- package/node/cloud-storage/operations/index.js +2 -2
- package/node/images/index.d.ts +1 -1
- package/node/images/index.js +1 -1
- package/node/images/transform/operations/index.d.ts +5 -5
- package/node/images/transform/operations/index.js +5 -5
- package/node/index.d.ts +1 -1
- package/node/index.js +1 -1
- package/node/sftp/file/index.d.ts +1 -1
- package/node/sftp/file/index.js +1 -1
- package/package.json +26 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Matches `hh:mm:ss,mmm`, with any number of digits in each field. */
|
|
2
|
+
const timecodeRegexp = /^\s*(\d+):(\d+):(\d+),(\d+)\s*$/v;
|
|
3
|
+
/**
|
|
4
|
+
* Reads the fractional field as a **number of digits**, not as a value.
|
|
5
|
+
*
|
|
6
|
+
* SRT writes milliseconds as a decimal fraction of a second: `,5` is half a second,
|
|
7
|
+
* where reading it as an integer would call it 5ms. Padding to three digits gives
|
|
8
|
+
* the fraction its place value back, and truncating past them drops precision the
|
|
9
|
+
* format doesn't carry.
|
|
10
|
+
*/
|
|
11
|
+
function fractionToMs(fraction) {
|
|
12
|
+
return parseInt(`${fraction}00`.slice(0, 3), 10);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Converts an SRT timecode to milliseconds.
|
|
16
|
+
*
|
|
17
|
+
* @param timecode - A timecode in `hh:mm:ss,mmm` form. Surrounding whitespace is
|
|
18
|
+
* tolerated; anything else is not a timecode.
|
|
19
|
+
* @returns The position in milliseconds, or `null` when `timecode` doesn't parse —
|
|
20
|
+
* a caller reading a subtitle file needs to tell a malformed line from one that
|
|
21
|
+
* legitimately sits at zero.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* srtTimecodeToMs('00:00:01,500') // 1500
|
|
25
|
+
* srtTimecodeToMs('00:00:01,5') // 1500, not 1005
|
|
26
|
+
*/
|
|
27
|
+
export function srtTimecodeToMs(timecode) {
|
|
28
|
+
const match = timecode.match(timecodeRegexp);
|
|
29
|
+
if (match === null)
|
|
30
|
+
return null;
|
|
31
|
+
const [, hours = '0', minutes = '0', seconds = '0', fraction = '0'] = match;
|
|
32
|
+
return parseInt(hours, 10) * 60 * 60 * 1000
|
|
33
|
+
+ parseInt(minutes, 10) * 60 * 1000
|
|
34
|
+
+ parseInt(seconds, 10) * 1000
|
|
35
|
+
+ fractionToMs(fraction);
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { srtTimecodeToMs } from './index.js';
|
|
3
|
+
describe('srtTimecodeToMs', () => {
|
|
4
|
+
it('adds up hours, minutes, seconds and milliseconds', () => {
|
|
5
|
+
expect(srtTimecodeToMs('01:02:03,004')).toBe(3723004);
|
|
6
|
+
});
|
|
7
|
+
it('sits at zero for the start of a file', () => {
|
|
8
|
+
expect(srtTimecodeToMs('00:00:00,000')).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
describe('the fractional field', () => {
|
|
11
|
+
it('reads a single digit as tenths of a second', () => {
|
|
12
|
+
expect(srtTimecodeToMs('00:00:01,5')).toBe(1500);
|
|
13
|
+
});
|
|
14
|
+
it('reads two digits as hundredths', () => {
|
|
15
|
+
expect(srtTimecodeToMs('00:00:01,05')).toBe(1050);
|
|
16
|
+
});
|
|
17
|
+
it('reads three digits as milliseconds', () => {
|
|
18
|
+
expect(srtTimecodeToMs('00:00:01,005')).toBe(1005);
|
|
19
|
+
});
|
|
20
|
+
it('drops precision the format does not carry', () => {
|
|
21
|
+
expect(srtTimecodeToMs('00:00:01,5009')).toBe(1500);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
it('tolerates surrounding whitespace, as found around an arrow', () => {
|
|
25
|
+
expect(srtTimecodeToMs(' 00:00:02,000 ')).toBe(2000);
|
|
26
|
+
});
|
|
27
|
+
it('accepts hour counts beyond two digits', () => {
|
|
28
|
+
expect(srtTimecodeToMs('100:00:00,000')).toBe(360000000);
|
|
29
|
+
});
|
|
30
|
+
describe('refusing what is not a timecode', () => {
|
|
31
|
+
it('returns null on a dot separator, which SRT does not use', () => {
|
|
32
|
+
expect(srtTimecodeToMs('00:00:01.500')).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
it('returns null on a missing field', () => {
|
|
35
|
+
expect(srtTimecodeToMs('00:01,000')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it('returns null on plain text', () => {
|
|
38
|
+
expect(srtTimecodeToMs('Bonjour')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
it('returns null rather than zero, so a caller can tell the two apart', () => {
|
|
41
|
+
expect(srtTimecodeToMs('')).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -30,10 +30,11 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
30
30
|
* as a ratio between `0` and `1`. Handles both mouse and touch input, telling a
|
|
31
31
|
* click apart from a drag.
|
|
32
32
|
*
|
|
33
|
-
* The
|
|
34
|
-
* -
|
|
35
|
-
*
|
|
36
|
-
* - a `data-ratio` attribute
|
|
33
|
+
* The divider's position is exposed on the root element as:
|
|
34
|
+
* - the unitless custom property `--lm-before-after-ratio`. No percentage twin —
|
|
35
|
+
* `calc(var(--lm-before-after-ratio) * 100%)` covers it.
|
|
36
|
+
* - a `data-ratio` attribute, for what a custom property cannot reach:
|
|
37
|
+
* `content: attr(data-ratio)` and attribute selectors.
|
|
37
38
|
*
|
|
38
39
|
* ### CSS modifiers
|
|
39
40
|
* - `horizontal` — applied when `mode` is `'horizontal'`.
|
|
@@ -13,10 +13,11 @@ import cssModule from './styles.module.css';
|
|
|
13
13
|
* as a ratio between `0` and `1`. Handles both mouse and touch input, telling a
|
|
14
14
|
* click apart from a drag.
|
|
15
15
|
*
|
|
16
|
-
* The
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
19
|
-
* - a `data-ratio` attribute
|
|
16
|
+
* The divider's position is exposed on the root element as:
|
|
17
|
+
* - the unitless custom property `--lm-before-after-ratio`. No percentage twin —
|
|
18
|
+
* `calc(var(--lm-before-after-ratio) * 100%)` covers it.
|
|
19
|
+
* - a `data-ratio` attribute, for what a custom property cannot reach:
|
|
20
|
+
* `content: attr(data-ratio)` and attribute selectors.
|
|
20
21
|
*
|
|
21
22
|
* ### CSS modifiers
|
|
22
23
|
* - `horizontal` — applied when `mode` is `'horizontal'`.
|
|
@@ -105,8 +106,7 @@ export const ControlledBeforeAfter = ({ mode = 'horizontal', ratio = 0, before,
|
|
|
105
106
|
}), className);
|
|
106
107
|
const roundedRatio = round(clampedRatio, 4);
|
|
107
108
|
const customProps = {
|
|
108
|
-
'--lm-before-after-ratio': `${roundedRatio}
|
|
109
|
-
'--lm-before-after-ratio-percent': `${round(clampedRatio * 100, 2)}%`
|
|
109
|
+
'--lm-before-after-ratio': `${roundedRatio}`
|
|
110
110
|
};
|
|
111
111
|
return _jsxs("div", { ref: rootRef, className: rootClss, onMouseDown: handlePointerDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onTouchStart: handlePointerDown, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, style: { ...customProps }, "data-ratio": roundedRatio, children: [_jsx("div", { className: c('before'), children: before }), _jsx("div", { className: c('after'), children: after }), _jsx("div", { className: c('separator') }), children] });
|
|
112
112
|
};
|
|
@@ -43,6 +43,8 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
43
43
|
* - `opened` — the drawer is open.
|
|
44
44
|
* - `closed` — the drawer is closed.
|
|
45
45
|
*
|
|
46
|
+
* - `--measured` — the content has been measured, so the properties below are set.
|
|
47
|
+
*
|
|
46
48
|
* ### CSS elements
|
|
47
49
|
* - `opener`
|
|
48
50
|
* - `closer`
|
|
@@ -54,8 +56,8 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
54
56
|
* Absent until the first measurement lands.
|
|
55
57
|
*
|
|
56
58
|
* ### Data attributes on the root element
|
|
57
|
-
* - `data-content-width`, `data-content-height` — the measured content size.
|
|
58
59
|
* Absent until the first measurement lands.
|
|
60
|
+
* - `data-content-width`, `data-content-height` — the measured content size.
|
|
59
61
|
*
|
|
60
62
|
* @param props - Component properties.
|
|
61
63
|
* @see {@link Props}
|
|
@@ -15,6 +15,8 @@ import cssModule from './styles.module.css';
|
|
|
15
15
|
* - `opened` — the drawer is open.
|
|
16
16
|
* - `closed` — the drawer is closed.
|
|
17
17
|
*
|
|
18
|
+
* - `--measured` — the content has been measured, so the properties below are set.
|
|
19
|
+
*
|
|
18
20
|
* ### CSS elements
|
|
19
21
|
* - `opener`
|
|
20
22
|
* - `closer`
|
|
@@ -26,8 +28,8 @@ import cssModule from './styles.module.css';
|
|
|
26
28
|
* Absent until the first measurement lands.
|
|
27
29
|
*
|
|
28
30
|
* ### Data attributes on the root element
|
|
29
|
-
* - `data-content-width`, `data-content-height` — the measured content size.
|
|
30
31
|
* Absent until the first measurement lands.
|
|
32
|
+
* - `data-content-width`, `data-content-height` — the measured content size.
|
|
31
33
|
*
|
|
32
34
|
* @param props - Component properties.
|
|
33
35
|
* @see {@link Props}
|
|
@@ -71,7 +73,8 @@ export const Drawer = ({ openerContent, closerContent, defaultIsOpened = false,
|
|
|
71
73
|
const c = clss(publicClassName, { cssModule });
|
|
72
74
|
const rootClss = mergeClassNames(c(null, {
|
|
73
75
|
'opened': isOpened,
|
|
74
|
-
'closed': !isOpened
|
|
76
|
+
'closed': !isOpened,
|
|
77
|
+
measured: contentDimensions !== undefined
|
|
75
78
|
}), className);
|
|
76
79
|
const openerClss = c('opener');
|
|
77
80
|
const closerClss = c('closer');
|
|
@@ -24,6 +24,10 @@ export type Props = WithClassName<{
|
|
|
24
24
|
* The parent component updates the iframe height via React state and `style`
|
|
25
25
|
* prop (no direct DOM mutation).
|
|
26
26
|
*
|
|
27
|
+
* ### Root element modifiers
|
|
28
|
+
* - `--auto-height` — the component is listening for a height from inside the frame.
|
|
29
|
+
* - `--measured` — one has arrived, and the frame is sized to it.
|
|
30
|
+
*
|
|
27
31
|
* @param props - Component properties.
|
|
28
32
|
* @see {@link Props}
|
|
29
33
|
*/
|
|
@@ -33,6 +33,10 @@ const innerAutoHeightNotifier = (messageType) => `<script>
|
|
|
33
33
|
* The parent component updates the iframe height via React state and `style`
|
|
34
34
|
* prop (no direct DOM mutation).
|
|
35
35
|
*
|
|
36
|
+
* ### Root element modifiers
|
|
37
|
+
* - `--auto-height` — the component is listening for a height from inside the frame.
|
|
38
|
+
* - `--measured` — one has arrived, and the frame is sized to it.
|
|
39
|
+
*
|
|
36
40
|
* @param props - Component properties.
|
|
37
41
|
* @see {@link Props}
|
|
38
42
|
*/
|
|
@@ -68,7 +72,10 @@ export const Iframe = ({ autoHeight = false, className, srcDoc, style, ...iframe
|
|
|
68
72
|
return { ...base, height: `${height}px` };
|
|
69
73
|
}, [style, autoHeight, height]);
|
|
70
74
|
const c = clss(publicClassName, { cssModule });
|
|
71
|
-
const rootClss = mergeClassNames(c(
|
|
75
|
+
const rootClss = mergeClassNames(c(null, {
|
|
76
|
+
'auto-height': autoHeight,
|
|
77
|
+
measured: height !== null
|
|
78
|
+
}), className);
|
|
72
79
|
const html = useMemo(() => {
|
|
73
80
|
const autoHeightScript = autoHeight
|
|
74
81
|
? innerAutoHeightNotifier(innerMessageType)
|
|
@@ -23,6 +23,10 @@ export type Props = InputHTMLAttributes<HTMLInputElement> & WithClassName<{
|
|
|
23
23
|
* A stable auto-generated `id` is created on mount and used to associate the
|
|
24
24
|
* rendered label through the `htmlFor` attribute.
|
|
25
25
|
*
|
|
26
|
+
* ### Root element modifiers
|
|
27
|
+
* - `--controlled` — the value comes from the parent, not from inside.
|
|
28
|
+
* - `--empty` / `--filled` — whether the current value is the empty string.
|
|
29
|
+
*
|
|
26
30
|
* ### CSS elements
|
|
27
31
|
* - `label`
|
|
28
32
|
* - `error`
|
|
@@ -15,6 +15,10 @@ import cssModule from './styles.module.css';
|
|
|
15
15
|
* A stable auto-generated `id` is created on mount and used to associate the
|
|
16
16
|
* rendered label through the `htmlFor` attribute.
|
|
17
17
|
*
|
|
18
|
+
* ### Root element modifiers
|
|
19
|
+
* - `--controlled` — the value comes from the parent, not from inside.
|
|
20
|
+
* - `--empty` / `--filled` — whether the current value is the empty string.
|
|
21
|
+
*
|
|
18
22
|
* ### CSS elements
|
|
19
23
|
* - `label`
|
|
20
24
|
* - `error`
|
|
@@ -44,6 +48,10 @@ export const Input = ({ label, error, value, defaultValue, onChange, className,
|
|
|
44
48
|
};
|
|
45
49
|
// Rendering
|
|
46
50
|
const c = clss(publicClassName, { cssModule });
|
|
47
|
-
const rootClss = mergeClassNames(c(
|
|
51
|
+
const rootClss = mergeClassNames(c(null, {
|
|
52
|
+
controlled: isControlled,
|
|
53
|
+
empty: currentValue === '',
|
|
54
|
+
filled: currentValue !== ''
|
|
55
|
+
}), className);
|
|
48
56
|
return _jsxs(_Fragment, { children: [isNotFalsy(label) && _jsx("label", { className: c('label'), htmlFor: id, children: label }), _jsx("input", { ...rest, id: id, className: rootClss, value: currentValue, onChange: handleChange }), isNotFalsy(error) && _jsx("span", { className: c('error'), children: error })] });
|
|
49
57
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { WithClassName } from '../utils/types.js';
|
|
2
|
+
import type { ViewportObserverOptions, WithClassName } from '../utils/types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Props for the {@link ControlledListLoader} component.
|
|
5
5
|
*
|
|
@@ -26,9 +26,14 @@ import type { WithClassName } from '../utils/types.js';
|
|
|
26
26
|
* @property autoLoadPrevWhenVisible - Wraps the leading load button in an
|
|
27
27
|
* {@link IntersectionObserverComponent} and fires `onLoadPageClicked` when it enters the viewport.
|
|
28
28
|
* @property autoLoadNextWhenVisible - Same, for the trailing load button.
|
|
29
|
+
* @property threshold - How much of a load button has to be in view before it
|
|
30
|
+
* counts as visible, forwarded to each observer.
|
|
31
|
+
* @property root - Their root. Defaults to the viewport.
|
|
32
|
+
* @property rootMargin - Grows or shrinks that root before measuring — the way to
|
|
33
|
+
* start loading a page before its button actually shows.
|
|
29
34
|
* @property className - Additional class name(s) applied to the root element.
|
|
30
35
|
*/
|
|
31
|
-
export type Props<T> = WithClassName<{
|
|
36
|
+
export type Props<T> = WithClassName<ViewportObserverOptions & {
|
|
32
37
|
pages: number[];
|
|
33
38
|
firstPagePos: number;
|
|
34
39
|
lastPagePos: number;
|
|
@@ -55,6 +60,9 @@ export type Props<T> = WithClassName<{
|
|
|
55
60
|
* - in place of each page missing between two loaded pages,
|
|
56
61
|
* - after the last loaded page, unless it is already `lastPagePos`.
|
|
57
62
|
*
|
|
63
|
+
* ### Root element modifiers
|
|
64
|
+
* - `--loading` — at least one page is being fetched.
|
|
65
|
+
*
|
|
58
66
|
* ### CSS elements
|
|
59
67
|
* - `item` — wraps each rendered item. Carries `data-page`.
|
|
60
68
|
* - `load` — a load button. Rendered empty, so its label belongs in CSS.
|
|
@@ -75,4 +83,4 @@ export type Props<T> = WithClassName<{
|
|
|
75
83
|
* Deduplication runs after `filter`, so an item hidden by `filter` never evicts
|
|
76
84
|
* an earlier duplicate.
|
|
77
85
|
*/
|
|
78
|
-
export declare const ControlledListLoader: <T>({ className, pages, firstPagePos, lastPagePos, itemsPages, filter, display, getIdentifier, loadingPages, onLoadPageClicked, autoLoadPrevWhenVisible, autoLoadNextWhenVisible }: Props<T>) => ReactNode;
|
|
86
|
+
export declare const ControlledListLoader: <T>({ className, pages, firstPagePos, lastPagePos, itemsPages, filter, display, getIdentifier, loadingPages, onLoadPageClicked, autoLoadPrevWhenVisible, autoLoadNextWhenVisible, threshold, root, rootMargin }: Props<T>) => ReactNode;
|
|
@@ -19,6 +19,9 @@ import cssModule from './styles.module.css';
|
|
|
19
19
|
* - in place of each page missing between two loaded pages,
|
|
20
20
|
* - after the last loaded page, unless it is already `lastPagePos`.
|
|
21
21
|
*
|
|
22
|
+
* ### Root element modifiers
|
|
23
|
+
* - `--loading` — at least one page is being fetched.
|
|
24
|
+
*
|
|
22
25
|
* ### CSS elements
|
|
23
26
|
* - `item` — wraps each rendered item. Carries `data-page`.
|
|
24
27
|
* - `load` — a load button. Rendered empty, so its label belongs in CSS.
|
|
@@ -39,7 +42,7 @@ import cssModule from './styles.module.css';
|
|
|
39
42
|
* Deduplication runs after `filter`, so an item hidden by `filter` never evicts
|
|
40
43
|
* an earlier duplicate.
|
|
41
44
|
*/
|
|
42
|
-
export const ControlledListLoader = ({ className, pages, firstPagePos, lastPagePos, itemsPages, filter, display, getIdentifier, loadingPages, onLoadPageClicked, autoLoadPrevWhenVisible, autoLoadNextWhenVisible }) => {
|
|
45
|
+
export const ControlledListLoader = ({ className, pages, firstPagePos, lastPagePos, itemsPages, filter, display, getIdentifier, loadingPages, onLoadPageClicked, autoLoadPrevWhenVisible, autoLoadNextWhenVisible, threshold, root, rootMargin }) => {
|
|
43
46
|
const inBoundsPages = pages.filter(pagePos => pagePos >= firstPagePos && pagePos <= lastPagePos);
|
|
44
47
|
const filteredItems = Array.from(itemsPages)
|
|
45
48
|
.filter(([pagePos]) => inBoundsPages.includes(pagePos))
|
|
@@ -78,14 +81,14 @@ export const ControlledListLoader = ({ className, pages, firstPagePos, lastPageP
|
|
|
78
81
|
.filter(pagePos => inBoundsPages.includes(pagePos))
|
|
79
82
|
.sort((a, b) => a - b);
|
|
80
83
|
const c = clss(publicClassName, { cssModule });
|
|
81
|
-
const rootClss = mergeClassNames(c(), className);
|
|
84
|
+
const rootClss = mergeClassNames(c(null, { loading: sortedLoadingPages.length > 0 }), className);
|
|
82
85
|
const renderLoadButton = (pagePos, kind) => {
|
|
83
86
|
const button = _jsx("button", { className: c('load', kind), "data-page": pagePos, onClick: () => onLoadPageClicked?.(pagePos) }, `load/${pagePos}`);
|
|
84
87
|
const autoLoads = (kind === 'prev' && autoLoadPrevWhenVisible === true)
|
|
85
88
|
|| (kind === 'next' && autoLoadNextWhenVisible === true);
|
|
86
89
|
if (!autoLoads)
|
|
87
90
|
return button;
|
|
88
|
-
return _jsx(IntersectionObserverComponent, { className: c('load-observer', kind), onIntersected: ({ ioEntry }) => {
|
|
91
|
+
return _jsx(IntersectionObserverComponent, { className: c('load-observer', kind), threshold: threshold, root: root, rootMargin: rootMargin, onIntersected: ({ ioEntry }) => {
|
|
89
92
|
if (ioEntry?.isIntersecting !== true)
|
|
90
93
|
return;
|
|
91
94
|
onLoadPageClicked?.(pagePos);
|
|
@@ -66,4 +66,4 @@ export type Props<T> = Omit<ControlledProps<T>, 'itemsPages' | 'loadingPages' |
|
|
|
66
66
|
* - No page outside `firstPagePos`–`lastPagePos` is ever fetched, whichever mode
|
|
67
67
|
* is in use.
|
|
68
68
|
*/
|
|
69
|
-
export declare const ListLoader: <T>({ className, pages, defaultPage, fillGaps, dropPagesFurtherThan, autoLoadPrevWhenVisible, autoLoadNextWhenVisible, firstPagePos, lastPagePos, fetch, filter, display, getIdentifier, onLoadPageClicked, staleAfterMs, onPageFetched, onPageFetchFailed, fetchRetriesNb, fetchRetriesDelayMs }: Props<T>) => ReactNode;
|
|
69
|
+
export declare const ListLoader: <T>({ className, pages, defaultPage, fillGaps, dropPagesFurtherThan, autoLoadPrevWhenVisible, autoLoadNextWhenVisible, threshold, root, rootMargin, firstPagePos, lastPagePos, fetch, filter, display, getIdentifier, onLoadPageClicked, staleAfterMs, onPageFetched, onPageFetchFailed, fetchRetriesNb, fetchRetriesDelayMs }: Props<T>) => ReactNode;
|
|
@@ -25,7 +25,7 @@ import { rangeBetween } from './utils.js';
|
|
|
25
25
|
* - No page outside `firstPagePos`–`lastPagePos` is ever fetched, whichever mode
|
|
26
26
|
* is in use.
|
|
27
27
|
*/
|
|
28
|
-
export const ListLoader = ({ className, pages, defaultPage, fillGaps = true, dropPagesFurtherThan, autoLoadPrevWhenVisible, autoLoadNextWhenVisible, firstPagePos, lastPagePos, fetch, filter, display, getIdentifier, onLoadPageClicked, staleAfterMs, onPageFetched, onPageFetchFailed, fetchRetriesNb = Infinity, fetchRetriesDelayMs = 1000 }) => {
|
|
28
|
+
export const ListLoader = ({ className, pages, defaultPage, fillGaps = true, dropPagesFurtherThan, autoLoadPrevWhenVisible, autoLoadNextWhenVisible, threshold, root, rootMargin, firstPagePos, lastPagePos, fetch, filter, display, getIdentifier, onLoadPageClicked, staleAfterMs, onPageFetched, onPageFetchFailed, fetchRetriesNb = Infinity, fetchRetriesDelayMs = 1000 }) => {
|
|
29
29
|
const [itemsPages, setItemsPages] = useState(new Map());
|
|
30
30
|
const [loadingPages, setLoadingPages] = useState(new Set());
|
|
31
31
|
const [internalPages, setInternalPages] = useState([
|
|
@@ -142,5 +142,5 @@ export const ListLoader = ({ className, pages, defaultPage, fillGaps = true, dro
|
|
|
142
142
|
const controlledItemsPages = new Map(Array
|
|
143
143
|
.from(itemsPages)
|
|
144
144
|
.map(([page, { items }]) => [page, items]));
|
|
145
|
-
return _jsx(ControlledListLoader, { className: className, pages: currentPages, firstPagePos: firstPagePos, lastPagePos: lastPagePos, itemsPages: controlledItemsPages, filter: filter, display: display, getIdentifier: getIdentifier, loadingPages: Array.from(loadingPages), onLoadPageClicked: handleLoadClick, autoLoadPrevWhenVisible: autoLoadPrevWhenVisible, autoLoadNextWhenVisible: autoLoadNextWhenVisible });
|
|
145
|
+
return _jsx(ControlledListLoader, { className: className, pages: currentPages, firstPagePos: firstPagePos, lastPagePos: lastPagePos, itemsPages: controlledItemsPages, filter: filter, display: display, getIdentifier: getIdentifier, loadingPages: Array.from(loadingPages), onLoadPageClicked: handleLoadClick, autoLoadPrevWhenVisible: autoLoadPrevWhenVisible, autoLoadNextWhenVisible: autoLoadNextWhenVisible, threshold: threshold, root: root, rootMargin: rootMargin });
|
|
146
146
|
};
|
|
@@ -20,9 +20,14 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
20
20
|
* rect on its root element. Both sets stay absent until the first measurement
|
|
21
21
|
* lands.
|
|
22
22
|
*
|
|
23
|
+
* ### Root element modifiers
|
|
24
|
+
* - `--measured` — a first measurement landed, so the sets below are populated.
|
|
25
|
+
*
|
|
23
26
|
* ### Data attributes
|
|
24
27
|
* `data-x`, `data-y`, `data-top`, `data-left`, `data-bottom`, `data-right`,
|
|
25
|
-
* `data-width` and `data-height` — the content rect, as plain numbers.
|
|
28
|
+
* `data-width` and `data-height` — the content rect, as plain numbers. They carry
|
|
29
|
+
* what a custom property cannot reach: `content: attr(data-width)`, and attribute
|
|
30
|
+
* selectors.
|
|
26
31
|
*
|
|
27
32
|
* ### CSS custom properties
|
|
28
33
|
* The same eight measurements, each exposed twice: `--lm-resize-observer-width`
|
|
@@ -9,9 +9,14 @@ import cssModule from './styles.module.css';
|
|
|
9
9
|
* rect on its root element. Both sets stay absent until the first measurement
|
|
10
10
|
* lands.
|
|
11
11
|
*
|
|
12
|
+
* ### Root element modifiers
|
|
13
|
+
* - `--measured` — a first measurement landed, so the sets below are populated.
|
|
14
|
+
*
|
|
12
15
|
* ### Data attributes
|
|
13
16
|
* `data-x`, `data-y`, `data-top`, `data-left`, `data-bottom`, `data-right`,
|
|
14
|
-
* `data-width` and `data-height` — the content rect, as plain numbers.
|
|
17
|
+
* `data-width` and `data-height` — the content rect, as plain numbers. They carry
|
|
18
|
+
* what a custom property cannot reach: `content: attr(data-width)`, and attribute
|
|
19
|
+
* selectors.
|
|
15
20
|
*
|
|
16
21
|
* ### CSS custom properties
|
|
17
22
|
* The same eight measurements, each exposed twice: `--lm-resize-observer-width`
|
|
@@ -89,6 +94,6 @@ export const ResizeObserverComponent = ({ className, onResized, children }) => {
|
|
|
89
94
|
};
|
|
90
95
|
}
|
|
91
96
|
const c = clss(publicClassName, { cssModule });
|
|
92
|
-
const rootClss = mergeClassNames(c(), className);
|
|
97
|
+
const rootClss = mergeClassNames(c(null, { measured: contentRect !== undefined }), className);
|
|
93
98
|
return _jsx("div", { ...dataAttributes, className: rootClss, ref: rootRef, style: { ...cssCustomProps }, children: children });
|
|
94
99
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type PropsWithChildren, type FunctionComponent } from 'react';
|
|
2
2
|
import type { WithClassName } from '../utils/types.js';
|
|
3
|
-
import { type TrackedBlockContext } from './utils.js';
|
|
3
|
+
import { type TrackedBlockContext, type ViewportOffset } from './utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Common properties shared by all block types.
|
|
6
6
|
*
|
|
@@ -62,9 +62,17 @@ export type PropsPage = {
|
|
|
62
62
|
*
|
|
63
63
|
* @property pages - Ordered list of pages that compose the scrollytelling
|
|
64
64
|
* sequence. Each page may contain any mix of {@link PropsBlock} variants.
|
|
65
|
-
* @property thresholdOffsetPercent -
|
|
66
|
-
*
|
|
67
|
-
* which page
|
|
65
|
+
* @property thresholdOffsetPercent - Where the threshold line sits, as a
|
|
66
|
+
* percentage of the **visible zone** — not of the viewport, so a threshold stays
|
|
67
|
+
* put under a sticky nav. It decides which page counts as current, and anchors
|
|
68
|
+
* every progression handed to a tracked block.
|
|
69
|
+
* @property viewportOffsetTop - How much of the top of the screen is covered by
|
|
70
|
+
* something else — a sticky nav, say — and should be kept out of the visible zone.
|
|
71
|
+
* A number means pixels; a string is any CSS length, `var()` and `clamp()`
|
|
72
|
+
* included, so the value can follow a breakpoint without JavaScript.
|
|
73
|
+
* @property viewportOffsetRight - Same, on the right edge.
|
|
74
|
+
* @property viewportOffsetBottom - Same, on the bottom edge.
|
|
75
|
+
* @property viewportOffsetLeft - Same, on the left edge.
|
|
68
76
|
* @property stickyBlocksLazyLoadDistance - Number of pages around the current
|
|
69
77
|
* page within which sticky blocks are mounted. Blocks outside this window are
|
|
70
78
|
* unmounted to save resources. Defaults to `2`.
|
|
@@ -85,6 +93,10 @@ export type PropsPage = {
|
|
|
85
93
|
export type Props = WithClassName<{
|
|
86
94
|
pages?: PropsPage[];
|
|
87
95
|
thresholdOffsetPercent?: number;
|
|
96
|
+
viewportOffsetTop?: ViewportOffset;
|
|
97
|
+
viewportOffsetRight?: ViewportOffset;
|
|
98
|
+
viewportOffsetBottom?: ViewportOffset;
|
|
99
|
+
viewportOffsetLeft?: ViewportOffset;
|
|
88
100
|
stickyBlocksLazyLoadDistance?: number;
|
|
89
101
|
forceStickBlocks?: 'before' | 'after' | 'both' | 'none';
|
|
90
102
|
onPageChanged?: (currentPagePos: number, pageData?: PropsPage) => void;
|
|
@@ -125,6 +137,16 @@ export type Props = WithClassName<{
|
|
|
125
137
|
* `-width` and `-height` for the component's own stylesheet. They are internal:
|
|
126
138
|
* do not read or override them.
|
|
127
139
|
*
|
|
140
|
+
* ### Visible zone
|
|
141
|
+
* Set any of the `viewportOffset*` props and the component stops treating the whole
|
|
142
|
+
* screen as available: the fixed layers are inset by those edges, the top / content
|
|
143
|
+
* / bottom detection observes the reduced box, and `thresholdOffsetPercent` reads as
|
|
144
|
+
* a percentage of it. With none of them set, everything behaves exactly as before.
|
|
145
|
+
*
|
|
146
|
+
* The four edges are echoed as `--lm-scrllgngn-viewport-offset-top`, `-right`,
|
|
147
|
+
* `-bottom` and `-left`, carrying the length as authored — pixels for a number, the
|
|
148
|
+
* string untouched otherwise.
|
|
149
|
+
*
|
|
128
150
|
* ### Sticky block elements
|
|
129
151
|
* Each lazy-loaded sticky block receives:
|
|
130
152
|
* - `--active` modifier when the block's page range includes the current page.
|
|
@@ -156,11 +178,19 @@ export type Props = WithClassName<{
|
|
|
156
178
|
*
|
|
157
179
|
* @param props - Component properties.
|
|
158
180
|
* @see {@link Props}
|
|
159
|
-
* @returns A div wrapping the full scrollytelling structure:
|
|
160
|
-
* back-blocks layer, front-blocks layer, paginated scrolling
|
|
161
|
-
* bottom-bound sentinel.
|
|
181
|
+
* @returns A div wrapping the full scrollytelling structure: visible zone probe,
|
|
182
|
+
* top-bound sentinel, back-blocks layer, front-blocks layer, paginated scrolling
|
|
183
|
+
* content, and bottom-bound sentinel.
|
|
162
184
|
*
|
|
163
185
|
* @remarks
|
|
186
|
+
* A `viewportOffset*` given as a CSS length is resolved by CSS, not parsed: a hidden
|
|
187
|
+
* probe is inset by the four of them and observed, so `var()`, `clamp()` and
|
|
188
|
+
* breakpoint-dependent values all work, and any restyling that changes them is
|
|
189
|
+
* picked up as a resize. A change that moves the zone without altering its size —
|
|
190
|
+
* a top offset traded for an equal bottom one — is caught instead by a re-read every
|
|
191
|
+
* 100ms, which runs only while the component is on screen and only reaches state
|
|
192
|
+
* when the zone really moved.
|
|
193
|
+
*
|
|
164
194
|
* Tracking costs nothing until a block asks for it: with no `onScrolled` anywhere,
|
|
165
195
|
* the component never joins the shared scroll listener. Once it does, each frame
|
|
166
196
|
* measures only the pages the displayed tracked blocks span, not the whole sequence.
|