panelui-native 0.88.2 → 0.90.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/lib/module/components/animated-badge/index.js +10 -2
- package/lib/module/components/animated-badge/index.js.map +1 -1
- package/lib/module/components/avatar/index.js +10 -1
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/search-bar/index.js +35 -7
- package/lib/module/components/search-bar/index.js.map +1 -1
- package/lib/module/components/section-progress/index.js +637 -0
- package/lib/module/components/section-progress/index.js.map +1 -0
- package/lib/module/components/selection-mode/index.js +33 -3
- package/lib/module/components/selection-mode/index.js.map +1 -1
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-scroll-sections.js +58 -4
- package/lib/module/hooks/use-scroll-sections.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/animated-badge/index.d.ts +19 -0
- package/lib/typescript/src/components/animated-badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/search-bar/index.d.ts +4 -1
- package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-progress/index.d.ts +156 -0
- package/lib/typescript/src/components/section-progress/index.d.ts.map +1 -0
- package/lib/typescript/src/components/selection-mode/index.d.ts +63 -0
- package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +1 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts +24 -0
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/animated-badge/index.tsx +24 -2
- package/src/components/avatar/index.tsx +10 -1
- package/src/components/search-bar/index.tsx +33 -8
- package/src/components/section-progress/index.tsx +813 -0
- package/src/components/selection-mode/index.tsx +30 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-scroll-sections.ts +84 -5
- package/src/index.ts +8 -0
|
@@ -134,7 +134,7 @@ const selectionVariants = tv({
|
|
|
134
134
|
header: 'h-14 flex-row items-center gap-3 border-b border-border px-4',
|
|
135
135
|
title: 'flex-1 text-center text-base font-semibold text-foreground',
|
|
136
136
|
close: 'h-10 w-10 items-center justify-center rounded-full bg-muted',
|
|
137
|
-
group: 'overflow-hidden rounded-2xl
|
|
137
|
+
group: 'overflow-hidden rounded-2xl',
|
|
138
138
|
ring: 'rounded-full border-2 border-transparent p-0.5',
|
|
139
139
|
bar: 'flex-row items-stretch',
|
|
140
140
|
action: 'flex-1 items-center justify-center gap-1.5 px-2 py-3',
|
|
@@ -162,6 +162,23 @@ const selectionVariants = tv({
|
|
|
162
162
|
disabled: {
|
|
163
163
|
true: { action: 'opacity-[0.44]' },
|
|
164
164
|
},
|
|
165
|
+
/**
|
|
166
|
+
* The group's surface, which depends on what is behind it.
|
|
167
|
+
*
|
|
168
|
+
* `card` is a step *down* from the background on a screen, which is what a
|
|
169
|
+
* grouped list wants there. A sheet is already `popover`, and popover sits
|
|
170
|
+
* above card in every dark theme — so the same card inside a sheet is
|
|
171
|
+
* darker than the sheet around it and reads as a hole rather than a raised
|
|
172
|
+
* set of rows.
|
|
173
|
+
*
|
|
174
|
+
* `muted` is an alpha tint, so it steps up from whatever it is drawn on and
|
|
175
|
+
* is correct against either. It is only used in the sheet because the
|
|
176
|
+
* screen's card is right where it is.
|
|
177
|
+
*/
|
|
178
|
+
surface: {
|
|
179
|
+
screen: { group: 'bg-card' },
|
|
180
|
+
sheet: { group: 'bg-muted' },
|
|
181
|
+
},
|
|
165
182
|
/**
|
|
166
183
|
* Flush to the bottom edge, or lifted off it.
|
|
167
184
|
*
|
|
@@ -177,6 +194,7 @@ const selectionVariants = tv({
|
|
|
177
194
|
},
|
|
178
195
|
defaultVariants: {
|
|
179
196
|
placement: 'bar',
|
|
197
|
+
surface: 'screen',
|
|
180
198
|
},
|
|
181
199
|
});
|
|
182
200
|
|
|
@@ -665,7 +683,10 @@ function SelectionModeGroup({
|
|
|
665
683
|
...props
|
|
666
684
|
}: SelectionModeGroupProps) {
|
|
667
685
|
const items = Children.toArray(children).filter(Boolean);
|
|
668
|
-
|
|
686
|
+
// A sheet's own surface already sits above a card's, so which fill the group
|
|
687
|
+
// takes depends on where it is. Every other part reads the context too.
|
|
688
|
+
const { sheet } = useSelectionMode();
|
|
689
|
+
const slots = selectionVariants({ surface: sheet ? 'sheet' : 'screen' });
|
|
669
690
|
|
|
670
691
|
/**
|
|
671
692
|
* The caption, and the wrapper that carries it. A group with no `label` is
|
|
@@ -1133,7 +1154,13 @@ function SelectionModeSheet({
|
|
|
1133
1154
|
* screen but in the wrong branch of the tree.
|
|
1134
1155
|
*/}
|
|
1135
1156
|
<SelectionModeContext.Provider value={context}>
|
|
1136
|
-
|
|
1157
|
+
{/*
|
|
1158
|
+
* `pe-0` gives the trailing edge back. `BottomSheet.Header` reserves
|
|
1159
|
+
* room there for the close button, and this sheet draws none — left
|
|
1160
|
+
* alone, the reserved 48 points push the centred title off the middle
|
|
1161
|
+
* of the sheet and hold "All" that far in from the corner.
|
|
1162
|
+
*/}
|
|
1163
|
+
<BottomSheet.Header className="pe-0">
|
|
1137
1164
|
{/* The sheet already draws the rule and the padding. */}
|
|
1138
1165
|
<SelectionModeHeader
|
|
1139
1166
|
title={title}
|
package/src/hooks/index.ts
CHANGED
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
*
|
|
24
24
|
* <SectionRail value={sections.active} onValueChange={sections.scrollTo}>…</SectionRail>
|
|
25
25
|
* ```
|
|
26
|
+
*
|
|
27
|
+
* It also publishes the scroll position itself, as `scroll`, so a component
|
|
28
|
+
* that wants a percentage as well as a section name does not need a second
|
|
29
|
+
* scroll listener to get one. The three values are everything a fraction is
|
|
30
|
+
* made of, and they come out of the event this hook is already handling.
|
|
26
31
|
*/
|
|
27
32
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
28
33
|
import type {
|
|
@@ -31,6 +36,7 @@ import type {
|
|
|
31
36
|
NativeSyntheticEvent,
|
|
32
37
|
ScrollView,
|
|
33
38
|
} from 'react-native';
|
|
39
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated';
|
|
34
40
|
|
|
35
41
|
export interface UseScrollSectionsOptions {
|
|
36
42
|
/** Section ids, in the order they appear down the page. */
|
|
@@ -46,15 +52,39 @@ export interface UseScrollSectionsOptions {
|
|
|
46
52
|
scrollPadding?: number;
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Where the scroller is, as shared values.
|
|
57
|
+
*
|
|
58
|
+
* Written from the scroll handler on the JavaScript thread, which is where the
|
|
59
|
+
* section tracking already runs — so they arrive at `scrollEventThrottle`
|
|
60
|
+
* rather than every frame, and anything reading them should ease towards the
|
|
61
|
+
* value rather than jumping to it.
|
|
62
|
+
*/
|
|
63
|
+
export interface ScrollSectionsPosition {
|
|
64
|
+
/** Distance scrolled, in points. */
|
|
65
|
+
offset: SharedValue<number>;
|
|
66
|
+
/** Height of the visible area. */
|
|
67
|
+
viewport: SharedValue<number>;
|
|
68
|
+
/** Total height of the content. */
|
|
69
|
+
content: SharedValue<number>;
|
|
70
|
+
}
|
|
71
|
+
|
|
49
72
|
export interface UseScrollSectionsResult {
|
|
50
73
|
/** Attach to the ScrollView, so `scrollTo` has something to drive. */
|
|
51
74
|
ref: React.RefObject<ScrollView | null>;
|
|
52
75
|
/** The section being read. */
|
|
53
76
|
active: string | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The scroll position, for anything that needs how far through the page the
|
|
79
|
+
* reader is rather than which part of it they are in.
|
|
80
|
+
*/
|
|
81
|
+
scroll: ScrollSectionsPosition;
|
|
54
82
|
/** Spread onto the ScrollView. */
|
|
55
83
|
scrollProps: {
|
|
56
84
|
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
57
85
|
onMomentumScrollEnd: () => void;
|
|
86
|
+
onLayout: (event: LayoutChangeEvent) => void;
|
|
87
|
+
onContentSizeChange: (width: number, height: number) => void;
|
|
58
88
|
scrollEventThrottle: number;
|
|
59
89
|
};
|
|
60
90
|
/** `onLayout` for a section's wrapper: `onLayout={measure(id)}`. */
|
|
@@ -81,6 +111,10 @@ export function useScrollSections({
|
|
|
81
111
|
const settle = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
82
112
|
const [active, setActive] = useState<string | undefined>(ids[0]);
|
|
83
113
|
|
|
114
|
+
const scrollOffset = useSharedValue(0);
|
|
115
|
+
const viewport = useSharedValue(0);
|
|
116
|
+
const content = useSharedValue(0);
|
|
117
|
+
|
|
84
118
|
// Read inside the scroll handler, which must not be re-created on every
|
|
85
119
|
// render — a new handler each frame would defeat the throttle.
|
|
86
120
|
const idsRef = useRef(ids);
|
|
@@ -107,10 +141,24 @@ export function useScrollSections({
|
|
|
107
141
|
|
|
108
142
|
const onScroll = useCallback(
|
|
109
143
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
144
|
+
const { contentOffset, layoutMeasurement, contentSize } = event.nativeEvent;
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
* The position is published before the jump guard, not after it.
|
|
148
|
+
*
|
|
149
|
+
* A jump is still travel: the page really is moving under the reader, and
|
|
150
|
+
* a progress indicator that froze for the length of the animation would
|
|
151
|
+
* be reporting something that is not true. What the guard protects is the
|
|
152
|
+
* *section*, which the reader chose and which should not flicker through
|
|
153
|
+
* every heading the scroll passes on the way there.
|
|
154
|
+
*/
|
|
155
|
+
scrollOffset.value = contentOffset.y;
|
|
156
|
+
viewport.value = layoutMeasurement.height;
|
|
157
|
+
content.value = contentSize.height;
|
|
158
|
+
|
|
110
159
|
// Mid-jump, the position is the animation's and not the reader's.
|
|
111
160
|
if (jumpingTo.current !== null) return;
|
|
112
161
|
|
|
113
|
-
const { contentOffset, layoutMeasurement, contentSize } = event.nativeEvent;
|
|
114
162
|
const list = idsRef.current;
|
|
115
163
|
if (!list.length) return;
|
|
116
164
|
|
|
@@ -129,7 +177,27 @@ export function useScrollSections({
|
|
|
129
177
|
}
|
|
130
178
|
setActive(current);
|
|
131
179
|
},
|
|
132
|
-
[offset, endThreshold]
|
|
180
|
+
[offset, endThreshold, scrollOffset, viewport, content]
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
/*
|
|
184
|
+
* Seeded at layout as well as on scroll, so a page that has not been touched
|
|
185
|
+
* yet still reports a viewport and a content height. Without these the
|
|
186
|
+
* fraction is zero over zero until the first scroll event, and anything
|
|
187
|
+
* drawing it starts by being wrong rather than by being empty.
|
|
188
|
+
*/
|
|
189
|
+
const onLayout = useCallback(
|
|
190
|
+
(event: LayoutChangeEvent) => {
|
|
191
|
+
viewport.value = event.nativeEvent.layout.height;
|
|
192
|
+
},
|
|
193
|
+
[viewport]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const onContentSizeChange = useCallback(
|
|
197
|
+
(_width: number, height: number) => {
|
|
198
|
+
content.value = height;
|
|
199
|
+
},
|
|
200
|
+
[content]
|
|
133
201
|
);
|
|
134
202
|
|
|
135
203
|
const scrollTo = useCallback(
|
|
@@ -168,9 +236,20 @@ export function useScrollSections({
|
|
|
168
236
|
);
|
|
169
237
|
|
|
170
238
|
const scrollProps = useMemo(
|
|
171
|
-
() => ({
|
|
172
|
-
|
|
239
|
+
() => ({
|
|
240
|
+
onScroll,
|
|
241
|
+
onMomentumScrollEnd,
|
|
242
|
+
onLayout,
|
|
243
|
+
onContentSizeChange,
|
|
244
|
+
scrollEventThrottle: 16,
|
|
245
|
+
}),
|
|
246
|
+
[onScroll, onMomentumScrollEnd, onLayout, onContentSizeChange]
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const scroll = useMemo<ScrollSectionsPosition>(
|
|
250
|
+
() => ({ offset: scrollOffset, viewport, content }),
|
|
251
|
+
[scrollOffset, viewport, content]
|
|
173
252
|
);
|
|
174
253
|
|
|
175
|
-
return { ref, active, scrollProps, measure, scrollTo };
|
|
254
|
+
return { ref, active, scroll, scrollProps, measure, scrollTo };
|
|
176
255
|
}
|
package/src/index.ts
CHANGED
|
@@ -386,6 +386,14 @@ export {
|
|
|
386
386
|
type SectionRailPlacement,
|
|
387
387
|
type SectionRailAlign,
|
|
388
388
|
} from './components/section-rail';
|
|
389
|
+
export {
|
|
390
|
+
SectionProgress,
|
|
391
|
+
type SectionProgressProps,
|
|
392
|
+
type SectionProgressItemProps,
|
|
393
|
+
type SectionProgressPlacement,
|
|
394
|
+
type SectionProgressColor,
|
|
395
|
+
type SectionProgressScroll,
|
|
396
|
+
} from './components/section-progress';
|
|
389
397
|
export {
|
|
390
398
|
Tabs,
|
|
391
399
|
type TabsProps,
|