@oxyhq/bloom 0.49.0 → 0.50.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.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +5 -0
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/dialog/DialogHeader.js +248 -31
- package/lib/commonjs/dialog/DialogHeader.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/dialog/Dialog.js +5 -0
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/dialog/DialogHeader.js +245 -28
- package/lib/module/dialog/DialogHeader.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +2 -2
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogHeader.d.ts +6 -0
- package/lib/typescript/commonjs/dialog/DialogHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogMorph.d.ts +2 -2
- package/lib/typescript/commonjs/dialog/types.d.ts +67 -0
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +2 -2
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogHeader.d.ts +6 -0
- package/lib/typescript/module/dialog/DialogHeader.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogMorph.d.ts +2 -2
- package/lib/typescript/module/dialog/types.d.ts +67 -0
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/Dialog.test.tsx +77 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +2 -1
- package/src/dialog/Dialog.tsx +6 -1
- package/src/dialog/DialogBottomSheet.tsx +2 -1
- package/src/dialog/DialogHeader.tsx +301 -27
- package/src/dialog/types.ts +81 -0
|
@@ -23,9 +23,20 @@ import Animated, {
|
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
25
|
ChevronLeft_Stroke2_Corner0_Rounded,
|
|
26
|
+
DotGrid3x1_Stroke2_Corner0_Rounded,
|
|
26
27
|
TimesLarge_Stroke2_Corner0_Rounded,
|
|
27
28
|
} from '../icons';
|
|
28
29
|
import { FrostedIconButton } from '../frosted-icon-button';
|
|
30
|
+
import { Button } from '../button';
|
|
31
|
+
import { Search } from '../search';
|
|
32
|
+
import {
|
|
33
|
+
SegmentedControl,
|
|
34
|
+
SegmentedControlItem,
|
|
35
|
+
SegmentedControlItemText,
|
|
36
|
+
} from '../segmented-control';
|
|
37
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
|
|
38
|
+
import { Item } from '../item';
|
|
39
|
+
import { useDialogControl } from './context';
|
|
29
40
|
import { useTheme } from '../theme/use-theme';
|
|
30
41
|
import { H1, Text } from '../typography';
|
|
31
42
|
import type { DialogHeaderConfig } from './types';
|
|
@@ -68,6 +79,17 @@ const HEADER_H_PADDING = 20;
|
|
|
68
79
|
/** Circular icon-button diameter for the default back / close affordances. */
|
|
69
80
|
const NAV_BUTTON_SIZE = 36;
|
|
70
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Trailing icon `actions` shown inline before they collapse into a "more"
|
|
84
|
+
* overflow menu (Material overflow pattern). Kept small so the nav row never
|
|
85
|
+
* crowds the title.
|
|
86
|
+
*/
|
|
87
|
+
export const DIALOG_HEADER_MAX_INLINE_ACTIONS = 3;
|
|
88
|
+
|
|
89
|
+
/** Light content color for `tone: 'onImage'` (legible over any media). */
|
|
90
|
+
const ON_IMAGE_TEXT = '#FFFFFF';
|
|
91
|
+
const ON_IMAGE_SUBTEXT = 'rgba(255,255,255,0.82)';
|
|
92
|
+
|
|
71
93
|
// --- Runtime override store (child screen → nav bar / large title) ----------
|
|
72
94
|
//
|
|
73
95
|
// The nav bar overlay and the in-content large title live in DIFFERENT subtrees
|
|
@@ -95,7 +117,15 @@ function configsEqual(a: DialogHeaderConfig | null, b: DialogHeaderConfig | null
|
|
|
95
117
|
a.left === b.left &&
|
|
96
118
|
a.right === b.right &&
|
|
97
119
|
a.onBack === b.onBack &&
|
|
98
|
-
a.showClose === b.showClose
|
|
120
|
+
a.showClose === b.showClose &&
|
|
121
|
+
// Rich fields — object fields compared by identity (callers memoize them);
|
|
122
|
+
// `tone` is a plain value.
|
|
123
|
+
a.primaryAction === b.primaryAction &&
|
|
124
|
+
a.actions === b.actions &&
|
|
125
|
+
a.search === b.search &&
|
|
126
|
+
a.segments === b.segments &&
|
|
127
|
+
a.tone === b.tone &&
|
|
128
|
+
a.progress === b.progress
|
|
99
129
|
);
|
|
100
130
|
}
|
|
101
131
|
|
|
@@ -189,6 +219,12 @@ export function useDialogHeader(config: DialogHeaderConfig | null | undefined):
|
|
|
189
219
|
config?.right,
|
|
190
220
|
config?.onBack,
|
|
191
221
|
config?.showClose,
|
|
222
|
+
config?.primaryAction,
|
|
223
|
+
config?.actions,
|
|
224
|
+
config?.search,
|
|
225
|
+
config?.segments,
|
|
226
|
+
config?.tone,
|
|
227
|
+
config?.progress,
|
|
192
228
|
]);
|
|
193
229
|
}
|
|
194
230
|
|
|
@@ -201,6 +237,125 @@ function useMergedHeaderConfig(
|
|
|
201
237
|
return useMemo(() => (override ? { ...base, ...override } : base), [base, override]);
|
|
202
238
|
}
|
|
203
239
|
|
|
240
|
+
// --- Rich header sub-parts --------------------------------------------------
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The "more" overflow: a Bloom `Popover` that DROPS from the ⋯ trigger — anchored
|
|
244
|
+
* under the button on web (measured trigger + `bottom-end` placement) and a bottom
|
|
245
|
+
* sheet on native, matching how top libraries present a toolbar overflow (Radix
|
|
246
|
+
* DropdownMenu / Material Menu on desktop, an action sheet on mobile). Items are
|
|
247
|
+
* Bloom `Item` rows; pressing one closes the popover before firing.
|
|
248
|
+
*/
|
|
249
|
+
function HeaderOverflowMenu({
|
|
250
|
+
items,
|
|
251
|
+
onImage,
|
|
252
|
+
}: {
|
|
253
|
+
items: NonNullable<DialogHeaderConfig['actions']>;
|
|
254
|
+
onImage: boolean;
|
|
255
|
+
}): React.ReactElement {
|
|
256
|
+
// Own the control so an item press can close the popover before acting.
|
|
257
|
+
const control = useDialogControl();
|
|
258
|
+
return (
|
|
259
|
+
<Popover control={control}>
|
|
260
|
+
<PopoverTrigger label="More">
|
|
261
|
+
{({ props }) => (
|
|
262
|
+
<FrostedIconButton
|
|
263
|
+
size="sm"
|
|
264
|
+
onPress={props.onPress}
|
|
265
|
+
accessibilityLabel={props.accessibilityLabel}
|
|
266
|
+
icon={
|
|
267
|
+
<DotGrid3x1_Stroke2_Corner0_Rounded
|
|
268
|
+
size="md"
|
|
269
|
+
fill={onImage ? ON_IMAGE_TEXT : undefined}
|
|
270
|
+
/>
|
|
271
|
+
}
|
|
272
|
+
/>
|
|
273
|
+
)}
|
|
274
|
+
</PopoverTrigger>
|
|
275
|
+
<PopoverContent label="More actions" placement="bottom-end">
|
|
276
|
+
{items.map((action) => (
|
|
277
|
+
<Item
|
|
278
|
+
key={action.accessibilityLabel}
|
|
279
|
+
title={action.accessibilityLabel}
|
|
280
|
+
leading={action.icon}
|
|
281
|
+
density="compact"
|
|
282
|
+
disabled={action.disabled}
|
|
283
|
+
onPress={() => control.close(() => action.onPress())}
|
|
284
|
+
/>
|
|
285
|
+
))}
|
|
286
|
+
</PopoverContent>
|
|
287
|
+
</Popover>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Trailing icon `actions`: the first {@link DIALOG_HEADER_MAX_INLINE_ACTIONS}
|
|
293
|
+
* render inline as frosted circles; any surplus collapses into a "more" overflow
|
|
294
|
+
* that drops from the ⋯ ({@link HeaderOverflowMenu}) — never hand-rolled.
|
|
295
|
+
*/
|
|
296
|
+
function HeaderTrailingActions({
|
|
297
|
+
actions,
|
|
298
|
+
onImage,
|
|
299
|
+
}: {
|
|
300
|
+
actions: NonNullable<DialogHeaderConfig['actions']>;
|
|
301
|
+
onImage: boolean;
|
|
302
|
+
}): React.ReactElement | null {
|
|
303
|
+
if (actions.length === 0) return null;
|
|
304
|
+
const overflows = actions.length > DIALOG_HEADER_MAX_INLINE_ACTIONS;
|
|
305
|
+
// Keep room for the "more" trigger when collapsing.
|
|
306
|
+
const inline = overflows ? actions.slice(0, DIALOG_HEADER_MAX_INLINE_ACTIONS - 1) : actions;
|
|
307
|
+
const overflow = overflows ? actions.slice(DIALOG_HEADER_MAX_INLINE_ACTIONS - 1) : [];
|
|
308
|
+
return (
|
|
309
|
+
<>
|
|
310
|
+
{inline.map((action) => (
|
|
311
|
+
<FrostedIconButton
|
|
312
|
+
key={action.accessibilityLabel}
|
|
313
|
+
size="sm"
|
|
314
|
+
icon={action.icon}
|
|
315
|
+
onPress={action.onPress}
|
|
316
|
+
disabled={action.disabled}
|
|
317
|
+
accessibilityLabel={action.accessibilityLabel}
|
|
318
|
+
/>
|
|
319
|
+
))}
|
|
320
|
+
{overflow.length > 0 ? <HeaderOverflowMenu items={overflow} onImage={onImage} /> : null}
|
|
321
|
+
</>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* A thin wizard step/progress indicator for the large-title zone. `step` is
|
|
327
|
+
* 1-based; the bar fills `step / total`, clamped to [0, 1].
|
|
328
|
+
*/
|
|
329
|
+
function HeaderProgressBar({
|
|
330
|
+
step,
|
|
331
|
+
total,
|
|
332
|
+
onImage,
|
|
333
|
+
}: {
|
|
334
|
+
step: number;
|
|
335
|
+
total: number;
|
|
336
|
+
onImage: boolean;
|
|
337
|
+
}): React.ReactElement {
|
|
338
|
+
const theme = useTheme();
|
|
339
|
+
const fraction = total > 0 ? Math.min(1, Math.max(0, step / total)) : 0;
|
|
340
|
+
return (
|
|
341
|
+
<View
|
|
342
|
+
accessibilityRole="progressbar"
|
|
343
|
+
accessibilityValue={{ min: 0, max: total, now: step }}
|
|
344
|
+
style={[
|
|
345
|
+
styles.progressTrack,
|
|
346
|
+
{ backgroundColor: onImage ? 'rgba(255,255,255,0.25)' : theme.colors.border },
|
|
347
|
+
]}
|
|
348
|
+
>
|
|
349
|
+
<View
|
|
350
|
+
style={[
|
|
351
|
+
styles.progressFill,
|
|
352
|
+
{ width: `${Math.round(fraction * 100)}%`, backgroundColor: theme.colors.primary },
|
|
353
|
+
]}
|
|
354
|
+
/>
|
|
355
|
+
</View>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
204
359
|
// --- Nav bar overlay --------------------------------------------------------
|
|
205
360
|
|
|
206
361
|
/**
|
|
@@ -260,24 +415,62 @@ export const DialogNavHeader = memo(function DialogNavHeader({
|
|
|
260
415
|
// reads MUST be in the deps array or it freezes on the first frame.
|
|
261
416
|
}, [scrollY, largeTitleHeight, hasLargeTitle]);
|
|
262
417
|
|
|
418
|
+
// Over media (`tone: 'onImage'`) the scrim strengthens and the default title +
|
|
419
|
+
// icon buttons flip to light content so they stay legible.
|
|
420
|
+
const onImage = config.tone === 'onImage';
|
|
421
|
+
const iconFill = onImage ? ON_IMAGE_TEXT : undefined;
|
|
422
|
+
const titleColor = onImage ? ON_IMAGE_TEXT : theme.colors.text;
|
|
423
|
+
const subtitleColor = onImage ? ON_IMAGE_SUBTEXT : theme.colors.textSecondary;
|
|
424
|
+
|
|
425
|
+
const closeButton = (
|
|
426
|
+
<FrostedIconButton
|
|
427
|
+
onPress={onDismiss}
|
|
428
|
+
accessibilityLabel="Close"
|
|
429
|
+
icon={<TimesLarge_Stroke2_Corner0_Rounded size="md" fill={iconFill} />}
|
|
430
|
+
/>
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Trailing edge: a custom `right` wins; else the rich trailing (icon actions +
|
|
434
|
+
// the single primary CTA); else the default close affordance.
|
|
435
|
+
const hasRichTrailing = !config.right && !!(config.actions?.length || config.primaryAction);
|
|
436
|
+
|
|
437
|
+
const right = config.right ?? (
|
|
438
|
+
hasRichTrailing ? (
|
|
439
|
+
<View style={styles.trailing}>
|
|
440
|
+
{config.actions?.length ? (
|
|
441
|
+
<HeaderTrailingActions actions={config.actions} onImage={onImage} />
|
|
442
|
+
) : null}
|
|
443
|
+
{config.primaryAction ? (
|
|
444
|
+
<Button
|
|
445
|
+
variant="primary"
|
|
446
|
+
size="small"
|
|
447
|
+
onPress={config.primaryAction.onPress}
|
|
448
|
+
disabled={config.primaryAction.disabled || config.primaryAction.loading}
|
|
449
|
+
loading={config.primaryAction.loading}
|
|
450
|
+
accessibilityLabel={config.primaryAction.label}
|
|
451
|
+
>
|
|
452
|
+
{config.primaryAction.label}
|
|
453
|
+
</Button>
|
|
454
|
+
) : null}
|
|
455
|
+
</View>
|
|
456
|
+
) : config.showClose !== false ? (
|
|
457
|
+
closeButton
|
|
458
|
+
) : null
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// Leading edge: a custom `left` wins; else the back button; else — when the
|
|
462
|
+
// trailing edge is taken by the rich CTA and there is no back — the close
|
|
463
|
+
// affordance moves here so there is always exactly one dismiss control.
|
|
263
464
|
const left =
|
|
264
465
|
config.left ??
|
|
265
466
|
(config.onBack ? (
|
|
266
467
|
<FrostedIconButton
|
|
267
468
|
onPress={config.onBack}
|
|
268
469
|
accessibilityLabel="Go back"
|
|
269
|
-
icon={<ChevronLeft_Stroke2_Corner0_Rounded size="md" />}
|
|
270
|
-
/>
|
|
271
|
-
) : null);
|
|
272
|
-
|
|
273
|
-
const right =
|
|
274
|
-
config.right ??
|
|
275
|
-
(config.showClose !== false ? (
|
|
276
|
-
<FrostedIconButton
|
|
277
|
-
onPress={onDismiss}
|
|
278
|
-
accessibilityLabel="Close"
|
|
279
|
-
icon={<TimesLarge_Stroke2_Corner0_Rounded size="md" />}
|
|
470
|
+
icon={<ChevronLeft_Stroke2_Corner0_Rounded size="md" fill={iconFill} />}
|
|
280
471
|
/>
|
|
472
|
+
) : hasRichTrailing && config.showClose !== false ? (
|
|
473
|
+
closeButton
|
|
281
474
|
) : null);
|
|
282
475
|
|
|
283
476
|
return (
|
|
@@ -286,10 +479,15 @@ export const DialogNavHeader = memo(function DialogNavHeader({
|
|
|
286
479
|
style={[styles.overlay, style]}
|
|
287
480
|
>
|
|
288
481
|
{/* Opaque (surface bg) at the top → transparent at the bottom, so scrolled
|
|
289
|
-
content fades out under the bar.
|
|
482
|
+
content fades out under the bar. `onImage` swaps to a dark scrim so the
|
|
483
|
+
chrome reads over media. Pure NativeWind — no SVG dependency. */}
|
|
290
484
|
<View
|
|
291
485
|
pointerEvents="none"
|
|
292
|
-
className=
|
|
486
|
+
className={
|
|
487
|
+
onImage
|
|
488
|
+
? 'bg-gradient-to-b from-black/60 to-transparent'
|
|
489
|
+
: 'bg-gradient-to-b from-bg to-transparent'
|
|
490
|
+
}
|
|
293
491
|
style={[StyleSheet.absoluteFill, { height: DIALOG_HEADER_OVERLAY_HEIGHT }]}
|
|
294
492
|
/>
|
|
295
493
|
<View pointerEvents="box-none" style={styles.navRow}>
|
|
@@ -301,7 +499,7 @@ export const DialogNavHeader = memo(function DialogNavHeader({
|
|
|
301
499
|
>
|
|
302
500
|
{config.titleContent ?? null}
|
|
303
501
|
{!config.titleContent && config.title ? (
|
|
304
|
-
<Text numberOfLines={1} style={[styles.smallTitle, { color:
|
|
502
|
+
<Text numberOfLines={1} style={[styles.smallTitle, { color: titleColor }]}>
|
|
305
503
|
{config.title}
|
|
306
504
|
</Text>
|
|
307
505
|
) : null}
|
|
@@ -310,10 +508,7 @@ export const DialogNavHeader = memo(function DialogNavHeader({
|
|
|
310
508
|
screen keeps a clean single-line collapsed title, and a branded
|
|
311
509
|
`titleContent` bar carries no supporting copy at all. */}
|
|
312
510
|
{!hasLargeTitle && !config.titleContent && config.subtitle ? (
|
|
313
|
-
<Text
|
|
314
|
-
numberOfLines={1}
|
|
315
|
-
style={[styles.smallSubtitle, { color: theme.colors.textSecondary }]}
|
|
316
|
-
>
|
|
511
|
+
<Text numberOfLines={1} style={[styles.smallSubtitle, { color: subtitleColor }]}>
|
|
317
512
|
{config.subtitle}
|
|
318
513
|
</Text>
|
|
319
514
|
) : null}
|
|
@@ -346,25 +541,83 @@ export const DialogLargeTitle = memo(function DialogLargeTitle({
|
|
|
346
541
|
// A branded `titleContent` lives in the bar and never collapses, so the
|
|
347
542
|
// surface starts flush under the bar with no large title above it.
|
|
348
543
|
const hasLargeTitle = !config.titleContent && (config.largeTitle ?? true) && !!config.title;
|
|
544
|
+
// The large-title zone also hosts search / segments / progress. These require a
|
|
545
|
+
// scrolling surface (they live here, not in the fixed bar) and collapse with the
|
|
546
|
+
// title on scroll (iOS `.searchable` style).
|
|
547
|
+
const hasExtras = !!(config.search || config.segments || config.progress);
|
|
548
|
+
const onImage = config.tone === 'onImage';
|
|
349
549
|
|
|
350
550
|
const onLayout = (e: LayoutChangeEvent) => {
|
|
351
551
|
largeTitleHeight.value = e.nativeEvent.layout.height;
|
|
352
552
|
};
|
|
353
553
|
|
|
354
|
-
if (!hasLargeTitle) {
|
|
355
|
-
//
|
|
356
|
-
|
|
554
|
+
if (!hasLargeTitle && !hasExtras) {
|
|
555
|
+
// Pure content (no header chrome to render in-flow). Under `tone: 'onImage'`
|
|
556
|
+
// the content is media (a banner / photo canvas), so it slides UP under the
|
|
557
|
+
// translucent nav bar — the bar FLOATS over the media, immersive at rest (the
|
|
558
|
+
// standard iOS large-title-over-photo / Material collapsing-toolbar pattern).
|
|
559
|
+
// Default tone keeps the nav-bar-height inset so content clears the bar.
|
|
560
|
+
return onImage ? null : <View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />;
|
|
357
561
|
}
|
|
358
562
|
|
|
359
563
|
return (
|
|
360
564
|
<>
|
|
565
|
+
{/* Clear the whole gradient overlay so the block starts below the bar. */}
|
|
361
566
|
<View style={{ height: DIALOG_HEADER_CONTENT_TOP }} />
|
|
362
567
|
<View onLayout={onLayout} testID="dialog-large-title" style={styles.largeTitleBlock}>
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
568
|
+
{hasLargeTitle ? (
|
|
569
|
+
<>
|
|
570
|
+
<H1 style={[styles.largeTitle, { color: onImage ? ON_IMAGE_TEXT : theme.colors.text }]}>
|
|
571
|
+
{config.title}
|
|
572
|
+
</H1>
|
|
573
|
+
{config.subtitle ? (
|
|
574
|
+
<Text
|
|
575
|
+
style={[
|
|
576
|
+
styles.largeSubtitle,
|
|
577
|
+
{ color: onImage ? ON_IMAGE_SUBTEXT : theme.colors.textSecondary },
|
|
578
|
+
]}
|
|
579
|
+
>
|
|
580
|
+
{config.subtitle}
|
|
581
|
+
</Text>
|
|
582
|
+
) : null}
|
|
583
|
+
</>
|
|
584
|
+
) : null}
|
|
585
|
+
{config.search ? (
|
|
586
|
+
<View style={hasLargeTitle ? styles.extraRow : undefined}>
|
|
587
|
+
<Search
|
|
588
|
+
value={config.search.value}
|
|
589
|
+
onChangeText={config.search.onChangeText}
|
|
590
|
+
label={config.search.placeholder ?? 'Search'}
|
|
591
|
+
onSubmitEditing={config.search.onSubmit}
|
|
592
|
+
onClearText={() => config.search?.onChangeText('')}
|
|
593
|
+
/>
|
|
594
|
+
</View>
|
|
595
|
+
) : null}
|
|
596
|
+
{config.segments ? (
|
|
597
|
+
<View style={hasLargeTitle || config.search ? styles.extraRow : undefined}>
|
|
598
|
+
<SegmentedControl
|
|
599
|
+
label={config.title ?? 'View'}
|
|
600
|
+
type="tabs"
|
|
601
|
+
size="small"
|
|
602
|
+
value={config.segments.value}
|
|
603
|
+
onChange={config.segments.onChange}
|
|
604
|
+
>
|
|
605
|
+
{config.segments.items.map((item) => (
|
|
606
|
+
<SegmentedControlItem key={item.key} value={item.key}>
|
|
607
|
+
<SegmentedControlItemText>{item.label}</SegmentedControlItemText>
|
|
608
|
+
</SegmentedControlItem>
|
|
609
|
+
))}
|
|
610
|
+
</SegmentedControl>
|
|
611
|
+
</View>
|
|
612
|
+
) : null}
|
|
613
|
+
{config.progress ? (
|
|
614
|
+
<View style={hasLargeTitle || config.search || config.segments ? styles.extraRow : undefined}>
|
|
615
|
+
<HeaderProgressBar
|
|
616
|
+
step={config.progress.step}
|
|
617
|
+
total={config.progress.total}
|
|
618
|
+
onImage={onImage}
|
|
619
|
+
/>
|
|
620
|
+
</View>
|
|
368
621
|
) : null}
|
|
369
622
|
</View>
|
|
370
623
|
</>
|
|
@@ -394,6 +647,12 @@ const styles = StyleSheet.create({
|
|
|
394
647
|
sideRight: {
|
|
395
648
|
justifyContent: 'flex-end',
|
|
396
649
|
},
|
|
650
|
+
/** Trailing row: icon actions + the primary CTA, right-aligned. */
|
|
651
|
+
trailing: {
|
|
652
|
+
flexDirection: 'row',
|
|
653
|
+
alignItems: 'center',
|
|
654
|
+
gap: 8,
|
|
655
|
+
},
|
|
397
656
|
centerTitle: {
|
|
398
657
|
flex: 1,
|
|
399
658
|
alignItems: 'center',
|
|
@@ -426,4 +685,19 @@ const styles = StyleSheet.create({
|
|
|
426
685
|
lineHeight: 22,
|
|
427
686
|
marginTop: 4,
|
|
428
687
|
},
|
|
688
|
+
/** Spacing between the large title and each large-title-zone extra. */
|
|
689
|
+
extraRow: {
|
|
690
|
+
marginTop: 12,
|
|
691
|
+
},
|
|
692
|
+
/** Thin wizard progress bar in the large-title zone. */
|
|
693
|
+
progressTrack: {
|
|
694
|
+
height: 4,
|
|
695
|
+
width: '100%',
|
|
696
|
+
borderRadius: 2,
|
|
697
|
+
overflow: 'hidden',
|
|
698
|
+
},
|
|
699
|
+
progressFill: {
|
|
700
|
+
height: '100%',
|
|
701
|
+
borderRadius: 2,
|
|
702
|
+
},
|
|
429
703
|
});
|
package/src/dialog/types.ts
CHANGED
|
@@ -53,6 +53,87 @@ export interface DialogHeaderConfig {
|
|
|
53
53
|
* affordance entirely (e.g. a blocking surface).
|
|
54
54
|
*/
|
|
55
55
|
showClose?: boolean;
|
|
56
|
+
|
|
57
|
+
// -------------------------------------------------------------------------
|
|
58
|
+
// Rich navigation-header fields (all OPTIONAL + backward-compatible).
|
|
59
|
+
//
|
|
60
|
+
// Modeled on iOS `UINavigationBar` / SwiftUI `.toolbar` + Material 3
|
|
61
|
+
// `TopAppBar`. Nav-row trailing = [actions…] + [primaryAction]; the
|
|
62
|
+
// collapsing large-title zone stacks title → search → segments → progress.
|
|
63
|
+
// A screen that sets none of these renders byte-for-byte as before. Object
|
|
64
|
+
// fields are compared by identity — memoize them so the header does not
|
|
65
|
+
// thrash (same contract as `left`/`right`).
|
|
66
|
+
// -------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The ONE trailing call-to-action (Upload / Use photo / Save / Done) — a
|
|
70
|
+
* proper Bloom `Button` rendered at the trailing edge of the nav row, AFTER
|
|
71
|
+
* any `actions`. Standardizes what screens used to cram into `right`; supports
|
|
72
|
+
* `disabled` and a `loading` spinner (async submit). When set (and no explicit
|
|
73
|
+
* `right`), the default close affordance moves to the leading edge if the
|
|
74
|
+
* screen has no `onBack`, so there is always exactly one dismiss control.
|
|
75
|
+
*/
|
|
76
|
+
primaryAction?: {
|
|
77
|
+
label: string;
|
|
78
|
+
onPress: () => void;
|
|
79
|
+
disabled?: boolean;
|
|
80
|
+
loading?: boolean;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Trailing icon buttons (share / more / sort), rendered as frosted circles
|
|
85
|
+
* BEFORE `primaryAction`. Past {@link DIALOG_HEADER_MAX_INLINE_ACTIONS} the
|
|
86
|
+
* surplus collapses into a "more" overflow menu (Material pattern) built on
|
|
87
|
+
* Bloom's `Menu` — never hand-rolled. Each item's `accessibilityLabel` is also
|
|
88
|
+
* its overflow-menu row text.
|
|
89
|
+
*/
|
|
90
|
+
actions?: Array<{
|
|
91
|
+
icon: ReactNode;
|
|
92
|
+
accessibilityLabel: string;
|
|
93
|
+
onPress: () => void;
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
}>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A search field in the collapsing large-title zone (iOS `.searchable` style:
|
|
99
|
+
* sits under the title and scrolls away with it). Requires a scrolling surface
|
|
100
|
+
* (`scrollable !== false`) — it lives in the same in-content block as the large
|
|
101
|
+
* title. Built on Bloom's `Search`.
|
|
102
|
+
*/
|
|
103
|
+
search?: {
|
|
104
|
+
value: string;
|
|
105
|
+
onChangeText: (text: string) => void;
|
|
106
|
+
placeholder?: string;
|
|
107
|
+
onSubmit?: () => void;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A segmented control row UNDER the title (Material secondary row) for view
|
|
112
|
+
* switching. Requires a scrolling surface (lives in the large-title zone).
|
|
113
|
+
* Built on Bloom's `SegmentedControl` (`type="tabs"`).
|
|
114
|
+
*/
|
|
115
|
+
segments?: {
|
|
116
|
+
items: Array<{ key: string; label: string }>;
|
|
117
|
+
value: string;
|
|
118
|
+
onChange: (key: string) => void;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Over a banner / dark media canvas, `'onImage'` strengthens the gradient
|
|
123
|
+
* scrim and forces light-content title + default icon buttons so the chrome
|
|
124
|
+
* stays legible over the media. `'default'` (the default) is unchanged.
|
|
125
|
+
*/
|
|
126
|
+
tone?: 'default' | 'onImage';
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A thin step/progress indicator for wizards, rendered in the large-title zone
|
|
130
|
+
* below any title/search/segments. `step` is 1-based; the bar fills
|
|
131
|
+
* `step / total`.
|
|
132
|
+
*/
|
|
133
|
+
progress?: {
|
|
134
|
+
step: number;
|
|
135
|
+
total: number;
|
|
136
|
+
};
|
|
56
137
|
}
|
|
57
138
|
|
|
58
139
|
/**
|