@oxy.so/bloom 2.12.4 → 3.0.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/README.md +4 -4
- package/docs/button-group.mdx +46 -4
- package/docs/composition.mdx +159 -0
- package/docs/field.mdx +39 -1
- package/docs/glass.mdx +61 -0
- package/docs/layout.mdx +72 -4
- package/docs/migrating-to-3.0.mdx +169 -0
- package/docs/page-header.mdx +237 -36
- package/lib/commonjs/button/Button.js +2 -0
- package/lib/commonjs/button/Button.js.map +1 -1
- package/lib/commonjs/button-group/ButtonGroup.js +124 -19
- package/lib/commonjs/button-group/ButtonGroup.js.map +1 -1
- package/lib/commonjs/control-surface/ControlSurface.js +46 -0
- package/lib/commonjs/control-surface/ControlSurface.js.map +1 -0
- package/lib/commonjs/control-surface/context.js +84 -0
- package/lib/commonjs/control-surface/context.js.map +1 -0
- package/lib/commonjs/control-surface/index.js +26 -0
- package/lib/commonjs/control-surface/index.js.map +1 -0
- package/lib/commonjs/control-surface/types.js +6 -0
- package/lib/commonjs/control-surface/types.js.map +1 -0
- package/lib/commonjs/field/Field.js +71 -26
- package/lib/commonjs/field/Field.js.map +1 -1
- package/lib/commonjs/field/context.js +80 -0
- package/lib/commonjs/field/context.js.map +1 -0
- package/lib/commonjs/field/index.js +7 -0
- package/lib/commonjs/field/index.js.map +1 -1
- package/lib/commonjs/floating/TriggerSlot.js +32 -0
- package/lib/commonjs/floating/TriggerSlot.js.map +1 -1
- package/lib/commonjs/glass/GlassIsland.js +107 -0
- package/lib/commonjs/glass/GlassIsland.js.map +1 -0
- package/lib/commonjs/glass/GlassSurface.js +4 -1
- package/lib/commonjs/glass/GlassSurface.js.map +1 -1
- package/lib/commonjs/glass/index.js +7 -0
- package/lib/commonjs/glass/index.js.map +1 -1
- package/lib/commonjs/index.js +564 -517
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +564 -517
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/label/Label.js +4 -1
- package/lib/commonjs/label/Label.js.map +1 -1
- package/lib/commonjs/layout/bottom-edge.js +3 -42
- package/lib/commonjs/layout/bottom-edge.js.map +1 -1
- package/lib/commonjs/layout/edge-store.js +65 -0
- package/lib/commonjs/layout/edge-store.js.map +1 -0
- package/lib/commonjs/layout/index.js +32 -0
- package/lib/commonjs/layout/index.js.map +1 -1
- package/lib/commonjs/layout/scroll-offset.js +83 -0
- package/lib/commonjs/layout/scroll-offset.js.map +1 -0
- package/lib/commonjs/layout/top-edge.js +109 -0
- package/lib/commonjs/layout/top-edge.js.map +1 -0
- package/lib/commonjs/page-header/EdgeScrim.js +136 -0
- package/lib/commonjs/page-header/EdgeScrim.js.map +1 -0
- package/lib/commonjs/page-header/PageHeader.js +236 -61
- package/lib/commonjs/page-header/PageHeader.js.map +1 -1
- package/lib/commonjs/provider/BloomProvider.js +5 -2
- package/lib/commonjs/provider/BloomProvider.js.map +1 -1
- package/lib/commonjs/social-button/OxyMark.js +2 -2
- package/lib/commonjs/social-button/OxyMark.js.map +1 -1
- package/lib/commonjs/social-button/SocialButton.js +2 -2
- package/lib/commonjs/social-button/SocialButton.js.map +1 -1
- package/lib/commonjs/styles/svg-id.js +29 -0
- package/lib/commonjs/styles/svg-id.js.map +1 -0
- package/lib/commonjs/text-field/TextField.js +39 -6
- package/lib/commonjs/text-field/TextField.js.map +1 -1
- package/lib/commonjs/theme/glass-colors.js +81 -1
- package/lib/commonjs/theme/glass-colors.js.map +1 -1
- package/lib/module/button/Button.js +2 -0
- package/lib/module/button/Button.js.map +1 -1
- package/lib/module/button-group/ButtonGroup.js +124 -19
- package/lib/module/button-group/ButtonGroup.js.map +1 -1
- package/lib/module/control-surface/ControlSurface.js +41 -0
- package/lib/module/control-surface/ControlSurface.js.map +1 -0
- package/lib/module/control-surface/context.js +77 -0
- package/lib/module/control-surface/context.js.map +1 -0
- package/lib/module/control-surface/index.js +5 -0
- package/lib/module/control-surface/index.js.map +1 -0
- package/lib/module/control-surface/types.js +4 -0
- package/lib/module/control-surface/types.js.map +1 -0
- package/lib/module/field/Field.js +72 -27
- package/lib/module/field/Field.js.map +1 -1
- package/lib/module/field/context.js +74 -0
- package/lib/module/field/context.js.map +1 -0
- package/lib/module/field/index.js +1 -0
- package/lib/module/field/index.js.map +1 -1
- package/lib/module/floating/TriggerSlot.js +33 -1
- package/lib/module/floating/TriggerSlot.js.map +1 -1
- package/lib/module/glass/GlassIsland.js +102 -0
- package/lib/module/glass/GlassIsland.js.map +1 -0
- package/lib/module/glass/GlassSurface.js +5 -2
- package/lib/module/glass/GlassSurface.js.map +1 -1
- package/lib/module/glass/index.js +1 -0
- package/lib/module/glass/index.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/label/Label.js +4 -1
- package/lib/module/label/Label.js.map +1 -1
- package/lib/module/layout/bottom-edge.js +2 -41
- package/lib/module/layout/bottom-edge.js.map +1 -1
- package/lib/module/layout/edge-store.js +58 -0
- package/lib/module/layout/edge-store.js.map +1 -0
- package/lib/module/layout/index.js +8 -3
- package/lib/module/layout/index.js.map +1 -1
- package/lib/module/layout/scroll-offset.js +77 -0
- package/lib/module/layout/scroll-offset.js.map +1 -0
- package/lib/module/layout/top-edge.js +102 -0
- package/lib/module/layout/top-edge.js.map +1 -0
- package/lib/module/page-header/EdgeScrim.js +132 -0
- package/lib/module/page-header/EdgeScrim.js.map +1 -0
- package/lib/module/page-header/PageHeader.js +233 -58
- package/lib/module/page-header/PageHeader.js.map +1 -1
- package/lib/module/provider/BloomProvider.js +5 -2
- package/lib/module/provider/BloomProvider.js.map +1 -1
- package/lib/module/social-button/OxyMark.js +1 -1
- package/lib/module/social-button/OxyMark.js.map +1 -1
- package/lib/module/social-button/SocialButton.js +1 -1
- package/lib/module/social-button/SocialButton.js.map +1 -1
- package/lib/module/styles/svg-id.js +26 -0
- package/lib/module/styles/svg-id.js.map +1 -0
- package/lib/module/text-field/TextField.js +39 -6
- package/lib/module/text-field/TextField.js.map +1 -1
- package/lib/module/theme/glass-colors.js +79 -0
- package/lib/module/theme/glass-colors.js.map +1 -1
- package/lib/typescript/commonjs/button/Button.d.ts.map +1 -1
- package/lib/typescript/commonjs/button-group/ButtonGroup.d.ts.map +1 -1
- package/lib/typescript/commonjs/button-group/types.d.ts +61 -5
- package/lib/typescript/commonjs/button-group/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/control-surface/ControlSurface.d.ts +4 -0
- package/lib/typescript/commonjs/control-surface/ControlSurface.d.ts.map +1 -0
- package/lib/typescript/commonjs/control-surface/context.d.ts +21 -0
- package/lib/typescript/commonjs/control-surface/context.d.ts.map +1 -0
- package/lib/typescript/commonjs/control-surface/index.d.ts +4 -0
- package/lib/typescript/commonjs/control-surface/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/control-surface/types.d.ts +37 -0
- package/lib/typescript/commonjs/control-surface/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/field/Field.d.ts +1 -1
- package/lib/typescript/commonjs/field/Field.d.ts.map +1 -1
- package/lib/typescript/commonjs/field/context.d.ts +55 -0
- package/lib/typescript/commonjs/field/context.d.ts.map +1 -0
- package/lib/typescript/commonjs/field/index.d.ts +2 -0
- package/lib/typescript/commonjs/field/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/field/types.d.ts +22 -3
- package/lib/typescript/commonjs/field/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/floating/TriggerSlot.d.ts.map +1 -1
- package/lib/typescript/commonjs/glass/GlassIsland.d.ts +4 -0
- package/lib/typescript/commonjs/glass/GlassIsland.d.ts.map +1 -0
- package/lib/typescript/commonjs/glass/GlassSurface.d.ts.map +1 -1
- package/lib/typescript/commonjs/glass/index.d.ts +2 -1
- package/lib/typescript/commonjs/glass/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/glass/types.d.ts +43 -0
- package/lib/typescript/commonjs/glass/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/label/Label.d.ts.map +1 -1
- package/lib/typescript/commonjs/label/types.d.ts +7 -1
- package/lib/typescript/commonjs/label/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/bottom-edge.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/edge-store.d.ts +31 -0
- package/lib/typescript/commonjs/layout/edge-store.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/index.d.ts +8 -3
- package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/scroll-offset.d.ts +16 -0
- package/lib/typescript/commonjs/layout/scroll-offset.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/top-edge.d.ts +77 -0
- package/lib/typescript/commonjs/layout/top-edge.d.ts.map +1 -0
- package/lib/typescript/commonjs/page-header/EdgeScrim.d.ts +95 -0
- package/lib/typescript/commonjs/page-header/EdgeScrim.d.ts.map +1 -0
- package/lib/typescript/commonjs/page-header/PageHeader.d.ts +1 -1
- package/lib/typescript/commonjs/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-header/index.d.ts +1 -1
- package/lib/typescript/commonjs/page-header/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-header/types.d.ts +97 -10
- package/lib/typescript/commonjs/page-header/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/provider/BloomProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/svg-id.d.ts +20 -0
- package/lib/typescript/commonjs/styles/svg-id.d.ts.map +1 -0
- package/lib/typescript/commonjs/text-field/TextField.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/glass-colors.d.ts +67 -0
- package/lib/typescript/commonjs/theme/glass-colors.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.d.ts.map +1 -1
- package/lib/typescript/module/button-group/ButtonGroup.d.ts.map +1 -1
- package/lib/typescript/module/button-group/types.d.ts +61 -5
- package/lib/typescript/module/button-group/types.d.ts.map +1 -1
- package/lib/typescript/module/control-surface/ControlSurface.d.ts +4 -0
- package/lib/typescript/module/control-surface/ControlSurface.d.ts.map +1 -0
- package/lib/typescript/module/control-surface/context.d.ts +21 -0
- package/lib/typescript/module/control-surface/context.d.ts.map +1 -0
- package/lib/typescript/module/control-surface/index.d.ts +4 -0
- package/lib/typescript/module/control-surface/index.d.ts.map +1 -0
- package/lib/typescript/module/control-surface/types.d.ts +37 -0
- package/lib/typescript/module/control-surface/types.d.ts.map +1 -0
- package/lib/typescript/module/field/Field.d.ts +1 -1
- package/lib/typescript/module/field/Field.d.ts.map +1 -1
- package/lib/typescript/module/field/context.d.ts +55 -0
- package/lib/typescript/module/field/context.d.ts.map +1 -0
- package/lib/typescript/module/field/index.d.ts +2 -0
- package/lib/typescript/module/field/index.d.ts.map +1 -1
- package/lib/typescript/module/field/types.d.ts +22 -3
- package/lib/typescript/module/field/types.d.ts.map +1 -1
- package/lib/typescript/module/floating/TriggerSlot.d.ts.map +1 -1
- package/lib/typescript/module/glass/GlassIsland.d.ts +4 -0
- package/lib/typescript/module/glass/GlassIsland.d.ts.map +1 -0
- package/lib/typescript/module/glass/GlassSurface.d.ts.map +1 -1
- package/lib/typescript/module/glass/index.d.ts +2 -1
- package/lib/typescript/module/glass/index.d.ts.map +1 -1
- package/lib/typescript/module/glass/types.d.ts +43 -0
- package/lib/typescript/module/glass/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/label/Label.d.ts.map +1 -1
- package/lib/typescript/module/label/types.d.ts +7 -1
- package/lib/typescript/module/label/types.d.ts.map +1 -1
- package/lib/typescript/module/layout/bottom-edge.d.ts.map +1 -1
- package/lib/typescript/module/layout/edge-store.d.ts +31 -0
- package/lib/typescript/module/layout/edge-store.d.ts.map +1 -0
- package/lib/typescript/module/layout/index.d.ts +8 -3
- package/lib/typescript/module/layout/index.d.ts.map +1 -1
- package/lib/typescript/module/layout/scroll-offset.d.ts +16 -0
- package/lib/typescript/module/layout/scroll-offset.d.ts.map +1 -0
- package/lib/typescript/module/layout/top-edge.d.ts +77 -0
- package/lib/typescript/module/layout/top-edge.d.ts.map +1 -0
- package/lib/typescript/module/page-header/EdgeScrim.d.ts +95 -0
- package/lib/typescript/module/page-header/EdgeScrim.d.ts.map +1 -0
- package/lib/typescript/module/page-header/PageHeader.d.ts +1 -1
- package/lib/typescript/module/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/module/page-header/index.d.ts +1 -1
- package/lib/typescript/module/page-header/index.d.ts.map +1 -1
- package/lib/typescript/module/page-header/types.d.ts +97 -10
- package/lib/typescript/module/page-header/types.d.ts.map +1 -1
- package/lib/typescript/module/provider/BloomProvider.d.ts.map +1 -1
- package/lib/typescript/module/styles/svg-id.d.ts +20 -0
- package/lib/typescript/module/styles/svg-id.d.ts.map +1 -0
- package/lib/typescript/module/text-field/TextField.d.ts.map +1 -1
- package/lib/typescript/module/theme/glass-colors.d.ts +67 -0
- package/lib/typescript/module/theme/glass-colors.d.ts.map +1 -1
- package/package.json +15 -1
- package/src/button/Button.tsx +10 -1
- package/src/button-group/ButtonGroup.tsx +146 -19
- package/src/button-group/types.ts +68 -5
- package/src/control-surface/ControlSurface.tsx +41 -0
- package/src/control-surface/context.ts +81 -0
- package/src/control-surface/index.ts +4 -0
- package/src/control-surface/types.ts +40 -0
- package/src/field/Field.tsx +84 -27
- package/src/field/context.ts +110 -0
- package/src/field/index.ts +3 -0
- package/src/field/types.ts +22 -3
- package/src/floating/TriggerSlot.tsx +35 -1
- package/src/glass/GlassIsland.tsx +109 -0
- package/src/glass/GlassSurface.tsx +26 -2
- package/src/glass/index.ts +3 -0
- package/src/glass/types.ts +45 -0
- package/src/index.ts +6 -0
- package/src/index.web.ts +6 -0
- package/src/label/Label.tsx +3 -1
- package/src/label/types.ts +7 -1
- package/src/layout/bottom-edge.tsx +3 -56
- package/src/layout/edge-store.ts +69 -0
- package/src/layout/index.ts +8 -3
- package/src/layout/scroll-offset.ts +76 -0
- package/src/layout/top-edge.tsx +109 -0
- package/src/page-header/EdgeScrim.tsx +118 -0
- package/src/page-header/PageHeader.tsx +253 -61
- package/src/page-header/index.ts +9 -1
- package/src/page-header/types.ts +103 -10
- package/src/provider/BloomProvider.tsx +12 -1
- package/src/social-button/OxyMark.tsx +1 -1
- package/src/social-button/SocialButton.tsx +1 -1
- package/src/styles/svg-id.ts +23 -0
- package/src/text-field/TextField.tsx +44 -5
- package/src/theme/glass-colors.ts +80 -0
- package/lib/commonjs/social-button/use-svg-id.js +0 -16
- package/lib/commonjs/social-button/use-svg-id.js.map +0 -1
- package/lib/module/social-button/use-svg-id.js +0 -13
- package/lib/module/social-button/use-svg-id.js.map +0 -1
- package/lib/typescript/commonjs/social-button/use-svg-id.d.ts +0 -7
- package/lib/typescript/commonjs/social-button/use-svg-id.d.ts.map +0 -1
- package/lib/typescript/module/social-button/use-svg-id.d.ts +0 -7
- package/lib/typescript/module/social-button/use-svg-id.d.ts.map +0 -1
- package/src/social-button/use-svg-id.ts +0 -10
|
@@ -16,32 +16,65 @@ import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
|
|
|
16
16
|
|
|
17
17
|
import { Button } from '../button';
|
|
18
18
|
import { BUTTON_SHADOW, mixColor, resolveButtonRamps } from '../button/shared';
|
|
19
|
+
import { ButtonGroupItem } from '../button-group';
|
|
20
|
+
import { ControlSurface } from '../control-surface';
|
|
21
|
+
import { GlassIsland } from '../glass';
|
|
19
22
|
import { RiArrowLeftLine } from '../icons/remix';
|
|
23
|
+
import { useClaimTopEdge, useScrollOffset } from '../layout';
|
|
20
24
|
import { BREAKPOINTS } from '../styles/breakpoints';
|
|
21
25
|
import { WEB_POSITION_STICKY, type WebCssStyle } from '../styles/web-view-style';
|
|
22
26
|
import { Z_INDEX } from '../styles/z-index';
|
|
23
27
|
import { useTheme } from '../theme/use-theme';
|
|
24
28
|
import { Text } from '../typography';
|
|
29
|
+
import { EdgeScrim, SCRIM_TAIL_RATIO } from './EdgeScrim';
|
|
25
30
|
import type { PageHeaderProps } from './types';
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
|
-
* `PageHeader`: a screen's top
|
|
29
|
-
* node and actions, composed from the vocabulary Bloom's template headers and
|
|
30
|
-
* the settings modal's compact header use:
|
|
33
|
+
* `PageHeader`: a screen's top chrome.
|
|
31
34
|
*
|
|
32
|
-
*
|
|
33
|
-
* back secondary medium icon button (36, pill, shadow-xs), RiArrowLeftLine 20
|
|
34
|
-
* start group gap 6 (template header `gap-1.5`)
|
|
35
|
-
* title headline-medium, text-primary, px 4 (template `px-1`), one line
|
|
36
|
-
* subtitle body-2-regular, text-secondary (neutral-500), one line
|
|
37
|
-
* actions gap 10 (template `gap-2.5`)
|
|
38
|
-
* background background/full — card / dark neutral-925
|
|
39
|
-
* separator 1px separator-border — neutral-200 / dark neutral-800
|
|
40
|
-
* shadow shadow-xs, fading in with scroll
|
|
35
|
+
* ── THE DEFAULT IS FLOATING ─────────────────────────────────────────────────
|
|
41
36
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
37
|
+
* Not a bar. The back button sits in its OWN translucent capsule, the actions
|
|
38
|
+
* the consumer declared as a group share ONE island, the title floats between
|
|
39
|
+
* them, and the whole thing ends in a vertical gradient rather than a hairline.
|
|
40
|
+
* Content passes under it.
|
|
41
|
+
*
|
|
42
|
+
* Three things follow from that, and each is a prop rather than an assumption:
|
|
43
|
+
*
|
|
44
|
+
* `presentation` `floating` | `bar` — islands, or Bloom's flat strip
|
|
45
|
+
* `placement` `inline` | `overlay` — who reserves the space
|
|
46
|
+
* `scrim`/`titleReveal` — the edge effect and the title,
|
|
47
|
+
* separately
|
|
48
|
+
*
|
|
49
|
+
* The last pair used to be one flag. `transparent` faded the background in with
|
|
50
|
+
* scroll AND hid the title, which is right for a header over a hero photo and
|
|
51
|
+
* wrong everywhere else — and a header that inherited it got both behaviours
|
|
52
|
+
* whether or not it wanted either. They are two questions with two answers now.
|
|
53
|
+
*
|
|
54
|
+
* ── THE ISLANDS ARE THE CONSUMER'S GROUPING ─────────────────────────────────
|
|
55
|
+
*
|
|
56
|
+
* The header never infers grouping by reading its children's types. The
|
|
57
|
+
* `actions` slot is wrapped in a `ControlSurface` with `material: 'glass'`, so
|
|
58
|
+
* a `ButtonGroup` placed there becomes one island with no prop written on it,
|
|
59
|
+
* and two `ButtonGroup`s become two islands. That is a declaration the consumer
|
|
60
|
+
* makes, which is the only way the header can be right about which actions
|
|
61
|
+
* belong together.
|
|
62
|
+
*
|
|
63
|
+
* floating bar
|
|
64
|
+
* chrome islands + gradient opaque strip + hairline + shadow
|
|
65
|
+
* back 36 capsule, glass secondary medium icon `Button`
|
|
66
|
+
* title headline-medium, no capsule headline-medium
|
|
67
|
+
* scroll drives the scrim drives border, background, shadow
|
|
68
|
+
*
|
|
69
|
+
* The geometry both share:
|
|
70
|
+
*
|
|
71
|
+
* bar min-height 56, side inset 16 (below `sm`) / 24
|
|
72
|
+
* islands 36 tall (34 + hairline), 8 apart
|
|
73
|
+
* title headline-medium, text-primary, one line
|
|
74
|
+
* subtitle body-2-regular, secondary, one line
|
|
75
|
+
*
|
|
76
|
+
* Scroll-linked, not timed: everything interpolates over `[0, scrollThreshold]`
|
|
77
|
+
* of the resolved scroll offset. No press scale.
|
|
45
78
|
*/
|
|
46
79
|
|
|
47
80
|
const BAR_HEIGHT = 56;
|
|
@@ -50,9 +83,26 @@ const INSET_WIDE = 24;
|
|
|
50
83
|
const START_GAP = 6;
|
|
51
84
|
const ROW_GAP = 8;
|
|
52
85
|
const ACTIONS_GAP = 10;
|
|
86
|
+
const FLOATING_ACTIONS_GAP = 8;
|
|
53
87
|
const TITLE_PADDING = 4;
|
|
54
88
|
const DEFAULT_THRESHOLD = 20;
|
|
55
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The narrowest the centred title may be squeezed to before it stops yielding
|
|
92
|
+
* to the two sides.
|
|
93
|
+
*
|
|
94
|
+
* Without a floor the centring inset is `max(startWidth, endWidth)` applied to
|
|
95
|
+
* BOTH sides, so on a narrow container two wide sides can ask for more than the
|
|
96
|
+
* container has — and `left + right > width` is a box of negative width, which
|
|
97
|
+
* on web collapses the title to nothing and on native logs a layout error. The
|
|
98
|
+
* title stops centring before that happens and simply sits off-centre, which is
|
|
99
|
+
* the lesser of the two wrongs and the one a reader can still use.
|
|
100
|
+
*/
|
|
101
|
+
const MIN_CENTERED_TITLE_WIDTH = 96;
|
|
102
|
+
|
|
103
|
+
/** 44pt touch target from a 36pt capsule. Only for an island that stands alone. */
|
|
104
|
+
const CAPSULE_HIT_SLOP = { top: 4, bottom: 4, left: 4, right: 4 } as const;
|
|
105
|
+
|
|
56
106
|
const NO_INSETS = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
57
107
|
|
|
58
108
|
function PageHeaderComponent({
|
|
@@ -60,12 +110,17 @@ function PageHeaderComponent({
|
|
|
60
110
|
subtitle,
|
|
61
111
|
titleAlign = 'start',
|
|
62
112
|
headingLevel = 1,
|
|
113
|
+
titleReveal = 'always',
|
|
114
|
+
presentation = 'floating',
|
|
115
|
+
placement = 'inline',
|
|
63
116
|
onBack,
|
|
64
117
|
backLabel = 'Back',
|
|
65
118
|
leading,
|
|
66
119
|
actions,
|
|
67
120
|
border = 'auto',
|
|
68
121
|
transparent = false,
|
|
122
|
+
scrim = 'auto',
|
|
123
|
+
scrimColor,
|
|
69
124
|
scrollY: externalScrollY,
|
|
70
125
|
scrollThreshold = DEFAULT_THRESHOLD,
|
|
71
126
|
sticky = true,
|
|
@@ -74,12 +129,34 @@ function PageHeaderComponent({
|
|
|
74
129
|
testID,
|
|
75
130
|
}: PageHeaderProps) {
|
|
76
131
|
const isWeb = Platform.OS === 'web';
|
|
132
|
+
const floating = presentation === 'floating';
|
|
133
|
+
const overlay = placement === 'overlay';
|
|
77
134
|
const theme = useTheme();
|
|
78
|
-
const { width } = useWindowDimensions();
|
|
79
135
|
const insets = useContext(SafeAreaInsetsContext) ?? NO_INSETS;
|
|
80
136
|
const padTop = (safeArea ?? !isWeb) ? insets.top : 0;
|
|
137
|
+
|
|
138
|
+
// The header sizes itself against its CONTAINER, not the window: a desktop
|
|
139
|
+
// split view puts a 380px panel next to a 1200px one, and a window-derived
|
|
140
|
+
// inset gives the narrow panel the wide padding. The window is only the seed
|
|
141
|
+
// for the first frame, before `onLayout` has reported.
|
|
142
|
+
const windowWidth = useWindowDimensions().width;
|
|
143
|
+
const [measuredWidth, setMeasuredWidth] = useState(0);
|
|
144
|
+
const [measuredHeight, setMeasuredHeight] = useState(0);
|
|
145
|
+
const width = measuredWidth || windowWidth;
|
|
146
|
+
const height = measuredHeight || padTop + BAR_HEIGHT;
|
|
81
147
|
const sideInset = width >= BREAKPOINTS.sm ? INSET_WIDE : INSET_COMPACT;
|
|
82
148
|
|
|
149
|
+
const onContainerLayout = (e: LayoutChangeEvent) => {
|
|
150
|
+
setMeasuredWidth(e.nativeEvent.layout.width);
|
|
151
|
+
setMeasuredHeight(e.nativeEvent.layout.height);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// An overlaying header occupies no layout, so it declares what it covers and
|
|
155
|
+
// the content reads it back. Measured rather than computed: a second title
|
|
156
|
+
// line, an enlarged font or a notch all move it, and every one of those is a
|
|
157
|
+
// case a constant was written to survive and does not.
|
|
158
|
+
useClaimTopEdge(overlay ? height : 0);
|
|
159
|
+
|
|
83
160
|
const paint = useMemo(() => {
|
|
84
161
|
const { neutral } = resolveButtonRamps(theme);
|
|
85
162
|
return theme.isDark
|
|
@@ -97,19 +174,26 @@ function PageHeaderComponent({
|
|
|
97
174
|
};
|
|
98
175
|
}, [theme]);
|
|
99
176
|
|
|
177
|
+
// ── The scroll owner ──────────────────────────────────────────────────────
|
|
178
|
+
//
|
|
179
|
+
// Prop, then the nearest `ScrollOffsetProvider`, then the document on web.
|
|
180
|
+
// The context step is the one that matters for a Bloom composition: without
|
|
181
|
+
// it a header inside a scrolling panel followed `window.scrollY`, which on a
|
|
182
|
+
// desktop shell never moves, so the header looked deliberately inert.
|
|
183
|
+
const contextScrollY = useScrollOffset();
|
|
100
184
|
const internalScrollY = useSharedValue(0);
|
|
101
|
-
const scrollY = externalScrollY ?? internalScrollY;
|
|
185
|
+
const scrollY = externalScrollY ?? contextScrollY ?? internalScrollY;
|
|
186
|
+
const followsWindow = !externalScrollY && !contextScrollY;
|
|
102
187
|
|
|
103
|
-
// Web fallback: the document is the scroll owner, as in the fleet headers.
|
|
104
188
|
useEffect(() => {
|
|
105
|
-
if (!isWeb ||
|
|
189
|
+
if (!isWeb || !followsWindow || typeof window === 'undefined') return undefined;
|
|
106
190
|
const onScroll = () => {
|
|
107
191
|
internalScrollY.value = window.scrollY;
|
|
108
192
|
};
|
|
109
193
|
onScroll();
|
|
110
194
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
111
195
|
return () => window.removeEventListener('scroll', onScroll);
|
|
112
|
-
}, [isWeb,
|
|
196
|
+
}, [isWeb, followsWindow, internalScrollY]);
|
|
113
197
|
|
|
114
198
|
const threshold = Math.max(1, scrollThreshold);
|
|
115
199
|
|
|
@@ -123,12 +207,23 @@ function PageHeaderComponent({
|
|
|
123
207
|
: 1,
|
|
124
208
|
}), [scrollY, threshold, transparent]);
|
|
125
209
|
|
|
126
|
-
|
|
210
|
+
const scrimStyle = useAnimatedStyle(() => ({
|
|
211
|
+
opacity:
|
|
212
|
+
scrim === 'always'
|
|
213
|
+
? 1
|
|
214
|
+
: scrim === 'none'
|
|
215
|
+
? 0
|
|
216
|
+
: interpolate(scrollY.value, [0, threshold], [0, 1], Extrapolation.CLAMP),
|
|
217
|
+
}), [scrollY, threshold, scrim]);
|
|
218
|
+
|
|
219
|
+
// Separate from the background's: a header can be transparent and still name
|
|
220
|
+
// its screen, and one over a hero photo can do the opposite.
|
|
127
221
|
const titleStyle = useAnimatedStyle(() => ({
|
|
128
|
-
opacity:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
222
|
+
opacity:
|
|
223
|
+
titleReveal === 'always'
|
|
224
|
+
? 1
|
|
225
|
+
: interpolate(scrollY.value, [0, threshold], [0, 1], Extrapolation.CLAMP),
|
|
226
|
+
}), [scrollY, threshold, titleReveal]);
|
|
132
227
|
|
|
133
228
|
const borderStyle = useAnimatedStyle(() => ({
|
|
134
229
|
opacity:
|
|
@@ -139,14 +234,25 @@ function PageHeaderComponent({
|
|
|
139
234
|
: interpolate(scrollY.value, [0, threshold], [0, 1], Extrapolation.CLAMP),
|
|
140
235
|
}), [scrollY, threshold, border]);
|
|
141
236
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
237
|
+
// ── Centring ──────────────────────────────────────────────────────────────
|
|
238
|
+
//
|
|
239
|
+
// Inset the title by the WIDER side so it sits on the container's centre
|
|
240
|
+
// whatever the two sides hold. A slot that is not rendered contributes 0
|
|
241
|
+
// rather than its last measurement: `onLayout` does not fire on unmount, so a
|
|
242
|
+
// header that loses its actions would otherwise keep centring around them.
|
|
243
|
+
const [startMeasure, setStartMeasure] = useState(0);
|
|
244
|
+
const [endMeasure, setEndMeasure] = useState(0);
|
|
146
245
|
const centered = titleAlign === 'center';
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
246
|
+
const hasStart = onBack != null || leading != null;
|
|
247
|
+
const hasEnd = actions != null;
|
|
248
|
+
const startWidth = hasStart ? startMeasure : 0;
|
|
249
|
+
const endWidth = hasEnd ? endMeasure : 0;
|
|
250
|
+
const onStartLayout = (e: LayoutChangeEvent) => setStartMeasure(e.nativeEvent.layout.width);
|
|
251
|
+
const onEndLayout = (e: LayoutChangeEvent) => setEndMeasure(e.nativeEvent.layout.width);
|
|
252
|
+
const centerInset = Math.min(
|
|
253
|
+
Math.max(startWidth, endWidth) + ROW_GAP,
|
|
254
|
+
Math.max(0, (width - 2 * sideInset - MIN_CENTERED_TITLE_WIDTH) / 2),
|
|
255
|
+
);
|
|
150
256
|
|
|
151
257
|
const titleBlock =
|
|
152
258
|
title != null || subtitle != null ? (
|
|
@@ -187,14 +293,61 @@ function PageHeaderComponent({
|
|
|
187
293
|
</Animated.View>
|
|
188
294
|
) : null;
|
|
189
295
|
|
|
296
|
+
const backButton = onBack ? (
|
|
297
|
+
floating ? (
|
|
298
|
+
// Its own island: a back action is not a member of the page's action
|
|
299
|
+
// group, and a `role="group"` of one is noise a screen reader reads out.
|
|
300
|
+
// The control inside is a `ButtonGroupItem` because that is Bloom's
|
|
301
|
+
// island-aware control — it reads the material `GlassIsland` publishes
|
|
302
|
+
// and paints flush — rather than a second implementation of the same
|
|
303
|
+
// hover, press, focus and disabled behaviour.
|
|
304
|
+
<GlassIsland testID={testID ? `${testID}-back-island` : undefined}>
|
|
305
|
+
<ButtonGroupItem
|
|
306
|
+
iconOnly
|
|
307
|
+
leadingIcon={RiArrowLeftLine}
|
|
308
|
+
accessibilityLabel={backLabel}
|
|
309
|
+
onPress={onBack}
|
|
310
|
+
hitSlop={CAPSULE_HIT_SLOP}
|
|
311
|
+
testID={testID ? `${testID}-back` : undefined}
|
|
312
|
+
/>
|
|
313
|
+
</GlassIsland>
|
|
314
|
+
) : (
|
|
315
|
+
<Button
|
|
316
|
+
variant="secondary"
|
|
317
|
+
size="medium"
|
|
318
|
+
iconOnly
|
|
319
|
+
leadingIcon={RiArrowLeftLine}
|
|
320
|
+
accessibilityLabel={backLabel}
|
|
321
|
+
onPress={onBack}
|
|
322
|
+
testID={testID ? `${testID}-back` : undefined}
|
|
323
|
+
/>
|
|
324
|
+
)
|
|
325
|
+
) : null;
|
|
326
|
+
|
|
190
327
|
const containerWeb: WebCssStyle | null =
|
|
191
|
-
isWeb && sticky
|
|
328
|
+
isWeb && sticky && !overlay
|
|
329
|
+
? { position: WEB_POSITION_STICKY, top: 0, zIndex: Z_INDEX.floating }
|
|
330
|
+
: null;
|
|
192
331
|
|
|
193
|
-
|
|
194
|
-
<View
|
|
195
|
-
|
|
196
|
-
|
|
332
|
+
const chrome = floating ? (
|
|
333
|
+
<Animated.View
|
|
334
|
+
// A PROP: react-native-web resolves `none` from the prop path only, and
|
|
335
|
+
// the scrim must never take a press meant for the content under it.
|
|
336
|
+
pointerEvents="none"
|
|
337
|
+
style={[styles.scrim, { height: height * (1 + SCRIM_TAIL_RATIO) }, scrimStyle]}
|
|
338
|
+
testID={testID ? `${testID}-scrim` : undefined}
|
|
197
339
|
>
|
|
340
|
+
<EdgeScrim
|
|
341
|
+
// The PAGE colour by default: the scrim's job is to fade content into
|
|
342
|
+
// the surface it is leaving, so the right colour is the one that
|
|
343
|
+
// surface already is. A screen that paints its own background has to
|
|
344
|
+
// say so — the header cannot read the pixel behind it.
|
|
345
|
+
color={scrimColor ?? theme.colors.background}
|
|
346
|
+
testID={testID ? `${testID}-scrim-gradient` : undefined}
|
|
347
|
+
/>
|
|
348
|
+
</Animated.View>
|
|
349
|
+
) : (
|
|
350
|
+
<>
|
|
198
351
|
<Animated.View
|
|
199
352
|
pointerEvents="none"
|
|
200
353
|
style={[StyleSheet.absoluteFill, { boxShadow: paint.shadow }, shadowStyle]}
|
|
@@ -210,43 +363,71 @@ function PageHeaderComponent({
|
|
|
210
363
|
style={[styles.border, { backgroundColor: paint.separator }, borderStyle]}
|
|
211
364
|
testID={testID ? `${testID}-border` : undefined}
|
|
212
365
|
/>
|
|
366
|
+
</>
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const actionsSlot = actions ? (
|
|
370
|
+
<View
|
|
371
|
+
style={[styles.actions, { gap: floating ? FLOATING_ACTIONS_GAP : ACTIONS_GAP }]}
|
|
372
|
+
onLayout={centered ? onEndLayout : undefined}
|
|
373
|
+
// `box-none` so the gaps BETWEEN islands stay transparent to touch. A
|
|
374
|
+
// floating header covers content it does not own, and an invisible
|
|
375
|
+
// rectangle that swallows presses across the width of the screen is the
|
|
376
|
+
// defect this presentation could most easily ship.
|
|
377
|
+
//
|
|
378
|
+
// FLOATING ONLY, and the asymmetry is the point: a `bar` is an OPAQUE
|
|
379
|
+
// strip, and letting presses through the part of it that draws no control
|
|
380
|
+
// would hand a click on solid chrome to whatever list row is scrolled
|
|
381
|
+
// underneath.
|
|
382
|
+
pointerEvents={floating ? 'box-none' : undefined}
|
|
383
|
+
testID={testID ? `${testID}-actions` : undefined}
|
|
384
|
+
>
|
|
385
|
+
{floating ? <ControlSurface material="glass">{actions}</ControlSurface> : actions}
|
|
386
|
+
</View>
|
|
387
|
+
) : null;
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<View
|
|
391
|
+
testID={testID}
|
|
392
|
+
onLayout={onContainerLayout}
|
|
393
|
+
pointerEvents={overlay ? 'box-none' : undefined}
|
|
394
|
+
style={[
|
|
395
|
+
styles.container,
|
|
396
|
+
{ paddingTop: padTop },
|
|
397
|
+
overlay ? [styles.overlay, { zIndex: Z_INDEX.floating }] : null,
|
|
398
|
+
containerWeb,
|
|
399
|
+
style,
|
|
400
|
+
]}
|
|
401
|
+
>
|
|
402
|
+
{chrome}
|
|
213
403
|
<View
|
|
214
404
|
style={[styles.bar, { paddingLeft: sideInset, paddingRight: sideInset }]}
|
|
405
|
+
pointerEvents={floating ? 'box-none' : undefined}
|
|
215
406
|
testID={testID ? `${testID}-bar` : undefined}
|
|
216
407
|
>
|
|
217
408
|
<View
|
|
218
409
|
style={[styles.start, !centered && styles.startFill]}
|
|
219
410
|
onLayout={centered ? onStartLayout : undefined}
|
|
411
|
+
pointerEvents={floating ? 'box-none' : undefined}
|
|
220
412
|
testID={testID ? `${testID}-start` : undefined}
|
|
221
413
|
>
|
|
222
|
-
{
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
accessibilityLabel={backLabel}
|
|
229
|
-
onPress={onBack}
|
|
230
|
-
testID={testID ? `${testID}-back` : undefined}
|
|
231
|
-
/>
|
|
232
|
-
) : null}
|
|
233
|
-
{leading}
|
|
414
|
+
{backButton}
|
|
415
|
+
{floating && leading ? (
|
|
416
|
+
<ControlSurface material="glass">{leading}</ControlSurface>
|
|
417
|
+
) : (
|
|
418
|
+
leading
|
|
419
|
+
)}
|
|
234
420
|
{centered ? null : titleBlock}
|
|
235
421
|
</View>
|
|
236
|
-
{centered ? <View style={styles.spacer} /> : null}
|
|
237
|
-
{
|
|
238
|
-
<View
|
|
239
|
-
style={styles.actions}
|
|
240
|
-
onLayout={centered ? onEndLayout : undefined}
|
|
241
|
-
testID={testID ? `${testID}-actions` : undefined}
|
|
242
|
-
>
|
|
243
|
-
{actions}
|
|
244
|
-
</View>
|
|
245
|
-
) : null}
|
|
422
|
+
{centered ? <View style={styles.spacer} pointerEvents="none" /> : null}
|
|
423
|
+
{actionsSlot}
|
|
246
424
|
{centered && titleBlock ? (
|
|
247
425
|
<View
|
|
248
426
|
pointerEvents="box-none"
|
|
249
|
-
style={[
|
|
427
|
+
style={[
|
|
428
|
+
styles.centerSlot,
|
|
429
|
+
{ left: sideInset + centerInset, right: sideInset + centerInset },
|
|
430
|
+
]}
|
|
250
431
|
testID={testID ? `${testID}-center` : undefined}
|
|
251
432
|
>
|
|
252
433
|
{titleBlock}
|
|
@@ -262,6 +443,18 @@ const styles = StyleSheet.create({
|
|
|
262
443
|
width: '100%',
|
|
263
444
|
position: 'relative',
|
|
264
445
|
},
|
|
446
|
+
overlay: {
|
|
447
|
+
position: 'absolute',
|
|
448
|
+
top: 0,
|
|
449
|
+
left: 0,
|
|
450
|
+
right: 0,
|
|
451
|
+
},
|
|
452
|
+
scrim: {
|
|
453
|
+
position: 'absolute',
|
|
454
|
+
top: 0,
|
|
455
|
+
left: 0,
|
|
456
|
+
right: 0,
|
|
457
|
+
},
|
|
265
458
|
border: {
|
|
266
459
|
position: 'absolute',
|
|
267
460
|
left: 0,
|
|
@@ -310,7 +503,6 @@ const styles = StyleSheet.create({
|
|
|
310
503
|
flexDirection: 'row',
|
|
311
504
|
alignItems: 'center',
|
|
312
505
|
justifyContent: 'flex-end',
|
|
313
|
-
gap: ACTIONS_GAP,
|
|
314
506
|
flexShrink: 0,
|
|
315
507
|
},
|
|
316
508
|
});
|
package/src/page-header/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
export { PageHeader } from './PageHeader';
|
|
2
2
|
|
|
3
|
-
export type {
|
|
3
|
+
export type {
|
|
4
|
+
PageHeaderBorder,
|
|
5
|
+
PageHeaderPlacement,
|
|
6
|
+
PageHeaderPresentation,
|
|
7
|
+
PageHeaderProps,
|
|
8
|
+
PageHeaderScrim,
|
|
9
|
+
PageHeaderTitleAlign,
|
|
10
|
+
PageHeaderTitleReveal,
|
|
11
|
+
} from './types';
|
package/src/page-header/types.ts
CHANGED
|
@@ -5,12 +5,69 @@ import type { SharedValue } from 'react-native-reanimated';
|
|
|
5
5
|
/** Where the title sits: after the back button, or centred on the bar. */
|
|
6
6
|
export type PageHeaderTitleAlign = 'start' | 'center';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* How the header presents itself.
|
|
10
|
+
*
|
|
11
|
+
* `floating` is the default: no bar at all. The back button sits in its own
|
|
12
|
+
* translucent capsule, related actions share one island, the title floats
|
|
13
|
+
* between them, and the whole thing ends in a soft vertical gradient instead of
|
|
14
|
+
* a horizontal edge. Content passes UNDER it.
|
|
15
|
+
*
|
|
16
|
+
* `bar` is Bloom's original flat header — an opaque full-width strip with a
|
|
17
|
+
* hairline separator and a scroll-linked shadow. It is still the right shape
|
|
18
|
+
* for a dense tool screen, a split-pane sidebar header or anywhere the chrome
|
|
19
|
+
* must not be mistaken for content; it is no longer the default.
|
|
20
|
+
*/
|
|
21
|
+
export type PageHeaderPresentation = 'floating' | 'bar';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Who reserves the space the header occupies.
|
|
25
|
+
*
|
|
26
|
+
* `inline` (default): the header is a sibling ABOVE the content, in a column.
|
|
27
|
+
* It occupies layout, so nothing passes under it and nothing needs padding.
|
|
28
|
+
*
|
|
29
|
+
* `overlay`: the header is absolutely positioned over the content, which starts
|
|
30
|
+
* at the top of the container and scrolls under the islands. The header then
|
|
31
|
+
* CLAIMS its measured height on `layout/top-edge`, and the content reads it
|
|
32
|
+
* with `useTopEdgeInset()` so its first item is not born underneath. This is
|
|
33
|
+
* the placement the floating presentation was designed for; the two are
|
|
34
|
+
* separate props because a floating header above a fixed panel is also a real
|
|
35
|
+
* arrangement.
|
|
36
|
+
*/
|
|
37
|
+
export type PageHeaderPlacement = 'inline' | 'overlay';
|
|
38
|
+
|
|
8
39
|
/**
|
|
9
40
|
* The bottom separator: `auto` fades it in once the page has scrolled past
|
|
10
41
|
* `scrollThreshold`, `always` draws it at rest, `none` never draws it.
|
|
42
|
+
*
|
|
43
|
+
* `presentation="bar"` only. A floating header has no edge to separate.
|
|
11
44
|
*/
|
|
12
45
|
export type PageHeaderBorder = 'auto' | 'always' | 'none';
|
|
13
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The floating header's edge effect: a vertical gradient from the page colour
|
|
49
|
+
* to full transparency, behind the title and the islands.
|
|
50
|
+
*
|
|
51
|
+
* `auto` (default) fades it in over `[0, scrollThreshold]` of the scroll
|
|
52
|
+
* offset, so it appears exactly when content starts passing under it. `always`
|
|
53
|
+
* paints it at rest — for a header that is born over a photograph. `none`
|
|
54
|
+
* removes it, which leaves the islands to carry legibility on their own.
|
|
55
|
+
*/
|
|
56
|
+
export type PageHeaderScrim = 'auto' | 'always' | 'none';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* When the title is visible.
|
|
60
|
+
*
|
|
61
|
+
* `always` (default) is the ordinary screen header. `onScroll` holds the title
|
|
62
|
+
* back and fades it in with the scrim — the shape a header over a hero photo
|
|
63
|
+
* wants, where the photo already names the screen.
|
|
64
|
+
*
|
|
65
|
+
* It is a prop of its own rather than a side effect of the background, because
|
|
66
|
+
* the two are different questions and conflating them is how every header that
|
|
67
|
+
* turned transparent also lost its title.
|
|
68
|
+
*/
|
|
69
|
+
export type PageHeaderTitleReveal = 'always' | 'onScroll';
|
|
70
|
+
|
|
14
71
|
export interface PageHeaderProps {
|
|
15
72
|
/** A string renders as the heading; a node renders as-is (and owns its own semantics). */
|
|
16
73
|
title?: ReactNode;
|
|
@@ -20,6 +77,13 @@ export interface PageHeaderProps {
|
|
|
20
77
|
titleAlign?: PageHeaderTitleAlign;
|
|
21
78
|
/** The title's heading level. Default 1. */
|
|
22
79
|
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
80
|
+
/** Default `always`. */
|
|
81
|
+
titleReveal?: PageHeaderTitleReveal;
|
|
82
|
+
|
|
83
|
+
/** Default `floating`. */
|
|
84
|
+
presentation?: PageHeaderPresentation;
|
|
85
|
+
/** Default `inline`. */
|
|
86
|
+
placement?: PageHeaderPlacement;
|
|
23
87
|
|
|
24
88
|
/**
|
|
25
89
|
* Renders the back button and is called when it is pressed. The header is
|
|
@@ -31,25 +95,54 @@ export interface PageHeaderProps {
|
|
|
31
95
|
|
|
32
96
|
/** Rendered after the back button, before the title (an avatar, a logo). */
|
|
33
97
|
leading?: ReactNode;
|
|
34
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Rendered at the end of the bar. In the floating presentation this slot is
|
|
100
|
+
* wrapped in a `ControlSurface` with `material: 'glass'`, so a `ButtonGroup`
|
|
101
|
+
* inside it becomes one island with no prop written on it. Declare the
|
|
102
|
+
* grouping yourself — the header never infers it by reading children.
|
|
103
|
+
*/
|
|
35
104
|
actions?: ReactNode;
|
|
36
105
|
|
|
37
|
-
/** Default `auto`. */
|
|
106
|
+
/** `presentation="bar"` only. Default `auto`. */
|
|
38
107
|
border?: PageHeaderBorder;
|
|
39
108
|
/**
|
|
40
|
-
*
|
|
41
|
-
* showing at rest
|
|
42
|
-
*
|
|
109
|
+
* `presentation="bar"` only: the bar's background and shadow fade in with
|
|
110
|
+
* scroll instead of showing at rest.
|
|
111
|
+
*
|
|
112
|
+
* It no longer hides the title — that is `titleReveal="onScroll"`, and the
|
|
113
|
+
* two are set independently. A `bar` header that wants the old combined
|
|
114
|
+
* behaviour asks for both.
|
|
43
115
|
*/
|
|
44
116
|
transparent?: boolean;
|
|
117
|
+
/** `presentation="floating"` only. Default `auto`. */
|
|
118
|
+
scrim?: PageHeaderScrim;
|
|
119
|
+
/**
|
|
120
|
+
* The colour the edge effect fades FROM. Defaults to `theme.colors.background`
|
|
121
|
+
* — the page colour, which is what the content scrolling under the header is
|
|
122
|
+
* disappearing into.
|
|
123
|
+
*
|
|
124
|
+
* Pass it when the screen paints a background the theme does not know about:
|
|
125
|
+
* a tinted section, a brand panel, a dark surface inside a light app. The
|
|
126
|
+
* header cannot read the colour behind it, and a scrim in the theme's
|
|
127
|
+
* background over a page in some other colour is a wash of the wrong hue
|
|
128
|
+
* rather than a fade.
|
|
129
|
+
*
|
|
130
|
+
* It must be OPAQUE. The alpha is the ramp's, and a colour that brings its
|
|
131
|
+
* own would multiply with it — `rgba(0,0,0,0.5)` at the ramp's 0.86 paints
|
|
132
|
+
* 0.43 and reads as a weak, dirty veil rather than the colour you asked for.
|
|
133
|
+
*/
|
|
134
|
+
scrimColor?: string;
|
|
135
|
+
|
|
45
136
|
/**
|
|
46
|
-
* The scroll offset driving
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
137
|
+
* The scroll offset driving the scrim, the separator and the shadow.
|
|
138
|
+
*
|
|
139
|
+
* Resolution order: this prop, then the nearest `ScrollOffsetProvider`
|
|
140
|
+
* (`@oxy.so/bloom/layout`) — which is how a header inside a Bloom scroll
|
|
141
|
+
* composition gets it with no wiring — then, on web only, `window.scrollY`.
|
|
142
|
+
* On native with none of the three, the header stays at rest.
|
|
50
143
|
*/
|
|
51
144
|
scrollY?: SharedValue<number>;
|
|
52
|
-
/** The offset (px) over which
|
|
145
|
+
/** The offset (px) over which the scrim, separator and shadow fade in. Default 20. */
|
|
53
146
|
scrollThreshold?: number;
|
|
54
147
|
|
|
55
148
|
/** Web only: pin the bar to the top of its scroll container. Default true. */
|
|
@@ -35,6 +35,7 @@ import type { ReactNode } from 'react';
|
|
|
35
35
|
|
|
36
36
|
import { GlassBlurTargetProvider } from '../glass/blur-target';
|
|
37
37
|
import { BottomEdgeProvider } from '../layout/bottom-edge';
|
|
38
|
+
import { TopEdgeProvider } from '../layout/top-edge';
|
|
38
39
|
import { ImageResolverProvider, type ImageResolver } from '../image-resolver';
|
|
39
40
|
import { BloomHapticsProvider } from '../hooks/use-haptics';
|
|
40
41
|
import { TabBarMinimizeProvider } from '../tab-bar/context';
|
|
@@ -76,7 +77,17 @@ export function BloomProvider({
|
|
|
76
77
|
the other.
|
|
77
78
|
*/}
|
|
78
79
|
<BottomEdgeProvider>
|
|
79
|
-
|
|
80
|
+
{/*
|
|
81
|
+
And the top edge's, for the same reason one edge further up: a
|
|
82
|
+
floating `PageHeader` overlays the content rather than sitting
|
|
83
|
+
above it, so the content has to be told how much of the edge is
|
|
84
|
+
gone. Two registries rather than one keyed by edge, because a
|
|
85
|
+
reader of either only ever wants one number and must not
|
|
86
|
+
re-render when the other moves.
|
|
87
|
+
*/}
|
|
88
|
+
<TopEdgeProvider>
|
|
89
|
+
<TabBarMinimizeProvider>{children}</TabBarMinimizeProvider>
|
|
90
|
+
</TopEdgeProvider>
|
|
80
91
|
</BottomEdgeProvider>
|
|
81
92
|
</GlassBlurTargetProvider>
|
|
82
93
|
</BloomHapticsProvider>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import Svg, { ClipPath, Defs, G, Path } from 'react-native-svg';
|
|
3
3
|
|
|
4
|
-
import { useSvgIdPrefix } from '
|
|
4
|
+
import { useSvgIdPrefix } from '../styles/svg-id';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The Oxy mark for `SocialButton` — the same drawing the Oxy SDK ships.
|
|
@@ -20,7 +20,7 @@ import { StyledPressable } from '../styles/styled-primitives';
|
|
|
20
20
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
21
21
|
import { SOCIAL_COLOR_LOGOS } from './color-logos';
|
|
22
22
|
import { OxyMark } from './OxyMark';
|
|
23
|
-
import { useSvgIdPrefix } from '
|
|
23
|
+
import { useSvgIdPrefix } from '../styles/svg-id';
|
|
24
24
|
import { SOCIAL_PROVIDERS, type SocialColorLogo, type SocialProvider } from './providers';
|
|
25
25
|
import type {
|
|
26
26
|
SocialBrand,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A DOM-safe id prefix, per component instance.
|
|
5
|
+
*
|
|
6
|
+
* Two SVGs of one shape on a page must not resolve each other's `url(#…)`, and
|
|
7
|
+
* React's `useId` carries characters an SVG id cannot (`:r0:`). Stripping them
|
|
8
|
+
* keeps the id UNIQUE — `useId` is unique per instance before the strip and the
|
|
9
|
+
* strip is injective over React's own alphabet — while staying a legal XML name.
|
|
10
|
+
*
|
|
11
|
+
* It is `useId` rather than a module-scope counter for one reason a counter
|
|
12
|
+
* cannot give: SSR. A counter increments in a different order on the server than
|
|
13
|
+
* during hydration, so the markup React sends and the markup it re-renders
|
|
14
|
+
* reference different gradients and one of them is not there. `useId` is the
|
|
15
|
+
* hook React added for exactly this and is stable across the two passes.
|
|
16
|
+
*
|
|
17
|
+
* Lives in `styles/` rather than beside its first caller because it now has
|
|
18
|
+
* three (`social-button`'s mark and button, `page-header`'s edge gradient) —
|
|
19
|
+
* the second real need is the bar for extracting it, and this cleared it.
|
|
20
|
+
*/
|
|
21
|
+
export function useSvgIdPrefix(prefix: string): string {
|
|
22
|
+
return `${prefix}-${useId().replace(/[^a-zA-Z0-9]/g, '')}-`;
|
|
23
|
+
}
|