@oxy.so/bloom 4.24.1 → 4.25.1
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/docs/adoption-matrix.mdx +6 -6
- package/docs/ai-chat.mdx +1 -1
- package/docs/context-menu.mdx +2 -0
- package/lib/commonjs/ai-chat/AiChatCodePanel.js +2 -2
- package/lib/commonjs/ai-chat/AiChatCodePanel.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChatContainer.js +60 -7
- package/lib/commonjs/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChatShell.js +3 -3
- package/lib/commonjs/ai-chat/AiChatShell.js.map +1 -1
- package/lib/commonjs/ai-chat/use-panel-interaction.js +6 -1
- package/lib/commonjs/ai-chat/use-panel-interaction.js.map +1 -1
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +6 -2
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/bottom-sheet/context.js +29 -0
- package/lib/commonjs/bottom-sheet/context.js.map +1 -0
- package/lib/commonjs/composer-panel/ComposerPanelBase.js +32 -11
- package/lib/commonjs/composer-panel/ComposerPanelBase.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerPanelStatusTab.js +2 -2
- package/lib/commonjs/composer-panel/ComposerPanelStatusTab.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerPillBase.js +3 -1
- package/lib/commonjs/composer-panel/ComposerPillBase.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerPopover.js +13 -5
- package/lib/commonjs/composer-panel/ComposerPopover.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerStatusBarBase.js +3 -3
- package/lib/commonjs/composer-panel/ComposerStatusBarBase.js.map +1 -1
- package/lib/commonjs/composer-panel/ModelPickerBase.js +16 -4
- package/lib/commonjs/composer-panel/ModelPickerBase.js.map +1 -1
- package/lib/commonjs/composer-panel/PermissionMenu.js +19 -3
- package/lib/commonjs/composer-panel/PermissionMenu.js.map +1 -1
- package/lib/commonjs/context-menu/ContextMenu.js +10 -2
- package/lib/commonjs/context-menu/ContextMenu.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +27 -6
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/floating/LongPressArea.js +107 -0
- package/lib/commonjs/floating/LongPressArea.js.map +1 -0
- package/lib/commonjs/settings-modal/SettingsModal.js +8 -1
- package/lib/commonjs/settings-modal/SettingsModal.js.map +1 -1
- package/lib/commonjs/settings-modal/use-keyboard-reveal.js +76 -0
- package/lib/commonjs/settings-modal/use-keyboard-reveal.js.map +1 -0
- package/lib/commonjs/sidebar/SidebarItem.js +5 -1
- package/lib/commonjs/sidebar/SidebarItem.js.map +1 -1
- package/lib/module/ai-chat/AiChatCodePanel.js +2 -2
- package/lib/module/ai-chat/AiChatCodePanel.js.map +1 -1
- package/lib/module/ai-chat/AiChatContainer.js +60 -7
- package/lib/module/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/module/ai-chat/AiChatShell.js +3 -3
- package/lib/module/ai-chat/AiChatShell.js.map +1 -1
- package/lib/module/ai-chat/use-panel-interaction.js +7 -2
- package/lib/module/ai-chat/use-panel-interaction.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +6 -2
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/bottom-sheet/context.js +25 -0
- package/lib/module/bottom-sheet/context.js.map +1 -0
- package/lib/module/composer-panel/ComposerPanelBase.js +33 -12
- package/lib/module/composer-panel/ComposerPanelBase.js.map +1 -1
- package/lib/module/composer-panel/ComposerPanelStatusTab.js +2 -2
- package/lib/module/composer-panel/ComposerPanelStatusTab.js.map +1 -1
- package/lib/module/composer-panel/ComposerPillBase.js +3 -1
- package/lib/module/composer-panel/ComposerPillBase.js.map +1 -1
- package/lib/module/composer-panel/ComposerPopover.js +13 -5
- package/lib/module/composer-panel/ComposerPopover.js.map +1 -1
- package/lib/module/composer-panel/ComposerStatusBarBase.js +3 -3
- package/lib/module/composer-panel/ComposerStatusBarBase.js.map +1 -1
- package/lib/module/composer-panel/ModelPickerBase.js +16 -4
- package/lib/module/composer-panel/ModelPickerBase.js.map +1 -1
- package/lib/module/composer-panel/PermissionMenu.js +19 -3
- package/lib/module/composer-panel/PermissionMenu.js.map +1 -1
- package/lib/module/context-menu/ContextMenu.js +10 -2
- package/lib/module/context-menu/ContextMenu.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +25 -4
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/floating/LongPressArea.js +100 -0
- package/lib/module/floating/LongPressArea.js.map +1 -0
- package/lib/module/settings-modal/SettingsModal.js +8 -1
- package/lib/module/settings-modal/SettingsModal.js.map +1 -1
- package/lib/module/settings-modal/use-keyboard-reveal.js +72 -0
- package/lib/module/settings-modal/use-keyboard-reveal.js.map +1 -0
- package/lib/module/sidebar/SidebarItem.js +5 -1
- package/lib/module/sidebar/SidebarItem.js.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatCodePanel.d.ts +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts +1 -1
- package/lib/typescript/commonjs/ai-chat/use-panel-interaction.d.ts +5 -1
- package/lib/typescript/commonjs/ai-chat/use-panel-interaction.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/context.d.ts +5 -0
- package/lib/typescript/commonjs/bottom-sheet/context.d.ts.map +1 -0
- package/lib/typescript/commonjs/composer-panel/ComposerPanelBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPanelStatusTab.d.ts +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPillBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPopover.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerStatusBarBase.d.ts +2 -2
- package/lib/typescript/commonjs/composer-panel/ModelPickerBase.d.ts +1 -1
- package/lib/typescript/commonjs/composer-panel/ModelPickerBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/PermissionMenu.d.ts +3 -1
- package/lib/typescript/commonjs/composer-panel/PermissionMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/context-menu/ContextMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/floating/LongPressArea.d.ts +34 -0
- package/lib/typescript/commonjs/floating/LongPressArea.d.ts.map +1 -0
- package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/settings-modal/use-keyboard-reveal.d.ts +21 -0
- package/lib/typescript/commonjs/settings-modal/use-keyboard-reveal.d.ts.map +1 -0
- package/lib/typescript/commonjs/sidebar/SidebarItem.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatCodePanel.d.ts +1 -1
- package/lib/typescript/module/ai-chat/AiChatShell.d.ts +1 -1
- package/lib/typescript/module/ai-chat/use-panel-interaction.d.ts +5 -1
- package/lib/typescript/module/ai-chat/use-panel-interaction.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/context.d.ts +5 -0
- package/lib/typescript/module/bottom-sheet/context.d.ts.map +1 -0
- package/lib/typescript/module/composer-panel/ComposerPanelBase.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ComposerPanelStatusTab.d.ts +1 -1
- package/lib/typescript/module/composer-panel/ComposerPillBase.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ComposerPopover.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ComposerStatusBarBase.d.ts +2 -2
- package/lib/typescript/module/composer-panel/ModelPickerBase.d.ts +1 -1
- package/lib/typescript/module/composer-panel/ModelPickerBase.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/PermissionMenu.d.ts +3 -1
- package/lib/typescript/module/composer-panel/PermissionMenu.d.ts.map +1 -1
- package/lib/typescript/module/context-menu/ContextMenu.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/module/floating/LongPressArea.d.ts +34 -0
- package/lib/typescript/module/floating/LongPressArea.d.ts.map +1 -0
- package/lib/typescript/module/settings-modal/SettingsModal.d.ts.map +1 -1
- package/lib/typescript/module/settings-modal/use-keyboard-reveal.d.ts +21 -0
- package/lib/typescript/module/settings-modal/use-keyboard-reveal.d.ts.map +1 -0
- package/lib/typescript/module/sidebar/SidebarItem.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ai-chat/AiChatCodePanel.tsx +2 -2
- package/src/ai-chat/AiChatContainer.tsx +44 -5
- package/src/ai-chat/AiChatShell.tsx +3 -3
- package/src/ai-chat/use-panel-interaction.ts +7 -2
- package/src/bottom-sheet/BottomSheetBase.tsx +4 -1
- package/src/bottom-sheet/context.ts +23 -0
- package/src/composer-panel/ComposerPanelBase.tsx +38 -13
- package/src/composer-panel/ComposerPanelStatusTab.tsx +2 -2
- package/src/composer-panel/ComposerPillBase.tsx +3 -1
- package/src/composer-panel/ComposerPopover.tsx +13 -5
- package/src/composer-panel/ComposerStatusBarBase.tsx +3 -3
- package/src/composer-panel/ModelPickerBase.tsx +14 -5
- package/src/composer-panel/PermissionMenu.tsx +17 -4
- package/src/context-menu/ContextMenu.tsx +7 -2
- package/src/dialog/DialogBottomSheet.tsx +16 -4
- package/src/floating/LongPressArea.tsx +114 -0
- package/src/settings-modal/SettingsModal.tsx +9 -2
- package/src/settings-modal/use-keyboard-reveal.ts +62 -0
- package/src/sidebar/SidebarItem.tsx +5 -1
|
@@ -537,6 +537,16 @@ const ScrollThread = forwardRef<AiChatThreadHandle, AiChatThreadProps>(function
|
|
|
537
537
|
const contentHeight = useRef(0);
|
|
538
538
|
const offset = useRef(0);
|
|
539
539
|
const viewport = useRef(0);
|
|
540
|
+
/**
|
|
541
|
+
* A scroll to the end is on its way and has not landed. `offset` is only
|
|
542
|
+
* what the LAST scroll event said, so two growths between a `scrollToEnd`
|
|
543
|
+
* and the event it causes measured the reader against the old offset, read
|
|
544
|
+
* them as far from the bottom and dropped the follow for good (Android,
|
|
545
|
+
* streaming). While this holds, growth follows whatever `offset` says; it
|
|
546
|
+
* clears when a scroll event lands at the end or moves UP — only a reader
|
|
547
|
+
* (or a host) scrolls up, the follow never does.
|
|
548
|
+
*/
|
|
549
|
+
const following = useRef(false);
|
|
540
550
|
/** An `onStartReached` is out and its page has not landed yet. */
|
|
541
551
|
const pageOut = useRef(false);
|
|
542
552
|
/** The reader has left the top zone, so the next approach may fire again. */
|
|
@@ -560,7 +570,10 @@ const ScrollThread = forwardRef<AiChatThreadHandle, AiChatThreadProps>(function
|
|
|
560
570
|
useImperativeHandle(
|
|
561
571
|
ref,
|
|
562
572
|
(): AiChatThreadHandle => ({
|
|
563
|
-
scrollToEnd: (options) =>
|
|
573
|
+
scrollToEnd: (options) => {
|
|
574
|
+
following.current = true;
|
|
575
|
+
scrollRef.current?.scrollToEnd({ animated: options?.animated ?? true });
|
|
576
|
+
},
|
|
564
577
|
scrollToOffset: ({ offset: y, animated = false }) => scrollRef.current?.scrollTo({ y, animated }),
|
|
565
578
|
getScrollView: () => scrollRef.current,
|
|
566
579
|
}),
|
|
@@ -577,9 +590,13 @@ const ScrollThread = forwardRef<AiChatThreadHandle, AiChatThreadProps>(function
|
|
|
577
590
|
const handleScroll = useCallback(
|
|
578
591
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
579
592
|
const { contentOffset, layoutMeasurement, contentSize } = event.nativeEvent;
|
|
593
|
+
const movedUp = contentOffset.y < offset.current - 0.5;
|
|
580
594
|
offset.current = contentOffset.y;
|
|
581
595
|
viewport.current = layoutMeasurement.height;
|
|
582
596
|
if (contentSize?.height) contentHeight.current = contentSize.height;
|
|
597
|
+
if (movedUp || contentHeight.current - contentOffset.y - layoutMeasurement.height <= EDGE_THRESHOLD) {
|
|
598
|
+
following.current = false;
|
|
599
|
+
}
|
|
583
600
|
report();
|
|
584
601
|
if (onStartReached) {
|
|
585
602
|
if (contentOffset.y <= onStartReachedThreshold) {
|
|
@@ -624,7 +641,8 @@ const ScrollThread = forwardRef<AiChatThreadHandle, AiChatThreadProps>(function
|
|
|
624
641
|
const chromeShift = shift.top + shift.bottom;
|
|
625
642
|
if (chromeShift !== 0 && Math.abs(height - previous - chromeShift) < 1) {
|
|
626
643
|
const fromBottom = previous - offset.current - viewport.current;
|
|
627
|
-
if (fromBottom <= EDGE_THRESHOLD) {
|
|
644
|
+
if (following.current || fromBottom <= EDGE_THRESHOLD) {
|
|
645
|
+
following.current = true;
|
|
628
646
|
scrollRef.current?.scrollToEnd({ animated: false });
|
|
629
647
|
} else if (offset.current > 0 && shift.top !== 0) {
|
|
630
648
|
scrollRef.current?.scrollTo({ y: offset.current + shift.top, animated: false });
|
|
@@ -642,15 +660,23 @@ const ScrollThread = forwardRef<AiChatThreadHandle, AiChatThreadProps>(function
|
|
|
642
660
|
pageOut.current = false;
|
|
643
661
|
|
|
644
662
|
if (!autoFollow) return;
|
|
645
|
-
if (followThreshold !== undefined) {
|
|
663
|
+
if (followThreshold !== undefined && !following.current) {
|
|
646
664
|
// Measured against the height BEFORE the growth: was the reader near
|
|
647
665
|
// the bottom when this arrived?
|
|
648
666
|
const fromBottom = previous - offset.current - viewport.current;
|
|
649
|
-
if (fromBottom > followThreshold)
|
|
667
|
+
if (fromBottom > followThreshold) {
|
|
668
|
+
// Staying put moves no scroll, so a host tracking the distance to
|
|
669
|
+
// the end from `onScroll` (a jump-to-latest button) would not hear
|
|
670
|
+
// that the end moved away. Tell it, as `DocumentThread` does on every
|
|
671
|
+
// growth.
|
|
672
|
+
onScroll?.(syntheticScroll(offset.current, viewport.current, height));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
650
675
|
}
|
|
676
|
+
following.current = true;
|
|
651
677
|
scrollRef.current?.scrollToEnd({ animated: followAnimated });
|
|
652
678
|
},
|
|
653
|
-
[autoFollow, followAnimated, followThreshold, maintainStartPosition],
|
|
679
|
+
[autoFollow, followAnimated, followThreshold, maintainStartPosition, onScroll],
|
|
654
680
|
);
|
|
655
681
|
|
|
656
682
|
return (
|
|
@@ -682,6 +708,19 @@ const ScrollThread = forwardRef<AiChatThreadHandle, AiChatThreadProps>(function
|
|
|
682
708
|
);
|
|
683
709
|
});
|
|
684
710
|
|
|
711
|
+
/** A scroll event for a position that did not change while the content did. */
|
|
712
|
+
function syntheticScroll(offset: number, viewport: number, content: number): NativeSyntheticEvent<NativeScrollEvent> {
|
|
713
|
+
return {
|
|
714
|
+
nativeEvent: {
|
|
715
|
+
contentOffset: { x: 0, y: offset },
|
|
716
|
+
layoutMeasurement: { width: 0, height: viewport },
|
|
717
|
+
contentSize: { width: 0, height: content },
|
|
718
|
+
contentInset: { top: 0, left: 0, bottom: 0, right: 0 },
|
|
719
|
+
zoomScale: 1,
|
|
720
|
+
},
|
|
721
|
+
} as NativeSyntheticEvent<NativeScrollEvent>;
|
|
722
|
+
}
|
|
723
|
+
|
|
685
724
|
/** The window's scroll position and extent, px. */
|
|
686
725
|
function readWindow() {
|
|
687
726
|
const page = document.scrollingElement ?? document.documentElement;
|
|
@@ -236,7 +236,7 @@ export function AiChatResizeHandle({
|
|
|
236
236
|
// ---------------------------------------------------------------------------
|
|
237
237
|
|
|
238
238
|
/**
|
|
239
|
-
* The in-container header below `xl`: 48 tall, px 12 / pt 11. The menu
|
|
239
|
+
* The in-container header below `xl`: at least 48 tall, px 12 / pt 11. The menu
|
|
240
240
|
* button (below `lg`, where the sidebar is a drawer) and the chat's name in
|
|
241
241
|
* headline-medium on the left; the button opening the panel drawer on the right.
|
|
242
242
|
* Both are 36px secondary icon buttons. Renders nothing outside an
|
|
@@ -251,7 +251,7 @@ export function AiChatMobileHeader({ title, style, testID }: AiChatMobileHeaderP
|
|
|
251
251
|
testID={testID}
|
|
252
252
|
style={[
|
|
253
253
|
{
|
|
254
|
-
|
|
254
|
+
minHeight: 48,
|
|
255
255
|
flexShrink: 0,
|
|
256
256
|
flexDirection: 'row',
|
|
257
257
|
alignItems: 'center',
|
|
@@ -738,7 +738,7 @@ export function AiChatShell({
|
|
|
738
738
|
]}>
|
|
739
739
|
<View
|
|
740
740
|
style={{
|
|
741
|
-
|
|
741
|
+
minHeight: 40,
|
|
742
742
|
flexShrink: 0,
|
|
743
743
|
flexDirection: 'row',
|
|
744
744
|
alignItems: 'center',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
|
-
import { BackHandler, Platform, type View } from 'react-native';
|
|
2
|
+
import { BackHandler, Keyboard, Platform, type View } from 'react-native';
|
|
3
3
|
import { useAccessibilityFocus } from '../hooks/use-accessibility-focus';
|
|
4
4
|
import { listenForEscape, tabbablesWithin, wrapTab } from '../overlay/modal-keyboard';
|
|
5
5
|
import { hasActiveOverlays } from '../overlay/stack';
|
|
@@ -10,7 +10,11 @@ import { hasActiveOverlays } from '../overlay/stack';
|
|
|
10
10
|
*
|
|
11
11
|
* open Escape (web) and back (Android) close it
|
|
12
12
|
* modal it covers the workspace: focus moves in on open, Tab is kept
|
|
13
|
-
* inside, and focus goes back to the opener on close
|
|
13
|
+
* inside, and focus goes back to the opener on close (web); on
|
|
14
|
+
* native the keyboard goes down as it opens — a composer that had
|
|
15
|
+
* the focus (say, regained after a sheet closed) otherwise kept the
|
|
16
|
+
* keyboard up under the drawer (Android 16). Nothing refocuses the
|
|
17
|
+
* composer on close.
|
|
14
18
|
*
|
|
15
19
|
* Any portaled overlay above (a dialog, a menu) takes all of it first.
|
|
16
20
|
*/
|
|
@@ -22,6 +26,7 @@ export function usePanelInteraction(open: boolean, modal: boolean, close: () =>
|
|
|
22
26
|
useEffect(() => {
|
|
23
27
|
if (!open) return;
|
|
24
28
|
if (Platform.OS !== 'web') {
|
|
29
|
+
if (modal) Keyboard?.dismiss?.();
|
|
25
30
|
const subscription = BackHandler?.addEventListener('hardwareBackPress', () => {
|
|
26
31
|
if (hasActiveOverlays()) return false;
|
|
27
32
|
closeRef.current();
|
|
@@ -34,6 +34,7 @@ import { EDGE_GAP, windowEdgeGap } from '../layout/edge';
|
|
|
34
34
|
import { Backdrop, BACKDROP_DIM_OPACITY } from '../overlay';
|
|
35
35
|
import { ModalKeyboard } from '../overlay/ModalKeyboard';
|
|
36
36
|
import { useTheme } from '../theme/use-theme';
|
|
37
|
+
import { SheetKeyboardProvider } from './context';
|
|
37
38
|
import type { BottomSheetRef, BottomSheetProps, BottomSheetShellProps, BottomSheetBaseProps } from './types';
|
|
38
39
|
|
|
39
40
|
/** Hook that returns current screen dimensions and updates on rotation/resize. */
|
|
@@ -659,7 +660,9 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
659
660
|
// (`layout/screen-scope.tsx`).
|
|
660
661
|
const surfaceChildren = (
|
|
661
662
|
<SurfaceLevelProvider level={0} fill={colors.background}>
|
|
662
|
-
<
|
|
663
|
+
<SheetKeyboardProvider value={keyboardHeight}>
|
|
664
|
+
<ScreenScope>{children}</ScreenScope>
|
|
665
|
+
</SheetKeyboardProvider>
|
|
663
666
|
</SurfaceLevelProvider>
|
|
664
667
|
);
|
|
665
668
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The open keyboard's height under a `BottomSheet`, for content that owns a
|
|
6
|
+
* bottom inset of its own. Native only: the sheet's shell writes it, web leaves
|
|
7
|
+
* it at 0 (the browser owns keyboard layout).
|
|
8
|
+
*
|
|
9
|
+
* `DialogBottomSheet` reads it to fold its safe-area spacer while the keyboard
|
|
10
|
+
* is up: the keyboard covers the gesture bar the spacer clears, so keeping it
|
|
11
|
+
* floated the sheet's buttons a bar's height above the keyboard (Android 16).
|
|
12
|
+
*
|
|
13
|
+
* Internal: not in the family barrel.
|
|
14
|
+
*/
|
|
15
|
+
const SheetKeyboardContext = createContext<SharedValue<number> | null>(null);
|
|
16
|
+
|
|
17
|
+
export const SheetKeyboardProvider = SheetKeyboardContext.Provider;
|
|
18
|
+
|
|
19
|
+
/** The sheet's keyboard height, or a constant 0 outside a sheet. */
|
|
20
|
+
export function useSheetKeyboardHeight(): SharedValue<number> {
|
|
21
|
+
const none = useSharedValue(0);
|
|
22
|
+
return useContext(SheetKeyboardContext) ?? none;
|
|
23
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
Text as RNText,
|
|
3
4
|
TextInput,
|
|
4
5
|
View,
|
|
5
6
|
type LayoutChangeEvent,
|
|
6
7
|
type NativeSyntheticEvent,
|
|
7
|
-
type TextInputContentSizeChangeEventData,
|
|
8
8
|
type TextInputKeyPressEventData,
|
|
9
9
|
} from 'react-native';
|
|
10
10
|
import Animated, {
|
|
@@ -177,7 +177,11 @@ export function ComposerPanelBase({
|
|
|
177
177
|
|
|
178
178
|
// The prompt grows with its text, one 20px line at a time, up to 200 before it
|
|
179
179
|
// scrolls. Web measures the textarea, collapsing it only when the draft may
|
|
180
|
-
// have got shorter (`useTextareaAutosize`)
|
|
180
|
+
// have got shorter (`useTextareaAutosize`). Native lays the same text out in
|
|
181
|
+
// an invisible twin and takes ITS height: the field's own
|
|
182
|
+
// `onContentSizeChange` does not fire for a value set from outside until the
|
|
183
|
+
// field is focused (Android 16), so a question loaded to edit showed one line
|
|
184
|
+
// and a cancelled edit kept four.
|
|
181
185
|
const [height, setHeight] = useState(PROMPT_LINE);
|
|
182
186
|
useTextareaAutosize(fieldRef, text, {
|
|
183
187
|
enabled: IS_WEB,
|
|
@@ -185,13 +189,9 @@ export function ComposerPanelBase({
|
|
|
185
189
|
minHeight: PROMPT_LINE,
|
|
186
190
|
onMeasure: (content) => setHeight(Math.min(PROMPT_MAX_HEIGHT, Math.max(PROMPT_LINE, content))),
|
|
187
191
|
});
|
|
188
|
-
const
|
|
189
|
-
(event
|
|
190
|
-
|
|
191
|
-
setHeight(Math.min(PROMPT_MAX_HEIGHT, Math.max(PROMPT_LINE, event.nativeEvent.contentSize.height)));
|
|
192
|
-
},
|
|
193
|
-
[],
|
|
194
|
-
);
|
|
192
|
+
const onTwinLayout = useCallback((event: LayoutChangeEvent) => {
|
|
193
|
+
setHeight(Math.min(PROMPT_MAX_HEIGHT, Math.max(PROMPT_LINE, event.nativeEvent.layout.height)));
|
|
194
|
+
}, []);
|
|
195
195
|
|
|
196
196
|
const submit = useCallback(() => {
|
|
197
197
|
if (disabled || busy) return;
|
|
@@ -253,7 +253,6 @@ export function ComposerPanelBase({
|
|
|
253
253
|
value={text}
|
|
254
254
|
onChangeText={setText}
|
|
255
255
|
onKeyPress={onKeyPress}
|
|
256
|
-
onContentSizeChange={onContentSizeChange}
|
|
257
256
|
placeholder={placeholder}
|
|
258
257
|
placeholderTextColor={palette.textTertiary}
|
|
259
258
|
selectionColor={palette.accent500}
|
|
@@ -273,10 +272,36 @@ export function ComposerPanelBase({
|
|
|
273
272
|
...(IS_WEB ? { caretColor: palette.accent500 } : null),
|
|
274
273
|
}}
|
|
275
274
|
/>
|
|
275
|
+
{IS_WEB ? null : (
|
|
276
|
+
<RNText
|
|
277
|
+
aria-hidden
|
|
278
|
+
accessibilityElementsHidden
|
|
279
|
+
importantForAccessibility="no-hide-descendants"
|
|
280
|
+
pointerEvents="none"
|
|
281
|
+
testID={testID ? `${testID}-input-twin` : undefined}
|
|
282
|
+
onLayout={onTwinLayout}
|
|
283
|
+
style={{
|
|
284
|
+
position: 'absolute',
|
|
285
|
+
top: 0,
|
|
286
|
+
left: 6,
|
|
287
|
+
right: 6,
|
|
288
|
+
opacity: 0,
|
|
289
|
+
...TYPE_SCALE['body-regular'],
|
|
290
|
+
fontFamily: 'Inter',
|
|
291
|
+
}}>
|
|
292
|
+
{/* A trailing newline is a line of its own in the field; a
|
|
293
|
+
Text drops it unless something follows. */}
|
|
294
|
+
{text.endsWith('\n') || text === '' ? `${text} ` : text}
|
|
295
|
+
</RNText>
|
|
296
|
+
)}
|
|
276
297
|
</View>
|
|
277
298
|
|
|
278
299
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
279
|
-
|
|
300
|
+
{/* The actions never give way: + and the permission chip on the
|
|
301
|
+
left, voice and send on the right keep their size, and the
|
|
302
|
+
model chip between them is what truncates (a long model name
|
|
303
|
+
once covered + and pushed voice off the card). */}
|
|
304
|
+
<View style={{ flexShrink: 0, flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
280
305
|
{addMenu.length > 0 ? (
|
|
281
306
|
<AddMenu
|
|
282
307
|
palette={palette}
|
|
@@ -299,7 +324,7 @@ export function ComposerPanelBase({
|
|
|
299
324
|
/>
|
|
300
325
|
) : null}
|
|
301
326
|
</View>
|
|
302
|
-
<View style={{
|
|
327
|
+
<View style={{ minWidth: 0, flexShrink: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 16 }}>
|
|
303
328
|
{providers && providers.length > 0 ? (
|
|
304
329
|
<ModelPickerBase
|
|
305
330
|
providers={providers}
|
|
@@ -314,7 +339,7 @@ export function ComposerPanelBase({
|
|
|
314
339
|
testID={testID ? `${testID}-model` : undefined}
|
|
315
340
|
/>
|
|
316
341
|
) : null}
|
|
317
|
-
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
342
|
+
<View style={{ flexShrink: 0, flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
318
343
|
<MicButton
|
|
319
344
|
listening={isListening}
|
|
320
345
|
onToggle={() => setListening(!isListening)}
|
|
@@ -44,7 +44,7 @@ function StatusItem({ icon, label, palette }: { icon: React.ReactNode; label: st
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* The status tab: the grey tab hanging off the Composer Panel's
|
|
47
|
-
* top edge — 34 tall, inset 28 each side, radius 16 on the top corners only,
|
|
47
|
+
* top edge — at least 34 tall, inset 28 each side, radius 16 on the top corners only,
|
|
48
48
|
* px 8 / py 4. Branch (mirrored merge glyph) and project folder on the left, 12
|
|
49
49
|
* apart; the context meter on the right (py 4, pl 6 / pr 8, gap 4).
|
|
50
50
|
*/
|
|
@@ -58,7 +58,7 @@ function ComposerPanelStatusTabComponent({ branch, project, context, style, test
|
|
|
58
58
|
{
|
|
59
59
|
marginLeft: TAB_INSET,
|
|
60
60
|
marginRight: TAB_INSET,
|
|
61
|
-
|
|
61
|
+
minHeight: TAB_HEIGHT,
|
|
62
62
|
flexDirection: 'row',
|
|
63
63
|
alignItems: 'center',
|
|
64
64
|
justifyContent: 'space-between',
|
|
@@ -251,7 +251,9 @@ function ModelMenu({
|
|
|
251
251
|
const modelName = models.find((entry) => entry.id === modelId)?.name ?? modelId;
|
|
252
252
|
const hasEffort = levels.length > 0;
|
|
253
253
|
const triggerStyle: WebCssStyle = {
|
|
254
|
-
|
|
254
|
+
// At least 32, never exactly: a fixed height clips the name at the largest
|
|
255
|
+
// system font.
|
|
256
|
+
minHeight: 32,
|
|
255
257
|
flexShrink: 0,
|
|
256
258
|
flexDirection: 'row',
|
|
257
259
|
alignItems: 'center',
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* sheet is not something `BottomSheet` stacks. So each panel is a transparent
|
|
8
8
|
* `Modal` holding a dismiss layer and the panel, placed against the trigger's
|
|
9
9
|
* `measureInWindow` box: on the requested side when it fits, flipped when it
|
|
10
|
-
* does not, clamped 8px inside the window.
|
|
10
|
+
* does not, clamped 8px inside the window's SAFE area. The Modal draws edge to
|
|
11
|
+
* edge, so a clamp to the bare window put the effort panel under Android's
|
|
12
|
+
* gesture bar (Pixel 8a, Android 16). Nested `Modal`s stack on both
|
|
11
13
|
* platforms, so the effort panel opens above the picker.
|
|
12
14
|
*
|
|
13
15
|
* Motion is the popover entry without the blur (no filter on native):
|
|
@@ -30,6 +32,7 @@ import Animated, {
|
|
|
30
32
|
useSharedValue,
|
|
31
33
|
withTiming,
|
|
32
34
|
} from 'react-native-reanimated';
|
|
35
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
33
36
|
|
|
34
37
|
import { TRANSITION_MS } from './shared';
|
|
35
38
|
import type { ComposerPopoverProps } from './types';
|
|
@@ -56,6 +59,7 @@ export function ComposerPopover({
|
|
|
56
59
|
children,
|
|
57
60
|
}: ComposerPopoverProps) {
|
|
58
61
|
const window = useWindowDimensions();
|
|
62
|
+
const insets = useSafeAreaInsets();
|
|
59
63
|
const reducedMotion = useReducedMotion();
|
|
60
64
|
const [mounted, setMounted] = useState(open);
|
|
61
65
|
const [anchor, setAnchor] = useState<Box | null>(null);
|
|
@@ -112,14 +116,18 @@ export function ComposerPopover({
|
|
|
112
116
|
let left = GUTTER;
|
|
113
117
|
let top = GUTTER;
|
|
114
118
|
if (anchor && size) {
|
|
115
|
-
|
|
119
|
+
const minLeft = insets.left + GUTTER;
|
|
120
|
+
const maxRight = window.width - insets.right - GUTTER;
|
|
121
|
+
const minTop = insets.top + GUTTER;
|
|
122
|
+
const maxBottom = window.height - insets.bottom - GUTTER;
|
|
123
|
+
left = Math.max(minLeft, Math.min(anchor.x, maxRight - size.width));
|
|
116
124
|
const above = anchor.y - sideOffset - size.height;
|
|
117
125
|
const below = anchor.y + anchor.height + sideOffset;
|
|
118
|
-
const fitsAbove = above >=
|
|
119
|
-
const fitsBelow = below + size.height <=
|
|
126
|
+
const fitsAbove = above >= minTop;
|
|
127
|
+
const fitsBelow = below + size.height <= maxBottom;
|
|
120
128
|
const preferAbove = side === 'top' ? fitsAbove || !fitsBelow : !fitsBelow && fitsAbove;
|
|
121
129
|
top = preferAbove ? above : below;
|
|
122
|
-
top = Math.
|
|
130
|
+
top = Math.max(minTop, Math.min(top, maxBottom - size.height));
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
return (
|
|
@@ -142,8 +142,8 @@ function FolderRow({
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
|
-
* The line under the AI chat's pill composer (the reference's `StatusBar`):
|
|
146
|
-
* tall, space-between.
|
|
145
|
+
* The line under the AI chat's pill composer (the reference's `StatusBar`): at
|
|
146
|
+
* least 26 tall (it grows with the system font), space-between.
|
|
147
147
|
*
|
|
148
148
|
* left gap 12: the branch (mirrored merge glyph) and the project folder —
|
|
149
149
|
* its caret turns over while the "Local Folders" panel is open
|
|
@@ -188,7 +188,7 @@ export function ComposerStatusBarBase({
|
|
|
188
188
|
return (
|
|
189
189
|
<View
|
|
190
190
|
testID={testID}
|
|
191
|
-
style={[{ width: '100%',
|
|
191
|
+
style={[{ width: '100%', minHeight: 26, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, style]}>
|
|
192
192
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
|
193
193
|
{branch !== undefined ? (
|
|
194
194
|
<StatusItem
|
|
@@ -48,6 +48,8 @@ const RAIL_PADDING = 3;
|
|
|
48
48
|
const MARK_HEIGHT = 30;
|
|
49
49
|
const MARK_GAP = 6;
|
|
50
50
|
const ROW_HEIGHT = 36;
|
|
51
|
+
/** The model chip's widest: past this a name truncates even with room to spare. */
|
|
52
|
+
const MODEL_TRIGGER_MAX_WIDTH = 240;
|
|
51
53
|
|
|
52
54
|
const DEFAULT_LABELS: Required<ModelPickerLabels> = {
|
|
53
55
|
models: 'Models',
|
|
@@ -367,7 +369,8 @@ function ModelRow({
|
|
|
367
369
|
<View
|
|
368
370
|
style={{
|
|
369
371
|
position: 'relative',
|
|
370
|
-
height:
|
|
372
|
+
// A floor, not a height: the name grows with the system font.
|
|
373
|
+
minHeight: ROW_HEIGHT,
|
|
371
374
|
width: '100%',
|
|
372
375
|
flexShrink: 0,
|
|
373
376
|
flexDirection: 'row',
|
|
@@ -422,7 +425,7 @@ function ModelRow({
|
|
|
422
425
|
* mark in view (instantly on open, smoothly after)
|
|
423
426
|
* column top 8 / right 8 / left 52: a 20px header ("Models" + a 50%-opacity
|
|
424
427
|
* "Quick Search" hint, or the search field), then the list 4 below
|
|
425
|
-
* rows 36 tall, radius 10, px 8: 16px mark, 6, name (+ provider while
|
|
428
|
+
* rows min 36 tall, radius 10, px 8: 16px mark, 6, name (+ provider while
|
|
426
429
|
* searching), the effort chip on the selected row, 12, 14px radio;
|
|
427
430
|
* 4 apart, pb 4, soft top edge once scrolled
|
|
428
431
|
*
|
|
@@ -546,8 +549,14 @@ export function ModelPickerBase({
|
|
|
546
549
|
const rows: Match[] = results ?? (activeProvider ? activeProvider.models.map((model) => ({ provider: activeProvider, model })) : []);
|
|
547
550
|
|
|
548
551
|
const triggerStyle: WebCssStyle = {
|
|
549
|
-
|
|
550
|
-
|
|
552
|
+
// At least 32, never exactly: at the largest system font the name is taller
|
|
553
|
+
// than 32 and a fixed height clipped it. It shrinks (with an ellipsis) to
|
|
554
|
+
// leave the composer's own buttons their room, and stops at a width that
|
|
555
|
+
// still reads as a chip.
|
|
556
|
+
minHeight: 32,
|
|
557
|
+
minWidth: 0,
|
|
558
|
+
maxWidth: MODEL_TRIGGER_MAX_WIDTH,
|
|
559
|
+
flexShrink: 1,
|
|
551
560
|
flexDirection: 'row',
|
|
552
561
|
alignItems: 'center',
|
|
553
562
|
justifyContent: 'center',
|
|
@@ -590,7 +599,7 @@ export function ModelPickerBase({
|
|
|
590
599
|
onHoverIn={() => setHovered(true)}
|
|
591
600
|
onHoverOut={() => setHovered(false)}
|
|
592
601
|
style={[triggerStyle, style]}>
|
|
593
|
-
<Text variant="body-medium" numberOfLines={1} style={{ paddingLeft: 2, paddingRight: 2, color: palette.textSecondary }}>
|
|
602
|
+
<Text variant="body-medium" numberOfLines={1} ellipsizeMode="tail" style={{ flexShrink: 1, minWidth: 0, paddingLeft: 2, paddingRight: 2, color: palette.textSecondary }}>
|
|
594
603
|
{selected?.model.name ?? ''}
|
|
595
604
|
</Text>
|
|
596
605
|
<TurningChevron degrees={open ? 180 : 0} color={palette.iconSecondary} />
|
|
@@ -10,6 +10,8 @@ import type { ComposerPanelLabels, ComposerPanelPermissionOption } from './types
|
|
|
10
10
|
import { dataHook } from './web-hooks';
|
|
11
11
|
|
|
12
12
|
const FLIP: ViewStyle = { transform: [{ scaleY: -1 }] };
|
|
13
|
+
/** The permission chip's widest: past this a mode's name truncates. */
|
|
14
|
+
const PERMISSION_TRIGGER_MAX_WIDTH = 160;
|
|
13
15
|
|
|
14
16
|
interface PermissionMenuProps {
|
|
15
17
|
palette: ComposerPalette;
|
|
@@ -27,7 +29,9 @@ interface PermissionMenuProps {
|
|
|
27
29
|
* press or while its menu is open, and a 323px panel of radio rows that opens
|
|
28
30
|
* upward.
|
|
29
31
|
*
|
|
30
|
-
* trigger h 30
|
|
32
|
+
* trigger min-h 30 (grows with the system font), max-w 160 then the
|
|
33
|
+
* label ellipsizes, pl 8 / pr 10, gap 4, icon 16, body-medium
|
|
34
|
+
* secondary
|
|
31
35
|
* panel w 323, radius 20, border, p 6, shadow-dropdown
|
|
32
36
|
* body pt 4, header→rows 6; header px 8 gap 10, body-medium tertiary
|
|
33
37
|
* row p 8, radius 14, gap 8, icon 20, label body-medium secondary,
|
|
@@ -82,7 +86,12 @@ export function PermissionMenu({
|
|
|
82
86
|
onHoverOut={() => setHovered(false)}
|
|
83
87
|
style={({ pressed }) => {
|
|
84
88
|
const trigger: WebCssStyle = {
|
|
85
|
-
|
|
89
|
+
// At least 30, never exactly: at the largest system font the label
|
|
90
|
+
// is taller than 30 and a fixed height clipped it top and bottom.
|
|
91
|
+
// A long mode name truncates at a width that still reads as a chip.
|
|
92
|
+
minHeight: 30,
|
|
93
|
+
minWidth: 0,
|
|
94
|
+
maxWidth: PERMISSION_TRIGGER_MAX_WIDTH,
|
|
86
95
|
flexShrink: 0,
|
|
87
96
|
flexDirection: 'row',
|
|
88
97
|
alignItems: 'center',
|
|
@@ -98,10 +107,14 @@ export function PermissionMenu({
|
|
|
98
107
|
};
|
|
99
108
|
return trigger;
|
|
100
109
|
}}>
|
|
101
|
-
<View style={current.flip ? FLIP :
|
|
110
|
+
<View style={current.flip ? [FLIP, { flexShrink: 0 }] : { flexShrink: 0 }}>
|
|
102
111
|
<CurrentIcon width={16} height={16} fill={palette.iconSecondary} />
|
|
103
112
|
</View>
|
|
104
|
-
<Text
|
|
113
|
+
<Text
|
|
114
|
+
variant="body-medium"
|
|
115
|
+
numberOfLines={1}
|
|
116
|
+
ellipsizeMode="tail"
|
|
117
|
+
style={{ flexShrink: 1, minWidth: 0, color: palette.textSecondary }}>
|
|
105
118
|
{current.label}
|
|
106
119
|
</Text>
|
|
107
120
|
</Pressable>
|
|
@@ -14,6 +14,7 @@ import { MENU_TRIGGER_POPUP } from '../floating/constants';
|
|
|
14
14
|
import { MenuSurfaceProvider, type MenuSurfaceContextValue } from '../floating/context';
|
|
15
15
|
import { createMenuRows } from '../floating/menu-rows';
|
|
16
16
|
import { createInlineMenuSub } from '../floating/menu-sub-inline';
|
|
17
|
+
import { LongPressArea } from '../floating/LongPressArea';
|
|
17
18
|
import { TriggerSlot } from '../floating/TriggerSlot';
|
|
18
19
|
import { useSheetOpenBridge } from '../floating/use-sheet-open-bridge';
|
|
19
20
|
import { ContextMenuProvider, useContextMenu } from './context';
|
|
@@ -57,9 +58,13 @@ export function ContextMenuTrigger({
|
|
|
57
58
|
}: ContextMenuTriggerProps) {
|
|
58
59
|
const menu = useContextMenu();
|
|
59
60
|
|
|
61
|
+
// Without `asChild` the trigger is Bloom's own, and it is a `LongPressArea`
|
|
62
|
+
// rather than TriggerSlot's `Pressable`: a Pressable holds the touch
|
|
63
|
+
// responder, which on Android keeps a horizontal ScrollView inside the
|
|
64
|
+
// trigger (a code block in a chat turn) from ever scrolling.
|
|
60
65
|
return (
|
|
61
66
|
<TriggerSlot
|
|
62
|
-
asChild
|
|
67
|
+
asChild
|
|
63
68
|
className={className}
|
|
64
69
|
style={style}
|
|
65
70
|
testID={testID}
|
|
@@ -76,7 +81,7 @@ export function ContextMenuTrigger({
|
|
|
76
81
|
'aria-haspopup': MENU_TRIGGER_POPUP,
|
|
77
82
|
'aria-expanded': menu.open,
|
|
78
83
|
}}>
|
|
79
|
-
{children}
|
|
84
|
+
{asChild ? children : <LongPressArea>{children}</LongPressArea>}
|
|
80
85
|
</TriggerSlot>
|
|
81
86
|
);
|
|
82
87
|
}
|
|
@@ -12,10 +12,11 @@ import {
|
|
|
12
12
|
type StyleProp,
|
|
13
13
|
type ViewStyle,
|
|
14
14
|
} from 'react-native';
|
|
15
|
-
import { type AnimatedStyle } from 'react-native-reanimated';
|
|
15
|
+
import Animated, { useAnimatedStyle, type AnimatedStyle } from 'react-native-reanimated';
|
|
16
16
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
17
17
|
|
|
18
18
|
import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
|
|
19
|
+
import { useSheetKeyboardHeight } from '../bottom-sheet/context';
|
|
19
20
|
import { useTheme } from '../theme/use-theme';
|
|
20
21
|
import { StyledView } from '../styles/styled-primitives';
|
|
21
22
|
import { Context } from './context';
|
|
@@ -345,9 +346,7 @@ export function DialogBottomSheet({
|
|
|
345
346
|
rather than a padding: the content box keeps the caller's
|
|
346
347
|
`padding`/`contentPadding` untouched on both platforms, and the
|
|
347
348
|
morph layer above still measures the content alone. */}
|
|
348
|
-
{bottomInset > 0 ?
|
|
349
|
-
<View testID={SAFE_AREA_SPACER_TESTID} style={{ height: bottomInset }} />
|
|
350
|
-
) : null}
|
|
349
|
+
{bottomInset > 0 ? <SafeAreaSpacer testID={SAFE_AREA_SPACER_TESTID} inset={bottomInset} /> : null}
|
|
351
350
|
</StyledView>
|
|
352
351
|
</Context.Provider>
|
|
353
352
|
</BottomSheet>
|
|
@@ -357,6 +356,19 @@ export function DialogBottomSheet({
|
|
|
357
356
|
/** Test handle for the bottom safe-area spacer. */
|
|
358
357
|
const SAFE_AREA_SPACER_TESTID = 'dialog-sheet-safe-area';
|
|
359
358
|
|
|
359
|
+
/**
|
|
360
|
+
* The gesture bar's height under the content — folded while the keyboard is
|
|
361
|
+
* up, because the keyboard covers the bar: the sheet already rides on top of
|
|
362
|
+
* the keyboard, and a spacer left standing floated its buttons a bar's height
|
|
363
|
+
* above it (Pixel 8a, Android 16). A child of the sheet, so it reads the
|
|
364
|
+
* sheet's own keyboard value.
|
|
365
|
+
*/
|
|
366
|
+
function SafeAreaSpacer({ inset, testID }: { inset: number; testID: string }) {
|
|
367
|
+
const keyboard = useSheetKeyboardHeight();
|
|
368
|
+
const style = useAnimatedStyle(() => ({ height: keyboard.value > 0 ? 0 : inset }), [keyboard, inset]);
|
|
369
|
+
return <Animated.View testID={testID} style={style} />;
|
|
370
|
+
}
|
|
371
|
+
|
|
360
372
|
/** Passes a bounded height down to a `scrollable={false}` body. */
|
|
361
373
|
const FILL_BOUNDED = { flex: 1, minHeight: 0 } as const;
|
|
362
374
|
|