panelui-native 0.40.0 → 0.43.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 (92) hide show
  1. package/README.md +4 -2
  2. package/lib/module/components/card/index.js +172 -3
  3. package/lib/module/components/card/index.js.map +1 -1
  4. package/lib/module/components/color-picker/index.js +1395 -0
  5. package/lib/module/components/color-picker/index.js.map +1 -0
  6. package/lib/module/components/combobox/index.js +87 -19
  7. package/lib/module/components/combobox/index.js.map +1 -1
  8. package/lib/module/components/drawer/index.js +13 -4
  9. package/lib/module/components/drawer/index.js.map +1 -1
  10. package/lib/module/components/kpi-chart/index.js +648 -0
  11. package/lib/module/components/kpi-chart/index.js.map +1 -0
  12. package/lib/module/components/map/index.js +72 -12
  13. package/lib/module/components/map/index.js.map +1 -1
  14. package/lib/module/components/menu/index.js +40 -10
  15. package/lib/module/components/menu/index.js.map +1 -1
  16. package/lib/module/components/pagination/index.js +19 -9
  17. package/lib/module/components/pagination/index.js.map +1 -1
  18. package/lib/module/components/panelside/index.js +68 -18
  19. package/lib/module/components/panelside/index.js.map +1 -1
  20. package/lib/module/components/radar-chart/index.js +704 -0
  21. package/lib/module/components/radar-chart/index.js.map +1 -0
  22. package/lib/module/components/section-rail/index.js +32 -7
  23. package/lib/module/components/section-rail/index.js.map +1 -1
  24. package/lib/module/components/slider/index.js +240 -39
  25. package/lib/module/components/slider/index.js.map +1 -1
  26. package/lib/module/components/tabs/index.js +194 -10
  27. package/lib/module/components/tabs/index.js.map +1 -1
  28. package/lib/module/components/text-animation/index.js +857 -0
  29. package/lib/module/components/text-animation/index.js.map +1 -0
  30. package/lib/module/icons/index.js +25 -7
  31. package/lib/module/icons/index.js.map +1 -1
  32. package/lib/module/index.js +5 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/theme/use-theme.js +1 -1
  35. package/lib/module/utils/chart.js +44 -0
  36. package/lib/module/utils/chart.js.map +1 -1
  37. package/lib/module/utils/color.js +268 -0
  38. package/lib/module/utils/color.js.map +1 -0
  39. package/lib/typescript/src/components/card/index.d.ts +20 -0
  40. package/lib/typescript/src/components/card/index.d.ts.map +1 -1
  41. package/lib/typescript/src/components/color-picker/index.d.ts +258 -0
  42. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -0
  43. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
  44. package/lib/typescript/src/components/drawer/index.d.ts +13 -1
  45. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
  46. package/lib/typescript/src/components/kpi-chart/index.d.ts +490 -0
  47. package/lib/typescript/src/components/kpi-chart/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/map/index.d.ts +37 -2
  49. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  50. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  51. package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
  52. package/lib/typescript/src/components/panelside/index.d.ts +50 -4
  53. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  54. package/lib/typescript/src/components/radar-chart/index.d.ts +214 -0
  55. package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -0
  56. package/lib/typescript/src/components/section-rail/index.d.ts +8 -1
  57. package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
  58. package/lib/typescript/src/components/slider/index.d.ts +19 -0
  59. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  60. package/lib/typescript/src/components/tabs/index.d.ts +11 -1
  61. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  62. package/lib/typescript/src/components/text-animation/index.d.ts +268 -0
  63. package/lib/typescript/src/components/text-animation/index.d.ts.map +1 -0
  64. package/lib/typescript/src/icons/index.d.ts +6 -0
  65. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  66. package/lib/typescript/src/index.d.ts +8 -3
  67. package/lib/typescript/src/index.d.ts.map +1 -1
  68. package/lib/typescript/src/utils/chart.d.ts +21 -0
  69. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  70. package/lib/typescript/src/utils/color.d.ts +74 -0
  71. package/lib/typescript/src/utils/color.d.ts.map +1 -0
  72. package/package.json +2 -1
  73. package/src/components/card/index.tsx +227 -2
  74. package/src/components/color-picker/index.tsx +1618 -0
  75. package/src/components/combobox/index.tsx +89 -19
  76. package/src/components/drawer/index.tsx +31 -9
  77. package/src/components/kpi-chart/index.tsx +719 -0
  78. package/src/components/map/index.tsx +158 -14
  79. package/src/components/menu/index.tsx +51 -6
  80. package/src/components/pagination/index.tsx +18 -6
  81. package/src/components/panelside/index.tsx +134 -19
  82. package/src/components/radar-chart/index.tsx +910 -0
  83. package/src/components/section-rail/index.tsx +42 -8
  84. package/src/components/slider/index.tsx +283 -41
  85. package/src/components/tabs/index.tsx +263 -8
  86. package/src/components/text-animation/index.tsx +1093 -0
  87. package/src/icons/index.tsx +23 -4
  88. package/src/index.ts +73 -1
  89. package/src/theme/use-theme.ts +1 -1
  90. package/src/utils/chart.ts +52 -0
  91. package/src/utils/color.ts +246 -0
  92. package/theme.css +125 -112
@@ -63,6 +63,7 @@ import {
63
63
  View,
64
64
  type LayoutChangeEvent,
65
65
  type ViewProps,
66
+ type ViewStyle,
66
67
  } from 'react-native';
67
68
  import Animated, {
68
69
  FadeIn,
@@ -103,7 +104,13 @@ const comboboxVariants = tv({
103
104
  // Chips wrap onto their own lines; the input keeps a sane minimum so it is
104
105
  // still tappable once a few of them are in front of it.
105
106
  fieldContent: 'flex-1 flex-row flex-wrap items-center gap-1.5 py-1',
106
- input: 'min-w-24 flex-1 py-1 text-base font-normal text-foreground',
107
+ /*
108
+ * A fixed height rather than vertical padding, because the chips beside it
109
+ * have one (`h-6`) and two differently-sized boxes on a `items-center` row
110
+ * centre to two different baselines. `py-0` clears the platform default,
111
+ * which is not the same on iOS and Android.
112
+ */
113
+ input: 'h-7 min-w-24 flex-1 py-0 text-base font-normal text-foreground',
107
114
  action: 'h-6 w-6 items-center justify-center rounded-full',
108
115
  list: 'overflow-hidden rounded-xl border border-border bg-popover p-2 shadow-sm',
109
116
  item: 'flex-row items-center gap-2 rounded-lg px-3 py-3',
@@ -190,6 +197,9 @@ function ComboboxItem({
190
197
  accessibilityState={{ selected, disabled: !!disabled }}
191
198
  disabled={disabled}
192
199
  onPress={() => context.onSelect(value, label)}
200
+ // The rows sit flush against each other, so the 4pt either side of the
201
+ // gap between two of them belongs to neither without this.
202
+ hitSlop={{ top: 2, bottom: 2 }}
193
203
  className={item()}
194
204
  >
195
205
  {start}
@@ -413,7 +423,17 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
413
423
  const [open, setOpen] = useState(false);
414
424
  const [focused, setFocused] = useState(false);
415
425
  const [anchor, setAnchor] = useState<Anchor | null>(null);
416
- const [listHeight, setListHeight] = useState(0);
426
+ /*
427
+ * Which side the list settled on, latched for as long as it stays open.
428
+ *
429
+ * The side is decided from the list's measured height, which is 0 on the
430
+ * frame it opens — so the honest answer changes once, from "below" to
431
+ * whatever fits, and the list jumps. A finger already on its way down to an
432
+ * option lands where the option used to be, which is the whole of "sometimes
433
+ * it takes two taps". Deciding once and holding it costs a list that opens
434
+ * downwards for one frame in a cramped viewport, which the fade covers.
435
+ */
436
+ const [flipped, setFlipped] = useState<boolean | null>(null);
417
437
  /*
418
438
  * The anchor is measured off the plain wrapper rather than off the animated
419
439
  * field inside it. In `overlay` the wrapper *is* the field's box — the list
@@ -508,6 +528,12 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
508
528
 
509
529
  const close = useCallback(() => setOpenState(false), [setOpenState]);
510
530
 
531
+ // A closed list has no side. Clearing the latch here rather than in `close`
532
+ // covers the caller closing it through `open` as well.
533
+ useEffect(() => {
534
+ if (!open) setFlipped(null);
535
+ }, [open]);
536
+
511
537
  // An open overlay list catches the Android back button, closing itself
512
538
  // instead of popping the screen behind it.
513
539
  useBackHandler(open && presentation === 'overlay', close);
@@ -741,6 +767,10 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
741
767
  <Chip
742
768
  key={entry}
743
769
  size="sm"
770
+ // A chip is `self-start` by default, which overrides the row's
771
+ // `items-center` and leaves it riding high against the input.
772
+ // Inside a field it is one of several things sharing a line.
773
+ className="self-center"
744
774
  onClose={disabled ? undefined : () => remove(entry)}
745
775
  closeLabel={`Remove ${labelOf(entry)}`}
746
776
  >
@@ -776,6 +806,10 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
776
806
  }
777
807
  }}
778
808
  editable={!disabled}
809
+ // Android lays a single-line input's text against the top of its box
810
+ // unless told otherwise; iOS centres it. Without this the text sits
811
+ // above the chips on one platform and level with them on the other.
812
+ textAlignVertical="center"
779
813
  placeholder={values.length && multiple ? undefined : placeholder}
780
814
  placeholderTextColor={placeholderColor}
781
815
  autoCapitalize="none"
@@ -840,12 +874,12 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
840
874
  }
841
875
 
842
876
  // Flip above the field when the list would run off the bottom — where the
843
- // bottom is the top of the keyboard, not the bottom of the screen. listHeight
844
- // is 0 on the first frame, so it opens downwards and corrects itself once
845
- // measured — invisible inside the 140ms fade.
877
+ // bottom is the top of the keyboard, not the bottom of the screen. The list
878
+ // has no height until it has been laid out, so the side is decided on that
879
+ // first measurement and then held; see `flipped`.
846
880
  const viewportBottom = screenHeight - keyboardHeight;
847
881
  const spaceBelow = anchor ? viewportBottom - (anchor.y + anchor.height) - offset : 0;
848
- const flip = !!anchor && listHeight > 0 && listHeight > spaceBelow;
882
+ const flip = flipped ?? false;
849
883
 
850
884
  const overlayPosition = anchor
851
885
  ? {
@@ -854,6 +888,11 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
854
888
  ...(flip
855
889
  ? { bottom: screenHeight - anchor.y + offset }
856
890
  : { top: anchor.y + anchor.height + offset }),
891
+ // Above the dismiss strips it shares the portal with. They are earlier
892
+ // siblings so paint order already puts the list on top on iOS, but on
893
+ // Android a sibling without an elevation is not reliably ordered.
894
+ zIndex: 1,
895
+ elevation: 1,
857
896
  ...(contentWidth === 'field'
858
897
  ? { width: anchor.width }
859
898
  : typeof contentWidth === 'number'
@@ -864,6 +903,21 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
864
903
  }
865
904
  : null;
866
905
 
906
+ /** The window minus the field: above it, below it, and either side of it. */
907
+ const catcherRects: ViewStyle[] = anchor
908
+ ? [
909
+ { top: 0, left: 0, right: 0, height: Math.max(anchor.y, 0) },
910
+ { top: anchor.y + anchor.height, left: 0, right: 0, bottom: 0 },
911
+ { top: anchor.y, height: anchor.height, left: 0, width: Math.max(anchor.x, 0) },
912
+ {
913
+ top: anchor.y,
914
+ height: anchor.height,
915
+ left: anchor.x + anchor.width,
916
+ right: 0,
917
+ },
918
+ ]
919
+ : [];
920
+
867
921
  return (
868
922
  <ComboboxContext.Provider value={context}>
869
923
  <View ref={fieldRef} className={cn(slots.root(), className)} {...props}>
@@ -879,26 +933,42 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
879
933
  * affordance for a pointer, and announcing it would put an unlabelled
880
934
  * full-screen "button" ahead of the options in the reading order.
881
935
  * Escaping the list is the back gesture's job.
936
+ *
937
+ * "Anywhere else" has to exclude the field, and one window-sized view
938
+ * cannot: covering the control that opened the list means a tap on
939
+ * the input to carry on typing, or on the chevron to close it, is
940
+ * spent dismissing instead and the field only answers on the second
941
+ * try. A view cannot have a hole cut in it, so the catcher is the
942
+ * four strips around the field's measured rect — which leaves the
943
+ * field uncovered and directly tappable.
882
944
  */}
883
- <Pressable
884
- accessible={false}
885
- importantForAccessibility="no-hide-descendants"
886
- accessibilityElementsHidden
887
- onPress={() => {
888
- close();
889
- inputRef.current?.blur();
890
- }}
891
- style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
892
- />
945
+ {catcherRects.map((rect, index) => (
946
+ <Pressable
947
+ key={index}
948
+ accessible={false}
949
+ importantForAccessibility="no-hide-descendants"
950
+ accessibilityElementsHidden
951
+ onPress={() => {
952
+ close();
953
+ inputRef.current?.blur();
954
+ }}
955
+ style={[{ position: 'absolute' }, rect]}
956
+ />
957
+ ))}
893
958
  {/* Portalled out of this subtree — re-provide the context so
894
959
  Combobox.Item keeps working. */}
895
960
  <ComboboxContext.Provider value={context}>
896
961
  <Animated.View
897
962
  entering={FadeIn.duration(140)}
898
963
  exiting={FadeOut.duration(120)}
899
- onLayout={(event: LayoutChangeEvent) =>
900
- setListHeight(event.nativeEvent.layout.height)
901
- }
964
+ onLayout={(event: LayoutChangeEvent) => {
965
+ // The first measurement decides the side, and nothing after it
966
+ // does — see `flipped`.
967
+ const measured = event.nativeEvent.layout.height;
968
+ setFlipped((current) =>
969
+ current === null ? measured > spaceBelow : current
970
+ );
971
+ }}
902
972
  style={overlayPosition}
903
973
  /*
904
974
  * The floating list covers the screen with a catcher and takes
@@ -103,6 +103,7 @@ const DISMISS_VELOCITY = 650;
103
103
 
104
104
  export type DrawerSide = 'start' | 'end' | 'top' | 'bottom';
105
105
  export type DrawerSize = 'sm' | 'md' | 'lg' | 'full';
106
+ export type DrawerCloseSide = 'start' | 'end';
106
107
 
107
108
  /**
108
109
  * How much of the screen each size asks for, and the cap it is never allowed
@@ -168,7 +169,7 @@ function useDrawer(component: string): DrawerContextValue {
168
169
  * so a header can leave it clear rather than wrap underneath it.
169
170
  */
170
171
  interface DrawerSurfaceValue {
171
- side: DrawerSide;
172
+ closeSide: DrawerCloseSide;
172
173
  showClose: boolean;
173
174
  }
174
175
 
@@ -270,6 +271,17 @@ export interface DrawerContentProps extends ViewProps {
270
271
  swipeToDismiss?: boolean;
271
272
  /** Show a close button in the drawer's inner top corner. Default true. */
272
273
  showClose?: boolean;
274
+ /**
275
+ * Which top corner the close button takes. Logical, like `side`: `end` is the
276
+ * corner text runs toward, so it is the right one in a left-to-right app and
277
+ * the left one in a right-to-left one.
278
+ *
279
+ * Defaults to the corner *away* from the docked edge, so an `end` drawer's
280
+ * button does not sit against the screen edge the panel came out of. Set it
281
+ * when the drawer reads better with the button on the outer corner instead —
282
+ * a panel people close by reaching for the same corner every time.
283
+ */
284
+ closeSide?: DrawerCloseSide;
273
285
  /**
274
286
  * Frost the screen behind the drawer instead of dimming it. Needs the
275
287
  * optional `expo-blur`; without it this dims, rather than failing.
@@ -285,6 +297,7 @@ function DrawerContent({
285
297
  dismissible = true,
286
298
  swipeToDismiss = true,
287
299
  showClose = true,
300
+ closeSide,
288
301
  blur = false,
289
302
  children,
290
303
  ...props
@@ -384,9 +397,17 @@ function DrawerContent({
384
397
  : { transform: [{ translateY: travel.value * outward }] }
385
398
  );
386
399
 
400
+ /*
401
+ * Resolved once and shared, because the button's corner and the header's
402
+ * clearance are the same decision seen from two places. Letting each work it
403
+ * out from `side` is how they drift, and a header padded on the wrong side
404
+ * runs its title straight under the button.
405
+ */
406
+ const closeCorner: DrawerCloseSide = closeSide ?? (side === 'end' ? 'start' : 'end');
407
+
387
408
  const surface = useMemo<DrawerSurfaceValue>(
388
- () => ({ side, showClose }),
389
- [side, showClose]
409
+ () => ({ closeSide: closeCorner, showClose }),
410
+ [closeCorner, showClose]
390
411
  );
391
412
 
392
413
  if (!open) return null;
@@ -508,10 +529,11 @@ function DrawerContent({
508
529
  * Taking the same number the padding does is what keeps it
509
530
  * level with the first line of the header on every side.
510
531
  *
511
- * The corner follows the docked edge. An `end` drawer's own
512
- * edge is the trailing one, so its button moves to the
513
- * leading side rather than sitting against the screen edge
514
- * the drawer came out of.
532
+ * The corner follows the docked edge by default. An `end`
533
+ * drawer's own edge is the trailing one, so its button moves
534
+ * to the leading side rather than sitting against the screen
535
+ * edge the drawer came out of — and `closeSide` overrides
536
+ * that for a panel that wants the same corner every time.
515
537
  */}
516
538
  {showClose ? (
517
539
  <Pressable
@@ -521,7 +543,7 @@ function DrawerContent({
521
543
  hitSlop={8}
522
544
  className={cn(
523
545
  'absolute z-10 h-8 w-8 items-center justify-center rounded-full bg-muted active:opacity-70',
524
- side === 'end' ? 'start-4' : 'end-4'
546
+ closeCorner === 'start' ? 'start-4' : 'end-4'
525
547
  )}
526
548
  style={{ top: padding.paddingTop }}
527
549
  >
@@ -569,7 +591,7 @@ function DrawerHeader({
569
591
  */
570
592
  const clearance = !surface?.showClose
571
593
  ? undefined
572
- : surface.side === 'end'
594
+ : surface.closeSide === 'start'
573
595
  ? 'ps-14'
574
596
  : 'pe-14';
575
597