panelui-native 0.63.0 → 0.65.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/README.md +5 -0
- package/lib/module/components/badge/index.js +1 -1
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +39 -2
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +2 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +19 -9
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/carousel/index.js +28 -14
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +2 -3
- package/lib/module/components/checkbox/index.js.map +1 -1
- package/lib/module/components/dialog/index.js +2 -2
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +2 -2
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/fab/index.js +4 -3
- package/lib/module/components/fab/index.js.map +1 -1
- package/lib/module/components/field/index.js +41 -6
- package/lib/module/components/field/index.js.map +1 -1
- package/lib/module/components/flow/flow-accessibility.js +84 -0
- package/lib/module/components/flow/flow-accessibility.js.map +1 -0
- package/lib/module/components/flow/index.js +100 -39
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/form/use-form.js +93 -66
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +2 -2
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +2 -2
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/kpi/index.js +3 -1
- package/lib/module/components/kpi/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +3 -3
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +71 -8
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/map/map-accessibility.js +20 -0
- package/lib/module/components/map/map-accessibility.js.map +1 -0
- package/lib/module/components/map/maplibre.js.map +1 -1
- package/lib/module/components/menu/index.js +4 -1
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/number-input/index.js +1 -23
- package/lib/module/components/number-input/index.js.map +1 -1
- package/lib/module/components/number-input/number-input-math.js +44 -0
- package/lib/module/components/number-input/number-input-math.js.map +1 -0
- package/lib/module/components/plot/index.js +2 -2
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/radio-group/index.js +1 -2
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/sortable/index.js +5 -55
- package/lib/module/components/sortable/index.js.map +1 -1
- package/lib/module/components/sortable/reorder.js +63 -0
- package/lib/module/components/sortable/reorder.js.map +1 -0
- package/lib/module/components/swipe/index.js +4 -4
- package/lib/module/components/swipe/index.js.map +1 -1
- package/lib/module/components/switch/index.js +36 -3
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/time-picker/accessibility.js +21 -0
- package/lib/module/components/time-picker/accessibility.js.map +1 -0
- package/lib/module/components/time-picker/index.js +34 -8
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +1325 -0
- package/lib/module/components/waterfall-chart/index.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/modal-isolation-store.js +29 -0
- package/lib/module/primitives/modal-isolation-store.js.map +1 -0
- package/lib/module/primitives/portal.js +36 -4
- package/lib/module/primitives/portal.js.map +1 -1
- package/lib/module/providers/panel-ui-provider.js +27 -6
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/index.d.ts +1 -1
- package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
- package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
- package/lib/typescript/src/components/field/index.d.ts +8 -1
- package/lib/typescript/src/components/field/index.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts +49 -0
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +14 -2
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/use-form.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/kpi/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +13 -5
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/map-accessibility.d.ts +19 -0
- package/lib/typescript/src/components/map/map-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +2 -0
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/number-input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/number-input/number-input-math.d.ts +9 -0
- package/lib/typescript/src/components/number-input/number-input-math.d.ts.map +1 -0
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/reorder.d.ts +14 -0
- package/lib/typescript/src/components/sortable/reorder.d.ts.map +1 -0
- package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts +3 -3
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/accessibility.d.ts +10 -0
- package/lib/typescript/src/components/time-picker/accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts +379 -0
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/modal-isolation-store.d.ts +10 -0
- package/lib/typescript/src/primitives/modal-isolation-store.d.ts.map +1 -0
- package/lib/typescript/src/primitives/portal.d.ts +6 -0
- package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/components/badge/index.tsx +4 -1
- package/src/components/bar-chart/index.tsx +40 -3
- package/src/components/bottom-sheet/index.tsx +3 -3
- package/src/components/button/index.tsx +15 -21
- package/src/components/carousel/index.tsx +42 -20
- package/src/components/checkbox/index.tsx +1 -2
- package/src/components/dialog/index.tsx +3 -3
- package/src/components/drawer/index.tsx +3 -3
- package/src/components/fab/index.tsx +8 -12
- package/src/components/field/index.tsx +52 -10
- package/src/components/flow/flow-accessibility.ts +109 -0
- package/src/components/flow/index.tsx +162 -50
- package/src/components/form/use-form.ts +101 -65
- package/src/components/heatmap-chart/index.tsx +2 -3
- package/src/components/hex-chart/index.tsx +2 -3
- package/src/components/kpi/index.tsx +3 -1
- package/src/components/line-chart/index.tsx +3 -2
- package/src/components/map/index.tsx +110 -7
- package/src/components/map/map-accessibility.ts +41 -0
- package/src/components/map/maplibre.ts +2 -0
- package/src/components/menu/index.tsx +4 -1
- package/src/components/number-input/index.tsx +1 -26
- package/src/components/number-input/number-input-math.ts +50 -0
- package/src/components/plot/index.tsx +2 -1
- package/src/components/radio-group/index.tsx +1 -2
- package/src/components/sortable/index.tsx +12 -64
- package/src/components/sortable/reorder.ts +88 -0
- package/src/components/swipe/index.tsx +7 -4
- package/src/components/switch/index.tsx +42 -6
- package/src/components/time-picker/accessibility.ts +24 -0
- package/src/components/time-picker/index.tsx +48 -2
- package/src/components/waterfall-chart/index.tsx +1589 -0
- package/src/index.ts +23 -0
- package/src/primitives/modal-isolation-store.ts +31 -0
- package/src/primitives/portal.tsx +34 -4
- package/src/providers/panel-ui-provider.tsx +41 -9
- package/theme.css +30 -3
|
@@ -1083,6 +1083,15 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
|
|
|
1083
1083
|
const width = plot.width;
|
|
1084
1084
|
const height = plot.height;
|
|
1085
1085
|
|
|
1086
|
+
/*
|
|
1087
|
+
* The readout's own height, measured rather than assumed. Sideways it has to
|
|
1088
|
+
* be clamped inside the plot vertically, and how tall it is depends on how
|
|
1089
|
+
* many series are listed in it — a constant here would either let a
|
|
1090
|
+
* three-series readout hang off the bottom or reserve room a one-series
|
|
1091
|
+
* readout never uses.
|
|
1092
|
+
*/
|
|
1093
|
+
const labelHeight = useSharedValue(0);
|
|
1094
|
+
|
|
1086
1095
|
/*
|
|
1087
1096
|
* Declared inside the memo, next to its callers: a worklet may only call
|
|
1088
1097
|
* another worklet, and the rule is enforced by crashing rather than warning.
|
|
@@ -1118,8 +1127,17 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
|
|
|
1118
1127
|
});
|
|
1119
1128
|
}, [total, left, top, width, height, horizontal, activeIndex, setActiveIndexJS]);
|
|
1120
1129
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1130
|
+
/*
|
|
1131
|
+
* The readout centres over its band and is clamped inside the plot, so it
|
|
1132
|
+
* never runs off the edge at the first or last one.
|
|
1133
|
+
*
|
|
1134
|
+
* Which axis the band runs along is the whole difference between the two
|
|
1135
|
+
* orientations. Upright, the bands are side by side, so the readout slides
|
|
1136
|
+
* across and sits above them all. Sideways, the bands are stacked down the
|
|
1137
|
+
* plot, so it slides *down* to the row it is describing and stays centred
|
|
1138
|
+
* across — held at the top instead, it would name the row under the finger
|
|
1139
|
+
* while covering the first one, which is the row a reader checks it against.
|
|
1140
|
+
*/
|
|
1123
1141
|
const labelStyle = useAnimatedStyle(() => {
|
|
1124
1142
|
const index = activeIndex.value;
|
|
1125
1143
|
if (index < 0 || !total) return { opacity: 0 };
|
|
@@ -1130,7 +1148,23 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
|
|
|
1130
1148
|
plot.left + plot.width - half,
|
|
1131
1149
|
Math.max(plot.left + half, horizontal ? plot.left + plot.width / 2 : centre)
|
|
1132
1150
|
);
|
|
1133
|
-
|
|
1151
|
+
|
|
1152
|
+
if (!horizontal) {
|
|
1153
|
+
return { opacity: 1, transform: [{ translateX: clamped - half }] };
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Until the first measurement lands the height is zero, which clamps to
|
|
1157
|
+
// the top of the plot — the same place it used to sit, rather than a jump
|
|
1158
|
+
// from somewhere it never was.
|
|
1159
|
+
const tall = labelHeight.value;
|
|
1160
|
+
const y = Math.min(
|
|
1161
|
+
plot.top + Math.max(plot.height - tall, 0),
|
|
1162
|
+
Math.max(plot.top, centre - tall / 2)
|
|
1163
|
+
);
|
|
1164
|
+
return {
|
|
1165
|
+
opacity: 1,
|
|
1166
|
+
transform: [{ translateX: clamped - half }, { translateY: y }],
|
|
1167
|
+
};
|
|
1134
1168
|
});
|
|
1135
1169
|
|
|
1136
1170
|
const active = activeIndexJS >= 0 ? data[activeIndexJS] : null;
|
|
@@ -1151,6 +1185,9 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
|
|
|
1151
1185
|
>
|
|
1152
1186
|
{active ? (
|
|
1153
1187
|
<View
|
|
1188
|
+
onLayout={(event) => {
|
|
1189
|
+
labelHeight.value = event.nativeEvent.layout.height;
|
|
1190
|
+
}}
|
|
1154
1191
|
className={cn(
|
|
1155
1192
|
'rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg',
|
|
1156
1193
|
className
|
|
@@ -31,7 +31,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
31
31
|
import { tv } from 'tailwind-variants';
|
|
32
32
|
import { getNativeUI } from '../../native';
|
|
33
33
|
import { XIcon } from '../../icons';
|
|
34
|
-
import {
|
|
34
|
+
import { ModalPortal } from '../../primitives/portal';
|
|
35
35
|
import { Scrim } from '../../primitives/scrim';
|
|
36
36
|
import { Text, textChildren } from '../../primitives/text';
|
|
37
37
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
@@ -435,7 +435,7 @@ function BottomSheetContent({
|
|
|
435
435
|
if (!open) return null;
|
|
436
436
|
|
|
437
437
|
return (
|
|
438
|
-
<
|
|
438
|
+
<ModalPortal>
|
|
439
439
|
{/* Portal content mounts under PortalHost, outside this provider's
|
|
440
440
|
subtree — re-provide the context so nested consumers keep working. */}
|
|
441
441
|
<BottomSheetContext.Provider value={context}>
|
|
@@ -507,7 +507,7 @@ function BottomSheetContent({
|
|
|
507
507
|
</GestureDetector>
|
|
508
508
|
</View>
|
|
509
509
|
</BottomSheetContext.Provider>
|
|
510
|
-
</
|
|
510
|
+
</ModalPortal>
|
|
511
511
|
);
|
|
512
512
|
}
|
|
513
513
|
|
|
@@ -42,8 +42,9 @@ const buttonVariants = tv({
|
|
|
42
42
|
},
|
|
43
43
|
destructive: {
|
|
44
44
|
root: 'border-destructive bg-destructive shadow-sm',
|
|
45
|
-
label: 'text-
|
|
46
|
-
spinner:
|
|
45
|
+
label: 'text-destructive-solid-foreground',
|
|
46
|
+
spinner:
|
|
47
|
+
'border-destructive-solid-foreground/32 border-t-destructive-solid-foreground',
|
|
47
48
|
},
|
|
48
49
|
/** Neutral surface for third-party sign-in, sized for a full-width stack. */
|
|
49
50
|
social: {
|
|
@@ -53,8 +54,8 @@ const buttonVariants = tv({
|
|
|
53
54
|
},
|
|
54
55
|
},
|
|
55
56
|
size: {
|
|
56
|
-
sm: { root: 'h-9 gap-1.5 px-2.5', label: 'text-sm' },
|
|
57
|
-
md: { root: 'h-11 px-4', label: 'text-base' },
|
|
57
|
+
sm: { root: 'h-9 min-w-9 gap-1.5 px-2.5', label: 'text-sm' },
|
|
58
|
+
md: { root: 'h-11 min-w-11 px-4', label: 'text-base' },
|
|
58
59
|
lg: { root: 'h-12 px-6', label: 'text-lg' },
|
|
59
60
|
icon: { root: 'h-11 w-11 px-0' },
|
|
60
61
|
},
|
|
@@ -134,18 +135,16 @@ const ATTACHED = 'rounded-none border-transparent shadow-none active:bg-accent';
|
|
|
134
135
|
|
|
135
136
|
const SPINNER_SIZE = { sm: 'sm', md: 'sm', lg: 'md', icon: 'sm' } as const;
|
|
136
137
|
|
|
138
|
+
/** Extra room around the styled sizes so even the compact button reaches 48dp. */
|
|
139
|
+
const BUTTON_HIT_SLOP = { sm: 6, md: 2, lg: 0, icon: 2 } as const;
|
|
140
|
+
|
|
137
141
|
/**
|
|
138
142
|
* The theme token each variant's content reads against. Icons in the content
|
|
139
143
|
* slots inherit the resolved value, so they follow the theme automatically.
|
|
140
|
-
*
|
|
141
|
-
* `destructive` is absent on purpose: its background is a saturated red in
|
|
142
|
-
* every theme, so its content is always white rather than a themed token.
|
|
143
144
|
*/
|
|
144
|
-
const CONTENT_COLOR_VAR: Record<
|
|
145
|
-
Exclude<NonNullable<ButtonVariantProps['variant']>, 'destructive'>,
|
|
146
|
-
string
|
|
147
|
-
> = {
|
|
145
|
+
const CONTENT_COLOR_VAR: Record<NonNullable<ButtonVariantProps['variant']>, string> = {
|
|
148
146
|
primary: '--color-primary-foreground',
|
|
147
|
+
destructive: '--color-destructive-solid-foreground',
|
|
149
148
|
secondary: '--color-secondary-foreground',
|
|
150
149
|
outline: '--color-foreground',
|
|
151
150
|
ghost: '--color-foreground',
|
|
@@ -328,17 +327,9 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
328
327
|
// Icons in the content slots inherit this, so they stay legible when the
|
|
329
328
|
// theme inverts the button's background. Without it every caller has to
|
|
330
329
|
// hardcode a hex that is wrong in one theme or the other.
|
|
331
|
-
const themedColor = useCSSVariable(
|
|
332
|
-
CONTENT_COLOR_VAR[
|
|
333
|
-
resolvedVariant === 'destructive' ? 'primary' : (resolvedVariant ?? 'primary')
|
|
334
|
-
]
|
|
335
|
-
);
|
|
330
|
+
const themedColor = useCSSVariable(CONTENT_COLOR_VAR[resolvedVariant ?? 'primary']);
|
|
336
331
|
const contentColor =
|
|
337
|
-
|
|
338
|
-
? '#ffffff'
|
|
339
|
-
: typeof themedColor === 'string'
|
|
340
|
-
? themedColor
|
|
341
|
-
: undefined;
|
|
332
|
+
typeof themedColor === 'string' ? themedColor : undefined;
|
|
342
333
|
|
|
343
334
|
if (nativeUI) {
|
|
344
335
|
const { Host, Button: NativeButton, RNHostView } = nativeUI;
|
|
@@ -446,6 +437,9 @@ export const Button = forwardRef<View, ButtonProps>(
|
|
|
446
437
|
accessibilityRole="button"
|
|
447
438
|
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
448
439
|
disabled={isDisabled}
|
|
440
|
+
// Joined segments cannot borrow room from each other without making
|
|
441
|
+
// two actions claim the same pixels. ButtonGroup owns that layout.
|
|
442
|
+
hitSlop={attached ? undefined : BUTTON_HIT_SLOP[resolvedSize ?? 'md']}
|
|
449
443
|
// Before the spread, so a caller can still ask for the scale back.
|
|
450
444
|
pressScale={attached ? 1 : undefined}
|
|
451
445
|
className={root({ className: cn(attached && ATTACHED, className) })}
|
|
@@ -207,7 +207,7 @@ export interface CarouselProps extends ViewProps {
|
|
|
207
207
|
autoplay?: boolean;
|
|
208
208
|
/** Milliseconds each slide is held when `autoplay` is set. */
|
|
209
209
|
autoplayInterval?: number;
|
|
210
|
-
/** Controlled active slide. */
|
|
210
|
+
/** Controlled active slide. Requests move visually only after this value changes. */
|
|
211
211
|
index?: number;
|
|
212
212
|
/** Starting slide when uncontrolled. */
|
|
213
213
|
defaultIndex?: number;
|
|
@@ -269,28 +269,37 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
|
|
|
269
269
|
[isControlled, onIndexChange]
|
|
270
270
|
);
|
|
271
271
|
|
|
272
|
-
const
|
|
272
|
+
const animateTo = useCallback(
|
|
273
273
|
(target: number) => {
|
|
274
|
-
if (count <= 0) return;
|
|
275
|
-
const settled = loop ? wrap(target, count) : Math.max(0, Math.min(target, count - 1));
|
|
276
|
-
|
|
277
274
|
if (reducedMotion) {
|
|
278
|
-
progress.value =
|
|
275
|
+
progress.value = target;
|
|
279
276
|
} else if (loop) {
|
|
280
277
|
// Spring to the nearest representation of the target rather than to the
|
|
281
278
|
// target itself, so a wrap from the last slide to the first travels one
|
|
282
279
|
// step forward instead of winding all the way back through the run.
|
|
283
|
-
const shortest = progress.value + distance(
|
|
280
|
+
const shortest = progress.value + distance(target, progress.value, count, true);
|
|
284
281
|
progress.value = withSpring(shortest, SPRING, (finished) => {
|
|
285
282
|
if (finished) progress.value = wrap(progress.value, count);
|
|
286
283
|
});
|
|
287
284
|
} else {
|
|
288
|
-
progress.value = withSpring(
|
|
285
|
+
progress.value = withSpring(target, SPRING);
|
|
289
286
|
}
|
|
287
|
+
},
|
|
288
|
+
[count, loop, progress, reducedMotion]
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const scrollTo = useCallback(
|
|
292
|
+
(target: number) => {
|
|
293
|
+
if (count <= 0) return;
|
|
294
|
+
const settled = loop ? wrap(target, count) : Math.max(0, Math.min(target, count - 1));
|
|
290
295
|
|
|
296
|
+
// A controlled request belongs to its owner. The finger may move the run,
|
|
297
|
+
// but after release it returns to the current prop until the owner accepts
|
|
298
|
+
// the request by changing that prop.
|
|
299
|
+
animateTo(isControlled ? index : settled);
|
|
291
300
|
setIndex(settled);
|
|
292
301
|
},
|
|
293
|
-
[count,
|
|
302
|
+
[animateTo, count, index, isControlled, loop, setIndex]
|
|
294
303
|
);
|
|
295
304
|
|
|
296
305
|
const next = useCallback(() => scrollTo(index + 1), [index, scrollTo]);
|
|
@@ -302,8 +311,8 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
|
|
|
302
311
|
useEffect(() => {
|
|
303
312
|
if (!isControlled || indexProp === undefined || count <= 0) return;
|
|
304
313
|
if (Math.abs(progress.value - indexProp) < 0.001) return;
|
|
305
|
-
|
|
306
|
-
}, [isControlled, indexProp, count, progress
|
|
314
|
+
animateTo(indexProp);
|
|
315
|
+
}, [animateTo, isControlled, indexProp, count, progress]);
|
|
307
316
|
|
|
308
317
|
/*
|
|
309
318
|
* Autoplay stops for good the first time a finger lands, rather than pausing.
|
|
@@ -688,16 +697,30 @@ function CarouselDots({
|
|
|
688
697
|
key={dot}
|
|
689
698
|
disabled={!interactive}
|
|
690
699
|
onPress={() => scrollTo(dot)}
|
|
691
|
-
hitSlop={6}
|
|
692
700
|
accessibilityRole="tab"
|
|
693
701
|
accessibilityLabel={`Slide ${dot + 1} of ${count}`}
|
|
694
702
|
accessibilityState={{ selected: active }}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
703
|
+
// 24, not the 48 the buttons get. Dots sit a few points apart, so
|
|
704
|
+
// a 48 box would either overlap its neighbours — making a tap near
|
|
705
|
+
// the join land on the wrong slide — or push a five-slide run out
|
|
706
|
+
// to the width of the screen. 24 clears the minimum with the pitch
|
|
707
|
+
// still wider than the target, so no two dots contend for a touch.
|
|
708
|
+
className={cn('items-center justify-center', interactive && 'h-6 w-6')}
|
|
709
|
+
>
|
|
710
|
+
<View
|
|
711
|
+
className={cn(
|
|
712
|
+
'rounded-full',
|
|
713
|
+
horizontal
|
|
714
|
+
? active
|
|
715
|
+
? 'h-1 w-4'
|
|
716
|
+
: 'h-1 w-1'
|
|
717
|
+
: active
|
|
718
|
+
? 'h-4 w-1'
|
|
719
|
+
: 'h-1 w-1',
|
|
720
|
+
active ? 'bg-foreground' : 'bg-foreground/30'
|
|
721
|
+
)}
|
|
722
|
+
/>
|
|
723
|
+
</Pressable>
|
|
701
724
|
);
|
|
702
725
|
})}
|
|
703
726
|
</View>
|
|
@@ -723,12 +746,11 @@ function makeArrow(direction: 'previous' | 'next') {
|
|
|
723
746
|
<Pressable
|
|
724
747
|
disabled={disabled}
|
|
725
748
|
onPress={direction === 'next' ? next : previous}
|
|
726
|
-
hitSlop={8}
|
|
727
749
|
accessibilityRole="button"
|
|
728
750
|
accessibilityLabel={direction === 'next' ? 'Next slide' : 'Previous slide'}
|
|
729
751
|
accessibilityState={{ disabled }}
|
|
730
752
|
className={cn(
|
|
731
|
-
'items-center justify-center rounded-full
|
|
753
|
+
'h-12 w-12 items-center justify-center rounded-full',
|
|
732
754
|
disabled ? 'opacity-30' : 'active:bg-foreground/10',
|
|
733
755
|
className
|
|
734
756
|
)}
|
|
@@ -13,7 +13,7 @@ import { Text } from '../../primitives/text';
|
|
|
13
13
|
|
|
14
14
|
const checkboxVariants = tv({
|
|
15
15
|
slots: {
|
|
16
|
-
row: 'flex-row items-center gap-2.5',
|
|
16
|
+
row: 'min-h-12 min-w-12 flex-row items-center gap-2.5',
|
|
17
17
|
box: 'h-5 w-5 items-center justify-center rounded-md border border-input bg-background',
|
|
18
18
|
fill: 'absolute inset-0 items-center justify-center rounded-md bg-primary',
|
|
19
19
|
content: 'flex-1 gap-0.5',
|
|
@@ -130,7 +130,6 @@ export const Checkbox = forwardRef<View, CheckboxProps>(
|
|
|
130
130
|
disabled={disabled}
|
|
131
131
|
// An indeterminate box resolves upward: the press turns the group on.
|
|
132
132
|
onPress={() => onCheckedChange?.(indeterminate ? true : !checked)}
|
|
133
|
-
hitSlop={8}
|
|
134
133
|
// A description makes the row two lines tall — align the box to the
|
|
135
134
|
// label rather than centring it against the whole block.
|
|
136
135
|
className={slots.row({
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { Pressable, View, type ViewProps } from 'react-native';
|
|
14
14
|
import Animated, { FadeIn, FadeOut, ZoomIn } from 'react-native-reanimated';
|
|
15
15
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
16
|
-
import {
|
|
16
|
+
import { ModalPortal } from '../../primitives/portal';
|
|
17
17
|
import { Scrim } from '../../primitives/scrim';
|
|
18
18
|
import { Text, type TextProps, textChildren } from '../../primitives/text';
|
|
19
19
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
@@ -111,7 +111,7 @@ function DialogContent({
|
|
|
111
111
|
if (!open) return null;
|
|
112
112
|
|
|
113
113
|
return (
|
|
114
|
-
<
|
|
114
|
+
<ModalPortal>
|
|
115
115
|
{/* Portal content mounts under PortalHost, outside this provider's
|
|
116
116
|
subtree — re-provide the context so Dialog.Close etc. keep working. */}
|
|
117
117
|
<DialogContext.Provider value={context}>
|
|
@@ -142,7 +142,7 @@ function DialogContent({
|
|
|
142
142
|
</Animated.View>
|
|
143
143
|
</Animated.View>
|
|
144
144
|
</DialogContext.Provider>
|
|
145
|
-
</
|
|
145
|
+
</ModalPortal>
|
|
146
146
|
);
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -84,7 +84,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
84
84
|
import { tv } from 'tailwind-variants';
|
|
85
85
|
import { useCSSVariable } from 'uniwind';
|
|
86
86
|
import { XIcon } from '../../icons';
|
|
87
|
-
import {
|
|
87
|
+
import { ModalPortal } from '../../primitives/portal';
|
|
88
88
|
import { Scrim } from '../../primitives/scrim';
|
|
89
89
|
import { Text, textChildren } from '../../primitives/text';
|
|
90
90
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
@@ -458,7 +458,7 @@ function DrawerContent({
|
|
|
458
458
|
};
|
|
459
459
|
|
|
460
460
|
return (
|
|
461
|
-
<
|
|
461
|
+
<ModalPortal>
|
|
462
462
|
{/* Portal content mounts under PortalHost, outside this provider's
|
|
463
463
|
subtree — re-provide the context so Drawer.Close keeps working. */}
|
|
464
464
|
<DrawerContext.Provider value={{ open, setOpen }}>
|
|
@@ -561,7 +561,7 @@ function DrawerContent({
|
|
|
561
561
|
</View>
|
|
562
562
|
</DirectionContext.Provider>
|
|
563
563
|
</DrawerContext.Provider>
|
|
564
|
-
</
|
|
564
|
+
</ModalPortal>
|
|
565
565
|
);
|
|
566
566
|
}
|
|
567
567
|
|
|
@@ -129,7 +129,10 @@ const fabVariants = tv({
|
|
|
129
129
|
primary: { root: 'bg-primary', label: 'text-primary-foreground' },
|
|
130
130
|
secondary: { root: 'bg-secondary', label: 'text-secondary-foreground' },
|
|
131
131
|
surface: { root: 'border border-border bg-popover', label: 'text-foreground' },
|
|
132
|
-
destructive: {
|
|
132
|
+
destructive: {
|
|
133
|
+
root: 'bg-destructive',
|
|
134
|
+
label: 'text-destructive-solid-foreground',
|
|
135
|
+
},
|
|
133
136
|
},
|
|
134
137
|
disabled: {
|
|
135
138
|
true: { root: 'opacity-[0.64] shadow-none' },
|
|
@@ -158,8 +161,9 @@ export type FabSize = NonNullable<FabVariantProps['size']>;
|
|
|
158
161
|
export type FabVariant = NonNullable<FabVariantProps['variant']>;
|
|
159
162
|
|
|
160
163
|
/** The theme token each variant's icon reads against. */
|
|
161
|
-
const CONTENT_COLOR_VAR: Record<
|
|
164
|
+
const CONTENT_COLOR_VAR: Record<FabVariant, string> = {
|
|
162
165
|
primary: '--color-primary-foreground',
|
|
166
|
+
destructive: '--color-destructive-solid-foreground',
|
|
163
167
|
secondary: '--color-secondary-foreground',
|
|
164
168
|
surface: '--color-foreground',
|
|
165
169
|
};
|
|
@@ -234,17 +238,9 @@ const FabRoot = forwardRef<View, FabProps>(
|
|
|
234
238
|
disabled,
|
|
235
239
|
});
|
|
236
240
|
|
|
237
|
-
const themed = useCSSVariable(
|
|
238
|
-
CONTENT_COLOR_VAR[
|
|
239
|
-
variant === 'destructive' ? 'primary' : (variant ?? 'primary')
|
|
240
|
-
]
|
|
241
|
-
);
|
|
241
|
+
const themed = useCSSVariable(CONTENT_COLOR_VAR[variant ?? 'primary']);
|
|
242
242
|
const contentColor =
|
|
243
|
-
|
|
244
|
-
? '#ffffff'
|
|
245
|
-
: typeof themed === 'string'
|
|
246
|
-
? themed
|
|
247
|
-
: undefined;
|
|
243
|
+
typeof themed === 'string' ? themed : undefined;
|
|
248
244
|
|
|
249
245
|
const handlePress = useCallback(() => {
|
|
250
246
|
if (haptics) selectionTick();
|
|
@@ -18,7 +18,12 @@
|
|
|
18
18
|
* <Field.Title>Marketing emails</Field.Title>
|
|
19
19
|
* <Field.Description>Product updates, at most weekly.</Field.Description>
|
|
20
20
|
* </Field.Content>
|
|
21
|
-
* <Switch
|
|
21
|
+
* <Switch
|
|
22
|
+
* value={subscribed}
|
|
23
|
+
* onValueChange={setSubscribed}
|
|
24
|
+
* accessibilityLabel="Marketing emails"
|
|
25
|
+
* accessibilityHint="Product updates, at most weekly."
|
|
26
|
+
* />
|
|
22
27
|
* </Field>
|
|
23
28
|
* ```
|
|
24
29
|
*/
|
|
@@ -26,6 +31,7 @@ import {
|
|
|
26
31
|
createContext,
|
|
27
32
|
forwardRef,
|
|
28
33
|
useContext,
|
|
34
|
+
useId,
|
|
29
35
|
useMemo,
|
|
30
36
|
type ReactNode,
|
|
31
37
|
} from 'react';
|
|
@@ -73,14 +79,22 @@ interface FieldState {
|
|
|
73
79
|
invalid: boolean;
|
|
74
80
|
disabled: boolean;
|
|
75
81
|
required: boolean;
|
|
82
|
+
relationshipIds: string[];
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
const FieldContext = createContext<FieldState>({
|
|
79
86
|
invalid: false,
|
|
80
87
|
disabled: false,
|
|
81
88
|
required: false,
|
|
89
|
+
relationshipIds: [],
|
|
82
90
|
});
|
|
83
91
|
|
|
92
|
+
/** Android can announce the label, help and error text rendered beside a control. */
|
|
93
|
+
export function useFieldLabelledBy(): string[] | undefined {
|
|
94
|
+
const { relationshipIds } = useContext(FieldContext);
|
|
95
|
+
return relationshipIds.length > 0 ? relationshipIds : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
export interface FieldProps extends ViewProps {
|
|
85
99
|
className?: string;
|
|
86
100
|
/** `horizontal` puts a label beside the control instead of above it. */
|
|
@@ -107,9 +121,19 @@ const FieldRoot = forwardRef<View, FieldProps>(
|
|
|
107
121
|
ref
|
|
108
122
|
) => {
|
|
109
123
|
const { root } = fieldVariants({ orientation, disabled });
|
|
124
|
+
const id = useId().replace(/:/g, '');
|
|
125
|
+
const labelId = `panelui-field-${id}-label`;
|
|
126
|
+
const titleId = `panelui-field-${id}-title`;
|
|
127
|
+
const descriptionId = `panelui-field-${id}-description`;
|
|
128
|
+
const errorId = `panelui-field-${id}-error`;
|
|
110
129
|
const state = useMemo(
|
|
111
|
-
() => ({
|
|
112
|
-
|
|
130
|
+
() => ({
|
|
131
|
+
invalid,
|
|
132
|
+
disabled,
|
|
133
|
+
required,
|
|
134
|
+
relationshipIds: [labelId, titleId, descriptionId, errorId],
|
|
135
|
+
}),
|
|
136
|
+
[invalid, disabled, required, labelId, titleId, descriptionId, errorId]
|
|
113
137
|
);
|
|
114
138
|
|
|
115
139
|
return (
|
|
@@ -145,7 +169,7 @@ FieldContent.displayName = 'Field.Content';
|
|
|
145
169
|
export interface FieldLabelProps extends LabelProps {}
|
|
146
170
|
|
|
147
171
|
const FieldLabel = forwardRef<View, FieldLabelProps>(
|
|
148
|
-
({ isRequired, isInvalid, isDisabled, ...props }, ref) => {
|
|
172
|
+
({ isRequired, isInvalid, isDisabled, nativeID, ...props }, ref) => {
|
|
149
173
|
const ctx = useContext(FieldContext);
|
|
150
174
|
return (
|
|
151
175
|
<Label
|
|
@@ -153,6 +177,7 @@ const FieldLabel = forwardRef<View, FieldLabelProps>(
|
|
|
153
177
|
isRequired={isRequired ?? ctx.required}
|
|
154
178
|
isInvalid={isInvalid ?? ctx.invalid}
|
|
155
179
|
isDisabled={isDisabled ?? ctx.disabled}
|
|
180
|
+
nativeID={nativeID ?? ctx.relationshipIds[0]}
|
|
156
181
|
{...props}
|
|
157
182
|
/>
|
|
158
183
|
);
|
|
@@ -165,10 +190,17 @@ export interface FieldDescriptionProps extends TextProps {
|
|
|
165
190
|
}
|
|
166
191
|
|
|
167
192
|
const FieldDescription = forwardRef<RNText, FieldDescriptionProps>(
|
|
168
|
-
({ className, ...props }, ref) => {
|
|
169
|
-
const { disabled } = useContext(FieldContext);
|
|
193
|
+
({ className, nativeID, ...props }, ref) => {
|
|
194
|
+
const { disabled, relationshipIds } = useContext(FieldContext);
|
|
170
195
|
const { description } = fieldVariants({ disabled });
|
|
171
|
-
return
|
|
196
|
+
return (
|
|
197
|
+
<Text
|
|
198
|
+
ref={ref}
|
|
199
|
+
nativeID={nativeID ?? relationshipIds[2]}
|
|
200
|
+
className={description({ className })}
|
|
201
|
+
{...props}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
172
204
|
}
|
|
173
205
|
);
|
|
174
206
|
FieldDescription.displayName = 'Field.Description';
|
|
@@ -186,7 +218,8 @@ export interface FieldErrorProps extends Omit<ViewProps, 'children'> {
|
|
|
186
218
|
}
|
|
187
219
|
|
|
188
220
|
const FieldError = forwardRef<View, FieldErrorProps>(
|
|
189
|
-
({ className, errors, children, ...props }, ref) => {
|
|
221
|
+
({ className, errors, children, nativeID, ...props }, ref) => {
|
|
222
|
+
const { relationshipIds } = useContext(FieldContext);
|
|
190
223
|
const messages = useMemo(() => {
|
|
191
224
|
if (!errors) return null;
|
|
192
225
|
const seen = new Set<string>();
|
|
@@ -213,6 +246,7 @@ const FieldError = forwardRef<View, FieldErrorProps>(
|
|
|
213
246
|
// accessibilityRole list has no alert, the ARIA-aligned one does.
|
|
214
247
|
role="alert"
|
|
215
248
|
accessibilityLiveRegion="polite"
|
|
249
|
+
nativeID={nativeID ?? relationshipIds[3]}
|
|
216
250
|
className={error({ className })}
|
|
217
251
|
{...props}
|
|
218
252
|
>
|
|
@@ -307,9 +341,17 @@ export interface FieldTitleProps extends TextProps {
|
|
|
307
341
|
}
|
|
308
342
|
|
|
309
343
|
const FieldTitle = forwardRef<RNText, FieldTitleProps>(
|
|
310
|
-
({ className, ...props }, ref) => {
|
|
344
|
+
({ className, nativeID, ...props }, ref) => {
|
|
311
345
|
const { title } = fieldVariants();
|
|
312
|
-
|
|
346
|
+
const { relationshipIds } = useContext(FieldContext);
|
|
347
|
+
return (
|
|
348
|
+
<Text
|
|
349
|
+
ref={ref}
|
|
350
|
+
nativeID={nativeID ?? relationshipIds[1]}
|
|
351
|
+
className={title({ className })}
|
|
352
|
+
{...props}
|
|
353
|
+
/>
|
|
354
|
+
);
|
|
313
355
|
}
|
|
314
356
|
);
|
|
315
357
|
FieldTitle.displayName = 'Field.Title';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { FlowConnection, FlowNodePosition } from '.';
|
|
2
|
+
import type { FlowRect } from './flow-paths';
|
|
3
|
+
|
|
4
|
+
export const FLOW_MOVE_ACTIONS = [
|
|
5
|
+
{ name: 'flow-move-up', label: 'Move up', dx: 0, dy: -1 },
|
|
6
|
+
{ name: 'flow-move-right', label: 'Move right', dx: 1, dy: 0 },
|
|
7
|
+
{ name: 'flow-move-down', label: 'Move down', dx: 0, dy: 1 },
|
|
8
|
+
{ name: 'flow-move-left', label: 'Move left', dx: -1, dy: 0 },
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export const FLOW_DELETE_ACTION = { name: 'flow-delete', label: 'Delete node' } as const;
|
|
12
|
+
|
|
13
|
+
export interface FlowAccessibilityNode {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FlowAccessibilityHandle {
|
|
19
|
+
key: string;
|
|
20
|
+
node: string;
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
type: 'source' | 'target' | 'both';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FlowConnectionAction {
|
|
27
|
+
name: string;
|
|
28
|
+
label: string;
|
|
29
|
+
connection: FlowConnection;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Keep pointer and assistive-technology movement inside the same group bounds. */
|
|
33
|
+
export function clampNodePosition(
|
|
34
|
+
rect: FlowRect,
|
|
35
|
+
bounds: FlowRect | undefined,
|
|
36
|
+
position: FlowNodePosition
|
|
37
|
+
): FlowNodePosition {
|
|
38
|
+
'worklet';
|
|
39
|
+
if (!bounds) return position;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
x: Math.min(Math.max(position.x, bounds.x), bounds.x + bounds.width - rect.width),
|
|
43
|
+
y: Math.min(Math.max(position.y, bounds.y), bounds.y + bounds.height - rect.height),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function moveNodePosition(
|
|
48
|
+
rect: FlowRect,
|
|
49
|
+
bounds: FlowRect | undefined,
|
|
50
|
+
actionName: string,
|
|
51
|
+
step: number
|
|
52
|
+
): FlowNodePosition | undefined {
|
|
53
|
+
const action = FLOW_MOVE_ACTIONS.find((entry) => entry.name === actionName);
|
|
54
|
+
if (!action) return undefined;
|
|
55
|
+
|
|
56
|
+
return clampNodePosition(rect, bounds, {
|
|
57
|
+
x: rect.x + action.dx * Math.max(1, step),
|
|
58
|
+
y: rect.y + action.dy * Math.max(1, step),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Build connection choices from the same registered nodes and handles used by drag hit-testing. */
|
|
63
|
+
export function getFlowConnectionActions(
|
|
64
|
+
sourceNode: string,
|
|
65
|
+
nodes: FlowAccessibilityNode[],
|
|
66
|
+
handles: FlowAccessibilityHandle[]
|
|
67
|
+
): FlowConnectionAction[] {
|
|
68
|
+
const sources = handles.filter(
|
|
69
|
+
(handle) => handle.node === sourceNode && handle.type !== 'target'
|
|
70
|
+
);
|
|
71
|
+
if (sources.length === 0) return [];
|
|
72
|
+
|
|
73
|
+
const actions: FlowConnectionAction[] = [];
|
|
74
|
+
for (const source of sources) {
|
|
75
|
+
for (const targetNode of nodes) {
|
|
76
|
+
if (targetNode.id === sourceNode) continue;
|
|
77
|
+
const targets = handles.filter(
|
|
78
|
+
(handle) => handle.node === targetNode.id && handle.type !== 'source'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (targets.length === 0) {
|
|
82
|
+
actions.push({
|
|
83
|
+
name: `flow-connect:${source.key}->${targetNode.id}`,
|
|
84
|
+
label: `Connect ${source.label} to ${targetNode.label}`,
|
|
85
|
+
connection: {
|
|
86
|
+
source: sourceNode,
|
|
87
|
+
sourceHandle: source.id,
|
|
88
|
+
target: targetNode.id,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const target of targets) {
|
|
95
|
+
actions.push({
|
|
96
|
+
name: `flow-connect:${source.key}->${target.key}`,
|
|
97
|
+
label: `Connect ${source.label} to ${targetNode.label}, ${target.label}`,
|
|
98
|
+
connection: {
|
|
99
|
+
source: sourceNode,
|
|
100
|
+
sourceHandle: source.id,
|
|
101
|
+
target: target.node,
|
|
102
|
+
targetHandle: target.id,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return actions;
|
|
109
|
+
}
|