@octane-xplat/pager 0.7.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.
@@ -0,0 +1,83 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { act, createRoot } from 'octane';
4
+ import { Pager } from './Pager.web.tsrx';
5
+
6
+ // jsdom has no layout — clientWidth is always 0 — so the leaf's scroll math
7
+ // is exercised against a stubbed page width. scrollTo is unimplemented in
8
+ // jsdom; the stub records controlled-page writes.
9
+
10
+ const items = ['a', 'b', 'c'];
11
+ const renderItem = (item: string) => <span class="pg">{item}</span>;
12
+
13
+ function mount(extra: any = {}) {
14
+ const el = document.createElement('div');
15
+ document.body.appendChild(el);
16
+ const root = createRoot(el);
17
+ act(() => root.render(<Pager items={items} renderItem={renderItem} {...extra} />));
18
+ const pager = el.querySelector('.vx-pager') as any;
19
+ Object.defineProperty(pager, 'clientWidth', { value: 100, configurable: true });
20
+ pager.scrollTo = vi.fn((opts: any) => {
21
+ pager.scrollLeft = opts.left;
22
+ pager.dispatchEvent(new Event('scroll'));
23
+ });
24
+
25
+ return { el, root, pager };
26
+ }
27
+
28
+ describe('Pager (web leaf)', () => {
29
+ afterEach(() => {
30
+ vi.useRealTimers();
31
+ });
32
+
33
+ it('renders one snap page per item, plus renderEmpty', () => {
34
+ const { el, root, pager } = mount();
35
+ expect(pager.className).toContain('vx-pager');
36
+ expect(el.querySelectorAll('.vx-pager-page')).toHaveLength(3);
37
+ expect(el.querySelectorAll('.pg')).toHaveLength(3);
38
+ root.unmount();
39
+ el.remove();
40
+ });
41
+
42
+ it('fires onPageChange once the scroll settles', () => {
43
+ vi.useFakeTimers();
44
+ const onPageChange = vi.fn();
45
+ const { el, root, pager } = mount({ onPageChange });
46
+ pager.scrollLeft = 200;
47
+ pager.dispatchEvent(new Event('scroll'));
48
+ vi.advanceTimersByTime(200);
49
+ expect(onPageChange).toHaveBeenCalledTimes(1);
50
+ expect(onPageChange).toHaveBeenCalledWith(2);
51
+ root.unmount();
52
+ el.remove();
53
+ });
54
+
55
+ it('suppresses the echo of its own controlled writes', () => {
56
+ vi.useFakeTimers();
57
+ const onPageChange = vi.fn();
58
+ const el = document.createElement('div');
59
+ document.body.appendChild(el);
60
+ const root = createRoot(el);
61
+ const set = { page: 0 };
62
+ function Host() @{
63
+ <Pager items={items} renderItem={renderItem} page={set.page} onPageChange={onPageChange} />;
64
+ }
65
+
66
+ act(() => root.render(<Host />));
67
+ const pager = el.querySelector('.vx-pager') as any;
68
+ Object.defineProperty(pager, 'clientWidth', { value: 100, configurable: true });
69
+ pager.scrollTo = vi.fn((opts: any) => {
70
+ pager.scrollLeft = opts.left;
71
+ pager.dispatchEvent(new Event('scroll'));
72
+ });
73
+
74
+ set.page = 1;
75
+ act(() => root.render(<Host />));
76
+ expect(pager.scrollTo).toHaveBeenCalledWith({ left: 100, behavior: 'smooth' });
77
+ vi.advanceTimersByTime(200);
78
+ // The programmatic scroll must not report back as a user swipe.
79
+ expect(onPageChange).not.toHaveBeenCalled();
80
+ root.unmount();
81
+ el.remove();
82
+ });
83
+ });
@@ -0,0 +1,104 @@
1
+ import { useEffect, useRef } from 'octane';
2
+ import type { PagerProps } from './props';
3
+ import { applyWebProps } from './platformProps.web';
4
+ import { layoutChildProps } from './layout-child.web';
5
+
6
+ /** Paged horizontal container — web leaf: a mandatory scroll-snap row, one
7
+ * page per item at 100% host width. `onPageChange` fires on the settled
8
+ * snap page (a scroll-idle debounce reads scrollLeft after the snap lands),
9
+ * matching the native widget's selected-index event. Programmatic `page`
10
+ * writes are tracked in `current` so their own scroll never echoes back
11
+ * through `onPageChange` — same contract as the muted property echo on
12
+ * native. No indicator — compose dots from Row + View outside. */
13
+ export function Pager(props: PagerProps) @{
14
+ const host = useRef<HTMLElement | null>(null);
15
+ const latest = useRef(props);
16
+ // The page the leaf believes it is on: seeded by `page`/`defaultPage`,
17
+ // updated by controlled writes and settled swipes. A scroll that lands
18
+ // on `current` is programmatic — the settle never reports it.
19
+ const current = useRef(props.page ?? props.defaultPage ?? 0);
20
+ const settle = useRef<ReturnType<typeof setTimeout> | null>(null);
21
+
22
+ useEffect(() => {
23
+ latest.current = props;
24
+ });
25
+
26
+ useEffect(() => {
27
+ const el = host.current;
28
+ if (!el) return;
29
+ const onScroll = () => {
30
+ if (settle.current !== null) clearTimeout(settle.current);
31
+ settle.current = setTimeout(() => {
32
+ settle.current = null;
33
+ const el2 = host.current;
34
+ if (!el2 || el2.clientWidth === 0) return;
35
+ const max = Math.max(latest.current.items.length - 1, 0);
36
+ const next = Math.max(0, Math.min(max, Math.round(el2.scrollLeft / el2.clientWidth)));
37
+ if (next !== current.current) {
38
+ current.current = next;
39
+ latest.current.onPageChange?.(next);
40
+ }
41
+ }, 90);
42
+ };
43
+
44
+ el.addEventListener('scroll', onScroll, { passive: true });
45
+ return () => {
46
+ el.removeEventListener('scroll', onScroll);
47
+ if (settle.current !== null) clearTimeout(settle.current);
48
+ };
49
+ }, []);
50
+
51
+ // Mount: jump (no animation) to the starting page — an initial position
52
+ // is not a scroll. Runs after the scroll listener attaches so the write
53
+ // still flows through the tracked-index path.
54
+ useEffect(() => {
55
+ const el = host.current;
56
+ if (!el || current.current <= 0) return;
57
+ el.scrollLeft = current.current * el.clientWidth;
58
+ }, []);
59
+
60
+ // Controlled writes scroll smoothly. Skipped when the page already
61
+ // matches `current` — a user swipe that the parent echoes back unchanged
62
+ // must not scroll again.
63
+ useEffect(() => {
64
+ const el = host.current;
65
+ if (!el || props.page === undefined || props.page === current.current) return;
66
+ current.current = props.page;
67
+ el.scrollTo({ left: props.page * el.clientWidth, behavior: 'smooth' });
68
+ }, [props.page]);
69
+
70
+ @if (props.items.length > 0) {
71
+ <div
72
+ ref={(el: any) => {
73
+ host.current = el;
74
+ applyWebProps(el, props.web);
75
+ }}
76
+ id={props.id}
77
+ className={['vx-pager', props.className]}
78
+ {...layoutChildProps(props)}
79
+ role={props.accessibilityRole ?? 'group'}
80
+ aria-roledescription="carousel"
81
+ aria-label={props.accessibilityLabel}
82
+ aria-hidden={props.accessible === false ? true : undefined}
83
+ aria-description={props.accessibilityHint}
84
+ aria-valuetext={props.accessibilityValue}
85
+ aria-disabled={props.accessibilityState?.disabled}
86
+ aria-selected={props.accessibilityState?.selected}
87
+ aria-checked={props.accessibilityState?.checked}
88
+ aria-live={props.accessibilityLiveRegion === 'none' ? 'off' : props.accessibilityLiveRegion}
89
+ >
90
+ @for (const item of props.items; index i; key i) {
91
+ <div
92
+ className="vx-pager-page"
93
+ role="group"
94
+ aria-roledescription="slide"
95
+ aria-label={`${i + 1} of ${props.items.length}`}
96
+ >
97
+ {props.renderItem(item, i)}
98
+ </div>
99
+ }
100
+ </div>
101
+ } @else {
102
+ <>{props.renderEmpty?.()}</>
103
+ }
104
+ }
@@ -0,0 +1,8 @@
1
+ /** NS `className` splits on ' ' only, and the universal driver applies the
2
+ * prop via `String(value)` — an array would arrive comma-joined and match
3
+ * no class at all. Native leaves normalize through cx() before handing
4
+ * className to an intrinsic. (The DOM renderer already flattens arrays,
5
+ * so web leaves don't need this.) */
6
+ export function cx(...parts: any[]): string {
7
+ return parts.flat(Infinity).filter(Boolean).join(' ')
8
+ }
@@ -0,0 +1,65 @@
1
+ import { isAndroid, isIOS } from '@nativescript/core'
2
+ import type { Role } from './props'
3
+
4
+ /** Apply only the active device's escape bag after the primitive's own props. */
5
+ export function applyEscapeProps(view: any, props: { ios?: any; android?: any }): void {
6
+ if (!view) {
7
+ return
8
+ }
9
+
10
+ const bag = isIOS ? props.ios : isAndroid ? props.android : undefined
11
+ if (bag) {
12
+ Object.assign(view, bag)
13
+ }
14
+ }
15
+
16
+ const NATIVE_ACCESSIBILITY_ROLES: Record<Role, string> = {
17
+ button: 'button',
18
+ link: 'link',
19
+ search: 'search',
20
+ image: 'image',
21
+ heading: 'header',
22
+ adjustable: 'adjustable',
23
+ summary: 'summary',
24
+ text: 'text',
25
+ none: 'none',
26
+ progressbar: 'progressBar',
27
+ checkbox: 'checkbox',
28
+ switch: 'switch',
29
+ radio: 'radioButton',
30
+ spinbutton: 'spinButton',
31
+ // NativeScript has no tab role; a tab is still an actionable button and
32
+ // its selected state is carried separately by accessibilityState.
33
+ tab: 'button',
34
+ }
35
+
36
+ /** Translate the shared/ARIA spelling to NativeScript's narrower role enum. */
37
+ export function nativeAccessibilityRole(role?: Role): string | undefined {
38
+ return role ? NATIVE_ACCESSIBILITY_ROLES[role] : undefined
39
+ }
40
+
41
+ /** NativeScript's AccessibilityState is a single enum value, unlike the
42
+ * web's independent ARIA booleans. Priority follows the strongest state. */
43
+ export function nativeAccessibilityState(state?: {
44
+ disabled?: boolean
45
+ selected?: boolean
46
+ checked?: boolean
47
+ }): string | undefined {
48
+ if (state?.disabled) {
49
+ return 'disabled'
50
+ }
51
+
52
+ if (state?.selected) {
53
+ return 'selected'
54
+ }
55
+
56
+ if (state?.checked === true) {
57
+ return 'checked'
58
+ }
59
+
60
+ if (state?.checked === false) {
61
+ return 'unchecked'
62
+ }
63
+
64
+ return undefined
65
+ }
@@ -0,0 +1,2 @@
1
+ export { Pager } from './Pager.macos.tsrx'
2
+ export type { PagerProps } from './props'
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Pager } from './Pager.tsrx'
2
+ export type { PagerProps } from './props'
@@ -0,0 +1,2 @@
1
+ export { Pager } from './Pager.web.tsrx'
2
+ export type { PagerProps } from './props'
@@ -0,0 +1,57 @@
1
+ import type { LayoutChildProps, FlexContainerProps } from './props'
2
+
3
+ // start/end shorthand → flex-start/flex-end.
4
+ const FLEX_JUSTIFY: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
5
+ const FLEX_ALIGN: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
6
+
7
+ /** NativeScript reads these attributes from each child view during parent
8
+ * layout. Values are forwarded with NativeScript's own property names.
9
+ * The flex-container props (justifyContent/alignItems/flexWrap/gap) land on
10
+ * the host flexboxlayout itself — its own attributes, not child metadata. */
11
+ export function layoutChildProps(props: LayoutChildProps & Partial<FlexContainerProps>) {
12
+ const result: Record<string, any> = {}
13
+ for (const key of [
14
+ 'row',
15
+ 'col',
16
+ 'rowSpan',
17
+ 'colSpan',
18
+ 'dock',
19
+ 'left',
20
+ 'top',
21
+ 'flexGrow',
22
+ 'flexShrink',
23
+ 'alignSelf',
24
+ 'order',
25
+ ] as const) {
26
+ if (props[key] !== undefined) {
27
+ result[key] = props[key]
28
+ }
29
+ }
30
+
31
+ if (props.justifyContent !== undefined) {
32
+ result.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent
33
+ }
34
+
35
+ if (props.alignItems !== undefined) {
36
+ result.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems
37
+ }
38
+
39
+ if (props.flexWrap !== undefined) {
40
+ result.flexWrap =
41
+ props.flexWrap === true ? 'wrap' : props.flexWrap === false ? 'nowrap' : props.flexWrap
42
+ }
43
+
44
+ if (props.gap !== undefined) {
45
+ result.gap = props.gap
46
+ }
47
+
48
+ if (props.rowGap !== undefined) {
49
+ result.rowGap = props.rowGap
50
+ }
51
+
52
+ if (props.columnGap !== undefined) {
53
+ result.columnGap = props.columnGap
54
+ }
55
+
56
+ return result
57
+ }
@@ -0,0 +1,108 @@
1
+ import type { LayoutChildProps, FlexContainerProps } from './props'
2
+
3
+ /** The shared contract is "numbers are dips" (RN semantics), but octane's DOM
4
+ * renderer keeps React's unitless list — lineHeight is unitless there, so
5
+ * `style={{lineHeight: 40}}` would render a 40× multiplier instead of 40px.
6
+ * lineHeight is the one unitless-in-React prop that is a length in the
7
+ * shared vocabulary; force the px suffix. */
8
+ export function normStyle(style: any) {
9
+ if (style && typeof style.lineHeight === 'number') {
10
+ return { ...style, lineHeight: style.lineHeight + 'px' }
11
+ }
12
+
13
+ return style
14
+ }
15
+
16
+ // start/end shorthand → flex-start/flex-end (valid in both engines).
17
+ const FLEX_JUSTIFY: Record<string, string> = {
18
+ start: 'flex-start',
19
+ end: 'flex-end',
20
+ }
21
+
22
+ const FLEX_ALIGN: Record<string, string> = {
23
+ start: 'flex-start',
24
+ end: 'flex-end',
25
+ }
26
+
27
+ /** Convert parent-layout metadata to CSS on the web leaf. Native's `dock`
28
+ * attribute has no CSS equivalent and is ignored on web. Grid indices are
29
+ * zero-based in the shared API, matching NativeScript. */
30
+ export function layoutChildProps(
31
+ props: LayoutChildProps & Partial<FlexContainerProps> & { style?: any },
32
+ baseStyle = props.style,
33
+ ) {
34
+ const style = { ...normStyle(baseStyle) }
35
+ if (props.row !== undefined) {
36
+ style.gridRow = `${props.row + 1} / span ${props.rowSpan ?? 1}`
37
+ } else if (props.rowSpan !== undefined) {
38
+ style.gridRow = `span ${props.rowSpan}`
39
+ }
40
+
41
+ if (props.col !== undefined) {
42
+ style.gridColumn = `${props.col + 1} / span ${props.colSpan ?? 1}`
43
+ } else if (props.colSpan !== undefined) {
44
+ style.gridColumn = `span ${props.colSpan}`
45
+ }
46
+
47
+ if (props.left !== undefined) {
48
+ style.left = props.left
49
+ }
50
+
51
+ if (props.top !== undefined) {
52
+ style.top = props.top
53
+ }
54
+
55
+ if (props.flexGrow !== undefined) {
56
+ style.flexGrow = props.flexGrow
57
+ }
58
+
59
+ if (props.flexShrink !== undefined) {
60
+ style.flexShrink = props.flexShrink
61
+ }
62
+
63
+ if (props.alignSelf !== undefined) {
64
+ style.alignSelf = props.alignSelf
65
+ }
66
+
67
+ if (props.order !== undefined) {
68
+ style.order = props.order
69
+ }
70
+
71
+ // Flex-container props (View/Row/Pressable hosts) — RN vocabulary on the
72
+ // element style. Numbers become px via octane's style normalization.
73
+ if (props.justifyContent !== undefined) {
74
+ style.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent
75
+ }
76
+
77
+ if (props.alignItems !== undefined) {
78
+ style.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems
79
+ }
80
+
81
+ if (props.flexWrap !== undefined) {
82
+ style.flexWrap =
83
+ props.flexWrap === true ? 'wrap' : props.flexWrap === false ? 'nowrap' : props.flexWrap
84
+ }
85
+
86
+ if (props.gap !== undefined) {
87
+ style.gap = props.gap
88
+ }
89
+
90
+ if (props.rowGap !== undefined) {
91
+ style.rowGap = props.rowGap
92
+ }
93
+
94
+ if (props.columnGap !== undefined) {
95
+ style.columnGap = props.columnGap
96
+ }
97
+
98
+ // The shared contract is "numbers are dips" (RN semantics), but octane's
99
+ // DOM renderer keeps React's unitless list — lineHeight is unitless there,
100
+ // so `style={{lineHeight: 40}}` would render a 40× multiplier instead of
101
+ // 40px. lineHeight is the one unitless-in-React prop that is a length in
102
+ // the shared vocabulary; force the px suffix.
103
+ if (typeof style.lineHeight === 'number') {
104
+ style.lineHeight = style.lineHeight + 'px'
105
+ }
106
+
107
+ return { style }
108
+ }
@@ -0,0 +1,14 @@
1
+ /** Apply a web escape-hatch bag after the primitive's standard props. */
2
+ export function applyWebProps(el: any, props: Record<string, any> | undefined) {
3
+ if (!el || !props) {
4
+ return
5
+ }
6
+
7
+ for (const [name, value] of Object.entries(props)) {
8
+ if (name.includes('-')) {
9
+ el.setAttribute(name, String(value))
10
+ } else {
11
+ el[name] = value
12
+ }
13
+ }
14
+ }
package/src/props.ts ADDED
@@ -0,0 +1,84 @@
1
+ export interface LayoutChildProps {
2
+ row?: number
3
+ col?: number
4
+ rowSpan?: number
5
+ colSpan?: number
6
+ dock?: 'left' | 'top' | 'right' | 'bottom'
7
+ left?: number
8
+ top?: number
9
+ flexGrow?: number
10
+ flexShrink?: number
11
+ alignSelf?: string
12
+ order?: number
13
+ }
14
+
15
+ /** Flex-container props shared by View/Row/Pressable — RN vocabulary, applied
16
+ * to the host flexboxlayout natively and the element's style on web.
17
+ * `gap` is a dip number (px on web); NS supports it on FlexboxLayout only
18
+ * (GridLayout has no gap). */
19
+ export interface FlexContainerProps {
20
+ justifyContent?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly'
21
+ alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline'
22
+ flexWrap?: boolean | 'wrap' | 'nowrap' | 'wrap-reverse'
23
+ gap?: number | string
24
+ rowGap?: number | string
25
+ columnGap?: number | string
26
+ }
27
+
28
+ export type Role =
29
+ | 'button'
30
+ | 'link'
31
+ | 'search'
32
+ | 'image'
33
+ | 'heading'
34
+ | 'adjustable'
35
+ | 'summary'
36
+ | 'text'
37
+ | 'none'
38
+ | 'progressbar'
39
+ | 'checkbox'
40
+ | 'switch'
41
+ | 'radio'
42
+ | 'spinbutton'
43
+ | 'tab'
44
+
45
+ export interface AccessibilityProps {
46
+ accessible?: boolean
47
+ accessibilityLabel?: string
48
+ accessibilityRole?: Role
49
+ accessibilityHint?: string
50
+ accessibilityValue?: string
51
+ accessibilityState?: {
52
+ disabled?: boolean
53
+ selected?: boolean
54
+ checked?: boolean
55
+ }
56
+ accessibilityLiveRegion?: 'none' | 'polite' | 'assertive'
57
+ }
58
+
59
+ /** Paged horizontal swipe container — onboarding flows, media galleries.
60
+ * Chrome-reset OS paging on native (ViewPager2 / UICollectionView paging via
61
+ * @nativescript-community/ui-pager), a scroll-snap scroller on web. Follows
62
+ * the platform-list contract: `items` + `renderItem`, no children — each
63
+ * page is a full-host-size cell. No indicator is built in; compose dots
64
+ * from `Row` + `View` driven by `page`/`onPageChange`. */
65
+ export interface PagerProps extends LayoutChildProps, AccessibilityProps {
66
+ className?: any
67
+ style?: any
68
+ id?: string
69
+ items: any[]
70
+ renderItem: (item: any, index: number) => any
71
+ renderEmpty?: () => any
72
+ /** Controlled page index — pass with `onPageChange` to own the page.
73
+ * Writes scroll/settle to the page; user swipes call `onPageChange`. */
74
+ page?: number
75
+ /** Starting page for uncontrolled use (default 0). */
76
+ defaultPage?: number
77
+ /** Fires when the settled page changes — a completed swipe on both
78
+ * targets. Programmatic `page` writes do not echo back through it. */
79
+ onPageChange?: (index: number) => void
80
+ /** Platform escape hatches, applied after the shared props. */
81
+ ios?: Record<string, any>
82
+ android?: Record<string, any>
83
+ web?: Record<string, any>
84
+ }
@@ -0,0 +1,5 @@
1
+ import type { UniversalComponent } from 'octane/universal'
2
+ import type { PagerProps } from './props'
3
+
4
+ export declare const Pager: UniversalComponent<PagerProps>
5
+ export type { PagerProps } from './props'
@@ -0,0 +1,4 @@
1
+ import type { PagerProps } from './props'
2
+
3
+ export type { PagerProps } from './props'
4
+ export declare function Pager(props: PagerProps): unknown
@@ -0,0 +1,65 @@
1
+ export interface LayoutChildProps {
2
+ row?: number;
3
+ col?: number;
4
+ rowSpan?: number;
5
+ colSpan?: number;
6
+ dock?: 'left' | 'top' | 'right' | 'bottom';
7
+ left?: number;
8
+ top?: number;
9
+ flexGrow?: number;
10
+ flexShrink?: number;
11
+ alignSelf?: string;
12
+ order?: number;
13
+ }
14
+ /** Flex-container props shared by View/Row/Pressable — RN vocabulary, applied
15
+ * to the host flexboxlayout natively and the element's style on web.
16
+ * `gap` is a dip number (px on web); NS supports it on FlexboxLayout only
17
+ * (GridLayout has no gap). */
18
+ export interface FlexContainerProps {
19
+ justifyContent?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly';
20
+ alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
21
+ flexWrap?: boolean | 'wrap' | 'nowrap' | 'wrap-reverse';
22
+ gap?: number | string;
23
+ rowGap?: number | string;
24
+ columnGap?: number | string;
25
+ }
26
+ export type Role = 'button' | 'link' | 'search' | 'image' | 'heading' | 'adjustable' | 'summary' | 'text' | 'none' | 'progressbar' | 'checkbox' | 'switch' | 'radio' | 'spinbutton' | 'tab';
27
+ export interface AccessibilityProps {
28
+ accessible?: boolean;
29
+ accessibilityLabel?: string;
30
+ accessibilityRole?: Role;
31
+ accessibilityHint?: string;
32
+ accessibilityValue?: string;
33
+ accessibilityState?: {
34
+ disabled?: boolean;
35
+ selected?: boolean;
36
+ checked?: boolean;
37
+ };
38
+ accessibilityLiveRegion?: 'none' | 'polite' | 'assertive';
39
+ }
40
+ /** Paged horizontal swipe container — onboarding flows, media galleries.
41
+ * Chrome-reset OS paging on native (ViewPager2 / UICollectionView paging via
42
+ * @nativescript-community/ui-pager), a scroll-snap scroller on web. Follows
43
+ * the platform-list contract: `items` + `renderItem`, no children — each
44
+ * page is a full-host-size cell. No indicator is built in; compose dots
45
+ * from `Row` + `View` driven by `page`/`onPageChange`. */
46
+ export interface PagerProps extends LayoutChildProps, AccessibilityProps {
47
+ className?: any;
48
+ style?: any;
49
+ id?: string;
50
+ items: any[];
51
+ renderItem: (item: any, index: number) => any;
52
+ renderEmpty?: () => any;
53
+ /** Controlled page index — pass with `onPageChange` to own the page.
54
+ * Writes scroll/settle to the page; user swipes call `onPageChange`. */
55
+ page?: number;
56
+ /** Starting page for uncontrolled use (default 0). */
57
+ defaultPage?: number;
58
+ /** Fires when the settled page changes — a completed swipe on both
59
+ * targets. Programmatic `page` writes do not echo back through it. */
60
+ onPageChange?: (index: number) => void;
61
+ /** Platform escape hatches, applied after the shared props. */
62
+ ios?: Record<string, any>;
63
+ android?: Record<string, any>;
64
+ web?: Record<string, any>;
65
+ }