panelui-native 0.75.0 → 0.76.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/ai-input/ai-input-growth.js +62 -0
- package/lib/module/components/ai-input/ai-input-growth.js.map +1 -0
- package/lib/module/components/ai-input/index.js +1568 -0
- package/lib/module/components/ai-input/index.js.map +1 -0
- package/lib/module/components/alert/index.js +1 -1
- package/lib/module/components/alert/index.js.map +1 -1
- package/lib/module/components/avatar/avatar-source.js +15 -0
- package/lib/module/components/avatar/avatar-source.js.map +1 -0
- package/lib/module/components/avatar/index.js +32 -31
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +3 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button-group/index.js +2 -3
- package/lib/module/components/button-group/index.js.map +1 -1
- package/lib/module/components/carousel/index.js +10 -7
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/collapsible/index.js +1 -1
- package/lib/module/components/collapsible/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +18 -14
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/field/index.js +7 -8
- package/lib/module/components/field/index.js.map +1 -1
- package/lib/module/components/label/index.js +1 -1
- package/lib/module/components/label/index.js.map +1 -1
- package/lib/module/components/marquee/index.js +36 -11
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/marquee/marquee-math.js +22 -6
- package/lib/module/components/marquee/marquee-math.js.map +1 -1
- package/lib/module/components/plan/index.js +2 -1
- package/lib/module/components/plan/index.js.map +1 -1
- package/lib/module/components/plot/index.js +27 -9
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/search-bar/index.js +11 -3
- package/lib/module/components/search-bar/index.js.map +1 -1
- package/lib/module/components/split-view/index.js +26 -27
- package/lib/module/components/split-view/index.js.map +1 -1
- package/lib/module/components/split-view/split-view-lifecycle.js +32 -0
- package/lib/module/components/split-view/split-view-lifecycle.js.map +1 -0
- package/lib/module/components/splitter/index.js +7 -13
- package/lib/module/components/splitter/index.js.map +1 -1
- package/lib/module/components/splitter/splitter-math.js +14 -0
- package/lib/module/components/splitter/splitter-math.js.map +1 -1
- package/lib/module/components/tabs/index.js +1 -1
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +156 -42
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/components/timeline/timeline-geometry.js +11 -0
- package/lib/module/components/timeline/timeline-geometry.js.map +1 -1
- package/lib/module/components/toast/index.js +1 -1
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/components/tooltip/index.js +2 -2
- package/lib/module/components/tooltip/index.js.map +1 -1
- package/lib/module/components/tree/index.js +2 -2
- package/lib/module/components/tree/index.js.map +1 -1
- package/lib/module/components/typography/index.js +2 -1
- package/lib/module/components/typography/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +22 -3
- package/lib/module/components/waterfall-chart/index.js.map +1 -1
- package/lib/module/hooks/use-disclosure.js +10 -8
- package/lib/module/hooks/use-disclosure.js.map +1 -1
- package/lib/module/icons/index.js +135 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +5 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/controllable-state.js +61 -0
- package/lib/module/primitives/controllable-state.js.map +1 -0
- package/lib/module/primitives/finite-chart.js +15 -0
- package/lib/module/primitives/finite-chart.js.map +1 -0
- package/lib/module/primitives/glass.js +136 -0
- package/lib/module/primitives/glass.js.map +1 -0
- package/lib/module/primitives/scrim.js +8 -1
- package/lib/module/primitives/scrim.js.map +1 -1
- package/lib/typescript/src/components/ai-input/ai-input-growth.d.ts +59 -0
- package/lib/typescript/src/components/ai-input/ai-input-growth.d.ts.map +1 -0
- package/lib/typescript/src/components/ai-input/index.d.ts +423 -0
- package/lib/typescript/src/components/ai-input/index.d.ts.map +1 -0
- package/lib/typescript/src/components/avatar/avatar-source.d.ts +4 -0
- package/lib/typescript/src/components/avatar/avatar-source.d.ts.map +1 -0
- package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +10 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
- package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/index.d.ts +15 -6
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/marquee-math.d.ts +6 -0
- package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -1
- package/lib/typescript/src/components/plan/index.d.ts.map +1 -1
- package/lib/typescript/src/components/plot/index.d.ts +2 -1
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/split-view/index.d.ts.map +1 -1
- package/lib/typescript/src/components/split-view/split-view-lifecycle.d.ts +14 -0
- package/lib/typescript/src/components/split-view/split-view-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/splitter/index.d.ts +2 -2
- package/lib/typescript/src/components/splitter/index.d.ts.map +1 -1
- package/lib/typescript/src/components/splitter/splitter-math.d.ts +2 -0
- package/lib/typescript/src/components/splitter/splitter-math.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/index.d.ts +24 -2
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/timeline-geometry.d.ts +2 -0
- package/lib/typescript/src/components/timeline/timeline-geometry.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-disclosure.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +18 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/controllable-state.d.ts +33 -0
- package/lib/typescript/src/primitives/controllable-state.d.ts.map +1 -0
- package/lib/typescript/src/primitives/finite-chart.d.ts +5 -0
- package/lib/typescript/src/primitives/finite-chart.d.ts.map +1 -0
- package/lib/typescript/src/primitives/glass.d.ts +43 -0
- package/lib/typescript/src/primitives/glass.d.ts.map +1 -0
- package/lib/typescript/src/primitives/scrim.d.ts +7 -0
- package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
- package/package.json +7 -2
- package/src/components/ai-input/ai-input-growth.ts +65 -0
- package/src/components/ai-input/index.tsx +1937 -0
- package/src/components/alert/index.tsx +1 -1
- package/src/components/avatar/avatar-source.ts +18 -0
- package/src/components/avatar/index.tsx +35 -27
- package/src/components/bottom-sheet/index.tsx +15 -3
- package/src/components/button-group/index.tsx +1 -1
- package/src/components/carousel/index.tsx +10 -11
- package/src/components/collapsible/index.tsx +1 -1
- package/src/components/drawer/index.tsx +24 -20
- package/src/components/field/index.tsx +4 -4
- package/src/components/label/index.tsx +1 -1
- package/src/components/marquee/index.tsx +56 -12
- package/src/components/marquee/marquee-math.ts +26 -3
- package/src/components/plan/index.tsx +6 -2
- package/src/components/plot/index.tsx +37 -11
- package/src/components/search-bar/index.tsx +9 -3
- package/src/components/split-view/index.tsx +24 -33
- package/src/components/split-view/split-view-lifecycle.ts +43 -0
- package/src/components/splitter/index.tsx +7 -11
- package/src/components/splitter/splitter-math.ts +28 -0
- package/src/components/tabs/index.tsx +1 -1
- package/src/components/timeline/index.tsx +189 -34
- package/src/components/timeline/timeline-geometry.ts +11 -0
- package/src/components/toast/index.tsx +6 -1
- package/src/components/tooltip/index.tsx +1 -1
- package/src/components/tree/index.tsx +2 -2
- package/src/components/typography/index.tsx +2 -2
- package/src/components/waterfall-chart/index.tsx +37 -4
- package/src/hooks/use-disclosure.ts +7 -12
- package/src/icons/index.tsx +106 -0
- package/src/index.ts +27 -0
- package/src/primitives/controllable-state.ts +112 -0
- package/src/primitives/finite-chart.ts +14 -0
- package/src/primitives/glass.tsx +171 -0
- package/src/primitives/scrim.tsx +7 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ImageSourcePropType } from 'react-native';
|
|
2
|
+
|
|
3
|
+
function stableValue(value: unknown): string {
|
|
4
|
+
if (Array.isArray(value)) return `[${value.map(stableValue).join(',')}]`;
|
|
5
|
+
if (value && typeof value === 'object') {
|
|
6
|
+
return `{${Object.entries(value)
|
|
7
|
+
.filter(([, entry]) => entry !== undefined)
|
|
8
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
9
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${stableValue(entry)}`)
|
|
10
|
+
.join(',')}}`;
|
|
11
|
+
}
|
|
12
|
+
return JSON.stringify(value) ?? String(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Stable across equivalent inline source objects, but changes with request inputs. */
|
|
16
|
+
export function avatarSourceIdentity(source: ImageSourcePropType | undefined) {
|
|
17
|
+
return source === undefined ? undefined : stableValue(source);
|
|
18
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
avatarGroupOverlap,
|
|
23
23
|
type AvatarSizeName,
|
|
24
24
|
} from './avatar-group';
|
|
25
|
+
import { avatarSourceIdentity } from './avatar-source';
|
|
25
26
|
|
|
26
27
|
const avatarVariants = tv({
|
|
27
28
|
slots: {
|
|
@@ -53,16 +54,20 @@ export interface AvatarProps extends ViewProps, VariantProps<typeof avatarVarian
|
|
|
53
54
|
|
|
54
55
|
const AvatarRoot = forwardRef<View, AvatarProps>(
|
|
55
56
|
({ className, size, source, fallback, imageProps, children, ...props }, ref) => {
|
|
56
|
-
const [
|
|
57
|
+
const [failedSource, setFailedSource] = useState<string>();
|
|
57
58
|
const { root, image, fallback: fallbackSlot } = avatarVariants({ size });
|
|
58
|
-
const
|
|
59
|
+
const sourceIdentity = avatarSourceIdentity(source);
|
|
60
|
+
const showImage = !!source && failedSource !== sourceIdentity;
|
|
59
61
|
|
|
60
62
|
const face = showImage ? (
|
|
61
63
|
<Image
|
|
62
|
-
source={source}
|
|
63
|
-
onError={() => setErrored(true)}
|
|
64
64
|
className={image()}
|
|
65
65
|
{...imageProps}
|
|
66
|
+
source={source}
|
|
67
|
+
onError={(event) => {
|
|
68
|
+
setFailedSource(sourceIdentity);
|
|
69
|
+
imageProps?.onError?.(event);
|
|
70
|
+
}}
|
|
66
71
|
/>
|
|
67
72
|
) : (
|
|
68
73
|
<Text className={fallbackSlot()}>{fallback ?? '?'}</Text>
|
|
@@ -149,11 +154,9 @@ export interface AvatarGroupProps extends ViewProps {
|
|
|
149
154
|
* A row of avatars, each overlapping the one after it, with the people who did
|
|
150
155
|
* not fit counted at the end.
|
|
151
156
|
*
|
|
152
|
-
* The faces
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* them has to be flipped. Flipping the layout keeps the children in the order
|
|
156
|
-
* they were written.
|
|
157
|
+
* The faces stay in logical child order for assistive traversal. Explicit
|
|
158
|
+
* z-indices, rather than a reversed render/layout order, put the first face on
|
|
159
|
+
* top while the overflow count remains the final list item.
|
|
157
160
|
*
|
|
158
161
|
* Each face gets a ring in the page background so it separates from the one
|
|
159
162
|
* underneath whatever surface the stack sits on.
|
|
@@ -172,10 +175,21 @@ const AvatarGroup = forwardRef<View, AvatarGroupProps>(
|
|
|
172
175
|
const { visible, overflow } = avatarGroupCount(faces.length, max, total);
|
|
173
176
|
const slide = avatarGroupOverlap(size, overlap);
|
|
174
177
|
|
|
175
|
-
//
|
|
176
|
-
//
|
|
178
|
+
// Logical reading order: visible people first, then the overflow summary.
|
|
179
|
+
// Painting order is independent and applied to each wrapper below.
|
|
177
180
|
const stack: { key: string; node: ReactNode }[] = [];
|
|
178
181
|
|
|
182
|
+
for (let index = 0; index < visible; index += 1) {
|
|
183
|
+
const face = faces[index]!;
|
|
184
|
+
stack.push({
|
|
185
|
+
key: String(face.key ?? index),
|
|
186
|
+
node: cloneElement(face, {
|
|
187
|
+
size: face.props.size ?? size,
|
|
188
|
+
className: cn('border-2 border-background', face.props.className),
|
|
189
|
+
}),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
179
193
|
if (overflow > 0) {
|
|
180
194
|
stack.push({
|
|
181
195
|
key: 'overflow',
|
|
@@ -190,28 +204,22 @@ const AvatarGroup = forwardRef<View, AvatarGroupProps>(
|
|
|
190
204
|
});
|
|
191
205
|
}
|
|
192
206
|
|
|
193
|
-
for (let index = visible - 1; index >= 0; index -= 1) {
|
|
194
|
-
const face = faces[index]!;
|
|
195
|
-
stack.push({
|
|
196
|
-
key: String(face.key ?? index),
|
|
197
|
-
node: cloneElement(face, {
|
|
198
|
-
size: face.props.size ?? size,
|
|
199
|
-
className: cn('border-2 border-background', face.props.className),
|
|
200
|
-
}),
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
207
|
return (
|
|
205
208
|
<View
|
|
206
209
|
ref={ref}
|
|
207
|
-
|
|
208
|
-
className={cn('flex-row-reverse self-start', className)}
|
|
210
|
+
className={cn('flex-row self-start', className)}
|
|
209
211
|
{...props}
|
|
212
|
+
accessibilityRole="list"
|
|
210
213
|
>
|
|
211
214
|
{stack.map((entry, index) => (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
<View
|
|
216
|
+
key={entry.key}
|
|
217
|
+
role="listitem"
|
|
218
|
+
style={{
|
|
219
|
+
zIndex: stack.length - index,
|
|
220
|
+
marginStart: index === 0 ? 0 : -slide,
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
215
223
|
{entry.node}
|
|
216
224
|
</View>
|
|
217
225
|
))}
|
|
@@ -224,6 +224,15 @@ export interface BottomSheetContentProps extends ViewProps {
|
|
|
224
224
|
* affordances.
|
|
225
225
|
*/
|
|
226
226
|
showClose?: boolean;
|
|
227
|
+
/**
|
|
228
|
+
* Show the drag handle at the top of the sheet. On by default, because a
|
|
229
|
+
* sheet that can be dragged should say so.
|
|
230
|
+
*
|
|
231
|
+
* Turn it off when the sheet draws its own — a component wrapping this one
|
|
232
|
+
* to give the surface a material of its own has to put the handle on that
|
|
233
|
+
* material, and a handle floating above it belongs to nothing.
|
|
234
|
+
*/
|
|
235
|
+
showGrabber?: boolean;
|
|
227
236
|
/**
|
|
228
237
|
* Float the sheet clear of the screen edges instead of docking it to the
|
|
229
238
|
* bottom, so it reads as a card laid over the app rather than a drawer
|
|
@@ -267,6 +276,7 @@ function BottomSheetContent({
|
|
|
267
276
|
className,
|
|
268
277
|
dismissible = true,
|
|
269
278
|
showClose = true,
|
|
279
|
+
showGrabber = true,
|
|
270
280
|
detached = false,
|
|
271
281
|
blur = false,
|
|
272
282
|
size = 'auto',
|
|
@@ -475,9 +485,11 @@ function BottomSheetContent({
|
|
|
475
485
|
]}
|
|
476
486
|
>
|
|
477
487
|
<SheetSurfaceContext.Provider value={surface}>
|
|
478
|
-
|
|
479
|
-
<View className="
|
|
480
|
-
|
|
488
|
+
{showGrabber ? (
|
|
489
|
+
<View className="mb-3 self-center">
|
|
490
|
+
<View className="h-1 w-10 rounded-full bg-muted-foreground/30" />
|
|
491
|
+
</View>
|
|
492
|
+
) : null}
|
|
481
493
|
{textChildren(children)}
|
|
482
494
|
{/*
|
|
483
495
|
* Last, and lifted above the content.
|
|
@@ -171,11 +171,11 @@ const ButtonGroupRoot = forwardRef<View, ButtonGroupProps>(
|
|
|
171
171
|
<ButtonGroupProvider value={context}>
|
|
172
172
|
<View
|
|
173
173
|
ref={ref}
|
|
174
|
+
{...props}
|
|
174
175
|
// A run of related actions is a toolbar; a screen reader announcing
|
|
175
176
|
// the group is what tells someone the buttons in it belong together.
|
|
176
177
|
accessibilityRole="toolbar"
|
|
177
178
|
className={root({ className })}
|
|
178
|
-
{...props}
|
|
179
179
|
>
|
|
180
180
|
{items.map((child, index) => (
|
|
181
181
|
// Index keys: these fragments have no identity of their own, and
|
|
@@ -74,6 +74,7 @@ import Animated, {
|
|
|
74
74
|
type SharedValue,
|
|
75
75
|
} from 'react-native-reanimated';
|
|
76
76
|
import { ChevronLeftIcon, ChevronRightIcon } from '../../icons';
|
|
77
|
+
import { useControllableState } from '../../primitives/controllable-state';
|
|
77
78
|
import { Text, textChildren } from '../../primitives/text';
|
|
78
79
|
import { cn } from '../../utils/cn';
|
|
79
80
|
import {
|
|
@@ -264,14 +265,20 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
|
|
|
264
265
|
ref
|
|
265
266
|
) {
|
|
266
267
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
267
|
-
const [internalIndex, setInternalIndex] = useState(defaultIndex);
|
|
268
268
|
const [count, setCountValue] = useState(0);
|
|
269
269
|
const [countKnown, setCountKnown] = useState(false);
|
|
270
270
|
const [touched, setTouched] = useState(false);
|
|
271
271
|
const reducedMotion = useReducedMotion();
|
|
272
272
|
|
|
273
|
-
const
|
|
274
|
-
|
|
273
|
+
const {
|
|
274
|
+
value: requestedIndex,
|
|
275
|
+
setValue: setIndex,
|
|
276
|
+
isControlled,
|
|
277
|
+
} = useControllableState({
|
|
278
|
+
value: indexProp,
|
|
279
|
+
defaultValue: defaultIndex,
|
|
280
|
+
onChange: onIndexChange,
|
|
281
|
+
});
|
|
275
282
|
|
|
276
283
|
const progress = useSharedValue(indexProp ?? defaultIndex);
|
|
277
284
|
const engaged = useSharedValue(0);
|
|
@@ -287,14 +294,6 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
|
|
|
287
294
|
setCountKnown(true);
|
|
288
295
|
}, []);
|
|
289
296
|
|
|
290
|
-
const setIndex = useCallback(
|
|
291
|
-
(next: number) => {
|
|
292
|
-
if (!isControlled) setInternalIndex(next);
|
|
293
|
-
onIndexChange?.(next);
|
|
294
|
-
},
|
|
295
|
-
[isControlled, onIndexChange]
|
|
296
|
-
);
|
|
297
|
-
|
|
298
297
|
/*
|
|
299
298
|
* The slide the run is currently travelling to.
|
|
300
299
|
*
|
|
@@ -180,11 +180,11 @@ const CollapsibleTrigger = forwardRef<View, CollapsibleTriggerProps>(
|
|
|
180
180
|
return (
|
|
181
181
|
<Pressable
|
|
182
182
|
ref={ref}
|
|
183
|
+
{...props}
|
|
183
184
|
accessibilityRole="button"
|
|
184
185
|
accessibilityState={{ expanded: open, disabled: isDisabled }}
|
|
185
186
|
disabled={isDisabled}
|
|
186
187
|
className={trigger({ className })}
|
|
187
|
-
{...props}
|
|
188
188
|
onPress={(event) => {
|
|
189
189
|
onPress?.(event);
|
|
190
190
|
toggle();
|
|
@@ -415,26 +415,6 @@ function DrawerContent({
|
|
|
415
415
|
[closeCorner, showClose]
|
|
416
416
|
);
|
|
417
417
|
|
|
418
|
-
if (!open) return null;
|
|
419
|
-
|
|
420
|
-
/*
|
|
421
|
-
* The slide has to be given a physical direction, since the animation
|
|
422
|
-
* presets are physical. This is the same mirroring Yoga did for the panel's
|
|
423
|
-
* position, applied by hand to the one thing Yoga does not own.
|
|
424
|
-
*/
|
|
425
|
-
const physical: 'left' | 'right' | 'top' | 'bottom' = horizontal
|
|
426
|
-
? (side === 'start') === (dir === 'ltr')
|
|
427
|
-
? 'left'
|
|
428
|
-
: 'right'
|
|
429
|
-
: side;
|
|
430
|
-
|
|
431
|
-
const entering = {
|
|
432
|
-
left: SlideInLeft,
|
|
433
|
-
right: SlideInRight,
|
|
434
|
-
top: SlideInUp,
|
|
435
|
-
bottom: SlideInDown,
|
|
436
|
-
}[physical];
|
|
437
|
-
|
|
438
418
|
/*
|
|
439
419
|
* The slide out is written by hand, because the panel is not always at rest
|
|
440
420
|
* when it starts.
|
|
@@ -447,6 +427,10 @@ function DrawerContent({
|
|
|
447
427
|
* picks the panel up wherever the finger left it and carries it the rest of
|
|
448
428
|
* the way. A press on the close button or the backdrop leaves `travel` at
|
|
449
429
|
* zero, so that path is the same slide it always was.
|
|
430
|
+
*
|
|
431
|
+
* Above the early return, with every other hook. It reads nothing that is
|
|
432
|
+
* only known once the drawer is open, and a hook below that return is one
|
|
433
|
+
* that runs on an open drawer and not on a closed one.
|
|
450
434
|
*/
|
|
451
435
|
const exiting = useCallback(() => {
|
|
452
436
|
'worklet';
|
|
@@ -468,6 +452,26 @@ function DrawerContent({
|
|
|
468
452
|
};
|
|
469
453
|
}, [extent, horizontal, outward, travel]);
|
|
470
454
|
|
|
455
|
+
if (!open) return null;
|
|
456
|
+
|
|
457
|
+
/*
|
|
458
|
+
* The slide has to be given a physical direction, since the animation
|
|
459
|
+
* presets are physical. This is the same mirroring Yoga did for the panel's
|
|
460
|
+
* position, applied by hand to the one thing Yoga does not own.
|
|
461
|
+
*/
|
|
462
|
+
const physical: 'left' | 'right' | 'top' | 'bottom' = horizontal
|
|
463
|
+
? (side === 'start') === (dir === 'ltr')
|
|
464
|
+
? 'left'
|
|
465
|
+
: 'right'
|
|
466
|
+
: side;
|
|
467
|
+
|
|
468
|
+
const entering = {
|
|
469
|
+
left: SlideInLeft,
|
|
470
|
+
right: SlideInRight,
|
|
471
|
+
top: SlideInUp,
|
|
472
|
+
bottom: SlideInDown,
|
|
473
|
+
}[physical];
|
|
474
|
+
|
|
471
475
|
/*
|
|
472
476
|
* The inset and the panel's own padding stack rather than compete.
|
|
473
477
|
*
|
|
@@ -140,10 +140,10 @@ const FieldRoot = forwardRef<View, FieldProps>(
|
|
|
140
140
|
<FieldContext.Provider value={state}>
|
|
141
141
|
<View
|
|
142
142
|
ref={ref}
|
|
143
|
+
{...props}
|
|
143
144
|
role="group"
|
|
144
145
|
accessibilityState={{ disabled }}
|
|
145
146
|
className={root({ className })}
|
|
146
|
-
{...props}
|
|
147
147
|
>
|
|
148
148
|
{textChildren(children)}
|
|
149
149
|
</View>
|
|
@@ -242,13 +242,13 @@ const FieldError = forwardRef<View, FieldErrorProps>(
|
|
|
242
242
|
return (
|
|
243
243
|
<View
|
|
244
244
|
ref={ref}
|
|
245
|
+
{...props}
|
|
245
246
|
// Web-aligned `role`, same as `Separator` already uses — RN's older
|
|
246
247
|
// accessibilityRole list has no alert, the ARIA-aligned one does.
|
|
247
248
|
role="alert"
|
|
248
249
|
accessibilityLiveRegion="polite"
|
|
249
250
|
nativeID={nativeID ?? relationshipIds[3]}
|
|
250
251
|
className={error({ className })}
|
|
251
|
-
{...props}
|
|
252
252
|
>
|
|
253
253
|
{messages ? (
|
|
254
254
|
messages.length === 1 ? (
|
|
@@ -278,7 +278,7 @@ export interface FieldSetProps extends ViewProps {
|
|
|
278
278
|
const FieldSet = forwardRef<View, FieldSetProps>(
|
|
279
279
|
({ className, ...props }, ref) => {
|
|
280
280
|
const { set } = fieldVariants();
|
|
281
|
-
return <View ref={ref} role="group" className={set({ className })}
|
|
281
|
+
return <View ref={ref} {...props} role="group" className={set({ className })} />;
|
|
282
282
|
}
|
|
283
283
|
);
|
|
284
284
|
FieldSet.displayName = 'Field.Set';
|
|
@@ -293,7 +293,7 @@ const FieldLegend = forwardRef<RNText, FieldLegendProps>(
|
|
|
293
293
|
({ className, variant = 'legend', ...props }, ref) => {
|
|
294
294
|
const { legend } = fieldVariants({ legendVariant: variant });
|
|
295
295
|
return (
|
|
296
|
-
<Text ref={ref} role="heading" className={legend({ className })}
|
|
296
|
+
<Text ref={ref} {...props} role="heading" className={legend({ className })} />
|
|
297
297
|
);
|
|
298
298
|
}
|
|
299
299
|
);
|
|
@@ -81,9 +81,9 @@ const LabelRoot = forwardRef<View, LabelProps>(
|
|
|
81
81
|
<LabelContext.Provider value={state}>
|
|
82
82
|
<View
|
|
83
83
|
ref={ref}
|
|
84
|
+
{...props}
|
|
84
85
|
className={root({ className })}
|
|
85
86
|
accessibilityState={{ disabled: isDisabled }}
|
|
86
|
-
{...props}
|
|
87
87
|
>
|
|
88
88
|
{/* Bare strings are the common case — wrap them so callers don't have to. */}
|
|
89
89
|
{textChildren(children, (text) => <LabelText>{text}</LabelText>)}
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* ## How it loops
|
|
16
16
|
*
|
|
17
17
|
* The content is measured once, then laid out end to end enough times to cover
|
|
18
|
-
* the container
|
|
19
|
-
*
|
|
20
|
-
* content
|
|
21
|
-
*
|
|
18
|
+
* the container. One track holds every copy and it is that track, not the
|
|
19
|
+
* copies, that moves. The track has a fixed copy budget: exceptionally short
|
|
20
|
+
* content gets extra whitespace instead of multiplying its React subtree
|
|
21
|
+
* hundreds of times. It travels exactly one layout period and starts over, so
|
|
22
|
+
* the state it ends on is the state it began on and the seam never shows.
|
|
22
23
|
*
|
|
23
24
|
* The travel is a linear timing driven on the UI thread, so it costs nothing
|
|
24
25
|
* per frame in JavaScript and keeps running while the thread is busy.
|
|
@@ -43,8 +44,10 @@
|
|
|
43
44
|
* Screen readers get one copy. The rest are duplicates of content already
|
|
44
45
|
* announced, and hearing a sponsor list four times over is not thoroughness.
|
|
45
46
|
*/
|
|
46
|
-
import { useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
47
|
+
import { useEffect, useMemo, useState, type ComponentType, type ReactNode } from 'react';
|
|
47
48
|
import {
|
|
49
|
+
Platform,
|
|
50
|
+
Pressable,
|
|
48
51
|
ScrollView,
|
|
49
52
|
StyleSheet,
|
|
50
53
|
View,
|
|
@@ -61,6 +64,7 @@ import Animated, {
|
|
|
61
64
|
withTiming,
|
|
62
65
|
} from 'react-native-reanimated';
|
|
63
66
|
import { useDirectionSign } from '../../hooks/use-direction';
|
|
67
|
+
import { Text } from '../../primitives/text';
|
|
64
68
|
import { cn } from '../../utils/cn';
|
|
65
69
|
import {
|
|
66
70
|
DEFAULT_MARQUEE_SPEED,
|
|
@@ -70,6 +74,9 @@ import {
|
|
|
70
74
|
|
|
71
75
|
export type MarqueeDirection = 'horizontal' | 'vertical';
|
|
72
76
|
|
|
77
|
+
type InertViewProps = ViewProps & { inert?: boolean };
|
|
78
|
+
const InertView = View as ComponentType<InertViewProps>;
|
|
79
|
+
|
|
73
80
|
export interface MarqueeProps extends Omit<ViewProps, 'children'> {
|
|
74
81
|
className?: string;
|
|
75
82
|
/** The content to repeat. Measured once, then tiled along the axis. */
|
|
@@ -81,7 +88,7 @@ export interface MarqueeProps extends Omit<ViewProps, 'children'> {
|
|
|
81
88
|
* rather than moving faster.
|
|
82
89
|
*/
|
|
83
90
|
speed?: number;
|
|
84
|
-
/**
|
|
91
|
+
/** Minimum gap between the end of one copy and the start of the next. */
|
|
85
92
|
spacing?: number;
|
|
86
93
|
/** Axis the content travels along. */
|
|
87
94
|
direction?: MarqueeDirection;
|
|
@@ -92,6 +99,14 @@ export interface MarqueeProps extends Omit<ViewProps, 'children'> {
|
|
|
92
99
|
reverse?: boolean;
|
|
93
100
|
/** Set false to hold the content where it is. */
|
|
94
101
|
playing?: boolean;
|
|
102
|
+
/** Show the built-in user pause/play control while motion is enabled. */
|
|
103
|
+
showPauseControl?: boolean;
|
|
104
|
+
/** Visible and spoken label for the moving state. */
|
|
105
|
+
pauseLabel?: string;
|
|
106
|
+
/** Visible and spoken label for the user-paused state. */
|
|
107
|
+
playLabel?: string;
|
|
108
|
+
/** Reports changes made by the built-in control. */
|
|
109
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
export function Marquee({
|
|
@@ -102,6 +117,10 @@ export function Marquee({
|
|
|
102
117
|
direction = 'horizontal',
|
|
103
118
|
reverse = false,
|
|
104
119
|
playing = true,
|
|
120
|
+
showPauseControl = true,
|
|
121
|
+
pauseLabel = 'Pause',
|
|
122
|
+
playLabel = 'Play',
|
|
123
|
+
onPlayingChange,
|
|
105
124
|
style,
|
|
106
125
|
onLayout,
|
|
107
126
|
...props
|
|
@@ -114,7 +133,9 @@ export function Marquee({
|
|
|
114
133
|
|
|
115
134
|
const [viewport, setViewport] = useState(0);
|
|
116
135
|
const [content, setContent] = useState(0);
|
|
136
|
+
const [userPaused, setUserPaused] = useState(false);
|
|
117
137
|
const speed = normalizeMarqueeSpeed(speedProp);
|
|
138
|
+
const moving = playing && !userPaused;
|
|
118
139
|
|
|
119
140
|
// The distance from one copy to the same point on the next, and therefore
|
|
120
141
|
// both the layout step and the exact loop length. They are the same number
|
|
@@ -136,7 +157,7 @@ export function Marquee({
|
|
|
136
157
|
useEffect(() => {
|
|
137
158
|
cancelAnimation(offset);
|
|
138
159
|
offset.value = 0;
|
|
139
|
-
if (reducedMotion || !
|
|
160
|
+
if (reducedMotion || !moving || period <= 0 || speed <= 0) return undefined;
|
|
140
161
|
offset.value = withRepeat(
|
|
141
162
|
withTiming(period, {
|
|
142
163
|
duration: (period / speed) * 1000,
|
|
@@ -146,7 +167,7 @@ export function Marquee({
|
|
|
146
167
|
false
|
|
147
168
|
);
|
|
148
169
|
return () => cancelAnimation(offset);
|
|
149
|
-
}, [offset, period,
|
|
170
|
+
}, [offset, period, moving, reducedMotion, speed]);
|
|
150
171
|
|
|
151
172
|
const trackStyle = useAnimatedStyle(() => {
|
|
152
173
|
const travel = (reverse ? offset.value : -offset.value) * flip;
|
|
@@ -200,7 +221,12 @@ export function Marquee({
|
|
|
200
221
|
accessibilityElementsHidden
|
|
201
222
|
importantForAccessibility="no-hide-descendants"
|
|
202
223
|
>
|
|
203
|
-
<
|
|
224
|
+
<InertView
|
|
225
|
+
inert={Platform.OS === 'web' ? true : undefined}
|
|
226
|
+
onLayout={onContentLayout}
|
|
227
|
+
>
|
|
228
|
+
{children}
|
|
229
|
+
</InertView>
|
|
204
230
|
</ScrollView>
|
|
205
231
|
|
|
206
232
|
<Animated.View style={[StyleSheet.absoluteFill, trackStyle]} pointerEvents="box-none">
|
|
@@ -210,18 +236,36 @@ export function Marquee({
|
|
|
210
236
|
const spoken = index === 1;
|
|
211
237
|
const at = (index - 1) * period;
|
|
212
238
|
return (
|
|
213
|
-
<
|
|
239
|
+
<InertView
|
|
214
240
|
key={index}
|
|
215
241
|
style={[styles.copy, horizontal ? { left: at } : { top: at }]}
|
|
216
|
-
pointerEvents=
|
|
242
|
+
pointerEvents={spoken ? 'box-none' : 'none'}
|
|
243
|
+
inert={Platform.OS === 'web' && !spoken ? true : undefined}
|
|
244
|
+
aria-hidden={!spoken}
|
|
217
245
|
accessibilityElementsHidden={!spoken}
|
|
218
246
|
importantForAccessibility={spoken ? 'auto' : 'no-hide-descendants'}
|
|
219
247
|
>
|
|
220
248
|
{children}
|
|
221
|
-
</
|
|
249
|
+
</InertView>
|
|
222
250
|
);
|
|
223
251
|
})}
|
|
224
252
|
</Animated.View>
|
|
253
|
+
{showPauseControl && playing && period > 0 && speed > 0 ? (
|
|
254
|
+
<Pressable
|
|
255
|
+
accessibilityRole="button"
|
|
256
|
+
accessibilityLabel={userPaused ? playLabel : pauseLabel}
|
|
257
|
+
className="absolute bottom-2 end-2 min-h-12 min-w-12 items-center justify-center rounded-full border border-border bg-background/95 px-3"
|
|
258
|
+
onPress={() => {
|
|
259
|
+
const next = !userPaused;
|
|
260
|
+
setUserPaused(next);
|
|
261
|
+
onPlayingChange?.(!next);
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<Text className="text-xs font-medium text-foreground">
|
|
265
|
+
{userPaused ? playLabel : pauseLabel}
|
|
266
|
+
</Text>
|
|
267
|
+
</Pressable>
|
|
268
|
+
) : null}
|
|
225
269
|
</View>
|
|
226
270
|
);
|
|
227
271
|
}
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export const DEFAULT_MARQUEE_SPEED = 40;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Includes the copy just before and just after the viewport. Keeping this
|
|
19
|
+
* fixed prevents tiny content (for example a one-point separator) from
|
|
20
|
+
* multiplying an arbitrary React subtree hundreds of times.
|
|
21
|
+
*/
|
|
22
|
+
export const MAX_MARQUEE_COPIES = 32;
|
|
23
|
+
|
|
17
24
|
export function normalizeMarqueeSpeed(value: number): number {
|
|
18
25
|
if (!Number.isFinite(value)) return DEFAULT_MARQUEE_SPEED;
|
|
19
26
|
return Math.max(value, 0);
|
|
@@ -31,7 +38,23 @@ export function marqueeCopyCount(
|
|
|
31
38
|
): { period: number; count: number } {
|
|
32
39
|
const safeViewport = Number.isFinite(viewport) ? Math.max(viewport, 0) : 0;
|
|
33
40
|
const safeContent = Number.isFinite(content) ? Math.max(content, 0) : 0;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
const naturalPeriod =
|
|
42
|
+
safeContent > 0 ? safeContent + normalizeMarqueeSpacing(spacing) : 0;
|
|
43
|
+
if (naturalPeriod <= 0 || safeViewport <= 0) {
|
|
44
|
+
return { period: naturalPeriod, count: 0 };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Two copies sit outside the viewport to cover one complete loop. If the
|
|
48
|
+
// requested content-and-gap period would need more than the remaining copy
|
|
49
|
+
// budget, widen the layout period (effectively adding whitespace) instead
|
|
50
|
+
// of mounting an unbounded number of identical subtrees.
|
|
51
|
+
const minimumPeriod = safeViewport / (MAX_MARQUEE_COPIES - 2);
|
|
52
|
+
const period = Math.max(naturalPeriod, minimumPeriod);
|
|
53
|
+
return {
|
|
54
|
+
period,
|
|
55
|
+
count: Math.min(
|
|
56
|
+
MAX_MARQUEE_COPIES,
|
|
57
|
+
Math.ceil(safeViewport / period) + 2
|
|
58
|
+
),
|
|
59
|
+
};
|
|
37
60
|
}
|
|
@@ -296,11 +296,15 @@ function PlanTitle({ className, children, ...props }: PlanTitleProps) {
|
|
|
296
296
|
const { title } = planVariants();
|
|
297
297
|
|
|
298
298
|
if (isStreaming && typeof children === 'string') {
|
|
299
|
-
return
|
|
299
|
+
return (
|
|
300
|
+
<Shimmer accessibilityRole="header" textClassName={cn(title(), className)}>
|
|
301
|
+
{children}
|
|
302
|
+
</Shimmer>
|
|
303
|
+
);
|
|
300
304
|
}
|
|
301
305
|
|
|
302
306
|
return (
|
|
303
|
-
<Text accessibilityRole="header" className={cn(title(), className)}
|
|
307
|
+
<Text {...props} accessibilityRole="header" className={cn(title(), className)}>
|
|
304
308
|
{children}
|
|
305
309
|
</Text>
|
|
306
310
|
);
|