panelui-native 0.83.0 → 0.85.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.
Files changed (74) hide show
  1. package/README.md +17 -8
  2. package/lib/module/components/bottom-sheet/index.js +3 -2
  3. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  4. package/lib/module/components/button/index.js +37 -16
  5. package/lib/module/components/button/index.js.map +1 -1
  6. package/lib/module/components/circular-text/circular-text-geometry.js +63 -0
  7. package/lib/module/components/circular-text/circular-text-geometry.js.map +1 -0
  8. package/lib/module/components/circular-text/index.js +146 -0
  9. package/lib/module/components/circular-text/index.js.map +1 -0
  10. package/lib/module/components/flip-card/index.js +338 -0
  11. package/lib/module/components/flip-card/index.js.map +1 -0
  12. package/lib/module/components/marquee/index.js +12 -5
  13. package/lib/module/components/marquee/index.js.map +1 -1
  14. package/lib/module/components/panelside/index.js +15 -417
  15. package/lib/module/components/panelside/index.js.map +1 -1
  16. package/lib/module/components/popover/index.js +120 -7
  17. package/lib/module/components/popover/index.js.map +1 -1
  18. package/lib/module/components/select/index.js +3 -2
  19. package/lib/module/components/select/index.js.map +1 -1
  20. package/lib/module/components/slider/index.js +16 -5
  21. package/lib/module/components/slider/index.js.map +1 -1
  22. package/lib/module/components/switch/index.js +28 -7
  23. package/lib/module/components/switch/index.js.map +1 -1
  24. package/lib/module/icons/index.js +24 -4
  25. package/lib/module/icons/index.js.map +1 -1
  26. package/lib/module/index.js +2 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/native/index.js +35 -0
  29. package/lib/module/native/index.js.map +1 -1
  30. package/lib/module/native/native-host.js +62 -0
  31. package/lib/module/native/native-host.js.map +1 -0
  32. package/lib/module/primitives/glass.js +14 -0
  33. package/lib/module/primitives/glass.js.map +1 -1
  34. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  35. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  36. package/lib/typescript/src/components/circular-text/circular-text-geometry.d.ts +39 -0
  37. package/lib/typescript/src/components/circular-text/circular-text-geometry.d.ts.map +1 -0
  38. package/lib/typescript/src/components/circular-text/index.d.ts +69 -0
  39. package/lib/typescript/src/components/circular-text/index.d.ts.map +1 -0
  40. package/lib/typescript/src/components/flip-card/index.d.ts +116 -0
  41. package/lib/typescript/src/components/flip-card/index.d.ts.map +1 -0
  42. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
  43. package/lib/typescript/src/components/panelside/index.d.ts +10 -202
  44. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  45. package/lib/typescript/src/components/popover/index.d.ts +25 -1
  46. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  47. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  48. package/lib/typescript/src/components/switch/index.d.ts +0 -4
  49. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  50. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  51. package/lib/typescript/src/index.d.ts +3 -1
  52. package/lib/typescript/src/index.d.ts.map +1 -1
  53. package/lib/typescript/src/native/index.d.ts +67 -10
  54. package/lib/typescript/src/native/index.d.ts.map +1 -1
  55. package/lib/typescript/src/native/native-host.d.ts +71 -0
  56. package/lib/typescript/src/native/native-host.d.ts.map +1 -0
  57. package/lib/typescript/src/primitives/glass.d.ts.map +1 -1
  58. package/package.json +11 -1
  59. package/src/components/bottom-sheet/index.tsx +8 -3
  60. package/src/components/button/index.tsx +36 -15
  61. package/src/components/circular-text/circular-text-geometry.ts +80 -0
  62. package/src/components/circular-text/index.tsx +217 -0
  63. package/src/components/flip-card/index.tsx +419 -0
  64. package/src/components/marquee/index.tsx +12 -5
  65. package/src/components/panelside/index.tsx +16 -604
  66. package/src/components/popover/index.tsx +146 -4
  67. package/src/components/select/index.tsx +3 -3
  68. package/src/components/slider/index.tsx +28 -6
  69. package/src/components/switch/index.tsx +28 -7
  70. package/src/icons/index.tsx +23 -2
  71. package/src/index.ts +10 -6
  72. package/src/native/index.ts +89 -9
  73. package/src/native/native-host.tsx +75 -0
  74. package/src/primitives/glass.tsx +12 -0
@@ -0,0 +1,419 @@
1
+ /**
2
+ * FlipCard — two faces of one card, and a turn between them.
3
+ *
4
+ * ```tsx
5
+ * <FlipCard>
6
+ * <FlipCard.Front className="items-center justify-center rounded-2xl bg-card p-6">
7
+ * <Text>Tap me</Text>
8
+ * </FlipCard.Front>
9
+ * <FlipCard.Back className="items-center justify-center rounded-2xl bg-primary p-6">
10
+ * <Text className="text-primary-foreground">The other side</Text>
11
+ * </FlipCard.Back>
12
+ * </FlipCard>
13
+ * ```
14
+ *
15
+ * For content that is genuinely two-sided — a bank card and its security code,
16
+ * a term and its definition, a photograph and what it is of. Not for
17
+ * progressive disclosure: the back replaces the front rather than extending
18
+ * it, so anything the reader needs to compare across the turn belongs in
19
+ * [Collapsible](../collapsible) or a second card instead.
20
+ *
21
+ * ## One box, and both faces in it
22
+ *
23
+ * The back is laid over the front, so there is one box and the two faces fill
24
+ * it. State the height on the root and both faces take it; leave it off and
25
+ * the front's own content decides, with the back given whatever that came to.
26
+ *
27
+ * A back with more in it than the front overflows rather than growing the
28
+ * card, and the fix is a height on the root rather than more padding on the
29
+ * back — a card whose height changed halfway through the turn would be a card
30
+ * that moved everything under it.
31
+ *
32
+ * ## Both faces are hidden the same way twice
33
+ *
34
+ * `backfaceVisibility` is the mechanism, and it is not reliable on every
35
+ * Android surface — so the face that has turned away is also faded out and
36
+ * dropped behind, on the same shared value, at the halfway point. Two
37
+ * mechanisms for one job, because the failure of the first is a card that
38
+ * shows both faces mirrored through each other and nothing in the tree that
39
+ * says why.
40
+ *
41
+ * ## Under reduce motion
42
+ *
43
+ * The faces swap with no turn at all. Not a shorter turn — none. The rotation
44
+ * is the part that moves, and moving is the part the setting is about; which
45
+ * face is showing is the information, and it is kept.
46
+ */
47
+ import {
48
+ Children,
49
+ createContext,
50
+ forwardRef,
51
+ isValidElement,
52
+ useCallback,
53
+ useContext,
54
+ useEffect,
55
+ useMemo,
56
+ useState,
57
+ type ReactNode,
58
+ } from 'react';
59
+ import { StyleSheet, View, type ViewProps } from 'react-native';
60
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
61
+ import Animated, {
62
+ cancelAnimation,
63
+ interpolate,
64
+ runOnJS,
65
+ useAnimatedStyle,
66
+ useReducedMotion,
67
+ useSharedValue,
68
+ withSpring,
69
+ type SharedValue,
70
+ } from 'react-native-reanimated';
71
+ import { tv } from 'tailwind-variants';
72
+ import { cn } from '../../utils/cn';
73
+
74
+ /**
75
+ * How the card settles on a face.
76
+ *
77
+ * Clamped, and a little heavier than the library's other springs. A card has
78
+ * apparent mass — it is a rectangle the size of your hand — and a turn that
79
+ * overshoots and comes back reads as a playing card being flicked rather than
80
+ * an object being turned over. The overshoot is also the one place the hidden
81
+ * face can appear, since it puts the rotation past 360.
82
+ */
83
+ const SPRING = {
84
+ damping: 18,
85
+ stiffness: 140,
86
+ mass: 0.9,
87
+ overshootClamping: true,
88
+ } as const;
89
+
90
+ /**
91
+ * Distance across the card that counts as a whole turn, as a fraction of its
92
+ * own width or height.
93
+ *
94
+ * Less than the full span: a drag that has crossed two thirds of the card has
95
+ * made its point, and requiring the whole width means the last part of every
96
+ * deliberate flip is spent travelling to an edge the finger has to reach.
97
+ */
98
+ const DRAG_SPAN = 0.66;
99
+
100
+ /** Velocity, in points per second, that carries a short drag over anyway. */
101
+ const FLICK_VELOCITY = 500;
102
+
103
+ const flipCardVariants = tv({
104
+ slots: {
105
+ root: 'relative',
106
+ face: 'w-full',
107
+ },
108
+ });
109
+
110
+ /** Which axis the card turns about. */
111
+ export type FlipCardDirection = 'horizontal' | 'vertical';
112
+
113
+ /** Which way round it turns. */
114
+ export type FlipCardRotation = 'normal' | 'reverse';
115
+
116
+ /** What flips the card. */
117
+ export type FlipCardTrigger = 'press' | 'drag' | 'none';
118
+
119
+ interface FlipCardContextValue {
120
+ flipped: boolean;
121
+ direction: FlipCardDirection;
122
+ rotation: FlipCardRotation;
123
+ perspective: number;
124
+ progress: SharedValue<number>;
125
+ flip: () => void;
126
+ }
127
+
128
+ const FlipCardContext = createContext<FlipCardContextValue | null>(null);
129
+
130
+ function useFlipCardContext(component: string) {
131
+ const context = useContext(FlipCardContext);
132
+ if (!context) throw new Error(`${component} must be used within a <FlipCard>`);
133
+ return context;
134
+ }
135
+
136
+ /**
137
+ * The card's state, for a face that needs to know which way round it is.
138
+ *
139
+ * `progress` runs 0 → 1 across the turn and is a shared value, so a face can
140
+ * drive its own animation off the same turn rather than starting a second one
141
+ * beside it.
142
+ */
143
+ export function useFlipCard() {
144
+ const { flipped, direction, rotation, progress, flip } = useFlipCardContext('useFlipCard');
145
+ return { flipped, direction, rotation, progress, flip };
146
+ }
147
+
148
+ export interface FlipCardProps extends Omit<ViewProps, 'children'> {
149
+ /** `FlipCard.Front` and `FlipCard.Back`, in either order. */
150
+ children?: ReactNode;
151
+ /**
152
+ * Which axis the card turns about. `horizontal` turns it left to right about
153
+ * its vertical axis; `vertical` turns it top over bottom.
154
+ */
155
+ direction?: FlipCardDirection;
156
+ /**
157
+ * Which way round the turn goes. `reverse` sends it the other way, for a
158
+ * pair of cards that should not turn identically.
159
+ */
160
+ rotation?: FlipCardRotation;
161
+ /**
162
+ * Which face is showing, when the caller holds it. Leave unset to let the
163
+ * card keep its own, and pair with `trigger="none"` for a card flipped only
164
+ * from outside.
165
+ */
166
+ flipped?: boolean;
167
+ /** Which face an uncontrolled card starts on. */
168
+ defaultFlipped?: boolean;
169
+ /** Fires whenever the card settles on the other face, however it got there. */
170
+ onFlippedChange?: (flipped: boolean) => void;
171
+ /**
172
+ * What turns the card. `press` is a tap anywhere on it; `drag` turns it with
173
+ * the finger and springs to whichever face is nearer on release; `none`
174
+ * leaves it to `flipped`.
175
+ */
176
+ trigger?: FlipCardTrigger;
177
+ /**
178
+ * How deep the turn looks, in points. Smaller is more dramatic — the near
179
+ * edge swings further out — and below about 400 a full card starts to read
180
+ * as a door rather than a card.
181
+ */
182
+ perspective?: number;
183
+ /** Classes for the card's own box. The front's size is the card's size. */
184
+ className?: string;
185
+ }
186
+
187
+ const FlipCardRoot = forwardRef<View, FlipCardProps>(
188
+ (
189
+ {
190
+ children,
191
+ direction = 'horizontal',
192
+ rotation = 'normal',
193
+ flipped: flippedProp,
194
+ defaultFlipped = false,
195
+ onFlippedChange,
196
+ trigger = 'press',
197
+ perspective = 1000,
198
+ className,
199
+ ...props
200
+ },
201
+ ref
202
+ ) => {
203
+ const [internal, setInternal] = useState(defaultFlipped);
204
+ const isControlled = flippedProp !== undefined;
205
+ const flipped = isControlled ? flippedProp : internal;
206
+
207
+ const progress = useSharedValue(defaultFlipped ? 1 : 0);
208
+ const startProgress = useSharedValue(0);
209
+ const span = useSharedValue(0);
210
+ const reduceMotion = useReducedMotion();
211
+
212
+ const { root, face } = flipCardVariants();
213
+
214
+ /*
215
+ * The settled face is React state and the turn is a shared value, and the
216
+ * two are kept in step from here rather than from the gesture: a drag that
217
+ * is let go past halfway has already decided, and telling React at the
218
+ * moment of release rather than at the end of the spring is what keeps the
219
+ * hidden face out of the accessibility tree while it is still turning.
220
+ */
221
+ const settle = useCallback(
222
+ (next: boolean) => {
223
+ if (!isControlled) setInternal(next);
224
+ onFlippedChange?.(next);
225
+ },
226
+ [isControlled, onFlippedChange]
227
+ );
228
+
229
+ useEffect(() => {
230
+ progress.value = reduceMotion
231
+ ? flipped
232
+ ? 1
233
+ : 0
234
+ : withSpring(flipped ? 1 : 0, SPRING);
235
+ }, [flipped, progress, reduceMotion]);
236
+
237
+ useEffect(() => () => cancelAnimation(progress), [progress]);
238
+
239
+ const flip = useCallback(() => settle(!flipped), [flipped, settle]);
240
+
241
+ const gesture = useMemo(() => {
242
+ const tap = Gesture.Tap()
243
+ .enabled(trigger === 'press')
244
+ .onEnd((_event, success) => {
245
+ if (success) runOnJS(flip)();
246
+ });
247
+
248
+ /*
249
+ * The span is read from the shared value rather than from a captured
250
+ * number, so a card that is measured or resized mid-touch does not leave
251
+ * the gesture dividing by the width it had when the finger landed.
252
+ */
253
+ const pan = Gesture.Pan()
254
+ .enabled(trigger === 'drag')
255
+ .onBegin(() => {
256
+ startProgress.value = progress.value;
257
+ })
258
+ .onUpdate((event) => {
259
+ const travel = direction === 'horizontal' ? event.translationX : event.translationY;
260
+ const reach = Math.max(span.value * DRAG_SPAN, 1);
261
+ const next = startProgress.value + travel / reach;
262
+ progress.value = Math.min(Math.max(next, 0), 1);
263
+ })
264
+ .onEnd((event) => {
265
+ const velocity = direction === 'horizontal' ? event.velocityX : event.velocityY;
266
+ const committed =
267
+ progress.value > 0.5 || (progress.value > 0.1 && velocity > FLICK_VELOCITY);
268
+ runOnJS(settle)(committed);
269
+ });
270
+
271
+ return Gesture.Exclusive(pan, tap);
272
+ }, [direction, flip, progress, settle, span, startProgress, trigger]);
273
+
274
+ /*
275
+ * One turn, read two ways. The front runs 0 → 180 and the back 180 → 360,
276
+ * so at any moment exactly one of them has its face toward the reader.
277
+ *
278
+ * `sign` is not an RTL mirror. Turning a physical object over is a physical
279
+ * direction, not a reading direction, and a card that spun the other way
280
+ * in an Arabic layout would be a different card rather than the same one
281
+ * laid out correctly.
282
+ */
283
+ const sign = rotation === 'reverse' ? -1 : 1;
284
+ const axis = direction === 'horizontal' ? 'rotateY' : 'rotateX';
285
+
286
+ const frontStyle = useAnimatedStyle(() => {
287
+ const angle = interpolate(progress.value, [0, 1], [0, 180 * sign]);
288
+ const showing = progress.value < 0.5;
289
+ return {
290
+ transform: [{ perspective }, { [axis]: `${angle}deg` } as never],
291
+ opacity: showing ? 1 : 0,
292
+ zIndex: showing ? 1 : 0,
293
+ };
294
+ });
295
+
296
+ const backStyle = useAnimatedStyle(() => {
297
+ const angle = interpolate(progress.value, [0, 1], [-180 * sign, 0]);
298
+ const showing = progress.value >= 0.5;
299
+ return {
300
+ transform: [{ perspective }, { [axis]: `${angle}deg` } as never],
301
+ opacity: showing ? 1 : 0,
302
+ zIndex: showing ? 1 : 0,
303
+ };
304
+ });
305
+
306
+ let front: ReactNode = null;
307
+ let back: ReactNode = null;
308
+ for (const child of Children.toArray(children)) {
309
+ if (!isValidElement(child)) continue;
310
+ if (child.type === FlipCardFront) front = child;
311
+ else if (child.type === FlipCardBack) back = child;
312
+ }
313
+
314
+ const context = useMemo<FlipCardContextValue>(
315
+ () => ({ flipped, direction, rotation, perspective, progress, flip }),
316
+ [direction, flip, flipped, perspective, progress, rotation]
317
+ );
318
+
319
+ /*
320
+ * The gesture goes on the card itself rather than on a view around it.
321
+ *
322
+ * A wrapper is an extra box with no width of its own, and a card sized
323
+ * `w-full` inside one is asking for a percentage of nothing — which is how
324
+ * a two-hundred-point card ended up taller than the screen and hanging off
325
+ * the side of it.
326
+ *
327
+ * The accessibility action rides here for the same reason. A drag is not
328
+ * available to a screen reader, and a card that can only be turned by
329
+ * dragging is a card some readers can never see the back of.
330
+ */
331
+ const card = (
332
+ <View
333
+ ref={ref}
334
+ onLayout={(event) => {
335
+ span.value =
336
+ direction === 'horizontal'
337
+ ? event.nativeEvent.layout.width
338
+ : event.nativeEvent.layout.height;
339
+ }}
340
+ accessible={trigger !== 'none'}
341
+ accessibilityRole={trigger === 'none' ? undefined : 'button'}
342
+ accessibilityState={{ expanded: flipped }}
343
+ accessibilityActions={
344
+ trigger === 'none' ? undefined : [{ name: 'activate', label: 'Flip the card over' }]
345
+ }
346
+ onAccessibilityAction={trigger === 'none' ? undefined : flip}
347
+ {...props}
348
+ className={root({ className })}
349
+ >
350
+ {/* The front is in flow and the back is laid over it, so the card is
351
+ the front's size on both axes and the back is handed that box. */}
352
+ <Animated.View
353
+ style={frontStyle}
354
+ className={face({ className: 'grow' })}
355
+ accessibilityElementsHidden={flipped}
356
+ importantForAccessibility={flipped ? 'no-hide-descendants' : 'auto'}
357
+ >
358
+ {front}
359
+ </Animated.View>
360
+ <Animated.View
361
+ style={[StyleSheet.absoluteFill, backStyle]}
362
+ className={face()}
363
+ accessibilityElementsHidden={!flipped}
364
+ importantForAccessibility={flipped ? 'auto' : 'no-hide-descendants'}
365
+ >
366
+ {back}
367
+ </Animated.View>
368
+ </View>
369
+ );
370
+
371
+ return (
372
+ <FlipCardContext.Provider value={context}>
373
+ {/* A gesture rather than a Pressable, and no press feedback with it.
374
+ The card already answers the touch by turning; dipping it first
375
+ makes the turn look like it started late. */}
376
+ {trigger === 'none' ? card : <GestureDetector gesture={gesture}>{card}</GestureDetector>}
377
+ </FlipCardContext.Provider>
378
+ );
379
+ }
380
+ );
381
+
382
+ export interface FlipCardFaceProps extends ViewProps {
383
+ className?: string;
384
+ children?: ReactNode;
385
+ }
386
+
387
+ /** The face showing at rest. Its size is the card's size. */
388
+ const FlipCardFront = forwardRef<View, FlipCardFaceProps>(({ className, ...props }, ref) => (
389
+ <View
390
+ ref={ref}
391
+ // The backface is the first of the two mechanisms that hide a face turned
392
+ // away; the opacity on the animated parent is the second.
393
+ style={{ backfaceVisibility: 'hidden' }}
394
+ {...props}
395
+ // `h-full` of a card that states no height is ignored rather than
396
+ // collapsing, so one shape covers both: a card given a height is filled,
397
+ // and a card left to its content still measures it.
398
+ className={cn('h-full w-full', className)}
399
+ />
400
+ ));
401
+
402
+ /** The face revealed by the turn, laid over the front. */
403
+ const FlipCardBack = forwardRef<View, FlipCardFaceProps>(({ className, ...props }, ref) => (
404
+ <View
405
+ ref={ref}
406
+ style={{ backfaceVisibility: 'hidden' }}
407
+ {...props}
408
+ className={cn('h-full w-full', className)}
409
+ />
410
+ ));
411
+
412
+ FlipCardRoot.displayName = 'FlipCard';
413
+ FlipCardFront.displayName = 'FlipCard.Front';
414
+ FlipCardBack.displayName = 'FlipCard.Back';
415
+
416
+ export const FlipCard = Object.assign(FlipCardRoot, {
417
+ Front: FlipCardFront,
418
+ Back: FlipCardBack,
419
+ });
@@ -258,12 +258,19 @@ function MarqueeRoot({
258
258
  };
259
259
 
260
260
  /*
261
- * Whether there is motion here at all: content that measured, a speed to
262
- * travel it at, and a `playing` that has not already stopped it from
263
- * outside. A control offering to pause a still marquee is the same lie as a
264
- * disabled button with no reason on it.
261
+ * Whether there is motion here at all: copies on screen, a speed to travel
262
+ * them at, and a `playing` that has not already stopped it from outside. A
263
+ * control offering to pause a still marquee is the same lie as a disabled
264
+ * button with no reason on it.
265
+ *
266
+ * The copy count, not the period. Content that measured while its container
267
+ * did not has a period — it is the content's own length — and no copies to
268
+ * draw, because there is no width to lay them across. Asking the period
269
+ * meant a row that renders nothing still reported itself live, and a group
270
+ * drew a pause control for it: a marquee that is one button and no content,
271
+ * which is the shape of the bug this replaced.
265
272
  */
266
- const live = playing && !reducedMotion && period > 0 && speed > 0;
273
+ const live = playing && !reducedMotion && layout.count > 0 && speed > 0;
267
274
 
268
275
  // Reported to the group, if there is one, so its single control can be
269
276
  // decided the same way this one's is.