@oxyhq/bloom 0.46.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +34 -12
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/combobox/Combobox.js +3 -3
- package/lib/commonjs/combobox/Combobox.js.map +1 -1
- package/lib/commonjs/command/Command.js +3 -3
- package/lib/commonjs/command/Command.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +74 -5
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +153 -10
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +84 -29
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/dialog/DialogHeader.js +365 -0
- package/lib/commonjs/dialog/DialogHeader.js.map +1 -0
- package/lib/commonjs/dialog/index.js +7 -0
- package/lib/commonjs/dialog/index.js.map +1 -1
- package/lib/commonjs/dialog/index.web.js +7 -0
- package/lib/commonjs/dialog/index.web.js.map +1 -1
- package/lib/commonjs/index.js +11 -4
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +11 -4
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/react-native-className.d.js +6 -0
- package/lib/commonjs/react-native-className.d.js.map +1 -0
- package/lib/commonjs/{search-input → search}/index.js +5 -3
- package/lib/commonjs/search/index.js.map +1 -0
- package/lib/commonjs/settings-list/SettingsList.js +5 -1
- package/lib/commonjs/settings-list/SettingsList.js.map +1 -1
- package/lib/commonjs/surfaces/SurfaceHost.js +22 -9
- package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -1
- package/lib/commonjs/text-field/index.js +22 -6
- package/lib/commonjs/text-field/index.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +34 -12
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/combobox/Combobox.js +3 -3
- package/lib/module/combobox/Combobox.js.map +1 -1
- package/lib/module/command/Command.js +3 -3
- package/lib/module/command/Command.js.map +1 -1
- package/lib/module/dialog/Dialog.js +75 -6
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +156 -13
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +85 -30
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/dialog/DialogHeader.js +356 -0
- package/lib/module/dialog/DialogHeader.js.map +1 -0
- package/lib/module/dialog/index.js +1 -0
- package/lib/module/dialog/index.js.map +1 -1
- package/lib/module/dialog/index.web.js +1 -0
- package/lib/module/dialog/index.web.js.map +1 -1
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -2
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/react-native-className.d.js +20 -0
- package/lib/module/react-native-className.d.js.map +1 -0
- package/lib/module/{search-input → search}/index.js +6 -2
- package/lib/module/search/index.js.map +1 -0
- package/lib/module/settings-list/SettingsList.js +5 -1
- package/lib/module/settings-list/SettingsList.js.map +1 -1
- package/lib/module/surfaces/SurfaceHost.js +22 -9
- package/lib/module/surfaces/SurfaceHost.js.map +1 -1
- package/lib/module/text-field/index.js +22 -6
- package/lib/module/text-field/index.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +23 -0
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/combobox/Combobox.d.ts +1 -1
- package/lib/typescript/commonjs/command/Command.d.ts +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts +2 -2
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogHeader.d.ts +86 -0
- package/lib/typescript/commonjs/dialog/DialogHeader.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/index.d.ts +2 -1
- package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts +2 -1
- package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +75 -0
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/icons/common.d.ts +1 -0
- package/lib/typescript/commonjs/icons/common.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +3 -3
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/input-group/types.d.ts +1 -1
- package/lib/typescript/commonjs/{search-input → search}/index.d.ts +1 -1
- package/lib/typescript/commonjs/search/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/surfaces/types.d.ts +17 -1
- package/lib/typescript/commonjs/surfaces/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/text-field/index.d.ts +2 -1
- package/lib/typescript/commonjs/text-field/index.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +23 -0
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/combobox/Combobox.d.ts +1 -1
- package/lib/typescript/module/command/Command.d.ts +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts +2 -2
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogHeader.d.ts +86 -0
- package/lib/typescript/module/dialog/DialogHeader.d.ts.map +1 -0
- package/lib/typescript/module/dialog/index.d.ts +2 -1
- package/lib/typescript/module/dialog/index.d.ts.map +1 -1
- package/lib/typescript/module/dialog/index.web.d.ts +2 -1
- package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +75 -0
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/icons/common.d.ts +1 -0
- package/lib/typescript/module/icons/common.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +3 -3
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/input-group/types.d.ts +1 -1
- package/lib/typescript/module/{search-input → search}/index.d.ts +1 -1
- package/lib/typescript/module/search/index.d.ts.map +1 -0
- package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -1
- package/lib/typescript/module/surfaces/types.d.ts +17 -1
- package/lib/typescript/module/surfaces/types.d.ts.map +1 -1
- package/lib/typescript/module/text-field/index.d.ts +2 -1
- package/lib/typescript/module/text-field/index.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/BottomSheet.test.tsx +47 -0
- package/src/__tests__/DialogBottomSheet.test.tsx +30 -5
- package/src/bottom-sheet/BottomSheetBase.tsx +59 -10
- package/src/combobox/Combobox.tsx +3 -3
- package/src/command/Command.tsx +3 -3
- package/src/dialog/Dialog.tsx +74 -4
- package/src/dialog/Dialog.web.tsx +154 -23
- package/src/dialog/DialogBottomSheet.tsx +90 -28
- package/src/dialog/DialogHeader.tsx +420 -0
- package/src/dialog/index.ts +2 -0
- package/src/dialog/index.web.ts +2 -0
- package/src/dialog/types.ts +76 -0
- package/src/index.ts +3 -1
- package/src/index.web.ts +3 -1
- package/src/input-group/types.ts +1 -1
- package/src/react-native-className.d.ts +38 -0
- package/src/{search-input → search}/index.tsx +8 -5
- package/src/settings-list/SettingsList.tsx +5 -1
- package/src/surfaces/SurfaceHost.tsx +16 -1
- package/src/surfaces/types.ts +17 -1
- package/src/text-field/index.tsx +24 -5
- package/lib/commonjs/search-input/index.js.map +0 -1
- package/lib/module/search-input/index.js.map +0 -1
- package/lib/typescript/commonjs/search-input/index.d.ts.map +0 -1
- package/lib/typescript/module/search-input/index.d.ts.map +0 -1
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
memo,
|
|
4
|
+
useContext,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useSyncExternalStore,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import {
|
|
10
|
+
StyleSheet,
|
|
11
|
+
View,
|
|
12
|
+
type LayoutChangeEvent,
|
|
13
|
+
type StyleProp,
|
|
14
|
+
type ViewStyle,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import Animated, {
|
|
17
|
+
Extrapolation,
|
|
18
|
+
interpolate,
|
|
19
|
+
useAnimatedStyle,
|
|
20
|
+
useSharedValue,
|
|
21
|
+
type SharedValue,
|
|
22
|
+
} from 'react-native-reanimated';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
ChevronLeft_Stroke2_Corner0_Rounded,
|
|
26
|
+
TimesLarge_Stroke2_Corner0_Rounded,
|
|
27
|
+
} from '../icons';
|
|
28
|
+
import { FrostedIconButton } from '../frosted-icon-button';
|
|
29
|
+
import { useTheme } from '../theme/use-theme';
|
|
30
|
+
import { H1, Text } from '../typography';
|
|
31
|
+
import type { DialogHeaderConfig } from './types';
|
|
32
|
+
|
|
33
|
+
export type { DialogHeaderConfig } from './types';
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Dialog-owned navigation header (iOS large-title collapse).
|
|
37
|
+
//
|
|
38
|
+
// This is a DISTINCT surface mode from the declarative
|
|
39
|
+
// `title`/`description`/`actions` centered-heading path: it turns the Dialog
|
|
40
|
+
// into a scrolling page with a sticky, gradient top bar (left + right slots)
|
|
41
|
+
// and a large collapsing title that lives in the Dialog's OWN scroll content.
|
|
42
|
+
// As the large title scrolls under the bar, a small title cross-fades into the
|
|
43
|
+
// bar center.
|
|
44
|
+
//
|
|
45
|
+
// Screens render NO header of their own — they declare their title/subtitle
|
|
46
|
+
// (via the surface registry) or contribute runtime slots (a Save action, a
|
|
47
|
+
// dynamic title) through {@link useDialogHeader}.
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/** Interactive nav-bar row height (px). Buttons + collapsed title live here. */
|
|
51
|
+
export const DIALOG_NAV_BAR_HEIGHT = 52;
|
|
52
|
+
|
|
53
|
+
/** Extra gradient tail (px) below the bar, over which scrolled content fades. */
|
|
54
|
+
const GRADIENT_FADE = 20;
|
|
55
|
+
|
|
56
|
+
/** Total gradient overlay height (bar + fade tail). */
|
|
57
|
+
export const DIALOG_HEADER_OVERLAY_HEIGHT = DIALOG_NAV_BAR_HEIGHT + GRADIENT_FADE;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Top inset applied to the Dialog's scroll content so the large title starts
|
|
61
|
+
* BELOW the whole gradient overlay (fully clear at rest, then scrolls under it).
|
|
62
|
+
*/
|
|
63
|
+
export const DIALOG_HEADER_CONTENT_TOP = DIALOG_HEADER_OVERLAY_HEIGHT;
|
|
64
|
+
|
|
65
|
+
/** Horizontal gutter — matches the `screen-margin` (20px) SDK screens use. */
|
|
66
|
+
const HEADER_H_PADDING = 20;
|
|
67
|
+
|
|
68
|
+
/** Circular icon-button diameter for the default back / close affordances. */
|
|
69
|
+
const NAV_BUTTON_SIZE = 36;
|
|
70
|
+
|
|
71
|
+
// --- Runtime override store (child screen → nav bar / large title) ----------
|
|
72
|
+
//
|
|
73
|
+
// The nav bar overlay and the in-content large title live in DIFFERENT subtrees
|
|
74
|
+
// (the bar is a sibling ABOVE the scroller; the large title + the screen are
|
|
75
|
+
// inside it), so a plain React ancestor context cannot bridge a screen's runtime
|
|
76
|
+
// contribution up to the bar. We use a tiny external store instead: the screen
|
|
77
|
+
// writes its override through {@link useDialogHeader}; the bar and large title
|
|
78
|
+
// read it via `useSyncExternalStore`. Only those two re-render — never the
|
|
79
|
+
// screen subtree — and there is no shared-ancestor constraint.
|
|
80
|
+
|
|
81
|
+
interface HeaderStore {
|
|
82
|
+
getSnapshot: () => DialogHeaderConfig | null;
|
|
83
|
+
setOverride: (next: DialogHeaderConfig | null) => void;
|
|
84
|
+
subscribe: (listener: () => void) => () => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function configsEqual(a: DialogHeaderConfig | null, b: DialogHeaderConfig | null): boolean {
|
|
88
|
+
if (a === b) return true;
|
|
89
|
+
if (!a || !b) return false;
|
|
90
|
+
return (
|
|
91
|
+
a.title === b.title &&
|
|
92
|
+
a.subtitle === b.subtitle &&
|
|
93
|
+
a.largeTitle === b.largeTitle &&
|
|
94
|
+
a.left === b.left &&
|
|
95
|
+
a.right === b.right &&
|
|
96
|
+
a.onBack === b.onBack &&
|
|
97
|
+
a.showClose === b.showClose
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createHeaderStore(): HeaderStore {
|
|
102
|
+
let override: DialogHeaderConfig | null = null;
|
|
103
|
+
const listeners = new Set<() => void>();
|
|
104
|
+
return {
|
|
105
|
+
getSnapshot: () => override,
|
|
106
|
+
setOverride: (next) => {
|
|
107
|
+
if (configsEqual(override, next)) return;
|
|
108
|
+
override = next;
|
|
109
|
+
for (const l of listeners) l();
|
|
110
|
+
},
|
|
111
|
+
subscribe: (listener) => {
|
|
112
|
+
listeners.add(listener);
|
|
113
|
+
return () => listeners.delete(listener);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The per-surface header controller — the scroll offset + measured large-title
|
|
120
|
+
* height shared values plus the runtime-override store. Created once per Dialog
|
|
121
|
+
* (via {@link useDialogHeaderController}) and threaded into the scroller (writes
|
|
122
|
+
* `scrollY`), the nav bar, the large title, and the child provider.
|
|
123
|
+
*/
|
|
124
|
+
export interface DialogHeaderController {
|
|
125
|
+
scrollY: SharedValue<number>;
|
|
126
|
+
largeTitleHeight: SharedValue<number>;
|
|
127
|
+
store: HeaderStore;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Create the header controller for one Dialog. Stable for the Dialog's life. */
|
|
131
|
+
export function useDialogHeaderController(): DialogHeaderController {
|
|
132
|
+
const scrollY = useSharedValue(0);
|
|
133
|
+
const largeTitleHeight = useSharedValue(0);
|
|
134
|
+
const store = useMemo(() => createHeaderStore(), []);
|
|
135
|
+
return useMemo(
|
|
136
|
+
() => ({ scrollY, largeTitleHeight, store }),
|
|
137
|
+
[scrollY, largeTitleHeight, store],
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const DialogHeaderContext = createContext<DialogHeaderController | null>(null);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Wraps the Dialog's scroll content so a mounted screen can contribute header
|
|
145
|
+
* slots via {@link useDialogHeader}. Only the child subtree needs this — the nav
|
|
146
|
+
* bar and large title read the same controller directly.
|
|
147
|
+
*/
|
|
148
|
+
export function DialogHeaderProvider({
|
|
149
|
+
controller,
|
|
150
|
+
children,
|
|
151
|
+
}: {
|
|
152
|
+
controller: DialogHeaderController;
|
|
153
|
+
children: React.ReactNode;
|
|
154
|
+
}): React.ReactElement {
|
|
155
|
+
return (
|
|
156
|
+
<DialogHeaderContext.Provider value={controller}>
|
|
157
|
+
{children}
|
|
158
|
+
</DialogHeaderContext.Provider>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Contribute header content from within a Dialog surface — a dynamic title, a
|
|
164
|
+
* Save action, custom slots. Merges over the surface's static (registry) header
|
|
165
|
+
* config; unmounting clears the contribution. Slot nodes (`left`/`right`) MUST
|
|
166
|
+
* be referentially stable (memoize them) so the effect does not thrash.
|
|
167
|
+
*
|
|
168
|
+
* No-op outside a header-mode Dialog (e.g. a confirm dialog), so a screen can
|
|
169
|
+
* call it unconditionally.
|
|
170
|
+
*/
|
|
171
|
+
export function useDialogHeader(config: DialogHeaderConfig | null | undefined): void {
|
|
172
|
+
const controller = useContext(DialogHeaderContext);
|
|
173
|
+
const store = controller?.store;
|
|
174
|
+
// Set synchronously in the commit's layout phase so the bar/title fill in
|
|
175
|
+
// BEFORE the browser paints — no first-frame flash of an empty bar.
|
|
176
|
+
useLayoutEffect(() => {
|
|
177
|
+
if (!store) return;
|
|
178
|
+
store.setOverride(config ?? null);
|
|
179
|
+
return () => store.setOverride(null);
|
|
180
|
+
// Slot nodes are compared by identity; callers memoize them.
|
|
181
|
+
}, [
|
|
182
|
+
store,
|
|
183
|
+
config?.title,
|
|
184
|
+
config?.subtitle,
|
|
185
|
+
config?.largeTitle,
|
|
186
|
+
config?.left,
|
|
187
|
+
config?.right,
|
|
188
|
+
config?.onBack,
|
|
189
|
+
config?.showClose,
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Read the merged (base + runtime-override) header config, reactively. */
|
|
194
|
+
function useMergedHeaderConfig(
|
|
195
|
+
base: DialogHeaderConfig,
|
|
196
|
+
store: HeaderStore,
|
|
197
|
+
): DialogHeaderConfig {
|
|
198
|
+
const override = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
199
|
+
return useMemo(() => (override ? { ...base, ...override } : base), [base, override]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- Nav bar overlay --------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The sticky nav bar: gradient background + left / center (collapsing title) /
|
|
206
|
+
* right slots. Absolutely positioned at the top of the Dialog surface, painted
|
|
207
|
+
* ABOVE the scroll content. Reads the scroll offset to cross-fade the small
|
|
208
|
+
* title in as the large title scrolls under it.
|
|
209
|
+
*
|
|
210
|
+
* `onDismiss` is the Dialog's own close (the default right-slot close button
|
|
211
|
+
* calls it) — passed explicitly rather than read from the dialog context because
|
|
212
|
+
* the bar is rendered as a sibling of the scroller, outside that context on the
|
|
213
|
+
* bottom-sheet path.
|
|
214
|
+
*/
|
|
215
|
+
export const DialogNavHeader = memo(function DialogNavHeader({
|
|
216
|
+
controller,
|
|
217
|
+
header,
|
|
218
|
+
onDismiss,
|
|
219
|
+
collapse = true,
|
|
220
|
+
style,
|
|
221
|
+
}: {
|
|
222
|
+
controller: DialogHeaderController;
|
|
223
|
+
header: DialogHeaderConfig;
|
|
224
|
+
onDismiss: () => void;
|
|
225
|
+
/**
|
|
226
|
+
* Whether the small title cross-fades in on scroll (paired with an in-content
|
|
227
|
+
* {@link DialogLargeTitle}). Defaults to `true`. Set `false` for a surface
|
|
228
|
+
* whose content owns its own scroller (no Dialog scroll offset to drive the
|
|
229
|
+
* collapse): the small title is then always visible — a plain titled nav bar.
|
|
230
|
+
*/
|
|
231
|
+
collapse?: boolean;
|
|
232
|
+
style?: StyleProp<ViewStyle>;
|
|
233
|
+
}) {
|
|
234
|
+
const theme = useTheme();
|
|
235
|
+
const config = useMergedHeaderConfig(header, controller.store);
|
|
236
|
+
const { scrollY, largeTitleHeight } = controller;
|
|
237
|
+
|
|
238
|
+
const hasLargeTitle = collapse && (config.largeTitle ?? true) && !!config.title;
|
|
239
|
+
|
|
240
|
+
const titleStyle = useAnimatedStyle(() => {
|
|
241
|
+
// When there is no large title to collapse under, the bar title is always
|
|
242
|
+
// shown (a plain, non-collapsing nav bar). Otherwise it cross-fades in as the
|
|
243
|
+
// large title scrolls up under the bar.
|
|
244
|
+
if (!hasLargeTitle) return { opacity: 1, transform: [{ translateY: 0 }] };
|
|
245
|
+
const threshold = largeTitleHeight.value > 0 ? largeTitleHeight.value : 44;
|
|
246
|
+
const start = threshold * 0.35;
|
|
247
|
+
const end = threshold * 0.9;
|
|
248
|
+
return {
|
|
249
|
+
opacity: interpolate(scrollY.value, [start, end], [0, 1], Extrapolation.CLAMP),
|
|
250
|
+
transform: [
|
|
251
|
+
{ translateY: interpolate(scrollY.value, [start, end], [10, 0], Extrapolation.CLAMP) },
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
// Reanimated on RN-Web has no worklets plugin: every shared value the mapper
|
|
255
|
+
// reads MUST be in the deps array or it freezes on the first frame.
|
|
256
|
+
}, [scrollY, largeTitleHeight, hasLargeTitle]);
|
|
257
|
+
|
|
258
|
+
const left =
|
|
259
|
+
config.left ??
|
|
260
|
+
(config.onBack ? (
|
|
261
|
+
<FrostedIconButton
|
|
262
|
+
onPress={config.onBack}
|
|
263
|
+
accessibilityLabel="Go back"
|
|
264
|
+
icon={<ChevronLeft_Stroke2_Corner0_Rounded size="md" />}
|
|
265
|
+
/>
|
|
266
|
+
) : null);
|
|
267
|
+
|
|
268
|
+
const right =
|
|
269
|
+
config.right ??
|
|
270
|
+
(config.showClose !== false ? (
|
|
271
|
+
<FrostedIconButton
|
|
272
|
+
onPress={onDismiss}
|
|
273
|
+
accessibilityLabel="Close"
|
|
274
|
+
icon={<TimesLarge_Stroke2_Corner0_Rounded size="md" />}
|
|
275
|
+
/>
|
|
276
|
+
) : null);
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<View
|
|
280
|
+
pointerEvents="box-none"
|
|
281
|
+
style={[styles.overlay, style]}
|
|
282
|
+
>
|
|
283
|
+
{/* Opaque (surface bg) at the top → transparent at the bottom, so scrolled
|
|
284
|
+
content fades out under the bar. Pure NativeWind — no SVG dependency. */}
|
|
285
|
+
<View
|
|
286
|
+
pointerEvents="none"
|
|
287
|
+
className="bg-gradient-to-b from-bg to-transparent"
|
|
288
|
+
style={[StyleSheet.absoluteFill, { height: DIALOG_HEADER_OVERLAY_HEIGHT }]}
|
|
289
|
+
/>
|
|
290
|
+
<View pointerEvents="box-none" style={styles.navRow}>
|
|
291
|
+
<View style={styles.side}>{left}</View>
|
|
292
|
+
<Animated.View
|
|
293
|
+
pointerEvents="none"
|
|
294
|
+
testID="dialog-nav-title"
|
|
295
|
+
style={[styles.centerTitle, titleStyle]}
|
|
296
|
+
>
|
|
297
|
+
{config.title ? (
|
|
298
|
+
<Text numberOfLines={1} style={[styles.smallTitle, { color: theme.colors.text }]}>
|
|
299
|
+
{config.title}
|
|
300
|
+
</Text>
|
|
301
|
+
) : null}
|
|
302
|
+
{/* The subtitle rides along ONLY in the static (non-collapsing) bar —
|
|
303
|
+
own-scroller screens whose subtitle is a short count. A large-title
|
|
304
|
+
screen keeps a clean single-line collapsed title. */}
|
|
305
|
+
{!hasLargeTitle && config.subtitle ? (
|
|
306
|
+
<Text
|
|
307
|
+
numberOfLines={1}
|
|
308
|
+
style={[styles.smallSubtitle, { color: theme.colors.textSecondary }]}
|
|
309
|
+
>
|
|
310
|
+
{config.subtitle}
|
|
311
|
+
</Text>
|
|
312
|
+
) : null}
|
|
313
|
+
</Animated.View>
|
|
314
|
+
<View style={[styles.side, styles.sideRight]}>{right}</View>
|
|
315
|
+
</View>
|
|
316
|
+
</View>
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// --- In-content large title -------------------------------------------------
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The large collapsing title, rendered at the TOP of the Dialog's scroll content
|
|
324
|
+
* (before the screen body). Owns the content top inset (so the title starts
|
|
325
|
+
* below the gradient overlay) and measures its own text height into the
|
|
326
|
+
* controller so the nav bar knows when to cross-fade the small title in.
|
|
327
|
+
*/
|
|
328
|
+
export const DialogLargeTitle = memo(function DialogLargeTitle({
|
|
329
|
+
controller,
|
|
330
|
+
header,
|
|
331
|
+
}: {
|
|
332
|
+
controller: DialogHeaderController;
|
|
333
|
+
header: DialogHeaderConfig;
|
|
334
|
+
}) {
|
|
335
|
+
const theme = useTheme();
|
|
336
|
+
const config = useMergedHeaderConfig(header, controller.store);
|
|
337
|
+
const { largeTitleHeight } = controller;
|
|
338
|
+
|
|
339
|
+
const hasLargeTitle = (config.largeTitle ?? true) && !!config.title;
|
|
340
|
+
|
|
341
|
+
const onLayout = (e: LayoutChangeEvent) => {
|
|
342
|
+
largeTitleHeight.value = e.nativeEvent.layout.height;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
if (!hasLargeTitle) {
|
|
346
|
+
// No large title: just inset the content below the nav bar.
|
|
347
|
+
return <View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<>
|
|
352
|
+
<View style={{ height: DIALOG_HEADER_CONTENT_TOP }} />
|
|
353
|
+
<View onLayout={onLayout} testID="dialog-large-title" style={styles.largeTitleBlock}>
|
|
354
|
+
<H1 style={[styles.largeTitle, { color: theme.colors.text }]}>{config.title}</H1>
|
|
355
|
+
{config.subtitle ? (
|
|
356
|
+
<Text style={[styles.largeSubtitle, { color: theme.colors.textSecondary }]}>
|
|
357
|
+
{config.subtitle}
|
|
358
|
+
</Text>
|
|
359
|
+
) : null}
|
|
360
|
+
</View>
|
|
361
|
+
</>
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const styles = StyleSheet.create({
|
|
366
|
+
overlay: {
|
|
367
|
+
position: 'absolute',
|
|
368
|
+
top: 0,
|
|
369
|
+
left: 0,
|
|
370
|
+
right: 0,
|
|
371
|
+
height: DIALOG_HEADER_OVERLAY_HEIGHT,
|
|
372
|
+
zIndex: 10,
|
|
373
|
+
},
|
|
374
|
+
navRow: {
|
|
375
|
+
height: DIALOG_NAV_BAR_HEIGHT,
|
|
376
|
+
flexDirection: 'row',
|
|
377
|
+
alignItems: 'center',
|
|
378
|
+
paddingHorizontal: HEADER_H_PADDING - 4,
|
|
379
|
+
},
|
|
380
|
+
side: {
|
|
381
|
+
minWidth: NAV_BUTTON_SIZE,
|
|
382
|
+
flexDirection: 'row',
|
|
383
|
+
alignItems: 'center',
|
|
384
|
+
},
|
|
385
|
+
sideRight: {
|
|
386
|
+
justifyContent: 'flex-end',
|
|
387
|
+
},
|
|
388
|
+
centerTitle: {
|
|
389
|
+
flex: 1,
|
|
390
|
+
alignItems: 'center',
|
|
391
|
+
justifyContent: 'center',
|
|
392
|
+
paddingHorizontal: 8,
|
|
393
|
+
},
|
|
394
|
+
smallTitle: {
|
|
395
|
+
fontSize: 16,
|
|
396
|
+
fontWeight: '600',
|
|
397
|
+
letterSpacing: -0.2,
|
|
398
|
+
lineHeight: 20,
|
|
399
|
+
},
|
|
400
|
+
smallSubtitle: {
|
|
401
|
+
fontSize: 12,
|
|
402
|
+
fontWeight: '400',
|
|
403
|
+
lineHeight: 14,
|
|
404
|
+
},
|
|
405
|
+
largeTitleBlock: {
|
|
406
|
+
paddingHorizontal: HEADER_H_PADDING,
|
|
407
|
+
paddingBottom: 4,
|
|
408
|
+
},
|
|
409
|
+
largeTitle: {
|
|
410
|
+
fontSize: 32,
|
|
411
|
+
lineHeight: 38,
|
|
412
|
+
letterSpacing: -0.5,
|
|
413
|
+
fontWeight: '700',
|
|
414
|
+
},
|
|
415
|
+
largeSubtitle: {
|
|
416
|
+
fontSize: 16,
|
|
417
|
+
lineHeight: 22,
|
|
418
|
+
marginTop: 4,
|
|
419
|
+
},
|
|
420
|
+
});
|
package/src/dialog/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { Dialog, DIALOG_SHEET_BACKDROP_TESTID } from './Dialog';
|
|
|
2
2
|
export { BloomDialogProvider } from './BloomDialogProvider';
|
|
3
3
|
export { alert } from './alert';
|
|
4
4
|
export { useDialogContext, useDialogControl } from './context';
|
|
5
|
+
export { useDialogHeader } from './DialogHeader';
|
|
5
6
|
export type {
|
|
6
7
|
AlertButton,
|
|
7
8
|
AlertButtonStyle,
|
|
@@ -11,6 +12,7 @@ export type {
|
|
|
11
12
|
DialogActionColor,
|
|
12
13
|
DialogContextProps,
|
|
13
14
|
DialogControlProps,
|
|
15
|
+
DialogHeaderConfig,
|
|
14
16
|
DialogInset,
|
|
15
17
|
DialogPlacement,
|
|
16
18
|
DialogProps,
|
package/src/dialog/index.web.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { Dialog, BLOOM_DIALOG_CSS, DIALOG_SHEET_BACKDROP_TESTID } from './Dialog
|
|
|
12
12
|
export { BloomDialogProvider } from './BloomDialogProvider.web';
|
|
13
13
|
export { alert } from './alert';
|
|
14
14
|
export { useDialogContext, useDialogControl } from './context';
|
|
15
|
+
export { useDialogHeader } from './DialogHeader';
|
|
15
16
|
export type {
|
|
16
17
|
AlertButton,
|
|
17
18
|
AlertButtonStyle,
|
|
@@ -21,6 +22,7 @@ export type {
|
|
|
21
22
|
DialogActionColor,
|
|
22
23
|
DialogContextProps,
|
|
23
24
|
DialogControlProps,
|
|
25
|
+
DialogHeaderConfig,
|
|
24
26
|
DialogInset,
|
|
25
27
|
DialogPlacement,
|
|
26
28
|
DialogProps,
|
package/src/dialog/types.ts
CHANGED
|
@@ -1,9 +1,51 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native';
|
|
2
3
|
|
|
3
4
|
import type { ResponsiveDialogPlacement } from './placement';
|
|
4
5
|
|
|
5
6
|
export type { DialogPlacement, ResponsiveDialogPlacement } from './placement';
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Config for the Dialog's OWN navigation header — a DISTINCT surface mode from
|
|
10
|
+
* the declarative `title`/`description`/`actions` centered-heading path. When
|
|
11
|
+
* present it turns the Dialog into a scrolling page with a sticky, gradient top
|
|
12
|
+
* bar (left + right slots) and a large collapsing title that lives in the
|
|
13
|
+
* Dialog's own scroll content; as the large title scrolls under the bar a small
|
|
14
|
+
* title cross-fades into the bar center. Screens render NO header of their own.
|
|
15
|
+
*
|
|
16
|
+
* A surface seeds this from its registry config; a mounted screen may override
|
|
17
|
+
* or augment any field at runtime via `useDialogHeader` (a dynamic title, a Save
|
|
18
|
+
* action on the right, etc.).
|
|
19
|
+
*/
|
|
20
|
+
export interface DialogHeaderConfig {
|
|
21
|
+
/** Collapsing title — rendered large in-content and small in the nav bar. */
|
|
22
|
+
title?: string;
|
|
23
|
+
/** Supporting copy under the large title (and, optionally, in the bar). */
|
|
24
|
+
subtitle?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to render the large collapsing title in the scroll content.
|
|
27
|
+
* Defaults to `true`. When `false`, only the small nav-bar title shows (always
|
|
28
|
+
* visible, no collapse) and the content starts flush under the bar.
|
|
29
|
+
*/
|
|
30
|
+
largeTitle?: boolean;
|
|
31
|
+
/** Custom left slot node. Overrides the default back button. */
|
|
32
|
+
left?: ReactNode;
|
|
33
|
+
/** Custom right slot node. Overrides the default close button. */
|
|
34
|
+
right?: ReactNode;
|
|
35
|
+
/**
|
|
36
|
+
* When set, the default left slot renders a back button that calls this. A
|
|
37
|
+
* custom `left` node still wins. Set by the surface host when the surface can
|
|
38
|
+
* navigate back.
|
|
39
|
+
*/
|
|
40
|
+
onBack?: () => void;
|
|
41
|
+
/**
|
|
42
|
+
* Whether the default right slot renders a close button (dismiss). Defaults to
|
|
43
|
+
* `true`. A custom `right` node still wins. `false` suppresses the close
|
|
44
|
+
* affordance entirely (e.g. a blocking surface).
|
|
45
|
+
*/
|
|
46
|
+
showClose?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
7
49
|
/**
|
|
8
50
|
* Side-sheet inset (px) from the root overlay's edges. Lets a host place a
|
|
9
51
|
* `left`/`right` placement "inside its own shell" — e.g. offset from the top
|
|
@@ -134,6 +176,40 @@ export type DialogProps = React.PropsWithChildren<{
|
|
|
134
176
|
inset?: DialogInset;
|
|
135
177
|
/** Whether to render the drag handle in bottom-sheet mode. Defaults to `true`. */
|
|
136
178
|
showHandle?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Turns on the Dialog's OWN navigation header (see {@link DialogHeaderConfig}).
|
|
181
|
+
* When present the Dialog renders a sticky gradient nav bar + a large
|
|
182
|
+
* collapsing title over its own scroll content, and its content is inset below
|
|
183
|
+
* the bar — a DISTINCT mode from the declarative
|
|
184
|
+
* `title`/`description`/`actions` centered heading (a confirm/alert dialog
|
|
185
|
+
* passes NO `header` and is unchanged). Screens render no header of their own;
|
|
186
|
+
* the title/slots come from this config, refined at runtime by a mounted
|
|
187
|
+
* screen's `useDialogHeader` contribution.
|
|
188
|
+
*/
|
|
189
|
+
header?: DialogHeaderConfig;
|
|
190
|
+
/**
|
|
191
|
+
* Open-INTENT seed for a freshly-mounted placement branch, distinct from the
|
|
192
|
+
* controlled `open` prop. It ONLY seeds a branch's initial visible state on
|
|
193
|
+
* mount — it never switches the dialog into controlled close semantics, so
|
|
194
|
+
* the imperative `control.close()` still owns the exit animation and fires
|
|
195
|
+
* `onClose` post-exit (no synchronous-`onClose` entry leak). Set by the
|
|
196
|
+
* surface stack so a responsive `Dialog` that swaps its inner component across
|
|
197
|
+
* a breakpoint (centered card ↔ bottom sheet on resize) re-mounts the new
|
|
198
|
+
* branch ALREADY OPEN instead of blank. Ignored when the controlled `open`
|
|
199
|
+
* prop is provided (that path seeds itself).
|
|
200
|
+
*/
|
|
201
|
+
startOpen?: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Whether the dialog wraps its content in an internal scroll container.
|
|
204
|
+
* Defaults to `true`. Set `false` when the content owns its own scrolling
|
|
205
|
+
* primitive (a `FlatList` / `SectionList` / any VirtualizedList, or its own
|
|
206
|
+
* `ScrollView`): the surface then renders children WITHOUT a ScrollView wrap —
|
|
207
|
+
* a bounded, overflow-clipped card on the centered/side placements and a
|
|
208
|
+
* non-scrollable `BottomSheet` body on the bottom placement — so the child
|
|
209
|
+
* gets a bounded height and owns its scroll, and no "VirtualizedList inside a
|
|
210
|
+
* plain ScrollView" warning is emitted.
|
|
211
|
+
*/
|
|
212
|
+
scrollable?: boolean;
|
|
137
213
|
/**
|
|
138
214
|
* Inner padding (px) of the dialog content container, applied uniformly across
|
|
139
215
|
* every placement (centered panel, side-sheet body, bottom-sheet body).
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ export {
|
|
|
59
59
|
alert,
|
|
60
60
|
useDialogContext,
|
|
61
61
|
useDialogControl,
|
|
62
|
+
useDialogHeader,
|
|
62
63
|
} from './dialog';
|
|
63
64
|
export type {
|
|
64
65
|
AlertButton,
|
|
@@ -67,6 +68,7 @@ export type {
|
|
|
67
68
|
DialogActionColor,
|
|
68
69
|
DialogContextProps,
|
|
69
70
|
DialogControlProps,
|
|
71
|
+
DialogHeaderConfig,
|
|
70
72
|
DialogInset,
|
|
71
73
|
DialogPlacement,
|
|
72
74
|
DialogProps,
|
|
@@ -144,7 +146,7 @@ export type { AnimatedCheckProps, AnimatedCheckRef } from './animated-check';
|
|
|
144
146
|
// Form components
|
|
145
147
|
export * from './text-field';
|
|
146
148
|
export * from './segmented-control';
|
|
147
|
-
export {
|
|
149
|
+
export { Search } from './search';
|
|
148
150
|
export { Label } from './label';
|
|
149
151
|
export type { LabelProps } from './label';
|
|
150
152
|
export { Field } from './field';
|
package/src/index.web.ts
CHANGED
|
@@ -64,6 +64,7 @@ export {
|
|
|
64
64
|
alert,
|
|
65
65
|
useDialogContext,
|
|
66
66
|
useDialogControl,
|
|
67
|
+
useDialogHeader,
|
|
67
68
|
} from './dialog/index.web';
|
|
68
69
|
export type {
|
|
69
70
|
AlertButton,
|
|
@@ -72,6 +73,7 @@ export type {
|
|
|
72
73
|
DialogActionColor,
|
|
73
74
|
DialogContextProps,
|
|
74
75
|
DialogControlProps,
|
|
76
|
+
DialogHeaderConfig,
|
|
75
77
|
DialogInset,
|
|
76
78
|
DialogPlacement,
|
|
77
79
|
DialogProps,
|
|
@@ -149,7 +151,7 @@ export type { AnimatedCheckProps, AnimatedCheckRef } from './animated-check';
|
|
|
149
151
|
// Form components
|
|
150
152
|
export * from './text-field';
|
|
151
153
|
export * from './segmented-control';
|
|
152
|
-
export {
|
|
154
|
+
export { Search } from './search';
|
|
153
155
|
export { Label } from './label';
|
|
154
156
|
export type { LabelProps } from './label';
|
|
155
157
|
export { Field } from './field';
|
package/src/input-group/types.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface InputGroupAddonProps {
|
|
|
21
21
|
export interface InputGroupProps {
|
|
22
22
|
/**
|
|
23
23
|
* Group children. Place `InputGroup.Addon` elements before/after the input
|
|
24
|
-
* (a Bloom `TextFieldInput`, a `
|
|
24
|
+
* (a Bloom `TextFieldInput`, a `Search`, or any control). The middle
|
|
25
25
|
* child stretches to fill.
|
|
26
26
|
*/
|
|
27
27
|
children: React.ReactNode;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `className` support for React Native core components in Bloom.
|
|
3
|
+
*
|
|
4
|
+
* Bloom styles some primitives with NativeWind/`react-native-css` utility
|
|
5
|
+
* classes (`className="..."`). react-native-css applies them at runtime — on web
|
|
6
|
+
* as real CSS classes emitted by the consumer's Tailwind pipeline (`@source`-
|
|
7
|
+
* scanning Bloom's lib), on native as compiled styles. This ambient augmentation
|
|
8
|
+
* makes `className` type-valid on the RN primitives we set it on.
|
|
9
|
+
*
|
|
10
|
+
* Heritage-free (a plain interface-merge `declare module`, no `extends` / import
|
|
11
|
+
* of RN's own module graph) so it never drags a second `@types/react` copy into
|
|
12
|
+
* the RN 0.85 / React 19 tree — the same hazard the `mergeRefs` note calls out.
|
|
13
|
+
*/
|
|
14
|
+
// `export {}` makes this file a MODULE, so the `declare module` below MERGES
|
|
15
|
+
// with react-native's real types (an augmentation) instead of replacing the
|
|
16
|
+
// module and hiding its exports.
|
|
17
|
+
export {};
|
|
18
|
+
|
|
19
|
+
declare module 'react-native' {
|
|
20
|
+
interface ViewProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
interface TextProps {
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
interface ImageProps {
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
interface PressableProps {
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
interface ScrollViewProps {
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
interface TextInputProps {
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -8,7 +8,10 @@ import { TextField, TextFieldIcon, TextFieldInput, type TextFieldInputProps } fr
|
|
|
8
8
|
import { MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon } from '../icons/MagnifyingGlass';
|
|
9
9
|
import { TimesLarge_Stroke2_Corner0_Rounded as X } from '../icons/Times';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/** Fully-rounded (pill) corner radius — clamps to half the field height. */
|
|
12
|
+
const PILL_RADIUS = 999;
|
|
13
|
+
|
|
14
|
+
type SearchProps = Omit<TextFieldInputProps, 'label'> & {
|
|
12
15
|
label?: TextFieldInputProps['label'];
|
|
13
16
|
/**
|
|
14
17
|
* Called when the user presses the clear (X) button.
|
|
@@ -16,14 +19,14 @@ type SearchInputProps = Omit<TextFieldInputProps, 'label'> & {
|
|
|
16
19
|
onClearText?: () => void;
|
|
17
20
|
};
|
|
18
21
|
|
|
19
|
-
export const
|
|
20
|
-
function
|
|
22
|
+
export const Search = forwardRef<TextInput, SearchProps>(
|
|
23
|
+
function Search({ value, label = 'Search', onClearText, ...rest }, ref) {
|
|
21
24
|
const theme = useTheme();
|
|
22
25
|
const showClear = value != null && value.length > 0;
|
|
23
26
|
|
|
24
27
|
return (
|
|
25
28
|
<View style={[a.w_full, a.relative]}>
|
|
26
|
-
<TextField>
|
|
29
|
+
<TextField radius={PILL_RADIUS}>
|
|
27
30
|
<TextFieldIcon icon={MagnifyingGlassIcon} />
|
|
28
31
|
<TextFieldInput
|
|
29
32
|
inputRef={ref}
|
|
@@ -70,4 +73,4 @@ export const SearchInput = forwardRef<TextInput, SearchInputProps>(
|
|
|
70
73
|
);
|
|
71
74
|
},
|
|
72
75
|
);
|
|
73
|
-
|
|
76
|
+
Search.displayName = 'Search';
|
|
@@ -243,7 +243,11 @@ const styles = StyleSheet.create({
|
|
|
243
243
|
marginBottom: 16,
|
|
244
244
|
},
|
|
245
245
|
groupCard: {
|
|
246
|
-
|
|
246
|
+
// No horizontal self-inset: a `SettingsListGroup` fills its parent's content
|
|
247
|
+
// width, so the single horizontal inset comes from the ONE content padding
|
|
248
|
+
// the screen/Dialog already applies (the same `px-screen-margin` that pads
|
|
249
|
+
// the title/search). Keeping a self-margin here double-inset grouped rows on
|
|
250
|
+
// every screen that also applies that padding.
|
|
247
251
|
borderRadius: 16,
|
|
248
252
|
overflow: 'hidden',
|
|
249
253
|
},
|