panelui-native 0.77.0 → 0.79.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/area-chart/index.js +32 -25
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +97 -15
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/funnel-chart/index.js +15 -2
- package/lib/module/components/funnel-chart/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +19 -27
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +2 -0
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +38 -31
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +63 -39
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/map/map-children.js +54 -0
- package/lib/module/components/map/map-children.js.map +1 -0
- package/lib/module/components/map/maplibre.js +31 -3
- package/lib/module/components/map/maplibre.js.map +1 -1
- package/lib/module/components/markdown-editor/index.js +288 -34
- package/lib/module/components/markdown-editor/index.js.map +1 -1
- package/lib/module/components/markdown-editor/markdown-transforms.js +272 -9
- package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -1
- package/lib/module/components/marquee/index.js +205 -46
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +162 -34
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/pie-chart/index.js +15 -2
- package/lib/module/components/pie-chart/index.js.map +1 -1
- package/lib/module/components/plot/index.js +42 -44
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/progress-button/index.js +617 -0
- package/lib/module/components/progress-button/index.js.map +1 -0
- package/lib/module/components/progress-button/progress-button-hold.js +91 -0
- package/lib/module/components/progress-button/progress-button-hold.js.map +1 -0
- package/lib/module/components/scatter-chart/index.js +6 -25
- package/lib/module/components/scatter-chart/index.js.map +1 -1
- package/lib/module/components/section-rail/index.js +40 -17
- package/lib/module/components/section-rail/index.js.map +1 -1
- package/lib/module/components/split-view/index.js +54 -10
- package/lib/module/components/split-view/index.js.map +1 -1
- package/lib/module/components/theme-selector/index.js +239 -0
- package/lib/module/components/theme-selector/index.js.map +1 -0
- package/lib/module/components/theme-selector/theme-preview.js +320 -0
- package/lib/module/components/theme-selector/theme-preview.js.map +1 -0
- package/lib/module/components/timeline/index.js +145 -14
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/components/toast/index.js +45 -11
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/components/treemap-chart/index.js +63 -6
- package/lib/module/components/treemap-chart/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +20 -4
- package/lib/module/components/waterfall-chart/index.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-scroll-sections.js +44 -2
- package/lib/module/hooks/use-scroll-sections.js.map +1 -1
- package/lib/module/hooks/use-skeleton-handoff.js +58 -0
- package/lib/module/hooks/use-skeleton-handoff.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/funnel-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +8 -4
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/map-children.d.ts +41 -0
- package/lib/typescript/src/components/map/map-children.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +8 -4
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
- package/lib/typescript/src/components/markdown-editor/index.d.ts +64 -5
- package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -1
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +33 -5
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/index.d.ts +55 -2
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +79 -4
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/pie-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/plot/index.d.ts +1 -3
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress-button/index.d.ts +311 -0
- package/lib/typescript/src/components/progress-button/index.d.ts.map +1 -0
- package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts +60 -0
- package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts.map +1 -0
- package/lib/typescript/src/components/scatter-chart/index.d.ts +1 -1
- package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
- package/lib/typescript/src/components/split-view/index.d.ts +10 -1
- package/lib/typescript/src/components/split-view/index.d.ts.map +1 -1
- package/lib/typescript/src/components/theme-selector/index.d.ts +107 -0
- package/lib/typescript/src/components/theme-selector/index.d.ts.map +1 -0
- package/lib/typescript/src/components/theme-selector/theme-preview.d.ts +32 -0
- package/lib/typescript/src/components/theme-selector/theme-preview.d.ts.map +1 -0
- package/lib/typescript/src/components/timeline/index.d.ts +7 -0
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- package/lib/typescript/src/components/treemap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +1 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts +1 -0
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-skeleton-handoff.d.ts +11 -0
- package/lib/typescript/src/hooks/use-skeleton-handoff.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/area-chart/index.tsx +49 -37
- package/src/components/bottom-sheet/index.tsx +109 -11
- package/src/components/funnel-chart/index.tsx +10 -2
- package/src/components/heatmap-chart/index.tsx +27 -22
- package/src/components/hex-chart/index.tsx +22 -6
- package/src/components/line-chart/index.tsx +57 -43
- package/src/components/map/index.tsx +63 -36
- package/src/components/map/map-children.ts +66 -0
- package/src/components/map/maplibre.ts +32 -4
- package/src/components/markdown-editor/index.tsx +413 -35
- package/src/components/markdown-editor/markdown-transforms.ts +274 -10
- package/src/components/marquee/index.tsx +254 -58
- package/src/components/panelside/index.tsx +179 -40
- package/src/components/pie-chart/index.tsx +10 -2
- package/src/components/plot/index.tsx +66 -50
- package/src/components/progress-button/index.tsx +684 -0
- package/src/components/progress-button/progress-button-hold.ts +88 -0
- package/src/components/scatter-chart/index.tsx +4 -25
- package/src/components/section-rail/index.tsx +39 -19
- package/src/components/split-view/index.tsx +53 -8
- package/src/components/theme-selector/index.tsx +281 -0
- package/src/components/theme-selector/theme-preview.tsx +205 -0
- package/src/components/timeline/index.tsx +143 -17
- package/src/components/toast/index.tsx +60 -11
- package/src/components/treemap-chart/index.tsx +65 -8
- package/src/components/waterfall-chart/index.tsx +14 -4
- package/src/hooks/index.ts +5 -0
- package/src/hooks/use-scroll-sections.ts +52 -3
- package/src/hooks/use-skeleton-handoff.ts +69 -0
- package/src/index.ts +25 -0
|
@@ -44,7 +44,17 @@
|
|
|
44
44
|
* Screen readers get one copy. The rest are duplicates of content already
|
|
45
45
|
* announced, and hearing a sponsor list four times over is not thoroughness.
|
|
46
46
|
*/
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
createContext,
|
|
49
|
+
useCallback,
|
|
50
|
+
useContext,
|
|
51
|
+
useEffect,
|
|
52
|
+
useId,
|
|
53
|
+
useMemo,
|
|
54
|
+
useState,
|
|
55
|
+
type ComponentType,
|
|
56
|
+
type ReactNode,
|
|
57
|
+
} from 'react';
|
|
48
58
|
import {
|
|
49
59
|
Platform,
|
|
50
60
|
Pressable,
|
|
@@ -61,6 +71,7 @@ import Animated, {
|
|
|
61
71
|
useReducedMotion,
|
|
62
72
|
useSharedValue,
|
|
63
73
|
withRepeat,
|
|
74
|
+
withSequence,
|
|
64
75
|
withTiming,
|
|
65
76
|
} from 'react-native-reanimated';
|
|
66
77
|
import { useDirectionSign } from '../../hooks/use-direction';
|
|
@@ -99,7 +110,13 @@ export interface MarqueeProps extends Omit<ViewProps, 'children'> {
|
|
|
99
110
|
reverse?: boolean;
|
|
100
111
|
/** Set false to hold the content where it is. */
|
|
101
112
|
playing?: boolean;
|
|
102
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Show the built-in user pause/play control while motion is enabled.
|
|
115
|
+
*
|
|
116
|
+
* Defaults to `true` on its own, and to `false` inside a `Marquee.Group` —
|
|
117
|
+
* the group draws one control for everything in it, and a control per row is
|
|
118
|
+
* how two of them end up stacked on top of each other.
|
|
119
|
+
*/
|
|
103
120
|
showPauseControl?: boolean;
|
|
104
121
|
/** Visible and spoken label for the moving state. */
|
|
105
122
|
pauseLabel?: string;
|
|
@@ -109,7 +126,27 @@ export interface MarqueeProps extends Omit<ViewProps, 'children'> {
|
|
|
109
126
|
onPlayingChange?: (playing: boolean) => void;
|
|
110
127
|
}
|
|
111
128
|
|
|
112
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Set by `Marquee.Group` so the marquees inside it stop drawing a pause control
|
|
131
|
+
* each and take the group's instead.
|
|
132
|
+
*/
|
|
133
|
+
interface MarqueeGroupContextValue {
|
|
134
|
+
/** True while the group's control is holding everything in it still. */
|
|
135
|
+
paused: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* How a marquee inside the group reports whether it has a loop to stop.
|
|
138
|
+
*
|
|
139
|
+
* The group draws one control for everything in it, and it should not offer
|
|
140
|
+
* to pause content that never measured — the failure mode a marquee already
|
|
141
|
+
* has, where a child with no intrinsic size never starts. Only the marquees
|
|
142
|
+
* themselves know that, so they say so.
|
|
143
|
+
*/
|
|
144
|
+
report: (id: string, live: boolean) => void;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const MarqueeGroupContext = createContext<MarqueeGroupContextValue | null>(null);
|
|
148
|
+
|
|
149
|
+
function MarqueeRoot({
|
|
113
150
|
className,
|
|
114
151
|
children,
|
|
115
152
|
speed: speedProp = DEFAULT_MARQUEE_SPEED,
|
|
@@ -117,7 +154,7 @@ export function Marquee({
|
|
|
117
154
|
direction = 'horizontal',
|
|
118
155
|
reverse = false,
|
|
119
156
|
playing = true,
|
|
120
|
-
showPauseControl
|
|
157
|
+
showPauseControl,
|
|
121
158
|
pauseLabel = 'Pause',
|
|
122
159
|
playLabel = 'Play',
|
|
123
160
|
onPlayingChange,
|
|
@@ -135,7 +172,13 @@ export function Marquee({
|
|
|
135
172
|
const [content, setContent] = useState(0);
|
|
136
173
|
const [userPaused, setUserPaused] = useState(false);
|
|
137
174
|
const speed = normalizeMarqueeSpeed(speedProp);
|
|
138
|
-
|
|
175
|
+
|
|
176
|
+
// A group's pause is the same instruction as this one's, so it is read here
|
|
177
|
+
// rather than pushed down as a `playing` prop — a caller who set `playing`
|
|
178
|
+
// themselves should not have it overwritten by the container.
|
|
179
|
+
const group = useContext(MarqueeGroupContext);
|
|
180
|
+
const showControl = showPauseControl ?? group === null;
|
|
181
|
+
const moving = playing && !userPaused && !(group?.paused ?? false);
|
|
139
182
|
|
|
140
183
|
// The distance from one copy to the same point on the next, and therefore
|
|
141
184
|
// both the layout step and the exact loop length. They are the same number
|
|
@@ -154,18 +197,43 @@ export function Marquee({
|
|
|
154
197
|
|
|
155
198
|
const offset = useSharedValue(0);
|
|
156
199
|
|
|
200
|
+
/*
|
|
201
|
+
* A re-measure is the only thing allowed to move the content back to the
|
|
202
|
+
* start. The loop length has changed, so an offset taken against the old one
|
|
203
|
+
* no longer means anything.
|
|
204
|
+
*/
|
|
157
205
|
useEffect(() => {
|
|
158
206
|
cancelAnimation(offset);
|
|
159
207
|
offset.value = 0;
|
|
208
|
+
}, [offset, period]);
|
|
209
|
+
|
|
210
|
+
/*
|
|
211
|
+
* Pausing freezes where it is. `cancelAnimation` already leaves the shared
|
|
212
|
+
* value at whatever it had reached, so stopping is simply not starting again
|
|
213
|
+
* — the content stays exactly where the reader stopped it, which is the
|
|
214
|
+
* whole point of a pause control on something they are trying to read.
|
|
215
|
+
*
|
|
216
|
+
* Resuming picks up from there. The first leg is shortened to the distance
|
|
217
|
+
* actually left, or the lap after a pause would run at a fraction of the
|
|
218
|
+
* speed every other lap runs at.
|
|
219
|
+
*/
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
cancelAnimation(offset);
|
|
160
222
|
if (reducedMotion || !moving || period <= 0 || speed <= 0) return undefined;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
223
|
+
|
|
224
|
+
const cycle = (period / speed) * 1000;
|
|
225
|
+
const from = ((offset.value % period) + period) % period;
|
|
226
|
+
const remaining = period - from;
|
|
227
|
+
|
|
228
|
+
offset.value = from;
|
|
229
|
+
offset.value = withSequence(
|
|
230
|
+
withTiming(period, { duration: cycle * (remaining / period), easing: Easing.linear }),
|
|
231
|
+
// Back to the seam in no time at all. The state at `period` is the state
|
|
232
|
+
// at `0`, so this is a bookkeeping step rather than a visible one.
|
|
233
|
+
withTiming(0, { duration: 0 }),
|
|
234
|
+
withRepeat(withTiming(period, { duration: cycle, easing: Easing.linear }), -1, false)
|
|
168
235
|
);
|
|
236
|
+
|
|
169
237
|
return () => cancelAnimation(offset);
|
|
170
238
|
}, [offset, period, moving, reducedMotion, speed]);
|
|
171
239
|
|
|
@@ -189,6 +257,24 @@ export function Marquee({
|
|
|
189
257
|
setContent(horizontal ? width : height);
|
|
190
258
|
};
|
|
191
259
|
|
|
260
|
+
/*
|
|
261
|
+
* Whether there is motion here at all: content that measured, a speed to
|
|
262
|
+
* travel it at, and a `playing` that has not already stopped it from
|
|
263
|
+
* outside. A control offering to pause a still marquee is the same lie as a
|
|
264
|
+
* disabled button with no reason on it.
|
|
265
|
+
*/
|
|
266
|
+
const live = playing && !reducedMotion && period > 0 && speed > 0;
|
|
267
|
+
|
|
268
|
+
// Reported to the group, if there is one, so its single control can be
|
|
269
|
+
// decided the same way this one's is.
|
|
270
|
+
const id = useId();
|
|
271
|
+
const report = group?.report;
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (!report) return undefined;
|
|
274
|
+
report(id, live);
|
|
275
|
+
return () => report(id, false);
|
|
276
|
+
}, [report, id, live]);
|
|
277
|
+
|
|
192
278
|
// Nothing to loop, so nothing to clip or clone: render the content plainly
|
|
193
279
|
// and let it sit where it falls.
|
|
194
280
|
if (reducedMotion) {
|
|
@@ -205,56 +291,61 @@ export function Marquee({
|
|
|
205
291
|
}
|
|
206
292
|
|
|
207
293
|
return (
|
|
208
|
-
<View
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
onLayout={onContainerLayout}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
accessibilityElementsHidden
|
|
222
|
-
importantForAccessibility="no-hide-descendants"
|
|
223
|
-
>
|
|
224
|
-
<InertView
|
|
225
|
-
inert={Platform.OS === 'web' ? true : undefined}
|
|
226
|
-
onLayout={onContentLayout}
|
|
294
|
+
<View className={cn('gap-3', className)} style={style} onLayout={onLayout} {...props}>
|
|
295
|
+
{/* The track, and only the track. The control below it is not clipped by
|
|
296
|
+
this and does not travel with it. */}
|
|
297
|
+
<View className="flex-1 overflow-hidden" onLayout={onContainerLayout}>
|
|
298
|
+
{/* Measured, never seen. A scroller on this axis is what frees the
|
|
299
|
+
content to report its own size instead of the container's. */}
|
|
300
|
+
<ScrollView
|
|
301
|
+
horizontal={horizontal}
|
|
302
|
+
scrollEnabled={false}
|
|
303
|
+
style={styles.measure}
|
|
304
|
+
pointerEvents="none"
|
|
305
|
+
accessibilityElementsHidden
|
|
306
|
+
importantForAccessibility="no-hide-descendants"
|
|
227
307
|
>
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
308
|
+
<InertView
|
|
309
|
+
inert={Platform.OS === 'web' ? true : undefined}
|
|
310
|
+
onLayout={onContentLayout}
|
|
311
|
+
>
|
|
312
|
+
{children}
|
|
313
|
+
</InertView>
|
|
314
|
+
</ScrollView>
|
|
315
|
+
|
|
316
|
+
<Animated.View style={[StyleSheet.absoluteFill, trackStyle]} pointerEvents="box-none">
|
|
317
|
+
{copies.map((index) => {
|
|
318
|
+
// Index 1 is the copy that starts flush with the container's edge,
|
|
319
|
+
// and it is the one a screen reader is given.
|
|
320
|
+
const spoken = index === 1;
|
|
321
|
+
const at = (index - 1) * period;
|
|
322
|
+
return (
|
|
323
|
+
<InertView
|
|
324
|
+
key={index}
|
|
325
|
+
style={[styles.copy, horizontal ? { left: at } : { top: at }]}
|
|
326
|
+
pointerEvents={spoken ? 'box-none' : 'none'}
|
|
327
|
+
inert={Platform.OS === 'web' && !spoken ? true : undefined}
|
|
328
|
+
aria-hidden={!spoken}
|
|
329
|
+
accessibilityElementsHidden={!spoken}
|
|
330
|
+
importantForAccessibility={spoken ? 'auto' : 'no-hide-descendants'}
|
|
331
|
+
>
|
|
332
|
+
{children}
|
|
333
|
+
</InertView>
|
|
334
|
+
);
|
|
335
|
+
})}
|
|
336
|
+
</Animated.View>
|
|
337
|
+
</View>
|
|
338
|
+
{showControl && live ? (
|
|
254
339
|
<Pressable
|
|
255
340
|
accessibilityRole="button"
|
|
256
341
|
accessibilityLabel={userPaused ? playLabel : pauseLabel}
|
|
257
|
-
|
|
342
|
+
/*
|
|
343
|
+
* Below the content, in flow, the way a group's control already is.
|
|
344
|
+
* Floating over the track it was clipped by the track's own
|
|
345
|
+
* `overflow-hidden` — a 48pt target on a strip of badges half that
|
|
346
|
+
* tall — and it covered the content it was there to let you read.
|
|
347
|
+
*/
|
|
348
|
+
className="min-h-12 self-end items-center justify-center rounded-full border border-border bg-background px-4"
|
|
258
349
|
onPress={() => {
|
|
259
350
|
const next = !userPaused;
|
|
260
351
|
setUserPaused(next);
|
|
@@ -270,6 +361,111 @@ export function Marquee({
|
|
|
270
361
|
);
|
|
271
362
|
}
|
|
272
363
|
|
|
364
|
+
export interface MarqueeGroupProps extends ViewProps {
|
|
365
|
+
className?: string;
|
|
366
|
+
/** Set false to hold every marquee in the group where it is. */
|
|
367
|
+
playing?: boolean;
|
|
368
|
+
/** Show the group's pause/play control. */
|
|
369
|
+
showPauseControl?: boolean;
|
|
370
|
+
/** Visible and spoken label for the moving state. */
|
|
371
|
+
pauseLabel?: string;
|
|
372
|
+
/** Visible and spoken label for the user-paused state. */
|
|
373
|
+
playLabel?: string;
|
|
374
|
+
/** Reports changes made by the group's control. */
|
|
375
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
376
|
+
children?: ReactNode;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Rows of travelling content that stop and start together, under one control.
|
|
381
|
+
*
|
|
382
|
+
* Moving content needs a way to stop it, so a marquee draws its own pause
|
|
383
|
+
* button. Stacked — two rows of logos travelling against each other — that is
|
|
384
|
+
* one button per row, each pinned to its own bottom corner, and the upper one
|
|
385
|
+
* lands on top of the row beneath it. Two buttons for one piece of motion, and
|
|
386
|
+
* one of them covering the content it belongs to.
|
|
387
|
+
*
|
|
388
|
+
* A group is the answer to both: the marquees inside it stop drawing their own
|
|
389
|
+
* control, and the group draws a single one below them rather than over them.
|
|
390
|
+
*
|
|
391
|
+
* ```tsx
|
|
392
|
+
* <Marquee.Group>
|
|
393
|
+
* <Marquee>{topRow}</Marquee>
|
|
394
|
+
* <Marquee reverse>{bottomRow}</Marquee>
|
|
395
|
+
* </Marquee.Group>
|
|
396
|
+
* ```
|
|
397
|
+
*
|
|
398
|
+
* Each marquee keeps its own `playing` prop. The group's pause is an additional
|
|
399
|
+
* hold, not a replacement — a row already held still stays still.
|
|
400
|
+
*/
|
|
401
|
+
function MarqueeGroup({
|
|
402
|
+
className,
|
|
403
|
+
children,
|
|
404
|
+
playing = true,
|
|
405
|
+
showPauseControl = true,
|
|
406
|
+
pauseLabel = 'Pause',
|
|
407
|
+
playLabel = 'Play',
|
|
408
|
+
onPlayingChange,
|
|
409
|
+
...props
|
|
410
|
+
}: MarqueeGroupProps) {
|
|
411
|
+
const [userPaused, setUserPaused] = useState(false);
|
|
412
|
+
const [liveRows, setLiveRows] = useState<ReadonlySet<string>>(() => new Set());
|
|
413
|
+
const reducedMotion = useReducedMotion();
|
|
414
|
+
|
|
415
|
+
// Each marquee inside says whether it has a loop to stop, and the group's
|
|
416
|
+
// control is drawn only if at least one of them does.
|
|
417
|
+
const report = useCallback((id: string, live: boolean) => {
|
|
418
|
+
setLiveRows((current) => {
|
|
419
|
+
if (current.has(id) === live) return current;
|
|
420
|
+
const next = new Set(current);
|
|
421
|
+
if (live) next.add(id);
|
|
422
|
+
else next.delete(id);
|
|
423
|
+
return next;
|
|
424
|
+
});
|
|
425
|
+
}, []);
|
|
426
|
+
|
|
427
|
+
const value = useMemo(() => ({ paused: userPaused, report }), [userPaused, report]);
|
|
428
|
+
|
|
429
|
+
// Nothing is moving, so there is nothing to stop. The control is not hidden
|
|
430
|
+
// to save space — offering to pause content that is already still is the
|
|
431
|
+
// same lie as a disabled button with no reason on it.
|
|
432
|
+
const showControl = showPauseControl && playing && !reducedMotion && liveRows.size > 0;
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<MarqueeGroupContext.Provider value={value}>
|
|
436
|
+
<View {...props} className={cn('w-full gap-3', className)}>
|
|
437
|
+
{children}
|
|
438
|
+
{showControl ? (
|
|
439
|
+
<Pressable
|
|
440
|
+
accessibilityRole="button"
|
|
441
|
+
accessibilityLabel={userPaused ? playLabel : pauseLabel}
|
|
442
|
+
// In flow and at the end, not floating over the last row. The
|
|
443
|
+
// control belongs to the group, and a group's control that covers
|
|
444
|
+
// one of its members is describing itself as part of that member.
|
|
445
|
+
className="min-h-12 self-end items-center justify-center rounded-full border border-border bg-background px-4"
|
|
446
|
+
onPress={() => {
|
|
447
|
+
const next = !userPaused;
|
|
448
|
+
setUserPaused(next);
|
|
449
|
+
onPlayingChange?.(!next);
|
|
450
|
+
}}
|
|
451
|
+
>
|
|
452
|
+
<Text className="text-xs font-medium text-foreground">
|
|
453
|
+
{userPaused ? playLabel : pauseLabel}
|
|
454
|
+
</Text>
|
|
455
|
+
</Pressable>
|
|
456
|
+
) : null}
|
|
457
|
+
</View>
|
|
458
|
+
</MarqueeGroupContext.Provider>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
MarqueeGroup.displayName = 'Marquee.Group';
|
|
462
|
+
|
|
463
|
+
MarqueeRoot.displayName = 'Marquee';
|
|
464
|
+
|
|
465
|
+
export const Marquee = Object.assign(MarqueeRoot, {
|
|
466
|
+
Group: MarqueeGroup,
|
|
467
|
+
});
|
|
468
|
+
|
|
273
469
|
const styles = StyleSheet.create({
|
|
274
470
|
/* Laid out so it measures, hidden so it does not draw, and behind everything
|
|
275
471
|
so it can never intercept anything. */
|
|
@@ -119,7 +119,7 @@ import { LinearGradient } from 'expo-linear-gradient';
|
|
|
119
119
|
import { EllipsisIcon, IconColorProvider, MenuIcon, SearchIcon } from '../../icons';
|
|
120
120
|
import { Button } from '../button';
|
|
121
121
|
import { AnimatedPressable } from '../../primitives/animated-pressable';
|
|
122
|
-
import { Text, textChildren } from '../../primitives/text';
|
|
122
|
+
import { Text, textChildren, type TextProps } from '../../primitives/text';
|
|
123
123
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
124
124
|
import { useDirectionSign } from '../../hooks/use-direction';
|
|
125
125
|
import { cn } from '../../utils/cn';
|
|
@@ -888,17 +888,47 @@ function PanelsideGroupLabel({ className, children, ...props }: PanelsideGroupLa
|
|
|
888
888
|
);
|
|
889
889
|
}
|
|
890
890
|
|
|
891
|
+
/**
|
|
892
|
+
* What a row's parts read off the row, rather than being handed it.
|
|
893
|
+
*
|
|
894
|
+
* A label goes medium and un-muted when the row is the current destination,
|
|
895
|
+
* and an icon takes the matching tint. Passing that to every part would mean
|
|
896
|
+
* `<Panelside.ItemLabel active={active}>` at every call site, with an `active`
|
|
897
|
+
* that has to be kept in step with the row's own.
|
|
898
|
+
*/
|
|
899
|
+
interface PanelsideItemContextValue {
|
|
900
|
+
active: boolean;
|
|
901
|
+
disabled: boolean;
|
|
902
|
+
size: PanelsideItemSize;
|
|
903
|
+
/** The colour an icon in this row should take, resolved once by the row. */
|
|
904
|
+
tint: string | undefined;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const PanelsideItemContext = createContext<PanelsideItemContextValue | null>(null);
|
|
908
|
+
|
|
909
|
+
function usePanelsideItem(part: string): PanelsideItemContextValue {
|
|
910
|
+
const value = useContext(PanelsideItemContext);
|
|
911
|
+
if (!value) throw new Error(`${part} must be used inside a <Panelside.Item>.`);
|
|
912
|
+
return value;
|
|
913
|
+
}
|
|
914
|
+
|
|
891
915
|
export interface PanelsideItemProps extends Omit<PressableProps, 'children'> {
|
|
892
916
|
className?: string;
|
|
893
|
-
/**
|
|
917
|
+
/**
|
|
918
|
+
* Leading element — an icon, an avatar, a coloured dot. The shorthand for
|
|
919
|
+
* `Panelside.ItemIcon`.
|
|
920
|
+
*/
|
|
894
921
|
icon?: ReactNode;
|
|
895
|
-
/**
|
|
922
|
+
/**
|
|
923
|
+
* The row's text, truncated to one line since chat titles run long. The
|
|
924
|
+
* shorthand for `Panelside.ItemLabel`.
|
|
925
|
+
*/
|
|
896
926
|
label?: string;
|
|
897
927
|
/** Marks the row as the current destination. */
|
|
898
928
|
active?: boolean;
|
|
899
929
|
/**
|
|
900
930
|
* Trailing count or status. A number or string renders as a pill; anything
|
|
901
|
-
* else renders as given.
|
|
931
|
+
* else renders as given. The shorthand for `Panelside.ItemBadge`.
|
|
902
932
|
*/
|
|
903
933
|
badge?: ReactNode;
|
|
904
934
|
disabled?: boolean;
|
|
@@ -908,10 +938,37 @@ export interface PanelsideItemProps extends Omit<PressableProps, 'children'> {
|
|
|
908
938
|
* worth more than two extra rows.
|
|
909
939
|
*/
|
|
910
940
|
size?: PanelsideItemSize;
|
|
911
|
-
/**
|
|
941
|
+
/**
|
|
942
|
+
* The row's contents, written out: `Panelside.ItemIcon`,
|
|
943
|
+
* `Panelside.ItemLabel`, `Panelside.ItemBadge` and `Panelside.Action`, in
|
|
944
|
+
* whatever order the row wants them. Anything else you draw works too.
|
|
945
|
+
*
|
|
946
|
+
* Children and the shorthand props compose — a row can take its label from
|
|
947
|
+
* `label` and still write a trailing `Panelside.Action` as a child.
|
|
948
|
+
*/
|
|
912
949
|
children?: ReactNode;
|
|
913
950
|
}
|
|
914
951
|
|
|
952
|
+
/**
|
|
953
|
+
* One destination, or one conversation.
|
|
954
|
+
*
|
|
955
|
+
* There are two ways to fill it, and they are the same row. The shorthand —
|
|
956
|
+
* `icon`, `label`, `badge` — covers the row every navigation panel has, and is
|
|
957
|
+
* what most call sites should use. The parts cover everything else: two lines
|
|
958
|
+
* of text, a label that is not a string, a badge before the label rather than
|
|
959
|
+
* after it, a trailing control that is not an overflow menu.
|
|
960
|
+
*
|
|
961
|
+
* ```tsx
|
|
962
|
+
* <Panelside.Item icon={<InboxIcon />} label="Inbox" badge={12} />
|
|
963
|
+
*
|
|
964
|
+
* <Panelside.Item active>
|
|
965
|
+
* <Panelside.ItemIcon><Avatar size="xs" src={author.avatar} /></Panelside.ItemIcon>
|
|
966
|
+
* <Panelside.ItemLabel>{thread.title}</Panelside.ItemLabel>
|
|
967
|
+
* <Panelside.ItemBadge>{thread.unread}</Panelside.ItemBadge>
|
|
968
|
+
* <Panelside.Action onPress={rename} />
|
|
969
|
+
* </Panelside.Item>
|
|
970
|
+
* ```
|
|
971
|
+
*/
|
|
915
972
|
function PanelsideItem({
|
|
916
973
|
className,
|
|
917
974
|
icon,
|
|
@@ -934,46 +991,122 @@ function PanelsideItem({
|
|
|
934
991
|
? restTint
|
|
935
992
|
: undefined;
|
|
936
993
|
|
|
994
|
+
const context = useMemo<PanelsideItemContextValue>(
|
|
995
|
+
() => ({ active, disabled, size, tint }),
|
|
996
|
+
[active, disabled, size, tint]
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
/*
|
|
1000
|
+
* The row needs something flexible in the middle or its trailing content
|
|
1001
|
+
* floats next to the icon instead of sitting at the end. A label supplies
|
|
1002
|
+
* that, and a written-out `Panelside.ItemLabel` supplies it too — so the
|
|
1003
|
+
* spacer is only for the row that has neither, which is a row of nothing but
|
|
1004
|
+
* an icon and a badge.
|
|
1005
|
+
*/
|
|
1006
|
+
const filled = label !== undefined || children !== undefined;
|
|
1007
|
+
|
|
937
1008
|
return (
|
|
938
|
-
<
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1009
|
+
<PanelsideItemContext.Provider value={context}>
|
|
1010
|
+
<AnimatedPressable
|
|
1011
|
+
className={itemVariants({ active, disabled, size, className })}
|
|
1012
|
+
disabled={disabled}
|
|
1013
|
+
accessibilityRole="button"
|
|
1014
|
+
accessibilityState={{ selected: active, disabled }}
|
|
1015
|
+
accessibilityLabel={label}
|
|
1016
|
+
pressScale={0.985}
|
|
1017
|
+
{...props}
|
|
1018
|
+
>
|
|
1019
|
+
{icon ? <PanelsideItemIcon>{icon}</PanelsideItemIcon> : null}
|
|
1020
|
+
|
|
1021
|
+
{label !== undefined ? <PanelsideItemLabel>{label}</PanelsideItemLabel> : null}
|
|
1022
|
+
|
|
1023
|
+
{filled ? null : <View className="flex-1" />}
|
|
1024
|
+
|
|
1025
|
+
{badge !== undefined && badge !== null ? (
|
|
1026
|
+
<PanelsideItemBadge>{badge}</PanelsideItemBadge>
|
|
1027
|
+
) : null}
|
|
1028
|
+
|
|
1029
|
+
{children}
|
|
1030
|
+
</AnimatedPressable>
|
|
1031
|
+
</PanelsideItemContext.Provider>
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
export interface PanelsideItemIconProps extends ViewProps {
|
|
1036
|
+
className?: string;
|
|
1037
|
+
children?: ReactNode;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* The leading slot on a row.
|
|
1042
|
+
*
|
|
1043
|
+
* Whatever is inside it inherits the row's own tint rather than each call site
|
|
1044
|
+
* passing a colour that stops being right the moment the row goes active.
|
|
1045
|
+
*/
|
|
1046
|
+
function PanelsideItemIcon({ className, children, ...props }: PanelsideItemIconProps) {
|
|
1047
|
+
const { tint } = usePanelsideItem('Panelside.ItemIcon');
|
|
1048
|
+
|
|
1049
|
+
// No wrapper unless one was asked for: an icon is already the right size,
|
|
1050
|
+
// and a `View` around it is a layout node between the row and its glyph.
|
|
1051
|
+
if (className === undefined && Object.keys(props).length === 0) {
|
|
1052
|
+
return <IconColorProvider color={tint}>{children}</IconColorProvider>;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
return (
|
|
1056
|
+
<View className={className} {...props}>
|
|
1057
|
+
<IconColorProvider color={tint}>{children}</IconColorProvider>
|
|
1058
|
+
</View>
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export interface PanelsideItemLabelProps extends TextProps {
|
|
1063
|
+
className?: string;
|
|
1064
|
+
children?: ReactNode;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* The row's text. It takes the flexible middle, so a long title truncates
|
|
1069
|
+
* rather than pushing the badge and the action off the end of the panel.
|
|
1070
|
+
*/
|
|
1071
|
+
function PanelsideItemLabel({ className, children, ...props }: PanelsideItemLabelProps) {
|
|
1072
|
+
const { active } = usePanelsideItem('Panelside.ItemLabel');
|
|
1073
|
+
|
|
1074
|
+
return (
|
|
1075
|
+
<Text
|
|
1076
|
+
size="base"
|
|
1077
|
+
weight={active ? 'medium' : 'normal'}
|
|
1078
|
+
muted={!active}
|
|
1079
|
+
numberOfLines={1}
|
|
1080
|
+
className={cn('flex-1', className)}
|
|
945
1081
|
{...props}
|
|
946
1082
|
>
|
|
947
|
-
{
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
{label ? (
|
|
952
|
-
<Text
|
|
953
|
-
size="base"
|
|
954
|
-
weight={active ? 'medium' : 'normal'}
|
|
955
|
-
muted={!active}
|
|
956
|
-
numberOfLines={1}
|
|
957
|
-
className="flex-1"
|
|
958
|
-
>
|
|
959
|
-
{label}
|
|
960
|
-
</Text>
|
|
961
|
-
) : (
|
|
962
|
-
<View className="flex-1" />
|
|
963
|
-
)}
|
|
1083
|
+
{children}
|
|
1084
|
+
</Text>
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
964
1087
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
</Text>
|
|
970
|
-
</View>
|
|
971
|
-
) : (
|
|
972
|
-
badge
|
|
973
|
-
)}
|
|
1088
|
+
export interface PanelsideItemBadgeProps extends ViewProps {
|
|
1089
|
+
className?: string;
|
|
1090
|
+
children?: ReactNode;
|
|
1091
|
+
}
|
|
974
1092
|
|
|
975
|
-
|
|
976
|
-
|
|
1093
|
+
/**
|
|
1094
|
+
* The trailing count or status on a row. Text becomes a pill; anything else is
|
|
1095
|
+
* drawn as given, so a dot or a chip needs no opting out of the pill.
|
|
1096
|
+
*/
|
|
1097
|
+
function PanelsideItemBadge({ className, children, ...props }: PanelsideItemBadgeProps) {
|
|
1098
|
+
usePanelsideItem('Panelside.ItemBadge');
|
|
1099
|
+
|
|
1100
|
+
if (typeof children !== 'string' && typeof children !== 'number') {
|
|
1101
|
+
return <>{children}</>;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return (
|
|
1105
|
+
<View className={cn('rounded-full bg-secondary px-2 py-0.5', className)} {...props}>
|
|
1106
|
+
<Text size="xs" muted>
|
|
1107
|
+
{children}
|
|
1108
|
+
</Text>
|
|
1109
|
+
</View>
|
|
977
1110
|
);
|
|
978
1111
|
}
|
|
979
1112
|
|
|
@@ -1456,6 +1589,9 @@ PanelsideContent.displayName = 'Panelside.Content';
|
|
|
1456
1589
|
PanelsideGroup.displayName = 'Panelside.Group';
|
|
1457
1590
|
PanelsideGroupLabel.displayName = 'Panelside.GroupLabel';
|
|
1458
1591
|
PanelsideItem.displayName = 'Panelside.Item';
|
|
1592
|
+
PanelsideItemIcon.displayName = 'Panelside.ItemIcon';
|
|
1593
|
+
PanelsideItemLabel.displayName = 'Panelside.ItemLabel';
|
|
1594
|
+
PanelsideItemBadge.displayName = 'Panelside.ItemBadge';
|
|
1459
1595
|
PanelsideAction.displayName = 'Panelside.Action';
|
|
1460
1596
|
PanelsideFooter.displayName = 'Panelside.Footer';
|
|
1461
1597
|
PanelsideCta.displayName = 'Panelside.Cta';
|
|
@@ -1470,6 +1606,9 @@ export const Panelside = Object.assign(PanelsideRoot, {
|
|
|
1470
1606
|
Group: PanelsideGroup,
|
|
1471
1607
|
GroupLabel: PanelsideGroupLabel,
|
|
1472
1608
|
Item: PanelsideItem,
|
|
1609
|
+
ItemIcon: PanelsideItemIcon,
|
|
1610
|
+
ItemLabel: PanelsideItemLabel,
|
|
1611
|
+
ItemBadge: PanelsideItemBadge,
|
|
1473
1612
|
Action: PanelsideAction,
|
|
1474
1613
|
Footer: PanelsideFooter,
|
|
1475
1614
|
Cta: PanelsideCta,
|