panelui-native 0.83.0 → 0.84.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 (39) hide show
  1. package/lib/module/components/circular-text/circular-text-geometry.js +63 -0
  2. package/lib/module/components/circular-text/circular-text-geometry.js.map +1 -0
  3. package/lib/module/components/circular-text/index.js +146 -0
  4. package/lib/module/components/circular-text/index.js.map +1 -0
  5. package/lib/module/components/marquee/index.js +12 -5
  6. package/lib/module/components/marquee/index.js.map +1 -1
  7. package/lib/module/components/panelside/index.js +15 -417
  8. package/lib/module/components/panelside/index.js.map +1 -1
  9. package/lib/module/components/popover/index.js +119 -7
  10. package/lib/module/components/popover/index.js.map +1 -1
  11. package/lib/module/icons/index.js +24 -4
  12. package/lib/module/icons/index.js.map +1 -1
  13. package/lib/module/index.js +1 -0
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/module/native/index.js +28 -0
  16. package/lib/module/native/index.js.map +1 -1
  17. package/lib/typescript/src/components/circular-text/circular-text-geometry.d.ts +39 -0
  18. package/lib/typescript/src/components/circular-text/circular-text-geometry.d.ts.map +1 -0
  19. package/lib/typescript/src/components/circular-text/index.d.ts +69 -0
  20. package/lib/typescript/src/components/circular-text/index.d.ts.map +1 -0
  21. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
  22. package/lib/typescript/src/components/panelside/index.d.ts +10 -202
  23. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  24. package/lib/typescript/src/components/popover/index.d.ts +25 -1
  25. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  26. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  27. package/lib/typescript/src/index.d.ts +2 -1
  28. package/lib/typescript/src/index.d.ts.map +1 -1
  29. package/lib/typescript/src/native/index.d.ts +40 -0
  30. package/lib/typescript/src/native/index.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/src/components/circular-text/circular-text-geometry.ts +80 -0
  33. package/src/components/circular-text/index.tsx +217 -0
  34. package/src/components/marquee/index.tsx +12 -5
  35. package/src/components/panelside/index.tsx +16 -604
  36. package/src/components/popover/index.tsx +146 -4
  37. package/src/icons/index.tsx +23 -2
  38. package/src/index.ts +1 -6
  39. package/src/native/index.ts +55 -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 for the same reason `searchOpen` is: the rows that navigate
428
- * are in the panel and the pages are in the scene, which are different
429
- * subtrees. Anything else is one `useState` every app threads through both.
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, searchOpen, setSearchOpen, route, navigate } =
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, searchOpen, setSearchOpen, route, navigate: go };
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 button that opens the search surface. Goes in `Panelside.Header`'s
2130
- * `action` slot.
2111
+ * The search button. Goes in `Panelside.Header`'s `action` slot.
2131
2112
  *
2132
- * It toggles the root's `searchOpen`, which `Panelside.SearchSheet` reads —
2133
- * so the two need nothing wired between them.
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 = useCallback(
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
  });