panelui-native 0.79.0 → 0.80.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 (34) hide show
  1. package/lib/module/components/accordion/index.js +13 -1
  2. package/lib/module/components/accordion/index.js.map +1 -1
  3. package/lib/module/components/bottom-sheet/index.js +9 -2
  4. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  5. package/lib/module/components/panelside/index.js +527 -19
  6. package/lib/module/components/panelside/index.js.map +1 -1
  7. package/lib/module/components/qr-code/index.js +84 -15
  8. package/lib/module/components/qr-code/index.js.map +1 -1
  9. package/lib/module/components/qr-code/qr-shapes.js +202 -0
  10. package/lib/module/components/qr-code/qr-shapes.js.map +1 -0
  11. package/lib/module/components/timeline/index.js +145 -18
  12. package/lib/module/components/timeline/index.js.map +1 -1
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  15. package/lib/typescript/src/components/bottom-sheet/index.d.ts +16 -0
  16. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  17. package/lib/typescript/src/components/panelside/index.d.ts +242 -5
  18. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  19. package/lib/typescript/src/components/qr-code/index.d.ts +42 -3
  20. package/lib/typescript/src/components/qr-code/index.d.ts.map +1 -1
  21. package/lib/typescript/src/components/qr-code/qr-shapes.d.ts +64 -0
  22. package/lib/typescript/src/components/qr-code/qr-shapes.d.ts.map +1 -0
  23. package/lib/typescript/src/components/timeline/index.d.ts +33 -0
  24. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  25. package/lib/typescript/src/index.d.ts +1 -1
  26. package/lib/typescript/src/index.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/components/accordion/index.tsx +13 -0
  29. package/src/components/bottom-sheet/index.tsx +9 -2
  30. package/src/components/panelside/index.tsx +750 -28
  31. package/src/components/qr-code/index.tsx +124 -18
  32. package/src/components/qr-code/qr-shapes.ts +234 -0
  33. package/src/components/timeline/index.tsx +165 -14
  34. package/src/index.ts +3 -0
@@ -78,7 +78,7 @@
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, useState } from 'react';
81
+ import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
82
82
  import { 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';
@@ -86,15 +86,19 @@ 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 } from "../../icons/index.js";
89
+ import { EllipsisIcon, IconColorProvider, MenuIcon, SearchIcon, XIcon } from "../../icons/index.js";
90
+ import { BottomSheet, bottomSheetDetentHeight } from "../bottom-sheet/index.js";
90
91
  import { Button } from "../button/index.js";
92
+ import { Tabs } from "../tabs/index.js";
93
+ import { getNativeUI } from "../../native/index.js";
91
94
  import { AnimatedPressable } from "../../primitives/animated-pressable.js";
95
+ import { KeyboardAvoider } from "../../primitives/keyboard-avoider.js";
92
96
  import { Text, textChildren } from "../../primitives/text.js";
93
97
  import { useBackHandler } from "../../hooks/use-back-handler.js";
94
98
  import { useDirectionSign } from "../../hooks/use-direction.js";
95
99
  import { cn } from "../../utils/cn.js";
96
100
  import { selectionTick } from "../../utils/haptics.js";
97
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
101
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
98
102
  const SPRING = {
99
103
  damping: 24,
100
104
  stiffness: 300,
@@ -216,6 +220,20 @@ const DOCK_WIDTH_MAX = 320;
216
220
  /** How far above the floating footer the list starts dissolving into it. */
217
221
  const FOOTER_FADE = 28;
218
222
 
223
+ /**
224
+ * What `BottomSheet.Content` leaves below its last child, maxed against the
225
+ * home indicator. Mirrored here so the search field can subtract the strip it
226
+ * already sits above before it travels with the keyboard.
227
+ */
228
+ const SHEET_BOTTOM_PADDING = 16;
229
+
230
+ /**
231
+ * The top padding the search surface asks `BottomSheet.Content` for, and takes
232
+ * back off the column's height. Smaller than the sheet's own default, because
233
+ * this surface leads with a round button rather than with a title.
234
+ */
235
+ const SHEET_TOP_PADDING = 12;
236
+
219
237
  /** Progress past which a layer is treated as fully hidden for accessibility. */
220
238
  const HIDDEN_EPSILON = 0.05;
221
239
  const clamp = (value, min, max) => {
@@ -223,6 +241,16 @@ const clamp = (value, min, max) => {
223
241
 
224
242
  return Math.min(Math.max(value, min), max);
225
243
  };
244
+
245
+ /**
246
+ * What a header or a footer paints behind itself.
247
+ *
248
+ * `transparent` paints nothing, and the list runs the full height of the panel
249
+ * underneath it. `fade` dissolves the list into the panel background over the
250
+ * strip above the controls. `solid` is a band with an edge on it, for a footer
251
+ * that is a row of the layout rather than something floating over one.
252
+ */
253
+
226
254
  const itemVariants = tv({
227
255
  // No width: in a group it stretches on its own, and pinning it to full width
228
256
  // would stop it sharing a footer row with anything else. `shrink` because
@@ -249,9 +277,14 @@ const itemVariants = tv({
249
277
  const ctaVariants = tv({
250
278
  // Taller and wider than a list row's control. It is the one thing in the
251
279
  // panel you are meant to reach for without reading, so it should not be the
252
- // same size as the eight chat titles above it — but it shares its footer row
253
- // with an account button, and a pill that stands a whole step above that row
254
- // makes the footer taller than anything in the panel needs it to be.
280
+ // same size as the eight chat titles above it.
281
+ //
282
+ // It used to be 40pt, chosen to sit level with the account button beside it.
283
+ // That is the wrong thing to size it against: the account button is a target
284
+ // you find once, and the compose pill is the one pressed every session — at
285
+ // matching heights the two read as a pair of equals and the pill stopped
286
+ // being the thing the footer is for. Four points is enough to separate them
287
+ // without making the footer taller than the panel needs.
255
288
  base: 'shrink flex-row items-center justify-center rounded-full',
256
289
  variants: {
257
290
  variant: {
@@ -259,8 +292,8 @@ const ctaVariants = tv({
259
292
  secondary: 'bg-secondary'
260
293
  },
261
294
  size: {
262
- default: 'h-10 gap-2 px-5',
263
- lg: 'h-12 gap-2 px-6'
295
+ default: 'h-11 gap-2 px-6',
296
+ lg: 'h-13 gap-2.5 px-7'
264
297
  }
265
298
  },
266
299
  defaultVariants: {
@@ -287,14 +320,18 @@ export function usePanelside() {
287
320
  setOpen,
288
321
  toggle,
289
322
  progress,
290
- docked
323
+ docked,
324
+ searchOpen,
325
+ setSearchOpen
291
326
  } = usePanelsideContext('usePanelside');
292
327
  return {
293
328
  open,
294
329
  setOpen,
295
330
  toggle,
296
331
  progress,
297
- docked
332
+ docked,
333
+ searchOpen,
334
+ setSearchOpen
298
335
  };
299
336
  }
300
337
 
@@ -331,6 +368,7 @@ function PanelsideRoot({
331
368
  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
332
369
  const controlled = controlledOpen !== undefined;
333
370
  const open = controlled ? controlledOpen : uncontrolledOpen;
371
+ const [searchOpen, setSearchOpen] = useState(false);
334
372
 
335
373
  /*
336
374
  * Measured rather than taken from the window, because Panelside does not
@@ -465,8 +503,10 @@ function PanelsideRoot({
465
503
  dismissible,
466
504
  scale,
467
505
  radius,
468
- dim
469
- }), [dim, dismissible, docked, mode, open, progress, radius, scale, setOpen, toggle, width]);
506
+ dim,
507
+ searchOpen,
508
+ setSearchOpen
509
+ }), [dim, dismissible, docked, mode, open, progress, radius, scale, searchOpen, setOpen, toggle, width]);
470
510
  return /*#__PURE__*/_jsx(PanelsideContext.Provider, {
471
511
  value: context,
472
512
  children: /*#__PURE__*/_jsx(View, {
@@ -551,11 +591,14 @@ function PanelsideHeader({
551
591
  className,
552
592
  title,
553
593
  action,
594
+ surface = 'transparent',
554
595
  children,
555
596
  style,
556
597
  ...props
557
598
  }) {
558
599
  const insets = useSafeAreaInsets();
600
+ const background = useCSSVariable('--color-background');
601
+ const solid = typeof background === 'string' ? background : '#000000';
559
602
  return /*#__PURE__*/_jsxs(View
560
603
  // The panel draws behind the status bar on purpose, so it reads as a
561
604
  // full-height surface rather than a card. That makes the inset the
@@ -569,9 +612,31 @@ function PanelsideHeader({
569
612
  // share one edge. A header inset further would leave the field floating
570
613
  // a few points inside the list it filters.
571
614
  ,
572
- className: cn('gap-3 px-3 pb-3', className),
615
+ className: cn('gap-3 px-3 pb-3', surface === 'solid' && 'border-b border-border bg-background', className),
573
616
  ...props,
574
- children: [(title || action) && /*#__PURE__*/_jsxs(View, {
617
+ children: [surface === 'fade' ? /*#__PURE__*/_jsxs(_Fragment, {
618
+ children: [/*#__PURE__*/_jsx(View, {
619
+ pointerEvents: "none",
620
+ className: "absolute end-0 start-0 top-0 bg-background",
621
+ style: {
622
+ bottom: FOOTER_FADE
623
+ }
624
+ }), /*#__PURE__*/_jsx(LinearGradient, {
625
+ colors: [solid, `${solid}00`],
626
+ start: {
627
+ x: 0,
628
+ y: 0
629
+ },
630
+ end: {
631
+ x: 0,
632
+ y: 1
633
+ },
634
+ pointerEvents: "none",
635
+ style: [styles.rise, {
636
+ height: FOOTER_FADE
637
+ }]
638
+ })]
639
+ }) : null, (title || action) && /*#__PURE__*/_jsxs(View, {
575
640
  className: "h-9 flex-row items-center justify-between gap-2",
576
641
  children: [title ? /*#__PURE__*/_jsx(Text, {
577
642
  size: "xl",
@@ -880,15 +945,20 @@ function PanelsideAction({
880
945
  function PanelsideFooter({
881
946
  className,
882
947
  floating = true,
948
+ surface = 'transparent',
883
949
  children,
884
950
  style,
885
951
  ...props
886
952
  }) {
887
953
  const insets = useSafeAreaInsets();
888
- const surface = useContext(PanelsideSurfaceContext);
889
- const setFooterHeight = surface?.setFooterHeight;
954
+ const panel = useContext(PanelsideSurfaceContext);
955
+ const setFooterHeight = panel?.setFooterHeight;
890
956
  const background = useCSSVariable('--color-background');
891
957
  const solid = typeof background === 'string' ? background : '#000000';
958
+
959
+ // A footer in the flow has nothing to float over, so the only thing it can
960
+ // be is the band — whatever was asked for.
961
+ const paint = floating ? surface : 'solid';
892
962
  const onLayout = useCallback(event => {
893
963
  setFooterHeight?.(event.nativeEvent.layout.height);
894
964
  }, [setFooterHeight]);
@@ -897,7 +967,7 @@ function PanelsideFooter({
897
967
  style: [{
898
968
  paddingBottom: Math.max(insets.bottom, 12)
899
969
  }, floating ? {
900
- paddingTop: FOOTER_FADE
970
+ paddingTop: paint === 'fade' ? FOOTER_FADE : 12
901
971
  } : null, style],
902
972
  className: cn(
903
973
  // `gap-3` and a wider inset: the compose control is the one thing in
@@ -906,7 +976,7 @@ function PanelsideFooter({
906
976
  // list uses.
907
977
  'flex-row items-center gap-3 px-4', floating ? 'absolute bottom-0 end-0 start-0' : 'border-t border-border bg-background pt-2', className),
908
978
  ...props,
909
- children: [floating ? /*#__PURE__*/_jsxs(_Fragment, {
979
+ children: [paint === 'fade' ? /*#__PURE__*/_jsxs(_Fragment, {
910
980
  children: [/*#__PURE__*/_jsx(LinearGradient, {
911
981
  colors: [`${solid}00`, solid],
912
982
  start: {
@@ -928,6 +998,9 @@ function PanelsideFooter({
928
998
  top: FOOTER_FADE
929
999
  }
930
1000
  })]
1001
+ }) : null, paint === 'solid' && floating ? /*#__PURE__*/_jsx(View, {
1002
+ pointerEvents: "none",
1003
+ className: "absolute bottom-0 end-0 start-0 top-0 border-t border-border bg-background"
931
1004
  }) : null, textChildren(children)]
932
1005
  });
933
1006
  }
@@ -937,6 +1010,12 @@ const styles = StyleSheet.create({
937
1010
  top: 0,
938
1011
  left: 0,
939
1012
  right: 0
1013
+ },
1014
+ rise: {
1015
+ position: 'absolute',
1016
+ bottom: 0,
1017
+ left: 0,
1018
+ right: 0
940
1019
  }
941
1020
  });
942
1021
  function PanelsideCta({
@@ -1172,6 +1251,421 @@ function PanelsideTrigger({
1172
1251
  })
1173
1252
  });
1174
1253
  }
1254
+
1255
+ /* ------------------------------------------------------------------ *
1256
+ * Search.
1257
+ *
1258
+ * A field in the header is the obvious way to put search in a navigation
1259
+ * panel, and it is the wrong one on a phone. The panel is 80% of the screen
1260
+ * and the field is 40 points of it, so a search that returns anything has to
1261
+ * push the history down the screen it is already filling — and the field is at
1262
+ * the top, which is the far end of the screen from the keyboard that has just
1263
+ * opened under it.
1264
+ *
1265
+ * So search is a surface rather than a row. A round button in the header opens
1266
+ * a sheet that is the whole screen; the tabs across the top narrow what is
1267
+ * being searched; the results fill the middle; and the field is at the bottom,
1268
+ * where the thumb already is, riding the keyboard rather than hiding behind
1269
+ * it.
1270
+ *
1271
+ * `Panelside.Search` — the inline field — is still exported, and is still
1272
+ * right for a docked panel on a tablet, where there is width for a field and
1273
+ * no keyboard covering half the screen.
1274
+ * ------------------------------------------------------------------ */
1275
+
1276
+ /**
1277
+ * The button that opens the search surface. Goes in `Panelside.Header`'s
1278
+ * `action` slot.
1279
+ *
1280
+ * It toggles the root's `searchOpen`, which `Panelside.SearchSheet` reads —
1281
+ * so the two need nothing wired between them.
1282
+ */
1283
+ function PanelsideSearchTrigger({
1284
+ className,
1285
+ label = 'Search',
1286
+ children,
1287
+ native = false,
1288
+ glass = false,
1289
+ onPress,
1290
+ ...props
1291
+ }) {
1292
+ const {
1293
+ setSearchOpen
1294
+ } = usePanelsideContext('Panelside.SearchTrigger');
1295
+ const tint = useCSSVariable('--color-foreground');
1296
+ const color = typeof tint === 'string' ? tint : undefined;
1297
+ const open = useCallback(event => {
1298
+ onPress?.(event);
1299
+ setSearchOpen(true);
1300
+ }, [onPress, setSearchOpen]);
1301
+ const glyph = children ?? /*#__PURE__*/_jsx(SearchIcon, {
1302
+ size: 18,
1303
+ color: native ? color : undefined
1304
+ });
1305
+ if (native) {
1306
+ return /*#__PURE__*/_jsx(Button, {
1307
+ native: true,
1308
+ glass: glass,
1309
+ size: "icon",
1310
+ variant: "ghost",
1311
+ accessibilityLabel: label,
1312
+ onPress: open,
1313
+ children: glyph
1314
+ });
1315
+ }
1316
+ return /*#__PURE__*/_jsx(AnimatedPressable, {
1317
+ ...props,
1318
+ 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),
1324
+ accessibilityRole: "button",
1325
+ accessibilityLabel: label,
1326
+ children: /*#__PURE__*/_jsx(IconColorProvider, {
1327
+ color: color,
1328
+ children: glyph
1329
+ })
1330
+ });
1331
+ }
1332
+ const PanelsideSearchSheetContext = /*#__PURE__*/createContext(null);
1333
+ function usePanelsideSearchSheet(part) {
1334
+ const value = useContext(PanelsideSearchSheetContext);
1335
+ if (!value) throw new Error(`${part} must be used inside a <Panelside.SearchSheet>.`);
1336
+ return value;
1337
+ }
1338
+ /**
1339
+ * The search surface: tabs, results, and a field at the bottom.
1340
+ *
1341
+ * ```tsx
1342
+ * <Panelside.SearchSheet value={query} onValueChange={setQuery} tab={tab} onTabChange={setTab}>
1343
+ * <Panelside.SearchTabs>
1344
+ * <Panelside.SearchTab value="all" icon={<SparklesIcon size={16} />}>All</Panelside.SearchTab>
1345
+ * <Panelside.SearchTab value="chats" icon={<MessageCircleIcon size={16} />}>Chats</Panelside.SearchTab>
1346
+ * </Panelside.SearchTabs>
1347
+ *
1348
+ * <Panelside.SearchResults>
1349
+ * {hits.map((hit) => (
1350
+ * <Panelside.SearchResult key={hit.id} title={hit.title} description={hit.kind} />
1351
+ * ))}
1352
+ * </Panelside.SearchResults>
1353
+ *
1354
+ * <Panelside.SearchField placeholder="Search chats" />
1355
+ * </Panelside.SearchSheet>
1356
+ * ```
1357
+ *
1358
+ * Put it under `<Panelside>` and outside `Panelside.Panel` — a sheet is
1359
+ * presented over the whole app, and the panel is a layer that slides.
1360
+ *
1361
+ * It reports what was typed and which tab is selected. What counts as a match,
1362
+ * and what a result is, are yours: a search that only read chat titles would
1363
+ * be wrong for the first app that indexes message bodies.
1364
+ */
1365
+ function PanelsideSearchSheet({
1366
+ className,
1367
+ open: openProp,
1368
+ onOpenChange,
1369
+ value,
1370
+ defaultValue = '',
1371
+ onValueChange,
1372
+ tab,
1373
+ defaultTab = '',
1374
+ onTabChange,
1375
+ native = true,
1376
+ snapPoints,
1377
+ keyboardGap = 10,
1378
+ showClose = true,
1379
+ closeLabel = 'Close search',
1380
+ children
1381
+ }) {
1382
+ const {
1383
+ searchOpen,
1384
+ setSearchOpen
1385
+ } = usePanelsideContext('Panelside.SearchSheet');
1386
+ const insets = useSafeAreaInsets();
1387
+ const {
1388
+ height: screenHeight
1389
+ } = useWindowDimensions();
1390
+ const tint = useCSSVariable('--color-foreground');
1391
+ const glyph = typeof tint === 'string' ? tint : undefined;
1392
+ const [uncontrolledQuery, setUncontrolledQuery] = useState(defaultValue);
1393
+ const [uncontrolledTab, setUncontrolledTab] = useState(defaultTab);
1394
+ const open = openProp ?? searchOpen;
1395
+ const query = value ?? uncontrolledQuery;
1396
+ const activeTab = tab ?? uncontrolledTab;
1397
+ const setOpen = useCallback(next => {
1398
+ if (openProp === undefined) setSearchOpen(next);
1399
+ onOpenChange?.(next);
1400
+ }, [onOpenChange, openProp, setSearchOpen]);
1401
+ const setQuery = useCallback(next => {
1402
+ if (value === undefined) setUncontrolledQuery(next);
1403
+ onValueChange?.(next);
1404
+ }, [onValueChange, value]);
1405
+ const setTab = useCallback(next => {
1406
+ if (tab === undefined) setUncontrolledTab(next);
1407
+ onTabChange?.(next);
1408
+ }, [onTabChange, tab]);
1409
+ const close = useCallback(() => setOpen(false), [setOpen]);
1410
+ const detents = snapPoints ?? ['full'];
1411
+
1412
+ /*
1413
+ * What `BottomSheet.Content` pads the bottom of the sheet by. The field
1414
+ * already sits that far above the screen edge, and docking travels by the
1415
+ * keyboard's height less whatever the element has already cleared — so
1416
+ * getting this wrong is a gap under the field, or the field over the
1417
+ * keyboard's top row.
1418
+ */
1419
+ const bottomInset = Math.max(insets.bottom, SHEET_BOTTOM_PADDING) - keyboardGap;
1420
+
1421
+ /*
1422
+ * A definite height for the column, not `flex-1` against the sheet's own.
1423
+ *
1424
+ * The platform sheet gives its hosted content a *minimum* height, and a
1425
+ * minimum is not something `flex-1` can divide: the results list sizes to
1426
+ * its own rows instead, grows past the sheet, and pushes the field off the
1427
+ * bottom — which is a search surface with no way to type in it. So the
1428
+ * column is told exactly how tall it is, and the list gets the room left
1429
+ * between the tabs and the field.
1430
+ *
1431
+ * Only under the platform sheet. The styled one is laid out by us and has a
1432
+ * real height already, so `flex-1` resolves there and a second opinion about
1433
+ * how tall the sheet is would only be a chance to disagree with it.
1434
+ */
1435
+ const hosted = native && getNativeUI() !== null;
1436
+ const columnHeight = hosted ? (bottomSheetDetentHeight(detents, screenHeight) ?? screenHeight * 0.9) - SHEET_TOP_PADDING - Math.max(insets.bottom, SHEET_BOTTOM_PADDING) : undefined;
1437
+ const context = useMemo(() => ({
1438
+ open,
1439
+ query,
1440
+ setQuery,
1441
+ tab: activeTab,
1442
+ setTab,
1443
+ close,
1444
+ bottomInset
1445
+ }), [activeTab, bottomInset, close, open, query, setQuery, setTab]);
1446
+ return /*#__PURE__*/_jsx(BottomSheet, {
1447
+ native: native,
1448
+ open: open,
1449
+ onOpenChange: setOpen,
1450
+ snapPoints: detents,
1451
+ children: /*#__PURE__*/_jsx(BottomSheet.Content, {
1452
+ size: "full",
1453
+ showClose: false,
1454
+ className: "gap-0 px-0 pt-3",
1455
+ children: /*#__PURE__*/_jsx(PanelsideSearchSheetContext.Provider, {
1456
+ value: context,
1457
+ children: /*#__PURE__*/_jsxs(View, {
1458
+ className: cn('gap-2', columnHeight === undefined && 'flex-1', className),
1459
+ style: columnHeight === undefined ? undefined : {
1460
+ height: columnHeight
1461
+ },
1462
+ children: [showClose ? /*#__PURE__*/_jsx(View, {
1463
+ className: "flex-row px-4 pb-1",
1464
+ children: /*#__PURE__*/_jsx(AnimatedPressable, {
1465
+ onPress: close,
1466
+ accessibilityRole: "button",
1467
+ accessibilityLabel: closeLabel,
1468
+ className: "h-9 w-9 items-center justify-center rounded-full bg-secondary",
1469
+ children: /*#__PURE__*/_jsx(XIcon, {
1470
+ size: 17,
1471
+ color: glyph
1472
+ })
1473
+ })
1474
+ }) : null, children]
1475
+ })
1476
+ })
1477
+ })
1478
+ });
1479
+ }
1480
+ /**
1481
+ * The row across the top of the search sheet, narrowing what is searched.
1482
+ *
1483
+ * The expanding variant: only the selected tab is open, and the rest are their
1484
+ * icons. A row of four full labels takes the whole width to say four words
1485
+ * nobody rereads, and this row has to leave the results the screen.
1486
+ *
1487
+ * Every `Panelside.SearchTab` therefore needs an `icon` — a closed tab has
1488
+ * nothing else to be.
1489
+ */
1490
+ function PanelsideSearchTabs({
1491
+ className,
1492
+ children
1493
+ }) {
1494
+ const {
1495
+ tab,
1496
+ setTab
1497
+ } = usePanelsideSearchSheet('Panelside.SearchTabs');
1498
+ return /*#__PURE__*/_jsx(Tabs, {
1499
+ variant: "expanding",
1500
+ value: tab,
1501
+ onValueChange: setTab,
1502
+ defaultValue: tab,
1503
+ className: cn('px-4 pt-1', className),
1504
+ children: /*#__PURE__*/_jsx(Tabs.List, {
1505
+ children: children
1506
+ })
1507
+ });
1508
+ }
1509
+ function PanelsideSearchTab({
1510
+ className,
1511
+ value,
1512
+ icon,
1513
+ children
1514
+ }) {
1515
+ return /*#__PURE__*/_jsx(Tabs.Trigger, {
1516
+ value: value,
1517
+ icon: icon,
1518
+ className: className,
1519
+ children: children
1520
+ });
1521
+ }
1522
+ /**
1523
+ * The scrolling middle of the search sheet.
1524
+ *
1525
+ * `BottomSheet.Body` rather than a `ScrollView`, so the list's scroll and the
1526
+ * sheet's dismiss drag agree on which of them a downward pull belongs to.
1527
+ */
1528
+ function PanelsideSearchResults({
1529
+ className,
1530
+ contentContainerClassName,
1531
+ children,
1532
+ ...props
1533
+ }) {
1534
+ usePanelsideSearchSheet('Panelside.SearchResults');
1535
+ return /*#__PURE__*/_jsx(BottomSheet.Body, {
1536
+ className: cn('flex-1', className),
1537
+ contentContainerClassName: cn('gap-1 px-4 pb-4 pt-1', contentContainerClassName),
1538
+ keyboardShouldPersistTaps: "handled",
1539
+ keyboardDismissMode: "interactive",
1540
+ ...props,
1541
+ children: textChildren(children)
1542
+ });
1543
+ }
1544
+ /**
1545
+ * One hit. A leading thumbnail, a title, and a line saying what it is.
1546
+ *
1547
+ * Taller than a `Panelside.Item`, and deliberately: a navigation row is a
1548
+ * place you already know the name of, and a result is a thing you are deciding
1549
+ * about — the second line is what the decision is made on.
1550
+ */
1551
+ function PanelsideSearchResult({
1552
+ className,
1553
+ media,
1554
+ title,
1555
+ description,
1556
+ children,
1557
+ ...props
1558
+ }) {
1559
+ const tint = useCSSVariable('--color-muted-foreground');
1560
+ const color = typeof tint === 'string' ? tint : undefined;
1561
+ return /*#__PURE__*/_jsxs(AnimatedPressable, {
1562
+ className: cn('flex-row items-center gap-3 rounded-xl px-2 py-2', className),
1563
+ accessibilityRole: "button",
1564
+ accessibilityLabel: title,
1565
+ pressScale: 0.985,
1566
+ ...props,
1567
+ children: [media ? /*#__PURE__*/_jsx(View, {
1568
+ className: "h-11 w-11 items-center justify-center overflow-hidden rounded-xl bg-secondary",
1569
+ children: /*#__PURE__*/_jsx(IconColorProvider, {
1570
+ color: color,
1571
+ children: media
1572
+ })
1573
+ }) : null, title !== undefined || description !== undefined ? /*#__PURE__*/_jsxs(View, {
1574
+ className: "flex-1 gap-0.5",
1575
+ children: [title !== undefined ? /*#__PURE__*/_jsx(Text, {
1576
+ size: "base",
1577
+ weight: "medium",
1578
+ numberOfLines: 1,
1579
+ children: title
1580
+ }) : null, description !== undefined ? /*#__PURE__*/_jsx(Text, {
1581
+ size: "sm",
1582
+ muted: true,
1583
+ numberOfLines: 1,
1584
+ children: description
1585
+ }) : null]
1586
+ }) : null, children]
1587
+ });
1588
+ }
1589
+ /**
1590
+ * The field, at the bottom of the sheet, riding the keyboard.
1591
+ *
1592
+ * At the bottom because that is where the thumb is and where the keyboard
1593
+ * comes up: a field at the top of a full-height sheet is at the far end of the
1594
+ * screen from both, and every character typed into it is read at the other
1595
+ * end of a list that is moving.
1596
+ *
1597
+ * The way out of the surface is not here — `Panelside.SearchSheet` draws it at
1598
+ * the leading edge of the top row, so the dismiss control does not move with
1599
+ * the keyboard and is not one mis-tap away from the field.
1600
+ */
1601
+ function PanelsideSearchField({
1602
+ className,
1603
+ containerClassName,
1604
+ placeholder = 'Search',
1605
+ value,
1606
+ onChangeText,
1607
+ ...props
1608
+ }) {
1609
+ const {
1610
+ open,
1611
+ query,
1612
+ setQuery,
1613
+ bottomInset
1614
+ } = usePanelsideSearchSheet('Panelside.SearchField');
1615
+ const placeholderTint = useCSSVariable('--color-muted-foreground');
1616
+ const textTint = useCSSVariable('--color-foreground');
1617
+ const muted = typeof placeholderTint === 'string' ? placeholderTint : undefined;
1618
+ const field = useRef(null);
1619
+ const text = value ?? query;
1620
+ const change = onChangeText ?? setQuery;
1621
+
1622
+ /*
1623
+ * Focus on the transition rather than with `autoFocus`. Under `native` the
1624
+ * sheet's content is mounted for the life of the screen and only
1625
+ * `isPresented` changes, so `autoFocus` fires once — at startup, on a sheet
1626
+ * that is not up — and the keyboard opens over whatever is.
1627
+ */
1628
+ useEffect(() => {
1629
+ if (open) field.current?.focus();else field.current?.blur();
1630
+ }, [open]);
1631
+ return /*#__PURE__*/_jsx(KeyboardAvoider, {
1632
+ mode: "dock",
1633
+ active: open,
1634
+ bottomInset: bottomInset,
1635
+ className: "w-full px-4 pb-1",
1636
+ 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, {
1639
+ size: 17,
1640
+ color: muted
1641
+ }), /*#__PURE__*/_jsx(TextInput, {
1642
+ ref: field,
1643
+ value: text,
1644
+ onChangeText: change,
1645
+ placeholder: placeholder,
1646
+ placeholderTextColor: muted
1647
+ /* `text-[16px]` rather than a `text-*` step — see Panelside.Search. */,
1648
+ className: cn('h-full flex-1 font-normal text-[16px] text-foreground', className),
1649
+ style: typeof textTint === 'string' ? {
1650
+ color: textTint
1651
+ } : undefined,
1652
+ accessibilityRole: "search",
1653
+ returnKeyType: "search",
1654
+ ...props
1655
+ }), text.length > 0 ? /*#__PURE__*/_jsx(AnimatedPressable, {
1656
+ onPress: () => change(''),
1657
+ hitSlop: 8,
1658
+ accessibilityRole: "button",
1659
+ accessibilityLabel: "Clear search",
1660
+ className: "h-5 w-5 items-center justify-center rounded-full bg-muted",
1661
+ children: /*#__PURE__*/_jsx(XIcon, {
1662
+ size: 12,
1663
+ color: muted
1664
+ })
1665
+ }) : null]
1666
+ })
1667
+ });
1668
+ }
1175
1669
  PanelsidePanel.displayName = 'Panelside.Panel';
1176
1670
  PanelsideHeader.displayName = 'Panelside.Header';
1177
1671
  PanelsideSearch.displayName = 'Panelside.Search';
@@ -1187,6 +1681,13 @@ PanelsideFooter.displayName = 'Panelside.Footer';
1187
1681
  PanelsideCta.displayName = 'Panelside.Cta';
1188
1682
  PanelsideScene.displayName = 'Panelside.Scene';
1189
1683
  PanelsideTrigger.displayName = 'Panelside.Trigger';
1684
+ PanelsideSearchTrigger.displayName = 'Panelside.SearchTrigger';
1685
+ PanelsideSearchSheet.displayName = 'Panelside.SearchSheet';
1686
+ PanelsideSearchTabs.displayName = 'Panelside.SearchTabs';
1687
+ PanelsideSearchTab.displayName = 'Panelside.SearchTab';
1688
+ PanelsideSearchResults.displayName = 'Panelside.SearchResults';
1689
+ PanelsideSearchResult.displayName = 'Panelside.SearchResult';
1690
+ PanelsideSearchField.displayName = 'Panelside.SearchField';
1190
1691
  export const Panelside = Object.assign(PanelsideRoot, {
1191
1692
  Panel: PanelsidePanel,
1192
1693
  Header: PanelsideHeader,
@@ -1202,6 +1703,13 @@ export const Panelside = Object.assign(PanelsideRoot, {
1202
1703
  Footer: PanelsideFooter,
1203
1704
  Cta: PanelsideCta,
1204
1705
  Scene: PanelsideScene,
1205
- Trigger: PanelsideTrigger
1706
+ Trigger: PanelsideTrigger,
1707
+ SearchTrigger: PanelsideSearchTrigger,
1708
+ SearchSheet: PanelsideSearchSheet,
1709
+ SearchTabs: PanelsideSearchTabs,
1710
+ SearchTab: PanelsideSearchTab,
1711
+ SearchResults: PanelsideSearchResults,
1712
+ SearchResult: PanelsideSearchResult,
1713
+ SearchField: PanelsideSearchField
1206
1714
  });
1207
1715
  //# sourceMappingURL=index.js.map