panelui-native 0.75.1 → 0.77.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 +1584 -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 +4 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +7 -1
- package/lib/module/components/button/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/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/select/index.js +1 -0
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +1 -0
- package/lib/module/components/slider/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/switch/index.js +1 -0
- package/lib/module/components/switch/index.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/native/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 +442 -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/button/index.d.ts +15 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/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/native/index.d.ts +17 -0
- package/lib/typescript/src/native/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 +1971 -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 +16 -4
- package/src/components/button/index.tsx +20 -1
- 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/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/select/index.tsx +1 -1
- package/src/components/slider/index.tsx +1 -1
- 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/switch/index.tsx +1 -1
- 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/native/index.ts +17 -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',
|
|
@@ -398,7 +408,7 @@ function BottomSheetContent({
|
|
|
398
408
|
// sheet cannot measure RN views directly. Without it the sheet sizes to
|
|
399
409
|
// nothing and the content spills outside its container.
|
|
400
410
|
return (
|
|
401
|
-
<Host matchContents style={{ position: 'absolute' }}>
|
|
411
|
+
<Host matchContents ignoreSafeArea="keyboard" style={{ position: 'absolute' }}>
|
|
402
412
|
<NativeBottomSheet
|
|
403
413
|
isPresented={open}
|
|
404
414
|
onDismiss={dismissible ? close : () => {}}
|
|
@@ -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.
|
|
@@ -207,6 +207,21 @@ export interface ButtonProps
|
|
|
207
207
|
* default capsule.
|
|
208
208
|
*/
|
|
209
209
|
glass?: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* A glyph beside the label, named from the platform's own symbol set rather
|
|
212
|
+
* than passed as an element.
|
|
213
|
+
*
|
|
214
|
+
* This is how a labelled native button gets an icon at all. Elements have to
|
|
215
|
+
* be hosted inside the native tree, and a hosted view inside a labelled
|
|
216
|
+
* button has no width anything can resolve — so a name is not a shortcut
|
|
217
|
+
* here, it is the only form that works. The platform draws it, at its own
|
|
218
|
+
* size, in the label's colour.
|
|
219
|
+
*
|
|
220
|
+
* iOS only, and `native` only. Android's toolkit has no equivalent symbol
|
|
221
|
+
* set, so a button there is its label alone; the drawn button takes
|
|
222
|
+
* `startContent` and always did.
|
|
223
|
+
*/
|
|
224
|
+
systemImage?: string;
|
|
210
225
|
}
|
|
211
226
|
|
|
212
227
|
/**
|
|
@@ -299,6 +314,7 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
299
314
|
startContent,
|
|
300
315
|
endContent,
|
|
301
316
|
native,
|
|
317
|
+
systemImage,
|
|
302
318
|
glass = false,
|
|
303
319
|
accessibilityState,
|
|
304
320
|
...props
|
|
@@ -379,9 +395,12 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
379
395
|
const nativeContent = prominent ? '#ffffff' : contentColor;
|
|
380
396
|
|
|
381
397
|
return (
|
|
382
|
-
<Host matchContents>
|
|
398
|
+
<Host matchContents ignoreSafeArea="keyboard">
|
|
383
399
|
<NativeButton
|
|
384
400
|
label={isStringLabel ? children : undefined}
|
|
401
|
+
// The platform only reads a symbol beside a label it drew itself,
|
|
402
|
+
// so an icon button — which has no label — never carries one.
|
|
403
|
+
systemImage={isStringLabel ? systemImage : undefined}
|
|
385
404
|
variant={NATIVE_VARIANT[resolvedVariant ?? 'primary']}
|
|
386
405
|
disabled={isDisabled}
|
|
387
406
|
/*
|
|
@@ -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();
|
|
@@ -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
|
);
|