@trackunit/react-components 3.2.3 → 3.3.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/index.cjs.js
CHANGED
|
@@ -1490,6 +1490,59 @@ const cvaSpinnerContainer = cssClassVarianceUtilities.cva({
|
|
|
1490
1490
|
});
|
|
1491
1491
|
const cvaSpinnerLabel = cssClassVarianceUtilities.cva({ base: ["self-center", "text-center", "text-current"] });
|
|
1492
1492
|
|
|
1493
|
+
/**
|
|
1494
|
+
* Cycle duration, in milliseconds, matching Tailwind's `animate-spin` utility
|
|
1495
|
+
* (`spin 1s linear infinite`) — the animation `Spinner` uses.
|
|
1496
|
+
*/
|
|
1497
|
+
const DEFAULT_SPIN_DURATION_MS = 1000;
|
|
1498
|
+
/**
|
|
1499
|
+
* Synchronizes an `animate-spin` element's rotation phase to the shared wall clock, so every
|
|
1500
|
+
* element using this hook — anywhere on the page, including across iframe boundaries — renders
|
|
1501
|
+
* at the same point in its spin cycle regardless of when its DOM node, document, or iframe was
|
|
1502
|
+
* created.
|
|
1503
|
+
*
|
|
1504
|
+
* On mount, in a layout effect, it writes a negative `animation-delay` —
|
|
1505
|
+
* `-(Date.now() % durationMs)` — directly onto `ref.current`. A negative delay skips the
|
|
1506
|
+
* animation forward by that amount, effectively pinning the element to `now`'s position in the
|
|
1507
|
+
* cycle instead of starting fresh at phase 0. Because `Date.now()` is the same system clock
|
|
1508
|
+
* everywhere, this keeps `animate-spin` elements visually synchronized across the whole page with
|
|
1509
|
+
* no message-passing required.
|
|
1510
|
+
*
|
|
1511
|
+
* The sample is taken in a layout effect rather than during render: React's render phase can be
|
|
1512
|
+
* time-sliced across several elements (a large table/page mount, a Suspense-covered subtree,
|
|
1513
|
+
* etc.), so elements rendered in the same commit could otherwise sample the clock many
|
|
1514
|
+
* milliseconds apart and end up visibly out of phase with each other. Layout effects for a given
|
|
1515
|
+
* commit all run together, synchronously, right after the DOM mutations land and before paint —
|
|
1516
|
+
* so every element written to in the same commit gets, for practical purposes, the same
|
|
1517
|
+
* timestamp.
|
|
1518
|
+
*
|
|
1519
|
+
* The delay is written once, on mount, and then held fixed for the life of the element (the
|
|
1520
|
+
* effect only re-runs if `ref` or `durationMs` change identity, which does not happen for a
|
|
1521
|
+
* stable ref and a constant duration). Recomputing it on every render would be actively harmful:
|
|
1522
|
+
* once a CSS animation is running, changing `animation-delay` does not re-anchor its phase to the
|
|
1523
|
+
* new value — it recombines the new delay with the animation's own already-elapsed local clock,
|
|
1524
|
+
* which desyncs the element instead of correcting it.
|
|
1525
|
+
*
|
|
1526
|
+
* Known trade-off: an element that is `display: none` at mount (a collapsed panel, inactive tab)
|
|
1527
|
+
* doesn't have a running animation yet, so its delay is anchored to mount time rather than to
|
|
1528
|
+
* when the animation actually starts once shown, and it does not resync after a
|
|
1529
|
+
* backgrounded tab/iframe throttles the animation and later resumes it — both are acceptable
|
|
1530
|
+
* trade-offs for a short-lived, purely decorative loading spinner.
|
|
1531
|
+
*
|
|
1532
|
+
* @param {RefObject<HTMLElement | null>} ref Ref to the element with the `animate-spin` animation.
|
|
1533
|
+
* @param {number} durationMs Duration of one full spin cycle, in milliseconds, at mount time.
|
|
1534
|
+
* Must match the animation's own `animation-duration`. Defaults to 1000ms, matching
|
|
1535
|
+
* `Spinner`'s `animate-spin` usage.
|
|
1536
|
+
*/
|
|
1537
|
+
const useSyncedSpinDelay = (ref, durationMs = DEFAULT_SPIN_DURATION_MS) => {
|
|
1538
|
+
react.useLayoutEffect(() => {
|
|
1539
|
+
const el = ref.current;
|
|
1540
|
+
if (!el)
|
|
1541
|
+
return;
|
|
1542
|
+
el.style.animationDelay = `${-(Date.now() % durationMs)}ms`;
|
|
1543
|
+
}, [ref, durationMs]);
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1493
1546
|
/**
|
|
1494
1547
|
* Spinner provides visual feedback that data is being processed. It reassures users that their action is being handled
|
|
1495
1548
|
* during short operations (1-5 seconds) such as saving, loading, or refreshing data.
|
|
@@ -1526,7 +1579,11 @@ const cvaSpinnerLabel = cssClassVarianceUtilities.cva({ base: ["self-center", "t
|
|
|
1526
1579
|
* @returns {ReactElement} Spinner component
|
|
1527
1580
|
*/
|
|
1528
1581
|
const Spinner = ({ mode = "light", size = "medium", centering = "centered", className, containerClassName, "data-testid": dataTestId = "spinner", label, style, ref, }) => {
|
|
1529
|
-
|
|
1582
|
+
// Keeps every Spinner's rotation in phase with every other one on the page -- including
|
|
1583
|
+
// spinners rendered in other iframes -- since the delay is derived from the shared wall clock.
|
|
1584
|
+
const spinnerElRef = react.useRef(null);
|
|
1585
|
+
useSyncedSpinDelay(spinnerElRef);
|
|
1586
|
+
return (jsxRuntime.jsx("div", { className: cvaSpinnerContainer({ centering, className: containerClassName }), ref: ref, style: style, children: jsxRuntime.jsxs("div", { className: cvaSpinnerContainerInner(), children: [jsxRuntime.jsx("div", { className: cvaSpinner({ size, mode, className }), "data-testid": dataTestId, ref: spinnerElRef, role: "spinbutton" }), label ? jsxRuntime.jsx("span", { className: cvaSpinnerLabel(), children: label }) : null] }) }));
|
|
1530
1587
|
};
|
|
1531
1588
|
|
|
1532
1589
|
const cvaButton = cssClassVarianceUtilities.cva({
|
package/index.esm.js
CHANGED
|
@@ -1489,6 +1489,59 @@ const cvaSpinnerContainer = cva({
|
|
|
1489
1489
|
});
|
|
1490
1490
|
const cvaSpinnerLabel = cva({ base: ["self-center", "text-center", "text-current"] });
|
|
1491
1491
|
|
|
1492
|
+
/**
|
|
1493
|
+
* Cycle duration, in milliseconds, matching Tailwind's `animate-spin` utility
|
|
1494
|
+
* (`spin 1s linear infinite`) — the animation `Spinner` uses.
|
|
1495
|
+
*/
|
|
1496
|
+
const DEFAULT_SPIN_DURATION_MS = 1000;
|
|
1497
|
+
/**
|
|
1498
|
+
* Synchronizes an `animate-spin` element's rotation phase to the shared wall clock, so every
|
|
1499
|
+
* element using this hook — anywhere on the page, including across iframe boundaries — renders
|
|
1500
|
+
* at the same point in its spin cycle regardless of when its DOM node, document, or iframe was
|
|
1501
|
+
* created.
|
|
1502
|
+
*
|
|
1503
|
+
* On mount, in a layout effect, it writes a negative `animation-delay` —
|
|
1504
|
+
* `-(Date.now() % durationMs)` — directly onto `ref.current`. A negative delay skips the
|
|
1505
|
+
* animation forward by that amount, effectively pinning the element to `now`'s position in the
|
|
1506
|
+
* cycle instead of starting fresh at phase 0. Because `Date.now()` is the same system clock
|
|
1507
|
+
* everywhere, this keeps `animate-spin` elements visually synchronized across the whole page with
|
|
1508
|
+
* no message-passing required.
|
|
1509
|
+
*
|
|
1510
|
+
* The sample is taken in a layout effect rather than during render: React's render phase can be
|
|
1511
|
+
* time-sliced across several elements (a large table/page mount, a Suspense-covered subtree,
|
|
1512
|
+
* etc.), so elements rendered in the same commit could otherwise sample the clock many
|
|
1513
|
+
* milliseconds apart and end up visibly out of phase with each other. Layout effects for a given
|
|
1514
|
+
* commit all run together, synchronously, right after the DOM mutations land and before paint —
|
|
1515
|
+
* so every element written to in the same commit gets, for practical purposes, the same
|
|
1516
|
+
* timestamp.
|
|
1517
|
+
*
|
|
1518
|
+
* The delay is written once, on mount, and then held fixed for the life of the element (the
|
|
1519
|
+
* effect only re-runs if `ref` or `durationMs` change identity, which does not happen for a
|
|
1520
|
+
* stable ref and a constant duration). Recomputing it on every render would be actively harmful:
|
|
1521
|
+
* once a CSS animation is running, changing `animation-delay` does not re-anchor its phase to the
|
|
1522
|
+
* new value — it recombines the new delay with the animation's own already-elapsed local clock,
|
|
1523
|
+
* which desyncs the element instead of correcting it.
|
|
1524
|
+
*
|
|
1525
|
+
* Known trade-off: an element that is `display: none` at mount (a collapsed panel, inactive tab)
|
|
1526
|
+
* doesn't have a running animation yet, so its delay is anchored to mount time rather than to
|
|
1527
|
+
* when the animation actually starts once shown, and it does not resync after a
|
|
1528
|
+
* backgrounded tab/iframe throttles the animation and later resumes it — both are acceptable
|
|
1529
|
+
* trade-offs for a short-lived, purely decorative loading spinner.
|
|
1530
|
+
*
|
|
1531
|
+
* @param {RefObject<HTMLElement | null>} ref Ref to the element with the `animate-spin` animation.
|
|
1532
|
+
* @param {number} durationMs Duration of one full spin cycle, in milliseconds, at mount time.
|
|
1533
|
+
* Must match the animation's own `animation-duration`. Defaults to 1000ms, matching
|
|
1534
|
+
* `Spinner`'s `animate-spin` usage.
|
|
1535
|
+
*/
|
|
1536
|
+
const useSyncedSpinDelay = (ref, durationMs = DEFAULT_SPIN_DURATION_MS) => {
|
|
1537
|
+
useLayoutEffect(() => {
|
|
1538
|
+
const el = ref.current;
|
|
1539
|
+
if (!el)
|
|
1540
|
+
return;
|
|
1541
|
+
el.style.animationDelay = `${-(Date.now() % durationMs)}ms`;
|
|
1542
|
+
}, [ref, durationMs]);
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1492
1545
|
/**
|
|
1493
1546
|
* Spinner provides visual feedback that data is being processed. It reassures users that their action is being handled
|
|
1494
1547
|
* during short operations (1-5 seconds) such as saving, loading, or refreshing data.
|
|
@@ -1525,7 +1578,11 @@ const cvaSpinnerLabel = cva({ base: ["self-center", "text-center", "text-current
|
|
|
1525
1578
|
* @returns {ReactElement} Spinner component
|
|
1526
1579
|
*/
|
|
1527
1580
|
const Spinner = ({ mode = "light", size = "medium", centering = "centered", className, containerClassName, "data-testid": dataTestId = "spinner", label, style, ref, }) => {
|
|
1528
|
-
|
|
1581
|
+
// Keeps every Spinner's rotation in phase with every other one on the page -- including
|
|
1582
|
+
// spinners rendered in other iframes -- since the delay is derived from the shared wall clock.
|
|
1583
|
+
const spinnerElRef = useRef(null);
|
|
1584
|
+
useSyncedSpinDelay(spinnerElRef);
|
|
1585
|
+
return (jsx("div", { className: cvaSpinnerContainer({ centering, className: containerClassName }), ref: ref, style: style, children: jsxs("div", { className: cvaSpinnerContainerInner(), children: [jsx("div", { className: cvaSpinner({ size, mode, className }), "data-testid": dataTestId, ref: spinnerElRef, role: "spinbutton" }), label ? jsx("span", { className: cvaSpinnerLabel(), children: label }) : null] }) }));
|
|
1529
1586
|
};
|
|
1530
1587
|
|
|
1531
1588
|
const cvaButton = cva({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-components",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"migrations": "./migrations.json",
|
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
"@floating-ui/react": "^0.26.25",
|
|
15
15
|
"string-ts": "^2.0.0",
|
|
16
16
|
"tailwind-merge": "^2.0.0",
|
|
17
|
-
"@trackunit/ui-design-tokens": "1.15.
|
|
18
|
-
"@trackunit/css-class-variance-utilities": "2.0.
|
|
19
|
-
"@trackunit/shared-utils": "1.16.
|
|
20
|
-
"@trackunit/ui-icons": "1.14.
|
|
17
|
+
"@trackunit/ui-design-tokens": "1.15.42",
|
|
18
|
+
"@trackunit/css-class-variance-utilities": "2.0.22",
|
|
19
|
+
"@trackunit/shared-utils": "1.16.57",
|
|
20
|
+
"@trackunit/ui-icons": "1.14.50",
|
|
21
21
|
"es-toolkit": "^1.39.10",
|
|
22
22
|
"@tanstack/react-virtual": "^3.14.3",
|
|
23
23
|
"dequal": "^2.0.3",
|
|
24
24
|
"fflate": "^0.8.2",
|
|
25
25
|
"zod": "^3.25.76",
|
|
26
|
-
"@trackunit/i18n-library-translation": "2.5.
|
|
26
|
+
"@trackunit/i18n-library-translation": "2.5.9"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"react": "^19.0.0",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ReactElement } from "react";
|
|
2
2
|
import { CommonProps } from "../../common/CommonProps";
|
|
3
|
-
import type { Styleable } from "../../common/Styleable";
|
|
4
3
|
import { Refable } from "../../common/Refable";
|
|
5
4
|
import { Size } from "../../common/Size";
|
|
5
|
+
import type { Styleable } from "../../common/Styleable";
|
|
6
6
|
type Centering = "horizontally" | "vertically" | "centered";
|
|
7
7
|
export interface SpinnerProps extends CommonProps, Styleable, Refable<HTMLDivElement> {
|
|
8
8
|
/**
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Cycle duration, in milliseconds, matching Tailwind's `animate-spin` utility
|
|
4
|
+
* (`spin 1s linear infinite`) — the animation `Spinner` uses.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_SPIN_DURATION_MS = 1000;
|
|
7
|
+
/**
|
|
8
|
+
* Synchronizes an `animate-spin` element's rotation phase to the shared wall clock, so every
|
|
9
|
+
* element using this hook — anywhere on the page, including across iframe boundaries — renders
|
|
10
|
+
* at the same point in its spin cycle regardless of when its DOM node, document, or iframe was
|
|
11
|
+
* created.
|
|
12
|
+
*
|
|
13
|
+
* On mount, in a layout effect, it writes a negative `animation-delay` —
|
|
14
|
+
* `-(Date.now() % durationMs)` — directly onto `ref.current`. A negative delay skips the
|
|
15
|
+
* animation forward by that amount, effectively pinning the element to `now`'s position in the
|
|
16
|
+
* cycle instead of starting fresh at phase 0. Because `Date.now()` is the same system clock
|
|
17
|
+
* everywhere, this keeps `animate-spin` elements visually synchronized across the whole page with
|
|
18
|
+
* no message-passing required.
|
|
19
|
+
*
|
|
20
|
+
* The sample is taken in a layout effect rather than during render: React's render phase can be
|
|
21
|
+
* time-sliced across several elements (a large table/page mount, a Suspense-covered subtree,
|
|
22
|
+
* etc.), so elements rendered in the same commit could otherwise sample the clock many
|
|
23
|
+
* milliseconds apart and end up visibly out of phase with each other. Layout effects for a given
|
|
24
|
+
* commit all run together, synchronously, right after the DOM mutations land and before paint —
|
|
25
|
+
* so every element written to in the same commit gets, for practical purposes, the same
|
|
26
|
+
* timestamp.
|
|
27
|
+
*
|
|
28
|
+
* The delay is written once, on mount, and then held fixed for the life of the element (the
|
|
29
|
+
* effect only re-runs if `ref` or `durationMs` change identity, which does not happen for a
|
|
30
|
+
* stable ref and a constant duration). Recomputing it on every render would be actively harmful:
|
|
31
|
+
* once a CSS animation is running, changing `animation-delay` does not re-anchor its phase to the
|
|
32
|
+
* new value — it recombines the new delay with the animation's own already-elapsed local clock,
|
|
33
|
+
* which desyncs the element instead of correcting it.
|
|
34
|
+
*
|
|
35
|
+
* Known trade-off: an element that is `display: none` at mount (a collapsed panel, inactive tab)
|
|
36
|
+
* doesn't have a running animation yet, so its delay is anchored to mount time rather than to
|
|
37
|
+
* when the animation actually starts once shown, and it does not resync after a
|
|
38
|
+
* backgrounded tab/iframe throttles the animation and later resumes it — both are acceptable
|
|
39
|
+
* trade-offs for a short-lived, purely decorative loading spinner.
|
|
40
|
+
*
|
|
41
|
+
* @param {RefObject<HTMLElement | null>} ref Ref to the element with the `animate-spin` animation.
|
|
42
|
+
* @param {number} durationMs Duration of one full spin cycle, in milliseconds, at mount time.
|
|
43
|
+
* Must match the animation's own `animation-duration`. Defaults to 1000ms, matching
|
|
44
|
+
* `Spinner`'s `animate-spin` usage.
|
|
45
|
+
*/
|
|
46
|
+
export declare const useSyncedSpinDelay: (ref: RefObject<HTMLElement | null>, durationMs?: number) => void;
|