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.
- package/lib/module/components/accordion/index.js +13 -1
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +9 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +527 -19
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/qr-code/index.js +84 -15
- package/lib/module/components/qr-code/index.js.map +1 -1
- package/lib/module/components/qr-code/qr-shapes.js +202 -0
- package/lib/module/components/qr-code/qr-shapes.js.map +1 -0
- package/lib/module/components/timeline/index.js +145 -18
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +16 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +242 -5
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/qr-code/index.d.ts +42 -3
- package/lib/typescript/src/components/qr-code/index.d.ts.map +1 -1
- package/lib/typescript/src/components/qr-code/qr-shapes.d.ts +64 -0
- package/lib/typescript/src/components/qr-code/qr-shapes.d.ts.map +1 -0
- package/lib/typescript/src/components/timeline/index.d.ts +33 -0
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/accordion/index.tsx +13 -0
- package/src/components/bottom-sheet/index.tsx +9 -2
- package/src/components/panelside/index.tsx +750 -28
- package/src/components/qr-code/index.tsx +124 -18
- package/src/components/qr-code/qr-shapes.ts +234 -0
- package/src/components/timeline/index.tsx +165 -14
- 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,
|
|
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
|
|
253
|
-
//
|
|
254
|
-
//
|
|
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-
|
|
263
|
-
lg: 'h-
|
|
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
|
-
|
|
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: [
|
|
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
|
|
889
|
-
const 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: [
|
|
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
|