@paul-portfolio/react 0.4.2 → 0.4.4
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/Badge.d.ts +3 -1
- package/dist/Badge.js +2 -2
- package/dist/Ticker.d.ts +27 -0
- package/dist/Ticker.js +107 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/dist/Badge.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
type BadgeProps = {
|
|
3
3
|
variant?: 'success' | 'warning' | 'error' | 'info';
|
|
4
4
|
dot?: boolean;
|
|
5
|
+
/** Renders the badge as a spiky starburst seal (for "new"/"beta" flags). */
|
|
6
|
+
starburst?: boolean;
|
|
5
7
|
children?: ReactNode;
|
|
6
8
|
};
|
|
7
|
-
export declare function Badge({ variant, dot, children }: BadgeProps): import("react").JSX.Element;
|
|
9
|
+
export declare function Badge({ variant, dot, starburst, children }: BadgeProps): import("react").JSX.Element;
|
|
8
10
|
export {};
|
package/dist/Badge.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from './cx';
|
|
3
|
-
export function Badge({ variant, dot, children }) {
|
|
4
|
-
return (_jsx("span", { className: cx('badge', variant && `badge--${variant}`, dot && 'badge--dot'), children: children }));
|
|
3
|
+
export function Badge({ variant, dot, starburst, children }) {
|
|
4
|
+
return (_jsx("span", { className: cx('badge', variant && `badge--${variant}`, dot && 'badge--dot', starburst && 'badge--starburst'), children: children }));
|
|
5
5
|
}
|
package/dist/Ticker.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
type TickerProps = {
|
|
3
|
+
/** Accessible name for the strip. Scroll mode renders a labelled region. */
|
|
4
|
+
label: string;
|
|
5
|
+
/**
|
|
6
|
+
* `scroll` (default) is an accessible, real scroll container with an ambient
|
|
7
|
+
* auto-scroll — every item stays reachable. `marquee` is a decorative,
|
|
8
|
+
* aria-hidden CSS loop for pure flavour.
|
|
9
|
+
*/
|
|
10
|
+
mode?: 'scroll' | 'marquee';
|
|
11
|
+
/** Which edge the strip sits on; picks the border side. */
|
|
12
|
+
edge?: 'top' | 'bottom';
|
|
13
|
+
/** Which way the ambient motion travels. */
|
|
14
|
+
direction?: 'left' | 'right';
|
|
15
|
+
/** Ambient auto-scroll speed for scroll mode, in px/sec. */
|
|
16
|
+
speed?: number;
|
|
17
|
+
className?: string;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A horizontal ticker strip with two modes: an accessible, auto-scrolling
|
|
22
|
+
* container (`scroll`) and a decorative CSS marquee (`marquee`). Both loop
|
|
23
|
+
* seamlessly and both honour prefers-reduced-motion. Content is passed as
|
|
24
|
+
* children, so the strip stays content-agnostic.
|
|
25
|
+
*/
|
|
26
|
+
export declare function Ticker(props: TickerProps): import("react").JSX.Element;
|
|
27
|
+
export {};
|
package/dist/Ticker.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { cx } from './cx';
|
|
4
|
+
/**
|
|
5
|
+
* Tracks prefers-reduced-motion. Defaults to false so the server and the first
|
|
6
|
+
* client render agree (SSR-stable), then updates once mounted.
|
|
7
|
+
*/
|
|
8
|
+
function usePrefersReducedMotion() {
|
|
9
|
+
const [reduced, setReduced] = useState(false);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (typeof window === 'undefined' ||
|
|
12
|
+
typeof window.matchMedia !== 'function') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
16
|
+
// Microtask defer keeps the effect from setting state synchronously.
|
|
17
|
+
queueMicrotask(() => setReduced(query.matches));
|
|
18
|
+
const onChange = (e) => setReduced(e.matches);
|
|
19
|
+
query.addEventListener('change', onChange);
|
|
20
|
+
return () => query.removeEventListener('change', onChange);
|
|
21
|
+
}, []);
|
|
22
|
+
return reduced;
|
|
23
|
+
}
|
|
24
|
+
/** How long a touch keeps the strip frozen before the ambient scroll resumes. */
|
|
25
|
+
const TOUCH_RESUME_MS = 4000;
|
|
26
|
+
/**
|
|
27
|
+
* A horizontal ticker strip with two modes: an accessible, auto-scrolling
|
|
28
|
+
* container (`scroll`) and a decorative CSS marquee (`marquee`). Both loop
|
|
29
|
+
* seamlessly and both honour prefers-reduced-motion. Content is passed as
|
|
30
|
+
* children, so the strip stays content-agnostic.
|
|
31
|
+
*/
|
|
32
|
+
export function Ticker(props) {
|
|
33
|
+
return props.mode === 'marquee' ? (_jsx(MarqueeTicker, { ...props })) : (_jsx(ScrollTicker, { ...props }));
|
|
34
|
+
}
|
|
35
|
+
function edgeClassFor(edge) {
|
|
36
|
+
return edge === 'top' ? 'ticker--top' : 'ticker--bottom';
|
|
37
|
+
}
|
|
38
|
+
/** Decorative marquee: aria-hidden, CSS-driven, content duplicated for the loop. */
|
|
39
|
+
function MarqueeTicker({ edge = 'top', direction = 'left', className, children, }) {
|
|
40
|
+
return (_jsx("div", { "aria-hidden": "true", "data-direction": direction, className: cx('ticker', 'ticker--marquee', edgeClassFor(edge), className), children: _jsxs("div", { className: "ticker__track", children: [_jsx("div", { className: "ticker__group", children: children }), _jsx("div", { className: "ticker__group", children: children })] }) }));
|
|
41
|
+
}
|
|
42
|
+
/** Accessible scroll container with an ambient JS auto-scroll loop. */
|
|
43
|
+
function ScrollTicker({ label, edge = 'top', direction = 'left', speed = 40, className, children, }) {
|
|
44
|
+
const reduced = usePrefersReducedMotion();
|
|
45
|
+
const scrollerRef = useRef(null);
|
|
46
|
+
const cloneRef = useRef(null);
|
|
47
|
+
const [paused, setPaused] = useState(false);
|
|
48
|
+
// Mirror `paused` into a ref so the animation-frame loop reads the latest
|
|
49
|
+
// value without the scroll effect re-subscribing on every toggle.
|
|
50
|
+
const pausedRef = useRef(false);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
pausedRef.current = paused;
|
|
53
|
+
}, [paused]);
|
|
54
|
+
// The clone fills the trailing half of the loop and stays clickable for
|
|
55
|
+
// pointer users, so drop its focusables out of the tab order by hand. Paired
|
|
56
|
+
// with aria-hidden, screen readers and axe never see the duplicate.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const focusables = cloneRef.current?.querySelectorAll('a[href], button, input, select, textarea, [tabindex]');
|
|
59
|
+
focusables?.forEach((el) => {
|
|
60
|
+
el.tabIndex = -1;
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
// Ambient scroll: advance scrollLeft each frame and wrap by one copy width
|
|
64
|
+
// for a seamless loop. Pauses on hover/touch; user scrolling is left alone.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (reduced)
|
|
67
|
+
return;
|
|
68
|
+
const el = scrollerRef.current;
|
|
69
|
+
if (!el)
|
|
70
|
+
return;
|
|
71
|
+
const dir = direction === 'left' ? 1 : -1;
|
|
72
|
+
// Start the rightward strip one copy in, so it has somewhere to scroll back.
|
|
73
|
+
if (dir < 0)
|
|
74
|
+
el.scrollLeft = el.scrollWidth / 2;
|
|
75
|
+
let raf = 0;
|
|
76
|
+
let last = performance.now();
|
|
77
|
+
const step = (now) => {
|
|
78
|
+
const dt = now - last;
|
|
79
|
+
last = now;
|
|
80
|
+
const half = el.scrollWidth / 2;
|
|
81
|
+
if (!pausedRef.current && half > 0) {
|
|
82
|
+
let next = el.scrollLeft + (dir * speed * dt) / 1000;
|
|
83
|
+
if (next >= half)
|
|
84
|
+
next -= half;
|
|
85
|
+
else if (next < 0)
|
|
86
|
+
next += half;
|
|
87
|
+
el.scrollLeft = next;
|
|
88
|
+
}
|
|
89
|
+
raf = requestAnimationFrame(step);
|
|
90
|
+
};
|
|
91
|
+
raf = requestAnimationFrame(step);
|
|
92
|
+
return () => cancelAnimationFrame(raf);
|
|
93
|
+
}, [reduced, direction, speed]);
|
|
94
|
+
const resumeTimer = useRef(null);
|
|
95
|
+
const freezeForTouch = () => {
|
|
96
|
+
setPaused(true);
|
|
97
|
+
if (resumeTimer.current)
|
|
98
|
+
clearTimeout(resumeTimer.current);
|
|
99
|
+
resumeTimer.current = setTimeout(() => setPaused(false), TOUCH_RESUME_MS);
|
|
100
|
+
};
|
|
101
|
+
const classes = cx('ticker', edgeClassFor(edge), className);
|
|
102
|
+
// Reduced motion: a plain, single-copy scrollable row. No clone, no loop.
|
|
103
|
+
if (reduced) {
|
|
104
|
+
return (_jsx("section", { "aria-label": label, className: classes, children: _jsx("div", { className: "ticker__group", children: children }) }));
|
|
105
|
+
}
|
|
106
|
+
return (_jsx("section", { ref: scrollerRef, "aria-label": label, "data-direction": direction, className: classes, onMouseEnter: () => setPaused(true), onMouseLeave: () => setPaused(false), onTouchStart: freezeForTouch, children: _jsxs("div", { className: "ticker__track", "data-paused": paused || undefined, children: [_jsx("div", { className: "ticker__group", children: children }), _jsx("div", { ref: cloneRef, "aria-hidden": "true", className: "ticker__group", children: children })] }) }));
|
|
107
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,5 +15,6 @@ export { Badge } from './Badge';
|
|
|
15
15
|
export { Skeleton } from './Skeleton';
|
|
16
16
|
export { Spinner } from './Spinner';
|
|
17
17
|
export { Divider } from './Divider';
|
|
18
|
+
export { Ticker } from './Ticker';
|
|
18
19
|
export { VisuallyHidden } from './VisuallyHidden';
|
|
19
20
|
export { cx } from './cx';
|
package/dist/index.js
CHANGED
|
@@ -15,5 +15,6 @@ export { Badge } from './Badge';
|
|
|
15
15
|
export { Skeleton } from './Skeleton';
|
|
16
16
|
export { Spinner } from './Spinner';
|
|
17
17
|
export { Divider } from './Divider';
|
|
18
|
+
export { Ticker } from './Ticker';
|
|
18
19
|
export { VisuallyHidden } from './VisuallyHidden';
|
|
19
20
|
export { cx } from './cx';
|