panelui-native 0.80.0 → 0.82.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/lib/module/components/bottom-sheet/index.js +90 -17
  2. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  3. package/lib/module/components/button/index.js +13 -0
  4. package/lib/module/components/button/index.js.map +1 -1
  5. package/lib/module/components/button-group/index.js +3 -0
  6. package/lib/module/components/button-group/index.js.map +1 -1
  7. package/lib/module/components/image-generation/dot-field.js +210 -0
  8. package/lib/module/components/image-generation/dot-field.js.map +1 -0
  9. package/lib/module/components/image-generation/index.js +417 -0
  10. package/lib/module/components/image-generation/index.js.map +1 -0
  11. package/lib/module/components/kpi/index.js +2 -4
  12. package/lib/module/components/kpi/index.js.map +1 -1
  13. package/lib/module/components/menu/index.js +8 -16
  14. package/lib/module/components/menu/index.js.map +1 -1
  15. package/lib/module/components/panelside/index.js +340 -33
  16. package/lib/module/components/panelside/index.js.map +1 -1
  17. package/lib/module/components/planner/index.js +446 -32
  18. package/lib/module/components/planner/index.js.map +1 -1
  19. package/lib/module/components/planner/planner-entries.js +36 -0
  20. package/lib/module/components/planner/planner-entries.js.map +1 -1
  21. package/lib/module/components/planner/planner-grid-navigation.js +34 -2
  22. package/lib/module/components/planner/planner-grid-navigation.js.map +1 -1
  23. package/lib/module/components/planner/planner-weeks.js +95 -0
  24. package/lib/module/components/planner/planner-weeks.js.map +1 -0
  25. package/lib/module/components/select/index.js +127 -23
  26. package/lib/module/components/select/index.js.map +1 -1
  27. package/lib/module/components/slide-button/index.js +610 -0
  28. package/lib/module/components/slide-button/index.js.map +1 -0
  29. package/lib/module/components/slide-button/slide-button-track.js +98 -0
  30. package/lib/module/components/slide-button/slide-button-track.js.map +1 -0
  31. package/lib/module/icons/index.js +553 -1934
  32. package/lib/module/icons/index.js.map +1 -1
  33. package/lib/module/index.js +3 -1
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/primitives/portal.js +15 -1
  36. package/lib/module/primitives/portal.js.map +1 -1
  37. package/lib/module/utils/date.js +10 -3
  38. package/lib/module/utils/date.js.map +1 -1
  39. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  40. package/lib/typescript/src/components/button/index.d.ts +12 -0
  41. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  42. package/lib/typescript/src/components/button-group/index.d.ts +9 -0
  43. package/lib/typescript/src/components/button-group/index.d.ts.map +1 -1
  44. package/lib/typescript/src/components/image-generation/dot-field.d.ts +61 -0
  45. package/lib/typescript/src/components/image-generation/dot-field.d.ts.map +1 -0
  46. package/lib/typescript/src/components/image-generation/index.d.ts +126 -0
  47. package/lib/typescript/src/components/image-generation/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
  49. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  50. package/lib/typescript/src/components/panelside/index.d.ts +194 -10
  51. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  52. package/lib/typescript/src/components/planner/index.d.ts +65 -4
  53. package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
  54. package/lib/typescript/src/components/planner/planner-entries.d.ts +18 -0
  55. package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -1
  56. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts +7 -1
  57. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts.map +1 -1
  58. package/lib/typescript/src/components/planner/planner-weeks.d.ts +50 -0
  59. package/lib/typescript/src/components/planner/planner-weeks.d.ts.map +1 -0
  60. package/lib/typescript/src/components/select/index.d.ts +75 -2
  61. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  62. package/lib/typescript/src/components/slide-button/index.d.ts +317 -0
  63. package/lib/typescript/src/components/slide-button/index.d.ts.map +1 -0
  64. package/lib/typescript/src/components/slide-button/slide-button-track.d.ts +70 -0
  65. package/lib/typescript/src/components/slide-button/slide-button-track.d.ts.map +1 -0
  66. package/lib/typescript/src/icons/index.d.ts +136 -189
  67. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  68. package/lib/typescript/src/index.d.ts +5 -3
  69. package/lib/typescript/src/index.d.ts.map +1 -1
  70. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  71. package/lib/typescript/src/utils/date.d.ts +1 -1
  72. package/lib/typescript/src/utils/date.d.ts.map +1 -1
  73. package/package.json +3 -2
  74. package/src/components/bottom-sheet/index.tsx +97 -17
  75. package/src/components/button/index.tsx +10 -2
  76. package/src/components/button-group/index.tsx +1 -0
  77. package/src/components/image-generation/dot-field.ts +226 -0
  78. package/src/components/image-generation/index.tsx +488 -0
  79. package/src/components/kpi/index.tsx +2 -4
  80. package/src/components/menu/index.tsx +10 -15
  81. package/src/components/panelside/index.tsx +498 -33
  82. package/src/components/planner/index.tsx +516 -38
  83. package/src/components/planner/planner-entries.ts +33 -0
  84. package/src/components/planner/planner-grid-navigation.ts +37 -2
  85. package/src/components/planner/planner-weeks.ts +90 -0
  86. package/src/components/select/index.tsx +158 -31
  87. package/src/components/slide-button/index.tsx +681 -0
  88. package/src/components/slide-button/slide-button-track.ts +100 -0
  89. package/src/icons/index.tsx +338 -1327
  90. package/src/index.ts +30 -0
  91. package/src/primitives/portal.tsx +15 -1
  92. package/src/utils/date.ts +13 -3
@@ -78,17 +78,23 @@
78
78
  * accessibility setting on, every spring here resolves instantly to its target
79
79
  * rather than travelling.
80
80
  */
81
- import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
82
- import { Platform, Pressable, ScrollView, StyleSheet, TextInput, useWindowDimensions, View } from 'react-native';
81
+ import { Children, cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
82
+ import { Keyboard, Platform, Pressable, ScrollView, StyleSheet, TextInput, useWindowDimensions, View } from 'react-native';
83
83
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
84
84
  import Animated, { runOnJS, useAnimatedProps, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring } from 'react-native-reanimated';
85
85
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
86
86
  import { tv } from 'tailwind-variants';
87
87
  import { useCSSVariable } from 'uniwind';
88
88
  import { LinearGradient } from 'expo-linear-gradient';
89
- import { EllipsisIcon, IconColorProvider, MenuIcon, SearchIcon, XIcon } from "../../icons/index.js";
89
+ import { HugeiconsIcon } from '@hugeicons/react-native';
90
+ import Cancel01Icon from '@hugeicons/core-free-icons/Cancel01Icon';
91
+ import Menu01Icon from '@hugeicons/core-free-icons/Menu01Icon';
92
+ import MoreHorizontalIcon from '@hugeicons/core-free-icons/MoreHorizontalIcon';
93
+ import Search01Icon from '@hugeicons/core-free-icons/Search01Icon';
94
+ import { IconColorProvider, useIconColor } from "../../icons/index.js";
90
95
  import { BottomSheet, bottomSheetDetentHeight } from "../bottom-sheet/index.js";
91
96
  import { Button } from "../button/index.js";
97
+ import { Menu } from "../menu/index.js";
92
98
  import { Tabs } from "../tabs/index.js";
93
99
  import { getNativeUI } from "../../native/index.js";
94
100
  import { AnimatedPressable } from "../../primitives/animated-pressable.js";
@@ -242,6 +248,53 @@ const clamp = (value, min, max) => {
242
248
  return Math.min(Math.max(value, min), max);
243
249
  };
244
250
 
251
+ /**
252
+ * The stroke weight the panel's glyphs are drawn at.
253
+ *
254
+ * A shade heavier than the icon set's own default. The panel sits over a
255
+ * dimmed screen and its rows are quiet by design, so a hairline glyph goes
256
+ * soft against them at the sizes used here.
257
+ */
258
+ const GLYPH_STROKE = 1.8;
259
+
260
+ /**
261
+ * What a glyph is drawn in when nothing has said. The same neutral the rest of
262
+ * the chrome falls back to — visible against either theme, and never the thing
263
+ * that decides how a panel looks, since every surface here provides a colour.
264
+ */
265
+ const GLYPH_FALLBACK = '#737373';
266
+
267
+ /**
268
+ * Floor for a row's action menu.
269
+ *
270
+ * Wide enough for a verb and its glyph with the label doing the growing. Left
271
+ * to size itself, a menu takes its width from the one thing in each row that
272
+ * is not flexible — the glyph — and comes up as a column of icons.
273
+ */
274
+ const ACTIONS_MIN_WIDTH = 220;
275
+
276
+ /**
277
+ * One of the panel's own glyphs.
278
+ *
279
+ * It exists to keep the colour contract the rest of the library has: an icon
280
+ * takes an explicit colour, then the one an enclosing surface is providing,
281
+ * and only then falls back. The drawing component underneath knows nothing
282
+ * about that inheritance and needs the resolved value handed to it.
283
+ */
284
+ function Glyph({
285
+ icon,
286
+ size = 20,
287
+ color
288
+ }) {
289
+ const inherited = useIconColor();
290
+ return /*#__PURE__*/_jsx(HugeiconsIcon, {
291
+ icon: icon,
292
+ size: size,
293
+ color: color ?? inherited ?? GLYPH_FALLBACK,
294
+ strokeWidth: GLYPH_STROKE
295
+ });
296
+ }
297
+
245
298
  /**
246
299
  * What a header or a footer paints behind itself.
247
300
  *
@@ -251,6 +304,8 @@ const clamp = (value, min, max) => {
251
304
  * that is a row of the layout rather than something floating over one.
252
305
  */
253
306
 
307
+ /** How the panel's small round controls are drawn: a fill, or a ring. */
308
+
254
309
  const itemVariants = tv({
255
310
  // No width: in a group it stretches on its own, and pinning it to full width
256
311
  // would stop it sharing a footer row with anything else. `shrink` because
@@ -293,7 +348,8 @@ const ctaVariants = tv({
293
348
  },
294
349
  size: {
295
350
  default: 'h-11 gap-2 px-6',
296
- lg: 'h-13 gap-2.5 px-7'
351
+ lg: 'h-13 gap-2.5 px-7',
352
+ xl: 'h-14 gap-3 px-8'
297
353
  }
298
354
  },
299
355
  defaultVariants: {
@@ -322,8 +378,17 @@ export function usePanelside() {
322
378
  progress,
323
379
  docked,
324
380
  searchOpen,
325
- setSearchOpen
381
+ setSearchOpen,
382
+ route,
383
+ navigate
326
384
  } = usePanelsideContext('usePanelside');
385
+
386
+ // `navigate` closes the panel. The root cannot do it there without making
387
+ // its own `setOpen` a dependency of the callback every row holds.
388
+ const go = useCallback(next => {
389
+ navigate(next);
390
+ setOpen(false);
391
+ }, [navigate, setOpen]);
327
392
  return {
328
393
  open,
329
394
  setOpen,
@@ -331,7 +396,9 @@ export function usePanelside() {
331
396
  progress,
332
397
  docked,
333
398
  searchOpen,
334
- setSearchOpen
399
+ setSearchOpen,
400
+ route,
401
+ navigate: go
335
402
  };
336
403
  }
337
404
 
@@ -359,6 +426,9 @@ function PanelsideRoot({
359
426
  scale,
360
427
  radius,
361
428
  dim,
429
+ route: controlledRoute,
430
+ defaultRoute = '',
431
+ onRouteChange,
362
432
  className
363
433
  }) {
364
434
  const {
@@ -369,6 +439,19 @@ function PanelsideRoot({
369
439
  const controlled = controlledOpen !== undefined;
370
440
  const open = controlled ? controlledOpen : uncontrolledOpen;
371
441
  const [searchOpen, setSearchOpen] = useState(false);
442
+ const [uncontrolledRoute, setUncontrolledRoute] = useState(defaultRoute);
443
+ const route = controlledRoute ?? uncontrolledRoute;
444
+
445
+ /*
446
+ * Navigating closes the panel, and does so here rather than at each call
447
+ * site. The thing you just moved to would otherwise be behind the thing you
448
+ * moved from, which is the one arrangement no app wants — and a row that has
449
+ * to be told to close is a row every app writes the same three lines for.
450
+ */
451
+ const navigate = useCallback(next => {
452
+ if (controlledRoute === undefined) setUncontrolledRoute(next);
453
+ onRouteChange?.(next);
454
+ }, [controlledRoute, onRouteChange]);
372
455
 
373
456
  /*
374
457
  * Measured rather than taken from the window, because Panelside does not
@@ -505,8 +588,10 @@ function PanelsideRoot({
505
588
  radius,
506
589
  dim,
507
590
  searchOpen,
508
- setSearchOpen
509
- }), [dim, dismissible, docked, mode, open, progress, radius, scale, searchOpen, setOpen, toggle, width]);
591
+ setSearchOpen,
592
+ route,
593
+ navigate
594
+ }), [dim, dismissible, docked, mode, navigate, open, progress, radius, route, scale, searchOpen, setOpen, toggle, width]);
510
595
  return /*#__PURE__*/_jsx(PanelsideContext.Provider, {
511
596
  value: context,
512
597
  children: /*#__PURE__*/_jsx(View, {
@@ -669,7 +754,8 @@ function PanelsideSearch({
669
754
  const muted = typeof placeholderTint === 'string' ? placeholderTint : undefined;
670
755
  return /*#__PURE__*/_jsxs(View, {
671
756
  className: cn('h-10 flex-row items-center gap-2 rounded-xl bg-secondary px-3', containerClassName),
672
- children: [/*#__PURE__*/_jsx(SearchIcon, {
757
+ children: [/*#__PURE__*/_jsx(Glyph, {
758
+ icon: Search01Icon,
673
759
  size: 16,
674
760
  color: muted
675
761
  }), /*#__PURE__*/_jsx(TextInput, {
@@ -785,15 +871,29 @@ function PanelsideItem({
785
871
  className,
786
872
  icon,
787
873
  label,
788
- active = false,
874
+ to,
875
+ closeOnNavigate = true,
876
+ active: activeProp,
789
877
  badge,
790
878
  disabled = false,
791
879
  size = 'default',
880
+ onPress,
792
881
  children,
793
882
  ...props
794
883
  }) {
884
+ const panel = useContext(PanelsideContext);
795
885
  const restTint = useCSSVariable('--color-muted-foreground');
796
886
  const activeTint = useCSSVariable('--color-foreground');
887
+
888
+ // A row outside a `<Panelside>` still works — the parts are usable on their
889
+ // own — it simply has no route to match against.
890
+ const active = activeProp ?? (to !== undefined && panel?.route === to);
891
+ const press = useCallback(event => {
892
+ onPress?.(event);
893
+ if (to === undefined || !panel) return;
894
+ panel.navigate(to);
895
+ if (closeOnNavigate) panel.setOpen(false);
896
+ }, [closeOnNavigate, onPress, panel, to]);
797
897
  const tint = active ? typeof activeTint === 'string' ? activeTint : undefined : typeof restTint === 'string' ? restTint : undefined;
798
898
  const context = useMemo(() => ({
799
899
  active,
@@ -808,8 +908,16 @@ function PanelsideItem({
808
908
  * that, and a written-out `Panelside.ItemLabel` supplies it too — so the
809
909
  * spacer is only for the row that has neither, which is a row of nothing but
810
910
  * an icon and a badge.
911
+ *
912
+ * And only where there is something to push. A row of nothing but an icon
913
+ * has no trailing content, and a spacer in it is not inert: laid out against
914
+ * the width available rather than against the row's own contents, it expands,
915
+ * takes the space with it, and squeezes whatever shares the line — which is
916
+ * how an icon-only row in a footer beside a compose button cropped that
917
+ * button's label and left its own icon short of the trailing edge.
811
918
  */
812
919
  const filled = label !== undefined || children !== undefined;
920
+ const trailing = badge !== undefined && badge !== null;
813
921
  return /*#__PURE__*/_jsx(PanelsideItemContext.Provider, {
814
922
  value: context,
815
923
  children: /*#__PURE__*/_jsxs(AnimatedPressable, {
@@ -827,12 +935,13 @@ function PanelsideItem({
827
935
  },
828
936
  accessibilityLabel: label,
829
937
  pressScale: 0.985,
938
+ onPress: to !== undefined || onPress ? press : undefined,
830
939
  ...props,
831
940
  children: [icon ? /*#__PURE__*/_jsx(PanelsideItemIcon, {
832
941
  children: icon
833
942
  }) : null, label !== undefined ? /*#__PURE__*/_jsx(PanelsideItemLabel, {
834
943
  children: label
835
- }) : null, filled ? null : /*#__PURE__*/_jsx(View, {
944
+ }) : null, filled || !trailing ? null : /*#__PURE__*/_jsx(View, {
836
945
  className: "flex-1"
837
946
  }), badge !== undefined && badge !== null ? /*#__PURE__*/_jsx(PanelsideItemBadge, {
838
947
  children: badge
@@ -923,6 +1032,7 @@ function PanelsideAction({
923
1032
  className,
924
1033
  label = 'More options',
925
1034
  children,
1035
+ onPress,
926
1036
  ...props
927
1037
  }) {
928
1038
  const tint = useCSSVariable('--color-muted-foreground');
@@ -934,14 +1044,71 @@ function PanelsideAction({
934
1044
  // The glyph is small and sits next to a row-sized target, so it takes
935
1045
  // the difference back as slop rather than as layout.
936
1046
  ,
937
- hitSlop: 8,
1047
+ hitSlop: 8
1048
+ // The row underneath is pressable and usually navigates. A press that
1049
+ // reached both would open the menu and leave the thing it is about.
1050
+ ,
1051
+ onPress: event => {
1052
+ event.stopPropagation();
1053
+ onPress?.(event);
1054
+ },
938
1055
  ...props,
939
- children: children ?? /*#__PURE__*/_jsx(EllipsisIcon, {
1056
+ children: children ?? /*#__PURE__*/_jsx(Glyph, {
1057
+ icon: MoreHorizontalIcon,
940
1058
  size: 18,
941
1059
  color: color
942
1060
  })
943
1061
  });
944
1062
  }
1063
+ /**
1064
+ * A row's actions, behind an overflow button at the end of it.
1065
+ *
1066
+ * ```tsx
1067
+ * <Panelside.Item label={chat.title} to={chat.id}>
1068
+ * <Panelside.ItemActions>
1069
+ * <Menu.Item onSelect={rename}>Rename</Menu.Item>
1070
+ * <Menu.Separator />
1071
+ * <Menu.Item variant="destructive" onSelect={remove}>Delete</Menu.Item>
1072
+ * </Panelside.ItemActions>
1073
+ * </Panelside.Item>
1074
+ * ```
1075
+ *
1076
+ * The panel is anchored to the button rather than presented from the bottom of
1077
+ * the screen, so it lines up with the row it belongs to and the list it came
1078
+ * from stays readable behind it. It is also narrow: the panel it opens in is a
1079
+ * fraction of the screen, and a sheet covering that to offer four verbs costs
1080
+ * more than it says.
1081
+ *
1082
+ * Pressing the button does not press the row. A row that navigates would
1083
+ * otherwise navigate away from the thing the menu is about.
1084
+ */
1085
+ function PanelsideItemActions({
1086
+ className,
1087
+ label = 'More options',
1088
+ icon,
1089
+ placement = 'bottom',
1090
+ align = 'end',
1091
+ minWidth = ACTIONS_MIN_WIDTH,
1092
+ contentProps,
1093
+ children
1094
+ }) {
1095
+ return /*#__PURE__*/_jsxs(Menu, {
1096
+ children: [/*#__PURE__*/_jsx(Menu.Trigger, {
1097
+ children: /*#__PURE__*/_jsx(PanelsideAction, {
1098
+ className: className,
1099
+ label: label,
1100
+ children: icon
1101
+ })
1102
+ }), /*#__PURE__*/_jsx(Menu.Content, {
1103
+ placement: placement,
1104
+ align: align,
1105
+ width: "content-fit",
1106
+ minWidth: minWidth,
1107
+ ...contentProps,
1108
+ children: children
1109
+ })]
1110
+ });
1111
+ }
945
1112
  function PanelsideFooter({
946
1113
  className,
947
1114
  floating = true,
@@ -1016,6 +1183,9 @@ const styles = StyleSheet.create({
1016
1183
  bottom: 0,
1017
1184
  left: 0,
1018
1185
  right: 0
1186
+ },
1187
+ hidden: {
1188
+ display: 'none'
1019
1189
  }
1020
1190
  });
1021
1191
  function PanelsideCta({
@@ -1048,7 +1218,7 @@ function PanelsideCta({
1048
1218
  // The platform sizes a native button from its label, so a height means
1049
1219
  // nothing here — the step is asked for as a control size instead.
1050
1220
  ,
1051
- size: size === 'lg' ? 'lg' : 'md',
1221
+ size: size === 'default' ? 'md' : size,
1052
1222
  variant: variant,
1053
1223
  accessibilityLabel: label
1054
1224
  // Pressable allows `null` for disabled; Button does not.
@@ -1210,6 +1380,92 @@ function PanelsideScene({
1210
1380
  }) : null]
1211
1381
  });
1212
1382
  }
1383
+ /**
1384
+ * The pages the panel navigates between.
1385
+ *
1386
+ * Put it inside `Panelside.Scene` and give each page a `value` that a row's
1387
+ * `to` matches. Nothing is wired between the two: the row sets the panel's
1388
+ * route, and the page whose value equals it is the one shown.
1389
+ *
1390
+ * ```tsx
1391
+ * <Panelside.Scene>
1392
+ * <Panelside.Pages>
1393
+ * <Panelside.Page value="inbox"><Inbox /></Panelside.Page>
1394
+ * <Panelside.Page value="drafts"><Drafts /></Panelside.Page>
1395
+ * </Panelside.Pages>
1396
+ * </Panelside.Scene>
1397
+ * ```
1398
+ *
1399
+ * A page is mounted the first time it is visited and stays mounted after
1400
+ * that, hidden rather than removed. Going back to one is then a style change
1401
+ * rather than a mount: its list does not rebuild, its scroll position is where
1402
+ * you left it, and whatever it was fetching is already there. A page whose
1403
+ * contents go stale — or whose data is large enough that keeping it is worse
1404
+ * than fetching it again — takes `keepAlive={false}`.
1405
+ */
1406
+ function PanelsidePages({
1407
+ className,
1408
+ children,
1409
+ ...props
1410
+ }) {
1411
+ const {
1412
+ route
1413
+ } = usePanelsideContext('Panelside.Pages');
1414
+
1415
+ /*
1416
+ * Which pages have ever been the route.
1417
+ *
1418
+ * A `Set` in state rather than a ref: mounting a page for the first time has
1419
+ * to be a render, and the ref would not cause one. It only ever grows, and
1420
+ * only by one entry per page, so the identity change per first visit costs
1421
+ * nothing after the pages have all been seen once.
1422
+ */
1423
+ const [visited, setVisited] = useState(() => [route]);
1424
+ useEffect(() => {
1425
+ setVisited(current => current.includes(route) ? current : [...current, route]);
1426
+ }, [route]);
1427
+ return /*#__PURE__*/_jsx(View, {
1428
+ className: cn('flex-1', className),
1429
+ ...props,
1430
+ children: Children.map(children, child => {
1431
+ if (! /*#__PURE__*/isValidElement(child) || child.type !== PanelsidePage) {
1432
+ return child;
1433
+ }
1434
+ const {
1435
+ value,
1436
+ keepAlive = true
1437
+ } = child.props;
1438
+ const current = value === route;
1439
+ if (!current && (!keepAlive || !visited.includes(value))) return null;
1440
+ return /*#__PURE__*/cloneElement(child, {
1441
+ hidden: !current
1442
+ });
1443
+ })
1444
+ });
1445
+ }
1446
+ /** One page. Only meaningful inside `Panelside.Pages`. */
1447
+ function PanelsidePage({
1448
+ className,
1449
+ value,
1450
+ keepAlive,
1451
+ hidden = false,
1452
+ children,
1453
+ style,
1454
+ ...props
1455
+ }) {
1456
+ // `display: none` rather than unmounting, and rather than opacity: it takes
1457
+ // the page out of layout entirely, so a hidden page costs no measurement,
1458
+ // while its component tree — and everything it is holding — stays.
1459
+ return /*#__PURE__*/_jsx(View, {
1460
+ accessibilityElementsHidden: hidden,
1461
+ importantForAccessibility: hidden ? 'no-hide-descendants' : 'auto',
1462
+ pointerEvents: hidden ? 'none' : 'auto',
1463
+ style: [hidden ? styles.hidden : null, style],
1464
+ className: cn('flex-1', className),
1465
+ ...props,
1466
+ children: textChildren(children)
1467
+ });
1468
+ }
1213
1469
  function PanelsideTrigger({
1214
1470
  className,
1215
1471
  label = 'Open navigation panel',
@@ -1245,7 +1501,8 @@ function PanelsideTrigger({
1245
1501
  className: cn('h-10 w-10 items-center justify-center rounded-full', className),
1246
1502
  accessibilityRole: "button",
1247
1503
  accessibilityLabel: label,
1248
- children: /*#__PURE__*/_jsx(MenuIcon, {
1504
+ children: /*#__PURE__*/_jsx(Glyph, {
1505
+ icon: Menu01Icon,
1249
1506
  size: 20,
1250
1507
  color: color
1251
1508
  })
@@ -1283,6 +1540,7 @@ function PanelsideTrigger({
1283
1540
  function PanelsideSearchTrigger({
1284
1541
  className,
1285
1542
  label = 'Search',
1543
+ variant = 'filled',
1286
1544
  children,
1287
1545
  native = false,
1288
1546
  glass = false,
@@ -1298,7 +1556,8 @@ function PanelsideSearchTrigger({
1298
1556
  onPress?.(event);
1299
1557
  setSearchOpen(true);
1300
1558
  }, [onPress, setSearchOpen]);
1301
- const glyph = children ?? /*#__PURE__*/_jsx(SearchIcon, {
1559
+ const glyph = children ?? /*#__PURE__*/_jsx(Glyph, {
1560
+ icon: Search01Icon,
1302
1561
  size: 18,
1303
1562
  color: native ? color : undefined
1304
1563
  });
@@ -1316,11 +1575,12 @@ function PanelsideSearchTrigger({
1316
1575
  return /*#__PURE__*/_jsx(AnimatedPressable, {
1317
1576
  ...props,
1318
1577
  onPress: open,
1319
- className: cn(
1320
- // Filled rather than outlined. It sits on the panel's own surface with
1321
- // nothing else on that row, so an outline at this size reads as an
1322
- // empty circle before it reads as a control.
1323
- 'h-10 w-10 items-center justify-center rounded-full bg-secondary', className),
1578
+ className: cn('h-10 w-10 items-center justify-center rounded-full',
1579
+ // Filled by default. It sits on the panel's own surface with nothing
1580
+ // else on that row, so an outline at this size reads as an empty
1581
+ // circle before it reads as a control — which stops being true the
1582
+ // moment the rest of the screen's controls are outlined as well.
1583
+ variant === 'outline' ? 'border border-border' : 'bg-secondary', className),
1324
1584
  accessibilityRole: "button",
1325
1585
  accessibilityLabel: label,
1326
1586
  children: /*#__PURE__*/_jsx(IconColorProvider, {
@@ -1377,6 +1637,9 @@ function PanelsideSearchSheet({
1377
1637
  keyboardGap = 10,
1378
1638
  showClose = true,
1379
1639
  closeLabel = 'Close search',
1640
+ closeVariant = 'filled',
1641
+ closeNative = false,
1642
+ closeGlass = false,
1380
1643
  children
1381
1644
  }) {
1382
1645
  const {
@@ -1394,7 +1657,20 @@ function PanelsideSearchSheet({
1394
1657
  const open = openProp ?? searchOpen;
1395
1658
  const query = value ?? uncontrolledQuery;
1396
1659
  const activeTab = tab ?? uncontrolledTab;
1660
+
1661
+ /*
1662
+ * Closing takes the keyboard with it, wherever the close came from — the
1663
+ * button, a drag on the sheet, or the system back gesture.
1664
+ *
1665
+ * Leaving it to the field's own blur is too late. The field blurs when
1666
+ * `open` goes false, but the sheet is already on its way out by then: the
1667
+ * styled one unmounts its portal after the exit animation, and the platform
1668
+ * one keeps its content mounted and only stops presenting it. Either way the
1669
+ * blur lands on a field nothing can see, and the keyboard stays up until the
1670
+ * system notices for itself, some seconds later, that nothing is focused.
1671
+ */
1397
1672
  const setOpen = useCallback(next => {
1673
+ if (!next) Keyboard.dismiss();
1398
1674
  if (openProp === undefined) setSearchOpen(next);
1399
1675
  onOpenChange?.(next);
1400
1676
  }, [onOpenChange, openProp, setSearchOpen]);
@@ -1461,12 +1737,25 @@ function PanelsideSearchSheet({
1461
1737
  },
1462
1738
  children: [showClose ? /*#__PURE__*/_jsx(View, {
1463
1739
  className: "flex-row px-4 pb-1",
1464
- children: /*#__PURE__*/_jsx(AnimatedPressable, {
1740
+ children: closeNative ? /*#__PURE__*/_jsx(Button, {
1741
+ native: true,
1742
+ glass: closeGlass,
1743
+ size: "icon",
1744
+ variant: "ghost",
1745
+ accessibilityLabel: closeLabel,
1746
+ onPress: close,
1747
+ children: /*#__PURE__*/_jsx(Glyph, {
1748
+ icon: Cancel01Icon,
1749
+ size: 17,
1750
+ color: glyph
1751
+ })
1752
+ }) : /*#__PURE__*/_jsx(AnimatedPressable, {
1465
1753
  onPress: close,
1466
1754
  accessibilityRole: "button",
1467
1755
  accessibilityLabel: closeLabel,
1468
- className: "h-9 w-9 items-center justify-center rounded-full bg-secondary",
1469
- children: /*#__PURE__*/_jsx(XIcon, {
1756
+ className: cn('h-9 w-9 items-center justify-center rounded-full', closeVariant === 'outline' ? 'border border-border' : 'bg-secondary'),
1757
+ children: /*#__PURE__*/_jsx(Glyph, {
1758
+ icon: Cancel01Icon,
1470
1759
  size: 17,
1471
1760
  color: glyph
1472
1761
  })
@@ -1489,6 +1778,8 @@ function PanelsideSearchSheet({
1489
1778
  */
1490
1779
  function PanelsideSearchTabs({
1491
1780
  className,
1781
+ align = 'center',
1782
+ listClassName,
1492
1783
  children
1493
1784
  }) {
1494
1785
  const {
@@ -1502,6 +1793,7 @@ function PanelsideSearchTabs({
1502
1793
  defaultValue: tab,
1503
1794
  className: cn('px-4 pt-1', className),
1504
1795
  children: /*#__PURE__*/_jsx(Tabs.List, {
1796
+ className: cn(align === 'center' && 'justify-center', listClassName),
1505
1797
  children: children
1506
1798
  })
1507
1799
  });
@@ -1512,12 +1804,19 @@ function PanelsideSearchTab({
1512
1804
  icon,
1513
1805
  children
1514
1806
  }) {
1515
- return /*#__PURE__*/_jsx(Tabs.Trigger, {
1516
- value: value,
1517
- icon: icon,
1518
- className: className,
1519
- children: children
1520
- });
1807
+ return (
1808
+ /*#__PURE__*/
1809
+ // A step of padding above the tab row's own. This row is the only control
1810
+ // on a surface that is otherwise a list and a field, and a closed tab here
1811
+ // is a glyph with nothing else to be aimed at by — so it is sized to be
1812
+ // hit rather than to the density a row of labelled tabs can afford.
1813
+ _jsx(Tabs.Trigger, {
1814
+ value: value,
1815
+ icon: icon,
1816
+ className: cn('px-4 py-3', className),
1817
+ children: children
1818
+ })
1819
+ );
1521
1820
  }
1522
1821
  /**
1523
1822
  * The scrolling middle of the search sheet.
@@ -1634,8 +1933,9 @@ function PanelsideSearchField({
1634
1933
  bottomInset: bottomInset,
1635
1934
  className: "w-full px-4 pb-1",
1636
1935
  children: /*#__PURE__*/_jsxs(View, {
1637
- className: cn('h-12 w-full flex-row items-center gap-2 rounded-full bg-secondary px-4', containerClassName),
1638
- children: [/*#__PURE__*/_jsx(SearchIcon, {
1936
+ className: cn('h-12 w-full flex-row items-center gap-2 rounded-full border border-border bg-surface px-4 shadow-lg', containerClassName),
1937
+ children: [/*#__PURE__*/_jsx(Glyph, {
1938
+ icon: Search01Icon,
1639
1939
  size: 17,
1640
1940
  color: muted
1641
1941
  }), /*#__PURE__*/_jsx(TextInput, {
@@ -1658,7 +1958,8 @@ function PanelsideSearchField({
1658
1958
  accessibilityRole: "button",
1659
1959
  accessibilityLabel: "Clear search",
1660
1960
  className: "h-5 w-5 items-center justify-center rounded-full bg-muted",
1661
- children: /*#__PURE__*/_jsx(XIcon, {
1961
+ children: /*#__PURE__*/_jsx(Glyph, {
1962
+ icon: Cancel01Icon,
1662
1963
  size: 12,
1663
1964
  color: muted
1664
1965
  })
@@ -1677,9 +1978,12 @@ PanelsideItemIcon.displayName = 'Panelside.ItemIcon';
1677
1978
  PanelsideItemLabel.displayName = 'Panelside.ItemLabel';
1678
1979
  PanelsideItemBadge.displayName = 'Panelside.ItemBadge';
1679
1980
  PanelsideAction.displayName = 'Panelside.Action';
1981
+ PanelsideItemActions.displayName = 'Panelside.ItemActions';
1680
1982
  PanelsideFooter.displayName = 'Panelside.Footer';
1681
1983
  PanelsideCta.displayName = 'Panelside.Cta';
1682
1984
  PanelsideScene.displayName = 'Panelside.Scene';
1985
+ PanelsidePages.displayName = 'Panelside.Pages';
1986
+ PanelsidePage.displayName = 'Panelside.Page';
1683
1987
  PanelsideTrigger.displayName = 'Panelside.Trigger';
1684
1988
  PanelsideSearchTrigger.displayName = 'Panelside.SearchTrigger';
1685
1989
  PanelsideSearchSheet.displayName = 'Panelside.SearchSheet';
@@ -1700,9 +2004,12 @@ export const Panelside = Object.assign(PanelsideRoot, {
1700
2004
  ItemLabel: PanelsideItemLabel,
1701
2005
  ItemBadge: PanelsideItemBadge,
1702
2006
  Action: PanelsideAction,
2007
+ ItemActions: PanelsideItemActions,
1703
2008
  Footer: PanelsideFooter,
1704
2009
  Cta: PanelsideCta,
1705
2010
  Scene: PanelsideScene,
2011
+ Pages: PanelsidePages,
2012
+ Page: PanelsidePage,
1706
2013
  Trigger: PanelsideTrigger,
1707
2014
  SearchTrigger: PanelsideSearchTrigger,
1708
2015
  SearchSheet: PanelsideSearchSheet,