@nordwerk/scroll-carousel 0.1.3 → 0.1.5
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/AGENTS.md +5 -66
- package/CHANGELOG.md +18 -0
- package/README.md +6 -10
- package/dist/autoplay.d.ts +13 -11
- package/dist/autoplay.js +65 -66
- package/dist/carousel.css +4 -3
- package/dist/carousel.layer.css +4 -3
- package/dist/drag.d.ts +5 -3
- package/dist/drag.js +71 -78
- package/dist/index.d.ts +2 -95
- package/dist/index.js +391 -8
- package/dist/markup.d.ts +2 -51
- package/dist/markup.js +79 -13
- package/dist/preact.d.ts +11 -8
- package/dist/preact.js +13 -21
- package/dist/react.d.ts +11 -8
- package/dist/react.js +14 -22
- package/dist/shared/adapter-D4APPfnz.d.ts +36 -0
- package/dist/shared/adapter-DhmQTrqX.js +137 -0
- package/dist/shared/index-D1Np3wzP.d.ts +120 -0
- package/dist/shared/markup-CM54igQ_.d.ts +54 -0
- package/dist/shared/math-DhvRXjtu.js +73 -0
- package/package.json +58 -53
- package/dist/adapter.d.ts +0 -55
- package/dist/math.d.ts +0 -45
- package/dist/shared/chunk-3THCBXDW.js +0 -78
- package/dist/shared/chunk-HPGJKTV5.js +0 -377
- package/dist/shared/chunk-I3QEUR7L.js +0 -109
- package/dist/shared/chunk-QHWWA243.js +0 -61
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { n as CarouselOptions, r as CarouselState, s as Plugin, t as Carousel } from "./index-D1Np3wzP.js";
|
|
2
|
+
import { t as LayoutProps } from "./markup-CM54igQ_.js";
|
|
3
|
+
//#region src/adapter.d.ts
|
|
4
|
+
interface CarouselProps extends LayoutProps {
|
|
5
|
+
/** Accessible name of the scrolling list. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Element of the track. With 'ul' every child is wrapped in <li>, otherwise in <div>. */
|
|
8
|
+
as?: 'ul' | 'div';
|
|
9
|
+
children?: unknown;
|
|
10
|
+
className?: string;
|
|
11
|
+
style?: Record<string, string | number>;
|
|
12
|
+
slideClassName?: string;
|
|
13
|
+
/** Mark slides as groups with aria-roledescription="slide" and "n of count" labels. */
|
|
14
|
+
slideRoles?: boolean;
|
|
15
|
+
/** Keep a short row in the middle instead of at the start. */
|
|
16
|
+
centerFew?: boolean;
|
|
17
|
+
/** Default arrows. false renders none, e.g. when the host brings its own. */
|
|
18
|
+
arrows?: boolean;
|
|
19
|
+
/** Default dots. */
|
|
20
|
+
dots?: boolean;
|
|
21
|
+
/** Render a play/pause button for the autoplay plugin. */
|
|
22
|
+
playButton?: boolean;
|
|
23
|
+
labels?: CarouselOptions['labels'] & {
|
|
24
|
+
prev?: string;
|
|
25
|
+
next?: string;
|
|
26
|
+
};
|
|
27
|
+
initial?: number;
|
|
28
|
+
rewind?: CarouselOptions['rewind'];
|
|
29
|
+
group?: LayoutProps['group'];
|
|
30
|
+
plugins?: Plugin[];
|
|
31
|
+
onChange?: (state: CarouselState) => void;
|
|
32
|
+
/** Receives the carousel after attaching, and null after it is destroyed. */
|
|
33
|
+
carouselRef?: (carousel: Carousel | null) => void;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { CarouselProps as t };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { attach } from "../index.js";
|
|
2
|
+
import { PRE_POSITION, baseVars, responsiveCss } from "../markup.js";
|
|
3
|
+
//#region src/adapter.ts
|
|
4
|
+
const chevron = (h, path) => h("svg", {
|
|
5
|
+
viewBox: "0 0 24 24",
|
|
6
|
+
"aria-hidden": "true",
|
|
7
|
+
focusable: "false"
|
|
8
|
+
}, h("path", { d: path }));
|
|
9
|
+
function createAdapter(framework) {
|
|
10
|
+
const { createElement: h, Fragment, useState, useRef, useEffect, useLayoutEffect, useId, toChildArray } = framework;
|
|
11
|
+
const useBeforePaint = typeof window == "undefined" ? useEffect : useLayoutEffect;
|
|
12
|
+
/**
|
|
13
|
+
* Attach a carousel to your own markup. Put `ref` on the root. Options are read once, when
|
|
14
|
+
* the root mounts; give the component a new `key` to attach again with other options.
|
|
15
|
+
*/
|
|
16
|
+
function useCarousel(options = {}) {
|
|
17
|
+
const ref = useRef(null);
|
|
18
|
+
const latest = useRef(options);
|
|
19
|
+
useBeforePaint(() => {
|
|
20
|
+
latest.current = options;
|
|
21
|
+
});
|
|
22
|
+
const [carousel, setCarousel] = useState(null);
|
|
23
|
+
const [state, setState] = useState(null);
|
|
24
|
+
useBeforePaint(() => {
|
|
25
|
+
if (!ref.current) return;
|
|
26
|
+
const instance = attach(ref.current, {
|
|
27
|
+
...latest.current,
|
|
28
|
+
onChange: (next) => {
|
|
29
|
+
setState(next);
|
|
30
|
+
latest.current.onChange?.(next);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
setCarousel(instance);
|
|
34
|
+
setState(instance.state);
|
|
35
|
+
return () => {
|
|
36
|
+
instance.destroy();
|
|
37
|
+
setCarousel(null);
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
return {
|
|
41
|
+
ref,
|
|
42
|
+
carousel,
|
|
43
|
+
state
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function Carousel(props) {
|
|
47
|
+
const id = useId();
|
|
48
|
+
const { as = "div", label, children, className, style, slideClassName, slideRoles, centerFew } = props;
|
|
49
|
+
const { arrows = true, dots = true, playButton, labels = {}, initial = 0, carouselRef } = props;
|
|
50
|
+
const { ref, carousel } = useCarousel({
|
|
51
|
+
group: typeof props.group == "object" ? void 0 : props.group,
|
|
52
|
+
initial,
|
|
53
|
+
rewind: props.rewind,
|
|
54
|
+
labels,
|
|
55
|
+
plugins: props.plugins,
|
|
56
|
+
onChange: props.onChange
|
|
57
|
+
});
|
|
58
|
+
const handoff = useRef(carouselRef);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
handoff.current = carouselRef;
|
|
61
|
+
});
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!carousel) return;
|
|
64
|
+
handoff.current?.(carousel);
|
|
65
|
+
return () => handoff.current?.(null);
|
|
66
|
+
}, [carousel]);
|
|
67
|
+
const slides = toChildArray(children);
|
|
68
|
+
const item = as == "ul" ? "li" : "div";
|
|
69
|
+
const css = responsiveCss(id, props);
|
|
70
|
+
const root = h("div", {
|
|
71
|
+
ref,
|
|
72
|
+
className: [
|
|
73
|
+
"sc",
|
|
74
|
+
centerFew && "sc--center-few",
|
|
75
|
+
className
|
|
76
|
+
].filter(Boolean).join(" "),
|
|
77
|
+
"data-sc-id": id,
|
|
78
|
+
style: {
|
|
79
|
+
...baseVars(props),
|
|
80
|
+
...style
|
|
81
|
+
}
|
|
82
|
+
}, css && h("style", { dangerouslySetInnerHTML: { __html: css } }), h(as, {
|
|
83
|
+
className: "sc-track",
|
|
84
|
+
"data-sc-track": "",
|
|
85
|
+
tabIndex: 0,
|
|
86
|
+
"aria-label": label
|
|
87
|
+
}, slides.map((child, i) => h(item, {
|
|
88
|
+
key: child?.key ?? i,
|
|
89
|
+
className: slideClassName,
|
|
90
|
+
"data-sc-initial": i == initial && i > 0 ? "" : void 0,
|
|
91
|
+
...slideRoles && {
|
|
92
|
+
role: "group",
|
|
93
|
+
"aria-roledescription": "slide",
|
|
94
|
+
"aria-label": `${i + 1} of ${slides.length}`
|
|
95
|
+
}
|
|
96
|
+
}, child))), arrows && h(Fragment, null, h("button", {
|
|
97
|
+
type: "button",
|
|
98
|
+
className: "sc-nav sc-prev",
|
|
99
|
+
"data-sc-prev": "",
|
|
100
|
+
"aria-label": labels.prev || "Previous"
|
|
101
|
+
}, chevron(h, "m15 18-6-6 6-6")), h("button", {
|
|
102
|
+
type: "button",
|
|
103
|
+
className: "sc-nav sc-next",
|
|
104
|
+
"data-sc-next": "",
|
|
105
|
+
"aria-label": labels.next || "Next"
|
|
106
|
+
}, chevron(h, "m9 6 6 6-6 6"))), playButton && h("button", {
|
|
107
|
+
type: "button",
|
|
108
|
+
className: "sc-play",
|
|
109
|
+
"data-sc-play": "",
|
|
110
|
+
"aria-label": "Stop automatic scrolling"
|
|
111
|
+
}, h("svg", {
|
|
112
|
+
className: "sc-icon-play",
|
|
113
|
+
viewBox: "0 0 24 24",
|
|
114
|
+
"aria-hidden": "true",
|
|
115
|
+
focusable: "false"
|
|
116
|
+
}, h("path", { d: "M8 5v14l11-7z" })), h("svg", {
|
|
117
|
+
className: "sc-icon-pause",
|
|
118
|
+
viewBox: "0 0 24 24",
|
|
119
|
+
"aria-hidden": "true",
|
|
120
|
+
focusable: "false"
|
|
121
|
+
}, h("path", { d: "M7 5h3.5v14H7zM13.5 5H17v14h-3.5z" }))), dots && h("div", {
|
|
122
|
+
className: "sc-dots",
|
|
123
|
+
"data-sc-dots": ""
|
|
124
|
+
}), h("p", {
|
|
125
|
+
className: "sc-status",
|
|
126
|
+
"data-sc-status": "",
|
|
127
|
+
"aria-live": "polite"
|
|
128
|
+
}));
|
|
129
|
+
return initial > 0 ? h(Fragment, null, root, h("script", { dangerouslySetInnerHTML: { __html: PRE_POSITION } })) : root;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
Carousel,
|
|
133
|
+
useCarousel
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { createAdapter as t };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
//#region src/math.d.ts
|
|
2
|
+
type Align = 'start' | 'center' | 'end';
|
|
3
|
+
type Group = number | 'page';
|
|
4
|
+
interface Geometry {
|
|
5
|
+
/** Start of each slide in scroll-content coordinates, px. */
|
|
6
|
+
starts: number[];
|
|
7
|
+
/** Inline size of each slide, px. */
|
|
8
|
+
sizes: number[];
|
|
9
|
+
/** Width of the scrollport, px. */
|
|
10
|
+
port: number;
|
|
11
|
+
/** Scroll padding at the start and the end, px. */
|
|
12
|
+
padStart: number;
|
|
13
|
+
padEnd: number;
|
|
14
|
+
/** Largest scroll position, px. */
|
|
15
|
+
max: number;
|
|
16
|
+
align: Align;
|
|
17
|
+
}
|
|
18
|
+
interface Page {
|
|
19
|
+
/** Scroll position of the page. */
|
|
20
|
+
pos: number;
|
|
21
|
+
/** Index of the first slide of the page. */
|
|
22
|
+
first: number;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/index.d.ts
|
|
26
|
+
interface CarouselState {
|
|
27
|
+
/** The slide at the snap position, or the destination of a move in flight. */
|
|
28
|
+
index: number;
|
|
29
|
+
page: number;
|
|
30
|
+
pageCount: number;
|
|
31
|
+
isBeginning: boolean;
|
|
32
|
+
isEnd: boolean;
|
|
33
|
+
/** Whether the content overflows at all. Without overflow there are no controls. */
|
|
34
|
+
overflow: boolean;
|
|
35
|
+
}
|
|
36
|
+
interface Labels {
|
|
37
|
+
/** Accessible name of a default dot. */
|
|
38
|
+
page: (n: number, count: number) => string;
|
|
39
|
+
/** Live-region text after a settled move, from the first and last visible slide (1-based). */
|
|
40
|
+
status: (first: number, last: number, count: number, slides: HTMLElement[]) => string;
|
|
41
|
+
}
|
|
42
|
+
interface CarouselOptions {
|
|
43
|
+
/** Slides per step of next and previous: a number or 'page'. Default: --sc-group, then 1. */
|
|
44
|
+
group?: Group;
|
|
45
|
+
/** Initial slide. Default: the slide with [data-sc-initial], then 0. */
|
|
46
|
+
initial?: number;
|
|
47
|
+
/** Next at the end goes to the start and the reverse. true fades, 'scroll' scrolls back. */
|
|
48
|
+
rewind?: boolean | 'fade' | 'scroll';
|
|
49
|
+
labels?: Partial<Labels>;
|
|
50
|
+
/** Called after every settled move that changed the state, like the sc:change event. */
|
|
51
|
+
onChange?: (state: CarouselState) => void;
|
|
52
|
+
/** Default: [data-sc-prev], [data-sc-next], [data-sc-dots], [data-sc-status] inside the root. */
|
|
53
|
+
prev?: HTMLElement | null;
|
|
54
|
+
next?: HTMLElement | null;
|
|
55
|
+
dots?: HTMLElement | null;
|
|
56
|
+
status?: HTMLElement | null;
|
|
57
|
+
/** Opt-in behaviour, e.g. drag() and autoplay(). */
|
|
58
|
+
plugins?: Plugin[];
|
|
59
|
+
}
|
|
60
|
+
interface MoveOptions {
|
|
61
|
+
/** Jump instead of scrolling smoothly. */
|
|
62
|
+
instant?: boolean;
|
|
63
|
+
}
|
|
64
|
+
interface Carousel {
|
|
65
|
+
readonly root: HTMLElement;
|
|
66
|
+
readonly track: HTMLElement;
|
|
67
|
+
readonly slides: HTMLElement[];
|
|
68
|
+
readonly state: CarouselState;
|
|
69
|
+
readonly index: number;
|
|
70
|
+
readonly page: number;
|
|
71
|
+
readonly pageCount: number;
|
|
72
|
+
readonly isBeginning: boolean;
|
|
73
|
+
readonly isEnd: boolean;
|
|
74
|
+
next(): void;
|
|
75
|
+
prev(): void;
|
|
76
|
+
slideTo(index: number, options?: MoveOptions): void;
|
|
77
|
+
goToPage(page: number, options?: MoveOptions): void;
|
|
78
|
+
/** Measure again, e.g. after changing the direction or a custom property from script. */
|
|
79
|
+
update(): void;
|
|
80
|
+
destroy(): void;
|
|
81
|
+
/** Present when the autoplay plugin is attached. */
|
|
82
|
+
play?(): void;
|
|
83
|
+
pause?(): void;
|
|
84
|
+
}
|
|
85
|
+
interface Layout {
|
|
86
|
+
pages: Page[];
|
|
87
|
+
max: number;
|
|
88
|
+
rtl: boolean;
|
|
89
|
+
snap: 'mandatory' | 'proximity' | 'none';
|
|
90
|
+
state: CarouselState;
|
|
91
|
+
}
|
|
92
|
+
/** What a plugin gets to work with. Listeners added through `listen` end with destroy(). */
|
|
93
|
+
interface PluginContext {
|
|
94
|
+
root: HTMLElement;
|
|
95
|
+
track: HTMLElement;
|
|
96
|
+
listen(target: EventTarget | null | undefined, type: string, handler: (event: any) => void, options?: AddEventListenerOptions): void;
|
|
97
|
+
layout(): Layout;
|
|
98
|
+
toPage(page: number): void;
|
|
99
|
+
/** Scroll to a position, e.g. where a drag's momentum ends. */
|
|
100
|
+
scrollTo(pos: number): void;
|
|
101
|
+
/** Next or previous page; a quiet step is not announced in the live region. */
|
|
102
|
+
step(direction: 1 | -1, wrap: boolean, quiet?: boolean): void;
|
|
103
|
+
/** The user took hold of the track: a programmatic move in flight is abandoned. */
|
|
104
|
+
grab(): void;
|
|
105
|
+
/** While held, scrolling never counts as settled, e.g. during a drag. */
|
|
106
|
+
hold(on: boolean): void;
|
|
107
|
+
/** control: the user took over. settle: a move came to rest. measure: layout was read. */
|
|
108
|
+
on(event: 'control' | 'settle' | 'measure', handler: () => void): void;
|
|
109
|
+
}
|
|
110
|
+
type Plugin = (context: PluginContext) => {
|
|
111
|
+
destroy?(): void;
|
|
112
|
+
play?(): void;
|
|
113
|
+
pause?(): void;
|
|
114
|
+
} | void;
|
|
115
|
+
/** The carousel attached to `root`, if any. */
|
|
116
|
+
declare const getCarousel: (root: HTMLElement) => Carousel | undefined;
|
|
117
|
+
/** Attach to server-rendered markup. Attaching twice returns the same carousel. */
|
|
118
|
+
declare function attach(root: HTMLElement, options?: CarouselOptions): Carousel;
|
|
119
|
+
//#endregion
|
|
120
|
+
export { Layout as a, PluginContext as c, Align as d, Geometry as f, Labels as i, attach as l, Page as m, CarouselOptions as n, MoveOptions as o, Group as p, CarouselState as r, Plugin as s, Carousel as t, getCarousel as u };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/markup.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A value, or values by minimum container width in px, e.g. { 0: 2, 600: 3, 900: 4 }.
|
|
4
|
+
* Widths are container widths: the carousel root is the query container.
|
|
5
|
+
*/
|
|
6
|
+
type Responsive<T> = T | Record<number, T>;
|
|
7
|
+
interface LayoutProps {
|
|
8
|
+
/** Slides per view; fractions show part of the next slide. */
|
|
9
|
+
perView?: Responsive<number>;
|
|
10
|
+
/** A fixed slide size such as '18rem', or 'auto' for the content width. Wins over perView. */
|
|
11
|
+
slideSize?: Responsive<string>;
|
|
12
|
+
/** Numbers are px. */
|
|
13
|
+
gap?: Responsive<number | string>;
|
|
14
|
+
offsetBefore?: Responsive<number | string>;
|
|
15
|
+
offsetAfter?: Responsive<number | string>;
|
|
16
|
+
snap?: Responsive<'mandatory' | 'proximity' | 'none'>;
|
|
17
|
+
align?: Responsive<'start' | 'center'>;
|
|
18
|
+
/** Pad both ends so the first and last slide can reach the centre. Needs perView. */
|
|
19
|
+
centered?: boolean;
|
|
20
|
+
/** Slides per step of next and previous. */
|
|
21
|
+
group?: Responsive<number | 'page'>;
|
|
22
|
+
/** false hides arrows, dots and the play button, e.g. for free mode on small containers. */
|
|
23
|
+
controls?: Responsive<boolean>;
|
|
24
|
+
}
|
|
25
|
+
/** Custom properties for the root's style attribute: plain values and the 0 breakpoint. */
|
|
26
|
+
declare function baseVars(props: LayoutProps): Record<string, string>;
|
|
27
|
+
/** The same as a CSS declaration string, for templating languages. */
|
|
28
|
+
declare const baseStyle: (props: LayoutProps) => string;
|
|
29
|
+
/**
|
|
30
|
+
* Container-query rules for responsive values, scoped to [data-sc-id="id"]. Empty when nothing
|
|
31
|
+
* is responsive. Render it in a <style> element inside the root.
|
|
32
|
+
*/
|
|
33
|
+
declare function responsiveCss(id: string, props: LayoutProps): string;
|
|
34
|
+
/**
|
|
35
|
+
* Inline script to render immediately after a carousel that opens on a later slide. It sets the
|
|
36
|
+
* start position before the first paint, so a deferred or module script finds it in place.
|
|
37
|
+
* Start alignment only. It expects the root as its previous sibling.
|
|
38
|
+
*/
|
|
39
|
+
declare const PRE_POSITION: string;
|
|
40
|
+
/**
|
|
41
|
+
* Label templates such as 'Slide {n} of {count}' turned into label functions. `statusSingle`
|
|
42
|
+
* is used when only one slide is visible, e.g. 'Item {first} of {count}' next to a `status` of
|
|
43
|
+
* 'Items {first} to {last} of {count}'.
|
|
44
|
+
*/
|
|
45
|
+
declare function templateLabels(templates?: {
|
|
46
|
+
page?: string;
|
|
47
|
+
status?: string;
|
|
48
|
+
statusSingle?: string;
|
|
49
|
+
}): {
|
|
50
|
+
page?: ((n: number, count: number) => string) | undefined;
|
|
51
|
+
status?: ((first: number, last: number, count: number) => string) | undefined;
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { baseVars as a, baseStyle as i, PRE_POSITION as n, responsiveCss as o, Responsive as r, templateLabels as s, LayoutProps as t };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
2
|
+
/** Index of the value closest to `target`; the earliest one wins a tie. */
|
|
3
|
+
const closest = (values, target) => {
|
|
4
|
+
let best = 0;
|
|
5
|
+
for (let i = 1; i < values.length; i++) if (Math.abs(values[i] - target) < Math.abs(values[best] - target) - .5) best = i;
|
|
6
|
+
return best;
|
|
7
|
+
};
|
|
8
|
+
/** The scroll position at which each slide is aligned, clamped to the scroll range. */
|
|
9
|
+
const snapPositions = ({ starts, sizes, port, padStart, padEnd, max, align }) => starts.map((start, i) => clamp(align === "center" ? start + sizes[i] / 2 - padStart - (port - padStart - padEnd) / 2 : align === "end" ? start + sizes[i] - port + padEnd : start - padStart, 0, max));
|
|
10
|
+
/**
|
|
11
|
+
* The first slide of every page. Pages are counted outwards from `anchor` (the initial slide),
|
|
12
|
+
* so the initial slide always starts a page and the first page may be shorter than the rest.
|
|
13
|
+
* `group` is a number of slides, or 'page' for as many whole slides as fit in the view.
|
|
14
|
+
*/
|
|
15
|
+
const pageStarts = (geo, snaps, group, anchor) => {
|
|
16
|
+
const count = snaps.length;
|
|
17
|
+
const firsts = /* @__PURE__ */ new Set([0, anchor]);
|
|
18
|
+
if (group === "page") {
|
|
19
|
+
const fits = (from, last) => geo.starts[last] + geo.sizes[last] <= snaps[from] + geo.port - geo.padEnd + 2;
|
|
20
|
+
for (let i = anchor; i < count && snaps[i] < geo.max - 2;) {
|
|
21
|
+
let next = i + 1;
|
|
22
|
+
while (next < count && fits(i, next)) next++;
|
|
23
|
+
if (next >= count) break;
|
|
24
|
+
firsts.add(i = next);
|
|
25
|
+
}
|
|
26
|
+
for (let i = anchor; i > 0;) {
|
|
27
|
+
let first = i - 1;
|
|
28
|
+
while (first > 0 && fits(first - 1, i - 1)) first--;
|
|
29
|
+
firsts.add(i = first);
|
|
30
|
+
}
|
|
31
|
+
} else for (let i = anchor % group; i < count; i += group) firsts.add(i);
|
|
32
|
+
return [...firsts].filter((i) => i >= 0 && i < count).sort((a, b) => a - b);
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Pages at distinct positions, always ending at the end of the scroll range. A page's `first` is
|
|
36
|
+
* the earliest slide aligned at its position, which is also the index reported there: at the
|
|
37
|
+
* clamped end that is the first slide of the last full view, not the start of the last group.
|
|
38
|
+
*/
|
|
39
|
+
const buildPages = (geo, snaps, group, anchor) => {
|
|
40
|
+
const pages = [];
|
|
41
|
+
const add = (pos) => {
|
|
42
|
+
const first = snaps.findIndex((snap) => snap >= pos - 2);
|
|
43
|
+
pages.push({
|
|
44
|
+
pos,
|
|
45
|
+
first: first < 0 ? snaps.length - 1 : first
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
for (const start of pageStarts(geo, snaps, group, anchor)) if (!pages.length || snaps[start] > pages[pages.length - 1].pos + 2) add(snaps[start]);
|
|
49
|
+
if (pages.length && pages[pages.length - 1].pos < geo.max - 2) add(geo.max);
|
|
50
|
+
return pages;
|
|
51
|
+
};
|
|
52
|
+
/** The page that contains slide `index`. */
|
|
53
|
+
const pageOf = (pages, index) => {
|
|
54
|
+
let page = 0;
|
|
55
|
+
pages.forEach((candidate, i) => {
|
|
56
|
+
if (candidate.first <= index) page = i;
|
|
57
|
+
});
|
|
58
|
+
return page;
|
|
59
|
+
};
|
|
60
|
+
/** First and last slide that are at least half visible at `pos`, or [-1, -1]. */
|
|
61
|
+
const visibleRange = (pos, { starts, sizes, port }) => {
|
|
62
|
+
let first = -1;
|
|
63
|
+
let last = -1;
|
|
64
|
+
starts.forEach((start, i) => {
|
|
65
|
+
if (Math.min(start + sizes[i], pos + port) - Math.max(start, pos) >= sizes[i] / 2 - .5) {
|
|
66
|
+
if (first < 0) first = i;
|
|
67
|
+
last = i;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return [first, last];
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
export { snapPositions as a, pageOf as i, clamp as n, visibleRange as o, closest as r, buildPages as t };
|
package/package.json
CHANGED
|
@@ -1,27 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nordwerk/scroll-carousel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A dependency-free carousel on native scrolling: CSS scroll snap first, arrows, dots, paging and an API as progressive enhancement.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/studio-nordwerk/scroll-carousel.git"
|
|
10
|
-
},
|
|
11
|
-
"homepage": "https://www.nordwerk.studio/oss/scroll-carousel",
|
|
12
5
|
"keywords": [
|
|
6
|
+
"accessibility",
|
|
7
|
+
"astro",
|
|
13
8
|
"carousel",
|
|
14
|
-
"
|
|
15
|
-
"scroll-snap",
|
|
9
|
+
"preact",
|
|
16
10
|
"progressive-enhancement",
|
|
17
|
-
"accessibility",
|
|
18
11
|
"react",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
12
|
+
"scroll-snap",
|
|
13
|
+
"slider"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://www.nordwerk.studio/oss/scroll-carousel",
|
|
16
|
+
"bugs": "https://github.com/studio-nordwerk/oss-ui/issues",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/studio-nordwerk/oss-ui.git",
|
|
21
|
+
"directory": "packages/scroll-carousel"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src/astro",
|
|
26
|
+
"AGENTS.md",
|
|
27
|
+
"CHANGELOG.md",
|
|
28
|
+
"docs/migration.md"
|
|
21
29
|
],
|
|
30
|
+
"type": "module",
|
|
22
31
|
"sideEffects": [
|
|
23
32
|
"*.css"
|
|
24
33
|
],
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"typesVersions": {
|
|
37
|
+
"*": {
|
|
38
|
+
"drag": [
|
|
39
|
+
"./dist/drag.d.ts"
|
|
40
|
+
],
|
|
41
|
+
"autoplay": [
|
|
42
|
+
"./dist/autoplay.d.ts"
|
|
43
|
+
],
|
|
44
|
+
"markup": [
|
|
45
|
+
"./dist/markup.d.ts"
|
|
46
|
+
],
|
|
47
|
+
"react": [
|
|
48
|
+
"./dist/react.d.ts"
|
|
49
|
+
],
|
|
50
|
+
"preact": [
|
|
51
|
+
"./dist/preact.d.ts"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
25
55
|
"exports": {
|
|
26
56
|
".": {
|
|
27
57
|
"types": "./dist/index.d.ts",
|
|
@@ -52,27 +82,24 @@
|
|
|
52
82
|
"./carousel.layer.css": "./dist/carousel.layer.css",
|
|
53
83
|
"./package.json": "./package.json"
|
|
54
84
|
},
|
|
55
|
-
"files": [
|
|
56
|
-
"dist",
|
|
57
|
-
"src/astro",
|
|
58
|
-
"AGENTS.md",
|
|
59
|
-
"CHANGELOG.md",
|
|
60
|
-
"docs/migration.md"
|
|
61
|
-
],
|
|
62
85
|
"publishConfig": {
|
|
63
86
|
"access": "public"
|
|
64
87
|
},
|
|
65
88
|
"scripts": {
|
|
66
|
-
"build": "
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
89
|
+
"build": "vp pack",
|
|
90
|
+
"prepublishOnly": "vp pack"
|
|
91
|
+
},
|
|
92
|
+
"devDependencies": {
|
|
93
|
+
"@base-ui/react": "^1.8.0",
|
|
94
|
+
"@types/react": "^19.3.0",
|
|
95
|
+
"@types/react-dom": "^19.3.0",
|
|
96
|
+
"astro": "^7.3.3",
|
|
97
|
+
"class-variance-authority": "^0.7.1",
|
|
98
|
+
"cn": "^0.3.2",
|
|
99
|
+
"preact": "^10.29.8",
|
|
100
|
+
"preact-render-to-string": "6.7.0",
|
|
101
|
+
"react": "^19.3.0",
|
|
102
|
+
"react-dom": "^19.3.0"
|
|
76
103
|
},
|
|
77
104
|
"peerDependencies": {
|
|
78
105
|
"preact": ">=10",
|
|
@@ -85,27 +112,5 @@
|
|
|
85
112
|
"react": {
|
|
86
113
|
"optional": true
|
|
87
114
|
}
|
|
88
|
-
}
|
|
89
|
-
"devDependencies": {
|
|
90
|
-
"@axe-core/playwright": "4.13.0",
|
|
91
|
-
"@fontsource-variable/geist": "^5.3.0",
|
|
92
|
-
"@playwright/test": "1.61.1",
|
|
93
|
-
"@tailwindcss/cli": "4.3.3",
|
|
94
|
-
"@types/node": "^24.0.0",
|
|
95
|
-
"@types/react": "^19.3.0",
|
|
96
|
-
"@types/react-dom": "^19.3.0",
|
|
97
|
-
"astro": "^7.3.3",
|
|
98
|
-
"esbuild": "0.28.2",
|
|
99
|
-
"preact": "^10.29.8",
|
|
100
|
-
"preact-render-to-string": "6.7.0",
|
|
101
|
-
"react": "^19.3.0",
|
|
102
|
-
"react-dom": "^19.3.0",
|
|
103
|
-
"shadcn": "4.21.0",
|
|
104
|
-
"tailwindcss": "4.3.3",
|
|
105
|
-
"typescript": "^7.0.2"
|
|
106
|
-
},
|
|
107
|
-
"engines": {
|
|
108
|
-
"node": ">=22"
|
|
109
|
-
},
|
|
110
|
-
"packageManager": "pnpm@12.5.1"
|
|
115
|
+
}
|
|
111
116
|
}
|
package/dist/adapter.d.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { type Carousel, type CarouselOptions, type CarouselState, type Plugin } from './index.js';
|
|
2
|
-
import { type LayoutProps } from './markup.js';
|
|
3
|
-
export interface CarouselProps extends LayoutProps {
|
|
4
|
-
/** Accessible name of the scrolling list. */
|
|
5
|
-
label: string;
|
|
6
|
-
/** Element of the track. With 'ul' every child is wrapped in <li>, otherwise in <div>. */
|
|
7
|
-
as?: 'ul' | 'div';
|
|
8
|
-
children?: unknown;
|
|
9
|
-
className?: string;
|
|
10
|
-
style?: Record<string, string | number>;
|
|
11
|
-
slideClassName?: string;
|
|
12
|
-
/** Mark slides as groups with aria-roledescription="slide" and "n of count" labels. */
|
|
13
|
-
slideRoles?: boolean;
|
|
14
|
-
/** Keep a short row in the middle instead of at the start. */
|
|
15
|
-
centerFew?: boolean;
|
|
16
|
-
/** Default arrows. false renders none, e.g. when the host brings its own. */
|
|
17
|
-
arrows?: boolean;
|
|
18
|
-
/** Default dots. */
|
|
19
|
-
dots?: boolean;
|
|
20
|
-
/** Render a play/pause button for the autoplay plugin. */
|
|
21
|
-
playButton?: boolean;
|
|
22
|
-
labels?: CarouselOptions['labels'] & {
|
|
23
|
-
prev?: string;
|
|
24
|
-
next?: string;
|
|
25
|
-
};
|
|
26
|
-
initial?: number;
|
|
27
|
-
rewind?: CarouselOptions['rewind'];
|
|
28
|
-
group?: LayoutProps['group'];
|
|
29
|
-
plugins?: Plugin[];
|
|
30
|
-
onChange?: (state: CarouselState) => void;
|
|
31
|
-
/** Receives the carousel after attaching, and null after it is destroyed. */
|
|
32
|
-
carouselRef?: (carousel: Carousel | null) => void;
|
|
33
|
-
}
|
|
34
|
-
export interface Framework {
|
|
35
|
-
createElement: (...args: any[]) => any;
|
|
36
|
-
Fragment: any;
|
|
37
|
-
useState: <T>(initial: T) => [T, (value: T) => void];
|
|
38
|
-
useRef: <T>(initial: T) => {
|
|
39
|
-
current: T;
|
|
40
|
-
};
|
|
41
|
-
useEffect: (effect: () => void | (() => void), deps?: unknown[]) => void;
|
|
42
|
-
useLayoutEffect: (effect: () => void | (() => void), deps?: unknown[]) => void;
|
|
43
|
-
useId: () => string;
|
|
44
|
-
toChildArray: (children: unknown) => any[];
|
|
45
|
-
}
|
|
46
|
-
export declare function createAdapter(framework: Framework): {
|
|
47
|
-
Carousel: (props: CarouselProps) => any;
|
|
48
|
-
useCarousel: (options?: CarouselOptions) => {
|
|
49
|
-
ref: {
|
|
50
|
-
current: HTMLElement | null;
|
|
51
|
-
};
|
|
52
|
-
carousel: Carousel | null;
|
|
53
|
-
state: CarouselState | null;
|
|
54
|
-
};
|
|
55
|
-
};
|
package/dist/math.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export type Align = 'start' | 'center' | 'end';
|
|
2
|
-
export type Group = number | 'page';
|
|
3
|
-
export interface Geometry {
|
|
4
|
-
/** Start of each slide in scroll-content coordinates, px. */
|
|
5
|
-
starts: number[];
|
|
6
|
-
/** Inline size of each slide, px. */
|
|
7
|
-
sizes: number[];
|
|
8
|
-
/** Width of the scrollport, px. */
|
|
9
|
-
port: number;
|
|
10
|
-
/** Scroll padding at the start and the end, px. */
|
|
11
|
-
padStart: number;
|
|
12
|
-
padEnd: number;
|
|
13
|
-
/** Largest scroll position, px. */
|
|
14
|
-
max: number;
|
|
15
|
-
align: Align;
|
|
16
|
-
}
|
|
17
|
-
export interface Page {
|
|
18
|
-
/** Scroll position of the page. */
|
|
19
|
-
pos: number;
|
|
20
|
-
/** Index of the first slide of the page. */
|
|
21
|
-
first: number;
|
|
22
|
-
}
|
|
23
|
-
/** Two positions closer than this, in px, are the same place. */
|
|
24
|
-
export declare const EPS = 2;
|
|
25
|
-
export declare const clamp: (value: number, min: number, max: number) => number;
|
|
26
|
-
/** Index of the value closest to `target`; the earliest one wins a tie. */
|
|
27
|
-
export declare const closest: (values: number[], target: number) => number;
|
|
28
|
-
/** The scroll position at which each slide is aligned, clamped to the scroll range. */
|
|
29
|
-
export declare const snapPositions: ({ starts, sizes, port, padStart, padEnd, max, align }: Geometry) => number[];
|
|
30
|
-
/**
|
|
31
|
-
* The first slide of every page. Pages are counted outwards from `anchor` (the initial slide),
|
|
32
|
-
* so the initial slide always starts a page and the first page may be shorter than the rest.
|
|
33
|
-
* `group` is a number of slides, or 'page' for as many whole slides as fit in the view.
|
|
34
|
-
*/
|
|
35
|
-
export declare const pageStarts: (geo: Geometry, snaps: number[], group: Group, anchor: number) => number[];
|
|
36
|
-
/**
|
|
37
|
-
* Pages at distinct positions, always ending at the end of the scroll range. A page's `first` is
|
|
38
|
-
* the earliest slide aligned at its position, which is also the index reported there: at the
|
|
39
|
-
* clamped end that is the first slide of the last full view, not the start of the last group.
|
|
40
|
-
*/
|
|
41
|
-
export declare const buildPages: (geo: Geometry, snaps: number[], group: Group, anchor: number) => Page[];
|
|
42
|
-
/** The page that contains slide `index`. */
|
|
43
|
-
export declare const pageOf: (pages: Page[], index: number) => number;
|
|
44
|
-
/** First and last slide that are at least half visible at `pos`, or [-1, -1]. */
|
|
45
|
-
export declare const visibleRange: (pos: number, { starts, sizes, port }: Geometry) => [number, number];
|