panelui-native 0.83.0 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/lib/module/components/bottom-sheet/index.js +3 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +37 -16
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/circular-text/circular-text-geometry.js +63 -0
- package/lib/module/components/circular-text/circular-text-geometry.js.map +1 -0
- package/lib/module/components/circular-text/index.js +146 -0
- package/lib/module/components/circular-text/index.js.map +1 -0
- package/lib/module/components/flip-card/index.js +338 -0
- package/lib/module/components/flip-card/index.js.map +1 -0
- package/lib/module/components/marquee/index.js +12 -5
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +15 -417
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/popover/index.js +120 -7
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/select/index.js +3 -2
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +16 -5
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/switch/index.js +28 -7
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/icons/index.js +24 -4
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js +35 -0
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/native/native-host.js +62 -0
- package/lib/module/native/native-host.js.map +1 -0
- package/lib/module/primitives/glass.js +14 -0
- package/lib/module/primitives/glass.js.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/circular-text/circular-text-geometry.d.ts +39 -0
- package/lib/typescript/src/components/circular-text/circular-text-geometry.d.ts.map +1 -0
- package/lib/typescript/src/components/circular-text/index.d.ts +69 -0
- package/lib/typescript/src/components/circular-text/index.d.ts.map +1 -0
- package/lib/typescript/src/components/flip-card/index.d.ts +116 -0
- package/lib/typescript/src/components/flip-card/index.d.ts.map +1 -0
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +10 -202
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/popover/index.d.ts +25 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts +0 -4
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +67 -10
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/native/native-host.d.ts +71 -0
- package/lib/typescript/src/native/native-host.d.ts.map +1 -0
- package/lib/typescript/src/primitives/glass.d.ts.map +1 -1
- package/package.json +11 -1
- package/src/components/bottom-sheet/index.tsx +8 -3
- package/src/components/button/index.tsx +36 -15
- package/src/components/circular-text/circular-text-geometry.ts +80 -0
- package/src/components/circular-text/index.tsx +217 -0
- package/src/components/flip-card/index.tsx +419 -0
- package/src/components/marquee/index.tsx +12 -5
- package/src/components/panelside/index.tsx +16 -604
- package/src/components/popover/index.tsx +146 -4
- package/src/components/select/index.tsx +3 -3
- package/src/components/slider/index.tsx +28 -6
- package/src/components/switch/index.tsx +28 -7
- package/src/icons/index.tsx +23 -2
- package/src/index.ts +10 -6
- package/src/native/index.ts +89 -9
- package/src/native/native-host.tsx +75 -0
- package/src/primitives/glass.tsx +12 -0
|
@@ -411,22 +411,12 @@ interface PanelsideContextValue {
|
|
|
411
411
|
scale?: number;
|
|
412
412
|
radius?: number;
|
|
413
413
|
dim?: number;
|
|
414
|
-
/**
|
|
415
|
-
* Whether the search surface is up.
|
|
416
|
-
*
|
|
417
|
-
* It lives on the root rather than on the sheet because the two halves of
|
|
418
|
-
* search are in different subtrees: the button is in the header and the
|
|
419
|
-
* sheet is a sibling of the panel. Anything else means every app wiring one
|
|
420
|
-
* `useState` through both.
|
|
421
|
-
*/
|
|
422
|
-
searchOpen: boolean;
|
|
423
|
-
setSearchOpen: (open: boolean) => void;
|
|
424
414
|
/**
|
|
425
415
|
* The page the scene is showing.
|
|
426
416
|
*
|
|
427
|
-
* On the root
|
|
428
|
-
* are in the
|
|
429
|
-
*
|
|
417
|
+
* On the root because the rows that navigate are in the panel and the pages
|
|
418
|
+
* are in the scene, which are different subtrees. Anything else is one
|
|
419
|
+
* `useState` every app threads through both.
|
|
430
420
|
*/
|
|
431
421
|
route: string;
|
|
432
422
|
navigate: (route: string) => void;
|
|
@@ -454,9 +444,6 @@ export interface UsePanelsideResult {
|
|
|
454
444
|
progress: SharedValue<number>;
|
|
455
445
|
/** True while the panel is docked open beside the scene. */
|
|
456
446
|
docked: boolean;
|
|
457
|
-
/** Whether the search surface is up. `Panelside.SearchTrigger` sets it. */
|
|
458
|
-
searchOpen: boolean;
|
|
459
|
-
setSearchOpen: (open: boolean) => void;
|
|
460
447
|
/** The page the scene is showing — a `Panelside.Page`'s `value`. */
|
|
461
448
|
route: string;
|
|
462
449
|
/** Go to a page, closing the panel on the way. */
|
|
@@ -469,7 +456,7 @@ export interface UsePanelsideResult {
|
|
|
469
456
|
* lives.
|
|
470
457
|
*/
|
|
471
458
|
export function usePanelside(): UsePanelsideResult {
|
|
472
|
-
const { open, setOpen, toggle, progress, docked,
|
|
459
|
+
const { open, setOpen, toggle, progress, docked, route, navigate } =
|
|
473
460
|
usePanelsideContext('usePanelside');
|
|
474
461
|
|
|
475
462
|
// `navigate` closes the panel. The root cannot do it there without making
|
|
@@ -482,7 +469,7 @@ export function usePanelside(): UsePanelsideResult {
|
|
|
482
469
|
[navigate, setOpen]
|
|
483
470
|
);
|
|
484
471
|
|
|
485
|
-
return { open, setOpen, toggle, progress, docked,
|
|
472
|
+
return { open, setOpen, toggle, progress, docked, route, navigate: go };
|
|
486
473
|
}
|
|
487
474
|
|
|
488
475
|
/**
|
|
@@ -621,8 +608,6 @@ function PanelsideRoot({
|
|
|
621
608
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
622
609
|
const controlled = controlledOpen !== undefined;
|
|
623
610
|
const open = controlled ? controlledOpen : uncontrolledOpen;
|
|
624
|
-
const [searchOpen, setSearchOpen] = useState(false);
|
|
625
|
-
|
|
626
611
|
const [uncontrolledRoute, setUncontrolledRoute] = useState(defaultRoute);
|
|
627
612
|
const route = controlledRoute ?? uncontrolledRoute;
|
|
628
613
|
|
|
@@ -815,8 +800,6 @@ function PanelsideRoot({
|
|
|
815
800
|
scale,
|
|
816
801
|
radius,
|
|
817
802
|
dim,
|
|
818
|
-
searchOpen,
|
|
819
|
-
setSearchOpen,
|
|
820
803
|
route,
|
|
821
804
|
navigate,
|
|
822
805
|
}),
|
|
@@ -831,7 +814,6 @@ function PanelsideRoot({
|
|
|
831
814
|
radius,
|
|
832
815
|
route,
|
|
833
816
|
scale,
|
|
834
|
-
searchOpen,
|
|
835
817
|
setOpen,
|
|
836
818
|
toggle,
|
|
837
819
|
width,
|
|
@@ -2126,11 +2108,17 @@ export interface PanelsideSearchTriggerProps extends Omit<PressableProps, 'child
|
|
|
2126
2108
|
}
|
|
2127
2109
|
|
|
2128
2110
|
/**
|
|
2129
|
-
* The
|
|
2130
|
-
* `action` slot.
|
|
2111
|
+
* The search button. Goes in `Panelside.Header`'s `action` slot.
|
|
2131
2112
|
*
|
|
2132
|
-
* It
|
|
2133
|
-
*
|
|
2113
|
+
* It draws the control and nothing else: a circle, or the platform's own
|
|
2114
|
+
* button under `native`, with the magnifier already in it. What pressing it
|
|
2115
|
+
* opens is `onPress`'s to decide — a page the scene navigates to, a sheet, a
|
|
2116
|
+
* screen pushed onto the app's own stack.
|
|
2117
|
+
*
|
|
2118
|
+
* That is the same arrangement `Panelside.Trigger` has, and it is here for the
|
|
2119
|
+
* same reason: where search goes is a decision about the app, and a component
|
|
2120
|
+
* that answers it for you is one you have to work around the first time the
|
|
2121
|
+
* answer is different.
|
|
2134
2122
|
*/
|
|
2135
2123
|
function PanelsideSearchTrigger({
|
|
2136
2124
|
className,
|
|
@@ -2142,17 +2130,10 @@ function PanelsideSearchTrigger({
|
|
|
2142
2130
|
onPress,
|
|
2143
2131
|
...props
|
|
2144
2132
|
}: PanelsideSearchTriggerProps) {
|
|
2145
|
-
const { setSearchOpen } = usePanelsideContext('Panelside.SearchTrigger');
|
|
2146
2133
|
const tint = useCSSVariable('--color-foreground');
|
|
2147
2134
|
const color = typeof tint === 'string' ? tint : undefined;
|
|
2148
2135
|
|
|
2149
|
-
const open =
|
|
2150
|
-
(event: Parameters<NonNullable<PressableProps['onPress']>>[0]) => {
|
|
2151
|
-
onPress?.(event);
|
|
2152
|
-
setSearchOpen(true);
|
|
2153
|
-
},
|
|
2154
|
-
[onPress, setSearchOpen]
|
|
2155
|
-
);
|
|
2136
|
+
const open = onPress;
|
|
2156
2137
|
|
|
2157
2138
|
const glyph = children ?? <Glyph icon={Search01Icon} size={18} color={native ? color : undefined} />;
|
|
2158
2139
|
|
|
@@ -2192,563 +2173,6 @@ function PanelsideSearchTrigger({
|
|
|
2192
2173
|
);
|
|
2193
2174
|
}
|
|
2194
2175
|
|
|
2195
|
-
interface PanelsideSearchSheetContextValue {
|
|
2196
|
-
/**
|
|
2197
|
-
* Whether the sheet is up.
|
|
2198
|
-
*
|
|
2199
|
-
* The field needs it, and cannot read it from being mounted: under `native`
|
|
2200
|
-
* the platform owns presentation, so the content stays mounted for the life
|
|
2201
|
-
* of the screen and only `isPresented` changes. An `autoFocus` on a field
|
|
2202
|
-
* inside it would fire at app start, opening the keyboard over a sheet
|
|
2203
|
-
* nobody has asked for.
|
|
2204
|
-
*/
|
|
2205
|
-
open: boolean;
|
|
2206
|
-
query: string;
|
|
2207
|
-
setQuery: (query: string) => void;
|
|
2208
|
-
tab: string;
|
|
2209
|
-
setTab: (tab: string) => void;
|
|
2210
|
-
close: () => void;
|
|
2211
|
-
/** The inset the field already sits above, so docking does not travel it twice. */
|
|
2212
|
-
bottomInset: number;
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
const PanelsideSearchSheetContext = createContext<PanelsideSearchSheetContextValue | null>(
|
|
2216
|
-
null
|
|
2217
|
-
);
|
|
2218
|
-
|
|
2219
|
-
function usePanelsideSearchSheet(part: string): PanelsideSearchSheetContextValue {
|
|
2220
|
-
const value = useContext(PanelsideSearchSheetContext);
|
|
2221
|
-
if (!value) throw new Error(`${part} must be used inside a <Panelside.SearchSheet>.`);
|
|
2222
|
-
return value;
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
export interface PanelsideSearchSheetProps {
|
|
2226
|
-
className?: string;
|
|
2227
|
-
/** Controlled. Omit it and the sheet follows the root's `searchOpen`. */
|
|
2228
|
-
open?: boolean;
|
|
2229
|
-
onOpenChange?: (open: boolean) => void;
|
|
2230
|
-
/** The query. Controlled; pair it with `onValueChange`. */
|
|
2231
|
-
value?: string;
|
|
2232
|
-
defaultValue?: string;
|
|
2233
|
-
onValueChange?: (value: string) => void;
|
|
2234
|
-
/** Which tab is selected — the `value` of a `Panelside.SearchTab`. */
|
|
2235
|
-
tab?: string;
|
|
2236
|
-
/**
|
|
2237
|
-
* Which tab starts selected, when the sheet is not controlling `tab`. Set it
|
|
2238
|
-
* to the first tab's `value`: the expanding row shows the selected tab open
|
|
2239
|
-
* and the rest as their icons, so with nothing selected every tab is closed
|
|
2240
|
-
* and the row is a line of unlabelled glyphs.
|
|
2241
|
-
*/
|
|
2242
|
-
defaultTab?: string;
|
|
2243
|
-
onTabChange?: (tab: string) => void;
|
|
2244
|
-
/**
|
|
2245
|
-
* Present the platform's own sheet. Default true, because this one is the
|
|
2246
|
-
* whole screen and the system's presentation, detents and dismiss gesture
|
|
2247
|
-
* are the ones people already know. Requires the optional `@expo/ui`
|
|
2248
|
-
* package; without it the styled sheet renders instead.
|
|
2249
|
-
*/
|
|
2250
|
-
native?: boolean;
|
|
2251
|
-
/** Heights the sheet can rest at. Defaults to the tall one. */
|
|
2252
|
-
snapPoints?: BottomSheetProps['snapPoints'];
|
|
2253
|
-
/** Gap between the field and the top of the keyboard. */
|
|
2254
|
-
keyboardGap?: number;
|
|
2255
|
-
/**
|
|
2256
|
-
* Draw the round dismiss button at the leading edge of the top row. On by
|
|
2257
|
-
* default — it is the way out of a surface that covers the screen, and it
|
|
2258
|
-
* belongs where a thumb reaching across arrives rather than in the corner
|
|
2259
|
-
* furthest from one.
|
|
2260
|
-
*/
|
|
2261
|
-
showClose?: boolean;
|
|
2262
|
-
/** What a screen reader announces for that button. */
|
|
2263
|
-
closeLabel?: string;
|
|
2264
|
-
/**
|
|
2265
|
-
* How that button is drawn. Matches `Panelside.SearchTrigger`'s `variant`,
|
|
2266
|
-
* so the control that opens the surface and the one that closes it are the
|
|
2267
|
-
* same shape.
|
|
2268
|
-
*/
|
|
2269
|
-
closeVariant?: PanelsideControlVariant;
|
|
2270
|
-
/**
|
|
2271
|
-
* Draw that button as the platform's own, the way `Panelside.SearchTrigger`
|
|
2272
|
-
* does. Independent of `native` on the sheet itself: the surface can be ours
|
|
2273
|
-
* while the two controls on either end of it are the platform's, which is
|
|
2274
|
-
* the arrangement a native screen wants when the sheet's own presentation
|
|
2275
|
-
* cannot hold this content.
|
|
2276
|
-
*/
|
|
2277
|
-
closeNative?: boolean;
|
|
2278
|
-
/** Draw that native button in the platform's Liquid Glass material. */
|
|
2279
|
-
closeGlass?: boolean;
|
|
2280
|
-
children?: ReactNode;
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
/**
|
|
2284
|
-
* The search surface: tabs, results, and a field at the bottom.
|
|
2285
|
-
*
|
|
2286
|
-
* ```tsx
|
|
2287
|
-
* <Panelside.SearchSheet value={query} onValueChange={setQuery} tab={tab} onTabChange={setTab}>
|
|
2288
|
-
* <Panelside.SearchTabs>
|
|
2289
|
-
* <Panelside.SearchTab value="all" icon={<SparklesIcon size={16} />}>All</Panelside.SearchTab>
|
|
2290
|
-
* <Panelside.SearchTab value="chats" icon={<MessageCircleIcon size={16} />}>Chats</Panelside.SearchTab>
|
|
2291
|
-
* </Panelside.SearchTabs>
|
|
2292
|
-
*
|
|
2293
|
-
* <Panelside.SearchResults>
|
|
2294
|
-
* {hits.map((hit) => (
|
|
2295
|
-
* <Panelside.SearchResult key={hit.id} title={hit.title} description={hit.kind} />
|
|
2296
|
-
* ))}
|
|
2297
|
-
* </Panelside.SearchResults>
|
|
2298
|
-
*
|
|
2299
|
-
* <Panelside.SearchField placeholder="Search chats" />
|
|
2300
|
-
* </Panelside.SearchSheet>
|
|
2301
|
-
* ```
|
|
2302
|
-
*
|
|
2303
|
-
* Put it under `<Panelside>` and outside `Panelside.Panel` — a sheet is
|
|
2304
|
-
* presented over the whole app, and the panel is a layer that slides.
|
|
2305
|
-
*
|
|
2306
|
-
* It reports what was typed and which tab is selected. What counts as a match,
|
|
2307
|
-
* and what a result is, are yours: a search that only read chat titles would
|
|
2308
|
-
* be wrong for the first app that indexes message bodies.
|
|
2309
|
-
*/
|
|
2310
|
-
function PanelsideSearchSheet({
|
|
2311
|
-
className,
|
|
2312
|
-
open: openProp,
|
|
2313
|
-
onOpenChange,
|
|
2314
|
-
value,
|
|
2315
|
-
defaultValue = '',
|
|
2316
|
-
onValueChange,
|
|
2317
|
-
tab,
|
|
2318
|
-
defaultTab = '',
|
|
2319
|
-
onTabChange,
|
|
2320
|
-
native = true,
|
|
2321
|
-
snapPoints,
|
|
2322
|
-
keyboardGap = 10,
|
|
2323
|
-
showClose = true,
|
|
2324
|
-
closeLabel = 'Close search',
|
|
2325
|
-
closeVariant = 'filled',
|
|
2326
|
-
closeNative = false,
|
|
2327
|
-
closeGlass = false,
|
|
2328
|
-
children,
|
|
2329
|
-
}: PanelsideSearchSheetProps) {
|
|
2330
|
-
const { searchOpen, setSearchOpen } = usePanelsideContext('Panelside.SearchSheet');
|
|
2331
|
-
const insets = useSafeAreaInsets();
|
|
2332
|
-
const { height: screenHeight } = useWindowDimensions();
|
|
2333
|
-
const tint = useCSSVariable('--color-foreground');
|
|
2334
|
-
const glyph = typeof tint === 'string' ? tint : undefined;
|
|
2335
|
-
|
|
2336
|
-
const [uncontrolledQuery, setUncontrolledQuery] = useState(defaultValue);
|
|
2337
|
-
const [uncontrolledTab, setUncontrolledTab] = useState(defaultTab);
|
|
2338
|
-
|
|
2339
|
-
const open = openProp ?? searchOpen;
|
|
2340
|
-
const query = value ?? uncontrolledQuery;
|
|
2341
|
-
const activeTab = tab ?? uncontrolledTab;
|
|
2342
|
-
|
|
2343
|
-
/*
|
|
2344
|
-
* Closing takes the keyboard with it, wherever the close came from — the
|
|
2345
|
-
* button, a drag on the sheet, or the system back gesture.
|
|
2346
|
-
*
|
|
2347
|
-
* Leaving it to the field's own blur is too late. The field blurs when
|
|
2348
|
-
* `open` goes false, but the sheet is already on its way out by then: the
|
|
2349
|
-
* styled one unmounts its portal after the exit animation, and the platform
|
|
2350
|
-
* one keeps its content mounted and only stops presenting it. Either way the
|
|
2351
|
-
* blur lands on a field nothing can see, and the keyboard stays up until the
|
|
2352
|
-
* system notices for itself, some seconds later, that nothing is focused.
|
|
2353
|
-
*/
|
|
2354
|
-
const setOpen = useCallback(
|
|
2355
|
-
(next: boolean) => {
|
|
2356
|
-
if (!next) Keyboard.dismiss();
|
|
2357
|
-
if (openProp === undefined) setSearchOpen(next);
|
|
2358
|
-
onOpenChange?.(next);
|
|
2359
|
-
},
|
|
2360
|
-
[onOpenChange, openProp, setSearchOpen]
|
|
2361
|
-
);
|
|
2362
|
-
|
|
2363
|
-
const setQuery = useCallback(
|
|
2364
|
-
(next: string) => {
|
|
2365
|
-
if (value === undefined) setUncontrolledQuery(next);
|
|
2366
|
-
onValueChange?.(next);
|
|
2367
|
-
},
|
|
2368
|
-
[onValueChange, value]
|
|
2369
|
-
);
|
|
2370
|
-
|
|
2371
|
-
const setTab = useCallback(
|
|
2372
|
-
(next: string) => {
|
|
2373
|
-
if (tab === undefined) setUncontrolledTab(next);
|
|
2374
|
-
onTabChange?.(next);
|
|
2375
|
-
},
|
|
2376
|
-
[onTabChange, tab]
|
|
2377
|
-
);
|
|
2378
|
-
|
|
2379
|
-
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
2380
|
-
|
|
2381
|
-
const detents = snapPoints ?? (['full'] as const satisfies BottomSheetProps['snapPoints']);
|
|
2382
|
-
|
|
2383
|
-
/*
|
|
2384
|
-
* What `BottomSheet.Content` pads the bottom of the sheet by. The field
|
|
2385
|
-
* already sits that far above the screen edge, and docking travels by the
|
|
2386
|
-
* keyboard's height less whatever the element has already cleared — so
|
|
2387
|
-
* getting this wrong is a gap under the field, or the field over the
|
|
2388
|
-
* keyboard's top row.
|
|
2389
|
-
*/
|
|
2390
|
-
const bottomInset = Math.max(insets.bottom, SHEET_BOTTOM_PADDING) - keyboardGap;
|
|
2391
|
-
|
|
2392
|
-
/*
|
|
2393
|
-
* A definite height for the column, not `flex-1` against the sheet's own.
|
|
2394
|
-
*
|
|
2395
|
-
* The platform sheet gives its hosted content a *minimum* height, and a
|
|
2396
|
-
* minimum is not something `flex-1` can divide: the results list sizes to
|
|
2397
|
-
* its own rows instead, grows past the sheet, and pushes the field off the
|
|
2398
|
-
* bottom — which is a search surface with no way to type in it. So the
|
|
2399
|
-
* column is told exactly how tall it is, and the list gets the room left
|
|
2400
|
-
* between the tabs and the field.
|
|
2401
|
-
*
|
|
2402
|
-
* Only under the platform sheet. The styled one is laid out by us and has a
|
|
2403
|
-
* real height already, so `flex-1` resolves there and a second opinion about
|
|
2404
|
-
* how tall the sheet is would only be a chance to disagree with it.
|
|
2405
|
-
*/
|
|
2406
|
-
const hosted = native && getNativeUI() !== null;
|
|
2407
|
-
const columnHeight = hosted
|
|
2408
|
-
? (bottomSheetDetentHeight(detents as BottomSheetProps['snapPoints'], screenHeight) ??
|
|
2409
|
-
screenHeight * 0.9) -
|
|
2410
|
-
SHEET_TOP_PADDING -
|
|
2411
|
-
Math.max(insets.bottom, SHEET_BOTTOM_PADDING)
|
|
2412
|
-
: undefined;
|
|
2413
|
-
|
|
2414
|
-
const context = useMemo<PanelsideSearchSheetContextValue>(
|
|
2415
|
-
() => ({ open, query, setQuery, tab: activeTab, setTab, close, bottomInset }),
|
|
2416
|
-
[activeTab, bottomInset, close, open, query, setQuery, setTab]
|
|
2417
|
-
);
|
|
2418
|
-
|
|
2419
|
-
return (
|
|
2420
|
-
<BottomSheet
|
|
2421
|
-
native={native}
|
|
2422
|
-
open={open}
|
|
2423
|
-
onOpenChange={setOpen}
|
|
2424
|
-
snapPoints={detents as BottomSheetProps['snapPoints']}
|
|
2425
|
-
>
|
|
2426
|
-
{/*
|
|
2427
|
-
`showClose` off on the sheet itself: this surface draws its own, at the
|
|
2428
|
-
leading edge of the top row, where the reference for this pattern puts
|
|
2429
|
-
it and where a thumb reaching across the screen arrives.
|
|
2430
|
-
*/}
|
|
2431
|
-
<BottomSheet.Content size="full" showClose={false} className="gap-0 px-0 pt-3">
|
|
2432
|
-
{/*
|
|
2433
|
-
The provider is *inside* Content, not around the sheet.
|
|
2434
|
-
|
|
2435
|
-
The styled sheet mounts its content through a portal, under the
|
|
2436
|
-
portal host and outside this component's subtree — so a provider
|
|
2437
|
-
wrapped around the sheet is a provider the children never see, and
|
|
2438
|
-
every part below throws about not being inside a `SearchSheet`. Only
|
|
2439
|
-
the native path happened to work, because the platform hosts the
|
|
2440
|
-
content in place.
|
|
2441
|
-
*/}
|
|
2442
|
-
<PanelsideSearchSheetContext.Provider value={context}>
|
|
2443
|
-
<View
|
|
2444
|
-
className={cn('gap-2', columnHeight === undefined && 'flex-1', className)}
|
|
2445
|
-
style={columnHeight === undefined ? undefined : { height: columnHeight }}
|
|
2446
|
-
>
|
|
2447
|
-
{showClose ? (
|
|
2448
|
-
<View className="flex-row px-4 pb-1">
|
|
2449
|
-
{closeNative ? (
|
|
2450
|
-
<Button
|
|
2451
|
-
native
|
|
2452
|
-
glass={closeGlass}
|
|
2453
|
-
size="icon"
|
|
2454
|
-
variant="ghost"
|
|
2455
|
-
accessibilityLabel={closeLabel}
|
|
2456
|
-
onPress={close}
|
|
2457
|
-
>
|
|
2458
|
-
<Glyph icon={Cancel01Icon} size={17} color={glyph} />
|
|
2459
|
-
</Button>
|
|
2460
|
-
) : (
|
|
2461
|
-
<AnimatedPressable
|
|
2462
|
-
onPress={close}
|
|
2463
|
-
accessibilityRole="button"
|
|
2464
|
-
accessibilityLabel={closeLabel}
|
|
2465
|
-
className={cn(
|
|
2466
|
-
'h-9 w-9 items-center justify-center rounded-full',
|
|
2467
|
-
closeVariant === 'outline' ? 'border border-border' : 'bg-secondary'
|
|
2468
|
-
)}
|
|
2469
|
-
>
|
|
2470
|
-
<Glyph icon={Cancel01Icon} size={17} color={glyph} />
|
|
2471
|
-
</AnimatedPressable>
|
|
2472
|
-
)}
|
|
2473
|
-
</View>
|
|
2474
|
-
) : null}
|
|
2475
|
-
{children}
|
|
2476
|
-
</View>
|
|
2477
|
-
</PanelsideSearchSheetContext.Provider>
|
|
2478
|
-
</BottomSheet.Content>
|
|
2479
|
-
</BottomSheet>
|
|
2480
|
-
);
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
export interface PanelsideSearchTabsProps {
|
|
2484
|
-
className?: string;
|
|
2485
|
-
/**
|
|
2486
|
-
* Where the row sits across the sheet. Default `center`.
|
|
2487
|
-
*
|
|
2488
|
-
* Centred because only the selected tab is open and the rest are their
|
|
2489
|
-
* icons, so the row's width changes as you move through it — anchored to the
|
|
2490
|
-
* leading edge that change reads as the row growing and shrinking, and
|
|
2491
|
-
* centred it reads as the selection moving.
|
|
2492
|
-
*
|
|
2493
|
-
* `start` anchors it to the leading edge, for a sheet whose tabs are wide
|
|
2494
|
-
* enough to fill the row anyway.
|
|
2495
|
-
*/
|
|
2496
|
-
align?: 'start' | 'center';
|
|
2497
|
-
/** Applied to the row itself rather than to the box around it. */
|
|
2498
|
-
listClassName?: string;
|
|
2499
|
-
children?: ReactNode;
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
/**
|
|
2503
|
-
* The row across the top of the search sheet, narrowing what is searched.
|
|
2504
|
-
*
|
|
2505
|
-
* The expanding variant: only the selected tab is open, and the rest are their
|
|
2506
|
-
* icons. A row of four full labels takes the whole width to say four words
|
|
2507
|
-
* nobody rereads, and this row has to leave the results the screen.
|
|
2508
|
-
*
|
|
2509
|
-
* Every `Panelside.SearchTab` therefore needs an `icon` — a closed tab has
|
|
2510
|
-
* nothing else to be.
|
|
2511
|
-
*/
|
|
2512
|
-
function PanelsideSearchTabs({
|
|
2513
|
-
className,
|
|
2514
|
-
align = 'center',
|
|
2515
|
-
listClassName,
|
|
2516
|
-
children,
|
|
2517
|
-
}: PanelsideSearchTabsProps) {
|
|
2518
|
-
const { tab, setTab } = usePanelsideSearchSheet('Panelside.SearchTabs');
|
|
2519
|
-
|
|
2520
|
-
return (
|
|
2521
|
-
<Tabs
|
|
2522
|
-
variant="expanding"
|
|
2523
|
-
value={tab}
|
|
2524
|
-
onValueChange={setTab}
|
|
2525
|
-
defaultValue={tab}
|
|
2526
|
-
className={cn('px-4 pt-1', className)}
|
|
2527
|
-
>
|
|
2528
|
-
<Tabs.List className={cn(align === 'center' && 'justify-center', listClassName)}>
|
|
2529
|
-
{children}
|
|
2530
|
-
</Tabs.List>
|
|
2531
|
-
</Tabs>
|
|
2532
|
-
);
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
export interface PanelsideSearchTabProps {
|
|
2536
|
-
className?: string;
|
|
2537
|
-
/** What selecting this tab reports as the sheet's `tab`. */
|
|
2538
|
-
value: string;
|
|
2539
|
-
/** Required: a closed tab is its icon and nothing else. */
|
|
2540
|
-
icon: ReactNode;
|
|
2541
|
-
children?: ReactNode;
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
function PanelsideSearchTab({ className, value, icon, children }: PanelsideSearchTabProps) {
|
|
2545
|
-
return (
|
|
2546
|
-
// A step of padding above the tab row's own. This row is the only control
|
|
2547
|
-
// on a surface that is otherwise a list and a field, and a closed tab here
|
|
2548
|
-
// is a glyph with nothing else to be aimed at by — so it is sized to be
|
|
2549
|
-
// hit rather than to the density a row of labelled tabs can afford.
|
|
2550
|
-
<Tabs.Trigger value={value} icon={icon} className={cn('px-4 py-3', className)}>
|
|
2551
|
-
{children}
|
|
2552
|
-
</Tabs.Trigger>
|
|
2553
|
-
);
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
export interface PanelsideSearchResultsProps extends BottomSheetBodyProps {
|
|
2557
|
-
className?: string;
|
|
2558
|
-
contentContainerClassName?: string;
|
|
2559
|
-
children?: ReactNode;
|
|
2560
|
-
}
|
|
2561
|
-
|
|
2562
|
-
/**
|
|
2563
|
-
* The scrolling middle of the search sheet.
|
|
2564
|
-
*
|
|
2565
|
-
* `BottomSheet.Body` rather than a `ScrollView`, so the list's scroll and the
|
|
2566
|
-
* sheet's dismiss drag agree on which of them a downward pull belongs to.
|
|
2567
|
-
*/
|
|
2568
|
-
function PanelsideSearchResults({
|
|
2569
|
-
className,
|
|
2570
|
-
contentContainerClassName,
|
|
2571
|
-
children,
|
|
2572
|
-
...props
|
|
2573
|
-
}: PanelsideSearchResultsProps) {
|
|
2574
|
-
usePanelsideSearchSheet('Panelside.SearchResults');
|
|
2575
|
-
|
|
2576
|
-
return (
|
|
2577
|
-
<BottomSheet.Body
|
|
2578
|
-
className={cn('flex-1', className)}
|
|
2579
|
-
contentContainerClassName={cn('gap-1 px-4 pb-4 pt-1', contentContainerClassName)}
|
|
2580
|
-
keyboardShouldPersistTaps="handled"
|
|
2581
|
-
keyboardDismissMode="interactive"
|
|
2582
|
-
{...props}
|
|
2583
|
-
>
|
|
2584
|
-
{textChildren(children)}
|
|
2585
|
-
</BottomSheet.Body>
|
|
2586
|
-
);
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
|
-
export interface PanelsideSearchResultProps extends Omit<PressableProps, 'children'> {
|
|
2590
|
-
className?: string;
|
|
2591
|
-
/** Leading element — a thumbnail, an icon, an avatar. */
|
|
2592
|
-
media?: ReactNode;
|
|
2593
|
-
/** The result's name, truncated to one line. */
|
|
2594
|
-
title?: string;
|
|
2595
|
-
/** What kind of thing it is, or where it was found. */
|
|
2596
|
-
description?: string;
|
|
2597
|
-
children?: ReactNode;
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
/**
|
|
2601
|
-
* One hit. A leading thumbnail, a title, and a line saying what it is.
|
|
2602
|
-
*
|
|
2603
|
-
* Taller than a `Panelside.Item`, and deliberately: a navigation row is a
|
|
2604
|
-
* place you already know the name of, and a result is a thing you are deciding
|
|
2605
|
-
* about — the second line is what the decision is made on.
|
|
2606
|
-
*/
|
|
2607
|
-
function PanelsideSearchResult({
|
|
2608
|
-
className,
|
|
2609
|
-
media,
|
|
2610
|
-
title,
|
|
2611
|
-
description,
|
|
2612
|
-
children,
|
|
2613
|
-
...props
|
|
2614
|
-
}: PanelsideSearchResultProps) {
|
|
2615
|
-
const tint = useCSSVariable('--color-muted-foreground');
|
|
2616
|
-
const color = typeof tint === 'string' ? tint : undefined;
|
|
2617
|
-
|
|
2618
|
-
return (
|
|
2619
|
-
<AnimatedPressable
|
|
2620
|
-
className={cn('flex-row items-center gap-3 rounded-xl px-2 py-2', className)}
|
|
2621
|
-
accessibilityRole="button"
|
|
2622
|
-
accessibilityLabel={title}
|
|
2623
|
-
pressScale={0.985}
|
|
2624
|
-
{...props}
|
|
2625
|
-
>
|
|
2626
|
-
{media ? (
|
|
2627
|
-
<View className="h-11 w-11 items-center justify-center overflow-hidden rounded-xl bg-secondary">
|
|
2628
|
-
<IconColorProvider color={color}>{media}</IconColorProvider>
|
|
2629
|
-
</View>
|
|
2630
|
-
) : null}
|
|
2631
|
-
{title !== undefined || description !== undefined ? (
|
|
2632
|
-
<View className="flex-1 gap-0.5">
|
|
2633
|
-
{title !== undefined ? (
|
|
2634
|
-
<Text size="base" weight="medium" numberOfLines={1}>
|
|
2635
|
-
{title}
|
|
2636
|
-
</Text>
|
|
2637
|
-
) : null}
|
|
2638
|
-
{description !== undefined ? (
|
|
2639
|
-
<Text size="sm" muted numberOfLines={1}>
|
|
2640
|
-
{description}
|
|
2641
|
-
</Text>
|
|
2642
|
-
) : null}
|
|
2643
|
-
</View>
|
|
2644
|
-
) : null}
|
|
2645
|
-
{children}
|
|
2646
|
-
</AnimatedPressable>
|
|
2647
|
-
);
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
|
-
export interface PanelsideSearchFieldProps extends TextInputProps {
|
|
2651
|
-
className?: string;
|
|
2652
|
-
containerClassName?: string;
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
/**
|
|
2656
|
-
* The field, at the bottom of the sheet, riding the keyboard.
|
|
2657
|
-
*
|
|
2658
|
-
* At the bottom because that is where the thumb is and where the keyboard
|
|
2659
|
-
* comes up: a field at the top of a full-height sheet is at the far end of the
|
|
2660
|
-
* screen from both, and every character typed into it is read at the other
|
|
2661
|
-
* end of a list that is moving.
|
|
2662
|
-
*
|
|
2663
|
-
* The way out of the surface is not here — `Panelside.SearchSheet` draws it at
|
|
2664
|
-
* the leading edge of the top row, so the dismiss control does not move with
|
|
2665
|
-
* the keyboard and is not one mis-tap away from the field.
|
|
2666
|
-
*/
|
|
2667
|
-
function PanelsideSearchField({
|
|
2668
|
-
className,
|
|
2669
|
-
containerClassName,
|
|
2670
|
-
placeholder = 'Search',
|
|
2671
|
-
value,
|
|
2672
|
-
onChangeText,
|
|
2673
|
-
...props
|
|
2674
|
-
}: PanelsideSearchFieldProps) {
|
|
2675
|
-
const { open, query, setQuery, bottomInset } = usePanelsideSearchSheet(
|
|
2676
|
-
'Panelside.SearchField'
|
|
2677
|
-
);
|
|
2678
|
-
const placeholderTint = useCSSVariable('--color-muted-foreground');
|
|
2679
|
-
const textTint = useCSSVariable('--color-foreground');
|
|
2680
|
-
const muted = typeof placeholderTint === 'string' ? placeholderTint : undefined;
|
|
2681
|
-
const field = useRef<TextInput>(null);
|
|
2682
|
-
|
|
2683
|
-
const text = value ?? query;
|
|
2684
|
-
const change = onChangeText ?? setQuery;
|
|
2685
|
-
|
|
2686
|
-
/*
|
|
2687
|
-
* Focus on the transition rather than with `autoFocus`. Under `native` the
|
|
2688
|
-
* sheet's content is mounted for the life of the screen and only
|
|
2689
|
-
* `isPresented` changes, so `autoFocus` fires once — at startup, on a sheet
|
|
2690
|
-
* that is not up — and the keyboard opens over whatever is.
|
|
2691
|
-
*/
|
|
2692
|
-
useEffect(() => {
|
|
2693
|
-
if (open) field.current?.focus();
|
|
2694
|
-
else field.current?.blur();
|
|
2695
|
-
}, [open]);
|
|
2696
|
-
|
|
2697
|
-
return (
|
|
2698
|
-
<KeyboardAvoider
|
|
2699
|
-
mode="dock"
|
|
2700
|
-
active={open}
|
|
2701
|
-
bottomInset={bottomInset}
|
|
2702
|
-
className="w-full px-4 pb-1"
|
|
2703
|
-
>
|
|
2704
|
-
{/*
|
|
2705
|
-
An opaque surface with a ring, not the secondary tint the rest of the
|
|
2706
|
-
panel's controls use.
|
|
2707
|
-
|
|
2708
|
-
The field rides the keyboard by translating rather than by taking a row
|
|
2709
|
-
of the layout, so once it is up the results are directly behind it —
|
|
2710
|
-
and the secondary token is a few percent of a colour, which the list
|
|
2711
|
-
then reads straight through, taking the placeholder with it. The ring
|
|
2712
|
-
is what a floating pill needs to have an edge once its fill is the same
|
|
2713
|
-
colour as most of the sheet.
|
|
2714
|
-
*/}
|
|
2715
|
-
<View
|
|
2716
|
-
className={cn(
|
|
2717
|
-
'h-12 w-full flex-row items-center gap-2 rounded-full border border-border bg-surface px-4 shadow-lg',
|
|
2718
|
-
containerClassName
|
|
2719
|
-
)}
|
|
2720
|
-
>
|
|
2721
|
-
<Glyph icon={Search01Icon} size={17} color={muted} />
|
|
2722
|
-
<TextInput
|
|
2723
|
-
ref={field}
|
|
2724
|
-
value={text}
|
|
2725
|
-
onChangeText={change}
|
|
2726
|
-
placeholder={placeholder}
|
|
2727
|
-
placeholderTextColor={muted}
|
|
2728
|
-
/* `text-[16px]` rather than a `text-*` step — see Panelside.Search. */
|
|
2729
|
-
className={cn('h-full flex-1 font-normal text-[16px] text-foreground', className)}
|
|
2730
|
-
style={typeof textTint === 'string' ? { color: textTint } : undefined}
|
|
2731
|
-
accessibilityRole="search"
|
|
2732
|
-
returnKeyType="search"
|
|
2733
|
-
{...props}
|
|
2734
|
-
/>
|
|
2735
|
-
{text.length > 0 ? (
|
|
2736
|
-
<AnimatedPressable
|
|
2737
|
-
onPress={() => change('')}
|
|
2738
|
-
hitSlop={8}
|
|
2739
|
-
accessibilityRole="button"
|
|
2740
|
-
accessibilityLabel="Clear search"
|
|
2741
|
-
className="h-5 w-5 items-center justify-center rounded-full bg-muted"
|
|
2742
|
-
>
|
|
2743
|
-
<Glyph icon={Cancel01Icon} size={12} color={muted} />
|
|
2744
|
-
</AnimatedPressable>
|
|
2745
|
-
) : null}
|
|
2746
|
-
</View>
|
|
2747
|
-
|
|
2748
|
-
</KeyboardAvoider>
|
|
2749
|
-
);
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2752
2176
|
PanelsidePanel.displayName = 'Panelside.Panel';
|
|
2753
2177
|
PanelsideHeader.displayName = 'Panelside.Header';
|
|
2754
2178
|
PanelsideSearch.displayName = 'Panelside.Search';
|
|
@@ -2768,12 +2192,6 @@ PanelsidePages.displayName = 'Panelside.Pages';
|
|
|
2768
2192
|
PanelsidePage.displayName = 'Panelside.Page';
|
|
2769
2193
|
PanelsideTrigger.displayName = 'Panelside.Trigger';
|
|
2770
2194
|
PanelsideSearchTrigger.displayName = 'Panelside.SearchTrigger';
|
|
2771
|
-
PanelsideSearchSheet.displayName = 'Panelside.SearchSheet';
|
|
2772
|
-
PanelsideSearchTabs.displayName = 'Panelside.SearchTabs';
|
|
2773
|
-
PanelsideSearchTab.displayName = 'Panelside.SearchTab';
|
|
2774
|
-
PanelsideSearchResults.displayName = 'Panelside.SearchResults';
|
|
2775
|
-
PanelsideSearchResult.displayName = 'Panelside.SearchResult';
|
|
2776
|
-
PanelsideSearchField.displayName = 'Panelside.SearchField';
|
|
2777
2195
|
|
|
2778
2196
|
export const Panelside = Object.assign(PanelsideRoot, {
|
|
2779
2197
|
Panel: PanelsidePanel,
|
|
@@ -2795,10 +2213,4 @@ export const Panelside = Object.assign(PanelsideRoot, {
|
|
|
2795
2213
|
Page: PanelsidePage,
|
|
2796
2214
|
Trigger: PanelsideTrigger,
|
|
2797
2215
|
SearchTrigger: PanelsideSearchTrigger,
|
|
2798
|
-
SearchSheet: PanelsideSearchSheet,
|
|
2799
|
-
SearchTabs: PanelsideSearchTabs,
|
|
2800
|
-
SearchTab: PanelsideSearchTab,
|
|
2801
|
-
SearchResults: PanelsideSearchResults,
|
|
2802
|
-
SearchResult: PanelsideSearchResult,
|
|
2803
|
-
SearchField: PanelsideSearchField,
|
|
2804
2216
|
});
|